Decompiled source of GunGame Progressions v1.4.1
GunGameProgressionsMetadataExporter.dll
Decompiled 2 months ago
The result has been truncated due to the large size, download it to view full contents!
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.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using FistVR; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyCompany("GunGameProgressionsMetadataExporter")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.4.1.0")] [assembly: AssemblyInformationalVersion("1.4.1")] [assembly: AssemblyProduct("GunGameProgressionsMetadataExporter")] [assembly: AssemblyTitle("GunGameProgressionsMetadataExporter")] [assembly: AssemblyVersion("1.4.1.0")] [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 HLin.GunGameProgressions { public static class AtlasMenuSceneResolver { public static object GetSceneInfo(object menuScreen) { return ReadMember(ReadMember(menuScreen, "m_def"), "CustomSceneInfo"); } public static bool IsGunGameSelection(object menuScreen) { return GunGameSceneIdentity.IsMatch(ReadMember(GetSceneInfo(menuScreen), "Identifier") as string); } private static object ReadMember(object instance, string memberName) { if (instance == null) { return null; } Type type = instance.GetType(); FieldInfo field = type.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if ((object)field != null) { return field.GetValue(instance); } return type.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance, null); } } public sealed class EnemySpawnWeight { public int Value { get; private set; } public int Multiplicity { get; private set; } public EnemySpawnWeight(int value, int multiplicity) { Value = value; Multiplicity = multiplicity; } } public static class EnemyWeightPolicy { private enum OperatorTier { None, Standard, Advanced, Apex } public static EnemySpawnWeight Resolve(RuntimeEnemyEntry enemy) { if (enemy == null) { throw new ArgumentNullException("enemy"); } int value = EnemyValue(enemy); return new EnemySpawnWeight(value, SpawnMultiplicity(enemy, value)); } private static int EnemyValue(RuntimeEnemyEntry enemy) { switch (GetOperatorTier(enemy.EnemyNameString)) { case OperatorTier.Standard: return 2; case OperatorTier.Advanced: case OperatorTier.Apex: return 1; default: switch (enemy.EnemyNameString) { case "RW_Rot": return 8; case "M_Swat_Scout": return 5; case "M_MercWiener_Riflewiener": return 3; case "M_Swat_SpecOps": return 2; case "M_Swat_Heavy": return 1; default: if (!IsCoreEnemyFamily(enemy.EnemyNameString)) { return OtherSpawnWeight(enemy.DifficultyScore); } return CoreSpawnWeight(enemy.DifficultyScore); } } } private static int CoreSpawnWeight(int score) { if (score <= 15) { return 8; } if (score <= 40) { return 5; } if (score <= 65) { return 3; } if (score <= 100) { return 2; } return 1; } private static int OtherSpawnWeight(int score) { if (score > 40) { return 1; } return 2; } private static int SpawnMultiplicity(RuntimeEnemyEntry enemy, int value) { switch (GetOperatorTier(enemy.EnemyNameString)) { case OperatorTier.Apex: return 1; case OperatorTier.Standard: case OperatorTier.Advanced: return 2; default: if (!IsCoreEnemyFamily(enemy.EnemyNameString)) { return 1; } return value switch { 8 => 13, 5 => 8, 3 => 6, 2 => 4, _ => 2, }; } } private static bool IsCoreEnemyFamily(string enemyNameString) { if (!enemyNameString.StartsWith("RW_", StringComparison.Ordinal) && !enemyNameString.StartsWith("M_Swat_", StringComparison.Ordinal) && !enemyNameString.StartsWith("M_MercWiener_", StringComparison.Ordinal)) { return enemyNameString.StartsWith("Comperator_", StringComparison.Ordinal); } return true; } private static OperatorTier GetOperatorTier(string enemyNameString) { if (string.IsNullOrEmpty(enemyNameString) || !enemyNameString.StartsWith("Comperator_", StringComparison.OrdinalIgnoreCase)) { return OperatorTier.None; } if (ContainsToken(enemyNameString, "Heavy_") || ContainsToken(enemyNameString, "Tier5") || ContainsToken(enemyNameString, "MixedHighTier")) { return OperatorTier.Apex; } if (ContainsToken(enemyNameString, "Tier4") || ContainsToken(enemyNameString, "MixedMedTier") || ContainsToken(enemyNameString, "Medium_Tier3")) { return OperatorTier.Advanced; } return OperatorTier.Standard; } private static bool ContainsToken(string value, string token) { return value.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0; } } public enum ExternalContentLoadState { Unavailable, Loading, Complete } public static class GunGameSceneIdentity { public const string Identifier = "GunGame"; public static bool IsMatch(string sceneIdentifier) { return string.Equals(sceneIdentifier, "GunGame", StringComparison.OrdinalIgnoreCase); } } public sealed class GunGameSelectorInstanceTracker { private object activeSelector; public bool Observe(object selector) { if (selector == null) { activeSelector = null; return false; } if (activeSelector == selector) { return false; } activeSelector = selector; return true; } } public static class GunGameSelectorLocator { public static object Resolve(Type selectorType) { object obj = ResolveSingleton(selectorType); if (IsAlive(obj)) { return obj; } if ((object)selectorType != null) { return Object.FindObjectOfType(selectorType); } return null; } public static object ResolveSingleton(Type selectorType) { if ((object)selectorType == null) { return null; } PropertyInfo property = selectorType.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); if ((object)property != null && property.CanRead) { return property.GetValue(null, null); } return null; } private static bool IsAlive(object candidate) { if (candidate == null) { return false; } Object val = (Object)((candidate is Object) ? candidate : null); if (val != null) { return val != (Object)null; } return true; } } public static class GunGameSpawnSafetyPolicy { public static bool HasExpectedCategory(string role, string category) { if (role == "Gun") { return category == "Firearm"; } if (role == "Feed") { switch (category) { default: return category == "Cartridge"; case "Magazine": case "Clip": case "SpeedLoader": return true; } } if (!(role != "Extra")) { return category == "Attachment"; } return true; } } public sealed class GunGameSpawnSafety { private static GunGameSpawnSafety active; private readonly MonoBehaviour host; private readonly Action<string> trace; private bool skipQueued; private object queuedProgression; private int queuedWeaponId; public GunGameSpawnSafety(MonoBehaviour host, Action<string> trace) { this.host = host; this.trace = trace; } public bool Install(Harmony harmony) { //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0114: 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_0127: Expected O, but got Unknown //IL_0127: Expected O, but got Unknown //IL_0127: Expected O, but got Unknown //IL_012c: 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_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Expected O, but got Unknown //IL_0146: Expected O, but got Unknown //IL_0146: Expected O, but got Unknown Type type = AccessTools.TypeByName("GunGame.Scripts.Progression"); Type type2 = AccessTools.TypeByName("GunGame.Scripts.Weapons.WeaponBuffer"); MethodInfo methodInfo = type?.GetMethod("SpawnAndEquip", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(bool) }, null); MethodInfo methodInfo2 = (((object)type2 == null) ? null : FindMethod(type2, "SpawnAsync", 2)); MethodInfo methodInfo3 = AccessTools.Method(typeof(GunGameSpawnSafety), "SpawnAndEquipPrefix", (Type[])null, (Type[])null); MethodInfo methodInfo4 = AccessTools.Method(typeof(GunGameSpawnSafety), "SpawnAndEquipPostfix", (Type[])null, (Type[])null); MethodInfo methodInfo5 = AccessTools.Method(typeof(GunGameSpawnSafety), "SpawnAndEquipFinalizer", (Type[])null, (Type[])null); MethodInfo methodInfo6 = AccessTools.Method(typeof(GunGameSpawnSafety), "SpawnAsyncPrefix", (Type[])null, (Type[])null); MethodInfo methodInfo7 = AccessTools.Method(typeof(GunGameSpawnSafety), "SpawnAsyncPostfix", (Type[])null, (Type[])null); MethodInfo methodInfo8 = AccessTools.Method(typeof(GunGameSpawnSafety), "SpawnAsyncFinalizer", (Type[])null, (Type[])null); if ((object)methodInfo == null || (object)methodInfo2 == null || (object)methodInfo3 == null || (object)methodInfo4 == null || (object)methodInfo5 == null || (object)methodInfo6 == null || (object)methodInfo7 == null || (object)methodInfo8 == null) { return false; } try { active = this; harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo3), new HarmonyMethod(methodInfo4), (HarmonyMethod)null, new HarmonyMethod(methodInfo5), (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(methodInfo6), new HarmonyMethod(methodInfo7), (HarmonyMethod)null, new HarmonyMethod(methodInfo8), (HarmonyMethod)null); Patches patchInfo = Harmony.GetPatchInfo((MethodBase)methodInfo); Patches patchInfo2 = Harmony.GetPatchInfo((MethodBase)methodInfo2); return patchInfo != null && patchInfo2 != null; } catch { active = null; return false; } } public static void Clear() { active = null; } private static bool SpawnAndEquipPrefix(object __instance) { if (active == null) { return true; } if (active.TryValidateCurrentLoadout(__instance, out var weaponId, out var reason)) { return true; } active.QueueSkip(__instance, weaponId, reason); return false; } private static Exception SpawnAndEquipFinalizer(object __instance, Exception __exception) { if (__exception == null || active == null) { return __exception; } active.QueueSkip(__instance, ReadCurrentWeaponId(__instance), "spawn exception " + __exception.GetType().Name); return null; } private static void SpawnAndEquipPostfix(object __instance) { if (active != null) { active.TryMountGeneratedOptic(__instance); } } private static bool SpawnAsyncPrefix(object __instance, object __1, ref IEnumerator __result) { if (active == null) { return true; } if (active.TryValidateGunData(__1, out var reason)) { return true; } active.QueueSkipFromWeaponBuffer(__instance, "invalid pre-buffer: " + reason); active.trace("ignored invalid GunGame pre-buffer: " + reason); __result = EmptyEnumerator(); return false; } private static void SpawnAsyncPostfix(object __instance, ref IEnumerator __result) { if (active != null && __result != null) { __result = GuardSpawnAsync(__instance, __result); } } private static Exception SpawnAsyncFinalizer(object __instance, Exception __exception, ref IEnumerator __result) { if (__exception == null || active == null) { return __exception; } active.QueueSkipFromWeaponBuffer(__instance, "buffer spawn exception " + __exception.GetType().Name); __result = EmptyEnumerator(); return null; } private bool TryValidateCurrentLoadout(object progression, out int weaponId, out string reason) { weaponId = ReadCurrentWeaponId(progression); reason = null; try { object obj = AccessTools.TypeByName("GunGame.Scripts.Options.GameSettings")?.GetProperty("CurrentPool", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).GetValue(null, null); MethodInfo methodInfo = ((obj == null) ? null : FindMethod(obj.GetType(), "GetWeapon", 1)); if (weaponId < 0 || obj == null || (object)methodInfo == null) { return true; } object gunData = methodInfo.Invoke(obj, new object[1] { weaponId }); return TryValidateGunData(gunData, out reason); } catch { return true; } } private bool TryValidateGunData(object gunData, out string reason) { reason = null; if (gunData == null) { reason = "missing gun data"; return false; } Dictionary<string, FVRObject> oD; try { oD = IM.OD; } catch { return true; } if (oD == null) { return true; } if (!HasExpectedObject(oD, ReadField(gunData, "GunName"), "Gun", out reason)) { return false; } string text = ReadField(gunData, "MagName"); if (!string.IsNullOrEmpty(text) && !HasExpectedObject(oD, text, "Feed", out reason)) { return false; } string text2 = ReadField(gunData, "Extra"); if (!string.IsNullOrEmpty(text2)) { return HasExpectedObject(oD, text2, "Extra", out reason); } return true; } private void TryMountGeneratedOptic(object progression) { try { int num = ReadCurrentWeaponId(progression); object currentPool = GetCurrentPool(); if (num < 0 || !IsGeneratedRuntimePool(currentPool)) { return; } object currentGunData = GetCurrentGunData(currentPool, num); string text = ReadField(currentGunData, "Extra"); if (!string.IsNullOrEmpty(text)) { IList currentEquipment = GetCurrentEquipment(progression); FVRFireArm val = FindFirearm(currentEquipment, ReadField(currentGunData, "GunName")); FVRFireArmAttachment val2 = FindAttachment(currentEquipment, text); if (!((Object)(object)val == (Object)null) && !((Object)(object)val2 == (Object)null) && !((Object)(object)val2.curMount != (Object)null) && !TryAttachOptic((FVRPhysicalObject)(object)val, val2)) { trace("could not mount generated optic for loadout " + num + "."); } } } catch (Exception ex) { trace("could not mount generated optic: " + ex.GetType().Name); } } private static object GetCurrentPool() { try { return (AccessTools.TypeByName("GunGame.Scripts.Options.GameSettings")?.GetProperty("CurrentPool", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))?.GetValue(null, null); } catch { return null; } } private static object GetCurrentGunData(object currentPool, int weaponId) { try { return ((currentPool == null) ? null : FindMethod(currentPool.GetType(), "GetWeapon", 1))?.Invoke(currentPool, new object[1] { weaponId }); } catch { return null; } } private static bool IsGeneratedRuntimePool(object currentPool) { return ReadStringProperty(currentPool, "Name").StartsWith("Runtime ", StringComparison.Ordinal); } private static string ReadStringProperty(object value, string propertyName) { try { PropertyInfo propertyInfo = value?.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return ((object)propertyInfo == null) ? string.Empty : ((propertyInfo.GetValue(value, null) as string) ?? string.Empty); } catch { return string.Empty; } } private static IList GetCurrentEquipment(object progression) { FieldInfo fieldInfo = progression?.GetType().GetField("_currentEquipment", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if ((object)fieldInfo != null) { return fieldInfo.GetValue(progression) as IList; } return null; } private static FVRFireArm FindFirearm(IList equipment, string firearmId) { foreach (object item in equipment ?? new object[0]) { GameObject val = (GameObject)((item is GameObject) ? item : null); FVRFireArm val2 = (((Object)(object)val == (Object)null) ? null : val.GetComponent<FVRFireArm>()); if ((Object)(object)val2 != (Object)null && HasObjectId((FVRPhysicalObject)(object)val2, firearmId)) { return val2; } } return null; } private static FVRFireArmAttachment FindAttachment(IList equipment, string opticId) { foreach (object item in equipment ?? new object[0]) { GameObject val = (GameObject)((item is GameObject) ? item : null); FVRFireArmAttachment val2 = (((Object)(object)val == (Object)null) ? null : val.GetComponent<FVRFireArmAttachment>()); if ((Object)(object)val2 != (Object)null && HasObjectId((FVRPhysicalObject)(object)val2, opticId)) { return val2; } } return null; } private static bool HasObjectId(FVRPhysicalObject item, string objectId) { if ((Object)(object)item != (Object)null && (Object)(object)item.ObjectWrapper != (Object)null) { return string.Equals(item.ObjectWrapper.ItemID, objectId, StringComparison.Ordinal); } return false; } private static bool TryAttachOptic(FVRPhysicalObject parent, FVRFireArmAttachment optic) { FVRFireArmAttachmentMount val = FindCompatibleOpticMount(parent, optic); if ((Object)(object)val == (Object)null) { return false; } ((FVRPhysicalObject)optic).ClearQuickbeltState(); optic.AttachToMount(val, false); return true; } private static FVRFireArmAttachmentMount FindCompatibleOpticMount(FVRPhysicalObject parent, FVRFireArmAttachment attachment) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)parent == (Object)null || (Object)(object)attachment == (Object)null || !IsOpticMountType(attachment.Type)) { return null; } foreach (FVRFireArmAttachmentMount item in parent.AttachmentMounts ?? new List<FVRFireArmAttachmentMount>()) { if (!((Object)(object)item == (Object)null) && item.Type == attachment.Type && IsOpticMountType(item.Type) && IsTopSightingMount(parent, item) && item.isMountableOn(attachment)) { return item; } } return null; } private static bool IsTopSightingMount(FVRPhysicalObject parent, FVRFireArmAttachmentMount mount) { //IL_0020: 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) if (!OpticMountPolicy.RequiresTopSightingOrientation(((object)Unsafe.As<FVRFireArmAttachementMountType, FVRFireArmAttachementMountType>(ref mount.Type)/*cast due to .constrained prefix*/).ToString())) { return true; } return Vector3.Angle(((Component)mount).transform.up, ((Component)parent).transform.up) < 46f; } private unsafe static bool IsOpticMountType(FVRFireArmAttachementMountType mountType) { return OpticMountPolicy.IsOpticMountType(((object)(*(FVRFireArmAttachementMountType*)(&mountType))/*cast due to .constrained prefix*/).ToString()); } private void QueueSkip(object progression, int weaponId, string reason) { if (progression != null && (!skipQueued || queuedProgression != progression)) { skipQueued = true; queuedProgression = progression; queuedWeaponId = weaponId; ClearWeaponBuffer(progression); trace("skipping invalid GunGame loadout " + DescribeCurrentLoadout(progression, weaponId) + ": " + reason); host.StartCoroutine(AdvancePastInvalidWeapon()); } } private void QueueSkipFromWeaponBuffer(object weaponBuffer, string reason) { object obj = FindProgressionForWeaponBuffer(weaponBuffer); if (obj == null) { trace("could not advance invalid GunGame pre-buffer: " + reason); } else { QueueSkip(obj, ReadCurrentWeaponId(obj), reason); } } private static object FindProgressionForWeaponBuffer(object weaponBuffer) { try { Type type = AccessTools.TypeByName("GunGame.Scripts.Progression"); if ((object)type == null) { return null; } Component val = (Component)((weaponBuffer is Component) ? weaponBuffer : null); return ((object)(((Object)(object)val == (Object)null) ? null : val.GetComponentInParent(type))) ?? ((object)Object.FindObjectOfType(type)); } catch { return null; } } private static IEnumerator GuardSpawnAsync(object weaponBuffer, IEnumerator original) { using (original as IDisposable) { object current; Exception exception; while (TryMoveNext(original, out current, out exception)) { yield return current; } if (exception != null && active != null) { active.QueueSkipFromWeaponBuffer(weaponBuffer, "buffer spawn exception " + exception.GetType().Name); } } } private static bool TryMoveNext(IEnumerator original, out object current, out Exception exception) { current = null; exception = null; try { if (original == null || !original.MoveNext()) { return false; } current = original.Current; return true; } catch (Exception ex) { exception = ex; return false; } } private static string DescribeCurrentLoadout(object progression, int weaponId) { object currentGunData = GetCurrentGunData(GetCurrentPool(), weaponId); return "weapon " + weaponId + " (gun=" + ReadField(currentGunData, "GunName") + ", feed=" + ReadField(currentGunData, "MagName") + ", optic=" + ReadField(currentGunData, "Extra") + ")"; } private IEnumerator AdvancePastInvalidWeapon() { yield return null; object obj = queuedProgression; int num = queuedWeaponId; skipQueued = false; queuedProgression = null; if (obj == null || ReadCurrentWeaponId(obj) != num) { yield break; } MethodInfo methodInfo = FindMethod(obj.GetType(), "Promote", 0); if ((object)methodInfo == null) { trace("could not advance past invalid GunGame loadout."); yield break; } try { methodInfo.Invoke(obj, null); } catch (Exception ex) { trace("could not advance past invalid GunGame loadout: " + ex.GetType().Name); } } private static bool HasExpectedObject(IDictionary<string, FVRObject> objects, string objectId, string role, out string reason) { reason = null; if (string.IsNullOrEmpty(objectId) || !objects.TryGetValue(objectId, out var value) || (Object)(object)value == (Object)null) { reason = role + " id is unavailable"; return false; } if (!GunGameSpawnSafetyPolicy.HasExpectedCategory(role, ((object)Unsafe.As<ObjectCategory, ObjectCategory>(ref value.Category)/*cast due to .constrained prefix*/).ToString())) { reason = role + " id has category " + ((object)Unsafe.As<ObjectCategory, ObjectCategory>(ref value.Category)/*cast due to .constrained prefix*/).ToString(); return false; } return true; } private static void ClearWeaponBuffer(object progression) { try { Component val = (Component)((progression is Component) ? progression : null); Type type = AccessTools.TypeByName("GunGame.Scripts.Weapons.WeaponBuffer"); Component val2 = (((Object)(object)val == (Object)null || (object)type == null) ? null : val.GetComponent(type)); (((Object)(object)val2 == (Object)null) ? null : FindMethod(((object)val2).GetType(), "ClearBuffer", 0))?.Invoke(val2, null); } catch { } } private static int ReadCurrentWeaponId(object progression) { try { PropertyInfo propertyInfo = progression?.GetType().GetProperty("CurrentWeaponId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); return ((object)propertyInfo == null) ? (-1) : Convert.ToInt32(propertyInfo.GetValue(progression, null)); } catch { return -1; } } private static string ReadField(object value, string fieldName) { FieldInfo fieldInfo = value?.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); object obj; if ((object)fieldInfo != null) { obj = fieldInfo.GetValue(value) as string; if (obj == null) { return string.Empty; } } else { obj = string.Empty; } return (string)obj; } private static MethodInfo FindMethod(Type type, string methodName, int parameterCount) { if ((object)type == null) { return null; } MethodInfo[] methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { if (methodInfo.Name == methodName && methodInfo.GetParameters().Length == parameterCount) { return methodInfo; } } return null; } private static IEnumerator EmptyEnumerator() { yield break; } } public sealed class MountResolution { public string RawMount { get; private set; } public string CanonicalMount { get; private set; } public bool IsResolved { get; private set; } private MountResolution(string rawMount, string canonicalMount, bool isResolved) { RawMount = rawMount; CanonicalMount = canonicalMount; IsResolved = isResolved; } public static MountResolution Resolve(string rawMount) { if (string.IsNullOrEmpty(rawMount) || rawMount.Trim().Length == 0) { return new MountResolution(string.Empty, string.Empty, isResolved: false); } string text = rawMount.Trim(); if (string.Equals(text, "99", StringComparison.Ordinal)) { return new MountResolution(text, "RMR", isResolved: true); } if (int.TryParse(text, out var _)) { return new MountResolution(text, string.Empty, isResolved: false); } return new MountResolution(text, text, isResolved: true); } } public sealed class OpticMountRule { public string MountType { get; private set; } public List<string> OpticKinds { get; private set; } public int Priority { get; private set; } public OpticMountRule(string mountType, IEnumerable<string> opticKinds, int priority) { MountType = mountType; OpticKinds = (opticKinds ?? Enumerable.Empty<string>()).Where((string kind) => !string.IsNullOrEmpty(kind)).Distinct<string>(StringComparer.Ordinal).ToList(); Priority = priority; } public bool Accepts(string opticKind) { return OpticKinds.Any((string kind) => string.Equals(kind, opticKind, StringComparison.Ordinal)); } } public static class OpticMountPolicy { private static readonly HashSet<string> ReflexOnlyMounts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "RMR", "Handgun" }; private static readonly HashSet<string> RailMounts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Picatinny", "MLokRail" }; private static readonly HashSet<string> ScopeOnlyMounts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Russian", "MAS4956Scope", "SVTScope", "M16HandleMount", "M1GarandScope", "M1CarbineScope", "MP5RailMount", "PythonScopeMount", "FamasTopRail", "Mini14TopRail", "R1022TopRail" }; public static IEnumerable<OpticMountRule> Rank(IEnumerable<string> physicalMountTypes) { return (from rule in (from resolution in (physicalMountTypes ?? Enumerable.Empty<string>()).Select(MountResolution.Resolve) where resolution.IsResolved select resolution.CanonicalMount).Distinct<string>(StringComparer.OrdinalIgnoreCase).Select(CreateRule) where rule != null orderby rule.Priority select rule).ThenBy<OpticMountRule, string>((OpticMountRule rule) => rule.MountType, StringComparer.Ordinal); } public static bool IsOpticMountType(string rawMountType) { MountResolution mountResolution = MountResolution.Resolve(rawMountType); if (mountResolution.IsResolved) { return GetOpticKinds(mountResolution.CanonicalMount) != null; } return false; } public static bool RequiresTopSightingOrientation(string rawMountType) { MountResolution mountResolution = MountResolution.Resolve(rawMountType); if (mountResolution.IsResolved) { return RailMounts.Contains(mountResolution.CanonicalMount); } return false; } private static OpticMountRule CreateRule(string mountType) { IEnumerable<string> opticKinds = GetOpticKinds(mountType); if (opticKinds == null) { return null; } return new OpticMountRule(mountType, opticKinds, GetPriority(mountType)); } private static IEnumerable<string> GetOpticKinds(string mountType) { if (ReflexOnlyMounts.Contains(mountType)) { return new string[1] { "Reflex" }; } if (RailMounts.Contains(mountType)) { return new string[2] { "Scope", "Reflex" }; } if (!IsScopeMount(mountType)) { return null; } return new string[1] { "Scope" }; } private static int GetPriority(string mountType) { if (ReflexOnlyMounts.Contains(mountType)) { return 20; } if (!RailMounts.Contains(mountType)) { return 10; } return 100; } private static bool IsScopeMount(string mountType) { if (!ScopeOnlyMounts.Contains(mountType) && !mountType.StartsWith("Scope_", StringComparison.OrdinalIgnoreCase)) { return mountType.EndsWith("Scope", StringComparison.OrdinalIgnoreCase); } return true; } } public sealed class OtherLoaderStatusProbe { private bool initialized; private bool failureLogged; private MethodInfo progressMethod; private FieldInfo startTimeField; private PropertyInfo activeLoadersProperty; public ExternalContentLoadState Read(Action<string> logDebug) { Initialize(); if ((object)progressMethod == null) { return ExternalContentLoadState.Unavailable; } try { float num = Convert.ToSingle(progressMethod.Invoke(null, null)); float num2 = (((object)startTimeField == null) ? 0f : Convert.ToSingle(startTimeField.GetValue(null))); int num3 = (((object)activeLoadersProperty != null) ? Convert.ToInt32(activeLoadersProperty.GetValue(null, null)) : 0); if (num >= 1f && num3 <= 0 && ((object)startTimeField != null || (object)activeLoadersProperty != null)) { return ExternalContentLoadState.Complete; } return (num2 > 0f || num3 > 0) ? ExternalContentLoadState.Loading : ExternalContentLoadState.Unavailable; } catch (Exception ex) { LogFailureOnce(logDebug, "Could not read OtherLoader load status: " + ex); return ExternalContentLoadState.Unavailable; } } private void Initialize() { if (!initialized) { initialized = true; Type type = AccessTools.TypeByName("OtherLoader.LoaderStatus"); if ((object)type != null) { BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; progressMethod = type.GetMethod("GetLoaderProgress", bindingAttr); startTimeField = type.GetField("LoadStartTime", bindingAttr); activeLoadersProperty = type.GetProperty("NumActiveLoaders", bindingAttr); } } } private void LogFailureOnce(Action<string> logDebug, string message) { if (!failureLogged) { failureLogged = true; logDebug?.Invoke(message); } } } public static class PipScopeOpticClassifier { private const string Pso1ScopeObjectId = "MagnifierPSO1"; public static string ClassifyFromMetadata(string objectId, string attachmentFeature) { if (IsPso1Scope(objectId) && attachmentFeature == "Magnification") { return "Scope"; } if (!string.IsNullOrEmpty(objectId) && objectId.IndexOf("magnifier", StringComparison.OrdinalIgnoreCase) >= 0) { return "Magnifier"; } if (attachmentFeature == "Reflex") { return "Reflex"; } if (!(attachmentFeature == "Magnification")) { return string.Empty; } return "Scope"; } public static string Classify(string objectId, bool hasPipScope, bool hasReflexSight) { if (IsPso1Scope(objectId) && hasPipScope) { return "Scope"; } if (!string.IsNullOrEmpty(objectId) && objectId.IndexOf("magnifier", StringComparison.OrdinalIgnoreCase) >= 0) { return "Magnifier"; } if (hasPipScope) { return "Scope"; } if (!hasReflexSight) { return string.Empty; } return "Reflex"; } private static bool IsPso1Scope(string objectId) { return string.Equals(objectId, "MagnifierPSO1", StringComparison.Ordinal); } } [BepInPlugin("HLin.GunGameProgressionsMetadataExporter", "GunGame Progressions Metadata Exporter", "1.4.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInProcess("h3vr.exe")] public sealed class Plugin : BaseUnityPlugin { private enum RuntimeGenerationPhase { Vanilla, Modded, CompatibilityProbe } private sealed class RuntimeGenerationJob { private readonly object sync = new object(); private readonly string packagePath; private readonly List<RuntimeMetadataEntry> entries; private readonly List<RuntimeEnemyEntry> enemyEntries; private readonly RuntimeGenerationPhase phase; private readonly bool confirmedEmptySnapshot; private readonly bool allowPolicyReplacement; private bool isCompleted; private Exception error; private RuntimeGenerationReport report; public bool IsCompleted { get { lock (sync) { return isCompleted; } } } public Exception Error { get { lock (sync) { return error; } } } public RuntimeGenerationReport Report { get { lock (sync) { return report; } } } public RuntimeGenerationJob(string packagePath, List<RuntimeMetadataEntry> entries, List<RuntimeEnemyEntry> enemyEntries, RuntimeGenerationPhase phase, bool confirmedEmptySnapshot, bool allowPolicyReplacement) { this.packagePath = packagePath; this.entries = entries; this.enemyEntries = enemyEntries; this.phase = phase; this.confirmedEmptySnapshot = confirmedEmptySnapshot; this.allowPolicyReplacement = allowPolicyReplacement; } public void Start() { Thread thread = new Thread(Generate); thread.IsBackground = true; thread.Priority = ThreadPriority.BelowNormal; thread.Start(); } private void Generate() { Stopwatch stopwatch = Stopwatch.StartNew(); RuntimeGenerationReport runtimeGenerationReport = null; Exception ex = null; try { runtimeGenerationReport = GenerateRuntimeFiles(packagePath, entries, enemyEntries, phase, confirmedEmptySnapshot, allowPolicyReplacement); runtimeGenerationReport.ElapsedMilliseconds = stopwatch.ElapsedMilliseconds; } catch (Exception ex2) { ex = ex2; } lock (sync) { report = runtimeGenerationReport; error = ex; isCompleted = true; } } } private sealed class RuntimeMetadataCapture { public List<RuntimeMetadataEntry> Entries { get; private set; } public long ElapsedMilliseconds { get; private set; } public RuntimeMetadataCapture(List<RuntimeMetadataEntry> entries, long elapsedMilliseconds) { Entries = entries; ElapsedMilliseconds = elapsedMilliseconds; } } private sealed class RuntimeEnemyCapture { public List<RuntimeEnemyEntry> Entries { get; private set; } public long ElapsedMilliseconds { get; private set; } public RuntimeEnemyCapture(List<RuntimeEnemyEntry> entries, long elapsedMilliseconds) { Entries = entries; ElapsedMilliseconds = elapsedMilliseconds; } } private sealed class RuntimeGenerationReport { public int EntryCount { get; private set; } public int ModdedEntryCount { get; private set; } public int EnemyCount { get; private set; } public int PoolCount { get; private set; } public int EligibleWeaponsPerPool { get; private set; } public int SkippedFirearmCount { get; private set; } public List<string> PoolFileNames { get; private set; } public bool WasWritten { get; private set; } public long ElapsedMilliseconds { get; set; } public RuntimeGenerationReport(int entryCount, int moddedEntryCount, int enemyCount, int poolCount, int eligibleWeaponsPerPool, int skippedFirearmCount, List<string> poolFileNames, bool wasWritten) { EntryCount = entryCount; ModdedEntryCount = moddedEntryCount; EnemyCount = enemyCount; PoolCount = poolCount; EligibleWeaponsPerPool = eligibleWeaponsPerPool; SkippedFirearmCount = skippedFirearmCount; PoolFileNames = poolFileNames; WasWritten = wasWritten; } } private const long CaptureFrameBudgetMilliseconds = 2L; private const float SelectorSubscriptionRetrySeconds = 10f; private const string HarmonyId = "HLin.GunGameProgressionsMetadataExporter.KodemanRefresh"; private static Plugin instance; private readonly GunGameSelectorInstanceTracker selectorTracker = new GunGameSelectorInstanceTracker(); private readonly OtherLoaderStatusProbe otherLoaderStatusProbe = new OtherLoaderStatusProbe(); private List<RuntimeMetadataEntry> vanillaMetadata; private bool vanillaGenerationFinished; private bool moddedRefreshRunning; private bool moddedRefreshRequested; private bool policyReplacementEligible; private bool startupWarmupScheduled; private Harmony harmony; private GunGameSpawnSafety spawnSafety; private Type weaponPoolLoaderType; private MethodInfo gameManagerOnDestroy; private EventInfo weaponPoolLoadedEvent; private Delegate weaponPoolLoadedHandler; private bool selectorSubscriptionWaitingLogged; private bool objectDataUnavailableLogged; private void Awake() { instance = this; InstallGunGameRefreshHooks(); InstallGunGameSpawnSafety(); ScheduleStartupProfileWarmup(); ((MonoBehaviour)this).StartCoroutine(WaitForWeaponPoolLoaderReadyEvent()); Trace("plugin awake."); if (RuntimeBuildFeatures.CompatibilityProbeEnabled) { Trace("local Debug build: Runtime 05 compatibility probe enabled."); } ((BaseUnityPlugin)this).Logger.LogInfo((object)"GunGame Progressions: ready."); } private void Start() { ScheduleStartupProfileWarmup(); } private void ScheduleStartupProfileWarmup() { if (!startupWarmupScheduled) { startupWarmupScheduled = true; Trace("starting vanilla and modded profile warmup."); ((MonoBehaviour)this).StartCoroutine(GenerateVanillaPoolsAtStartup()); RequestModdedRefresh(); ((MonoBehaviour)this).StartCoroutine(RequestStartupModdedRescan(60f, "startup 1-minute rescan requested.", enablePolicyReplacement: false)); ((MonoBehaviour)this).StartCoroutine(RequestStartupModdedRescan(300f, "startup 5-minute rescan requested.", enablePolicyReplacement: false)); ((MonoBehaviour)this).StartCoroutine(RequestStartupModdedRescan(600f, "startup 10-minute rescan requested; policy replacement eligible.", enablePolicyReplacement: true)); } } private void OnDestroy() { if (harmony != null) { harmony.UnpatchSelf(); } GunGameSpawnSafety.Clear(); if ((object)weaponPoolLoadedEvent != null && (object)weaponPoolLoadedHandler != null) { weaponPoolLoadedEvent.RemoveEventHandler(null, weaponPoolLoadedHandler); } if ((Object)(object)instance == (Object)(object)this) { instance = null; } } private void InstallGunGameRefreshHooks() { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Expected O, but got Unknown //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected O, but got Unknown Type type = AccessTools.TypeByName("GunGame.Scripts.GameManager"); gameManagerOnDestroy = (((object)type == null) ? null : AccessTools.Method(type, "OnDestroy", (Type[])null, (Type[])null)); MethodInfo methodInfo = AccessTools.Method(typeof(Plugin), "GameManagerOnDestroyPostfix", (Type[])null, (Type[])null); if ((object)gameManagerOnDestroy == null || (object)methodInfo == null) { ((BaseUnityPlugin)this).Logger.LogError((object)"GunGame Progressions: scene hook unavailable; packaged pools will be used."); return; } harmony = new Harmony("HLin.GunGameProgressionsMetadataExporter.KodemanRefresh"); harmony.Patch((MethodBase)gameManagerOnDestroy, (HarmonyMethod)null, new HarmonyMethod(methodInfo), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Patches patchInfo = Harmony.GetPatchInfo((MethodBase)gameManagerOnDestroy); if (patchInfo == null || !patchInfo.Postfixes.Any((Patch patch) => patch.owner == "HLin.GunGameProgressionsMetadataExporter.KodemanRefresh")) { ((BaseUnityPlugin)this).Logger.LogError((object)"GunGame Progressions: scene hook unavailable; packaged pools will be used."); } else { Trace("GunGame session-exit hook active."); } } private void InstallGunGameSpawnSafety() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown if (harmony == null) { harmony = new Harmony("HLin.GunGameProgressionsMetadataExporter.KodemanRefresh"); } spawnSafety = new GunGameSpawnSafety((MonoBehaviour)(object)this, Trace); if (spawnSafety.Install(harmony)) { Trace("GunGame invalid-loadout safety active."); } else { ((BaseUnityPlugin)this).Logger.LogError((object)"GunGame Progressions: spawn safety unavailable."); } } private static void GameManagerOnDestroyPostfix() { if ((Object)(object)instance != (Object)null) { instance.Trace("GunGame session ended; background refresh requested."); instance.RequestModdedRefresh(); } } private IEnumerator WaitForWeaponPoolLoaderReadyEvent() { while ((object)weaponPoolLoadedHandler == null && !TrySubscribeToWeaponPoolLoaderReadyEvent()) { if (!selectorSubscriptionWaitingLogged) { selectorSubscriptionWaitingLogged = true; Trace("waiting for GunGame selector event."); } yield return (object)new WaitForSecondsRealtime(10f); } } private bool TrySubscribeToWeaponPoolLoaderReadyEvent() { weaponPoolLoaderType = AccessTools.TypeByName("GunGame.Scripts.Weapons.WeaponPoolLoader"); MethodInfo methodInfo = AccessTools.Method(typeof(Plugin), "WeaponPoolLoaderReady", (Type[])null, (Type[])null); weaponPoolLoadedEvent = (((object)weaponPoolLoaderType == null) ? null : weaponPoolLoaderType.GetEvent("WeaponLoadedEvent", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); if ((object)weaponPoolLoadedEvent == null || (object)methodInfo == null || (object)weaponPoolLoadedEvent.EventHandlerType == null) { return false; } try { weaponPoolLoadedHandler = Delegate.CreateDelegate(weaponPoolLoadedEvent.EventHandlerType, methodInfo); weaponPoolLoadedEvent.AddEventHandler(null, weaponPoolLoadedHandler); Trace("GunGame selector ready event subscribed."); return true; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogDebug((object)("Could not subscribe to GunGame selector ready event: " + ex)); return false; } } private static void WeaponPoolLoaderReady() { if (!((Object)(object)instance == (Object)null)) { object obj = instance.FindGunGamePoolLoader(); if (instance.selectorTracker.Observe(obj)) { instance.Trace("live selector ready event received."); instance.RestorePersistedRuntimeProfilesForSelector(obj); } instance.RequestModdedRefresh(); } } private object FindGunGamePoolLoader() { if ((object)weaponPoolLoaderType == null) { weaponPoolLoaderType = AccessTools.TypeByName("GunGame.Scripts.Weapons.WeaponPoolLoader"); if ((object)weaponPoolLoaderType != null) { Trace("selector type resolved."); } } return GunGameSelectorLocator.Resolve(weaponPoolLoaderType); } private void RestorePersistedRuntimeProfilesForSelector(object weaponPoolLoader) { int num = AddPersistedRuntimePoolChoices(weaponPoolLoader); if (num > 0) { Trace("selector restored " + num + " persisted runtime profiles."); } } private void Trace(string message) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("GunGame Progressions trace: " + message)); } private void RequestModdedRefresh() { moddedRefreshRequested = true; if (!moddedRefreshRunning) { ((MonoBehaviour)this).StartCoroutine(RefreshModdedPoolsInBackground()); } } private IEnumerator RequestStartupModdedRescan(float delaySeconds, string traceMessage, bool enablePolicyReplacement) { yield return (object)new WaitForSecondsRealtime(delaySeconds); if (enablePolicyReplacement) { policyReplacementEligible = true; } Trace(traceMessage); RequestModdedRefresh(); } private IEnumerator GenerateVanillaPoolsAtStartup() { Dictionary<string, FVRObject> objects; while (!TryGetObjectData(out objects) || objects.Count == 0) { yield return null; } RuntimeMetadataCapture metadataCapture = null; yield return ((MonoBehaviour)this).StartCoroutine(CaptureRuntimeMetadata(objects, (FVRObject item) => !item.IsModContent, delegate(RuntimeMetadataCapture capture) { metadataCapture = capture; })); RuntimeEnemyCapture enemyCapture = null; yield return ((MonoBehaviour)this).StartCoroutine(CaptureEnemyEntries(delegate(RuntimeEnemyCapture capture) { enemyCapture = capture; })); if (metadataCapture == null) { vanillaGenerationFinished = true; ((BaseUnityPlugin)this).Logger.LogWarning((object)"GunGame Progressions: using packaged fallback pools."); yield break; } vanillaMetadata = metadataCapture.Entries; string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); RuntimeGenerationJob job = new RuntimeGenerationJob(directoryName, vanillaMetadata, (enemyCapture == null) ? new List<RuntimeEnemyEntry>() : enemyCapture.Entries, RuntimeGenerationPhase.Vanilla, confirmedEmptySnapshot: false, allowPolicyReplacement: false); job.Start(); while (!job.IsCompleted) { yield return null; } vanillaGenerationFinished = true; if (job.Error != null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"GunGame Progressions: using packaged fallback pools."); ((BaseUnityPlugin)this).Logger.LogDebug((object)("GunGame vanilla pool generation failed: " + job.Error)); yield break; } ((BaseUnityPlugin)this).Logger.LogInfo((object)"GunGame Progressions: vanilla pools ready."); ((BaseUnityPlugin)this).Logger.LogDebug((object)("GunGame vanilla pools: " + job.Report.PoolCount + " pools, " + job.Report.EligibleWeaponsPerPool + " items per pool; capture " + metadataCapture.ElapsedMilliseconds + "ms + background build/write " + job.Report.ElapsedMilliseconds + "ms.")); } private IEnumerator RefreshModdedPoolsInBackground() { moddedRefreshRunning = true; while (moddedRefreshRequested) { moddedRefreshRequested = false; bool allowPolicyReplacement = policyReplacementEligible; yield return ((MonoBehaviour)this).StartCoroutine(GenerateModdedPoolsForRefresh(allowPolicyReplacement)); } moddedRefreshRunning = false; } private IEnumerator GenerateModdedPoolsForRefresh(bool allowPolicyReplacement) { while (!vanillaGenerationFinished) { yield return null; } Stopwatch totalTimer = Stopwatch.StartNew(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"GunGame Progressions: preparing pools."); if (!TryGetObjectData(out var objects) || objects.Count == 0) { Trace("modded capture skipped; object registry unavailable."); yield break; } ExternalContentLoadState externalLoadState = otherLoaderStatusProbe.Read((Action<string>)((BaseUnityPlugin)this).Logger.LogDebug); RuntimeMetadataCapture metadataCapture = null; Trace("modded capture started."); yield return ((MonoBehaviour)this).StartCoroutine(CaptureRuntimeMetadata(objects, (FVRObject item) => item.IsModContent, delegate(RuntimeMetadataCapture capture) { metadataCapture = capture; })); if (metadataCapture == null) { Trace("modded capture failed."); yield break; } Trace("modded capture complete: " + metadataCapture.Entries.Count + " entries."); yield return ((MonoBehaviour)this).StartCoroutine(GenerateModdedPoolCandidate(metadataCapture, totalTimer, externalLoadState == ExternalContentLoadState.Complete, allowPolicyReplacement)); } private IEnumerator GenerateModdedPoolCandidate(RuntimeMetadataCapture metadataCapture, Stopwatch totalTimer, bool confirmedEmptySnapshot, bool allowPolicyReplacement, Action<RuntimeGenerationReport> complete = null) { RuntimeEnemyCapture enemyCapture = null; yield return ((MonoBehaviour)this).StartCoroutine(CaptureEnemyEntries(delegate(RuntimeEnemyCapture capture) { enemyCapture = capture; })); string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); List<RuntimeMetadataEntry> entries = MergeRuntimeMetadata(vanillaMetadata ?? new List<RuntimeMetadataEntry>(), metadataCapture.Entries); RuntimeGenerationJob job = new RuntimeGenerationJob(directoryName, entries, (enemyCapture == null) ? new List<RuntimeEnemyEntry>() : enemyCapture.Entries, RuntimeGenerationPhase.Modded, confirmedEmptySnapshot, allowPolicyReplacement); job.Start(); while (!job.IsCompleted) { yield return null; } if (job.Error != null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"GunGame Progressions: using packaged fallback pools."); ((BaseUnityPlugin)this).Logger.LogDebug((object)("GunGame runtime pool generation failed: " + job.Error)); complete?.Invoke(null); yield break; } RuntimeGenerationReport report = job.Report; if (report.PoolCount == 0 && report.WasWritten) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"GunGame Progressions: no modded pools available."); } else if (report.WasWritten) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"GunGame Progressions: pools ready."); } else if (report.PoolCount == 0) { ((BaseUnityPlugin)this).Logger.LogDebug((object)"GunGame Modded snapshot was empty but not confirmed complete; saved profiles were retained."); } else { ((BaseUnityPlugin)this).Logger.LogDebug((object)"GunGame Modded candidate did not exceed the saved profile count."); } ((BaseUnityPlugin)this).Logger.LogDebug((object)("GunGame runtime pools: " + report.PoolCount + " pools, " + report.EntryCount + " items, " + report.EnemyCount + " Sosig types; capture " + metadataCapture.ElapsedMilliseconds + "ms + enemy capture " + ((enemyCapture == null) ? 0 : enemyCapture.ElapsedMilliseconds) + "ms + background build/write " + report.ElapsedMilliseconds + "ms, total " + totalTimer.ElapsedMilliseconds + "ms.")); ((BaseUnityPlugin)this).Logger.LogInfo((object)RuntimeStatusMessages.ModdedScanCompleted(totalTimer.ElapsedMilliseconds, metadataCapture.Entries.Count)); complete?.Invoke(report); } private static string RuntimePoolDisplayName(string poolFileName) { if (poolFileName != null && poolFileName.IndexOf("_02_Modded_Rot_", StringComparison.Ordinal) >= 0) { return "Runtime 02 - Modded Rot"; } if (poolFileName != null && poolFileName.IndexOf("_04_Modded_Mixed_Enemy_", StringComparison.Ordinal) >= 0) { return "Runtime 04 - Modded Mixed Enemy"; } return string.Empty; } private int AddPersistedRuntimePoolChoices(object loader) { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); if (string.IsNullOrEmpty(directoryName) || !Directory.Exists(directoryName)) { return 0; } List<string> poolFileNames = (from fileName in Directory.GetFiles(directoryName, "GunGameWeaponPool_Runtime_*.json").Select(Path.GetFileName) where RuntimeProfileFamily.IsModdedPoolFile(fileName) select fileName).OrderBy<string, string>((string fileName) => fileName, StringComparer.Ordinal).ToList(); return AddPoolChoices(loader, poolFileNames); } private int AddPoolChoices(object loader, IEnumerable<string> poolFileNames) { try { Type type = loader.GetType(); MethodInfo method = type.GetMethod("LoadWeaponPool", new Type[1] { typeof(string) }); FieldInfo field = type.GetField("_weaponPools", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = type.GetField("_choices", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field3 = type.GetField("ChoicePrefab", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); FieldInfo field4 = type.GetField("ChoicesListParent", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); IList list = (((object)field == null) ? null : (field.GetValue(loader) as IList)); IList list2 = (((object)field2 == null) ? null : (field2.GetValue(loader) as IList)); Object val = (Object)(((object)field3 == null) ? null : /*isinst with value type is only supported in some contexts*/); Transform val2 = (Transform)(((object)field4 == null) ? null : /*isinst with value type is only supported in some contexts*/); if ((object)method == null || list == null || list2 == null || val == (Object)null || (Object)(object)val2 == (Object)null) { return 0; } string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); HashSet<string> hashSet = new HashSet<string>(list.Cast<object>().Select(GunGamePoolName), StringComparer.Ordinal); int num = 0; foreach (string item in poolFileNames ?? Enumerable.Empty<string>()) { string text = RuntimePoolDisplayName(item); if (!string.IsNullOrEmpty(text) && hashSet.Contains(text)) { continue; } object obj = method.Invoke(loader, new object[1] { Path.Combine(directoryName, item) }); if (obj == null) { continue; } Object val3 = Object.Instantiate(val, val2); MethodInfo method2 = ((object)val3).GetType().GetMethod("Initialize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if ((object)method2 == null) { Object.Destroy(val3); continue; } method2.Invoke(val3, new object[1] { obj }); int num2 = RuntimePoolInsertionIndex(list, GunGamePoolName(obj)); list.Insert(num2, obj); list2.Insert(num2, val3); Component val4 = (Component)(object)((val3 is Component) ? val3 : null); if ((Object)(object)val4 != (Object)null) { val4.transform.SetSiblingIndex(num2); } hashSet.Add(GunGamePoolName(obj)); num++; } return num; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogDebug((object)("GunGame selector update failed: " + ex)); return 0; } } private static int RuntimePoolInsertionIndex(IList pools, string generatedPoolName) { if (generatedPoolName.StartsWith("Runtime 02", StringComparison.Ordinal)) { for (int i = 0; i < pools.Count; i++) { if (GunGamePoolName(pools[i]).StartsWith("Runtime 03", StringComparison.Ordinal)) { return i; } } } return pools.Count; } private static string GunGamePoolName(object pool) { if (pool == null) { return string.Empty; } MethodInfo method = pool.GetType().GetMethod("GetName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); object obj; if ((object)method != null) { obj = method.Invoke(pool, null) as string; if (obj == null) { return string.Empty; } } else { obj = string.Empty; } return (string)obj; } private bool TryGetObjectData(out Dictionary<string, FVRObject> objects) { objects = null; try { objects = IM.OD; return objects != null; } catch (Exception ex) { if (!objectDataUnavailableLogged) { objectDataUnavailableLogged = true; ((BaseUnityPlugin)this).Logger.LogDebug((object)("H3VR object data is not ready: " + ex)); } return false; } } private unsafe IEnumerator CaptureRuntimeMetadata(Dictionary<string, FVRObject> objects, Func<FVRObject, bool> include, Action<RuntimeMetadataCapture> complete) { Stopwatch timer = Stopwatch.StartNew(); List<RuntimeMetadataEntry> entries = new List<RuntimeMetadataEntry>(); List<FVRObject> snapshot; try { snapshot = objects.Values.Where((FVRObject val2) => (Object)(object)val2 != (Object)null).ToList(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogDebug((object)("Could not snapshot H3VR object data for GunGame: " + ex)); complete(null); yield break; } long timestamp = Stopwatch.GetTimestamp(); for (int index = 0; index < snapshot.Count; index++) { FVRObject val = snapshot[index]; if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(val.ItemID) && (include == null || include(val))) { string text = ((object)Unsafe.As<ObjectCategory, ObjectCategory>(ref val.Category)/*cast due to .constrained prefix*/).ToString(); int roundType = (int)val.RoundType; RuntimeMetadataEntry item = new RuntimeMetadataEntry { ObjectID = val.ItemID, Category = text, IsModContent = val.IsModContent, MagazineType = (int)val.MagazineType, ClipType = (int)val.ClipType, RoundType = roundType, CompatibleMagazines = ObjectIds(val.CompatibleMagazines), CompatibleClips = ObjectIds(val.CompatibleClips), CompatibleSpeedLoaders = ObjectIds(val.CompatibleSpeedLoaders), CompatibleSingleRounds = ObjectIds(val.CompatibleSingleRounds), BespokeAttachments = ObjectIds(val.BespokeAttachments), FirearmSize = ((object)Unsafe.As<OTagFirearmSize, OTagFirearmSize>(ref val.TagFirearmSize)/*cast due to .constrained prefix*/).ToString(), FirearmRoundPower = ((object)Unsafe.As<OTagFirearmRoundPower, OTagFirearmRoundPower>(ref val.TagFirearmRoundPower)/*cast due to .constrained prefix*/).ToString(), FirearmAction = ((object)Unsafe.As<OTagFirearmAction, OTagFirearmAction>(ref val.TagFirearmAction)/*cast due to .constrained prefix*/).ToString(), FirearmFeedOptions = ((val.TagFirearmFeedOption == null) ? new List<string>() : val.TagFirearmFeedOption.Select((OTagFirearmFeedOption option) => ((object)(*(OTagFirearmFeedOption*)(&option))/*cast due to .constrained prefix*/).ToString()).ToList()), FirearmMounts = ((val.TagFirearmMounts == null) ? new List<string>() : val.TagFirearmMounts.Select((OTagFirearmMount mount) => ((object)(*(OTagFirearmMount*)(&mount))/*cast due to .constrained prefix*/).ToString()).ToList()), AttachmentMount = ((object)Unsafe.As<OTagFirearmMount, OTagFirearmMount>(ref val.TagAttachmentMount)/*cast due to .constrained prefix*/).ToString(), AttachmentFeature = ((object)Unsafe.As<OTagAttachmentFeature, OTagAttachmentFeature>(ref val.TagAttachmentFeature)/*cast due to .constrained prefix*/).ToString(), OpticKind = CatalogOpticKind(val, text), PhysicalMountTypes = CatalogPhysicalMountTypes(val, text), ProvidedMountTypes = new List<string>(), IsGunGameRoundDisplaySupported = (text != "Firearm" || HasGunGameRoundDisplayData(roundType)), IsVerifiedFirearmPrefab = (text != "Firearm" || HasCatalogFirearmProof(val)) }; entries.Add(item); if (HasExceededCaptureBudget(timestamp)) { yield return null; timestamp = Stopwatch.GetTimestamp(); } } } entries.Sort((RuntimeMetadataEntry left, RuntimeMetadataEntry right) => string.CompareOrdinal(left.ObjectID, right.ObjectID)); complete(new RuntimeMetadataCapture(entries, timer.ElapsedMilliseconds)); } private static bool HasGunGameRoundDisplayData(int roundType) { try { return AM.SRoundDisplayDataDic != null && AM.SRoundDisplayDataDic.ContainsKey((FireArmRoundType)roundType); } catch { return false; } } private static bool HasExceededCaptureBudget(long frameStart) { return (Stopwatch.GetTimestamp() - frameStart) * 1000 >= Stopwatch.Frequency * 2; } private static List<string> ObjectIds(IEnumerable<FVRObject> objects) { if (objects == null) { return new List<string>(); } return (from item in objects where (Object)(object)item != (Object)null && !string.IsNullOrEmpty(item.ItemID) select item.ItemID).Distinct().OrderBy<string, string>((string item) => item, StringComparer.Ordinal).ToList(); } private static bool HasCatalogFirearmIdentity(FVRObject item) { if ((Object)(object)item != (Object)null && ((object)Unsafe.As<OTagFirearmSize, OTagFirearmSize>(ref item.TagFirearmSize)/*cast due to .constrained prefix*/).ToString() != "None") { return ((object)Unsafe.As<OTagFirearmRoundPower, OTagFirearmRoundPower>(ref item.TagFirearmRoundPower)/*cast due to .constrained prefix*/).ToString() != "None"; } return false; } private static bool HasCatalogFirearmProof(FVRObject item) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) if (HasCatalogFirearmIdentity(item)) { return true; } if ((Object)(object)item == (Object)null) { return false; } if (HasDeclaredCompatibleFeed(item) || (int)item.MagazineType != 0 || (int)item.ClipType != 0) { return true; } return string.Equals(item.ItemID, "GravitonBeamer", StringComparison.OrdinalIgnoreCase); } private static bool HasDeclaredCompatibleFeed(FVRObject item) { if (!HasObjectId(item.CompatibleMagazines) && !HasObjectId(item.CompatibleClips) && !HasObjectId(item.CompatibleSpeedLoaders)) { return HasObjectId(item.CompatibleSingleRounds); } return true; } private static bool HasObjectId(IEnumerable<FVRObject> objects) { return objects?.Any((FVRObject candidate) => (Object)(object)candidate != (Object)null && !string.IsNullOrEmpty(candidate.ItemID)) ?? false; } private static string CatalogOpticKind(FVRObject item, string category) { if (!(category == "Attachment")) { return string.Empty; } return PipScopeOpticClassifier.ClassifyFromMetadata(item.ItemID, ((object)Unsafe.As<OTagAttachmentFeature, OTagAttachmentFeature>(ref item.TagAttachmentFeature)/*cast due to .constrained prefix*/).ToString()); } private unsafe static List<string> CatalogPhysicalMountTypes(FVRObject item, string category) { IEnumerable<string> source = ((category == "Firearm") ? ((item.TagFirearmMounts == null) ? Enumerable.Empty<string>() : item.TagFirearmMounts.Select((OTagFirearmMount mount) => ((object)(*(OTagFirearmMount*)(&mount))/*cast due to .constrained prefix*/).ToString())) : ((!(category == "Attachment")) ? Enumerable.Empty<string>() : new string[1] { ((object)Unsafe.As<OTagFirearmMount, OTagFirearmMount>(ref item.TagAttachmentMount)/*cast due to .constrained prefix*/).ToString() })); return source.Where((string mount) => !string.IsNullOrEmpty(mount) && mount != "None").Distinct<string>(StringComparer.Ordinal).OrderBy<string, string>((string mount) => mount, StringComparer.Ordinal) .ToList(); } private IEnumerator CaptureEnemyEntries(Action<RuntimeEnemyCapture> complete) { Stopwatch timer = Stopwatch.StartNew(); List<RuntimeEnemyEntry> entries = new List<RuntimeEnemyEntry>(); IM val = ManagerSingleton<IM>.Instance; if ((Object)(object)val == (Object)null || val.odicSosigObjsByID == null) { complete(new RuntimeEnemyCapture(entries, timer.ElapsedMilliseconds)); yield break; } List<KeyValuePair<SosigEnemyID, SosigEnemyTemplate>> list = val.odicSosigObjsByID.ToList(); long timestamp = Stopwatch.GetTimestamp(); foreach (KeyValuePair<SosigEnemyID, SosigEnemyTemplate> item in list) { SosigEnemyTemplate value = item.Value; if (!((Object)(object)value == (Object)null) && (int)item.Key != 0) { bool flag = Enum.IsDefined(typeof(SosigEnemyID), item.Key); string text = (flag ? ((object)item.Key/*cast due to .constrained prefix*/).ToString() : Convert.ToInt32(item.Key).ToString()); int num = EnemyHealthScore(value); int num2 = EnemyArmorScore(value); int num3 = EnemyWeaponThreatScore(value); int num4 = EnemySpecialThreatScore(value); entries.Add(new RuntimeEnemyEntry { EnemyNameString = text, DisplayName = (string.IsNullOrEmpty(value.DisplayName) ? text : value.DisplayName), IsModContent = !flag, IsSpawnable = true, HealthScore = num, ArmorScore = num2, WeaponThreatScore = num3, SpecialThreatScore = num4, DifficultyScore = Math.Max(1, num + num2 + num3 + num4) }); if (HasExceededCaptureBudget(timestamp)) { yield return null; timestamp = Stopwatch.GetTimestamp(); } } } complete(new RuntimeEnemyCapture(entries.OrderBy((RuntimeEnemyEntry entry) => entry.DifficultyScore).ThenBy<RuntimeEnemyEntry, string>((RuntimeEnemyEntry entry) => entry.EnemyNameString, StringComparer.Ordinal).ToList(), timer.ElapsedMilliseconds)); } private static int EnemyHealthScore(SosigEnemyTemplate template) { List<SosigConfigTemplate> list = EnemyConfigs(template).ToList(); if (list.Count == 0) { return 1; } return Math.Max(1, (int)Math.Round(list.Average((SosigConfigTemplate config) => config.TotalMustard) / 25f)); } private static int EnemyArmorScore(SosigEnemyTemplate template) { float num = 0f; foreach (SosigOutfitConfig item in template.OutfitConfig ?? new List<SosigOutfitConfig>()) { num += OutfitSlotScore(item.Headwear, item.Chance_Headwear); num += OutfitSlotScore(item.Eyewear, item.Chance_Eyewear) * 0.5f; num += OutfitSlotScore(item.Facewear, item.Chance_Facewear) * 0.75f; num += OutfitSlotScore(item.Torsowear, item.Chance_Torsowear) * 1.5f; num += OutfitSlotScore(item.Pantswear, item.Chance_Pantswear) * 0.5f; } return (int)Math.Round(num * 3f); } private static float OutfitSlotScore(List<FVRObject> items, float chance) { if (items == null || items.Count == 0) { return 0f; } if (!(chance > 0f)) { return 1f; } return chance; } private static int EnemyWeaponThreatScore(SosigEnemyTemplate template) { List<FVRObject> list = new List<FVRObject>(); list.AddRange(template.WeaponOptions ?? new List<FVRObject>()); list.AddRange(template.WeaponOptions_Secondary ?? new List<FVRObject>()); list.AddRange(template.WeaponOptions_Tertiary ?? new List<FVRObject>()); int num = list.Where((FVRObject weapon) => (Object)(object)weapon != (Object)null).Select(WeaponThreatScore).DefaultIfEmpty(0) .Max(); int num2 = Math.Min(10, list.Count((FVRObject weapon) => (Object)(object)weapon != (Object)null)); return num + num2 + (int)Math.Round((template.SecondaryChance + template.TertiaryChance) * 4f); } private static int WeaponThreatScore(FVRObject weapon) { if (((object)Unsafe.As<ObjectCategory, ObjectCategory>(ref weapon.Category)/*cast due to .constrained prefix*/).ToString() != "Firearm") { return 1; } return ((object)Unsafe.As<OTagFirearmRoundPower, OTagFirearmRoundPower>(ref weapon.TagFirearmRoundPower)/*cast due to .constrained prefix*/).ToString() switch { "Tiny" => 2, "Pistol" => 4, "Shotgun" => 6, "Intermediate" => 7, "FullPower" => 9, "AntiMaterial" => 12, "Ordnance" => 14, "Exotic" => 12, _ => 5, }; } private static int EnemySpecialThreatScore(SosigEnemyTemplate template) { float num = 0f; foreach (SosigConfigTemplate item in EnemyConfigs(template)) { num = Math.Max(num, Math.Max(0f, item.RunSpeed - 3f) * 2f); num = Math.Max(num, Math.Max(0f, item.ViewDistance - 150f) / 100f); num = Math.Max(num, HasBooleanProperty(item, "HasNightVision") ? 4f : 0f); num = Math.Max(num, item.AppliesDamageResistToIntegrityLoss ? 3f : 0f); num = Math.Max(num, (!item.CanBeGrabbed || !item.CanBeSevered || !item.CanBeStabbed) ? 3f : 0f); } return (int)Math.Ceiling(num); } private static IEnumerable<SosigConfigTemplate> EnemyConfigs(SosigEnemyTemplate template) { return from config in (template.ConfigTemplates ?? new List<SosigConfigTemplate>()).Concat(template.ConfigTemplates_Easy ?? new List<SosigConfigTemplate>()) where (Object)(object)config != (Object)null select config; } private static bool HasBooleanProperty(object value, string propertyName) { PropertyInfo property = value.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public); if ((object)property != null && (object)property.PropertyType == typeof(bool)) { return (bool)property.GetValue(value, null); } return false; } private static string RuntimePoolFileName(RuntimeWeaponPool pool) { return "GunGameWeaponPool_Runtime_" + pool.Family + "_" + pool.EnemyType + ".json"; } private static void RemoveStaleRuntimePools(string runtimePoolsPath, HashSet<string> expectedPoolFiles, Func<string, bool> isOwnedByPhase) { string[] files = Directory.GetFiles(runtimePoolsPath, "GunGameWeaponPool_Runtime_*.json"); foreach (string path in files) { string fileName = Path.GetFileName(path); if (isOwnedByPhase(fileName) && !expectedPoolFiles.Contains(fileName)) { File.Delete(path); } } } private static void WriteTextAtomically(string outputPath, string contents) { string text = outputPath + "." + Guid.NewGuid().ToString("N") + ".tmp"; File.WriteAllText(text, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); try { if (File.Exists(outputPath)) { File.Replace(text, outputPath, null); } else { File.Move(text, outputPath); } } catch (PlatformNotSupportedException) { File.Copy(text, outputPath, overwrite: true); File.Delete(text); } } private static string SerializeMetadata(List<RuntimeMetadataEntry> entries) { StringBuilder stringBuilder = new StringBuilder(entries.Count * 256); stringBuilder.Append("[\n"); for (int i = 0; i < entries.Count; i++) { RuntimeMetadataEntry runtimeMetadataEntry = entries[i]; stringBuilder.Append(" {\"ObjectID\":\""); AppendJsonString(stringBuilder, runtimeMetadataEntry.ObjectID); stringBuilder.Append("\",\"Category\":\""); AppendJsonString(stringBuilder, runtimeMetadataEntry.Category); stringBuilder.Append("\",\"IsModContent\":"); stringBuilder.Append(runtimeMetadataEntry.IsModContent ? "true" : "false"); stringBuilder.Append(",\"MagazineType\":"); stringBuilder.Append(runtimeMetadataEntry.MagazineType); stringBuilder.Append(",\"ClipType\":"); stringBuilder.Append(runtimeMetadataEntry.ClipType); stringBuilder.Append(",\"RoundType\":"); stringBuilder.Append(runtimeMetadataEntry.RoundType); stringBuilder.Append(",\"IsGunGameRoundDisplaySupported\":"); stringBuilder.Append(runtimeMetadataEntry.IsGunGameRoundDisplaySupported ? "true" : "false"); stringBuilder.Append(",\"IsVerifiedFirearmPrefab\":"); stringBuilder.Append(runtimeMetadataEntry.IsVerifiedFirearmPrefab ? "true" : "false"); AppendJsonNamedStringArray(stringBuilder, "CompatibleMagazines", runtimeMetadataEntry.CompatibleMagazines); AppendJsonNamedStringArray(stringBuilder, "CompatibleClips", runtimeMetadataEntry.CompatibleClips); AppendJsonNamedStringArray(stringBuilder, "CompatibleSpeedLoaders", runtimeMetadataEntry.CompatibleSpeedLoaders); AppendJsonNamedStringArray(stringBuilder, "CompatibleSingleRounds", runtimeMetadataEntry.CompatibleSingleRounds); AppendJsonNamedStringArray(stringBuilder, "BespokeAttachments", runtimeMetadataEntry.BespokeAttachments); AppendJsonNamedString(stringBuilder, "FirearmSize", runtimeMetadataEntry.FirearmSize); AppendJsonNamedString(stringBuilder, "FirearmRoundPower", runtimeMetadataEntry.FirearmRoundPower); AppendJsonNamedString(stringBuilder, "FirearmAction", runtimeMetadataEntry.FirearmAction); AppendJsonNamedStringArray(stringBuilder, "FirearmFeedOptions", runtimeMetadataEntry.FirearmFeedOptions); AppendJsonNamedStringArray(stringBuilder, "FirearmMounts", runtimeMetadataEntry.FirearmMounts); AppendJsonNamedString(stringBuilder, "AttachmentMount", runtimeMetadataEntry.AttachmentMount); AppendJsonNamedString(stringBuilder, "AttachmentFeature", runtimeMetadataEntry.AttachmentFeature); AppendJsonNamedString(stringBuilder, "OpticKind", runtimeMetadataEntry.OpticKind); AppendJsonNamedStringArray(stringBuilder, "PhysicalMountTypes", runtimeMetadataEntry.PhysicalMountTypes); AppendJsonNamedStringArray(stringBuilder, "ProvidedMountTypes", runtimeMetadataEntry.ProvidedMountTypes); stringBuilder.Append(",\"OpticMinMagnification\":"); stringBuilder.Append(runtimeMetadataEntry.OpticMinMagnification.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append(",\"OpticMaxMagnification\":"); stringBuilder.Append(runtimeMetadataEntry.OpticMaxMagnification.ToString(CultureInfo.InvariantCulture)); stringBuilder.Append(",\"IsVariableMagnification\":"); stringBuilder.Append(runtimeMetadataEntry.IsVariableMagnification ? "true" : "false"); stringBuilder.Append('}'); if (i < entries.Count - 1) { stringBuilder.Append(','); } stringBuilder.Append('\n'); } stringBuilder.Append(']'); return stringBuilder.ToString(); } private static string SerializePool(RuntimeWeaponPool pool) { StringBuilder stringBuilder = new StringBuilder(pool.Guns.Count * 128); stringBuilder.Append("{\n \"WeaponPoolType\": \"Advanced\",\n \"Description\": \""); AppendJsonString(stringBuilder, pool.Description); stringBuilder.Append("\",\n \"EnemyProgressionType\": "); stringBuilder.Append(pool.EnemyProgressionType); stringBuilder.Append(",\n \"Enemies\": ["); for (int i = 0; i < pool.Enemies.Count; i++) { if (i > 0) { stringBuilder.Append(','); } stringBuilder.Append("{\"EnemyName\":0,\"EnemyNameString\":\""); AppendJsonString(stringBuilder, pool.Enemies[i].EnemyNameString); stringBuilder.Append("\",\"Value\":"); stringBuilder.Append(pool.Enemies[i].Value); stringBuilder.Append('}'); } stringBuilder.Append("],\n \"Guns\": ["); for (int j = 0; j < pool.Guns.Count; j++) { if (j > 0) { stringBuilder.Append(','); } RuntimeGun runtimeGun = pool.Guns[j]; stringBuilder.Append("{\"GunName\":\""); AppendJsonString(stringBuilder, runtimeGun.GunName); stringBuilder.Append("\",\"MagName\":\""); AppendJsonString(stringBuilder, runtimeGun.MagName); stringBuilder.Append("\",\"MagNames\":"); AppendJsonStringArray(stringBuilder, runtimeGun.MagNames); stringBuilder.Append(",\"CategoryID\":"); stringBuilder.Append(runtimeGun.CategoryID); stringBuilder.Append(",\"Extra\":\""); AppendJsonString(stringBuilder, runtimeGun.Extra); stringBuilder.Append("\"}"); } stringBuilder.Append("],\n \"Name\": \""); AppendJsonString(stringBuilder, pool.Name); stringBuilder.Append("\",\n \"OrderType\": "); stringBuilder.Append(pool.OrderType); stringBuilder.Append("\n}"); return stringBuilder.ToString(); } private static string SerializeReceipt(List<RuntimeMetadataEntry> entries, List<RuntimeEnemyEntry> enemies, RuntimeGenerationResult result, int randomSeed, string phase, string contentFingerprint) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{\n \"generatedAtUtc\": \""); AppendJsonString(stringBuilder, DateTime.UtcNow.ToString("o")); stringBuilder.Append("\",\n \"randomSeed\": "); stringBuilder.Append(randomSeed); stringBuilder.Append(",\n \"phase\": \""); AppendJsonString(stringBuilder, phase); stringBuilder.Append('"'); stringBuilder.Append(",\n \"contentFingerprint\": \""); AppendJsonString(stringBuilder, contentFingerprint); stringBuilder.Append('"'); stringBuilder.Append(",\n \"generationPolicyVersion\": \""); AppendJsonString(stringBuilder, "21"); stringBuilder.Append('"'); stringBuilder.Append(",\n \"activeItems\": "); stringBuilder.Append(entries.Count); stringBuilder.Append(",\n \"activeModdedItems\": "); stringBuilder.Append(entries.Count((RuntimeMetadataEntry entry) => entry.IsModContent)); stringBuilder.Append(",\n \"activeSosigTypes\": "); stringBuilder.Append(enemies.Count); stringBuilder.Append(",\n \"activeModdedSosigTypes\": "); stringBuilder.Append(enemies.Count((RuntimeEnemyEntry enemy) => enemy.IsModContent)); stringBuilder.Append(",\n \"eligibleWeaponsPerPool\": "); stringBuilder.Append((result.Pools.Count != 0) ? result.Pools[0].Guns.Count : 0); stringBuilder.Append(",\n \"skippedFirearms\": "); AppendJsonStringArray(stringBuilder, result.SkippedFirearms); stringBuilder.Append(",\n \"firearmsWithoutOptics\": "); AppendJsonStringArray(stringBuilder, result.FirearmsWithoutOptics); stringBuilder.Append("\n}"); return stringBuilder.ToString(); } private static string SerializeEnemyCatalog(List<RuntimeEnemyEntry> enemies) { StringBuilder stringBuilder = new StringBuilder(enemies.Count * 192); stringBuilder.Append("[\n"); for (int i = 0; i < enemies.Count; i++) { RuntimeEnemyEntry runtimeEnemyEntry = enemies[i]; stringBuilder.Append(" {\"EnemyNameString\":\""); AppendJsonString(stringBuilder, runtimeEnemyEntry.EnemyNameString); stringBuilder.Append("\",\"DisplayName\":\""); AppendJsonString(stringBuilder, runtimeEnemyEntry.DisplayName); stringBuilder.Append("\",\"IsModContent\":"); stringBuilder.Append(runtimeEnemyEntry.IsModContent ? "true" : "false"); stringBuilder.Append(",\"IsSpawnable\":"); stringBuilder.Append(runtimeEnemyEntry.IsSpawnable ? "true" : "false"); stringBuilder.Append(",\"DifficultyScore\":"); stringBuilder.Append(runtimeEnemyEntry.DifficultyScore); stringBuilder.Append(",\"HealthScore\":"); stringBuilder.Append(runtimeEnemyEntry.HealthScore); stringBuilder.Append(",\"ArmorScore\":"); stringBuilder.Append(runtimeEnemyEntry.ArmorScore); stringBuilder.Append(",\"WeaponThreatScore\":"); stringBuilder.Append(runtimeEnemyEntry.WeaponThreatScore); stringBuilder.Append(",\"SpecialThreatScore\":"); stringBuilder.Append(runtimeEnemyEntry.SpecialThreatScore); stringBuilder.Append('}'); if (i < enemies.Count - 1) { stringBuilder.Append(','); } stringBuilder.Append('\n'); } stringBuilder.Append(']'); return stringBuilder.ToString(); } private static void AppendJsonNamedString(StringBuilder json, string name, string value) { json.Append(",\""); AppendJsonString(json, name); json.Append("\":\""); AppendJsonString(json, value ?? string.Empty); json.Append('"'); } private static void AppendJsonNamedStringArray(StringBuilder json, string name, List<string> values) { json.Append(",\""); AppendJsonString(json, name); json.Append("\":"); AppendJsonStringArray(json, values); } private static void AppendJsonStringArray(StringBuilder json, List<string> values) { values = values ?? new List<string>(); json.Append('['); for (int i = 0; i < values.Count; i++) { if (i > 0) { json.Append(','); } json.Append('"'); AppendJsonString(json, values[i]); json.Append('"'); } json.Append(']'); } private static void AppendJsonString(StringBuilder json, string value) { string text = value ?? string.Empty; foreach (char c in text) { switch (c) { case '\\': json.Append("\\\\"); continue; case '"': json.Append("\\\""); continue; case '\n': json.Append("\\n"); continue; case '\r': json.Append("\\r"); continue; case '\t': json.Append("\\t"); continue; } if (c < ' ') { json.Append("\\u"); int num = c; json.Append(num.ToString("x4")); } else { json.Append(c); } } } private static RuntimeGenerationReport GenerateRuntimeFiles(string packagePath, List<RuntimeMetadataEntry> entries, List<RuntimeEnemyEntry> enemyEntries, RuntimeGenerationPhase phase, bool confirmedEmptySnapshot, bool allowPolicyReplacement) { ProfileRules rules = ProfileRules.Load(packagePath); List<RuntimeMetadataEntry> list = ((phase == RuntimeGenerationPhase.Vanilla) ? entries.Where((RuntimeMetadataEntry entry) => !rules.IsGloballyBlacklisted(entry)).ToList() : entries.Where((RuntimeMetadataEntry entry) => !rules.IsBlacklisted(entry)).ToList()); string text = phase.ToString().ToLowerInvariant(); string text2 = ((phase == RuntimeGenerationPhase.CompatibilityProbe) ? RuntimePoolPersistence.CreateFingerprint(list, enemyEntries, rules.CompatibilityProbeFirearms) : RuntimePoolPersistence.CreateFingerprint(list, enemyEntries)); int num = RuntimePoolPersistence.CreateStableSeed(text2); RuntimeGenerationResult runtimeGenerationResult = ((phase == RuntimeGenerationPhase.CompatibilityProbe) ? RuntimeProfileBuilder.BuildCompatibilityProbe(list, enemyEntries, rules.CompatibilityProbeFirearms, new Random(num)) : RuntimeProfileBuilder.BuildWithDiagnostics(list, enemyEntries, new Random(num))); List<RuntimeWeaponPool> list2 = runtimeGenerationResult.Pools.Where((RuntimeWeaponPool pool) => (phase != RuntimeGenerationPhase.Vanilla) ? ((phase != RuntimeGenerationPhase.Modded) ? RuntimeProfileFamily.IsCompatibilityProbe(pool.Family) : RuntimeProfileFamily.IsModded(pool.Family)) : RuntimeProfileFamily.IsVanilla(pool.Family)).ToList(); RuntimeGenerationResult result = new RuntimeGenerationResult { Pools = list2, SkippedFirearms = runtimeGenerationResult.SkippedFirearms, FirearmsWithoutOptics = runtimeGenerationResult.FirearmsWithoutOptics }; int eligibleWeaponsPerPool = ((list2.Count != 0) ? list2[0].Guns.Count : 0); string text3 = Path.Combine(packagePath, "RuntimePools"); string text4 = Path.Combine(text3, "runtime-generation-" + text + "-receipt.json"); if (phase == RuntimeGenerationPhase.Modded && !RuntimePoolPersistence.ShouldPromoteModdedCandidate(list2.Count, eligibleWeaponsPerPool, RuntimePoolPersistence.ReadEligibleWeaponsPerPool(text4), RuntimePoolPersistence.HasCompleteModdedPoolFiles(packagePath), confirmedEmptySnapshot, !string.Equals(RuntimePoolPersistence.ReadGenerationPolicyVersion(text4), "21", StringComparison.Ordinal), allowPolicyReplacement)) { return new RuntimeGenerationReport(list.Count, list.Count((RuntimeMetadataEntry entry) => entry.IsModContent), enemyEntries.Count, list2.Count, eligibleWeaponsPerPool, runtimeGenerationResult.SkippedFirearms.Count, new List<string>(), wasWritten: false); } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (RuntimeWeaponPool item2 in list2) { string item = RuntimePoolFileName(item2); hashSet.Add(item); } Func<string, bool> isOwnedByPhase = ((phase == RuntimeGenerationPhase.Vanilla) ? new Func<string, bool>(RuntimeProfileFamily.IsVanillaPoolFile) : ((phase == RuntimeGenerationPhase.Modded) ? new Func<string, bool>(RuntimeProfileFamily.IsModdedPoolFile) : new Func<string, bool>(RuntimeProfileFamily.IsCompatibilityProbePoolFile))); string storedFingerprint = RuntimePoolPersistence.ReadFingerprint(text4); bool poolFilesMatch = RuntimePoolPersistence.HasExpectedPoolFiles(packagePath, hashSet, isOwnedByPhase); if (!RuntimePoolPersistence.ShouldWrite(storedFingerprint, text2, poolFilesMatch)) { return new RuntimeGenerationReport(list.Count, list.Count((RuntimeMetadataEntry entry) => entry.IsModContent), enemyEntries.Count, list2.Count, eligibleWeaponsPerPool, runtimeGenerationResult.SkippedFirearms.Count, hashSet.OrderBy<string, string>((string fileName) => fileName, StringComparer.Ordinal).ToList(), wasWritten: false); } if (phase != RuntimeGenerationPhase.CompatibilityProbe) { WriteTextAtomically(Path.Combine(packagePath, "ObjectData.json"), SerializeMetadata(list)); } Directory.CreateDirectory(text3); foreach (RuntimeWeaponPool item3 in list2) { WriteTextAtomically(Path.Combine(packagePath, RuntimePoolFileName(item3)), SerializePool(item3)); } RemoveStaleRuntimePools(packagePath, hashSet, isOwnedByPhase); WriteTextAtomically(text4, SerializeReceipt(list, enemyEntries, result, num, text, text2)); WriteTextAtomically(Path.Combine(text3, "enemy-catalog.json"), SerializeEnemyCatalog(enemyEntries)); return new RuntimeGenerationReport(list.Count, list.Count((RuntimeMetadataEntry entry) => entry.IsModContent), enemyEntries.Count, list2.Count, eligibleWeaponsPerPool, runtimeGenerationResult.SkippedFirearms.Count, hashSet.OrderBy<string, string>((string fileName) => fileName, StringComparer.Ordinal).ToList(), wasWritten: true); } private static List<RuntimeMetadataEntry> MergeRuntimeMetadata(IEnumerable<RuntimeMetadataEntry> first, IEnumerable<RuntimeMetadataEntry> second) { return (from @group in (from entry in (first ?? Enumerable.Empty<RuntimeMetadataEntry>()).Concat(second ?? Enumerable.Empty<RuntimeMetadataEntry>()) where entry != null && !string.IsNullOrEmpty(entry.ObjectID) select entry).GroupBy<RuntimeMetadataEntry, string>((RuntimeMetadataEntry entry) => entry.ObjectID, StringComparer.Ordinal) select @group.First()).OrderBy<RuntimeMetadataEntry, string>((RuntimeMetadataEntry entry) => entry.ObjectID, StringComparer.Ordinal).ToList(); } } public sealed class ProfileRules { public string[] FirearmBlacklist { get; set; } public string[] RuntimeFirearmBlacklist { get; set; } public string[] FeedBlacklist { get; set; } public string[] CompatibilityProbeFirearms { get; set; } public static ProfileRules Load(string packageDirectory) { string text = Path.Combine(packageDirectory, "profile-rules.json"); if (!File.Exists(text)) { throw new FileNotFoundException("GunGame profile rules are missing.", text); } string json = File.ReadAllText(text); string[] array = ReadStringArray(json, "firearmBlacklist"); string[] array2 = ReadStringArray(json, "runtimeFirearmBlacklist"); return new ProfileRules { FirearmBlacklist = array, RuntimeFirearmBlacklist = ((array2.Length == 0) ? array : array2), FeedBlacklist = ReadStringArray(json, "feedBlacklist"), CompatibilityProbeFirearms = ReadStringArray(json, "compatibilityProbeFirearms") }; } public bool IsBlacklisted(RuntimeMetadataEntry entry) { if (!IsGloballyBlacklisted(entry)) { if (entry.Category == "Firearm") { return Contains(RuntimeFirearmBlacklist, entry.ObjectID); } return false; } return true; } public bool IsGloballyBlacklisted(RuntimeMetadataEntry entry) { if (!(entry.Category == "Firearm")) { return Contains(FeedBlacklist, entry.ObjectID); } return Contains(FirearmBlacklist, entry.ObjectID); } private static bool Contains(IEnumerable<string> values, string value) { foreach (string value2 in values) { if (string.Equals(value2, value, StringComparison.Ordinal)) { return true; } } return false; } private static string[] ReadStringArray(string json, string propertyName) { string pattern = "\\\"" + Regex.Escape(propertyName) + "\\\"\\s*:\\s*\\[(?<items>.*?)\\]"; Match match = Regex.Match(json, pattern, RegexOptions.Singleline); if (!match.Success) { return new string[0]; } List<string> list = new List<string>(); foreach (Match item in Regex.Matches(match.Groups["items"].Value, "\\\"(?<value>(?:\\\\.|[^\\\"\\\\])*)\\\"")) { list.Add(item.Groups["value"].Value.Replace("\\\\\"", "\"").Replace("\\\\\\\\", "\\")); } return list.ToArray(); } } public static class RuntimeBuildFeatures { public static bool CompatibilityProbeEnabled => false; } public static class RuntimeItemRole { public static string Resolve(string declaredCategory, bool hasFirearm, bool hasMagazine, bool hasClip, bool hasSpeedloader, bool hasRound) { if (declaredCategory != "Firearm") { return declaredCategory; } if (hasFirearm) { return "Firearm"; } if (hasMagazine) { return "Magazine"; } if (hasClip) { return "Clip"; } if (hasSpeedloader) { return "SpeedLoader"; } if (hasRound) { return "Cartridge"; } return "Unknown"; } } public static class RuntimePoolPersistence { public const string CurrentGenerationPolicyVersion = "21"; private const int ExpectedModdedPoolCount = 2; public static string CreateFingerprint(IEnumerable<RuntimeMetadataEntry> entries, IEnumerable<RuntimeEnemyEntry> enemies) { return CreateFingerprint(entries, enemies, Enumerable.Empty<string>()); } public static string CreateFingerprint(IEnumerable<RuntimeMetadataEntry> entries, IEnumerable<RuntimeEnemyEntry> enemies, IEnumerable<string> phaseRules) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("generationPolicy|"); AppendValue(stringBuilder, "21"); foreach (string item in (phaseRules ?? Enumerable.Empty<string>()).Where((string rule) => !string.IsNullOrEmpty(rule)).Distinct<string>(StringComparer.Ordinal).OrderBy<string, string>((string rule) => rule, StringComparer.Ordinal)) { stringBuilder.Append("phaseRule|"); AppendValue(stringBuilder, item); } foreach (RuntimeMetadataEntry item2 in (entries ?? Enumerable.Empty<RuntimeMetadataEntry>()).Where((RuntimeMetadataEntry entry) => entry != null).OrderBy<RuntimeMetadataEntry, string>((RuntimeMetadataEntry entry) => entry.ObjectID ?? string.Empty, StringComparer.Ordinal)) { stringBuilder.Append("item|"); AppendEntry(stringBuilder, item2); } foreach (RuntimeEnemyEntry item3 in (enemies ?? Enumerable.Empty<RuntimeEnemyEntry>()).Where((RuntimeEnemyEntry enemy) => enemy != null).OrderBy<RuntimeEnemyEntry, string>((RuntimeEnemyEntry enemy) => enemy.EnemyNameString ?? string.Empty, StringComparer.Ordinal)) { stringBuilder.Append("enemy|"); AppendValue(stringBuilder, item3.EnemyNameString); AppendValue(stringBuilder, item3.DisplayName); AppendValue(stringBuilder, item3.IsModContent ? "1" : "0"); AppendValue(stringBuilder, item3.IsSpawnable ? "1" : "0"); AppendValue(stringBuilder, item3.DifficultyScore.ToString(CultureInfo.InvariantCulture)); AppendValue(stringBuilder, item3.HealthScore.ToString(CultureInfo.InvariantCulture)); AppendValue(stringBuilder, item3.ArmorScore.ToString(CultureInfo.InvariantCulture)); AppendValue(stringBuilder, item3.WeaponThreatScore.ToString(CultureInfo.InvariantCulture)); AppendValue(stringBuilder, item3.SpecialThreatScore.ToString(CultureInfo.InvariantCulture)); } using SHA256Managed sHA256Managed = new SHA256Managed(); StringBuilder stringBuilder2 = new StringBuilder(); byte[] array = sHA256Managed.ComputeHash(Encoding.UTF8.GetBytes(stringBuilder.ToString())); foreach (byte b in array) { stringBuilder2.Append(b.ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder2.ToString(); } public static int CreateStableSeed(string fingerprint) { int num = 17; string text = fingerprint ?? string.Empty; foreach (char c in text) { num = num * 31 + c; } return num & 0x7FFFFFFF; } public static bool ShouldWrite(string storedFingerprint, string candidateFingerprint, bool poolFilesMatch) { if (poolFilesMatch && !string.IsNullOrEmpty(storedFingerprint)) { return !string.Equals(storedFingerprint, candidateFingerprint, StringComparison.Ordinal); } return true; } public static bool ShouldPromoteModdedCandidate(int candidatePoolCount, int eligibleWeaponsPerPool, int? persistedEligibleWeaponsPerPool, bool hasPersistedPair, bool confirmedEmptySnapshot) { return ShouldPromoteModdedCandidate(candidatePoolCount, eligibleWeaponsPerPool, persistedEligibleWeaponsPerPool, hasPersistedPair, confirmedEmptySnapshot, generationPolicyChanged: false); } public static bool ShouldPromoteModdedCandidate(int candidatePoolCount, int eligibleWeaponsPerPool, int? persistedEligibleWeaponsPerPool, bool hasPersistedPair, bool confirmedEmptySnapshot, bool generationPolicyChanged) { return ShouldPromoteModdedCandidate(candidatePoolCount, eligibleWeaponsPerPool, persistedEligibleWeaponsPerPool, hasPersistedPair, confirmedEmptySnapshot, generationPolicyChanged, policyReplacementEligible: false); } public static bool ShouldPromoteModdedCandidate(int candidatePoolCount, int eligibleWeaponsPerPool, int? persistedEligibleWeaponsPerPool, bool hasPersistedPair, bool confirmedEmptySnapshot, bool generationPolicyChanged, bool policyReplacementEligible) { switch (candidatePoolCount) { case 0: return hasPersistedPair && confirmedEmptySnapshot; case 2: if (eligibleWeaponsPerPool > 0) { if (!hasPersistedPair) { return true; } if (generationPolicyChanged && policyReplacementEligible) { return true; } if (persistedEligibleWeaponsPerPool.HasValue) { return eligibleWeaponsPerPool > persistedEligibleWeaponsPerPool.Value; } return false; } goto default; default: return false; } } public static bool HasCompleteModdedPoolFiles(string packagePath) { if (string.IsNullOrEmpty(packagePath) || !Directory.Exists(packagePath)) { return false; } return Directory.GetFiles(packagePath, "GunGameWeaponPool_Runtime_*.json").Select(Path.GetFileName).Count(RuntimeProfileFamily.IsModdedPoolFile) == 2; } public static string ReadFingerprint(string receiptPath) { return ReadReceiptString(receiptPath, "contentFingerprint"); } public static string ReadGenerationPolicyVersion(string receiptPath) { return ReadReceiptString(receiptPath, "generationPolicyVersion"); } private static string ReadReceiptString(string receiptPath, string propertyName) { if (string.IsNullOrEmpty(receiptPath) || !File.Exists(receiptPath)) { return null; } string text = File.ReadAllText(receiptPath); string value = "\"" + propertyName + "\""; int num = text.IndexOf(value, StringComparison.Ordinal); if (num < 0) { return null; } int num2 = text.IndexOf(':', num); if (num2 < 0) { return null; } int num3 = text.IndexOf('"', num2 + 1); if (num3 < 0) { return null; } int num4 = text.IndexOf('"', num3 + 1); if (num4 >= 0) { return text.Substring(num3 + 1, num4 - num3 - 1); } return null; } public static int? ReadEligibleWeaponsPerPool(string receiptPath) { if (string.IsNullOrEmpty(receiptPath) || !File.Exists(receiptPath)) { return null; } string text = File.ReadAllText(receiptPath); int num = text.IndexOf("\"eligibleWeaponsPerPool\"", StringComparison.Ordinal); if (num < 0) { return null; } int num2 = text.IndexOf(':', num); if (num2 < 0) { return null; } int i; for (i = num2 + 1; i < text.Length && char.IsWhiteSpace(text[i]); i++) { } int j; for (j = i; j < text.Length && char.IsDigit(text[j]); j++) { } if (i == j) { return null; } if (!int.TryParse(text.Substring(i, j - i), NumberStyles.None, CultureInfo.InvariantCulture, out var result)) { return null; } return result; } public static bool HasExpectedPoolFiles(string packagePath, IEnumerable<string> expectedPoolFiles, Func<string, bool> isOwnedByPhase) { HashSet<string> hashSet = new HashSet<string>((expectedPoolFiles ?? Enumerable.Empty<string>()).Where((string fileName) => !string.IsNullOrEmpty(fileName)), StringComparer.Ordinal); if (!Directory.Exists(packagePath)) { return false; } HashSet<string> hashSet2 = new HashSet<string>(from fileName in Directory.GetFiles(packagePath, "GunGameWeaponPool_Runtime_*.json").Select(Path.GetFileName) where isOwnedByPhase(fileName) select fileName, StringComparer.Ordinal); return hashSet.SetEquals(hashSet2); } private static void AppendEntry(StringBuilder content, RuntimeMetadataEntry entry) { AppendValue(content, entry.ObjectID); AppendValue(content, entry.Category); AppendValue(content, entry.IsModContent ? "1" : "0"); AppendValue(content, entry.MagazineType.ToString(CultureInfo.InvariantCulture)); AppendValue(content, entry.ClipType.ToString(CultureInfo.InvariantCulture)); AppendValue(content, entry.RoundType.ToString(CultureInfo.InvariantCulture)); AppendList(content, entry.CompatibleMagazines); AppendList(content, entry.CompatibleClips); AppendList(content, entry.CompatibleSpeedLoaders); AppendList(content, entry.CompatibleSingleRounds); AppendList(content, entry.BespokeAttachments); AppendValue(content, entry.FirearmSize); AppendValue(content, entry.FirearmRoundPower); AppendValue(content, entry.FirearmAction); AppendList(content, entry.FirearmFeedOptions); AppendList(content, entry.FirearmMounts); AppendValue(content, entry.AttachmentMount); AppendValue(content, entry.AttachmentFeature); AppendValue(content, entry.OpticKind); AppendList(content, entry.PhysicalMountTypes); AppendList(content, entry.ProvidedMountTypes); AppendValue(content, entry.OpticMinMagnification.ToString("R", CultureInfo.InvariantCulture)); AppendValue(content, entry.OpticMaxMagnification.ToString("R", CultureInfo.InvariantCulture)); AppendValue(content, entry.IsVariableMagnification ? "1" : "0"); AppendValue(content, entry.IsGunGameRoundDisplaySupported ? "1" : "0"); AppendValue(content, entry.IsVerifiedFirearmPrefab ? "1" : "0"); } private static void AppendList(StringBuilder content, IEnumerable<string> values) { content.Append('['); foreach (string item in (values ?? Enumerable.Empty<string>()).OrderBy<string, string>((string value) => value ?? string.Empty, StringComparer.Ordinal)) { AppendValue(content, item); } content.Append(']'); } private static void AppendValue(StringBuilder content, string value) { value = value ?? string.Empty; content.Append(value.Length); content.Append(':'); content.Append(value); content.Append('|'); } } public sealed class RuntimeMetadataEntry { public string ObjectID { get; set; } public string Category { get; set; } public bool IsModContent { get; set; } public int MagazineType { get; set; } public int ClipType { get; set; } public int RoundType { get; set; } public List<string> CompatibleMagazines { get; set; } public List<string> CompatibleClips { get; set; } public List<string> CompatibleSpeedLoaders { get; set; } public List<string> CompatibleSingleRounds { get; set; } public List<string> BespokeAttachments { get; set; } public string FirearmSize { get; set; } public string FirearmRoundPower { get; set; } public string FirearmAction { get; set; } public List<string> FirearmFeedOptions { get; set; } public List<string> FirearmMounts { get; set; } public string AttachmentMount { get; set; } public string AttachmentFeature { get; set; } public string OpticKind { get; set; } public List<string> PhysicalMountTypes { get; set; } public List<string> ProvidedMountTypes { get; set; } public float OpticMinMagnification { get; set; } public float OpticMaxMagnification { get; set; } public bool IsVariableMagnification { get; set; } public bool IsGunGameRoundDisplaySupported { get; set; } = true; public bool IsVerifiedFirearmPrefab { get; set; } = true; } public sealed class RuntimeEnemyEntry { public string EnemyNameString { get; set; } public string DisplayName { get; set; } public bool IsModContent { get; set; } public bool IsSpawnable { get; set; } public int DifficultyScore { get; set; } public int HealthScore { get; set; } public int ArmorScore { get; set; } public int WeaponThreatScore { get; set; } public int SpecialThreatScore { get; set; } } public sealed class RuntimeGun { public string GunName { get; set; } public string MagName { get; set; } public List<string> MagNames { get; set; } public string Extra { get; set; } public int CategoryID { get; set; } } public sealed class RuntimeEnemy { public string EnemyNameString { get; set; } public int Value { get; set; } } public sealed class RuntimeWeaponPool { public string Name { get; set; } public string Description { get; set; } public int OrderType { get; set; } public string WeaponPoolType { get; set; } public int EnemyProgressionType { get; set; } public string Family { get; set; } public string EnemyType { get; set; } public List<RuntimeEnemy> Enemie