using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("CosmeticBoxConfig")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("CosmeticBoxConfig")] [assembly: AssemblyTitle("CosmeticBoxConfig")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.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 CosmeticBoxConfig { internal static class PluginInfo { public const string GUID = "cosmetic.box.config"; public const string NAME = "CosmeticBoxConfig"; public const string VERSION = "1.0.0"; } internal enum SpawnMode { Random, Fixed } internal readonly struct LevelSettings { public SpawnMode Mode { get; } public int BoxCount { get; } public int LoopMax { get; } public int CompensationLoopMax { get; } public LevelSettings(SpawnMode mode, int boxCount) { Mode = mode; BoxCount = Mathf.Clamp(boxCount, 0, 20); LoopMax = ((mode == SpawnMode.Fixed) ? GetTimeFormulaLoopMax(BoxCount) : 0); CompensationLoopMax = ((mode == SpawnMode.Fixed) ? GetCompensationLoopMax(BoxCount) : 0); } private static int GetTimeFormulaLoopMax(int boxCount) { if (boxCount > 0) { return boxCount - 1; } return -1; } private static int GetCompensationLoopMax(int boxCount) { if (boxCount > 0) { return Mathf.Max(boxCount - 1, boxCount * 3 - 1); } return -1; } } [BepInPlugin("cosmetic.box.config", "CosmeticBoxConfig", "1.0.0")] public class Plugin : BaseUnityPlugin { internal static ManualLogSource Log = null; private static ConfigEntry _mode = null; private static ConfigEntry _boxCount = null; private static readonly object SettingsLock = new object(); private static LevelSettings _pendingSettings = new LevelSettings(SpawnMode.Random, 1); private static LevelSettings _activeSettings = new LevelSettings(SpawnMode.Random, 1); private static int _actualBoxesThisLevel; private static bool _patchesEnabled; internal static LevelSettings ActiveSettings { get { lock (SettingsLock) { return _activeSettings; } } } internal static int ActualBoxesThisLevel { get { lock (SettingsLock) { return _actualBoxesThisLevel; } } } private void Awake() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; _mode = ((BaseUnityPlugin)this).Config.Bind("General", "Mode", SpawnMode.Random, new ConfigDescription("Spawn mode:\n Random - At least 1 actual cosmetic box when an original spawn volume is available; remaining rolls use vanilla probability.\n Fixed - Targets BoxCount actual cosmetic boxes when original spawn volumes are available, with compensation attempts.\nChanges apply on the next level load, not mid-level.", (AcceptableValueBase)null, Array.Empty())); _boxCount = ((BaseUnityPlugin)this).Config.Bind("General", "BoxCount", 1, new ConfigDescription("Fixed mode cosmetic boxes.\n 0 = 0 cosmetic boxes.\n 1-20 = target actual cosmetic box count, limited by original spawn-volume availability.\nIgnored in Random mode. Changes apply on the next level load.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 20), Array.Empty())); UpdatePendingSettings(); ActivatePendingSettings(); ((BaseUnityPlugin)this).Config.SettingChanged += delegate { UpdatePendingSettings(); }; _patchesEnabled = TryPatchAll(new Harmony("cosmetic.box.config")); Log.LogInfo((object)("CosmeticBoxConfig v1.0.0 loaded - patches " + (_patchesEnabled ? "enabled" : "disabled") + "; chance gate " + (PatchMethods.ChanceGatePatchActive ? "active" : "inactive") + "; active " + Describe(ActiveSettings))); } private static bool TryPatchAll(Harmony harmony) { //IL_01aa: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Expected O, but got Unknown //IL_01cc: Unknown result type (might be due to invalid IL or missing references) //IL_01d9: Expected O, but got Unknown //IL_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Expected O, but got Unknown //IL_0210: Unknown result type (might be due to invalid IL or missing references) //IL_021c: Expected O, but got Unknown //IL_022f: Unknown result type (might be due to invalid IL or missing references) //IL_0244: Unknown result type (might be due to invalid IL or missing references) //IL_0251: Expected O, but got Unknown //IL_0251: Expected O, but got Unknown try { MethodInfo methodInfo = AccessTools.Method(typeof(ValuableDirector), "SetupHost", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(ValuableDirector), "CosmeticWorldObjectLevelLoopsGet", (Type[])null, (Type[])null); MethodInfo methodInfo3 = AccessTools.Method(typeof(ValuableDirector), "CosmeticWorldObjectLevelLoopsClampedGet", (Type[])null, (Type[])null); MethodInfo methodInfo4 = AccessTools.Method(typeof(ValuableDirector), "SpawnCosmeticWorldObject", (Type[])null, (Type[])null); MethodInfo iteratorMoveNext = GetIteratorMoveNext(methodInfo); FieldInfo fieldInfo = AccessTools.Field(typeof(ValuableDirector), "cosmeticWorldObjectTargetAmount"); if (methodInfo == null || iteratorMoveNext == null || methodInfo2 == null || methodInfo3 == null || methodInfo4 == null || fieldInfo == null) { Log.LogError((object)("Required game methods were not found; CosmeticBoxConfig will not patch anything. SetupHost=" + ((methodInfo != null) ? "ok" : "missing") + ", SetupHost.MoveNext=" + ((iteratorMoveNext != null) ? "ok" : "missing") + ", CosmeticWorldObjectLevelLoopsGet=" + ((methodInfo2 != null) ? "ok" : "missing") + ", CosmeticWorldObjectLevelLoopsClampedGet=" + ((methodInfo3 != null) ? "ok" : "missing") + ", SpawnCosmeticWorldObject=" + ((methodInfo4 != null) ? "ok" : "missing") + ", cosmeticWorldObjectTargetAmount=" + ((fieldInfo != null) ? "ok" : "missing"))); return false; } harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(PatchMethods), "SetupHostPrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(PatchMethods), "LoopsGetPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, new HarmonyMethod(typeof(PatchMethods), "LoopsClampedGetPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)iteratorMoveNext, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(PatchMethods), "SetupHostMoveNextTranspiler", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); harmony.Patch((MethodBase)methodInfo4, new HarmonyMethod(typeof(PatchMethods), "SpawnCosmeticWorldObjectPrefix", (Type[])null), new HarmonyMethod(typeof(PatchMethods), "SpawnCosmeticWorldObjectPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); return true; } catch (Exception arg) { Log.LogError((object)$"Failed to register CosmeticBoxConfig patches; attempting to roll back any partial patches. {arg}"); try { harmony.UnpatchSelf(); Log.LogInfo((object)"Rolled back CosmeticBoxConfig patches after registration failure."); } catch (Exception arg2) { Log.LogError((object)$"Failed to roll back partial CosmeticBoxConfig patches. {arg2}"); } return false; } } private static MethodInfo? GetIteratorMoveNext(MethodInfo? method) { Type type = method?.GetCustomAttribute()?.StateMachineType; if (!(type == null)) { return AccessTools.Method(type, "MoveNext", (Type[])null, (Type[])null); } return null; } internal static void ActivatePendingSettingsForLevel() { lock (SettingsLock) { _activeSettings = _pendingSettings; _actualBoxesThisLevel = 0; } Log.LogInfo((object)("CosmeticBoxConfig level settings active: " + Describe(ActiveSettings))); } internal static void ActivatePendingSettings() { lock (SettingsLock) { _activeSettings = _pendingSettings; } Log.LogInfo((object)("CosmeticBoxConfig settings active: " + Describe(ActiveSettings))); } internal static void RecordActualBoxSpawned() { lock (SettingsLock) { _actualBoxesThisLevel++; } } internal static bool NeedsGuaranteedBox() { TryGetGuaranteeState(out var settings, out var actual); if (settings.Mode != SpawnMode.Fixed) { return actual < 1; } return actual < settings.BoxCount; } internal static bool TryGetGuaranteeState(out LevelSettings settings, out int actual) { lock (SettingsLock) { settings = _activeSettings; actual = _actualBoxesThisLevel; } if (settings.Mode != SpawnMode.Fixed) { return true; } return settings.BoxCount > 0; } private static void UpdatePendingSettings() { LevelSettings levelSettings = ReadSettingsFromConfig(); lock (SettingsLock) { _pendingSettings = levelSettings; } Log.LogInfo((object)("CosmeticBoxConfig config queued for next level: " + Describe(levelSettings))); } private static LevelSettings ReadSettingsFromConfig() { return new LevelSettings(_mode.Value, _boxCount.Value); } private static string Describe(LevelSettings settings) { if (settings.Mode != SpawnMode.Fixed) { return "Random, minimum actual boxes=1 then vanilla probability"; } return $"Fixed, target boxes={settings.BoxCount}, loopMax={settings.LoopMax}, compensationLoopMax={settings.CompensationLoopMax}"; } } internal static class PatchMethods { private const int ForcedFailureRoll = 99999; private static readonly FieldInfo TargetAmountField = AccessTools.Field(typeof(ValuableDirector), "cosmeticWorldObjectTargetAmount"); private static bool _chanceGatePatchActive; internal static bool ChanceGatePatchActive => _chanceGatePatchActive; internal static void SetupHostPrefix() { Plugin.ActivatePendingSettingsForLevel(); } internal static IEnumerable SetupHostMoveNextTranspiler(IEnumerable instructions) { List list = instructions.ToList(); MethodInfo methodInfo = AccessTools.Method(typeof(Random), "Range", new Type[2] { typeof(int), typeof(int) }, (Type[])null); MethodInfo operand = AccessTools.Method(typeof(PatchMethods), "GuaranteedChanceRoll", (Type[])null, (Type[])null); List list2 = new List(); for (int i = 0; i <= list.Count - 3; i++) { if (IsLdcI4(list[i], 0) && IsLdcI4(list[i + 1], 100) && CodeInstructionExtensions.Calls(list[i + 2], methodInfo)) { list2.Add(i); } } if (list2.Count != 1) { _chanceGatePatchActive = false; string text = $"Expected exactly one cosmetic Random.Range(0,100) chance gate in SetupHost.MoveNext, found {list2.Count}; rolling back patches."; Plugin.Log.LogError((object)text); throw new InvalidOperationException(text); } list[list2[0] + 2].operand = operand; _chanceGatePatchActive = true; Plugin.Log.LogInfo((object)"Patched SetupHost.MoveNext cosmetic chance gate."); return list; } internal static int GuaranteedChanceRoll(int min, int max) { int result = Random.Range(min, max); Plugin.TryGetGuaranteeState(out var settings, out var actual); if (settings.Mode == SpawnMode.Fixed) { if (actual >= settings.BoxCount) { return 99999; } return min; } if (actual >= 1) { return result; } return min; } internal static void LoopsGetPostfix(ref int __result) { LevelSettings activeSettings = Plugin.ActiveSettings; if (activeSettings.Mode == SpawnMode.Fixed) { __result = activeSettings.LoopMax; } } internal static void LoopsClampedGetPostfix(ref int __result) { LevelSettings activeSettings = Plugin.ActiveSettings; __result = ((activeSettings.Mode == SpawnMode.Fixed) ? activeSettings.CompensationLoopMax : Mathf.Max(__result, 0)); } internal static void SpawnCosmeticWorldObjectPrefix(ValuableDirector __instance, ref Rarity _cosmeticRarity, int[] _volumeIndex, List[] _volumes, out int __state) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Expected I4, but got Unknown __state = GetTargetAmount(__instance); try { if (Plugin.NeedsGuaranteedBox() && !HasAvailableVolume(__instance, _cosmeticRarity, _volumeIndex, _volumes) && TryFindAvailableRarity(__instance, _volumeIndex, _volumes, out var rarity)) { _cosmeticRarity = (Rarity)(int)rarity; } } catch (Exception arg) { Plugin.Log.LogError((object)$"SpawnCosmeticWorldObjectPrefix failed; leaving vanilla spawn logic unchanged. {arg}"); } } internal static void SpawnCosmeticWorldObjectPostfix(ValuableDirector __instance, int __state) { try { if (GetTargetAmount(__instance) > __state) { Plugin.RecordActualBoxSpawned(); } } catch (Exception arg) { Plugin.Log.LogError((object)$"SpawnCosmeticWorldObjectPostfix failed. {arg}"); } } private static int GetTargetAmount(ValuableDirector director) { if ((Object)(object)director == (Object)null || TargetAmountField == null || TargetAmountField.FieldType != typeof(int)) { return 0; } try { return (int)TargetAmountField.GetValue(director); } catch (Exception arg) { Plugin.Log.LogError((object)$"Failed to read cosmeticWorldObjectTargetAmount. {arg}"); return 0; } } private static bool TryFindAvailableRarity(ValuableDirector director, int[] volumeIndex, List[] volumes, out Rarity rarity) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: 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_004c: Expected I4, but got Unknown if (director?.cosmeticWorldObjectSetups == null) { rarity = (Rarity)0; return false; } for (int i = 0; i < director.cosmeticWorldObjectSetups.Count; i++) { if (Enum.IsDefined(typeof(Rarity), i) && director.cosmeticWorldObjectSetups[i] != null) { Rarity val = (Rarity)i; if (HasAvailableVolume(director, val, volumeIndex, volumes)) { rarity = (Rarity)(int)val; return true; } } } rarity = (Rarity)0; return false; } private static bool TryGetVolumeIndexForRarity(ValuableDirector director, Rarity rarity, out int volumeIndex) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) volumeIndex = 0; if (director?.cosmeticWorldObjectSetups == null) { return false; } int num = Convert.ToInt32(rarity); if (num < 0 || num >= director.cosmeticWorldObjectSetups.Count) { return false; } CosmeticWorldObjectSetup val = director.cosmeticWorldObjectSetups[num]; if (val == null) { return false; } volumeIndex = Convert.ToInt32(val.volume); return true; } private static bool HasAvailableVolume(ValuableDirector director, Rarity rarity, int[] volumeIndex, List[] volumes) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (TryGetVolumeIndexForRarity(director, rarity, out var volumeIndex2) && volumes != null && volumeIndex != null && volumeIndex2 >= 0 && volumeIndex2 < volumes.Length && volumeIndex2 < volumeIndex.Length && volumes[volumeIndex2] != null && volumeIndex[volumeIndex2] >= 0) { return volumeIndex[volumeIndex2] < volumes[volumeIndex2].Count; } return false; } private static bool IsLdcI4(CodeInstruction instruction, int expected) { if (instruction.opcode == OpCodes.Ldc_I4_M1) { return expected == -1; } if (instruction.opcode == OpCodes.Ldc_I4_0) { return expected == 0; } if (instruction.opcode == OpCodes.Ldc_I4_1) { return expected == 1; } if (instruction.opcode == OpCodes.Ldc_I4_2) { return expected == 2; } if (instruction.opcode == OpCodes.Ldc_I4_3) { return expected == 3; } if (instruction.opcode == OpCodes.Ldc_I4_4) { return expected == 4; } if (instruction.opcode == OpCodes.Ldc_I4_5) { return expected == 5; } if (instruction.opcode == OpCodes.Ldc_I4_6) { return expected == 6; } if (instruction.opcode == OpCodes.Ldc_I4_7) { return expected == 7; } if (instruction.opcode == OpCodes.Ldc_I4_8) { return expected == 8; } if (instruction.opcode == OpCodes.Ldc_I4_S || instruction.opcode == OpCodes.Ldc_I4) { return Convert.ToInt32(instruction.operand) == expected; } return false; } } }