Decompiled source of MapsNotSoRandom v1.0.0

MapsNotSoRandom.dll

Decompiled 18 hours ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("MapsNotSoRandom")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyDescription("Weighted random map selection with per-map level limits for R.E.P.O.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("MapsNotSoRandom")]
[assembly: AssemblyTitle("MapsNotSoRandom")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace MapsNotSoRandom;

internal static class ModInfo
{
	public const string Guid = "Local.MapsNotSoRandom";

	public const string Name = "MapsNotSoRandom";

	public const string Version = "1.0.0";
}
internal sealed class MapSettings
{
	public ConfigEntry<float> Weight;

	public ConfigEntry<int> MinLevel;

	public ConfigEntry<int> MaxLevel;

	public ConfigEntry<int> BoostFromLevel;

	public ConfigEntry<float> BoostMultiplier;
}
internal sealed class Candidate
{
	public Level Map;

	public string Name;

	public float Weight;
}
[BepInPlugin("Local.MapsNotSoRandom", "MapsNotSoRandom", "1.0.0")]
public class MapsNotSoRandomPlugin : BaseUnityPlugin
{
	private static ManualLogSource log;

	private static ConfigFile cfg;

	private static ConfigEntry<bool> modEnabled;

	private static ConfigEntry<bool> avoidRepeat;

	private static ConfigEntry<int> levelOffset;

	private static ConfigEntry<bool> logDraws;

	private static readonly Dictionary<string, MapSettings> settings = new Dictionary<string, MapSettings>();

	private static readonly HashSet<string> failedMaps = new HashSet<string>();

	private static readonly HashSet<string> usedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	private static readonly char[] invalidConfigChars = new char[9] { '=', '\n', '\r', '\t', '\\', '"', '\'', '[', ']' };

	private static string lastPickedMap;

	private static bool scanErrorLogged;

	private static bool levelCounterLookupDone;

	private static FieldInfo levelsCompletedField;

	private static bool levelCounterWarned;

	private void Awake()
	{
		log = ((BaseUnityPlugin)this).Logger;
		cfg = ((BaseUnityPlugin)this).Config;
		((Component)this).gameObject.transform.parent = null;
		((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
		BindGeneralSettings();
		PatchGame();
		((MonoBehaviour)this).StartCoroutine(ScanLoop());
		log.LogInfo((object)"MapsNotSoRandom 1.0.0 loaded.");
	}

	private static void BindGeneralSettings()
	{
		//IL_0062: Unknown result type (might be due to invalid IL or missing references)
		//IL_006c: Expected O, but got Unknown
		modEnabled = cfg.Bind<bool>("General", "Enabled", true, "Master switch. When false, this mod does nothing and the game picks maps the vanilla way.");
		avoidRepeat = cfg.Bind<bool>("General", "Avoid Repeating Last Map", false, "When true, the map played on the previous level is removed from the pool for the next draw (unless it is the only eligible map).");
		levelOffset = cfg.Bind<int>("General", "Level Number Offset", 0, new ConfigDescription("Added to the level number used by Min Level, Max Level and Boost From Level. By default the first map of a run is level 1. Use +1 or -1 if the numbers you set do not line up with the level counter you see in game.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(-10, 10), Array.Empty<object>()));
		logDraws = cfg.Bind<bool>("General", "Log Draws", true, "When true, every draw prints the level number, each eligible map with its chance, and the result to the BepInEx log.");
	}

	private static void PatchGame()
	{
		//IL_0053: Unknown result type (might be due to invalid IL or missing references)
		//IL_005b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0068: Expected O, but got Unknown
		try
		{
			MethodBase methodBase = AccessTools.DeclaredMethod(typeof(RunManager), "SetRunLevel", (Type[])null, (Type[])null);
			if (methodBase == null)
			{
				log.LogError((object)"RunManager.SetRunLevel was not found (the game may have been updated). Map selection stays vanilla.");
				return;
			}
			MethodInfo method = typeof(MapsNotSoRandomPlugin).GetMethod("SetRunLevelPostfix", BindingFlags.Static | BindingFlags.NonPublic);
			new Harmony("Local.MapsNotSoRandom").Patch(methodBase, (HarmonyMethod)null, new HarmonyMethod(method), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			log.LogInfo((object)"Hooked RunManager.SetRunLevel.");
		}
		catch (Exception ex)
		{
			log.LogError((object)("Could not hook the game: " + ex));
		}
	}

	private IEnumerator ScanLoop()
	{
		WaitForSeconds pause = new WaitForSeconds(2f);
		while (true)
		{
			try
			{
				RunManager rm = RunManager.instance;
				if ((Object)(object)rm != (Object)null)
				{
					SyncMaps(rm);
				}
			}
			catch (Exception ex)
			{
				Exception e = ex;
				if (!scanErrorLogged)
				{
					scanErrorLogged = true;
					log.LogError((object)("Scanning the map list failed: " + e));
				}
			}
			yield return pause;
		}
	}

	private static void SyncMaps(RunManager rm)
	{
		if (rm.levels == null)
		{
			return;
		}
		List<string> list = null;
		bool saveOnConfigSet = cfg.SaveOnConfigSet;
		cfg.SaveOnConfigSet = false;
		try
		{
			foreach (Level level in rm.levels)
			{
				if ((Object)(object)level == (Object)null)
				{
					continue;
				}
				string name = ((Object)level).name;
				if (string.IsNullOrEmpty(name) || settings.ContainsKey(name) || failedMaps.Contains(name))
				{
					continue;
				}
				try
				{
					settings[name] = BindMap(level);
					if (list == null)
					{
						list = new List<string>();
					}
					list.Add(name);
				}
				catch (Exception ex)
				{
					failedMaps.Add(name);
					log.LogError((object)("Could not create settings for map '" + name + "' (it will use Weight 1, no level limits): " + ex));
				}
			}
		}
		finally
		{
			cfg.SaveOnConfigSet = saveOnConfigSet;
		}
		if (list != null)
		{
			cfg.Save();
			log.LogInfo((object)("Found " + list.Count + " new map(s): " + string.Join(", ", list.ToArray()) + ". Their settings are now in the config file."));
		}
	}

	private static MapSettings BindMap(Level level)
	{
		//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b3: Expected O, but got Unknown
		//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f0: Expected O, but got Unknown
		//IL_0123: Unknown result type (might be due to invalid IL or missing references)
		//IL_012d: Expected O, but got Unknown
		//IL_0160: Unknown result type (might be due to invalid IL or missing references)
		//IL_016a: Expected O, but got Unknown
		//IL_019a: Unknown result type (might be due to invalid IL or missing references)
		//IL_01a4: Expected O, but got Unknown
		string name = ((Object)level).name;
		string text = MakeSectionName(name);
		string narrativeName = GetNarrativeName(level);
		string text2 = ((narrativeName == null || !(narrativeName != name)) ? ("'" + name + "'") : ("'" + narrativeName + "' (" + name + ")"));
		MapSettings mapSettings = new MapSettings();
		mapSettings.Weight = cfg.Bind<float>(text, "Weight", 1f, new ConfigDescription("Relative chance of " + text2 + " being drawn on levels where it is eligible. The real chance is this weight divided by the sum of the weights of all eligible maps, so the same weight everywhere means equal chances. 0 = never drawn.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1000f), Array.Empty<object>()));
		mapSettings.MinLevel = cfg.Bind<int>(text, "Min Level", 1, new ConfigDescription("First level number (inclusive) on which " + text2 + " can be drawn. The first map of a run is level 1, so 1 = no minimum. Example: 4 = it only enters the pool from the 4th map on.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 999), Array.Empty<object>()));
		mapSettings.MaxLevel = cfg.Bind<int>(text, "Max Level", 0, new ConfigDescription("Last level number (inclusive) on which " + text2 + " can be drawn. 0 = no maximum, the map never leaves the pool.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 999), Array.Empty<object>()));
		mapSettings.BoostFromLevel = cfg.Bind<int>(text, "Boost From Level", 0, new ConfigDescription("From this level number on (inclusive), the Weight of " + text2 + " is multiplied by Boost Multiplier. 0 = boost disabled.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 999), Array.Empty<object>()));
		mapSettings.BoostMultiplier = cfg.Bind<float>(text, "Boost Multiplier", 1f, new ConfigDescription("Factor applied to Weight once Boost From Level is reached. 1 = no change, 2 = twice as likely, 0.5 = half as likely.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1000f), Array.Empty<object>()));
		return mapSettings;
	}

	private static string MakeSectionName(string levelName)
	{
		string text = levelName;
		char[] array = invalidConfigChars;
		foreach (char oldChar in array)
		{
			text = text.Replace(oldChar, '_');
		}
		text = text.Trim();
		if (text.Length == 0 || text.Equals("General", StringComparison.OrdinalIgnoreCase))
		{
			text = "Map - " + text;
		}
		string text2 = text;
		int num = 2;
		while (!usedSections.Add(text2))
		{
			text2 = text + " (" + num + ")";
			num++;
		}
		return text2;
	}

	private static string GetNarrativeName(Level level)
	{
		try
		{
			FieldInfo field = typeof(Level).GetField("NarrativeName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null)
			{
				string text = field.GetValue(level) as string;
				if (!string.IsNullOrEmpty(text))
				{
					return text.Replace('\n', ' ').Replace('\r', ' ');
				}
			}
		}
		catch (Exception)
		{
		}
		return null;
	}

	private static void SetRunLevelPostfix(RunManager __instance)
	{
		try
		{
			if (modEnabled != null && modEnabled.Value && SemiFunc.IsMasterClientOrSingleplayer())
			{
				DrawNextMap(__instance);
			}
		}
		catch (Exception ex)
		{
			log.LogError((object)("Map draw failed, keeping the game's own pick: " + ex));
		}
	}

	private static void DrawNextMap(RunManager rm)
	{
		if ((Object)(object)rm == (Object)null || rm.levels == null || rm.levels.Count == 0)
		{
			return;
		}
		Level levelCurrent = rm.levelCurrent;
		if ((Object)(object)levelCurrent == (Object)null || !rm.levels.Contains(levelCurrent))
		{
			return;
		}
		SyncMaps(rm);
		int levelsCompleted = GetLevelsCompleted(rm);
		int num = Math.Max(1, Math.Max(levelsCompleted, 0) + 1 + levelOffset.Value);
		if (levelsCompleted <= 0)
		{
			lastPickedMap = null;
		}
		List<Candidate> list = new List<Candidate>();
		HashSet<string> hashSet = new HashSet<string>();
		StringBuilder stringBuilder = new StringBuilder();
		foreach (Level level in rm.levels)
		{
			if ((Object)(object)level == (Object)null || !hashSet.Add(((Object)level).name))
			{
				continue;
			}
			float num2 = 1f;
			string text = null;
			if (settings.TryGetValue(((Object)level).name, out var value))
			{
				num2 = value.Weight.Value;
				if (num < value.MinLevel.Value)
				{
					text = "Min Level " + value.MinLevel.Value;
				}
				else if (value.MaxLevel.Value > 0 && num > value.MaxLevel.Value)
				{
					text = "Max Level " + value.MaxLevel.Value;
				}
				else
				{
					if (value.BoostFromLevel.Value > 0 && num >= value.BoostFromLevel.Value)
					{
						num2 *= value.BoostMultiplier.Value;
					}
					if (float.IsNaN(num2) || num2 <= 0f)
					{
						text = "weight 0";
					}
				}
			}
			if (text != null)
			{
				AppendReason(stringBuilder, ((Object)level).name, text);
				continue;
			}
			Candidate candidate = new Candidate();
			candidate.Map = level;
			candidate.Name = ((Object)level).name;
			candidate.Weight = num2;
			list.Add(candidate);
		}
		if (avoidRepeat.Value && lastPickedMap != null && list.Count > 1)
		{
			int num3 = list.FindIndex((Candidate x) => x.Name == lastPickedMap);
			if (num3 >= 0)
			{
				list.RemoveAt(num3);
				AppendReason(stringBuilder, lastPickedMap, "played last");
			}
		}
		if (list.Count == 0)
		{
			log.LogWarning((object)("No map is eligible on level " + num + " (check Weight / Min Level / Max Level). Keeping the game's own pick: " + ((Object)levelCurrent).name));
			return;
		}
		float num4 = 0f;
		foreach (Candidate item in list)
		{
			num4 += item.Weight;
		}
		float num5 = Random.Range(0f, num4);
		Candidate candidate2 = list[list.Count - 1];
		float num6 = 0f;
		foreach (Candidate item2 in list)
		{
			num6 += item2.Weight;
			if (num5 < num6)
			{
				candidate2 = item2;
				break;
			}
		}
		rm.levelCurrent = candidate2.Map;
		lastPickedMap = candidate2.Name;
		if (logDraws.Value)
		{
			StringBuilder stringBuilder2 = new StringBuilder();
			stringBuilder2.Append("Map draw for level ").Append(num).Append(" (levels completed: ")
				.Append(levelsCompleted)
				.Append(')');
			foreach (Candidate item3 in list)
			{
				float num7 = item3.Weight / num4 * 100f;
				stringBuilder2.Append("\n  ").Append((item3 == candidate2) ? "> " : "  ").Append(item3.Name)
					.Append("   weight ")
					.Append(item3.Weight.ToString("0.##", CultureInfo.InvariantCulture))
					.Append("   chance ")
					.Append(num7.ToString("0.0", CultureInfo.InvariantCulture))
					.Append('%');
			}
			if (stringBuilder.Length > 0)
			{
				stringBuilder2.Append("\n  Not eligible: ").Append(stringBuilder.ToString());
			}
			stringBuilder2.Append("\n  Picked: ").Append(candidate2.Name);
			log.LogInfo((object)stringBuilder2.ToString());
		}
		else
		{
			log.LogInfo((object)("Level " + num + ": picked " + candidate2.Name));
		}
	}

	private static void AppendReason(StringBuilder sb, string mapName, string reason)
	{
		if (sb.Length > 0)
		{
			sb.Append(", ");
		}
		sb.Append(mapName).Append(" (").Append(reason)
			.Append(')');
	}

	private static int GetLevelsCompleted(RunManager rm)
	{
		try
		{
			if (!levelCounterLookupDone)
			{
				levelCounterLookupDone = true;
				levelsCompletedField = typeof(RunManager).GetField("levelsCompleted", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			}
			if (levelsCompletedField != null)
			{
				return Convert.ToInt32(levelsCompletedField.GetValue(rm), CultureInfo.InvariantCulture);
			}
			int num = ReadLevelFromStats();
			if (num >= 0)
			{
				if (!levelCounterWarned)
				{
					levelCounterWarned = true;
					log.LogWarning((object)"RunManager.levelsCompleted was not found; using StatsManager.runStats[\"level\"] instead. If Min/Max Level feel off by one, adjust 'Level Number Offset' in [General].");
				}
				return num;
			}
		}
		catch (Exception ex)
		{
			if (!levelCounterWarned)
			{
				levelCounterWarned = true;
				log.LogError((object)("Could not read the level counter: " + ex));
			}
			return 0;
		}
		if (!levelCounterWarned)
		{
			levelCounterWarned = true;
			log.LogError((object)"Could not find the game's level counter. Every draw is treated as level 1 (+ offset), so Min Level / Max Level will not work as intended.");
		}
		return 0;
	}

	private static int ReadLevelFromStats()
	{
		Type type = typeof(RunManager).Assembly.GetType("StatsManager");
		if (type == null)
		{
			return -1;
		}
		BindingFlags bindingAttr = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
		FieldInfo field = type.GetField("instance", bindingAttr);
		FieldInfo field2 = type.GetField("runStats", bindingAttr);
		if (field == null || field2 == null)
		{
			return -1;
		}
		object value = field.GetValue(null);
		if (value == null)
		{
			return -1;
		}
		if (field2.GetValue(value) is Dictionary<string, int> dictionary && dictionary.TryGetValue("level", out var value2))
		{
			return value2;
		}
		return -1;
	}
}