using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using LevelGeneration;
using MakerGraphTool;
using MakerGraphTool.Nodes;
using Microsoft.CodeAnalysis;
using PerfectRandom.Sulfur.Core;
using PerfectRandom.Sulfur.Core.LevelGeneration;
using SULFURTogether.Api;
using UnityEngine;
using XNode;
[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("SulfurTogetherScaler")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.1.0")]
[assembly: AssemblyInformationalVersion("0.2.1+d69d1912094b53f167392cb739f93d797db28d66")]
[assembly: AssemblyProduct("SulfurTogetherScaler")]
[assembly: AssemblyTitle("SulfurTogetherScaler")]
[assembly: AssemblyVersion("0.2.1.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace SulfurTogetherScaler
{
internal static class FactorSync
{
internal sealed class State
{
public int FactorMilli = 1000;
public string Chapter = "";
public int LevelIndex = -1;
public string Preset = "";
public int Players = 1;
}
public const string ChannelId = "sts.factor";
private const float PollSeconds = 0.5f;
private static IExternalChannelRegistration _registration;
private static float _accum;
private static string _lastPeerKey = "";
private static SessionRole _lastRole = (SessionRole)0;
private static int _requestTicksLeft;
private const int RequestTicks = 120;
private static bool _requestSentOk;
private static State _hostState;
private static State _clientState;
private static bool _warnedNoHostState;
public static void Start()
{
try
{
_registration = NetExternalChannel.Register("sts.factor", (Action<string, byte[]>)OnReceive);
}
catch (Exception ex)
{
Plugin.Log.LogError((object)("[STS] could not register channel 'sts.factor': " + ex.Message + ". Clients cannot receive the host's factor and will generate unscaled levels; disable the mod on the host or the levels will not match."));
}
}
public static void Stop()
{
try
{
((IDisposable)_registration)?.Dispose();
}
catch
{
}
_registration = null;
}
public static int FactorMilliFor(int players, ScalePreset preset)
{
if (players <= 1)
{
return 1000;
}
int num;
int num2;
if (preset == ScalePreset.Linear)
{
num = 1000;
num2 = 4000;
}
else
{
num = 500;
num2 = 3000;
}
long num3 = 1000 + (long)num * (long)(players - 1);
if (num3 > num2)
{
num3 = num2;
}
return (int)num3;
}
public static int ScaleBudget(int budget, int factorMilli)
{
if (factorMilli == 1000 || budget <= 0)
{
return budget;
}
return (int)(((long)budget * (long)factorMilli + 500) / 1000);
}
public static string Fmt(int factorMilli)
{
return ((double)factorMilli / 1000.0).ToString("0.000", CultureInfo.InvariantCulture);
}
public static int FactorForLevelStart(string chapter, int levelIndex, out string source)
{
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_000b: Unknown result type (might be due to invalid IL or missing references)
//IL_001b: Unknown result type (might be due to invalid IL or missing references)
//IL_0013: 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_0033: Invalid comparison between Unknown and I4
int num = PlayerCount();
SessionRole val = SafeRole();
if (!NetSessionInfo.IsSessionActive || (int)val == 0 || num <= 1)
{
source = (((int)val == 0) ? "solo" : "session-of-one");
return 1000;
}
if ((int)val == 1)
{
int num2 = FactorMilliFor(num, Plugin.Preset.Value);
_hostState = new State
{
FactorMilli = num2,
Chapter = chapter,
LevelIndex = levelIndex,
Preset = Plugin.Preset.Value.ToString(),
Players = num
};
SendState(_hostState, (ExternalTarget)1, null);
source = "host-frozen";
return num2;
}
State clientState = _clientState;
if (clientState != null)
{
bool flag = string.Equals(clientState.Chapter, chapter, StringComparison.Ordinal) && clientState.LevelIndex == levelIndex;
source = (flag ? "host-state" : "host-state-earlier-level");
return clientState.FactorMilli;
}
SendRequest();
if (!_warnedNoHostState)
{
_warnedNoHostState = true;
Plugin.Log.LogWarning((object)"[STS] no factor from the host; this level is not scaled. If this happens every level, the host does not have the mod.");
}
source = "no-host-state";
return 1000;
}
public static void Tick(float dt)
{
//IL_0023: Unknown result type (might be due to invalid IL or missing references)
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
//IL_002a: Unknown result type (might be due to invalid IL or missing references)
//IL_0042: 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_0036: Unknown result type (might be due to invalid IL or missing references)
//IL_003c: Unknown result type (might be due to invalid IL or missing references)
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
//IL_004a: Unknown result type (might be due to invalid IL or missing references)
//IL_004c: Invalid comparison between Unknown and I4
//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
//IL_00ca: Invalid comparison between Unknown and I4
_accum += dt;
if (_accum < 0.5f)
{
return;
}
_accum = 0f;
try
{
SessionRole val = SafeRole();
if (val != _lastRole)
{
OnRoleChanged(_lastRole, val);
_lastRole = val;
}
if ((int)val == 0)
{
return;
}
if ((int)val == 2 && _clientState == null && _requestTicksLeft > 0)
{
_requestTicksLeft--;
if (TrySend("R", (ExternalTarget)0, null, quiet: true))
{
if (!_requestSentOk)
{
Plugin.Log.LogInfo((object)"[STS] asked the host for its factor");
}
_requestSentOk = true;
}
else if (_requestTicksLeft == 0)
{
Plugin.Log.LogWarning((object)"[STS] could not reach the host within 60 s; levels are not scaled until its factor arrives");
}
}
string text = PeerKey();
if (!(text != _lastPeerKey))
{
return;
}
_lastPeerKey = text;
if ((int)val == 1)
{
State state = _hostState ?? new State();
if (_hostState == null)
{
int players = PlayerCount();
state.FactorMilli = FactorMilliFor(players, Plugin.Preset.Value);
state.Preset = Plugin.Preset.Value.ToString();
state.Players = players;
}
SendState(state, (ExternalTarget)1, null);
Plugin.Log.LogInfo((object)$"[STS] peers changed ({PlayerCount()} players); sent factor {Fmt(state.FactorMilli)} to all clients");
}
}
catch (Exception ex)
{
Plugin.Log.LogWarning((object)("[STS] tick: " + ex.Message));
}
}
private static void OnRoleChanged(SessionRole from, SessionRole to)
{
//IL_000a: 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_0020: Unknown result type (might be due to invalid IL or missing references)
//IL_003a: Unknown result type (might be due to invalid IL or missing references)
//IL_003c: Invalid comparison between Unknown and I4
//IL_0058: Unknown result type (might be due to invalid IL or missing references)
//IL_005a: Invalid comparison between Unknown and I4
Plugin.Log.LogInfo((object)$"[STS] session role {from} -> {to}");
if ((int)to == 0)
{
_hostState = null;
_clientState = null;
_lastPeerKey = "";
}
else if ((int)to == 2)
{
_clientState = null;
_requestTicksLeft = 120;
_requestSentOk = false;
_warnedNoHostState = false;
}
else if ((int)to == 1)
{
_hostState = null;
}
}
private static void OnReceive(string senderPeerId, byte[] payload)
{
//IL_0040: Unknown result type (might be due to invalid IL or missing references)
//IL_0046: Invalid comparison between Unknown and I4
//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
//IL_00f0: Invalid comparison between Unknown and I4
try
{
string[] array = Encoding.UTF8.GetString(payload ?? Array.Empty<byte>()).Split(new char[1] { '|' });
if (array.Length == 0)
{
return;
}
if (array[0] == "R")
{
if ((int)SafeRole() == 1)
{
State state = _hostState;
if (state == null)
{
int players = PlayerCount();
state = new State
{
FactorMilli = FactorMilliFor(players, Plugin.Preset.Value),
Preset = Plugin.Preset.Value.ToString(),
Players = players
};
}
SendState(state, (ExternalTarget)2, senderPeerId);
Plugin.Log.LogInfo((object)("[STS] peer " + senderPeerId + " asked; sent factor " + Fmt(state.FactorMilli)));
}
}
else if (array[0] == "F" && array.Length >= 6 && (int)SafeRole() == 2)
{
State state2 = new State
{
FactorMilli = int.Parse(array[1], CultureInfo.InvariantCulture),
Chapter = array[2],
LevelIndex = int.Parse(array[3], CultureInfo.InvariantCulture),
Preset = array[4],
Players = int.Parse(array[5], CultureInfo.InvariantCulture)
};
if (state2.FactorMilli < 1000 || state2.FactorMilli > 10000)
{
Plugin.Log.LogWarning((object)$"[STS] ignored factor {state2.FactorMilli} from host (out of range)");
return;
}
_clientState = state2;
Plugin.Log.LogInfo((object)string.Format("[STS] host factor {0} preset={1} players={2} for {3}", Fmt(state2.FactorMilli), state2.Preset, state2.Players, (state2.LevelIndex < 0) ? "next level" : (state2.Chapter + "/" + state2.LevelIndex)));
}
}
catch (Exception ex)
{
Plugin.Log.LogWarning((object)("[STS] bad message from " + senderPeerId + ": " + ex.Message));
}
}
private static void SendState(State s, ExternalTarget target, string peerId)
{
//IL_0075: Unknown result type (might be due to invalid IL or missing references)
Send(string.Join("|", "F", s.FactorMilli.ToString(CultureInfo.InvariantCulture), s.Chapter ?? "", s.LevelIndex.ToString(CultureInfo.InvariantCulture), s.Preset ?? "", s.Players.ToString(CultureInfo.InvariantCulture)), target, peerId);
}
private static void SendRequest()
{
if (!TrySend("R", (ExternalTarget)0, null, quiet: true))
{
_requestTicksLeft = 120;
}
}
private static void Send(string text, ExternalTarget target, string peerId)
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
TrySend(text, target, peerId, quiet: false);
}
private static bool TrySend(string text, ExternalTarget target, string peerId, bool quiet)
{
//IL_0054: Unknown result type (might be due to invalid IL or missing references)
//IL_001b: 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)
if (_registration == null)
{
return false;
}
try
{
bool num = NetExternalChannel.Send("sts.factor", Encoding.UTF8.GetBytes(text), (ExternalDelivery)0, target, peerId);
if (!num && !quiet)
{
Plugin.Log.LogWarning((object)$"[STS] send to {target} refused by SULFUR Together: {text}");
}
return num;
}
catch (Exception ex)
{
if (!quiet)
{
Plugin.Log.LogWarning((object)$"[STS] send to {target} threw: {ex.Message}");
}
return false;
}
}
private static SessionRole SafeRole()
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0005: Unknown result type (might be due to invalid IL or missing references)
//IL_000a: Unknown result type (might be due to invalid IL or missing references)
//IL_000d: Unknown result type (might be due to invalid IL or missing references)
try
{
return NetSessionInfo.Role;
}
catch
{
return (SessionRole)0;
}
}
public static int PlayerCount()
{
try
{
IReadOnlyList<ExternalPeer> peers = NetSessionInfo.Peers;
return (peers == null || peers.Count == 0) ? 1 : peers.Count;
}
catch
{
return 1;
}
}
private static string PeerKey()
{
//IL_0024: Unknown result type (might be due to invalid IL or missing references)
//IL_0029: Unknown result type (might be due to invalid IL or missing references)
try
{
IReadOnlyList<ExternalPeer> peers = NetSessionInfo.Peers;
if (peers == null)
{
return "";
}
List<string> list = new List<string>(peers.Count);
for (int i = 0; i < peers.Count; i++)
{
ExternalPeer val = peers[i];
list.Add(((ExternalPeer)(ref val)).PeerId);
}
list.Sort(StringComparer.Ordinal);
return string.Join(",", list);
}
catch
{
return "";
}
}
}
public enum ScalePreset
{
Mild,
Linear
}
[BepInPlugin("com.s8systems.sulfur.togetherscaler", "SULFUR Together Scaler", "0.2.1")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
public sealed class Plugin : BaseUnityPlugin
{
public const string Guid = "com.s8systems.sulfur.togetherscaler";
public const string Name = "SULFUR Together Scaler";
public const string Version = "0.2.1";
internal static ManualLogSource Log;
internal static ConfigEntry<bool> Enabled;
internal static ConfigEntry<ScalePreset> Preset;
internal static ConfigEntry<string> ExcludedEnvironments;
internal static ConfigEntry<string> ExcludedLevelSets;
private Harmony _harmony;
private void Awake()
{
//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
//IL_00b7: Expected O, but got Unknown
Log = ((BaseUnityPlugin)this).Logger;
Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, Live("Scale the number of enemies by the number of players in a SULFUR Together session."));
Preset = ((BaseUnityPlugin)this).Config.Bind<ScalePreset>("General", "Preset", ScalePreset.Mild, Live("Mild: 2 players x1.5, 3 x2.0, 4 x2.5, 5+ x3.0. Linear: 2 players x2, 3 x3, 4+ x4. Only the host's setting matters. A change applies at the next level."));
ExcludedEnvironments = ((BaseUnityPlugin)this).Config.Bind<string>("Exclusions", "ExcludedEnvironments", "ChurchHub,Onboarding,Onboarding_LoopAround,EndlessMode,ChallengeEnvironments,DebugChapter", Live("Comma-separated environment ids that are never scaled."));
ExcludedLevelSets = ((BaseUnityPlugin)this).Config.Bind<string>("Exclusions", "ExcludedLevelSets", "", Live("Comma-separated level set names that are never scaled. Set names are in the [STS] level lines of the BepInEx log (set=)."));
_harmony = new Harmony("com.s8systems.sulfur.togetherscaler");
SpawnPatch.Apply(_harmony);
FactorSync.Start();
Log.LogInfo((object)string.Format("{0} {1} loaded. preset={2} enabled={3}", "SULFUR Together Scaler", "0.2.1", Preset.Value, Enabled.Value));
}
private static ConfigDescription Live(string text)
{
//IL_0010: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Expected O, but got Unknown
return new ConfigDescription(text, (AcceptableValueBase)null, new object[1] { "LiveApply" });
}
private void Update()
{
FactorSync.Tick(Time.unscaledDeltaTime);
}
private void OnDestroy()
{
FactorSync.Stop();
Harmony harmony = _harmony;
if (harmony != null)
{
harmony.UnpatchSelf();
}
}
}
internal static class SpawnPatch
{
private static readonly FieldInfo PackBudgetField = AccessTools.Field(typeof(SpawnEnemiesNode), "packBudget");
private static readonly MethodInfo ScaleMethod = AccessTools.Method(typeof(SpawnPatch), "ScaledBudget", (Type[])null, (Type[])null);
private static int _activeFactorMilli = 1000;
private static SpawnEnemiesNode _activeNode;
private static string _activeLabel = "";
private static int _unitsBefore;
private static int _replacements;
public static void Apply(Harmony harmony)
{
//IL_0062: Unknown result type (might be due to invalid IL or missing references)
//IL_0070: Expected O, but got Unknown
//IL_0084: Unknown result type (might be due to invalid IL or missing references)
//IL_008a: Expected O, but got Unknown
//IL_009a: Unknown result type (might be due to invalid IL or missing references)
//IL_00a7: Expected O, but got Unknown
MethodInfo methodInfo = AccessTools.Method(typeof(SpawnEnemiesNode), "Execute", (Type[])null, (Type[])null);
if (methodInfo == null)
{
Plugin.Log.LogError((object)"[STS] SpawnEnemiesNode.Execute not found; the game changed. Nothing patched.");
return;
}
MethodInfo methodInfo2 = AccessTools.EnumeratorMoveNext((MethodBase)methodInfo);
if (methodInfo2 == null)
{
Plugin.Log.LogError((object)"[STS] SpawnEnemiesNode.Execute has no iterator MoveNext; nothing patched.");
return;
}
harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(SpawnPatch), "ExecutePrefix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
HarmonyMethod val = new HarmonyMethod(typeof(SpawnPatch), "MoveNextTranspiler", (Type[])null);
harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(SpawnPatch), "MoveNextPostfix", (Type[])null), val, (HarmonyMethod)null, (HarmonyMethod)null);
if (_replacements == 0)
{
Plugin.Log.LogError((object)"[STS] transpiler found no read of packBudget in the pack node; the factor will have no effect.");
}
else
{
Plugin.Log.LogInfo((object)$"[STS] patched {methodInfo2.DeclaringType?.Name}.MoveNext: {_replacements} packBudget read(s) scaled");
}
}
public static int ScaledBudget(int budget)
{
return FactorSync.ScaleBudget(budget, _activeFactorMilli);
}
private static void ExecutePrefix(SpawnEnemiesNode __instance)
{
_activeFactorMilli = 1000;
_activeNode = __instance;
try
{
string text = "?";
int num = -1;
string name = "?";
GameManager instance = StaticInstance<GameManager>.Instance;
if ((Object)(object)instance != (Object)null)
{
WorldEnvironment currentEnvironment = instance.currentEnvironment;
if ((Object)(object)currentEnvironment != (Object)null)
{
text = ((object)Unsafe.As<WorldEnvironmentIds, WorldEnvironmentIds>(ref currentEnvironment.id)/*cast due to .constrained prefix*/).ToString();
name = ((Object)currentEnvironment).name;
}
num = instance.currentLevelIndex;
}
string text2 = (((Object)(object)((MakerNodeBase)__instance).Context != (Object)null && (Object)(object)((MakerNodeBase)__instance).Context.Set != (Object)null) ? ((Object)((MakerNodeBase)__instance).Context.Set).name : (((Object)(object)((Node)__instance).graph != (Object)null) ? ((Object)((Node)__instance).graph).name : "?"));
_activeLabel = $"{text}/{num} set={text2}";
MakerGraphContext context = ((MakerNodeBase)__instance).Context;
LevelGenerationContext val = (LevelGenerationContext)(object)((context is LevelGenerationContext) ? context : null);
_unitsBefore = (((Object)(object)val != (Object)null && val.units != null) ? val.units.Count : (-1));
int packBudget = __instance.packBudget;
int num2 = FactorSync.PlayerCount();
int num3;
string source;
if (!Plugin.Enabled.Value)
{
num3 = 1000;
source = "disabled";
}
else if (InList(Plugin.ExcludedEnvironments.Value, text) || InList(Plugin.ExcludedEnvironments.Value, name))
{
num3 = 1000;
source = "excluded-environment";
}
else if (InList(Plugin.ExcludedLevelSets.Value, text2))
{
num3 = 1000;
source = "excluded-set";
}
else
{
num3 = FactorSync.FactorForLevelStart(text, num, out source);
}
_activeFactorMilli = num3;
Plugin.Log.LogInfo((object)($"[STS] level {_activeLabel} players={num2} preset={Plugin.Preset.Value} factor={FactorSync.Fmt(num3)} " + $"budget {packBudget} -> {FactorSync.ScaleBudget(packBudget, num3)} density={__instance.packDensity} source={source}"));
}
catch (Exception arg)
{
_activeFactorMilli = 1000;
Plugin.Log.LogWarning((object)$"[STS] level prefix failed, factor 1 for this level: {arg}");
}
}
private static void MoveNextPostfix(bool __result)
{
if (__result)
{
return;
}
try
{
if ((Object)(object)_activeNode != (Object)null)
{
MakerGraphContext context = ((MakerNodeBase)_activeNode).Context;
LevelGenerationContext val = (LevelGenerationContext)(object)((context is LevelGenerationContext) ? context : null);
int num = (((Object)(object)val != (Object)null && val.units != null) ? val.units.Count : (-1));
int num2 = ((num >= 0 && _unitsBefore >= 0) ? (num - _unitsBefore) : (-1));
Plugin.Log.LogInfo((object)$"[STS] level {_activeLabel} done: factor={FactorSync.Fmt(_activeFactorMilli)} units placed by the pack node={num2} total units={num}");
}
}
catch (Exception ex)
{
Plugin.Log.LogWarning((object)("[STS] level postfix: " + ex.Message));
}
finally
{
_activeFactorMilli = 1000;
_activeNode = null;
}
}
private static IEnumerable<CodeInstruction> MoveNextTranspiler(IEnumerable<CodeInstruction> instructions)
{
int n = 0;
foreach (CodeInstruction ci in instructions)
{
yield return ci;
if (ci.opcode == OpCodes.Ldfld && ci.operand is FieldInfo fieldInfo && fieldInfo == PackBudgetField)
{
n++;
yield return new CodeInstruction(OpCodes.Call, (object)ScaleMethod);
}
}
_replacements = n;
}
private static bool InList(string csv, string name)
{
if (string.IsNullOrEmpty(csv) || string.IsNullOrEmpty(name))
{
return false;
}
string[] array = csv.Split(new char[1] { ',' });
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (text.Length > 0 && string.Equals(text, name, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}
}