Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of MacosModpackIntegration v1.2.0
MacosModpackIntegration.dll
Decompiled 14 hours 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.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using Dawn; using Dawn.Utils; using DunGen.Graph; using Dusk; using Dusk.Weights; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("AmazingAssets.TerrainToMesh")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp-firstpass")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: IgnoresAccessChecksTo("ClientNetworkTransform")] [assembly: IgnoresAccessChecksTo("com.olegknyazev.softmask")] [assembly: IgnoresAccessChecksTo("DissonanceVoip")] [assembly: IgnoresAccessChecksTo("DunGen")] [assembly: IgnoresAccessChecksTo("DunGen.Integration.ASPP")] [assembly: IgnoresAccessChecksTo("DunGen.Integration.UnityNav")] [assembly: IgnoresAccessChecksTo("EasyTextEffects")] [assembly: IgnoresAccessChecksTo("Facepunch Transport for Netcode for GameObjects")] [assembly: IgnoresAccessChecksTo("Facepunch.Steamworks.Win64")] [assembly: IgnoresAccessChecksTo("Unity.AI.Navigation")] [assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging")] [assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging.DocCodeExamples")] [assembly: IgnoresAccessChecksTo("Unity.Burst")] [assembly: IgnoresAccessChecksTo("Unity.Burst.Unsafe")] [assembly: IgnoresAccessChecksTo("Unity.Collections")] [assembly: IgnoresAccessChecksTo("Unity.Collections.LowLevel.ILSupport")] [assembly: IgnoresAccessChecksTo("Unity.InputSystem")] [assembly: IgnoresAccessChecksTo("Unity.InputSystem.ForUI")] [assembly: IgnoresAccessChecksTo("Unity.Jobs")] [assembly: IgnoresAccessChecksTo("Unity.Mathematics")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.Common")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.MetricTypes")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStats")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Component")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Configuration")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Implementation")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsReporting")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkProfiler.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkSolutionInterface")] [assembly: IgnoresAccessChecksTo("Unity.Netcode.Components")] [assembly: IgnoresAccessChecksTo("Unity.Netcode.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.Networking.Transport")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Csg")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.KdTree")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Poly2Tri")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Stl")] [assembly: IgnoresAccessChecksTo("Unity.Profiling.Core")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.ShaderLibrary")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Config.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary")] [assembly: IgnoresAccessChecksTo("Unity.Services.Authentication")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Analytics")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Components")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Configuration")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Device")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments.Internal")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Internal")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Networking")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Registration")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Scheduler")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Telemetry")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Threading")] [assembly: IgnoresAccessChecksTo("Unity.Services.QoS")] [assembly: IgnoresAccessChecksTo("Unity.Services.Relay")] [assembly: IgnoresAccessChecksTo("Unity.TextMeshPro")] [assembly: IgnoresAccessChecksTo("Unity.Timeline")] [assembly: IgnoresAccessChecksTo("Unity.VisualEffectGraph.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.XR.CoreUtils")] [assembly: IgnoresAccessChecksTo("Unity.XR.Management")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.ConformanceAutomation")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MetaQuestSupport")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MockRuntime")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.OculusQuestSupport")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.RuntimeDebugger")] [assembly: IgnoresAccessChecksTo("UnityEngine.ARModule")] [assembly: IgnoresAccessChecksTo("UnityEngine.NVIDIAModule")] [assembly: IgnoresAccessChecksTo("UnityEngine.SpatialTracking")] [assembly: IgnoresAccessChecksTo("UnityEngine.UI")] [assembly: IgnoresAccessChecksTo("UnityEngine.XR.LegacyInputHelpers")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("MacosModpackIntegration")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("The fixes, content and cross-mod integration MACO's modpack needed and no published mod provides: CodeRebirth's weapons crafted from scrap instead of a coin economy, crafting parts on every moon, 36 weather combinations and nineteen fixes.")] [assembly: AssemblyFileVersion("1.2.0.0")] [assembly: AssemblyInformationalVersion("1.2.0")] [assembly: AssemblyProduct("MACO's Modpack Integration")] [assembly: AssemblyTitle("MacosModpackIntegration")] [assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was fully created with the assistance of Generative AI: the code and its documentation were written by Claude (Anthropic) through Claude Code, directed and play-tested by Maco.")] [assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace MacosModpackIntegration { [HarmonyPatch] internal static class ArmoryApparatusEvents { private const string FlowPrefix = "GrandArmoryFlow"; private const string TeleporterName = "Teleporter"; private const string CloserName = "EntranceCloser"; [HarmonyPrefix] [HarmonyPatch(typeof(LungProp), "EquipItem")] private static void Prefix(LungProp __instance) { try { if (!Cfg.RestoreArmoryApparatusEvents.Value || !__instance.isLungDocked) { return; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsServer) { return; } DungeonFlow val = RoundManager.Instance?.dungeonGenerator?.Generator?.DungeonFlow; if ((Object)(object)val == (Object)null || !((Object)val).name.StartsWith("GrandArmoryFlow", StringComparison.Ordinal)) { return; } PlayerControllerB val2 = GameNetworkManager.Instance?.localPlayerController; if ((Object)(object)val2 == (Object)null) { return; } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = 0; AnimatedObjectTrigger[] array = Object.FindObjectsByType<AnimatedObjectTrigger>((FindObjectsInactive)1, (FindObjectsSortMode)0); foreach (AnimatedObjectTrigger val3 in array) { if (((Object)((Component)val3).gameObject).name != "Teleporter") { continue; } num++; if (InsideACloser(((Component)val3).transform)) { num2++; if (!((Component)val3).gameObject.activeInHierarchy) { num3++; continue; } if (!((NetworkBehaviour)val3).IsSpawned) { num4++; continue; } val3.TriggerAnimation(val2); num5++; } } if (num5 > 0) { Plugin.Log.LogInfo((object)("The apparatus left its dock in the Grand Armory, so " + num5 + " teleporter door(s) were flipped, as Wesley's events would have.")); return; } Plugin.Log.LogWarning((object)("The apparatus left its dock in the Grand Armory but no teleporter door was flipped. Objects named 'Teleporter' with an animated trigger: " + num + "; of those inside an EntranceCloser: " + num2 + "; switched off: " + num3 + "; not spawned on the network: " + num4 + ". If the first number is 0 the interior no longer ships those doors; otherwise the last two say what stopped them.")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not flip the Grand Armory's teleporter doors for the apparatus, so they stay as they were: " + ex.Message)); } } private static bool InsideACloser(Transform from) { Transform val = from; while ((Object)(object)val != (Object)null) { if (((Object)val).name.StartsWith("EntranceCloser", StringComparison.Ordinal)) { return true; } val = val.parent; } return false; } } internal static class Cfg { public enum Notice { Tip, Notification, Chat, Off } internal struct WeatherRate { public string Item; public string Match; public float Landings; } public static ConfigEntry<string> Stock; public static ConfigEntry<string> CrateOnly; public static ConfigEntry<string> Components; public static ConfigEntry<string> WeatherNameRates; public static ConfigEntry<string> Floors; public static ConfigEntry<string> FloorSkipMoons; public static ConfigEntry<float> InteriorSpawnerChance; public static ConfigEntry<bool> RemoveCoinEconomy; public static ConfigEntry<string> Removed; public static ConfigEntry<bool> ClampFloorOffsets; public static ConfigEntry<float> MaxFloorOffset; public static ConfigEntry<bool> FixSluggerAim; public static ConfigEntry<bool> FixSluggerPointBlank; public static ConfigEntry<bool> GuardChameleonDoorFixer; public static ConfigEntry<bool> FixStoptimeWatch; public static ConfigEntry<bool> FixOxidizerActivation; public static ConfigEntry<bool> ClearStuckActivation; public static ConfigEntry<bool> ReportInteractLocks; public static ConfigEntry<bool> ClearStuckEnemyAnimation; public static ConfigEntry<bool> FixEazyBakeReach; public static ConfigEntry<bool> FixLaserTurretWalls; public static ConfigEntry<bool> FixSnailCatOwner; public static ConfigEntry<bool> SmoothUnlockerVideo; public static ConfigEntry<bool> RestoreWeatherCombos; public static ConfigEntry<bool> TornadoProgression; public static ConfigEntry<bool> WesleyWeatherCombos; public static ConfigEntry<bool> RestoreArmoryApparatusEvents; public static ConfigEntry<bool> SilenceDeadRadarBlips; public static ConfigEntry<bool> DespawnOfficeElevator; public static ConfigEntry<bool> RestorePoseAfterEmote; public static ConfigEntry<bool> SteadyEmoteCamera; public static ConfigEntry<bool> RepairRadarNames; public static ConfigEntry<bool> SyncRadarTarget; public static ConfigEntry<bool> RerollShipUnlocker; public static ConfigEntry<string> UnlockerPool; public static ConfigEntry<string> UnlockerExclude; public static ConfigEntry<Notice> Announce; public static ConfigEntry<int> MoonListGroupSize; public static ConfigEntry<bool> SortUnrankedMoonsByLetter; public static ConfigEntry<int> MoonListWeatherWidth; public static ConfigEntry<string> WeatherShortNames; public static ConfigEntry<bool> ColorMoonListWeather; public static ConfigEntry<bool> SpeedUpFairAiTurrets; public static ConfigEntry<bool> FrameProbe; public static ConfigEntry<float> FrameProbeInterval; public static ConfigEntry<bool> FrameProbeTimers; public static ConfigEntry<int> FrameProbeTimerRows; public static ConfigEntry<bool> FrameProbeAblation; public static ConfigEntry<float> FrameProbeAblationDelay; public static ConfigEntry<float> FrameProbeAblationHold; public static ConfigEntry<string> FrameProbeAblationTypes; public static void Bind(ConfigFile c) { //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Expected O, but got Unknown //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Expected O, but got Unknown CrateOnly = c.Bind<string>("Shop", "CrateOnly", "Rocket Launcher", "Items that are never sold but can still turn up in CodeRebirth's wooden crates, comma separated.\nA wooden crate gives one random shop item, and the pool it picks from is the terminal's own stock: DawnLib registers every buyable item, and the crate keeps the ones that have a shop entry and no scrap entry. So an item is normally either buyable and cratable or neither, and there is no way to have a thing that can only ever be found. Named here, an item joins the crate pool without joining the store.\nThe default is LethalThings' Rocket Launcher. It is registered but unreachable - not scrap, not sold, no spawn weights - and one rocket kills all but a boss outright, so finding one is meant to be the event. It arrives loaded with four; its Missile ammo is bought at the terminal as normal.\nNames are the in-game item names. Anything that does not resolve is named in the log rather than quietly skipped. Empty this to leave crates exactly as CodeRebirth built them."); Stock = c.Bind<string>("Shop", "Stock", "", "Merchant items to sell in the terminal, as a comma-separated list of Name=Price.\nEmpty by default: none of CodeRebirth's weapons or tools were buyable in vanilla, and putting them in the store makes every other way of getting one pointless. They are meant to be built instead - see Components below and the recipes in BepInEx/config/CraftingLibrary/. Fill this in to have them on the shelf as well.\nThe names are the ones CodeRebirth uses in its own config: look in CodeRebirth.cfg under [Merchant Options] and take the part before the ' | '. Anything in the merchant bundle works, not only weapons - Stoptime Watch, InfiniKey, X-ray Glasses, Lifeform Analyser, Guardsman Phone, WalkieYellie, Mouse Trap, Hand Drill, Hard Hat, FogHorn, Rubber band, Disposable Camera and Yoyo are all there too.\nPrices are guesses, not the merchant's own numbers - those are coin prices baked into the merchant prefab, not readable from config. Tune them to taste.\nThe Orbital Strike Caller is deliberately not here. Activating it does not fire anything - it throws itself out of your hands at 30 m/s along your look direction, sets grabbable to false, and only becomes pickable again once it has come to rest on a navmesh and finished its laser. Thrown anywhere it can leave the level it is gone for good, and its LateUpdate logs the rigidbody's speed every frame for as long as it falls - one test produced 127,936 warning lines. Add it back if you want it, but throw it at flat ground outdoors.\nThe Guardsman Phone is in the default list for a reason beyond taste: the Loyal Guardsman is left alive by RemoveCoinEconomy and he is a bodyguard, hunting any monster within 45 metres that is not on his blacklist. The phone summons one five metres in front of you three seconds after use, outdoors only. It is the only way to call one on purpose, and with the merchant gone it would otherwise be unobtainable - an enemy switched on with the item that uses it switched off. It is reusable, and it spawns through RoundManager directly, so it ignores the Guardsman's own Max Spawn Count of 1. Price it high or drop it if that is too strong for your lobby.\nSelling an item here does not stop it spawning as scrap. Three of the defaults already drop on their own (Mole Digger, Nitroglycerin Crate, Remote Detonator); to make any of the others drop too, give it a moon weight in CodeRebirth.cfg - '<name> | Preset Moon Weights' - and set its 'Allow Editing Config' to true so a launch does not wipe it.\nEmpty this to leave the shop alone."); Components = c.Bind<string>("Components", "Weights", "Ruby Crystal : none | Sapphire Crystal : none | Emerald Crystal : none | Birthday Card : lethal_company:all=+2 | Disposable Camera : lethal_company:all=+3 | Music Box : lethal_company:all=+3 | Rolling Duck : lethal_company:all=+2 | Scribbled Paper : lethal_company:all=+3 | Teddy Bear : lethal_company:all=+3 | Walking Cane : lethal_company:all=+3 | Wilted Rose : lethal_company:all=+3 | Spud Tater : lethal_company:valley=+2,lethal_company:marsh=+2 | Plastic Cake : lethal_company:all=+3 | Ducky Tube : lethal_company:all=+2 | Mole Digger : none | Nitroglycerin Crate : none | Remote Detonator : none", "CodeRebirth scrap to switch on so it can be found and used for crafting, as 'Name : weights' entries separated by ' | '.\nCodeRebirth registers 43 items that have a value, a model and a scan node but no moon weights at all, so no player ever sees them. They are finished content going to waste, which makes them free crafting components - nothing to model, nothing to package.\nThe six parts the recipes gate on are not in this list. A weight has to name the moons it applies to and a moon installed later is named by nothing, so they are switched on by MinimumRates instead - see there. What is left here is the rest of the merchant's stock.\nThe names are the ones CodeRebirth uses in its own config: look in CodeRebirth.cfg and take the part before the ' | '. The weights are DawnLib's own syntax, the same you would write into '<name> | Preset Moon Weights': a comma-separated list of 'namespace:key=+N', where the key is a moon (lethal_company:titan) or a tag (lethal_company:rocky, lethal_company:all).\nSetting them here rather than in CodeRebirth.cfg is not a matter of taste. Dusk rewrites '<name> | Preset Moon Weights' from the definition on every launch whenever '<name> | Allow Editing Config' is false, which is the default - so a hand-edited weight lasts exactly until the next start. This writes the definition, which is the thing that wins.\nWrite 'none' as the weights to switch an item OFF instead. The Mole Digger, Nitroglycerin Crate and Remote Detonator are the only merchant items CodeRebirth gives weights to, and they are craftable now, so finding one on the floor would make its recipe pointless.\nOnly items that do nothing are given weights here. Anything with an ItemActivate of its own - the FogHorn, InfiniKey, Lifeform Analyser, Mouse Trap, Old Booze, Rubber band, Stoptime Watch, WalkieYellie and Ship Upgrade Unlocker - is crafted instead, for the same reason the weapons are: a tool found on the floor is a lottery, a tool you built is a decision. Telling them apart is easy in the decompile, where a merchant item with no behaviour is a 22-line class of netcode boilerplate and nothing else.\nMost of this list is not components at all - it is the rest of the merchant's stock. Nothing in CodeRebirth is 'disabled': the 43 items with no weights each have another way in, and 21 of them still work (meteor craters drop the crystals, burning a wooden crate drops the burnt flesh, the microwave makes the charred food, the Puppeteer drops its needle, Compactor Toby leaves a flattened body). The 22 merchant items are the exception, and this mod is why: RemoveCoinEconomy deletes the Merchant, and he was their only source. Giving them a weight here is restoring what this mod took away, not overriding the author.\nThe three crystals the recipes want are cleared to 'none' here rather than given weights. They are meant to be found during an eclipse - see WeatherNameRates - and by meteor shower as they always were: every impact has a 2.5% chance to drop one of the six at random ('MeteorShower | Meteorite Spawn Chance'), which is a better way to find ore than tripping over it in a corridor. The other three of the six are left to the shower alone, since no recipe asks for them.\nEmpty this to leave every dormant item dormant."); WeatherNameRates = c.Bind<string>("Components", "WeatherNameRates", "Ruby Crystal : eclips : 2 | Sapphire Crystal : eclips : 2 | Emerald Crystal : eclips : 2", "Items that turn up while the weather's NAME contains a given word, as 'Name : text : landings' entries separated by ' | '.\n'Ruby Crystal : eclips : 2' means one Ruby every two landings that happen in weather with 'eclips' in its name - so Eclipsed, but also Eclipsed Flare, Eclipsed + Flooded, Rainy > Eclipsed and every other variant a weather mod makes up. The match is case-insensitive and matches anywhere in the name, which is why the word is written short.\nIt matches on the NAME because the obvious alternative does not work. DawnLib has a weather track of its own, but it can only name weathers that were registered during startup - fifteen in a pack like this one - and a rule naming anything else is skipped in silence. WeatherTweaks builds twelve combined weathers and ten of them are not in that fifteen, so a key-based eclipse rule reaches about 8% of landings where every eclipse-flavoured weather together is about 17%. A name is the one thing every weather has, so matching on it covers all of them. The log says on the first landing which weathers each rule matches, so a rule that matches nothing is visible rather than silent.\nThe rate works the same way as MinimumRates, and counts only the landings that match: at ' : 2' you get one every second eclipse, not one every second landing. It is a floor, so a moon already carrying more keeps what it has, and the moons in MinimumRatesSkipMoons are skipped here too.\nThe three meteor crystals use this. Their moon weights are cleared to 'none' in Weights, so an eclipse is the only place they are found besides a meteor shower - which turns a crystal from something you trip over into a reason to route somewhere dangerous on purpose.\nEmpty this to give nothing a weather-name rule."); Floors = c.Bind<string>("Components", "MinimumRates", "Big bolt : 5 | Metal sheet : 5 | Large axle : 5 | Bottles : 5 | Cookie pan : 5 | Tea kettle : 5 | Control pad : 6 | Remote : 6 | Magnifying glass : 6 | Airhorn : 6 | Old phone : 6 | Jar of pickles : 6 | Rubber Ducky : 6 | Homemade flashbang : 6 | Chemical jug : 6 | Hand Drill : 8 | Rusted Pick Head : 8 | Saw Blade : 8 | Mirror : 8 | Hard Hat : 8 | Alarm Clock : 8 | Gold bar : 12", "Items that must turn up at least this often on every moon, as 'Name : landings' entries separated by ' | '. 'Chemical jug : 6' means one jug every six landings.\nThis exists because a crafting part is an input, not loot. Left alone, most of these are carried by a handful of moons and no config anywhere can change that - vanilla weights are baked into each moon's asset, DawnLib only manages content its own mods registered, and LethalLevelLoader's per-moon list REPLACES the scrap and enemy lists rather than merging into them, so switching it on deletes modded content from that moon. This tops the items up directly instead, just before the scrap is rolled.\nIt is a rate, not a weight: the number needed is worked out per moon from that moon's pool and how many pieces it spawns, so a moon that drops thirty items and one that drops fifteen both hand you a jug every six landings. Install another scrap mod and the maths follows it.\nIt is a floor, never a cap. A moon already carrying more than this keeps what it has, so Metal sheet stays at 100 on Embrion and the map keeps its character. It is also put back after every roll, so landing on one moon five times in a row does not stack it.\nThe numbers are a guarantee, not a supply. The filler parts are common vanilla scrap on most moons already, so 5 only matters on the few moons that lack one; the mid parts at 6 close the real holes (Chemical jug was on two moons). The first tuning was 1.5 and 4, and on a moon that rolls ten pieces that put about half of them into crafting parts.\nThe six parts the recipes gate on - Hand Drill, Rusted Pick Head, Saw Blade, Mirror, Hard Hat, Alarm Clock - are here rather than in Weights, where CodeRebirth content is normally switched on. A weight has to name the moons it applies to, so a moon installed later carries none of them and six of the recipes quietly become vanilla-only. A rate names no moon, so a new moon is covered the day it arrives. One every eight landings is what their per-moon weights were tuned to deliver, now computed rather than written down.\nGold bar is here at a deliberately slow rate. Outside Artifice it is close to unfindable, so the one recipe that wants it was really a trip to Artifice - but it is worth 255-525 credits, so a filler rate would move the economy rather than the recipe. One every twelve landings closes the hole and adds about thirty credits a run. The meteor crystals are absent for a different reason: they are meant to be found during an eclipse, see WeatherNameRates.\nEmpty this to let every part stay exactly as rare as its own mod left it."); InteriorSpawnerChance = c.Bind<float>("Components", "InteriorSpawnerChance", 0.25f, new ConfigDescription("How often an interior that places its own scrap is allowed to place a crafting part, as a chance from 0 to 1.\nSome interiors carry item spawners of their own (JLL's ItemSpawner) that drop set scrap on top of the moon's roll. Mones' Endless Hall places fifteen to thirty-five Big bolts and Tea kettles a landing this way, the Attic about ten, Underwater Mines a few - real, pickable scrap, all of it crafting parts, against a roll of eight to fifteen pieces. With MinimumRates already guaranteeing those parts everywhere that is a pile of junk for no reason, so each such spawn of a part named in MinimumRates goes ahead only this often. Anything a spawner places that is not a crafting part - the Tower's elevator key, say - is never touched.\n1 leaves every interior exactly as its author built it; 0 stops them placing crafting parts at all.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); FloorSkipMoons = c.Bind<string>("Components", "MinimumRatesSkipMoons", "7 Dine", "Moons the rates above are not applied to, comma separated. The number is optional - either 'Dine' or '7 Dine' matches, so a moon being renumbered by its author does not quietly drop off this list.\nA moon belongs here when it rolls so many pieces of scrap that one point of weight is already more than the rate asks for. One point is the least an entry can weigh and still exist, so on such a moon no rate can be held to. In this pack that is only Dine: 200-250 pieces a landing from a ten-item table of severed body parts, and no value normalisation in v81 to trim it. Leaving it out keeps it what it is.\nThe pool the floor measures is the one at spawn time, with every mod's entries in it - around 400 points on the smallest moons once ImmersiveScrap's flat six per item is counted - not the moon's own short table, so a moon rolling 40 to 55 pieces (Lecaro, Trite, Carnate) still gets a weight of one or two and sits on its rate.\nThe log says when a moon needs adding here. A part that cannot be held to its rate is named in a warning on landing, with how many times too often it will turn up."); RemoveCoinEconomy = c.Bind<bool>("Economy", "RemoveCoinEconomy", true, "Strip out the coin half of CodeRebirth's merchant: coins, dollar bills and wallets stop spawning, enemies stop dropping them, the vending machines and the Merchant himself stop appearing, and the Denomination Analyzer leaves the store so nobody can buy their way into the debt system.\nThis is what makes the merchant weapons worth putting in the store - without it you would have two parallel economies. Turn it off to get vanilla CodeRebirth behaviour back with the store items added on top."); Removed = c.Bind<string>("Economy", "Removed", "Coin, Crisp Dollar Bill, Wallet, Merchant, Military Plane, Common Vending Machine, Uncommon Vending Machine, Rare Vending Machine, Legendary Vending Machine, Debt Collector, Denomination Analyzer, Scrap-a-pult, X-ray Glasses", "Which pieces of the merchant bundle RemoveCoinEconomy takes out, by CodeRebirth config name. Each one is tagged 'unimplemented', which is DawnLib's own marker for content that loads but is never registered - so it does not spawn, is not sold, and does not appear in the bestiary.\nThe last two are not coin machinery, they are unfinished: the Scrap-a-pult and the X-ray Glasses both have empty classes in CodeRebirth 1.6.9 - no ItemActivate, no behaviour of any kind, 16 bytes of netcode boilerplate and nothing else - and both are worth 0 as scrap, so there is nothing to keep them for. Other merchant items with no code of their own (Alarm Clock, Ducky Tube, Meteorite Shard, Sally Cube, Gold Rigo, Disposable Camera) are plain valuables that are not supposed to do anything, and are left alone.\nThe Loyal Guardsman is deliberately NOT in this list. He is merchant-faction but he spawns on his own weights and needs nothing from the coin system, so he survives as a plain outside enemy. Add 'Guardsman' here if you want him gone.\nMilitary Plane is the bomber that delivers the Boogey Man, so it goes with the Debt Collector."); ClampFloorOffsets = c.Bind<bool>("Fixes", "ClampFloorOffsets", true, "Stop a dropped item drifting up through the ceiling.\nItems do not fall under physics here: dropping one aims it at the floor plus its verticalOffset and slides it there, through anything in between. That offset is measured at runtime rather than authored - Matty's Fixes does it - and on a model whose renderers sit far from its origin the measurement comes out enormous. Some of CodeRebirth's do: one log had the Rail Slugger asking for 124 metres.\nWith this on, an offset past MaxFloorOffset is treated as a bad measurement and the item rests on the floor instead, once, with a line in the log naming it.\nApplies to every item, not only merchant ones, because the fault is in how the offset is measured rather than in who made the item."); MaxFloorOffset = c.Bind<float>("Fixes", "MaxFloorOffset", 1.5f, new ConfigDescription("How far above the floor an item is allowed to claim it rests, in metres. Every vanilla item measures under 0.25, so the default has a lot of room in it and only catches answers that cannot be about the item at all.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 50f), Array.Empty<object>())); FixSluggerAim = c.Bind<bool>("Fixes", "FixSluggerAim", true, "Make the Rail Slugger hit what you are looking at.\nThe shot leaves your eyes but travels along the direction your body faces, and a body does not tilt up or down - only the camera does. So the beam always flies level, whatever the crosshair says, and anything low enough to look down at is passed straight over. Crouching is the only way to hit a bunker spider with it.\nThe rest of the shot already follows the camera - the recoil shoves back along your look direction and the muzzle flash is on the model in your hands - which is why it looks like it fires where you aim.\nWith this on, the cast that decides what was hit follows the camera too.\nThis is only about the direction the shot travels. Something pressed right up against you is a separate problem with the origin - see FixSluggerPointBlank."); FixSluggerPointBlank = c.Bind<bool>("Fixes", "FixSluggerPointBlank", true, "Let the Rail Slugger hit an enemy that is already touching you.\nThe shot is decided by a sphere cast a metre wide, and a sphere cast reports nothing for a collider it is already inside of when it starts - the sweep begins where the sphere is clear. So a bunker spider mid-maul, a thumper that has just landed on you or a masked in your face all survive a point-blank blast: the gun fires, the battery drains and nothing is hit.\nWith this on, the same sphere is also asked what it is touching, and anything found that way is hit for the same damage as any other shot.\nPlayers are never added this way. A teammate standing in front of you can still be hit exactly as before, but one pressed against you is left alone rather than blasted for standing too close.\nClient-side, like FixSluggerAim: the gun applies its hits on the shooter's own machine, so this follows whoever has it installed and nothing desyncs."); GuardChameleonDoorFixer = c.Bind<bool>("Fixes", "GuardChameleonDoorFixer", true, "Keep Chameleon's door fixer from cutting level setup short on interiors it does not understand.\nChameleon's FixDoorMeshes looks at every door in the level as it finishes generating. On some custom interiors (Mones' Underwater Mines for one) a door is missing a material or a mesh, Chameleon trips over it, and the error stops everything that was still to run at that moment: the rest of Chameleon's own touches and any other mod that hooks the same point. The log shows it as an 'Unhandled RPC exception' from Chameleon.Overrides.DoorMaterialsFixer.\nWith this on, that one error is caught and logged as a warning and setup carries on. Doors on that interior keep their stock look, which is what they had anyway. Does nothing when Chameleon is not installed."); FixStoptimeWatch = c.Bind<bool>("Fixes", "FixStoptimeWatch", true, "Stop the Stoptime Watch leaving something behind every time it is used.\nIt speeds the user up to move through the slowed world by dividing their movement speed by 0.16, and slows them back down afterwards by dividing by 6. Those are not the same number, so every use leaves you about four per cent faster - permanently, and compounding, because movementSpeed is written in exactly one place in the whole game and that is its field initialiser. Nothing resets it: not death, not the end of a round, not a new save. Seventeen uses is roughly double base speed.\nThis is also why Lategame Upgrades appears to be involved. Running Shoes and the other movement upgrades read movementSpeed and add on top of it, so they amplify a corrupted base without ever writing one; only Sick Beats writes the field, and its symmetric plus-and-minus comes out crooked if the watch multiplies in between. Fixing the watch fixes both.\nThe same fix also covers hold-to-interact. While time is slowed, whichever trigger you look at has its hold speed multiplied by five so a hold still takes the same real time; the undo reads Time.timeScale live, but the flag that gates it is cleared a full second after the timescale is restored, so stopping a hold in that window leaves the trigger five times faster for good. Doors repair themselves; the ship lever does not.\nLocal, like the Rail Slugger fixes: the effect only ever touches your own player and your own Unity timescale, so this follows whoever installed it."); FixOxidizerActivation = c.Bind<bool>("Fixes", "FixOxidizerActivation", true, "Stop the Oxidizer leaving you unable to press E.\nPressing the trigger sets PlayerControllerB.activatingItem, which the game uses to mean 'an item is mid-use' - while it is set, the interact key and every slot switch are ignored. Releasing the trigger is supposed to clear it, but the flamethrower's ItemActivate returns early when its tank is not charged, before it reaches that line. Fill the tank or use the alt-fire supercharge mid-spray and the release runs into that early return, so the flag stays up.\nThe stock game only clears it when you drop the item or die, which is why dropping the flamethrower appears to fix it and nothing else does.\nWith this on, letting go of the trigger or putting the flamethrower away clears the flag whatever state the tank is in."); ClearStuckActivation = c.Bind<bool>("Fixes", "ClearStuckActivation", true, "Clear the interact lock when you are not holding anything.\nSame flag as FixOxidizerActivation, but the general case: any item destroyed out of your hands while it was being used - consumed by a crafting recipe, taken by a mod that empties a slot - leaves the flag set with nothing left to clear it.\nThe test is deliberately the narrowest one that is still worth having: the flag says 'the item I am holding is mid-use', so an empty hand means it is stranded, whatever set it. Anything wider would cancel real animations - a shovel held over the head sets the same flag and stays that way for as long as you hold it."); ClearStuckEnemyAnimation = c.Bind<bool>("Fixes", "ClearStuckEnemyAnimation", true, "Release a kill animation that never finished.\nWhen an enemy starts killing you the game sets inAnimationWithEnemy, and your death is what clears it. While it is set every interaction is refused - doors, the terminal, picking things up, all of it - and unlike the other interact locks there is no timeout on the player's side.\nSo anything that stops the death leaves it up for the rest of the round. God mode is the usual cause: an Eyeless Dog mauls someone who cannot die and the animation never completes. It can also happen in ordinary play, because a dog that grabs a second player while already mid-kill sets that player's flag but never runs the coroutine that would clear it.\nCleared when the enemy is dead or gone, or after fifteen seconds with the player still alive. Every real kill animation in the game is shorter than that and ends with a corpse, so this cannot cut one short."); ReportInteractLocks = c.Bind<bool>("Fixes", "ReportInteractLocks", true, "Write a line to the log when the interact key is pressed and nothing happens.\n'E stopped working' has a dozen possible causes in this game and they all look identical from the chair, so this names the field responsible instead of leaving you to guess: press E, get 'Interact was refused. Blocked by: ...' in LogOutput.log.\nWorth knowing when you read one: activatingItem and waitingToDropItem only stop you picking things up and changing slots - doors and the terminal still work. inSpecialInteractAnimation is the opposite, blocking every trigger in the level while pickup keeps working. Everything else kills the press outright.\nOnly fires on a real keypress, so sitting in the terminal or climbing a ladder does not produce a line, and at most one line every two seconds.\nReporting only. It never changes what the key does."); FixEazyBakeReach = c.Bind<bool>("Fixes", "FixEazyBakeReach", true, "Make the EZ Bake Oven hurt enemies.\nIts damage and its netcode are both fine - four hit-damage a second plus explosion force, which is four times a shovel's rate. What fails is finding anything to hit, and there are three candidates: its trigger volume may be on a physics layer that never reports enemies, its GetComponent lookup only answers for enemy colliders that carry the hit component themselves, and its line-of-sight test asks for QueryTriggerInteraction.Collide, which counts every trigger volume in the way as a wall. Vanilla's own melee code uses Ignore for exactly that reason.\nWith this on, CodeRebirth's tick runs untouched and this only steps in on a tick where it found no enemies at all - so nothing is ever hit twice. The replacement asks the same question over the same baking region, resolves colliders to the enemy they belong to, and uses vanilla's line of sight.\nThe first time it lands a hit the original missed, it reports which of the three causes it was in LogOutput.log.\nOther players are left to CodeRebirth's own code, which works."); FixLaserTurretWalls = c.Bind<bool>("Fixes", "FixLaserTurretWalls", true, "Stop the laser turret firing through the walls of a custom interior.\nIts line of fire is one sphere cast, and the mask that cast uses is Colliders, Room, Player and InteractableObject. Default is not in it. Vanilla interiors keep walls on Room and blocking geometry on Colliders, so the turret behaves there; a custom interior that leaves a wall on layer 0 - which is what an unassigned collider gets - is invisible to that cast, so the beam passes through the wall and burns whoever is standing behind it. Underwater Mines is where this pack met it.\nWith this on, CodeRebirth's cast runs untouched and the same sweep is then repeated against the Default layer alone, ignoring triggers so that a reverb zone or an out-of-bounds box never stops a laser in mid-air. Whichever hit is nearer is the one used, so the beam can only ever be shortened, never lengthened, and no new damage is possible: a wall is not a player.\nThe first time a wall stops a shot that would have gone through, it says so in LogOutput.log.\nClient-side: the turret damages only the local player, so this follows whoever installed it."); SmoothUnlockerVideo = c.Bind<bool>("Fixes", "SmoothUnlockerVideo", true, "Stop the game hitching when the Ship Upgrade Unlocker is used.\nUsing one plays a six-second jingle and a little video on the tablet screen. The video player CodeRebirth built has 'Wait For First Frame' switched on, which asks Unity to hold the game still until the first frame has been decoded and pushed to the texture - a stall on the main thread, paid the moment the item is used, and worst the first time when the decoder is still being set up.\nThis turns that wait off just before the video starts, so playback begins when it is ready instead of the game waiting for it. The cost is that the video may miss its first frame or two, on a tablet screen, during a jingle. Nothing else about the unlocker changes.\nThe log line each use prints - pool, grant and jingle in milliseconds - is how to tell whether this is the hitch you are seeing or something else is."); FixSnailCatOwner = c.Bind<bool>("Fixes", "FixSnailCatOwner", true, "Stop every snail cat throwing an unhandled RPC exception when it spawns.\nThe message that gives a new snail cat its name, size and colour also carries who is holding it, and for one that spawned on the floor that is nobody - player id -1. Converting that back to a player throws instead of returning null, and CodeRebirth's own null check sits on the line after the conversion, so it never runs.\nThe throw is the last statement in the method, so the cat itself is fine: name, scale, colour and light sensor are all applied before it. What you lose is the branch that puts the cat into the hands of whoever spawned holding it, which does not apply to a wild one - and what you get is a red block in the log every time one appears.\nWith this on, the lookup returns null for a reference that names nobody, which is the answer the next line is already written to handle. Nothing is swallowed: a reference naming a real player resolves exactly as before.\nNot fixed in CodeRebirth 1.6.11."); RestoreWeatherCombos = c.Bind<bool>("Fixes", "RestoreWeatherCombos", true, "Add the four combined weathers WeatherTweaks skips.\nIt writes down Stormy + Solar Flare, Foggy + Solar Flare, Foggy + Snowfall and Eclipsed + Snowfall, then only creates them if LethalElements reports exactly version 1.3.0. MrovLib compares that as a string, and the only LethalElements that runs on v81 is the 1.4.x fork, so the four never appear and nothing says so.\nWith this on they are registered with the same names and the same weights the author wrote for them, and skipped if WeatherTweaks has already made them. Does nothing without both WeatherTweaks and LethalElements.\nEvery player needs the same answer here: weathers are matched between host and client by name, and a mismatch is reported as 'Weathers are different between clients!'."); TornadoProgression = c.Bind<bool>("Fixes", "TornadoProgression", true, "Let the weather turn into a tornado, and back, partway through the day.\nFour ways in - Stormy, clear, Rainy or Foggy - and two ways out, to Stormy or \nto clear. Each has one step and always takes it, somewhere between a third \nand half way through the day.\nNote that a day does not start at zero: the game sets globalTime to 100 out \nof a total of 700, so the clock already reads about 0.14 when the ship doors \nopen, and more on a moon with a faster day. The usable window for a change is \nroughly 0.2 to 0.85.\nA weather arriving mid-day is only safe if it does nothing to the level when \nit starts, so not every weather could do this. CodeRebirth's tornado does: it \nreads the outside AI nodes, borrows some of the level's enemy power budget \nand spawns one tornado twenty seconds later, away from the doors and the \nship, then kills what it spawned and hands the budget back when it leaves. \nLethalElements' snow does not: it rebuilds the terrain when it starts, so a \nblizzard arriving mid-day would remesh the ground under everyone standing on \nit. There are no snow progressions here for that reason.\nNeeds CodeRebirth and WeatherTweaks. Every player needs the same answer: \nweathers are matched between host and client by name."); WesleyWeatherCombos = c.Bind<bool>("Fixes", "WesleyWeatherCombos", true, "Combine Wesley's five weathers with the others.\nTwenty combinations: Minefield in fog, snow, a blizzard or an eclipse; Forsaken with an eclipse, fog, snow, a blizzard, a solar flare or Hallowed; Hallowed after dark or in fog; a Hurricane with a flood, an eclipse or a tornado; Earthquakes in fog, in the dark, in a storm, a heatwave or a meteor shower.\nEach is weighted like WeatherTweaks' own (about ten, against a moon's total near 1,700) and is only offered on a moon where every part of it is allowed, so Wesley's own exclusion lists still apply. Their scrap multipliers come from WeatherTweaks' rule, the sum of the parts times 0.75, unless the config names them.\nNeeds WeatherTweaks and Wesley's Weathers; a combination whose weather is not registered is skipped and said so in the log. Every player needs the same answer here: weathers are matched between host and client by name."); RestoreArmoryApparatusEvents = c.Bind<bool>("Fixes", "RestoreArmoryApparatusEvents", true, "Make the Grand Armory's teleporters react to the apparatus being pulled.\nWesley wired eight of that interior's tiles so that pulling the apparatus flips every teleporter's animated trigger - what pressing its button does - through an ApparatusTakenEvent script from IAmBatby's LethalToolbox. Wesley's Interiors neither ships nor declares LethalToolbox, and its last build (1.0.4, July 2024) cannot load on v81, so on every install the script is missing and the teleporters ignore the apparatus. The events also handed the trigger no player, which the game dereferences, so even with the script present they threw.\nWith this on, the host flips every teleporter door in the Grand Armory the moment the apparatus leaves its dock, and the trigger's own RPC carries it to the other players. Off leaves the interior as it loads.\nIf it finds none, the log says how many it saw at each step rather than claiming success."); SilenceDeadRadarBlips = c.Bind<bool>("Fixes", "SilenceDeadRadarBlips", true, "Stop a door or turret that outlived its level from throwing once per frame.\nEverything the terminal can address by code carries the game's TerminalAccessibleObject, whose Update measures the distance to that object's icon on the ship's map screen. The icon belongs to the level and is destroyed when the ship leaves; anything that outlives its level then dereferences the destroyed icon every frame the local player is standing in the ship. One such object logged 5,500 NullReferenceExceptions in a single session here.\nWith this on, that check is skipped when the icon is gone - which is all it could have done anyway - and the first one found is named in the log. This does not stop the object leaking, only the cost of it."); DespawnOfficeElevator = c.Bind<bool>("Fixes", "DespawnOfficeElevator", true, "Remove LC Office's elevator when the ship leaves, and tell it when the apparatus is pulled.\nThe Office spawns its elevator cabin and the shelf inside it as network objects that survive the scene, and relied on a listener its Awake added to LethalLevelLoader's onShipLeave to despawn them. The v81 compatibility fix has to skip that Awake and does not add the listener back, so the elevator is never removed: it keeps its last position for the rest of the session, inside whatever interior generates next - a rack of shelves in a Bunker room, an invisible wall around it and a prompt to place an apparatus - and its storage script throws once a frame on the position it was following. 754,848 exceptions in one session here.\nWith this on, the host despawns every copy as the ship leaves, and the second listener the fix dropped - the elevator reacting to the apparatus leaving its dock, and the office's message screen going blank - is put back too. Does nothing without LC Office installed."); RestorePoseAfterEmote = c.Bind<bool>("Fixes", "RestorePoseAfterEmote", true, "Put a held item back in the hand when a TooManyEmotes emote ends.\nThe game keeps its hold pose on two animator layers that are entered when you pick an item up or move it between slots, and nothing re-enters them when an emote ends. So the arm comes out of the emote playing something that is not the hold pose, and the item, which hangs under the hand, goes with it. Dropping the item and picking it up fixes it because picking up is what re-enters the layer.\nTooManyEmotes also copies its own skeleton onto the player every frame of an emote and restores nothing, so the bones no clip animates - fingers, mostly - keep the emote's pose as well.\nWith this on, the pose from just before the emote is written back the moment it stops, and the game is told to replay its hold animation, the same parameters it sets itself on a slot change. No sound, nothing dropped, nothing networked. Does nothing without TooManyEmotes installed."); SteadyEmoteCamera = c.Bind<bool>("Fixes", "SteadyEmoteCamera", true, "Stop an emote you can walk in from turning you on the spot.\nThe game keeps your pitch on the camera and your yaw on the body, so the camera's own local yaw is always zero. TooManyEmotes relies on that: while you are moving during an emote it copies the camera's local rotation onto the emote camera's pivot each frame, and adds whatever yaw that pivot holds to your facing. Camera Overhaul writes its lean, its shake and the Bracken neck-snap into exactly that local yaw, so the offset is added to your facing once a frame instead of once - the view rotates on its own and, because movement follows the body, the same key sends you off sideways.\nWith this on, the pivot's yaw is cleared after the copy - what it would hold on an unmodified camera - except while you are holding TooManyEmotes' own rotate-character key, which is the one time that yaw is meant to turn you. Pitch and lean are untouched. Does nothing without TooManyEmotes, and nothing without a camera mod that leans the view."); RepairRadarNames = c.Bind<bool>("Fixes", "RepairRadarNames", true, "Keep the name on the ship monitor on the crewmate the monitor is showing.\nThe map screen draws radarTargets[i].name beside a camera pointed at radarTargets[i].transform, and the game writes those names by player index when somebody joins. That is only right while the list is still in player order, and it does not stay that way: switching a radar booster, Wesley's fog reducer or one of CodeRebirth's gals onto the radar re-sorts the whole list - players included - by network object id, and MoreCompany appends its extra slots. After that, the next person to join writes every name one slot out.\nWith this on, each entry is named from the player it actually points at, right after the game has named them, so the label cannot end up on the wrong body. Duplicate usernames keep the numbered suffix the game gives them; boosters and the like name themselves and are left alone."); SyncRadarTarget = c.Bind<bool>("Fixes", "SyncRadarTarget", true, "Put the same crewmate on everybody's ship monitor.\nAll the game sends when somebody presses the monitor is an index into a list each machine builds for itself, so the moment two lists differ the same number means a different person - and a client whose list is shorter than the index it is sent ignores the switch outright and is pointed somewhere else from then on. Lists drift apart when anything puts itself on the radar (which also re-sorts them), and a player who joined after that never got the entry at all.\nWith this on, the host says which network object its own monitor is showing - a few bytes when it changes and once every three seconds otherwise - and each client points its monitor at that object wherever it sits in its own list, adding it if it was missed. The game's own switching is left alone; this corrects the result. Every player needs this mod for it to work, and the host decides."); RerollShipUnlocker = c.Bind<bool>("Unlocker", "RerollShipUnlocker", true, "Choose what the Ship Upgrade Unlocker can hand out.\nAs shipped it rolls flat across every ship unlockable that is not already yours. The game's list is 34 entries and four of them change how a run plays - the Teleporter, the Inverse Teleporter, the Signal translator and the Loud horn. The rest are suits and furniture, so six times in seven a two-gate-part recipe pays out a Toilet. Lategame Upgrades' upgrades are not in that list at all and could never come up.\nWith this on the pool is Pool below, and what came out is said in chat.\nOff gives CodeRebirth's roll back, unchanged."); UnlockerPool = c.Bind<string>("Unlocker", "Pool", "upgrades, lategame", "What the unlocker may roll, as a comma-separated list of:\n upgrades - ship upgrades that do something. Vanilla's four, plus anything a mod registered as a ship upgrade rather than as decor.\n decor - furniture and ornaments. The Toilet, the Goldfish, the Dog house.\n suits - suits, including any a suit mod added.\n lategame - every upgrade in Lategame Upgrades' store that is not yet maxed out. One that is already owned advances a level instead. Ignored when that mod is not installed.\nAn upgrade is told from an ornament by the game's own alwaysInStock flag, which is what DawnLib sets for a modded ship upgrade and clears for modded decor, so a mod's content is sorted without this mod knowing its names."); UnlockerExclude = c.Bind<string>("Unlocker", "Exclude", "", "Names the unlocker will never roll, comma-separated.\nMatches the name shown in the store - 'Inverse Teleporter', 'Beekeeper'. Use it to keep one upgrade as something you have to buy."); Announce = c.Bind<Notice>("Unlocker", "Announce", Notice.Tip, "Where to say what the unlocker gave. The unlock itself is silent and happens somewhere behind you, so without this the only way to find out is to walk round the ship or open the store.\n Tip - the tip box, top left, with a header. The default.\n Notification - the centred banner, the one that says 'New creature data sent to terminal!'.\n Chat - the chat box. Fine unless something in your pack hides it.\n Off - nothing here.\nThis is per player: turning it off does not stop anyone else being told, and everyone in the lobby is told whoever used the item. A Lategame upgrade that unlocks for one player rather than the crew says whose it is."); FrameProbe = c.Bind<bool>("Diagnostics", "FrameProbe", false, "Log a frame-timing line every few seconds, naming the moon and whether you are inside.\nThis is a measuring tool, not a feature. Turn it on while a frame rate needs explaining and off again after - it writes a line to the log every interval for as long as it is on.\nIt exists because a frame rate stuck on a flat number cannot be diagnosed from config files. A cap, vsync landing on half the refresh rate, main-thread work and GPU fill all look identical from outside and need completely different fixes. Each line reports the frame time split into main-thread time and time spent waiting, which separates our own code from anything on the present side, plus every value that can pin a frame rate: targetFrameRate, vSyncCount, captureFramerate, timeScale, the display mode and refresh rate, and the resolution the gameplay camera actually renders at, which in this game is not the window size.\nNothing is changed by any of it. Reading a clock is free and the log line is once an interval, not once a frame."); FrameProbeInterval = c.Bind<float>("Diagnostics", "FrameProbeInterval", 3f, "Seconds between frame-timing lines. Minimum half a second, because a shorter window averages too few frames to mean anything."); SpeedUpFairAiTurrets = c.Bind<bool>("Fixes", "SpeedUpFairAiTurrets", true, "Remember the answers Fair AI's turret rewrite works out from scratch every frame.\nFair AI replaces Turret.Update so turrets can see and shoot modded enemies. The first thing the replacement does, for every turret on every frame, is ask whether one of its own features is switched on - and it answers that by walking every setting in its config file, building two throwaway strings for each one. Fair AI ships 638 settings, so one question costs about 1,900 string allocations, and the setting it asks about is off, so the walk never even stops early. Fifty-seven turrets at 27fps is roughly 2.9 million string allocations a second.\nMeasured on Trite, which places 57 turrets: Turret.Update was 23-26ms of a 36ms frame, 75% of it, with garbage collection running three times a second against a 700MB heap. Switching the turrets off took the frame from 36.0ms to 8.9ms. It looks like a graphics problem and is not one - 31ms of that frame was main-thread work against 3ms of waiting, so no shadow, resolution or draw distance setting touches it.\nThose answers can only change when a setting does, so Fair AI's own code works each one out once and it is remembered until one does, which LethalConfig can still do mid-session. The turret's private state, reached through Type.GetField at 21 read sites, 46 writes and 5 method calls in the same per-frame method, is cached the same way - that one is real but far smaller, and on its own it measured no improvement at all.\nNothing is reimplemented and no turret behaves differently. Does nothing without Fair AI installed, and nothing measurable on a moon with only a few turrets."); FrameProbeTimers = c.Bind<bool>("Diagnostics", "FrameProbeTimers", false, "Time Update, LateUpdate and FixedUpdate on every script in the game and in every installed mod, and name the expensive ones in the log.\nThis is the one measurement that needs no suspect. A main-thread cost can live in any of a hundred and thirty-two mods, and switching categories off only tests what somebody thought to list, so instead every per-frame method is wrapped and the time is attributed by full type name. Whatever is eating the frame appears at the top.\nIt costs two clock reads and a dictionary lookup per call, against methods that cost microseconds to milliseconds each, and it wraps a few hundred methods at startup which adds a moment to loading. Not something to leave on - turn it off once the answer is in."); FrameProbeTimerRows = c.Bind<int>("Diagnostics", "FrameProbeTimerRows", 10, "How many of the most expensive scripts to name per report. Anything under a tenth of a millisecond a frame is left out regardless."); FrameProbeAblation = c.Bind<bool>("Diagnostics", "FrameProbeAblation", false, "After landing, switch categories of the scene off one at a time and log what each one was costing.\nThis is how a main-thread cost gets a name. Counting assets cannot do it: the moon under investigation is the heaviest in the pack on five metrics at once, so every one of them is a plausible culprit and at most one is real. Switching a thing off and reading the clock settles it - whatever gives the frame back is the cost, and a category that changes nothing is cleared however large its count.\nExpect about a minute of strangeness after landing: sound cuts out for a few seconds, particles freeze, turrets go inert, distant detail pops in. Each category is restored immediately after it is timed, and again if you leave the moon part way through, so nothing here persists. It runs once per landing.\nTurn this off once the answer is in - it is a measurement, not a feature."); FrameProbeAblationDelay = c.Bind<float>("Diagnostics", "FrameProbeAblationDelay", 12f, "Seconds to wait after landing before the first measurement, so that loading, spawning and the first pass of every cache are finished and not being timed."); FrameProbeAblationHold = c.Bind<float>("Diagnostics", "FrameProbeAblationHold", 4f, "Seconds to hold each category off while timing it. Longer is steadier and makes the whole run take proportionally longer."); FrameProbeAblationTypes = c.Bind<string>("Diagnostics", "FrameProbeAblationTypes", "JLL.Components.DamageTrigger, WesleysInteriorTools.Features.MoldSpikes, JLL.Components.JClientAttatchedObject, JLL.Components.DelayScript, CullFactory.Behaviours.CullingMethods.CullingMethod", "Extra component types to test, in addition to the built-in list of particles, audio, LOD groups, animators, OccludeAudio, turrets and interact triggers.\nFull type names, comma separated, and they must be Behaviours - anything else has no switch to turn. A name that is not loaded is reported and skipped, so listing a type from a mod you do not have is harmless.\nThis exists so that a suspect can be added without a rebuild."); MoonListGroupSize = c.Bind<int>("Terminal", "MoonListGroupSize", 0, "How many moons to print before a blank line in the >moons catalogue. 0 is one unbroken list.\nThe catalogue is drawn by TerminalFormatter, which puts a blank row after every third moon. That number is written into the method rather than read from anywhere, so there is no setting for it: it is a literal 3 in both TerminalFormatter's Moons node and TerminalUtils' MoonCatalogue, whichever ends up drawing the page. Three was a reasonable default for the game's eleven moons and is a poor one for a pack with sixty-six, where the gaps cost a fifth of the page and split the list against whatever it is sorted by.\nLethalLevelLoader's own 'Moons Catalogue Group Split Count' does not help. It only feeds LethalLevelLoader's text, and TerminalFormatter replaces that page before it is ever used, so the setting reads as if it works and does nothing.\nThis rewrites that literal in both mods to whatever is set here. Needs at least one of them; with neither installed nothing is patched and a line says so."); MoonListWeatherWidth = c.Bind<int>("Terminal", "MoonListWeatherWidth", 16, "Longest weather name the >moons catalogue prints, in characters. 0 leaves every name at full length.\nThe catalogue is a fixed-width table and every cell in a column is padded to that column's widest entry, so one long weather name moves the columns to its right for every moon on the page. With sixty-six moons the name column is 21 characters and the price 5, and the screen is 48 wide, which leaves 16 for the weather before the risk letter no longer fits and wraps onto a row of its own - one letter per moon, all the way down.\nNo weather any mod ships on its own comes near it; 'Meteor Shower' is the longest at 13. Combined weathers do: 'Eclipsed + Meteor Shower' is 24.\nAnything longer than this is printed with each of its parts replaced by the short name WeatherShortNames gives it - 'Eclip+Meteor' - or, for a part that setting does not name, WeatherRegistry's own five-letter cut, and trimmed with an ellipsis if even that is too long. Only the catalogue column is touched; the route page, which has the line to itself, keeps the full name.\nTerminalUtils has this same number as planetWeatherWidth and a branch that swaps in the short form when a name reaches it, but the next line overwrites the result with the full name whenever WeatherRegistry is installed, so the branch never survives."); SortUnrankedMoonsByLetter = c.Bind<bool>("Terminal", "SortUnrankedMoonsByLetter", true, "Sort moons whose risk level is not a plain letter by the letter it starts with.\nOnly does anything with TerminalUtils' 'Sort Info Type = Difficulty'. That sort strips + and - off the risk level and looks the rest up in Safe, F, E, D, C, B, A, S; anything it cannot find is given an index past the end of that list, so it lands at the bottom of the catalogue in whatever order it arrived.\nMost of a modded pack's moons are fine. The ones that are not are the moons whose author wrote a joke in the field, and they are not all jokes about difficulty: 'B.. for Bees' is a B, '???' is the game's own unknown marker, and both were being filed after Artifice.\nWith this on, a risk level that is not recognised is retried on its first character, so those two sort where they read - and a genuine joke with no letter in it, 'QUACK' or 'I see you', still sorts last, which is the right place for a secret moon. The order itself is read out of TerminalUtils rather than copied here, so it stays correct if that mod changes it."); WeatherShortNames = c.Bind<string>("Terminal", "WeatherShortNames", "Stormy=Storm, Rainy=Rain, Foggy=Fog, Flooded=Flood, Eclipsed=Eclip, Eclipsed Flare=EFlare, Meteor Shower=Meteor, Tornado=Torn, Earthquakes=Quake, Solar Flare=Flare, Heatwave=Heat, Snowfall=Snow, Blizzard=Blizz, Toxic Smog=Smog, Minefield=Mines, Hurricane=Hurr, Night Shift=Night, Hallowed=Hallow, Forsaken=Forsak, Dust Clouds=Dust, Aurora Borealis=Aurora, Cloudy=Cloud, Blackout=Dark", "What each weather is called when a combined weather has to be shortened to fit the >moons catalogue: Full name=Short, comma-separated. Spaces and case in the full name do not matter.\nWeatherRegistry's own short form is the first five letters, which turns 'Stormy + Solar Flare' into 'Storm+Solar' - readable only if you already know which is which. With six letters at most a part, any two combine within MoonListWeatherWidth. A weather not listed here falls back to those five letters."); ColorMoonListWeather = c.Bind<bool>("Terminal", "ColorMoonListWeather", true, "Colour each weather in the >moons catalogue as the ship's map screen does.\nWeatherRegistry gives every weather a colour and paints the map screen's weather line with it, part by part for a combined weather. The catalogue prints plain text. With this on, each part of the catalogue's weather cell takes the colour the map screen uses for it, so a shortened name reads by colour as well as by letters. Needs WeatherRegistry; off prints plain text."); } public static Dictionary<string, string> WeatherShortNameMap() { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (string item in Split(WeatherShortNames.Value)) { int num = item.IndexOf('='); if (num <= 0 || num == item.Length - 1) { Plugin.Log.LogWarning((object)("Ignoring WeatherShortNames entry '" + item + "': it is not Full name=Short.")); } else { dictionary[item.Substring(0, num).Trim()] = item.Substring(num + 1).Trim(); } } return dictionary; } public static List<string> CrateOnlyList() { List<string> list = new List<string>(); foreach (string item in Split(CrateOnly.Value)) { list.Add(item); } return list; } public static Dictionary<string, int> ShopList() { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); foreach (string item in Split(Stock.Value)) { int num = item.LastIndexOf('='); if (num <= 0) { Plugin.Log.LogWarning((object)("Ignoring Stock entry '" + item + "': it has no =Price.")); continue; } string key = item.Substring(0, num).Trim(); if (!int.TryParse(item.Substring(num + 1).Trim(), out var result) || result < 0) { Plugin.Log.LogWarning((object)("Ignoring Stock entry '" + item + "': the price is not a whole number of credits.")); } else { dictionary[key] = result; } } return dictionary; } public static Dictionary<string, string> ComponentList() { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(Components.Value)) { return dictionary; } string[] array = Components.Value.Split('|'); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } int num = text.IndexOf(':'); if (num <= 0) { Plugin.Log.LogWarning((object)("Ignoring Components entry '" + text + "': it has no ' : weights' after the item name.")); continue; } string text2 = text.Substring(0, num).Trim(); string text3 = text.Substring(num + 1).Trim(); if (text2.Length == 0 || text3.Length == 0) { Plugin.Log.LogWarning((object)("Ignoring Components entry '" + text + "': the name or the weights are empty.")); } else { dictionary[text2] = text3; } } return dictionary; } public static Dictionary<string, float> FloorList() { Dictionary<string, float> dictionary = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, string> item in Pairs(Floors.Value, "MinimumRates")) { if (!float.TryParse(item.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || result <= 0f) { Plugin.Log.LogWarning((object)("Ignoring MinimumRates entry '" + item.Key + " : " + item.Value + "': the number after the colon has to be how many landings one copy should take, like '4'.")); } else { dictionary[item.Key] = result; } } return dictionary; } public static List<WeatherRate> WeatherRateList() { List<WeatherRate> list = new List<WeatherRate>(); if (string.IsNullOrWhiteSpace(WeatherNameRates.Value)) { return list; } string[] array = WeatherNameRates.Value.Split('|'); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } string[] array2 = text.Split(':'); if (array2.Length != 3) { Plugin.Log.LogWarning((object)("Ignoring WeatherNameRates entry '" + text + "': it needs three parts, like 'Ruby Crystal : eclips : 2'.")); continue; } string text2 = array2[0].Trim(); string text3 = array2[1].Trim(); if (text2.Length == 0 || text3.Length == 0 || !float.TryParse(array2[2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || result <= 0f) { Plugin.Log.LogWarning((object)("Ignoring WeatherNameRates entry '" + text + "': it needs an item name, some text to look for in the weather's name, and how many matching landings one copy should take.")); continue; } list.Add(new WeatherRate { Item = text2, Match = text3, Landings = result }); } return list; } public static HashSet<string> FloorSkippedMoons() { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (string item in Split(FloorSkipMoons.Value)) { hashSet.Add(item); hashSet.Add(Numberless(item)); } return hashSet; } internal static string Numberless(string planetName) { if (string.IsNullOrEmpty(planetName)) { return planetName; } for (int i = 0; i < planetName.Length; i++) { if (char.IsLetter(planetName[i])) { if (i != 0) { return planetName.Substring(i); } return planetName; } } return planetName; } private static Dictionary<string, string> Pairs(string list, string setting) { Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrWhiteSpace(list)) { return dictionary; } string[] array = list.Split('|'); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } int num = text.IndexOf(':'); if (num <= 0) { Plugin.Log.LogWarning((object)("Ignoring " + setting + " entry '" + text + "': it has no ' : value' after the item name.")); continue; } string text2 = text.Substring(0, num).Trim(); string text3 = text.Substring(num + 1).Trim(); if (text2.Length == 0 || text3.Length == 0) { Plugin.Log.LogWarning((object)("Ignoring " + setting + " entry '" + text + "': the name or the value is empty.")); } else { dictionary[text2] = text3; } } return dictionary; } public static HashSet<string> RemovedList() { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (!RemoveCoinEconomy.Value) { return hashSet; } foreach (string item in Split(Removed.Value)) { hashSet.Add(item); } return hashSet; } public static HashSet<string> UnlockerCategories() { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (string item in Split(UnlockerPool.Value)) { hashSet.Add(item.ToLowerInvariant()); } foreach (string item2 in hashSet) { bool flag; switch (item2) { case "upgrades": case "decor": case "suits": case "lategame": flag = true; break; default: flag = false; break; } if (!flag) { Plugin.Log.LogWarning((object)("Pool lists '" + item2 + "', which is not one of upgrades, decor, suits or lategame. It matches nothing.")); } } return hashSet; } public static HashSet<string> UnlockerExcluded() { HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (string item in Split(UnlockerExclude.Value)) { hashSet.Add(item); } return hashSet; } internal static IEnumerable<string> Split(string list) { if (string.IsNullOrWhiteSpace(list)) { yield break; } string[] array = list.Split(','); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length > 0) { yield return text; } } } } internal static class ChameleonDoorGuard { private const string FixerTypeName = "Chameleon.Overrides.DoorMaterialsFixer"; private static bool applied; internal static bool TryApply() { //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown if (applied) { return true; } if (!Cfg.GuardChameleonDoorFixer.Value) { applied = true; return true; } try { Type type = AccessTools.TypeByName("Chameleon.Overrides.DoorMaterialsFixer"); if (type == null) { return false; } applied = true; MethodInfo methodInfo = AccessTools.DeclaredMethod(type, "Apply", Type.EmptyTypes, (Type[])null); if (methodInfo == null) { Plugin.Log.LogWarning((object)"Chameleon has no DoorMaterialsFixer.Apply() any more, so its door fixer is left unguarded. It may have fixed the null itself; if landings on Underwater Mines still log an unhandled RPC exception, this needs updating."); return true; } Plugin.HarmonyInstance.Patch((MethodBase)methodInfo, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(ChameleonDoorGuard), "Finalizer", (Type[])null), (HarmonyMethod)null); Plugin.Log.LogInfo((object)"Chameleon's door fixer can no longer abort level setup."); } catch (Exception ex) { applied = true; Plugin.Log.LogWarning((object)("Could not guard Chameleon's door fixer, so a landing on an interior it chokes on will still cut level setup short: " + ex.Message)); } return true; } private static Exception Finalizer(Exception __exception) { if (__exception == null) { return null; } Plugin.Log.LogWarning((object)("Chameleon's door material fixer failed on this interior and was stopped early (" + __exception.GetType().Name + ": " + __exception.Message + "). Doors keep their stock look here; the rest of level setup carries on.")); return null; } } [HarmonyPatch] internal static class Components { private static readonly List<DuskItemDefinition> _edited = new List<DuskItemDefinition>(); private static readonly PropertyInfo NameOf = AccessTools.Property(typeof(DuskContentDefinition), "EntityNameReference"); private static readonly Regex Whitespace = new Regex("\\s+"); private static readonly FieldInfo OperationField = AccessTools.Field(typeof(NamespacedConfigWeight), "MathOperation"); [HarmonyPatch(typeof(ContentHandler), "LoadAllContent", new Type[] { typeof(IAssetBundleLoader) })] [HarmonyPrefix] private static void BeforeBundleRegisters(IAssetBundleLoader bundle) { try { Apply(bundle); } catch (Exception ex) { Plugin.Log.LogError((object)("Left the crafting components as CodeRebirth shipped them, so they will not spawn: " + ex)); } } private static void Apply(IAssetBundleLoader bundle) { if (((bundle != null) ? bundle.Content : null) == null || NameOf == null) { return; } Dictionary<string, string> dictionary = Cfg.ComponentList(); if (dictionary.Count == 0) { return; } DuskContentDefinition[] content = bundle.Content; foreach (DuskContentDefinition val in content) { DuskItemDefinition val2 = (DuskItemDefinition)(object)((val is DuskItemDefinition) ? val : null); if (val2 == null) { continue; } string text = NameOf.GetValue(val) as string; if (string.IsNullOrEmpty(text) || !dictionary.TryGetValue(text, out var value)) { continue; } bool flag = string.Equals(value, "none", StringComparison.OrdinalIgnoreCase); List<NamespacedConfigWeight> list = (flag ? new List<NamespacedConfigWeight>() : ParseWeights(value)); if (!flag && (list == null || list.Count == 0)) { Plugin.Log.LogWarning((object)("'" + text + "' has weights DawnLib could not read, so it stays as it was: " + value)); continue; } AccessTools.Property(typeof(DuskItemDefinition), "MoonSpawnWeightsConfig")?.SetValue(val2, list); if (flag) { AccessTools.Property(typeof(DuskItemDefinition), "MoonSpawnWeights")?.SetValue(val2, string.Empty); Plugin.Log.LogInfo((object)(text + " no longer spawns as scrap.")); } else { _edited.Add(val2); Plugin.Log.LogInfo((object)(text + " now spawns as scrap: " + value)); } } } private static List<NamespacedConfigWeight> ParseWeights(string input) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown List<NamespacedConfigWeight> list = new List<NamespacedConfigWeight>(); if (string.IsNullOrWhiteSpace(input)) { return list; } string[] array = input.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { string[] array2 = Whitespace.Replace(text.Trim().ToLowerInvariant(), "_").Split(new char[1] { '=' }, StringSplitOptions.RemoveEmptyEntries); if (array2.Length == 0) { continue; } NamespacedConfigWeight val = new NamespacedConfigWeight { NamespacedKey = NamespacedKey.ForceParse(array2[0]) }; string value = "Additive"; float result = 0f; if (array2.Length > 1) { value = array2[1][0] switch { '-' => "Subtractive", '*' => "Multiplicative", '/' => "Divisive", _ => "Additive", }; if (!float.TryParse(array2[1], NumberStyles.Float, CultureInfo.InvariantCulture, out result) && !float.TryParse(array2[1].Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture, out result)) { Plugin.Log.LogWarning((object)("Could not read the weight in '" + text.Trim() + "', so it counts as zero.")); result = 0f; } } val.Weight = Math.Abs(result); OperationField?.SetValue(val, Enum.Parse(OperationField.FieldType, value)); list.Add(val); } return list; } internal static void Verify() { foreach (DuskItemDefinition item in _edited) { try { if (string.IsNullOrWhiteSpace((item == null) ? null : item.Config?.MoonSpawnWeights?.Value)) { string text = NameOf.GetValue(item) as string; Plugin.Log.LogWarning((object)("'" + text + "' still has no spawn weights after registration. Set '" + text + " | Allow Editing Config' to false in CodeRebirth.cfg, or write the weights into '" + text + " | Preset Moon Weights' by hand.")); } } catch (Exception ex) { Plugin.Log.LogDebug((object)("Could not check a component's weights: " + ex.Message)); } } _edited.Clear(); } } internal static class CrateStock { private const string CrateTypeName = "CodeRebirth.src.Content.Maps.ItemCrate"; private static FieldInfo pool; private static bool stocked; internal static void Apply() { //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Expected O, but got Unknown try { if (Cfg.CrateOnlyList().Count == 0) { return; } Type type = AccessTools.TypeByName("CodeRebirth.src.Content.Maps.ItemCrate"); if (!(type == null)) { pool = AccessTools.Field(type, "ShopItemList"); MethodInfo methodInfo = AccessTools.Method(type, "Start", (Type[])null, (Type[])null); if (pool == null || methodInfo == null) { Plugin.Log.LogWarning((object)"CodeRebirth's crate no longer keeps its pool in ItemCrate.ShopItemList, so nothing named in CrateOnly can be put in a crate. Nothing else is affected."); } else { Plugin.HarmonyInstance.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(CrateStock), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not reach CodeRebirth's crate pool, so nothing named in CrateOnly can be put in a crate: " + ex.Message)); } } private static void Postfix() { if (stocked) { return; } try { if (!(pool.GetValue(null) is List<Item> { Count: not 0 } list)) { return; } List<string> list2 = new List<string>(); foreach (string item in Cfg.CrateOnlyList()) { Item val = Find(item); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)("CrateOnly names '" + item + "', which no installed mod registered, so no crate can hold one. Check the spelling against the item's in-game name.")); } else if (!list.Contains(val)) { list.Add(val); list2.Add(val.itemName); } } stocked = true; if (list2.Count > 0) { Plugin.Log.LogInfo((object)("Wooden crates can now hold " + string.Join(", ", list2) + " - one entry each in a pool of " + list.Count + ".")); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not add the CrateOnly items to the crate pool: " + ex.Message)); stocked = true; } } private static Item Find(string name) { List<Item> list = StartOfRound.Instance?.allItemsList?.itemsList; if (list == null) { return null; } foreach (Item item in list) { if ((Object)(object)item != (Object)null && string.Equals(item.itemName, name, StringComparison.OrdinalIgnoreCase)) { return item; } } return null; } } [HarmonyPatch] internal static class DeadRadarBlips { private static bool reported; [HarmonyPrefix] [HarmonyPatch(typeof(TerminalAccessibleObject), "Update")] private static bool Prefix(TerminalAccessibleObject __instance) { if (!Cfg.SilenceDeadRadarBlips.Value) { return true; } if ((Object)(object)__instance.mapRadarObject != (Object)null) { return true; } if (!reported) { reported = true; Plugin.Log.LogInfo((object)("'" + Path(((Component)__instance).transform) + "' outlived the level its map icon belonged to, so its per-frame radar check is being skipped instead of throwing. Whatever that path is parented to is what kept it alive. Said once a session.")); } return false; } private static string Path(Transform t) { string text = ((Object)t).name; Transform parent = t.parent; while ((Object)(object)parent != (Object)null) { text = ((Object)parent).name + "/" + text; parent = parent.parent; } return text; } } internal static class EazyBakeReach { private const string OvenTypeName = "CodeRebirth.src.Content.Items.EazyBake"; private const string RegionTypeName = "CodeRebirth.src.Content.Items.EzBakeBakingRegion"; private const int OvenHitId = 745737; private static bool applied; private static bool explained; private static readonly Collider[] Overlapping = (Collider[])(object)new Collider[64]; private static readonly List<EnemyAI> Found = new List<EnemyAI>(); private static readonly Dictionary<GrabbableObject, Collider> Regions = new Dictionary<GrabbableObject, Collider>(); private static Type regionType; private static bool regionTypeResolved; internal static void Apply() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Expected O, but got Unknown //IL_0092: Expected O, but got Unknown if (applied) { return; } applied = true; try { Type type = AccessTools.TypeByName("CodeRebirth.src.Content.Items.EazyBake"); if (!(type == null)) { MethodInfo methodInfo = AccessTools.Method(type, "Update", (Type[])null, (Type[])null); if (methodInfo == null) { throw new MissingMethodException("CodeRebirth.src.Content.Items.EazyBake", "Update"); } Plugin.HarmonyInstance.Patch((MethodBase)methodInfo, new HarmonyMethod(AccessTools.Method(typeof(EazyBakeReach), "BeforeTick", (Type[])null, (Type[])null)), new HarmonyMethod(AccessTools.Method(typeof(EazyBakeReach), "AfterTick", (Type[])null, (Type[])null)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Log.LogInfo((object)"The EZ Bake Oven now reaches enemies its own trigger missed."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not patch the EZ Bake Oven, so it keeps whatever reach CodeRebirth gave it: " + ex.Message)); } } private static void BeforeTick(float ___timeSinceLastDamage, out float __state) { __state = ___timeSinceLastDamage; } private static void AfterTick(GrabbableObject __instance, float ___timeSinceLastDamage, List<Collider> ___collidersInside, List<EnemyAI> ___hitEnemies, PlayerControllerB ___playerWhoToggledOn, float __state) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) try { if (!Cfg.FixEazyBakeReach.Value || ___timeSinceLastDamage == __state || !__instance.isBeingUsed || ___hitEnemies.Count > 0) { return; } Collider val = RegionOf(__instance); if ((Object)(object)val == (Object)null) { return; } Found.Clear(); Collect(val, ((Component)__instance).transform.position); if (Found.Count == 0) { return; } Explain(___collidersInside, ((Component)__instance).transform.position); foreach (EnemyAI item in Found) { if (((NetworkBehaviour)item).IsOwner) { item.HitFromExplosion(4f); } item.HitEnemy(1, ___playerWhoToggledOn, false, 745737); } } catch (Exception ex) { Plugin.Log.LogError((object)("Could not extend the EZ Bake Oven's reach this tick: " + ex)); } } private static void Collect(Collider region, Vector3 oven) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) Bounds bounds = region.bounds; int num = Physics.OverlapBoxNonAlloc(((Bounds)(ref bounds)).center, ((Bounds)(ref bounds)).extents, Overlapping, Quaternion.identity, -1, (QueryTriggerInteraction)2); for (int i = 0; i < num; i++) { Collider val = Overlapping[i]; if ((Object)(object)val == (Object)null) { continue; } EnemyAICollisionDetect componentInParent = ((Component)val).GetComponentInParent<EnemyAICollisionDetect>(); if ((Object)(object)componentInParent == (Object)null || (Object)(object)componentInParent.mainScript == (Object)null) { continue; } EnemyAI mainScript = componentInParent.mainScript; if (!mainScript.isEnemyDead && !Found.Contains(mainScript)) { Bounds bounds2 = val.bounds; if (!Physics.Linecast(oven, ((Bounds)(ref bounds2)).center, StartOfRound.Instance.collidersAndRoomMaskAndDefault, (QueryTriggerInteraction)1)) { Found.Add(mainScript); } } } } private static void Explain(List<Collider> collidersInside, Vector3 oven) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) if (explained) { return; } explained = true; int num = 0; int num2 = 0; int num3 = 0; foreach (Collider item in collidersInside) { if (!((Object)(object)item == (Object)null) && !((Object)(object)((Component)item).GetComponentInParent<EnemyAICollisionDetect>() == (Object)null)) { num++; if (((Component)item).GetComponent<IHittable>() != null) { num2++; } if (Physics.Linecast(((Component)item).transform.position, oven, StartOfRound.Instance.collidersAndRoomMaskAndDefault, (QueryTriggerInteraction)2)) { num3++; } } } string text = ((num == 0) ? "its trigger volume never reported an enemy collider at all, which points at the physics layer the baking region is on" : ((num2 == 0) ? "the enemy colliders it saw carry no IHittable of their own, so its GetComponent lookup found nothing to hit" : ((num3 > 0) ? "its line-of-sight test was blocked, which is what asking for QueryTriggerInteraction.Collide does when any trigger is in the way" : "its own tick found nothing for a reason this check cannot name"))); Plugin.Log.LogInfo((object)($"EZ Bake Oven: hit {Found.Count} enemy(ies) its own detection missed. " + $"It saw {collidersInside.Count} collider(s), {num} of " + $"them an enemy's, {num2} with an IHittable, {num3} " + "blocked by line of sight - " + text + ".")); } private static Collider RegionOf(GrabbableObject oven) { if (Regions.TryGetValue(oven, out var value)) { return value; } Collider val = null; if (!regionTypeResolved) { regionTypeResolved = true; regionType = AccessTools.TypeByName("CodeRebirth.src.Content.Items.EzBakeBakingRegion"); } if (regionType != null) { Component componentInChildren = ((Component)oven).GetComponentInChildren(regionType); if (componentInChildren != null) { val = componentInChildren.GetComponent<Collider>(); } } if ((Object)(object)val == (Object)null) { Collider[] componentsInChildren = ((Component)oven).GetComponentsInChildren<Collider>(); foreach (Collider val2 in componentsInChildren) { if (val2.isTrigger) { val = val2; break; } } } if (Regions.Count > 32) { Regions.Clear(); } Regions[oven] = val; return val; } } internal static class EmoteCameraYaw { private const string ControllerTypeName = "TooManyEmotes.Patches.ThirdPersonEmoteController"; private const string KeybindsTypeName = "TooManyEmotes.Input.Keybinds"; private const string SettingsTypeName = "TooManyEmotes.Config.ConfigSettings"; private static bool applied; private static bool reported; private static bool stoodDown; private static FieldInfo pivot; private static Func<bool> moving; private static FieldInfo toggleRotate; private static FieldInfo toggledRotating; private static FieldInfo holdingRotate; internal static void Apply() { //IL_0152: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Expected O, but got Unknown if (applied) { return; } applied = true; if (!Cfg.SteadyEmoteCamera.Value) { return; } try { Type type = AccessTools.TypeByName("TooManyEmotes.Patches.ThirdPersonEmoteController"); if (!(type == null)) { Type type2 = AccessTools.TypeByName("TooManyEmotes.Input.Keybinds"); Type type3 = AccessTools.TypeByName("TooManyEmotes.Config.ConfigSettings"); pivot = AccessTools.Field(type, "emoteCameraPivot"); MethodInfo methodInfo = AccessTools.PropertyGetter(type, "isMovingWhileEmoting"); toggleRotate = ((type3 == null) ? null : AccessTools.Field(type3, "toggleRotateCharacterInEmote")); toggledRotating = ((type2 == null) ? null : AccessTools.Field(type2, "toggledRotating")); holdingRotate = ((type2 == null) ? null : AccessTools.Field(type2, "holdingRotatePlayerModifier")); if (pivot == null || methodInfo == null || toggleRotate == null || toggledRotating == null || holdingRotate == null) { Plugin.Log.LogWarning((object)"TooManyEmotes is installed but its third-person emote controller no longer has what this reads (emoteCameraPivot, isMovingWhileEmoting, the rotate-character keybind), so an emote you can walk in will still turn you on the spot alongside a camera mod that leans the view. Its code has changed."); return; } moving = (Func<bool>)Delegate.CreateDelegate(typeof(Func<bool>), methodInfo); Plugin.HarmonyInstance.Patch((MethodBase)AccessTools.Method(typeof(PlayerControllerB), "PlayerLookInput", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(EmoteCameraYaw), "After", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Log.LogInfo((object)"An emote you can walk in no longer turns you on the spot."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not hook the emote camera, so an emote you can walk in may turn you on the spot: " + ex.GetType().Name + ": " + ex.Message)); } } private static void After() { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) if (stoodDown) { return; } try { object? value = pivot.GetValue(null); Transform val = (Transform)((value is Transform) ? value : null); if (val == null || (Object)(object)val == (Object)null) { return; } Vector3 localEulerAngles = val.localEulerAngles; if (localEulerAngles.y != 0f && Moving() && !Rotating()) { val.localEulerAngles = new Vector3(localEulerAngles.x, 0f, localEulerAngles.z); if (!reported) { reported = true; Plugin.Log.LogInfo((object)("The emote camera carried " + ((localEulerAngles.y > 180f) ? (localEulerAngles.y - 360f) : localEulerAngles.y).ToString("0.00") + " degrees of yaw from another camera mod, which would have been added to your facing every frame of the emote. Cleared; said once a session.")); } } } catch (Exception ex) { stoodDown = true; Plugin.Log.LogWarning((object)("The emote camera guard threw and has stood down for this session, so an emote you can walk in may turn you on the spot: " + ex.GetType().Name + ": " + ex.Message)); } } private static bool Moving() { try { return moving(); } catch { return false; } } private static bool Rotating() { object value = ((toggleRotate.GetValue(null) is ConfigEntry<bool> val && val.Value) ? toggledRotating : holdingRotate).GetValue(null); bool flag = default(bool); int num; if (value is bool) { flag = (bool)value; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } } internal static class EmotePoseRestore { private sealed class Pose { public Transform Bone; public Vector3 Position; public Quaternion Rotation; } private sealed class Carried { public GrabbableObject Item; public Transform Holder; } private sealed class Before { public readonly List<Carried> Items = new List<Carried>(); public readonly List<Pose> Bones = new List<Pose>(); } private const string ControllerTypeName = "TooManyEmotes.EmoteControllerPlayer"; private const string BaseTypeName = "TooManyEmotes.EmoteController"; private const string EmoteTypeName = "TooManyEmotes.UnlockableEmote"; private static readonly string[] HoldLayers = new string[2] { "HoldingItemsRightHand", "HoldingItemsBothHands" }; private static readonly ConditionalWeakTable<object, Before> Saved = new ConditionalWeakTable<object, Before>(); private static bool applied; private static bool reported; private static FieldInfo boneMap; private static FieldInfo armBoneMap; private static FieldInfo performing; private static FieldInfo owner; private static MethodInfo setGrabBools; internal static void Apply() { //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01c3: Expected O, but got Unknown //IL_01d3: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Expected O, but got Unknown //IL_01ea: Unknown result type (might be due to invalid IL or missing references) //IL_01f1: Expected O, but got Unknown if (applied) { return; } applied = true; if (!Cfg.RestorePoseAfterEmote.Value) { return; } try { Type type = AccessTools.TypeByName("TooManyEmotes.EmoteControllerPlayer"); if (type == null) { return; } Type type2 = AccessTools.TypeByName("TooManyEmotes.EmoteController"); Type type3 = AccessTools.TypeByName("TooManyEmotes.UnlockableEmote"); boneMap = ((type2 == null) ? null : AccessTools.Field(type2, "boneMap")); armBoneMap = AccessTools.Field(type, "boneMapLocalPlayerArms"); performing = ((type2 == null) ? null : AccessTools.Field(type2, "isPerformingEmote")); owner = AccessTools.Field(type, "playerController"); setGrabBools = AccessTools.Method(typeof(PlayerControllerB), "SetSpecialGrabAnimationBool", new Type[2] { typeof(bool), typeof(GrabbableObject) }, (Type[])null); MethodInfo methodInfo = ((type3 == null) ? null : AccessTools.Method(type, "PerformEmote", new Type[3] { type3, typeof(int), typeof(bool) }, (Type[])null)); MethodInfo methodInfo2 = ((type2 == null) ? null : AccessTools.Method(type, "SyncWithEmoteController", new Type[2] { type2, typeof(int) }, (Type[])null)); MethodInfo methodInfo3 = AccessTools.Method(type, "StopPerformingEmote", Type.EmptyTypes, (Type[])null); if (boneMap == null || performing == null || owner == null || methodInfo == null || methodInfo3 == null) { Plugin.Log.LogWarning((object)"TooManyEmotes is installed but its emote controller no longer has what this reads (boneMap, isPerformingEmote, playerController, PerformEmote, StopPerformingEmote), so a held item will still sit wrong after an emote until it is dropped. Its code has changed."); return; } HarmonyMethod val = new HarmonyMethod(typeof(EmotePoseRestore), "BeforeStart", (Type[])null); HarmonyMethod val2 = new HarmonyMethod(typeof(EmotePoseRestore), "AfterStart", (Type[])null); HarmonyMethod val3 = new HarmonyMethod(typeof(EmotePoseRestore), "AfterStop", (Type[])null); Plu