using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using Burst2Flame;
using Burst2Flame.Observable;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using RoguelikeScaling.Patches;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("RoguelikeScaling")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Per-level player scaling curves for Stolen Realm roguelike mode")]
[assembly: AssemblyFileVersion("1.0.2.0")]
[assembly: AssemblyInformationalVersion("1.0.2+a99f982db5f8bc1d22f4d2708298cc99f4e3932d")]
[assembly: AssemblyProduct("RoguelikeScaling")]
[assembly: AssemblyTitle("RoguelikeScaling")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.2.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 RoguelikeScaling
{
internal static class CurveMath
{
private static readonly Dictionary<long, float> Cache = new Dictionary<long, float>();
private const int CacheLimit = 512;
internal static float Compound(int level, float growthPerLevel, int pivot)
{
int num = level - pivot;
if (num <= 0 || growthPerLevel <= 0f)
{
return 1f;
}
long key = ((long)(growthPerLevel * 100000f) << 32) ^ (uint)((level << 8) ^ pivot);
if (Cache.TryGetValue(key, out var value))
{
return value;
}
float num2 = (float)Math.Pow(1f + growthPerLevel, num);
if (Cache.Count >= 512)
{
Cache.Clear();
}
Cache[key] = num2;
return num2;
}
internal static float Weighted(float growthPerLevel, float weight, int level, int pivot)
{
if (weight <= 0f)
{
return 1f;
}
if (Math.Abs(weight - 1f) < 0.0001f)
{
return Compound(level, growthPerLevel, pivot);
}
return Compound(level, growthPerLevel * weight, pivot);
}
internal static void ClearCache()
{
Cache.Clear();
}
}
internal static class ScaleLevel
{
private const int Fallback = 1;
internal static int For(Character character)
{
if (character == null)
{
return 1;
}
if (!RunGate.IsRoguelike())
{
return Clamp(character.Level);
}
return Clamp(RoguelikeBattleLevel(character));
}
private static int RoguelikeBattleLevel(Character character)
{
try
{
int roguelikeBattleLevel = RoguelikeManager.RoguelikeBattleLevel;
return (roguelikeBattleLevel > 0) ? roguelikeBattleLevel : character.Level;
}
catch
{
return character.Level;
}
}
private static int Clamp(int level)
{
if (level >= 1)
{
return level;
}
return 1;
}
}
internal static class RunGate
{
private const long RetryDelayTicks = 10000000L;
private static bool _cachedRoguelike;
private static long _nextRetry;
internal static bool ShouldScale(Character character)
{
if (character == null || character.IsAI)
{
return false;
}
if (!ScalingConfig.Enabled.Value)
{
return false;
}
if (ScalingConfig.RoguelikeOnly.Value)
{
return IsRoguelike();
}
return true;
}
internal static bool IsNetworked()
{
try
{
return (Object)(object)NetworkingManager.Instance != (Object)null && NetworkingManager.Instance.NetworkManager != null;
}
catch
{
return false;
}
}
internal static bool IsRoguelike()
{
long ticks = DateTime.UtcNow.Ticks;
if (ticks < _nextRetry)
{
return _cachedRoguelike;
}
try
{
_cachedRoguelike = (Object)(object)Game.Instance != (Object)null && Game.Instance.RoguelikeModeActive;
_nextRetry = 0L;
}
catch
{
_cachedRoguelike = false;
_nextRetry = ticks + 10000000;
}
return _cachedRoguelike;
}
}
[BepInPlugin("com.stolenrealm.roguelikescaling", "Roguelike Scaling", "1.0.2")]
[BepInProcess("Stolen Realm.exe")]
public class RoguelikeScalingPlugin : BaseUnityPlugin
{
internal static ManualLogSource Logger;
internal static bool IsShuttingDown;
private Harmony _harmony;
private void Awake()
{
//IL_004a: Unknown result type (might be due to invalid IL or missing references)
//IL_0054: Expected O, but got Unknown
Logger = ((BaseUnityPlugin)this).Logger;
try
{
ScalingConfig.Bind((BaseUnityPlugin)(object)this);
}
catch (Exception arg)
{
Logger.LogError((object)$"Config binding failed, disabling mod: {arg}");
((Behaviour)this).enabled = false;
return;
}
Logger.LogInfo((object)"=== Roguelike Scaling v1.0.2 ===");
try
{
_harmony = new Harmony("com.stolenrealm.roguelikescaling");
_harmony.PatchAll();
Logger.LogInfo((object)"Harmony patches applied.");
}
catch (Exception arg2)
{
Logger.LogError((object)$"PatchAll failed: {arg2}");
}
Logger.LogInfo((object)("Curves ready: growth=" + Pct(ScalingConfig.GrowthPerLevel.Value) + "/level past level " + $"{ScalingConfig.PivotLevel.Value}, weapon={Pct(ScalingConfig.WeaponGrowthPerLevel.Value)}/level, " + "enemyScaling=" + (ScalingConfig.AiTailEnabled.Value ? "on" : "off") + "."));
Logger.LogInfo((object)"Note: this game tears down BepInEx_Manager shortly after startup, so the plugin's own Update() never runs a frame. All scaling patches are on game assemblies and are unaffected; the balance probe is hosted on MainMenu instead (see ProbeStartPatch).");
LogStartupDiagnostics();
}
private string GameObjectName()
{
if (!((Object)(object)((Component)this).gameObject == (Object)null))
{
return ((Object)((Component)this).gameObject).name;
}
return "<null>";
}
private string SceneName()
{
//IL_0014: Unknown result type (might be due to invalid IL or missing references)
//IL_0019: Unknown result type (might be due to invalid IL or missing references)
try
{
object result;
if (!((Object)(object)((Component)this).gameObject == (Object)null))
{
Scene scene = ((Component)this).gameObject.scene;
result = ((Scene)(ref scene)).name;
}
else
{
result = "<null>";
}
return (string)result;
}
catch
{
return "<error>";
}
}
private static string Pct(float value)
{
return (value * 100f).ToString("0.#", CultureInfo.InvariantCulture) + "%";
}
private void LogStartupDiagnostics()
{
if (!ScalingConfig.LogLevelUps.Value)
{
return;
}
Logger.LogInfo((object)"--- startup diagnostics ---");
MethodBase[] array = ((_harmony != null) ? _harmony.GetPatchedMethods().ToArray() : Array.Empty<MethodBase>());
Logger.LogInfo((object)$"Harmony reports {array.Length} patched methods:");
foreach (MethodBase item in from m in array
orderby m.DeclaringType?.Name, m.Name
select m)
{
Logger.LogInfo((object)(" patched: " + item.DeclaringType?.FullName + "." + item.Name));
}
int value = ScalingConfig.PivotLevel.Value;
Logger.LogInfo((object)"Curve preview (attribute multiplier):");
int[] array2 = new int[11]
{
1, 3, 5, 8, 10, 12, 15, 20, 25, 30,
40
};
foreach (int num2 in array2)
{
Logger.LogInfo((object)($" L{num2,-3} xAttr={CurveMath.Compound(num2, ScalingConfig.GrowthPerLevel.Value, value),7:F3}" + $" xWpn={CurveMath.Compound(num2, ScalingConfig.WeaponGrowthPerLevel.Value, ScalingConfig.WeaponPivotLevel.Value),7:F3}" + $" xEnemyHP={CurveMath.Compound(num2, ScalingConfig.AiHealthGrowthPerLevel.Value, ScalingConfig.AiPivotLevel.Value),7:F3}"));
}
Logger.LogInfo((object)"--- end diagnostics ---");
}
private void OnDestroy()
{
IsShuttingDown = true;
Logger.LogInfo((object)"Roguelike Scaling unloaded (BepInEx_Manager torn down by the game).");
}
private static int ComputeConfigHash()
{
return ((((((((((((((17 * 31 + (ScalingConfig.Enabled?.Value.GetHashCode() ?? 0)) * 31 + (ScalingConfig.RoguelikeOnly?.Value.GetHashCode() ?? 0)) * 31 + ScalingConfig.GrowthPerLevel.Value.GetHashCode()) * 31 + ScalingConfig.PivotLevel.Value) * 31 + ScalingConfig.HealthWeight.Value.GetHashCode()) * 31 + ScalingConfig.ManaWeight.Value.GetHashCode()) * 31 + ScalingConfig.ArmorWeight.Value.GetHashCode()) * 31 + ScalingConfig.AbilityPowerWeight.Value.GetHashCode()) * 31 + ScalingConfig.WeaponGrowthPerLevel.Value.GetHashCode()) * 31 + ScalingConfig.WeaponWeight.Value.GetHashCode()) * 31 + ScalingConfig.BonusStatPointsPerLevel.Value) * 31 + (ScalingConfig.AiTailEnabled?.Value.GetHashCode() ?? 0)) * 31 + ScalingConfig.AiPivotLevel.Value) * 31 + ScalingConfig.AiHealthGrowthPerLevel.Value.GetHashCode()) * 31 + ScalingConfig.AiDamageGrowthPerLevel.Value.GetHashCode();
}
}
public static class ModInfo
{
public const string PluginGuid = "com.stolenrealm.roguelikescaling";
public const string PluginName = "Roguelike Scaling";
public const string PluginVersion = "1.0.2";
}
internal static class ScalingConfig
{
internal const string CatGeneral = "General";
internal const string CatCurves = "Curves";
internal const string CatAttributes = "Attribute Weights";
internal const string CatAi = "Enemy Scaling";
internal const string CatLog = "Diagnostics";
internal static ConfigEntry<bool> Enabled;
internal static ConfigEntry<bool> RoguelikeOnly;
internal static ConfigEntry<float> GrowthPerLevel;
internal static ConfigEntry<int> PivotLevel;
internal static ConfigEntry<float> HealthWeight;
internal static ConfigEntry<float> ManaWeight;
internal static ConfigEntry<float> ArmorWeight;
internal static ConfigEntry<float> AbilityPowerWeight;
internal static ConfigEntry<float> WeaponGrowthPerLevel;
internal static ConfigEntry<int> WeaponPivotLevel;
internal static ConfigEntry<float> WeaponWeight;
internal static ConfigEntry<int> BonusStatPointsPerLevel;
internal static ConfigEntry<bool> AiTailEnabled;
internal static ConfigEntry<int> AiPivotLevel;
internal static ConfigEntry<float> AiHealthGrowthPerLevel;
internal static ConfigEntry<float> AiDamageGrowthPerLevel;
internal static ConfigEntry<bool> LogLevelUps;
internal static ConfigEntry<bool> ShowScalingInStatScreen;
internal static ConfigEntry<bool> ShowVanillaValues;
internal static ConfigEntry<string> ProbeMode;
internal static ConfigEntry<int> ProbeMaxLevel;
internal static ConfigEntry<bool> ProbeQuitWhenDone;
internal static readonly string[] ScaledAttributes = new string[5] { "MaxHealth", "MaxMana", "Armor", "MagicArmor", "AbilityPower" };
internal static void Bind(BaseUnityPlugin plugin)
{
Enabled = plugin.Config.Bind<bool>("General", "Enabled", true, "Master switch. When false the mod applies no scaling at all and the game behaves as vanilla.");
RoguelikeOnly = plugin.Config.Bind<bool>("General", "RoguelikeOnly", true, "Only scale characters while a roguelike run is active. Turn off to also scale campaign characters.");
LogLevelUps = plugin.Config.Bind<bool>("Diagnostics", "LogLevelUps", true, "Write a calibration table to BepInEx/LogOutput.log every time a character levels up, showing the scaled MaxHealth, MaxMana, AbilityPower, Armor and WeaponDamage plus the curve multiplier at that level.");
ShowScalingInStatScreen = plugin.Config.Bind<bool>("Diagnostics", "ShowScalingInStatScreen", true, "Show the multiplier the mod applied next to each affected stat on the character screen (press C) and on the roguelike level-up attribute panel. Only stats actually being scaled are annotated, and nothing is shown at levels where the curve is still 1.");
ShowVanillaValues = plugin.Config.Bind<bool>("Diagnostics", "ShowVanillaValues", false, "Also show the un-scaled value in brackets next to the multiplier, e.g. \"x1.35 (6087)\". Handy for checking what the game would have given you without the mod.");
ProbeMode = plugin.Config.Bind<string>("Diagnostics", "ProbeMode", "", "DEV. Leave empty for normal play. Set to \"balance\" to run the headless balance probe on the next launch: it enters a roguelike run, force-levels the party hero, records every stat at each level, writes roguelike_scaling_probe.txt next to the game executable, then quits. Only the first character is selected so the party is a single hero with a predictable stat spread.");
ProbeMaxLevel = plugin.Config.Bind<int>("Diagnostics", "ProbeMaxLevel", 25, "Highest level the balance probe walks the hero up to.");
ProbeQuitWhenDone = plugin.Config.Bind<bool>("Diagnostics", "ProbeQuitWhenDone", true, "Quit the game once the balance probe has written its report. Turn off to inspect the result in game.");
GrowthPerLevel = plugin.Config.Bind<float>("Curves", "GrowthPerLevel", 0.02f, "Compounding rate per level past the pivot for health, mana, armor and ability power. Measured default is deliberately mild: the vanilla curves already keep player health at 2.7-4.0x a baseline-100 enemy across the whole run, so only a small top-up is warranted. Raise this only if late-game damage output still feels short.");
PivotLevel = plugin.Config.Bind<int>("Curves", "PivotLevel", 5, "Levels at or below this are left completely untouched so the opening of a run still plays like vanilla. Compounding begins at PivotLevel + 1.");
HealthWeight = plugin.Config.Bind<float>("Attribute Weights", "MaxHealth", 1f, "Weight applied to the master rate for MaxHealth. 1.0 follows GrowthPerLevel, 0.5 halves it, 0 disables it.");
ManaWeight = plugin.Config.Bind<float>("Attribute Weights", "MaxMana", 1f, "Weight applied to the master rate for MaxMana. 0 disables mana scaling.");
ArmorWeight = plugin.Config.Bind<float>("Attribute Weights", "ArmorAndMagicArmor", 1f, "Weight applied to the master rate for Armor and MagicArmor. Raise this if enemies outscale your mitigation.");
AbilityPowerWeight = plugin.Config.Bind<float>("Attribute Weights", "AbilityPower", 1f, "Weight applied to the master rate for AbilityPower. AbilityPower multiplies all outgoing damage and healing, so this is the single strongest lever on how fast a character snowballs.");
WeaponGrowthPerLevel = plugin.Config.Bind<float>("Curves", "WeaponGrowthPerLevel", 0.055f, "Compounding rate per weapon item level for weapon damage. This is the main balance lever. Vanilla weapon damage grows only about 6x from level 1 to 25 while a baseline-100 enemy grows about 214x, so weapon damage as a share of enemy health collapses roughly 34x over a run and turns-to-kill stretches from about 2.1 to 11.2. Measured turns-to-kill at the shipped default stays in a 2.0-3.0 band for the whole run instead. Raise it if combat still drags late.");
WeaponPivotLevel = plugin.Config.Bind<int>("Curves", "WeaponPivotLevel", 0, "Levels at or below this are untouched for weapon damage. Weapon damage needs to compound from level 1 because the deficit exists across the whole run, not only at depth, so this defaults to 0 rather than the attribute pivot.");
WeaponWeight = plugin.Config.Bind<float>("Attribute Weights", "WeaponDamage", 1f, "Weight applied to WeaponGrowthPerLevel for weapon damage. 0 disables weapon scaling.");
BonusStatPointsPerLevel = plugin.Config.Bind<int>("Attribute Weights", "BonusStatPointsPerLevel", 0, "Extra attribute points granted per level past the pivot, on top of the points the game already awards. 0 leaves the point budget alone. This is a blunt lever: attributes feed MaxHealth, AbilityPower, Armor and dodge simultaneously.");
AiTailEnabled = plugin.Config.Bind<bool>("Enemy Scaling", "Enabled", false, "Also extend enemy scaling past the level where the game's own multiplier table runs out. Off by default so the first playtest only measures the player curves. The roguelike branch of the game lerps the final two table nodes forever with no compounding, so enemies effectively flatten out at high levels.");
AiPivotLevel = plugin.Config.Bind<int>("Enemy Scaling", "PivotLevel", 30, "Enemy scaling stays on the game's own curve up to this level, then compounds. Matches the level at which the campaign code path starts its own exponential tail.");
AiHealthGrowthPerLevel = plugin.Config.Bind<float>("Enemy Scaling", "HealthGrowthPerLevel", 0.03f, "Enemy MaxHealth compounding rate per level past the enemy pivot, on top of the game's own curve.");
AiDamageGrowthPerLevel = plugin.Config.Bind<float>("Enemy Scaling", "DamageGrowthPerLevel", 0.02f, "Enemy damage compounding rate per level past the enemy pivot, on top of the game's own curve.");
}
}
internal static class TargetLookup
{
internal static MethodBase Find(string typeName, string methodName, params Type[] parameters)
{
Type type = AccessTools.TypeByName(typeName);
if (type == null)
{
ManualLogSource logger = RoguelikeScalingPlugin.Logger;
if (logger != null)
{
logger.LogWarning((object)("Type '" + typeName + "' not found, patch on " + typeName + "." + methodName + " skipped."));
}
return null;
}
MethodInfo methodInfo = AccessTools.Method(type, methodName, parameters, (Type[])null);
if ((MethodBase?)methodInfo == (MethodBase?)null)
{
ManualLogSource logger2 = RoguelikeScalingPlugin.Logger;
if (logger2 == null)
{
return methodInfo;
}
logger2.LogWarning((object)$"{typeName}.{methodName}({parameters.Length} params) not found, patch skipped.");
}
return methodInfo;
}
internal static MethodBase PropertyGetter(string typeName, string propertyName)
{
Type type = AccessTools.TypeByName(typeName);
if (type == null)
{
ManualLogSource logger = RoguelikeScalingPlugin.Logger;
if (logger != null)
{
logger.LogWarning((object)("Type '" + typeName + "' not found, patch on " + typeName + "." + propertyName + " skipped."));
}
return null;
}
MethodInfo methodInfo = AccessTools.PropertyGetter(type, propertyName);
if ((MethodBase?)methodInfo == (MethodBase?)null)
{
ManualLogSource logger2 = RoguelikeScalingPlugin.Logger;
if (logger2 == null)
{
return methodInfo;
}
logger2.LogWarning((object)(typeName + "." + propertyName + " getter not found, patch skipped."));
}
return methodInfo;
}
}
public static class PluginInfo
{
public const string PLUGIN_GUID = "RoguelikeScaling";
public const string PLUGIN_NAME = "RoguelikeScaling";
public const string PLUGIN_VERSION = "1.0.2";
}
}
namespace RoguelikeScaling.Probe
{
internal static class BalanceProbe
{
private sealed class Row
{
public int Level;
public float Health;
public float Mana;
public float AbilityPower;
public float Armor;
public float MagicArmor;
public float WeaponDamage;
public float Might;
public float Vitality;
public int StatPoints;
public float Curve;
public float WeaponCurve;
}
private enum Phase
{
Idle,
DumpEnemyCurves,
WaitMainMenu,
EnterRun,
WaitChoiceScreen,
WaitGameLogic,
SelectCharacters,
Accept,
WaitPartyHero,
WaitLevels,
ShowStatScreen,
Done
}
private static Phase _phase = Phase.Idle;
private static ManualLogSource _log;
private static int _maxLevel;
private static bool _quitWhenDone;
private static float _deadline;
private static float _settleUntil;
private static int _currentLevel;
private static bool _entryDone;
private static int _tickCount;
private static bool _levelFallbackLogged;
private static Character _hero;
private static readonly List<Row> Rows = new List<Row>();
private static readonly List<float[]> EnemyRows = new List<float[]>();
private static readonly string[] StatNames = new string[5] { "Might", "Dexterity", "Intelligence", "Vitality", "Reflex" };
private static int _statCursor;
private static bool _weaponWarned;
private static bool _screenshotPending;
private static bool _statScreenOpened;
private static int _statScreenPolls;
internal static void Begin(ManualLogSource log, int maxLevel, bool quitWhenDone)
{
if (_phase == Phase.Idle && RefusesToTouchRealSave(log))
{
_log = log;
_maxLevel = Math.Max(1, maxLevel);
_quitWhenDone = quitWhenDone;
_phase = Phase.DumpEnemyCurves;
ProbeSuppressLevelUpWindowsPatch.Active = true;
log.LogInfo((object)$"[PROBE] started (maxLevel={_maxLevel}).");
}
}
private static bool RefusesToTouchRealSave(ManualLogSource log)
{
string text = Try(() => Application.persistentDataPath);
if (string.IsNullOrEmpty(text))
{
log.LogWarning((object)"[PROBE] could not resolve the save folder; refusing to run to be safe.");
return false;
}
try
{
string path = Path.Combine(text, "QuestSaveRoguelike.json");
if (File.Exists(path))
{
log.LogError((object)("[PROBE] REFUSING TO RUN: a roguelike run is in progress (" + Path.GetFileName(path) + " in " + text + "). The probe enters a real run and would overwrite it. Move it aside, or test on a throwaway profile."));
return false;
}
string[] files = Directory.GetFiles(text, "Character*.json");
foreach (string path2 in files)
{
string text2 = File.ReadAllText(path2);
bool num2 = text2.Contains("\"IsRoguelikeCharacter\":true");
bool flag = text2.Contains("\"CharacterPresetFileGuid\":\"\"") || text2.Contains("\"CharacterPresetFileGuid\":null");
if (num2 && flag)
{
log.LogError((object)("[PROBE] REFUSING TO RUN: " + Path.GetFileName(path2) + " is a saved roguelike character with no CharacterPresetFile. That state hangs island loading on the next run, so the probe will not add to it. Delete that character first, or test on a throwaway profile. Save folder: " + text));
return false;
}
}
}
catch (Exception ex)
{
log.LogWarning((object)("[PROBE] could not inspect the save folder, refusing to run: " + ex.Message));
return false;
}
return true;
}
internal static void Tick()
{
if (_phase == Phase.Idle || _phase == Phase.Done || _log == null)
{
return;
}
try
{
_tickCount++;
if (_phase == Phase.WaitLevels && _tickCount % 3000 == 0)
{
Character probeHero = _hero;
ManualLogSource log = _log;
string[] obj = new string[5]
{
$"[PROBE] tick#{_tickCount} phase={_phase} target={_currentLevel + 1} ",
"heroLevel=",
null,
null,
null
};
Character obj2 = probeHero;
obj[2] = ((obj2 != null) ? obj2.Level.ToString() : null) ?? "none";
obj[3] = " ";
obj[4] = $"exp={SafeF(delegate
{
Character obj3 = probeHero;
return (obj3 == null) ? 0f : obj3.Experience;
}):F0}";
log.LogInfo((object)string.Concat(obj));
}
Advance();
}
catch (Exception arg)
{
_log.LogError((object)$"[PROBE] aborted in phase {_phase}: {arg}");
Finish(success: false);
}
}
private static void Advance()
{
switch (_phase)
{
case Phase.DumpEnemyCurves:
DumpEnemyCurves();
_phase = Phase.WaitMainMenu;
_deadline = Time.realtimeSinceStartup + 180f;
break;
case Phase.WaitMainMenu:
if ((Object)(object)MainMenu.Instance != (Object)null)
{
_log.LogInfo((object)"[PROBE] MainMenu found, entering roguelike (single player).");
_phase = Phase.EnterRun;
}
else if (Time.realtimeSinceStartup > _deadline)
{
Fail("MainMenu.Instance never appeared");
}
break;
case Phase.EnterRun:
if (!_entryDone)
{
_entryDone = true;
MainMenu instance2 = MainMenu.Instance;
if (instance2 != null)
{
instance2.ChooseRoguelikeMode();
}
(AccessTools.Method(typeof(MainMenu), "StartSinglePlayer", new Type[1] { typeof(bool) }, (Type[])null) ?? AccessTools.Method(typeof(MainMenu), "StartSinglePlayer", (Type[])null, (Type[])null))?.Invoke(instance2, new object[1] { false });
_log.LogInfo((object)"[PROBE] roguelike entry invoked; waiting for the choice screen.");
}
_phase = Phase.WaitChoiceScreen;
_deadline = Time.realtimeSinceStartup + 120f;
break;
case Phase.WaitChoiceScreen:
if ((Object)(object)CharacterChoiceManager.Instance != (Object)null)
{
_log.LogInfo((object)"[PROBE] choice screen up.");
_phase = Phase.WaitGameLogic;
_deadline = Time.realtimeSinceStartup + 90f;
}
else if (Time.realtimeSinceStartup > _deadline)
{
Fail("CharacterChoiceManager.Instance never appeared");
}
break;
case Phase.WaitGameLogic:
if ((Object)(object)GameLogic.instance != (Object)null)
{
_phase = Phase.SelectCharacters;
}
else if (Time.realtimeSinceStartup > _deadline)
{
Fail("GameLogic.instance never appeared");
}
break;
case Phase.SelectCharacters:
SelectCharacters();
_phase = Phase.Accept;
break;
case Phase.Accept:
{
GameLogic instance = GameLogic.instance;
AccessTools.Method(typeof(GameLogic), "AcceptCharacterChoices", new Type[2]
{
typeof(bool),
typeof(bool)
}, (Type[])null)?.Invoke(instance, new object[2] { false, false });
_log.LogInfo((object)"[PROBE] accepted; waiting for the party hero.");
_phase = Phase.WaitPartyHero;
_deadline = Time.realtimeSinceStartup + 120f;
break;
}
case Phase.WaitPartyHero:
{
Character val = FindPartyHero();
if (val != null)
{
_hero = val;
_log.LogInfo((object)("[PROBE] party hero '" + SafeStr(() => _hero.CharacterName) + "' " + $"level={_hero.Level} unarmed={_hero.IsUnarmed} equipped={_hero.EquippedItems?.Count ?? 0}"));
_phase = Phase.WaitLevels;
_currentLevel = 0;
}
else if (Time.realtimeSinceStartup > _deadline)
{
Fail("no party character appeared");
}
break;
}
case Phase.WaitLevels:
WalkLevels();
if (_screenshotPending)
{
_phase = Phase.ShowStatScreen;
_screenshotPending = false;
_deadline = Time.realtimeSinceStartup + 45f;
}
break;
case Phase.ShowStatScreen:
ShowStatScreen();
break;
}
}
private static void WalkLevels()
{
Character hero = _hero;
if (hero == null)
{
return;
}
if (_currentLevel == 0)
{
_log.LogInfo((object)($"[PROBE] party hero '{SafeStr(() => hero.CharacterName)}' level={hero.Level} " + $"unarmed={hero.IsUnarmed} equipped={hero.EquippedItems?.Count ?? 0}"));
}
int num = _currentLevel + 1;
if (num > _maxLevel)
{
AttributeScalingPatch.Audit();
_screenshotPending = true;
return;
}
if (hero.Level < num && _settleUntil == 0f)
{
int level = hero.Level;
float num2 = SafeF(() => hero.Experience);
try
{
hero.GiveExperience(hero.ExpForNextLevel * 2f, false);
}
catch (Exception ex)
{
_log.LogWarning((object)$"[PROBE] GiveExperience failed toward level {num}: {ex.Message}");
}
if (hero.Level <= level && !_levelFallbackLogged)
{
_levelFallbackLogged = true;
_log.LogWarning((object)($"[PROBE] GiveExperience did not raise the level (level={level}, " + $"exp={num2:F0}, expForNext={SafeF(() => hero.ExpForNextLevel):F0}). " + "Falling back to setting Level directly."));
}
if (hero.Level <= level)
{
try
{
hero.Level = num;
_log.LogInfo((object)$"[PROBE] set Level={num} directly (now {hero.Level}).");
}
catch (Exception ex2)
{
_log.LogWarning((object)$"[PROBE] could not set Level={num}: {ex2.Message}");
}
}
_settleUntil = Time.realtimeSinceStartup + 0.4f;
}
if (_settleUntil == 0f || !(Time.realtimeSinceStartup < _settleUntil))
{
_settleUntil = 0f;
_currentLevel = num;
SpendStatPoints(hero);
EquipWeapon(hero, _currentLevel);
Row row = ReadRow(hero);
Rows.Add(row);
LogRow(row);
}
}
private static void ShowStatScreen()
{
Character hero = _hero;
if (hero == null)
{
Finish(success: true);
return;
}
CharacterMenusManager val = Try(() => CharacterMenusManager.Instance);
if ((Object)(object)val == (Object)null)
{
_log.LogWarning((object)"[PROBE] CharacterMenusManager.Instance was null; skipping stat screen shot.");
Finish(success: true);
return;
}
if (!_statScreenOpened)
{
_statScreenOpened = true;
_log.LogInfo((object)"[PROBE] opening the character screen for a screenshot...");
try
{
val.OpenCharacterMenu();
}
catch (Exception ex)
{
_log.LogWarning((object)("[PROBE] OpenCharacterMenu failed: " + ex.Message));
}
}
InventoryManager inventory = FindInventoryWindow();
if ((Object)(object)inventory != (Object)null)
{
_statScreenPolls++;
if (_statScreenPolls == 1 || _statScreenPolls % 30 == 0)
{
_log.LogInfo((object)("[PROBE] InventoryManager found (" + Describe(inventory) + "); forcing a stat refresh."));
}
Try(delegate
{
int num = ActivateAncestors(((Component)inventory).transform);
if (num > 0)
{
_log.LogInfo((object)$"[PROBE] activated {num} inactive ancestor(s) of the InventoryManager.");
}
return "ok";
});
Try(delegate
{
inventory.RefreshCharacter(hero, false);
return "ok";
});
MethodInfo updateStatRows = AccessTools.Method(typeof(InventoryManager), "UpdateStatRows", new Type[1] { typeof(Character) }, (Type[])null);
if (updateStatRows == null)
{
_log.LogWarning((object)"[PROBE] InventoryManager.UpdateStatRows was not found; cannot force a refresh.");
}
else
{
Try(delegate
{
updateStatRows.Invoke(inventory, new object[1] { hero });
return "ok";
});
}
}
else if (_statScreenPolls == 0)
{
_log.LogInfo((object)"[PROBE] InventoryManager not in the scene yet; waiting for the window to load...");
_statScreenPolls++;
}
if (Time.realtimeSinceStartup < _deadline)
{
return;
}
if ((Object)(object)inventory != (Object)null)
{
Try(delegate
{
int num = ActivateAncestors(((Component)inventory).transform);
if (num > 0)
{
_log.LogInfo((object)$"[PROBE] activated {num} more ancestor(s) before the capture.");
}
return "ok";
});
Try(delegate
{
inventory.Initialize(hero);
return "ok";
});
Try(delegate
{
inventory.RefreshCharacter(hero, false);
return "ok";
});
MethodInfo finalRows = AccessTools.Method(typeof(InventoryManager), "UpdateStatRows", new Type[1] { typeof(Character) }, (Type[])null);
Try(delegate
{
finalRows?.Invoke(inventory, new object[1] { hero });
return "ok";
});
_log.LogInfo((object)("[PROBE] final stat refresh issued (" + Describe(inventory) + ")."));
}
else
{
_log.LogWarning((object)"[PROBE] InventoryManager never appeared, so the capture will not show the stat screen.");
}
try
{
_log.LogInfo((object)"[PROBE] capturing the character screen...");
ScreenCapture.CaptureScreenshot("roguelike_scaling_stats.png");
}
catch (Exception ex2)
{
_log.LogWarning((object)("[PROBE] screenshot failed: " + ex2.Message));
}
_log.LogInfo((object)"[PROBE] screenshot requested; it lands in Stolen Realm_Data\\roguelike_scaling_stats.png");
Finish(success: true);
}
private static InventoryManager FindInventoryWindow()
{
InventoryManager val = Try(() => Object.FindObjectOfType<InventoryManager>());
if ((Object)(object)val != (Object)null)
{
return val;
}
return Try((Func<InventoryManager>)delegate
{
Object[] array = (Object[])(object)Resources.FindObjectsOfTypeAll<InventoryManager>();
array = array;
foreach (Object obj in array)
{
InventoryManager val2 = (InventoryManager)(object)((obj is InventoryManager) ? obj : null);
if ((Object)(object)val2 != (Object)null)
{
return val2;
}
}
return (InventoryManager)null;
});
}
private static int ActivateAncestors(Transform transform)
{
int num = 0;
Transform val = transform;
while ((Object)(object)val != (Object)null && (Object)(object)((Component)val).gameObject != (Object)null && !((Component)val).gameObject.activeSelf)
{
((Component)val).gameObject.SetActive(true);
num++;
val = val.parent;
}
return num;
}
private static string Describe(InventoryManager inventory)
{
return SafeStr(delegate
{
//IL_0044: Unknown result type (might be due to invalid IL or missing references)
//IL_0049: Unknown result type (might be due to invalid IL or missing references)
string text = $"activeInHierarchy={((Component)inventory).gameObject.activeInHierarchy} ";
object arg = ((Component)inventory).gameObject.activeSelf;
Scene scene = ((Component)inventory).gameObject.scene;
return text + $"activeSelf={arg} scene='{((Scene)(ref scene)).name}'";
});
}
private static void EquipWeapon(Character hero, int level)
{
try
{
if (hero.EquippedItems != null)
{
foreach (Item equippedItem in hero.EquippedItems)
{
if (equippedItem is Weapon)
{
hero.EquippedItems.Remove(equippedItem);
break;
}
}
}
List<ItemInfo> list = Try(delegate
{
Game instance = Game.Instance;
return (instance == null) ? null : instance.Items;
});
if (list == null || list.Count == 0)
{
if (!_weaponWarned)
{
_weaponWarned = true;
_log.LogWarning((object)"[PROBE] Game.Instance.Items was empty; cannot equip a weapon.");
}
return;
}
ItemInfo weaponInfo = null;
foreach (ItemInfo item in list)
{
if (item is WeaponInfo)
{
weaponInfo = item;
break;
}
}
if ((Object)(object)weaponInfo == (Object)null)
{
if (!_weaponWarned)
{
_weaponWarned = true;
_log.LogWarning((object)"[PROBE] no WeaponInfo found in the item pool.");
}
return;
}
GameLogic logic = Try(() => GameLogic.instance);
if ((Object)(object)logic == (Object)null)
{
return;
}
Item val = Try(() => logic.CreateItem(weaponInfo, hero, level, 0, 0, false, false, false));
if (val == null)
{
if (!_weaponWarned)
{
_weaponWarned = true;
_log.LogWarning((object)"[PROBE] CreateItem returned null for the probe weapon.");
}
}
else
{
hero.EquipItem(val, false);
}
}
catch (Exception ex)
{
if (!_weaponWarned)
{
_weaponWarned = true;
_log.LogWarning((object)("[PROBE] EquipItemInfo failed: " + ex.Message));
}
}
}
private static void SpendStatPoints(Character hero)
{
int num = SafeI(() => hero.UnspentStatPoints);
if (num <= 0)
{
return;
}
int num2 = 0;
int num3 = 0;
while (num > 0 && num3++ < 500)
{
string name = StatNames[_statCursor++ % StatNames.Length];
CharacterAttribute val = Try(delegate
{
Game instance = Game.Instance;
return (instance == null) ? null : instance.GetAttribute(name);
});
if (!((Object)(object)val == (Object)null))
{
try
{
InventoryManager.AddStatToCharacter(hero, val, 1);
num--;
num2++;
}
catch (Exception ex)
{
_log.LogWarning((object)("[PROBE] AddStatToCharacter(" + name + ") failed: " + ex.Message));
return;
}
}
}
if (num2 > 0)
{
_log.LogInfo((object)$"[PROBE] spent {num2} stat point(s) at level {hero.Level}");
}
}
private static Row ReadRow(Character hero)
{
int level = hero.Level;
float curve = CurveMath.Compound(level, ScalingConfig.GrowthPerLevel.Value, ScalingConfig.PivotLevel.Value);
float weaponCurve = CurveMath.Weighted(ScalingConfig.WeaponGrowthPerLevel.Value, ScalingConfig.WeaponWeight.Value, level, ScalingConfig.WeaponPivotLevel.Value);
return new Row
{
Level = level,
Health = SafeF(() => hero.MaxHealth),
Mana = SafeF(() => hero.MaxMana),
AbilityPower = SafeF(() => hero.AbilityPower),
Armor = SafeF(() => hero["Armor"]),
MagicArmor = SafeF(() => hero["MagicArmor"]),
WeaponDamage = SafeF(() => hero.WeaponDamage),
Might = SafeF(() => hero.Might),
Vitality = SafeF(() => hero.Vitality),
StatPoints = SafeI(() => hero.UnspentStatPoints),
Curve = curve,
WeaponCurve = weaponCurve
};
}
private static void LogRow(Row r)
{
_log.LogInfo((object)($"[PROBE] L{r.Level,-3} xAttr={r.Curve,6:F3} xWpn={r.WeaponCurve,6:F2}" + $" | HP {Divide(r.Health, r.Curve),7:F0}->{r.Health,7:F0}" + $" | Mana {Divide(r.Mana, r.Curve),6:F0}->{r.Mana,6:F0}" + $" | AP {Divide(r.AbilityPower, r.Curve),6:F1}->{r.AbilityPower,6:F1}" + $" | Armor {Divide(r.Armor, r.Curve),5:F0}->{r.Armor,5:F0}" + $" | Wpn {Divide(r.WeaponDamage, r.WeaponCurve),6:F0}->{r.WeaponDamage,6:F0}" + $" | Might {r.Might,5:F0} Vit {r.Vitality,5:F0} SP {r.StatPoints}"));
}
private static void DumpEnemyCurves()
{
MethodInfo methodInfo = TargetLookup.Find("Character", "GetAIHealth", typeof(int), typeof(float), typeof(int), typeof(float), typeof(float), typeof(float), typeof(int), typeof(bool)) as MethodInfo;
MethodInfo methodInfo2 = TargetLookup.Find("Character", "GetAIPower", typeof(int), typeof(float), typeof(float), typeof(int), typeof(float), typeof(float), typeof(int), typeof(bool)) as MethodInfo;
if (methodInfo == null)
{
_log.LogWarning((object)"[PROBE] GetAIHealth unavailable, skipping enemy curve dump.");
return;
}
_log.LogInfo((object)"[PROBE] === ENEMY CURVES (baseHealth=100, basePower=100, 1 player) ===");
_log.LogInfo((object)"[PROBE] lvl enemyHP hpRate% enemyPow powRate%");
float num = 0f;
float num2 = 0f;
for (int i = 1; i <= _maxLevel; i++)
{
float num3 = Convert.ToSingle(methodInfo.Invoke(null, new object[8] { i, 100f, 1, 1f, 1f, 1f, 1, false }), CultureInfo.InvariantCulture);
float num4 = ((methodInfo2 == null) ? 0f : Convert.ToSingle(methodInfo2.Invoke(null, new object[8] { i, 100f, 1f, 1, 1f, 1f, 1, false }), CultureInfo.InvariantCulture));
string text = ((num > 0f) ? ((num3 / num - 1f) * 100f).ToString("F1", CultureInfo.InvariantCulture) : "-");
string text2 = ((num2 > 0f) ? ((num4 / num2 - 1f) * 100f).ToString("F1", CultureInfo.InvariantCulture) : "-");
_log.LogInfo((object)$"[PROBE] {i,-4} {num3,8:F1} {text,7} {num4,9:F1} {text2,8}");
EnemyRows.Add(new float[3] { i, num3, num4 });
num = num3;
num2 = num4;
}
}
private static bool TrySetPreset(Character hero, Root root)
{
try
{
List<CharacterPresetFile> list = Try(delegate
{
Game instance = Game.Instance;
return (instance == null) ? null : instance.CharacterPresetFiles?.ToList();
}) ?? new List<CharacterPresetFile>();
if (list.Count == 0)
{
_log.LogError((object)"[PROBE] no character presets are available to assign.");
return false;
}
CharacterPresetFile val = ((IEnumerable<CharacterPresetFile>)list).FirstOrDefault((Func<CharacterPresetFile, bool>)((CharacterPresetFile p) => (Object)(object)p != (Object)null && (int)p.PresetType == 1)) ?? ((IEnumerable<CharacterPresetFile>)list).FirstOrDefault((Func<CharacterPresetFile, bool>)((CharacterPresetFile p) => (Object)(object)p != (Object)null && (int)p.PresetType == 0)) ?? ((IEnumerable<CharacterPresetFile>)list).FirstOrDefault((Func<CharacterPresetFile, bool>)((CharacterPresetFile p) => (Object)(object)p != (Object)null));
if ((Object)(object)val == (Object)null)
{
_log.LogError((object)"[PROBE] every available character preset was null.");
return false;
}
hero.CharacterPresetFileGuid = val.Guid.ToString();
if ((Object)(object)hero.CharacterPresetFile == (Object)null)
{
_log.LogError((object)$"[PROBE] assigned preset {val.Guid} but CharacterPresetFile is still null.");
return false;
}
_log.LogInfo((object)$"[PROBE] assigned preset '{val.PresetName}' ({val.Guid}).");
return true;
}
catch (Exception ex)
{
_log.LogError((object)("[PROBE] could not assign a character preset: " + ex.Message));
return false;
}
}
private static void SelectCharacters()
{
GameLogic logic = GameLogic.instance;
Root root = Try(delegate
{
NetworkingManager instance2 = NetworkingManager.Instance;
return (instance2 == null) ? null : ((Observer<Root>)(object)instance2.NetworkManager)?.Root;
});
if (root == null)
{
_log.LogError((object)"[PROBE] could not resolve Root; cannot build a character.");
return;
}
int num = 0;
try
{
num = ((NetworkObserver<Root>)(object)NetworkingManager.Instance.NetworkManager).NetworkId;
}
catch (Exception ex)
{
_log.LogWarning((object)("[PROBE] could not read local network id: " + ex.Message));
}
List<Character> list = Try(() => logic.AllMyCharacters?.ToList()) ?? new List<Character>();
_log.LogInfo((object)$"[PROBE] {list.Count} existing character(s), netId={num}");
Character hero = ((IEnumerable<Character>)list).FirstOrDefault((Func<Character, bool>)((Character c) => c != null && !c.IsAI));
if (hero == null)
{
hero = Try(() => root.CreateNewHumanCharacter(-1));
if (hero == null)
{
_log.LogError((object)"[PROBE] CreateNewHumanCharacter returned null; cannot continue.");
return;
}
hero.CharacterName = "Probe Hero";
hero.Guid = Guid.NewGuid().ToString();
hero.IsRoguelikeCharacter = true;
hero.CharacterType = (CharacterType)0;
hero.TeamIndex = 0;
hero.OwnerID = num;
hero.ControllerPlayerId = num;
if (!TrySetPreset(hero, root))
{
_log.LogError((object)"[PROBE] refusing to continue: could not assign a CharacterPresetFile. A saved roguelike character without one hangs island loading.");
return;
}
try
{
if ((Object)(object)hero.PlayerMovement == (Object)null)
{
hero.CreatePlayerMovement((PlayerMovement)null, (BoundsInfo?)null, true);
}
}
catch (Exception ex2)
{
_log.LogWarning((object)("[PROBE] CreatePlayerMovement failed: " + ex2.Message));
}
try
{
hero.SetValues();
}
catch (Exception ex3)
{
_log.LogWarning((object)("[PROBE] SetValues failed: " + ex3.Message));
}
if (list != null && !list.Contains(hero))
{
list.Add(hero);
}
_log.LogInfo((object)"[PROBE] created a probe character from scratch (no save existed).");
}
else
{
_log.LogInfo((object)("[PROBE] reusing existing character '" + SafeStr(() => hero.CharacterName) + "'."));
}
try
{
root.AddToCharacterList(hero);
hero.IsNetworkLoaded = true;
hero.OwnerID = num;
hero.ControllerPlayerId = num;
hero.SelectedForBattle = true;
_log.LogInfo((object)"[PROBE] character selected for the party.");
}
catch (Exception arg)
{
_log.LogError((object)$"[PROBE] could not select the character: {arg}");
}
try
{
CharacterChoiceManager instance = CharacterChoiceManager.Instance;
if (instance != null)
{
instance.RefreshCharacters();
}
}
catch (Exception ex4)
{
_log.LogWarning((object)("[PROBE] RefreshCharacters failed: " + ex4.Message));
}
}
private static Character FindPartyHero()
{
GameLogic val = Try(() => GameLogic.instance);
if ((Object)(object)val == (Object)null)
{
return null;
}
try
{
return val.ControlledCharacters?.FirstOrDefault((Func<Character, bool>)((Character c) => c != null && !c.IsAI));
}
catch
{
return null;
}
}
private static void Fail(string reason)
{
_log.LogError((object)("[PROBE] FAILED: " + reason + ". See preceding log lines."));
Finish(success: false);
}
private static void Finish(bool success)
{
ProbeSuppressLevelUpWindowsPatch.Active = false;
_phase = Phase.Done;
string contents = BuildReport();
string path = Path.Combine(Application.dataPath, "..", "roguelike_scaling_probe.txt");
try
{
File.WriteAllText(path, contents);
_log.LogInfo((object)("[PROBE] report written to " + Path.GetFullPath(path)));
}
catch (Exception ex)
{
_log.LogWarning((object)("[PROBE] could not write report: " + ex.Message));
}
_log.LogInfo((object)(success ? $"[PROBE] DONE — {Rows.Count} level rows recorded." : "[PROBE] DONE (incomplete — see errors above)"));
if (_quitWhenDone)
{
_log.LogInfo((object)"[PROBE] quitting game.");
Application.Quit();
}
}
private static string BuildReport()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("# Roguelike Scaling - balance probe report");
stringBuilder.AppendLine();
stringBuilder.AppendLine($"growth/level = {ScalingConfig.GrowthPerLevel.Value}");
stringBuilder.AppendLine($"pivot = {ScalingConfig.PivotLevel.Value}");
stringBuilder.AppendLine($"weapon growth/level = {ScalingConfig.WeaponGrowthPerLevel.Value}");
stringBuilder.AppendLine($"bonus stat points = {ScalingConfig.BonusStatPointsPerLevel.Value}/level");
stringBuilder.AppendLine("enemy scaling = " + (ScalingConfig.AiTailEnabled.Value ? "on" : "off") + " " + $"(pivot {ScalingConfig.AiPivotLevel.Value}, hp {ScalingConfig.AiHealthGrowthPerLevel.Value}, " + $"dmg {ScalingConfig.AiDamageGrowthPerLevel.Value})");
stringBuilder.AppendLine();
stringBuilder.AppendLine("Player rows. 'vanilla' is the live value divided by the curve, which is exact");
stringBuilder.AppendLine("because every patch is a pure multiplicative factor.");
stringBuilder.AppendLine();
stringBuilder.AppendLine("| lvl | xAttr | xWpn | HP vanilla | HP modded | AP vanilla | AP modded | Armor vanilla | Armor modded | Weapon vanilla | Weapon modded | Might | Vit | SP |");
stringBuilder.AppendLine("|----:|-------|-----|-----------:|----------:|-----------:|----------:|--------------:|-------------:|---------------:|-------------:|-----:|----:|---:|");
foreach (Row row in Rows)
{
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "| {0} | {1:F3} | {13:F2} | {2:F0} | {3:F0} | {4:F1} | {5:F1} | {6:F0} | {7:F0} | {8:F0} | {9:F0} | {10:F0} | {11:F0} | {12} |", row.Level, row.Curve, Divide(row.Health, row.Curve), row.Health, Divide(row.AbilityPower, row.Curve), row.AbilityPower, Divide(row.Armor, row.Curve), row.Armor, Divide(row.WeaponDamage, row.WeaponCurve), row.WeaponDamage, row.Might, row.Vitality, row.StatPoints, row.WeaponCurve));
}
stringBuilder.Append(BuildEnemyTable());
return stringBuilder.ToString();
}
private static string BuildEnemyTable()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine();
stringBuilder.AppendLine("Enemy curve for a baseline-100 enemy with 1 player, straight from");
stringBuilder.AppendLine("Character.GetAIHealth / Character.GetAIPower. Weapon damage is compared against");
stringBuilder.AppendLine("this health column, which is what turns-to-kill is computed from.");
stringBuilder.AppendLine();
stringBuilder.AppendLine("| lvl | enemy MaxHealth | enemy spell power | weapon modded | turns to kill (vanilla) | turns to kill (modded) |");
stringBuilder.AppendLine("|----:|----------------:|------------------:|---------------:|-----------------------:|-----------------------:|");
foreach (float[] enemyRow in EnemyRows)
{
int level = (int)enemyRow[0];
float num = enemyRow[1];
float num2 = enemyRow[2];
Row row = Rows.FirstOrDefault((Row r) => r.Level == level);
float num3 = ((row == null) ? 0f : (num / Math.Max(1f, row.WeaponDamage / Math.Max(0.0001f, row.WeaponCurve) * (1f + row.AbilityPower / Math.Max(0.0001f, row.Curve) / 100f))));
float num4 = ((row == null) ? 0f : (num / Math.Max(1f, row.WeaponDamage * (1f + row.AbilityPower / 100f))));
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, "| {0} | {1:F0} | {2:F0} | {3} | {4:F1} | {5:F1} |", level, num, num2, (row == null) ? "-" : row.WeaponDamage.ToString("F0", CultureInfo.InvariantCulture), num3, num4));
}
return stringBuilder.ToString();
}
private static float Divide(float value, float divisor)
{
if (divisor != 0f)
{
return value / divisor;
}
return value;
}
private static float SafeF(Func<float> getter)
{
try
{
return getter();
}
catch
{
return 0f;
}
}
private static int SafeI(Func<int> getter)
{
try
{
return getter();
}
catch
{
return 0;
}
}
private static string SafeStr(Func<string> getter)
{
try
{
return getter();
}
catch (Exception ex)
{
return "n/a(" + ex.GetType().Name + ")";
}
}
private static T Try<T>(Func<T> getter) where T : class
{
try
{
return getter();
}
catch
{
return null;
}
}
}
[HarmonyPatch]
internal static class ProbeTickPatch
{
private static bool _warnedMultiplayer;
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("GUIManager", "Update");
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix()
{
MaybeTick();
}
internal static void MaybeTick()
{
if (string.Equals(ScalingConfig.ProbeMode.Value, "balance", StringComparison.OrdinalIgnoreCase))
{
BalanceProbe.Tick();
}
MaybeWarnAboutMultiplayer();
}
internal static void MaybeWarnAboutMultiplayer()
{
if (_warnedMultiplayer || !RunGate.IsNetworked())
{
return;
}
ManualLogSource logger = RoguelikeScalingPlugin.Logger;
if (logger != null)
{
_warnedMultiplayer = true;
logger.LogWarning((object)"[multiplayer] Networked session detected. This mod scales stats locally on each client, so every player must install it with the same curve settings or they will have different stats. Combat stays host-authoritative either way, so the session will not desync.");
if (ScalingConfig.AiTailEnabled.Value)
{
logger.LogWarning((object)"[multiplayer] Enemy scaling is ON. Enemy health and damage are host decisions, so a host with it enabled and a client without it will see enemy numbers that do not match what they are fighting. Leave Enemy Scaling off for co-op unless all players match.");
}
}
}
}
[HarmonyPatch]
internal static class ProbeTickGameLogicPatch
{
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("GameLogic", "Update");
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix()
{
ProbeTickPatch.MaybeTick();
}
}
[HarmonyPatch]
internal static class ProbeBootstrapPatch
{
private static bool _begun;
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("GUIManager", "Update");
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix()
{
if (!_begun && string.Equals(ScalingConfig.ProbeMode.Value, "balance", StringComparison.OrdinalIgnoreCase))
{
_begun = true;
BalanceProbe.Begin(RoguelikeScalingPlugin.Logger, ScalingConfig.ProbeMaxLevel.Value, ScalingConfig.ProbeQuitWhenDone.Value);
}
}
}
[HarmonyPatch]
internal static class ProbeSuppressLevelUpWindowsPatch
{
internal static bool Active;
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("RoguelikeManager", "AddToSkillSelectionQueue", typeof(Character), typeof(bool), typeof(int), typeof(int), typeof(bool));
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static bool Prefix()
{
return !Active;
}
}
}
namespace RoguelikeScaling.Patches
{
[HarmonyPatch]
internal static class AiHealthScalingPatch
{
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("Character", "GetAIHealth", typeof(int), typeof(float), typeof(int), typeof(float), typeof(float), typeof(float), typeof(int), typeof(bool));
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(int level, int teamIndex, bool isPlayerSummon, ref float __result)
{
if (!isPlayerSummon && teamIndex != 2)
{
float value = ScalingConfig.AiHealthGrowthPerLevel.Value;
if (AiTailGate.ShouldApply(value))
{
__result *= CurveMath.Compound(level, value, ScalingConfig.AiPivotLevel.Value);
}
}
}
}
[HarmonyPatch]
internal static class AiDamageScalingPatch
{
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("Character", "GetAIPower", typeof(int), typeof(float), typeof(float), typeof(int), typeof(float), typeof(float), typeof(int), typeof(bool));
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(int level, int teamIndex, bool isPlayerSummon, ref float __result)
{
if (!isPlayerSummon && teamIndex != 2)
{
float value = ScalingConfig.AiDamageGrowthPerLevel.Value;
if (AiTailGate.ShouldApply(value))
{
__result *= CurveMath.Compound(level, value, ScalingConfig.AiPivotLevel.Value);
}
}
}
}
internal static class AiTailGate
{
internal static bool ShouldApply(float growth)
{
if (ScalingConfig.AiTailEnabled.Value && growth > 0f && ScalingConfig.Enabled.Value)
{
return RunGate.IsRoguelike();
}
return false;
}
}
[HarmonyPatch]
internal static class AttributeScalingPatch
{
private static bool _inPatch;
private static int _calls;
private static int _scaled;
private static bool _audited;
private static readonly HashSet<string> Seen = new HashSet<string>();
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("Character", "get_Item", typeof(string));
}
private static bool Prepare()
{
return TargetMethod() != null;
}
internal static void Audit()
{
if (_audited)
{
return;
}
_audited = true;
ManualLogSource logger = RoguelikeScalingPlugin.Logger;
if (logger == null)
{
return;
}
logger.LogInfo((object)$"[AUDIT] get_Item calls={_calls} scaled={_scaled} namesSeen={Seen.Count}");
foreach (string item in Seen)
{
logger.LogInfo((object)("[AUDIT] '" + item + "'"));
}
logger.LogInfo((object)($"[AUDIT] gate: enabled={ScalingConfig.Enabled?.Value} " + $"roguelikeOnly={ScalingConfig.RoguelikeOnly?.Value} isRoguelike={RunGate.IsRoguelike()}"));
}
private static void Postfix(Character __instance, string attributeName, ref float __result)
{
if (_inPatch || attributeName == null)
{
return;
}
_calls++;
Seen.Add(attributeName);
if (!_audited && _calls >= 300)
{
Audit();
}
float value;
switch (attributeName)
{
default:
return;
case "MaxHealth":
value = ScalingConfig.HealthWeight.Value;
break;
case "MaxMana":
value = ScalingConfig.ManaWeight.Value;
break;
case "Armor":
case "MagicArmor":
value = ScalingConfig.ArmorWeight.Value;
break;
case "AbilityPower":
value = ScalingConfig.AbilityPowerWeight.Value;
break;
}
if (value <= 0f || !RunGate.ShouldScale(__instance))
{
return;
}
float num = CurveMath.Weighted(ScalingConfig.GrowthPerLevel.Value, value, ScaleLevel.For(__instance), ScalingConfig.PivotLevel.Value);
if (Math.Abs(num - 1f) < 0.0001f)
{
return;
}
_inPatch = true;
try
{
__result *= num;
_scaled++;
}
finally
{
_inPatch = false;
}
}
}
[HarmonyPatch]
internal static class LevelUpCalibrationPatch
{
private static readonly Dictionary<int, int> LastLevelByCharacter = new Dictionary<int, int>();
private static bool _inLog;
internal static MethodBase TargetMethod()
{
return TargetLookup.Find("Character", "GiveExperience", typeof(float), typeof(bool));
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(Character __instance)
{
if (!_inLog && ScalingConfig.LogLevelUps.Value && RunGate.ShouldScale(__instance))
{
int iD = __instance.ID;
int level = __instance.Level;
if (!LastLevelByCharacter.TryGetValue(iD, out var value) || level > value)
{
LastLevelByCharacter[iD] = level;
Write(__instance, ScaleLevel.For(__instance));
}
}
}
private static void Write(Character character, int level)
{
ManualLogSource logger = RoguelikeScalingPlugin.Logger;
if (logger == null)
{
return;
}
_inLog = true;
try
{
int value = ScalingConfig.PivotLevel.Value;
float num = CurveMath.Weighted(ScalingConfig.GrowthPerLevel.Value, ScalingConfig.HealthWeight.Value, level, value);
float num2 = CurveMath.Weighted(ScalingConfig.GrowthPerLevel.Value, ScalingConfig.AbilityPowerWeight.Value, level, value);
float num3 = CurveMath.Weighted(ScalingConfig.WeaponGrowthPerLevel.Value, ScalingConfig.WeaponWeight.Value, level, ScalingConfig.WeaponPivotLevel.Value);
string text = Safe(() => character.MaxHealth.ToString("F0", CultureInfo.InvariantCulture));
string text2 = Safe(() => character.MaxMana.ToString("F0", CultureInfo.InvariantCulture));
string text3 = Safe(() => character.AbilityPower.ToString("F1", CultureInfo.InvariantCulture));
string text4 = Safe(() => character["Armor"].ToString("F0", CultureInfo.InvariantCulture));
string text5 = Safe(() => character.WeaponDamage.ToString("F0", CultureInfo.InvariantCulture));
int num4 = ((ScalingConfig.BonusStatPointsPerLevel.Value > 0) ? (Math.Max(0, level - value) * ScalingConfig.BonusStatPointsPerLevel.Value) : 0);
logger.LogInfo((object)($"[calibration] lvl={level,3} xHP={num,6:F2} xAP={num2,6:F2} xWpn={num3,6:F2}" + $" || HP={text,6} Mana={text2,5} AP={text3,7} Armor={text4,5} WeaponDmg={text5,6}" + " UnspentStatPoints=" + Safe(() => character.UnspentStatPoints.ToString(CultureInfo.InvariantCulture)) + ((num4 > 0) ? $" (+{num4} bonus pts)" : string.Empty)));
}
finally
{
_inLog = false;
}
}
private static string Safe(Func<string> getter)
{
try
{
return getter();
}
catch (Exception ex)
{
return "n/a(" + ex.GetType().Name + ")";
}
}
}
[HarmonyPatch]
internal static class RoguelikeAttributePanelPatch
{
private const string Marker = "<size=80%><color=#7AB0FF>";
private static bool _inPatch;
internal static MethodBase TargetMethod()
{
Type type = AccessTools.TypeByName("RoguelikeManager");
if (!(type == null))
{
return AccessTools.Method(type, "UpdateAttributes", Type.EmptyTypes, (Type[])null);
}
return null;
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(RoguelikeManager __instance)
{
if (_inPatch || !ScalingConfig.ShowScalingInStatScreen.Value)
{
return;
}
Character val = null;
try
{
val = __instance.CurrentRoguelikeSkillSelectingCharacter;
}
catch
{
val = null;
}
if (val == null || !RunGate.ShouldScale(val))
{
return;
}
TextMeshProUGUI val2 = null;
try
{
val2 = Traverse.Create((object)__instance).Field<TextMeshProUGUI>("StatValues").Value;
}
catch (Exception ex)
{
ManualLogSource logger = RoguelikeScalingPlugin.Logger;
if (logger != null)
{
logger.LogWarning((object)("[STATS] could not read RoguelikeManager.StatValues: " + ex.Message));
}
return;
}
if ((Object)(object)val2 == (Object)null || string.IsNullOrEmpty(((TMP_Text)val2).text))
{
return;
}
string text = StripFooter(((TMP_Text)val2).text);
string text2 = BuildFooter(val);
if (!string.IsNullOrEmpty(text2))
{
_inPatch = true;
try
{
((TMP_Text)val2).text = text + "\n" + text2;
}
finally
{
_inPatch = false;
}
ManualLogSource logger2 = RoguelikeScalingPlugin.Logger;
if (logger2 != null)
{
logger2.LogInfo((object)$"[STATS] level-up panel annotated at level {val.Level}.");
}
}
}
private static string BuildFooter(Character character)
{
int num = ScaleLevel.For(character);
float value = CurveMath.Compound(num, ScalingConfig.GrowthPerLevel.Value, ScalingConfig.PivotLevel.Value);
float value2 = CurveMath.Weighted(ScalingConfig.WeaponGrowthPerLevel.Value, ScalingConfig.WeaponWeight.Value, num, ScalingConfig.WeaponPivotLevel.Value);
return string.Format("{0}Lv {1}: attributes x{2}, ", "<size=80%><color=#7AB0FF>", num, StatScreenText.Fmt(value)) + "weapon x" + StatScreenText.Fmt(value2) + "</color></size>";
}
private static string StripFooter(string text)
{
int num = text.IndexOf("<size=80%><color=#7AB0FF>", StringComparison.Ordinal);
if (num >= 0)
{
return text.Substring(0, num).TrimEnd('\n', '\r', ' ');
}
return text;
}
}
[HarmonyPatch]
internal static class StatPointScalingPatch
{
internal static MethodBase TargetMethod()
{
return TargetLookup.PropertyGetter("Character", "UnspentStatPoints");
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(Character __instance, ref int __result)
{
int value = ScalingConfig.BonusStatPointsPerLevel.Value;
if (value != 0 && RunGate.ShouldScale(__instance))
{
int num = ScaleLevel.For(__instance) - ScalingConfig.PivotLevel.Value;
if (num > 0)
{
__result += num * value;
}
}
}
}
[HarmonyPatch]
internal static class StatScreenScalingPatch
{
internal static MethodBase TargetMethod()
{
Type type = AccessTools.TypeByName("InventoryManager");
if (!(type == null))
{
return AccessTools.Method(type, "UpdateStatRows", new Type[1] { typeof(Character) }, (Type[])null);
}
return null;
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(InventoryManager __instance, Character character)
{
if (!ScalingConfig.ShowScalingInStatScreen.Value || character == null)
{
return;
}
ManualLogSource logger = RoguelikeScalingPlugin.Logger;
if (logger != null)
{
logger.LogInfo((object)($"[STATS] UpdateStatRows fired for character #{character.ID} level {character.Level} " + $"isRogue={RunGate.IsRoguelike()} qualifies={RunGate.ShouldScale(character)}"));
}
if (!RunGate.ShouldScale(character))
{
return;
}
List<StatRow> value;
try
{
value = Traverse.Create((object)__instance).Field<List<StatRow>>("statRows").Value;
}
catch (Exception ex)
{
ManualLogSource logger2 = RoguelikeScalingPlugin.Logger;
if (logger2 != null)
{
logger2.LogWarning((object)("[STATS] could not read InventoryManager.statRows: " + ex.Message));
}
return;
}
if (value == null)
{
ManualLogSource logger3 = RoguelikeScalingPlugin.Logger;
if (logger3 != null)
{
logger3.LogWarning((object)"[STATS] InventoryManager.statRows was null.");
}
return;
}
ManualLogSource logger4 = RoguelikeScalingPlugin.Logger;
if (logger4 != null)
{
logger4.LogInfo((object)$"[STATS] {value.Count} stat row(s): {DescribeRows(value)}");
}
int num = 0;
foreach (StatRow item in value)
{
TextMeshProUGUI val = item?.StatValue;
StatAttribute val2 = item?.StatAttribute;
if ((Object)(object)val == (Object)null || val2 == null)
{
continue;
}
float num2 = MultiplierFor(val2, character);
if (num2 <= 1.002f)
{
continue;
}
try
{
string text = StatScreenText.BuildSuffix(num2, StatScreenText.ParseLeadingNumber(((TMP_Text)val).text));
string text2 = ((TMP_Text)val).text + text;
((TMP_Text)val).text = text2;
num++;
}
catch (Exception ex2)
{
ManualLogSource logger5 = RoguelikeScalingPlugin.Logger;
if (logger5 != null)
{
logger5.LogWarning((object)("[STATS] could not annotate a stat row: " + ex2.Message));
}
}
}
if (num > 0)
{
ManualLogSource logger6 = RoguelikeScalingPlugin.Logger;
if (logger6 != null)
{
logger6.LogInfo((object)$"[STATS] annotated {num} row(s) on the character screen at level {character.Level}.");
}
}
}
private static float MultiplierFor(StatAttribute descriptor, Character character)
{
if (descriptor.AttackPower || descriptor.SpellPower)
{
return WeaponCurve(character) * CurveMath.Weighted(ScalingConfig.GrowthPerLevel.Value, ScalingConfig.AbilityPowerWeight.Value, ScaleLevel.For(character), ScalingConfig.PivotLevel.Value);
}
if ((Object)(object)descriptor.Attribute != (Object)null)
{
float num = WeightForAttribute(((Object)descriptor.Attribute).name);
if (num > 0f)
{
return CurveMath.Weighted(ScalingConfig.GrowthPerLevel.Value, num, ScaleLevel.For(character), ScalingConfig.PivotLevel.Value);
}
}
if (descriptor.AddedAttributes != null)
{
CharacterAttribute[] addedAttributes = descriptor.AddedAttributes;
foreach (CharacterAttribute val in addedAttributes)
{
float num2 = (((Object)(object)val == (Object)null) ? 0f : WeightForAttribute(((Object)val).name));
if (num2 > 0f)
{
return CurveMath.Weighted(ScalingConfig.GrowthPerLevel.Value, num2, ScaleLevel.For(character), ScalingConfig.PivotLevel.Value);
}
}
}
return 1f;
}
private static string DescribeRows(List<StatRow> rows)
{
List<string> list = new List<string>();
foreach (StatRow row in rows)
{
StatAttribute val = row?.StatAttribute;
object obj;
if (val == null)
{
obj = null;
}
else
{
CharacterAttribute attribute = val.Attribute;
obj = ((attribute != null) ? ((Object)attribute).name : null);
}
if (obj == null)
{
obj = "<none>";
}
string text = (string)obj;
string text2 = $"{val?.AttackPower}/{val?.SpellPower}";
object obj2;
if (row == null)
{
obj2 = null;
}
else
{
TextMeshProUGUI statValue = row.StatValue;
obj2 = ((statValue != null) ? ((TMP_Text)statValue).text : null);
}
if (obj2 == null)
{
obj2 = "<no value>";
}
string text3 = (string)obj2;
list.Add(text + "[ap=" + text2 + "]='" + text3 + "'");
}
return string.Join(", ", list.ToArray());
}
private static float WeightForAttribute(string attributeName)
{
switch (attributeName)
{
case "MaxHealth":
return ScalingConfig.HealthWeight.Value;
case "MaxMana":
return ScalingConfig.ManaWeight.Value;
case "Armor":
case "MagicArmor":
return ScalingConfig.ArmorWeight.Value;
case "AbilityPower":
return ScalingConfig.AbilityPowerWeight.Value;
default:
return 0f;
}
}
private static float WeaponCurve(Character character)
{
float value = ScalingConfig.WeaponWeight.Value;
if (!(value <= 0f))
{
return CurveMath.Weighted(ScalingConfig.WeaponGrowthPerLevel.Value, value, ScaleLevel.For(character), ScalingConfig.WeaponPivotLevel.Value);
}
return 1f;
}
}
internal static class StatScreenText
{
internal const string TintScale = "#7AB0FF";
internal const string TintVanilla = "#9A93A6";
internal static string BuildSuffix(float factor, float shownValue)
{
string text = " <color=#7AB0FF>x" + Fmt(factor) + "</color>";
if (ScalingConfig.ShowVanillaValues.Value && shownValue > 0f)
{
text = text + " <color=#9A93A6>(" + (shownValue / factor).ToString("F0", CultureInfo.InvariantCulture) + ")</color>";
}
return text;
}
internal static string Fmt(float value)
{
return value.ToString("0.00", CultureInfo.InvariantCulture);
}
internal static float ParseLeadingNumber(string text)
{
if (string.IsNullOrEmpty(text))
{
return 0f;
}
int i;
for (i = 0; i < text.Length && (char.IsDigit(text[i]) || text[i] == '.' || text[i] == '-'); i++)
{
}
if (i <= 0 || !float.TryParse(text.Substring(0, i), NumberStyles.Float, CultureInfo.InvariantCulture, out var result))
{
return 0f;
}
return result;
}
}
[HarmonyPatch]
internal static class UnarmedWeaponDamagePatch
{
internal static MethodBase TargetMethod()
{
return TargetLookup.PropertyGetter("Character", "WeaponDamage");
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(Character __instance, ref float __result)
{
UnarmedScaling.Apply(__instance, ref __result);
}
}
[HarmonyPatch]
internal static class UnarmedWeaponDamageAllPatch
{
internal static MethodBase TargetMethod()
{
return TargetLookup.PropertyGetter("Character", "WeaponDamageAll");
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(Character __instance, ref float __result)
{
UnarmedScaling.Apply(__instance, ref __result);
}
}
internal static class UnarmedScaling
{
private static bool _inPatch;
internal static void Apply(Character character, ref float result)
{
if (_inPatch)
{
return;
}
float value = ScalingConfig.WeaponWeight.Value;
if (value <= 0f || !character.IsUnarmed || !RunGate.ShouldScale(character))
{
return;
}
float num = CurveMath.Weighted(ScalingConfig.WeaponGrowthPerLevel.Value, value, ScaleLevel.For(character), ScalingConfig.WeaponPivotLevel.Value);
if (Math.Abs(num - 1f) < 0.0001f)
{
return;
}
_inPatch = true;
try
{
result *= num;
}
finally
{
_inPatch = false;
}
}
}
[HarmonyPatch]
internal static class WeaponScalingPatch
{
private static bool _inPatch;
internal static MethodBase TargetMethod()
{
return TargetLookup.PropertyGetter("Weapon", "WeaponDamageNoMod");
}
private static bool Prepare()
{
return TargetMethod() != null;
}
private static void Postfix(Weapon __instance, ref float __result)
{
if (_inPatch)
{
return;
}
float value = ScalingConfig.WeaponWeight.Value;
if (value <= 0f)
{
return;
}
Character owner = ((Item)__instance).Owner;
if (owner == null || owner.IsAI || !ScalingConfig.Enabled.Value || (ScalingConfig.RoguelikeOnly.Value && !RunGate.IsRoguelike()))
{
return;
}
float num = CurveMath.Weighted(ScalingConfig.WeaponGrowthPerLevel.Value, value, ScaleLevel.For(owner), ScalingConfig.WeaponPivotLevel.Value);
if (!(Math.Abs(num - 1f) >= 0.0001f))
{
return;
}
_inPatch = true;
try
{
__result *= num;
}
finally
{
_inPatch = false;
}
}
}
}