using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Text.Json;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Core.Logging.Interpolation;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP;
using DiceKingdoms.Game;
using DiceKingdoms.Interface;
using DiceKingdoms.Meta;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
using Il2CppInterop.Runtime.InteropTypes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppSystem;
using Il2CppSystem.Collections;
using Il2CppSystem.Collections.Generic;
using Il2CppSystem.IO;
using Il2CppSystem.Reflection;
using MLAPI;
using MLAPI.Connection;
using MLAPI.Messaging;
using MLAPI.Security;
using MLAPI.Transports.UNET;
using Microsoft.CodeAnalysis;
using TMPro;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = "")]
[assembly: AssemblyCompany("DiceKingdoms.DisasterControl")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.9.1.0")]
[assembly: AssemblyInformationalVersion("0.9.1")]
[assembly: AssemblyProduct("DiceKingdoms.DisasterControl")]
[assembly: AssemblyTitle("DiceKingdoms.DisasterControl")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.9.1.0")]
[module: UnverifiableCode]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
}
namespace DiceKingdoms.DisasterControl
{
public sealed class ControlPanel : MonoBehaviour
{
private GameObject? panel;
private SettingSetsInterface? window;
private TabArea? tabs;
private int tabIndex;
private BaseButton? buttonTemplate;
private RectTransform? content;
private TextMeshProUGUI? status;
private TMP_FontAsset? font;
private readonly List<Button> editors = new List<Button>();
private float next;
private string rendered = "";
private bool Ru => Plugin.Settings.Language.Value.Equals("ru", StringComparison.OrdinalIgnoreCase);
public int RuleRows { get; private set; }
public bool Opened
{
get
{
if ((Object)(object)panel != (Object)null)
{
return panel.activeInHierarchy;
}
return false;
}
}
public ControlPanel(IntPtr p)
: base(p)
{
}
private string L(string en, string ru)
{
if (!Ru)
{
return en;
}
return ru;
}
public void Update()
{
Plugin.Session.Tick(Time.realtimeSinceStartup);
if (Time.realtimeSinceStartup < next)
{
return;
}
next = Time.realtimeSinceStartup + 0.3f;
try
{
SettingSetsInterface[] source = ((IEnumerable<SettingSetsInterface>)Resources.FindObjectsOfTypeAll<SettingSetsInterface>()).Where(delegate(SettingSetsInterface s)
{
//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)
Scene scene = ((Component)s).gameObject.scene;
return ((Scene)(ref scene)).IsValid();
}).ToArray();
SettingSetsInterface val = ((IEnumerable<SettingSetsInterface>)source).FirstOrDefault((Func<SettingSetsInterface, bool>)((SettingSetsInterface s) => ((Component)s).gameObject.activeInHierarchy)) ?? source.LastOrDefault();
if ((Object)(object)val != (Object)null && ((Object)(object)window == (Object)null || ((Il2CppObjectBase)val).Pointer != ((Il2CppObjectBase)window).Pointer))
{
Build(val);
}
if ((Object)(object)panel == (Object)null || !Opened)
{
return;
}
if (Settings.Serialize(Plugin.Session.Draft) + Ru != rendered)
{
Render();
}
foreach (Button editor in editors)
{
((Selectable)editor).interactable = Plugin.Session.CanEdit;
}
((TMP_Text)status).text = L(Plugin.Session.Status, TranslateStatus()) + (Plugin.Session.CanEdit ? "" : L(" • Read only", " • Только просмотр"));
}
catch (Exception e)
{
Plugin.Error(e);
}
}
private string TranslateStatus()
{
if (Plugin.Healthy)
{
if (!Plugin.Session.CanEdit || !Plugin.Session.Frozen)
{
if (!Plugin.Session.Frozen)
{
if (Plugin.Session.InScope)
{
if (!Plugin.Session.IsHost)
{
return "Правила хоста; только просмотр.";
}
return "Для старта все участники должны подтвердить правила и версию мода.";
}
return "Открой одиночное или пользовательское лобби.";
}
return "Сетевая игра: правила заблокированы до конца матча.";
}
if (!Plugin.Session.Pending)
{
return "Одиночная игра: изменения применятся со следующего сбора урожая.";
}
return "Изменения ожидают начала следующего сбора урожая.";
}
return "Ошибка мода: перезапусти игру. Изменённые правила заблокированы.";
}
public void SetOpen(bool value)
{
if (!((Object)(object)panel == (Object)null) && !((Object)(object)window == (Object)null) && !((Object)(object)tabs == (Object)null))
{
if (value)
{
((ToggleableWindow)window).Open();
tabs.SelectTab(tabIndex);
Render();
}
else
{
((ToggleableWindow)window).Close();
}
}
}
private void Build(SettingSetsInterface settings)
{
//IL_01fb: Unknown result type (might be due to invalid IL or missing references)
//IL_0206: Unknown result type (might be due to invalid IL or missing references)
//IL_0212: Unknown result type (might be due to invalid IL or missing references)
//IL_0217: Unknown result type (might be due to invalid IL or missing references)
//IL_0218: Unknown result type (might be due to invalid IL or missing references)
//IL_021f: Unknown result type (might be due to invalid IL or missing references)
//IL_0297: Unknown result type (might be due to invalid IL or missing references)
//IL_02a3: Unknown result type (might be due to invalid IL or missing references)
//IL_02b9: Unknown result type (might be due to invalid IL or missing references)
//IL_02cf: Unknown result type (might be due to invalid IL or missing references)
//IL_0312: Unknown result type (might be due to invalid IL or missing references)
//IL_0339: Unknown result type (might be due to invalid IL or missing references)
//IL_0340: Unknown result type (might be due to invalid IL or missing references)
//IL_034d: Unknown result type (might be due to invalid IL or missing references)
//IL_03b6: Unknown result type (might be due to invalid IL or missing references)
//IL_03f8: Unknown result type (might be due to invalid IL or missing references)
//IL_03fd: Unknown result type (might be due to invalid IL or missing references)
//IL_0404: Unknown result type (might be due to invalid IL or missing references)
//IL_0410: Expected O, but got Unknown
window = settings;
Label? obj = ((IEnumerable<Label>)Resources.FindObjectsOfTypeAll<Label>()).FirstOrDefault((Func<Label, bool>)((Label l) => (Object)(object)l.visual != (Object)null));
font = ((obj != null) ? ((TMP_Text)obj.visual).font : null);
buttonTemplate = settings.closeButton;
tabs = ((Component)settings).GetComponentInChildren<TabArea>(true);
int num = Array.FindIndex(((IEnumerable<TabDefinition>)tabs.tabs).ToArray(), (TabDefinition t) => ((Object)t.content).name == "DisasterControl.Panel");
if (num >= 0)
{
tabIndex = num;
panel = ((Component)((Il2CppArrayBase<TabDefinition>)(object)tabs.tabs)[num].content).gameObject;
content = ((Il2CppObjectBase)panel.transform.Find("Viewport/Rules")).Cast<RectTransform>();
status = ((Component)panel.transform.Find("Text")).GetComponent<TextMeshProUGUI>();
rendered = "";
return;
}
TabDefinition val = ((Il2CppArrayBase<TabDefinition>)(object)tabs.tabs)[0];
TabAreaTabTitle val2 = Object.Instantiate<TabAreaTabTitle>(val.title, ((Component)val.title).transform.parent);
((Object)val2).name = "DisasterControl.Title";
foreach (Behaviour componentsInChild in ((Component)val2).GetComponentsInChildren<Behaviour>(true))
{
if ((Object)(object)((Il2CppObjectBase)componentsInChild).TryCast<Label>() != (Object)null || ((MemberInfo)((Object)componentsInChild).GetIl2CppType()).Name.Contains("Localizer"))
{
componentsInChild.enabled = false;
}
}
TextMeshProUGUI componentInChildren = ((Component)val2).GetComponentInChildren<TextMeshProUGUI>(true);
((TMP_Text)componentInChildren).text = "Disaster Control";
((TMP_Text)componentInChildren).fontSize = 21f;
((TMP_Text)componentInChildren).enableAutoSizing = true;
((TMP_Text)componentInChildren).fontSizeMin = 15f;
val2.Selected = false;
RectTransform val3 = Rect("DisasterControl.Panel", ((Transform)val.content).parent, 0f, 0f);
val3.anchorMin = Vector2.zero;
val3.anchorMax = Vector2.one;
Vector2 val4 = (val3.offsetMin = (val3.offsetMax = Vector2.zero));
panel = ((Component)val3).gameObject;
status = Text(panel.transform, "", 18, 700f, 78f);
Position(((TMP_Text)status).rectTransform, 25f, -8f);
RectTransform val6 = Rect("Viewport", panel.transform, 700f, 590f);
val6.anchorMin = Vector2.zero;
val6.anchorMax = Vector2.one;
val6.offsetMin = new Vector2(25f, 15f);
val6.offsetMax = new Vector2(-25f, -90f);
((Component)val6).gameObject.AddComponent<RectMask2D>();
content = Rect("Rules", (Transform)(object)val6, 700f, 2000f);
content.pivot = new Vector2(0f, 1f);
RectTransform? obj2 = content;
RectTransform? obj3 = content;
((Vector2)(ref val4))..ctor(0f, 1f);
obj3.anchorMax = val4;
obj2.anchorMin = val4;
content.anchoredPosition = Vector2.zero;
ScrollRect obj4 = ((Component)val6).gameObject.AddComponent<ScrollRect>();
obj4.viewport = val6;
obj4.content = content;
obj4.horizontal = false;
obj4.vertical = true;
obj4.movementType = (MovementType)2;
obj4.scrollSensitivity = 35f;
((Graphic)((Component)val6).gameObject.AddComponent<Image>()).color = new Color(0f, 0f, 0f, 0.01f);
panel.SetActive(false);
tabIndex = ((Il2CppArrayBase<TabDefinition>)(object)tabs.tabs).Length;
tabs.tabs = new Il2CppReferenceArray<TabDefinition>(((IEnumerable<TabDefinition>)tabs.tabs).ToArray().Append(new TabDefinition
{
title = val2,
content = val3
}).ToArray());
tabs.titleIndex.Add(((Component)val2).gameObject, tabIndex);
rendered = "";
}
[HideFromIl2Cpp]
private void Edit(Func<Rules, Rules> edit)
{
if (Plugin.Session.Set(edit(Plugin.Session.Draft)with
{
Preset = Preset.Custom
}))
{
rendered = "";
}
}
private void Render()
{
//IL_04dc: Unknown result type (might be due to invalid IL or missing references)
if ((Object)(object)content == (Object)null)
{
return;
}
foreach (Button editor in editors)
{
if ((Object)(object)editor != (Object)null)
{
((Selectable)editor).interactable = false;
}
}
editors.Clear();
for (int num = ((Transform)content).childCount - 1; num >= 0; num--)
{
Transform child = ((Transform)content).GetChild(num);
((Component)child).gameObject.SetActive(false);
Object.Destroy((Object)(object)((Component)child).gameObject);
}
float y = 8f;
RuleRows = 0;
Rules draft = Plugin.Session.Draft;
string hint = ((draft.Preset == Preset.Chaos) ? L("Chaos forces native Multi-Meteor on; 32 meteors maximum per island; Disease/Revolt 200%.", "Chaos включает Multi-Meteor: до 32 ударов на остров; Disease/Revolt 200%.") : L("Vanilla is the original game. Manual changes select Custom.", "Vanilla — оригинальная игра. Ручные изменения включают Custom."));
Row(ref y, L("Preset", "Пресет"), draft.Preset.ToString(), hint, delegate
{
Preset preset = (Preset)((int)(Plugin.Session.Draft.Preset + 1) % Enum.GetValues<Preset>().Length);
Plugin.Session.Set((preset == Preset.Custom) ? Plugin.Session.Draft with
{
Preset = Preset.Custom
} : Rules.For(preset, Catalog.Ids));
rendered = "";
});
Row(ref y, L("Meteor targeting", "Попадания метеоритов"), TargetName(draft.Targeting), L("Protect Castle: exclude the full castle footprint plus impact radius. Safer: prefer 2 extra tiles of clearance. No safe land: skip this meteor.", "Защита учитывает всю площадь замка и радиус удара. Safer добавляет отступ 2 клетки. Нет безопасной земли — удар пропускается."), delegate
{
Edit((Rules v) => v with
{
Targeting = (MeteorTargeting)((int)(v.Targeting + 1) % 3)
});
});
Row(ref y, L("Excess skulls", "Избыток черепов"), ExcessName(draft.Excess), L($"Cycle uses native thresholds {Catalog.First}–{Catalog.Last}. Single / NoExcess keep one threshold meteor; extra meteors are removed.", $"Цикл использует реальные пороги {Catalog.First}–{Catalog.Last}. Single / NoExcess сохраняют первый метеорит и убирают дополнительные."), delegate
{
Edit((Rules v) => v with
{
Excess = (ExcessSkullMode)((int)(v.Excess + 1) % 4)
});
});
Row(ref y, L("Skull reroll", "Переброс черепов"), On(draft.AllowSkullReroll), L("Select skull dice normally. Each reroll spends a native attempt; research and face probabilities are unchanged.", "Выбирайте кубики с черепами. Переброс расходует штатную попытку с учётом исследований."), delegate
{
Edit((Rules v) => v with
{
AllowSkullReroll = !v.AllowSkullReroll
});
});
Row(ref y, L("Force Multi-Meteor", "Несколько метеоритов"), On(draft.ForceMultiMeteor), L("Forces the lobby option with Vanilla excess skulls. Other excess modes allow at most one meteor.", "Включает Multi-Meteor при оригинальном режиме избытка. В остальных режимах остаётся не больше одного удара."), delegate
{
Edit((Rules v) => v with
{
ForceMultiMeteor = !v.ForceMultiMeteor
});
});
Row(ref y, L("Disease intensity", "Сила болезни"), draft.DiseasePercent + "%", L("Multiplier of the original Disease percentage. Separate Plague has its own toggle.", "Множитель штатной силы Disease. У Plague отдельный переключатель."), delegate
{
Edit((Rules v) => v with
{
DiseasePercent = ((v.DiseasePercent < 200) ? Math.Min(200, v.DiseasePercent + 25) : 0)
});
});
Row(ref y, L("Revolt intensity", "Сила восстания"), draft.RevoltPercent + "%", L("Multiplier of original military point loss; actual loss is capped at 100%.", "Множитель потери военной силы (мечи). Итоговая потеря не больше 100%."), delegate
{
Edit((Rules v) => v with
{
RevoltPercent = ((v.RevoltPercent < 200) ? Math.Min(200, v.RevoltPercent + 25) : 0)
});
});
Row(ref y, L("Meteor safety limit", "Лимит метеоритов"), draft.MeteorLimit.ToString(), L("Changing this value enables a bounded custom rule set. Balanced uses a cycle with at most one meteor per island.", "Изменение лимита включает свои правила. В Balanced цикл даёт не больше одного метеорита на остров."), delegate
{
Edit((Rules v) => v with
{
MeteorLimit = ((v.MeteorLimit >= 32) ? 1 : Math.Min(32, v.MeteorLimit * 2))
});
});
string[] ids = Catalog.Ids;
foreach (string text in ids)
{
string captured = text;
Row(ref y, DisasterName(text), On(draft.Enabled(text)), L("Disable the real event and its consequences for players and AI. Independent starvation stays original.", "Отключает событие и последствия для игроков и ИИ. Независимый голод сохраняется."), delegate
{
Edit((Rules v) => v with
{
Disabled = (v.Enabled(captured) ? v.Disabled.Append(captured).OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray() : v.Disabled.Where((string x) => x != captured).ToArray())
});
});
}
Row(ref y, L("Restore original rules", "Вернуть оригинальные правила"), "Vanilla", L("Reset every gameplay option to the unmodified game.", "Сброс всех параметров к оригинальной игре."), delegate
{
Plugin.Session.Set(new Rules());
rendered = "";
});
Position(((TMP_Text)Text((Transform)(object)content, L("Custom matches require the same mod on every player. Joining/reconnecting after a custom match starts is currently blocked; create a new lobby. Scroll to see every disaster.", "Для своих правил мод нужен всем игрокам. Вход и переподключение после старта такого матча пока закрыты; создайте новое лобби. Прокрутите список всех катастроф."), 17, 680f, 70f)).rectTransform, 15f, 0f - y);
y += 80f;
RectTransform val = Rect("Language", (Transform)(object)content, 680f, 45f);
Position(val, 15f, 0f - y);
ButtonOn(((Component)val).gameObject, Ru ? "English" : "Русский", delegate
{
Plugin.Settings.Language.Value = (Ru ? "en" : "ru");
rendered = "";
});
content.sizeDelta = new Vector2(700f, y + 65f);
rendered = Settings.Serialize(draft) + Ru;
}
private string On(bool value)
{
if (!value)
{
return L("Disabled", "Отключено");
}
return L("Enabled", "Включено");
}
private string TargetName(MeteorTargeting value)
{
if (!Ru)
{
return value.ToString();
}
return value switch
{
MeteorTargeting.ProtectCastle => "Защита замка",
MeteorTargeting.SaferTargeting => "Усиленный отступ",
_ => "Оригинальные",
};
}
private string ExcessName(ExcessSkullMode value)
{
if (!Ru)
{
return value.ToString();
}
return value switch
{
ExcessSkullMode.SingleMeteor => "Один метеорит",
ExcessSkullMode.CyclicDisasters => "По кругу",
ExcessSkullMode.NoExcessMeteors => "Без лишних ударов",
_ => "Оригинальные",
};
}
private string DisasterName(string id)
{
string text = id.Split('.').Last();
if (!Ru)
{
return text;
}
return text switch
{
"Robbers" => "Грабители",
"Disease" => "Болезнь",
"Lightning" => "Молния",
"Fire" => "Пожар",
"Thunderstorm" => "Гроза",
"Revolt" => "Восстание",
"Tornado" => "Торнадо",
"Plague" => "Чума",
"Satanism" => "Сатанизм",
"Pirates" => "Пираты",
"Tsunami" => "Цунами",
"Meteorite" => "Метеорит",
_ => text,
};
}
[HideFromIl2Cpp]
private void Row(ref float y, string label, string value, string hint, Action click)
{
//IL_00b2: Unknown result type (might be due to invalid IL or missing references)
RectTransform val = Rect(label, (Transform)(object)content, 700f, 128f);
Position(val, 15f, 0f - y);
Position(((TMP_Text)Text((Transform)(object)val, label, 23, 380f, 34f)).rectTransform, 0f, 0f);
RectTransform val2 = Rect("Value", (Transform)(object)val, 285f, 40f);
Position(val2, 395f, 0f);
editors.Add(ButtonOn(((Component)val2).gameObject, value, click));
TextMeshProUGUI obj = Text((Transform)(object)val, hint, 17, 680f, 62f);
((Graphic)obj).color = new Color(0.7f, 0.75f, 0.8f);
Position(((TMP_Text)obj).rectTransform, 0f, -43f);
y += 136f;
RuleRows++;
}
private static RectTransform Rect(string name, Transform parent, float width, float height)
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
RectTransform obj = new GameObject(name).AddComponent<RectTransform>();
((Transform)obj).SetParent(parent, false);
obj.sizeDelta = new Vector2(width, height);
return obj;
}
private static void Position(RectTransform r, float x, float y)
{
//IL_0013: 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)
//IL_002a: Unknown result type (might be due to invalid IL or missing references)
//IL_0037: Unknown result type (might be due to invalid IL or missing references)
Vector2 val = default(Vector2);
((Vector2)(ref val))..ctor(0f, 1f);
r.anchorMax = val;
r.anchorMin = val;
r.pivot = new Vector2(0f, 1f);
r.anchoredPosition = new Vector2(x, y);
}
private TextMeshProUGUI Text(Transform parent, string text, int size, float width, float height)
{
//IL_0053: Unknown result type (might be due to invalid IL or missing references)
TextMeshProUGUI val = ((Component)Rect("Text", parent, width, height)).gameObject.AddComponent<TextMeshProUGUI>();
if ((Object)(object)font != (Object)null)
{
((TMP_Text)val).font = font;
}
((TMP_Text)val).text = text;
((TMP_Text)val).fontSize = size;
((Graphic)val).color = new Color(0.95f, 0.9f, 0.77f);
((TMP_Text)val).enableWordWrapping = true;
((Graphic)val).raycastTarget = false;
return val;
}
[HideFromIl2Cpp]
private Button ButtonOn(GameObject go, string caption, Action action)
{
//IL_0032: 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_0048: Unknown result type (might be due to invalid IL or missing references)
//IL_004d: Unknown result type (might be due to invalid IL or missing references)
//IL_004e: Unknown result type (might be due to invalid IL or missing references)
//IL_0054: Unknown result type (might be due to invalid IL or missing references)
//IL_0101: Unknown result type (might be due to invalid IL or missing references)
//IL_010b: Expected O, but got Unknown
BaseButton val = Object.Instantiate<BaseButton>(buttonTemplate, go.transform);
val.onClick = null;
RectTransform obj = ((Il2CppObjectBase)((Component)val).transform).Cast<RectTransform>();
obj.anchorMin = Vector2.zero;
obj.anchorMax = Vector2.one;
Vector2 offsetMin = (obj.offsetMax = Vector2.zero);
obj.offsetMin = offsetMin;
LayoutElement component = ((Component)val).GetComponent<LayoutElement>();
if ((Object)(object)component != (Object)null)
{
component.ignoreLayout = true;
}
foreach (Behaviour componentsInChild in ((Component)val).GetComponentsInChildren<Behaviour>(true))
{
if ((Object)(object)((Il2CppObjectBase)componentsInChild).TryCast<Label>() != (Object)null || ((MemberInfo)((Object)componentsInChild).GetIl2CppType()).Name.Contains("Localizer"))
{
componentsInChild.enabled = false;
}
}
TextMeshProUGUI componentInChildren = ((Component)val).GetComponentInChildren<TextMeshProUGUI>(true);
((TMP_Text)componentInChildren).text = caption;
((TMP_Text)componentInChildren).fontSize = 21f;
((TMP_Text)componentInChildren).enableAutoSizing = true;
((TMP_Text)componentInChildren).fontSizeMin = 14f;
Button unityButton = val.unityButton;
unityButton.onClick = new ButtonClickedEvent();
((UnityEvent)unityButton.onClick).AddListener(UnityAction.op_Implicit((Action)delegate
{
try
{
action();
}
catch (Exception e)
{
Plugin.Error(e);
}
}));
return unityButton;
}
}
[BepInPlugin("io.github.mikhasan.dicekingdoms.disastercontrol", "Disaster Control", "0.9.1")]
[BepInProcess("Dice-Kingdoms.exe")]
public sealed class Plugin : BasePlugin
{
public const string Id = "io.github.mikhasan.dicekingdoms.disastercontrol";
public const string Version = "0.9.1";
internal static Plugin Instance = null;
internal static Settings Settings = null;
internal static bool Healthy;
internal static string GameFingerprint = "";
private readonly HashSet<string> errors = new HashSet<string>();
public static RuleSession Session { get; private set; } = null;
public override void Load()
{
//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
//IL_00e7: Expected O, but got Unknown
//IL_007f: Unknown result type (might be due to invalid IL or missing references)
Instance = this;
Settings = new Settings(((BasePlugin)this).Config);
Session = new RuleSession();
try
{
using FileStream inputStream = File.OpenRead(Path.Combine(Paths.GameRootPath, "GameAssembly.dll"));
using SHA256 sHA = SHA256.Create();
GameFingerprint = Application.version + "/" + Application.unityVersion + "/" + Convert.ToHexString(sHA.ComputeHash(inputStream));
new Harmony("io.github.mikhasan.dicekingdoms.disastercontrol").PatchAll(typeof(Plugin).Assembly);
Healthy = true;
}
catch (Exception e)
{
Error(e);
((BasePlugin)this).Log.LogError((object)"Required hooks unavailable. Modified rules are blocked.");
}
((BasePlugin)this).AddComponent<ControlPanel>();
ManualLogSource log = ((BasePlugin)this).Log;
bool flag = default(bool);
BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(78, 1, ref flag);
if (flag)
{
((BepInExLogInterpolatedStringHandler)val).AppendLiteral("Disaster Control ");
((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>("0.9.1");
((BepInExLogInterpolatedStringHandler)val).AppendLiteral("; default Vanilla; native seeded disaster execution retained.");
}
log.LogInfo(val);
}
internal static void Error(Exception e)
{
if (Instance.errors.Add(e.GetType().Name + e.Message))
{
((BasePlugin)Instance).Log.LogError((object)e);
}
}
}
public static class Catalog
{
internal static Disasters? Data;
public static string[] Ids { get; private set; } = Array.Empty<string>();
public static int First { get; private set; }
public static int Last { get; private set; }
public static string Signature { get; private set; } = "";
public static string Id(Disaster disaster)
{
return ((Object)disaster).GetIl2CppType().FullName;
}
public static bool Starvation(Disaster d)
{
return ((Il2CppObjectBase)d).TryCast<Starvation>() != null;
}
internal static void Discover(Disasters data)
{
if ((Object)(object)Data != (Object)null && ((Il2CppObjectBase)Data).Pointer == ((Il2CppObjectBase)data).Pointer && Ids.Length != 0)
{
return;
}
Disaster[] array = (from d in ((IEnumerable<Disaster>)data.Values)?.ToArray()
where d != null
select d).ToArray();
if (array != null && array.Length != 0)
{
Data = data;
Disaster[] source = array.Where((Disaster d) => !Starvation(d)).ToArray();
Ids = source.Select(Id).Distinct().OrderBy<string, string>((string x) => x, StringComparer.Ordinal)
.ToArray();
First = source.Where((Disaster d) => d.threshold >= 0).Min((Disaster d) => d.threshold);
Last = source.Max((Disaster d) => d.threshold);
Signature = Plugin.GameFingerprint + "|" + string.Join(";", array.Select((Disaster d) => $"{Id(d)}:{d.threshold}:{d.affectedOthers}")) + $"|radius={data.meteorite.radius}|castle={data.meteorite.minimumCastleTiles},{data.meteorite.meteoriteTilesToFullyAllowHitCastle}|disease={data.disease.percentageAffected}|revolt={data.revolt.percentageLost}|plague={data.plague.percentageAffected}|pirates={data.pirates.percentageLost}:{string.Join(",", (IEnumerable<Resource>)data.pirates.losableResources)}|robbers={string.Join(",", (IEnumerable<Resource>)data.robbers.losableResources)}|lightning={data.lightning.nbTile}|thunderstorm={data.thunderstorm.nbTile}|tsunami={data.tsunami.radius},{data.tsunami.startDistance},{data.tsunami.startToImpactDurationMilliseconds}";
Plugin.Settings.Discover(Ids);
((BasePlugin)Plugin.Instance).Log.LogInfo((object)("Native disaster scale: " + Signature + $"; inclusive period={Last - First + 1}"));
}
}
}
[HarmonyPatch(typeof(Disasters), "Init")]
internal static class CatalogPatch
{
private static void Postfix(Disasters __instance)
{
try
{
Catalog.Discover(__instance);
}
catch (Exception e)
{
Plugin.Healthy = false;
Plugin.Error(e);
}
}
}
[HarmonyPatch(typeof(DisastersPhase), "RandomOccurrences")]
internal static class OccurrencesPatch
{
[ThreadStatic]
internal static bool Selecting;
private static void Prefix(DisastersPhase __instance, out bool __state)
{
Catalog.Discover(__instance.disasters);
__state = Selecting;
Selecting = Plugin.Session.Active.Modified;
}
private static void Postfix(ref Il2CppReferenceArray<DisasterOccurrence> __result)
{
if (Plugin.Session.Active.Modified && __result != null)
{
__result = new Il2CppReferenceArray<DisasterOccurrence>(((IEnumerable<DisasterOccurrence>)__result).Where((DisasterOccurrence o) => o == null || Catalog.Starvation(o.Disaster) || Plugin.Session.Active.Enabled(Catalog.Id(o.Disaster))).ToArray());
}
}
private static Exception? Finalizer(Exception? __exception, bool __state)
{
Selecting = __state;
return __exception;
}
}
[HarmonyPatch(typeof(Statistics), "get_Storage")]
internal static class SelectionStoragePatch
{
private static void Postfix(ref ResourceStorage __result)
{
//IL_0008: 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)
//IL_0010: Unknown result type (might be due to invalid IL or missing references)
//IL_0035: Unknown result type (might be due to invalid IL or missing references)
//IL_0037: 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_0041: Unknown result type (might be due to invalid IL or missing references)
if (OccurrencesPatch.Selecting)
{
ResourceAmount stored = __result.stored;
stored.disaster = SkullRules.Map(stored.disaster, Catalog.First, Catalog.Last, Plugin.Session.Active);
__result = new ResourceStorage(stored, __result.capacity);
}
}
}
[HarmonyPatch(typeof(LobbySettings), "get_MultiMeteor")]
internal static class MultiMeteorPatch
{
private static void Postfix(ref bool __result)
{
Rules active = Plugin.Session.Active;
if (active.Modified)
{
if (active.Excess != ExcessSkullMode.Vanilla)
{
__result = false;
}
else if (active.ForceMultiMeteor)
{
__result = true;
}
}
}
}
[HarmonyPatch(typeof(DisastersPhase), "Disaster")]
internal static class SelectionPatch
{
private static void Prefix(ref int __0)
{
if (Plugin.Session.Active.Modified)
{
__0 = SkullRules.Map(__0, Catalog.First, Catalog.Last, Plugin.Session.Active);
}
}
private static void Postfix(ref Disaster __result, ref int __1)
{
if (__result != null && !Catalog.Starvation(__result) && !Plugin.Session.Active.Enabled(Catalog.Id(__result)))
{
__result = null;
__1 = 0;
}
}
}
[HarmonyPatch(typeof(DisasterGauge), "set_Value")]
internal static class GaugePatch
{
private sealed record Tint(DisasterGauge Gauge, List<(Image Image, Color Color)> Colors);
private static readonly Dictionary<IntPtr, Tint> tints = new Dictionary<IntPtr, Tint>();
private static void Prefix(DisasterGauge __instance, ref int __0)
{
//IL_00b5: Unknown result type (might be due to invalid IL or missing references)
IntPtr[] array = (from p in tints
where (Object)(object)p.Value.Gauge == (Object)null
select p.Key).ToArray();
foreach (IntPtr key in array)
{
tints.Remove(key);
}
if (tints.Remove(((Il2CppObjectBase)__instance).Pointer, out Tint value))
{
foreach (var color in value.Colors)
{
if ((Object)(object)color.Image != (Object)null)
{
((Graphic)color.Image).color = color.Color;
}
}
}
if (Plugin.Session.Active.Modified)
{
__0 = SkullRules.Map(__0, Catalog.First, Catalog.Last, Plugin.Session.Active);
}
}
private static void Postfix(DisasterGauge __instance)
{
//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
//IL_00eb: Unknown result type (might be due to invalid IL or missing references)
//IL_00fe: Unknown result type (might be due to invalid IL or missing references)
//IL_010b: Unknown result type (might be due to invalid IL or missing references)
//IL_0118: Unknown result type (might be due to invalid IL or missing references)
//IL_0125: Unknown result type (might be due to invalid IL or missing references)
//IL_0132: Unknown result type (might be due to invalid IL or missing references)
if (Plugin.Session.Active.Disabled.Length == 0 || __instance.icons == null || __instance.thresholds == null || (Object)(object)__instance.disasters == (Object)null)
{
return;
}
List<(Image, Color)> list = new List<(Image, Color)>();
Disaster[] source = ((IEnumerable<Disaster>)__instance.disasters.Values).ToArray();
int i;
for (i = 0; i < Math.Min(__instance.icons.Count, __instance.thresholds.Count); i++)
{
Disaster val = ((IEnumerable<Disaster>)source).FirstOrDefault((Func<Disaster, bool>)((Disaster d) => !Catalog.Starvation(d) && d.threshold == __instance.thresholds[i]));
if (val != null && !Plugin.Session.Active.Enabled(Catalog.Id(val)))
{
DisasterGaugeDisasterIcon val2 = __instance.icons[i];
Color color = ((Graphic)val2.disasterImage).color;
list.Add((val2.disasterImage, color));
((Graphic)val2.disasterImage).color = new Color(color.r * 0.35f, color.g * 0.35f, color.b * 0.35f, color.a * 0.6f);
val2.Selected = false;
val2.Multiplier = 0;
}
}
if (list.Count > 0)
{
tints[((Il2CppObjectBase)__instance).Pointer] = new Tint(__instance, list);
}
}
}
[HarmonyPatch(typeof(HarvestInterface), "UpdateResources")]
internal static class SkullAccountingPatch
{
private static void Postfix(HarvestInterface __instance)
{
//IL_0036: Unknown result type (might be due to invalid IL or missing references)
//IL_004c: Unknown result type (might be due to invalid IL or missing references)
//IL_0051: Unknown result type (might be due to invalid IL or missing references)
//IL_0079: Unknown result type (might be due to invalid IL or missing references)
//IL_007e: Unknown result type (might be due to invalid IL or missing references)
//IL_0082: Unknown result type (might be due to invalid IL or missing references)
//IL_0092: Unknown result type (might be due to invalid IL or missing references)
if (Plugin.Session.Active.AllowSkullReroll && !((Object)(object)((PhaseInterface<HarvestData, HarvestIslandState>)(object)__instance).Data == (Object)null) && !((Object)(object)__instance.disasterGauge == (Object)null))
{
HarvestData data = ((PhaseInterface<HarvestData, HarvestIslandState>)(object)__instance).Data;
long value = (long)data.TotalYields.disaster + (long)((PhaseData<HarvestData, HarvestIslandState>)(object)data).Island.statistics.Storage.stored.disaster;
__instance.disasterGauge.Value = (int)Math.Clamp(value, 0L, 2147483647L);
ResourceAmount alreadyCollected = __instance.alreadyCollected;
alreadyCollected.disaster = data.TotalYields.disaster;
__instance.alreadyCollected = alreadyCollected;
}
}
}
[HarmonyPatch(typeof(HarvestInterface), "OnRollResults")]
internal static class SkullCachePatch
{
private static void Postfix(HarvestInterface __instance, Il2CppReferenceArray<ValueTuple<Id, ResourceAmount>> __0)
{
//IL_0033: Unknown result type (might be due to invalid IL or missing references)
//IL_0046: Unknown result type (might be due to invalid IL or missing references)
if (!Plugin.Session.Active.AllowSkullReroll || __instance.harvestedSkulls == null || __instance.dices == null)
{
return;
}
BuildingDice val = default(BuildingDice);
foreach (ValueTuple<Id, ResourceAmount> item in (Il2CppArrayBase<ValueTuple<Id, ResourceAmount>>)(object)__0)
{
if (item.Item2.disaster == 0 && __instance.dices.TryGetValue(item.Item1, ref val))
{
__instance.harvestedSkulls.Remove(val);
}
}
}
}
[HarmonyPatch]
internal static class MeteorPatch
{
private static MethodBase TargetMethod()
{
return AccessTools.Method(typeof(Meteorite).GetNestedTypes().Single((Type t) => t.Name == "_Execute_d__4"), "MoveNext", (Type[])null, (Type[])null);
}
private static void Postfix(_Execute_d__4 __instance, ref bool __result)
{
//IL_0039: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
//IL_00fd: Unknown result type (might be due to invalid IL or missing references)
//IL_010b: Unknown result type (might be due to invalid IL or missing references)
//IL_0117: Unknown result type (might be due to invalid IL or missing references)
//IL_0092: Unknown result type (might be due to invalid IL or missing references)
//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
//IL_01a0: Unknown result type (might be due to invalid IL or missing references)
//IL_014c: Unknown result type (might be due to invalid IL or missing references)
//IL_014e: Unknown result type (might be due to invalid IL or missing references)
//IL_0154: Invalid comparison between Unknown and I4
//IL_0201: Unknown result type (might be due to invalid IL or missing references)
//IL_0156: Unknown result type (might be due to invalid IL or missing references)
//IL_0165: Unknown result type (might be due to invalid IL or missing references)
//IL_017e: Unknown result type (might be due to invalid IL or missing references)
//IL_0190: Unknown result type (might be due to invalid IL or missing references)
if (!__result || __instance.__1__state != 1 || Plugin.Session.Active.Targeting == MeteorTargeting.VanillaTargeting)
{
return;
}
try
{
IslandGrid grid = __instance._island_5__2.grid;
int radius = __instance.__4__this.radius;
int2 impact_5__ = __instance._impact_5__3;
List<int2> castleCells = new List<int2>();
Enumerator<Id, Building> enumerator = grid.Buildings.GetEnumerator();
while (enumerator.MoveNext())
{
Building current = enumerator.Current;
if (((Il2CppObjectBase)current.type).TryCast<BuildingTypeCastle>() == null)
{
continue;
}
IEnumerator<int2> enumerator2 = current.Positions().GetEnumerator();
IEnumerator val = ((Il2CppObjectBase)enumerator2).Cast<IEnumerator>();
try
{
while (val.MoveNext())
{
castleCells.Add(enumerator2.Current);
}
}
finally
{
((Il2CppObjectBase)enumerator2).Cast<IDisposable>().Dispose();
}
}
if (Plugin.Session.Active.Targeting == MeteorTargeting.ProtectCastle && Safe(impact_5__, radius))
{
return;
}
List<int2> list = new List<int2>();
List<int2> list2 = new List<int2>();
if (grid.size.x <= 0 || grid.size.y <= 0 || (long)grid.size.x * (long)grid.size.y > 1048576)
{
throw new InvalidOperationException("Invalid meteor grid size");
}
int2 val2 = default(int2);
for (int i = 0; i < grid.size.y; i++)
{
for (int j = 0; j < grid.size.x; j++)
{
((int2)(ref val2))..ctor(j, i);
if ((int)grid.Ground(val2) == 2 && Safe(val2, radius))
{
list.Add(val2);
if (Plugin.Session.Active.Targeting == MeteorTargeting.SaferTargeting && Safe(val2, (long)radius + 2L))
{
list2.Add(val2);
}
}
}
}
if (list2.Count > 0)
{
list = list2;
}
if (list.Count == 0)
{
__instance.__1__state = -1;
__result = false;
}
else
{
uint seed = __instance.occurrence.Seed;
__instance._impact_5__3 = list[(int)(seed % (uint)list.Count)];
}
bool Safe(int2 p, long r)
{
//IL_0010: Unknown result type (might be due to invalid IL or missing references)
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
//IL_001d: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_002c: Unknown result type (might be due to invalid IL or missing references)
foreach (int2 item in castleCells)
{
long num = (long)p.x - (long)item.x;
long num2 = (long)p.y - (long)item.y;
if (num * num + num2 * num2 <= r * r)
{
return false;
}
}
return true;
}
}
catch (Exception error)
{
__instance.__1__state = -1;
__result = false;
Plugin.Session.Fail(error);
}
}
}
[HarmonyPatch(typeof(DisasterOccurrence), "Execute")]
internal static class IntensityPatch
{
private static bool Prefix(DisasterOccurrence __instance, ref IEnumerable<int> __result)
{
Plugin.Session.ApplyIntensity();
if (!Catalog.Starvation(__instance.Disaster) && !Plugin.Session.Active.Enabled(Catalog.Id(__instance.Disaster)))
{
__result = ((Il2CppObjectBase)new Il2CppStructArray<int>(0L)).Cast<IEnumerable<int>>();
return false;
}
return true;
}
}
[HarmonyPatch(typeof(HarvestInterface), "InputSelect")]
internal static class SkullSelectPatch
{
[ThreadStatic]
internal static bool Selecting;
private static void Prefix(out bool __state)
{
__state = Selecting;
Selecting = Plugin.Session.Active.AllowSkullReroll;
}
private static Exception? Finalizer(Exception? __exception, bool __state)
{
Selecting = __state;
return __exception;
}
}
[HarmonyPatch(typeof(HarvestData), "Yield")]
internal static class SkullYieldPatch
{
private static void Postfix(ref ResourceAmount __result)
{
if (SkullSelectPatch.Selecting)
{
__result.disaster = 0;
}
}
}
[HarmonyPatch(typeof(HarvestInterface), "UpdateDices")]
internal static class SkullHoverPatch
{
private static void Postfix(HarvestInterface __instance)
{
//IL_005c: Unknown result type (might be due to invalid IL or missing references)
//IL_0062: Invalid comparison between Unknown and I4
if (!Plugin.Session.Active.AllowSkullReroll || __instance.sortedDices == null)
{
return;
}
Enumerator<BuildingDice> enumerator = __instance.sortedDices.GetEnumerator();
while (enumerator.MoveNext())
{
BuildingDice current = enumerator.Current;
if ((Object)(object)current.dice != (Object)null && (Object)(object)current.dice.hooverCursor != (Object)null)
{
((Behaviour)current.dice.hooverCursor).enabled = (int)current.state != 2;
}
}
}
}
[HarmonyPatch(typeof(GameState), "UpdateInit")]
internal static class StartGuardPatch
{
private static bool Prefix(GameState __instance)
{
Plugin.Session.Attach(__instance);
if (((NetworkedBehaviour)__instance).IsServer && !__instance.GameStarted && (__instance.startGame || __instance.islandsSpawned))
{
if (!Plugin.Session.CanStart)
{
__instance.startGame = false;
return false;
}
Plugin.Session.Freeze();
}
return true;
}
}
[HarmonyPatch(typeof(GameState), "StartGame")]
internal static class FreezePatch
{
private static bool Prefix(GameState __instance)
{
Plugin.Session.Attach(__instance);
if (((NetworkedBehaviour)__instance).IsServer && !Plugin.Session.CanStart)
{
return false;
}
return Plugin.Session.Freeze();
}
}
public enum Preset
{
Vanilla,
Balanced,
Peaceful,
Chaos,
Custom
}
public enum MeteorTargeting
{
VanillaTargeting,
ProtectCastle,
SaferTargeting
}
public enum ExcessSkullMode
{
Vanilla,
SingleMeteor,
CyclicDisasters,
NoExcessMeteors
}
public sealed record Rules
{
public Preset Preset { get; init; }
public MeteorTargeting Targeting { get; init; }
public ExcessSkullMode Excess { get; init; }
public bool AllowSkullReroll { get; init; }
public bool ForceMultiMeteor { get; init; }
public int DiseasePercent { get; init; } = 100;
public int RevoltPercent { get; init; } = 100;
public int MeteorLimit { get; init; } = 16;
public string[] Disabled { get; init; } = Array.Empty<string>();
public bool Modified
{
get
{
if (Targeting == MeteorTargeting.VanillaTargeting && Excess == ExcessSkullMode.Vanilla && !AllowSkullReroll && !ForceMultiMeteor && DiseasePercent == 100 && RevoltPercent == 100 && MeteorLimit == 16)
{
return Disabled.Length != 0;
}
return true;
}
}
public bool Valid
{
get
{
if (Enum.IsDefined(Preset) && Enum.IsDefined(Targeting) && Enum.IsDefined(Excess))
{
int diseasePercent = DiseasePercent;
if (diseasePercent >= 0 && diseasePercent <= 200)
{
diseasePercent = RevoltPercent;
if (diseasePercent >= 0 && diseasePercent <= 200)
{
diseasePercent = MeteorLimit;
if (diseasePercent >= 1 && diseasePercent <= 32 && Disabled != null && Disabled.Length <= 64 && Disabled.All((string x) => !string.IsNullOrWhiteSpace(x) && x.Length <= 100))
{
return Disabled.Distinct<string>(StringComparer.Ordinal).Count() == Disabled.Length;
}
}
}
}
return false;
}
}
public bool Enabled(string id)
{
return !Disabled.Contains<string>(id, StringComparer.Ordinal);
}
public static Rules For(Preset preset, IEnumerable<string> disasters)
{
return preset switch
{
Preset.Balanced => new Rules
{
Preset = preset,
Targeting = MeteorTargeting.ProtectCastle,
Excess = ExcessSkullMode.CyclicDisasters,
MeteorLimit = 1
},
Preset.Peaceful => new Rules
{
Preset = preset,
Targeting = MeteorTargeting.ProtectCastle,
Excess = ExcessSkullMode.SingleMeteor,
AllowSkullReroll = true,
Disabled = disasters.OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray()
},
Preset.Chaos => new Rules
{
Preset = preset,
ForceMultiMeteor = true,
MeteorLimit = 32,
DiseasePercent = 200,
RevoltPercent = 200
},
_ => new Rules
{
Preset = preset
},
};
}
}
public static class SkullRules
{
public static int Map(int skulls, int firstThreshold, int lastThreshold, Rules rules)
{
if (skulls <= 0 || lastThreshold < firstThreshold || lastThreshold <= 0)
{
return Math.Max(0, skulls);
}
switch (rules.Excess)
{
case ExcessSkullMode.CyclicDisasters:
if (skulls > lastThreshold)
{
return (int)(((long)skulls - (long)firstThreshold) % ((long)lastThreshold - (long)firstThreshold + 1) + firstThreshold);
}
break;
case ExcessSkullMode.SingleMeteor:
case ExcessSkullMode.NoExcessMeteors:
return Math.Min(skulls, lastThreshold);
}
if (rules.Modified)
{
return (int)Math.Min(skulls, (long)lastThreshold + (long)rules.MeteorLimit - 1);
}
return skulls;
}
public static int Scale(int original, int percent)
{
return (int)Math.Clamp(((long)original * (long)percent + 50) / 100, 0L, 100L);
}
}
public sealed class RuleSession
{
public sealed class Packet
{
public string Kind { get; set; } = "";
public string Version { get; set; } = "";
public string Session { get; set; } = "";
public long Revision { get; set; }
public string Digest { get; set; } = "";
public string Catalog { get; set; } = "";
public string Rules { get; set; } = "";
}
internal const string Channel = "Mikhasan.DisasterControl.v1";
private const int MaxPayload = 16000;
private GameState? game;
private Rules active = new Rules();
private string session = "";
private string digest = "";
private string sent = "";
private string clientError = "";
private readonly Dictionary<ulong, string> acknowledgements = new Dictionary<ulong, string>();
private readonly Dictionary<ulong, string> failures = new Dictionary<ulong, string>();
private readonly HashSet<ulong> exposedPeers = new HashSet<ulong>();
private float next;
private bool received;
private bool frozen;
private bool networkEver;
private bool soloMatch;
private Rules? pending;
private long revision;
private float requestedHarvestEnter;
private IntPtr requestedHarvest;
private bool loggedIncoming;
private int originalDisease = -1;
private int originalRevolt = -1;
private Disasters? intensityData;
public Rules Active => active;
public Rules Draft => pending ?? active;
public bool Pending => pending != null;
internal bool RejectLateJoin
{
get
{
if (frozen)
{
if (!active.Modified && !soloMatch)
{
return pending != null;
}
return true;
}
return false;
}
}
public bool Frozen => frozen;
public bool IsHost
{
get
{
if ((Object)(object)game != (Object)null)
{
return ((NetworkedBehaviour)game).IsServer;
}
return false;
}
}
public bool InScope
{
get
{
if ((Object)(object)game != (Object)null)
{
Interface obj = game.@interface;
if ((Object)(object)((obj != null) ? obj.startGame : null) != (Object)null && !game.@interface.startGame.isMatchmaking)
{
return !game.IsTraining;
}
}
return false;
}
}
public bool CanEdit
{
get
{
if ((Object)(object)game != (Object)null && IsHost && InScope && Plugin.Healthy)
{
if (game.GameStarted || game.startGame || frozen)
{
if (game.GameStarted && soloMatch && !networkEver)
{
return !HasRemotePeers();
}
return false;
}
return true;
}
return false;
}
}
public string Status { get; private set; } = "Open a solo or custom lobby.";
public string Digest => digest;
public int AcknowledgedPeers => acknowledgements.Count<KeyValuePair<ulong, string>>((KeyValuePair<ulong, string> p) => p.Value == digest);
public bool CanStart
{
get
{
RefreshLocal();
if (!active.Modified && exposedPeers.Count == 0)
{
return true;
}
if (!Plugin.Healthy || !InScope)
{
Status = "Modified rules unavailable in this session.";
return false;
}
NetworkingManager singleton = NetworkingManager.Singleton;
if ((Object)(object)singleton == (Object)null || !singleton.IsServer)
{
return false;
}
if (singleton.PendingClients.Count > 0)
{
Status = "Waiting for pending connections.";
return false;
}
Enumerator<NetworkedClient> enumerator = singleton.ConnectedClientsList.GetEnumerator();
while (enumerator.MoveNext())
{
NetworkedClient current = enumerator.Current;
if (current.ClientId != singleton.ServerClientId && current.ClientId != singleton.LocalClientId && (active.Modified || exposedPeers.Contains(current.ClientId)) && (!acknowledgements.TryGetValue(current.ClientId, out string value) || value != digest))
{
Status = (failures.TryGetValue(current.ClientId, out string value2) ? ("Cannot start: " + value2) : "Waiting for Disaster Control 0.9.1 on every player.");
return false;
}
}
Status = "All connected players confirmed these rules.";
return true;
}
}
internal void Fail(Exception error)
{
Plugin.Healthy = false;
pending = null;
Plugin.Error(error);
Status = "Disaster Control stopped this match after an execution error. Restart the game.";
if ((Object)(object)game != (Object)null)
{
game.Exit(Status);
}
}
internal void Attach(GameState value)
{
if (!((Object)(object)game != (Object)null) || !(((Il2CppObjectBase)game).Pointer == ((Il2CppObjectBase)value).Pointer))
{
Reset();
game = value;
RefreshLocal();
}
}
internal void Reset()
{
RestoreIntensity();
game = null;
active = new Rules();
pending = null;
frozen = (received = (soloMatch = (networkEver = false)));
revision = 0L;
session = Guid.NewGuid().ToString("N");
digest = "";
sent = "";
clientError = "";
acknowledgements.Clear();
failures.Clear();
exposedPeers.Clear();
next = 0f;
}
internal void Detach(GameState value)
{
if ((Object)(object)game != (Object)null && ((Il2CppObjectBase)game).Pointer == ((Il2CppObjectBase)value).Pointer)
{
Reset();
}
}
private void RefreshLocal()
{
if (frozen || (Object)(object)game == (Object)null)
{
return;
}
if (!InScope)
{
active = new Rules();
return;
}
if (IsHost)
{
active = Plugin.Settings.Local;
}
else if (!received)
{
active = new Rules();
}
string json = Settings.Serialize(active);
string text = Hash(json);
if (text != digest)
{
digest = text;
revision++;
acknowledgements.Clear();
failures.Clear();
sent = "";
}
}
private string Hash(string json)
{
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(Catalog.Signature + "\n" + json)));
}
public bool Set(Rules rules)
{
if (!CanEdit || !rules.Valid)
{
return false;
}
Plugin.Settings.Set(rules);
if (frozen)
{
pending = rules;
ITurnPhase turnPhase = game.TurnPhase;
HarvestPhase val = ((turnPhase != null) ? ((Il2CppObjectBase)turnPhase).TryCast<HarvestPhase>() : null);
requestedHarvest = ((val != null) ? ((Il2CppObjectBase)val).Pointer : IntPtr.Zero);
requestedHarvestEnter = ((TurnPhase<HarvestData, HarvestIslandState>)(object)val)?.enterTime ?? 0f;
Status = "Changes apply at the next harvest phase.";
}
else
{
RefreshLocal();
}
sent = "";
next = 0f;
return true;
}
internal void PhaseBoundary()
{
//IL_0057: Unknown result type (might be due to invalid IL or missing references)
if (!((Object)(object)game == (Object)null) && !(pending == null) && soloMatch && !networkEver && !HasRemotePeers())
{
ITurnPhase turnPhase = game.TurnPhase;
HarvestPhase val = ((turnPhase != null) ? ((Il2CppObjectBase)turnPhase).TryCast<HarvestPhase>() : null);
if (!((Object)(object)val == (Object)null) && (int)((TurnPhase<HarvestData, HarvestIslandState>)(object)val).State == 0 && (!(((Il2CppObjectBase)val).Pointer == requestedHarvest) || ((TurnPhase<HarvestData, HarvestIslandState>)(object)val).enterTime != requestedHarvestEnter))
{
RestoreIntensity();
active = pending;
pending = null;
digest = Hash(Settings.Serialize(active));
revision++;
ApplyIntensity();
Status = "Solo rules applied at the harvest boundary.";
}
}
}
public bool Freeze()
{
//IL_007f: Unknown result type (might be due to invalid IL or missing references)
//IL_0085: Expected O, but got Unknown
if (frozen)
{
return true;
}
RefreshLocal();
if (IsHost && !CanStart)
{
return false;
}
frozen = true;
soloMatch = IsHost && InScope && game.@interface.startGame.isSolo && !networkEver && !HasRemotePeers();
ApplyIntensity();
ManualLogSource log = ((BasePlugin)Plugin.Instance).Log;
bool flag = default(bool);
BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(37, 3, ref flag);
if (flag)
{
((BepInExLogInterpolatedStringHandler)val).AppendLiteral("Match rules frozen: ");
((BepInExLogInterpolatedStringHandler)val).AppendFormatted<Preset>(active.Preset);
((BepInExLogInterpolatedStringHandler)val).AppendLiteral("; ");
((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>(digest);
((BepInExLogInterpolatedStringHandler)val).AppendLiteral("; acknowledged=");
((BepInExLogInterpolatedStringHandler)val).AppendFormatted<int>(AcknowledgedPeers);
}
log.LogInfo(val);
return true;
}
internal void ApplyIntensity()
{
if ((Object)(object)Catalog.Data == (Object)null)
{
return;
}
if (active.DiseasePercent == 100 && active.RevoltPercent == 100)
{
RestoreIntensity();
return;
}
if ((Object)(object)intensityData != (Object)null && ((Il2CppObjectBase)intensityData).Pointer != ((Il2CppObjectBase)Catalog.Data).Pointer)
{
RestoreIntensity();
}
intensityData = Catalog.Data;
if (originalDisease < 0)
{
originalDisease = Catalog.Data.disease.percentageAffected;
}
if (originalRevolt < 0)
{
originalRevolt = Catalog.Data.revolt.percentageLost;
}
Catalog.Data.disease.percentageAffected = ((active.DiseasePercent == 100) ? originalDisease : SkullRules.Scale(originalDisease, active.DiseasePercent));
Catalog.Data.revolt.percentageLost = ((active.RevoltPercent == 100) ? originalRevolt : SkullRules.Scale(originalRevolt, active.RevoltPercent));
}
private void RestoreIntensity()
{
if ((Object)(object)intensityData != (Object)null)
{
if (originalDisease >= 0)
{
intensityData.disease.percentageAffected = originalDisease;
}
if (originalRevolt >= 0)
{
intensityData.revolt.percentageLost = originalRevolt;
}
}
intensityData = null;
originalDisease = (originalRevolt = -1);
}
public void Tick(float now)
{
try
{
Player localPlayer = Player.LocalPlayer;
GameState val = ((localPlayer != null) ? localPlayer.GameState : null);
if ((Object)(object)val == (Object)null)
{
if ((Object)(object)game != (Object)null)
{
Reset();
}
Status = "Open a solo or custom lobby.";
return;
}
Attach(val);
if (HasRemotePeers())
{
networkEver = true;
}
PhaseBoundary();
if (now < next)
{
return;
}
next = now + 1f;
RefreshLocal();
if (!InScope)
{
Status = "Original rules: matchmaking / training.";
}
else if (IsHost)
{
if (!frozen)
{
_ = CanStart;
if (!active.Modified)
{
Status = "Vanilla — original game rules.";
}
}
else
{
Status = ((!CanEdit) ? "Network rules are locked for this match." : (Pending ? "Changes apply at the next harvest phase." : "Solo: changes apply at the next harvest phase."));
}
NetworkingManager singleton = NetworkingManager.Singleton;
if ((Object)(object)singleton == (Object)null)
{
return;
}
HashSet<ulong> peers = ((IEnumerable<NetworkedClient>)singleton.ConnectedClientsList.ToArray()).Select((NetworkedClient p) => p.ClientId).ToHashSet();
ulong[] array = acknowledgements.Keys.Where((ulong id) => !peers.Contains(id)).ToArray();
foreach (ulong key in array)
{
acknowledgements.Remove(key);
}
array = failures.Keys.Where((ulong id) => !peers.Contains(id)).ToArray();
foreach (ulong key2 in array)
{
failures.Remove(key2);
}
exposedPeers.RemoveWhere((ulong id) => !peers.Contains(id));
Packet packet = new Packet
{
Kind = "rules",
Version = "0.9.1",
Session = session,
Revision = revision,
Digest = digest,
Catalog = Catalog.Signature,
Rules = Settings.Serialize(active)
};
foreach (ulong item in peers)
{
if (item != singleton.ServerClientId && item != singleton.LocalClientId && (sent != digest || !acknowledgements.TryGetValue(item, out string value) || value != digest))
{
if (active.Modified)
{
exposedPeers.Add(item);
}
Send(item, packet);
}
}
sent = digest;
}
else if (!received)
{
Status = ((clientError.Length > 0) ? clientError : "Vanilla until host rules are received.");
}
else
{
Status = (frozen ? "Host rules — locked for this match." : "Host rules — read only; confirmed.");
}
}
catch (Exception e)
{
Plugin.Error(e);
Status = "Rules synchronization error; modified start is blocked.";
acknowledgements.Clear();
}
}
internal void Receive(ulong sender, Stream stream)
{
try
{
if (!loggedIncoming)
{
((BasePlugin)Plugin.Instance).Log.LogInfo((object)"Received first disaster rules protocol message.");
loggedIncoming = true;
}
List<byte> list = new List<byte>();
int num;
while (list.Count <= 16000 && (num = stream.ReadByte()) >= 0)
{
list.Add((byte)num);
}
if (list.Count == 0 || list.Count > 16000)
{
return;
}
Packet packet = JsonSerializer.Deserialize<Packet>(Encoding.UTF8.GetString(list.ToArray()));
if (packet == null || (Object)(object)game == (Object)null || !InScope)
{
return;
}
NetworkingManager singleton = NetworkingManager.Singleton;
if ((Object)(object)singleton == (Object)null)
{
return;
}
if (IsHost)
{
if (!singleton.ConnectedClients.ContainsKey(sender))
{
return;
}
if (packet.Kind == "ack" && packet.Version == "0.9.1" && packet.Session == session && packet.Revision == revision && packet.Digest == digest)
{
acknowledgements[sender] = digest;
failures.Remove(sender);
if (!active.Modified)
{
exposedPeers.Remove(sender);
}
}
else if (packet.Kind == "error" || (packet.Kind == "ack" && packet.Version != "0.9.1"))
{
failures[sender] = "incompatible version or game data on player " + sender;
acknowledgements.Remove(sender);
}
return;
}
ulong serverClientId = singleton.NetworkConfig.NetworkTransport.ServerClientId;
bool flag = sender == singleton.ServerClientId || sender == serverClientId;
UnetTransport val = ((Il2CppObjectBase)singleton.NetworkConfig.NetworkTransport).TryCast<UnetTransport>();
if (!flag && (Object)(object)val != (Object)null)
{
flag = sender == val.GetMLAPIClientId((byte)val.serverHostId, (ushort)val.serverConnectionId, false);
}
if (!flag || packet.Kind != "rules" || frozen || (received && (packet.Session != session || packet.Revision < revision)))
{
return;
}
if (!Plugin.Healthy || packet.Version != "0.9.1" || packet.Catalog != Catalog.Signature)
{
active = new Rules();
received = false;
clientError = "Incompatible Disaster Control version, game binary or disaster data. Modified start is blocked.";
Send(singleton.ServerClientId, new Packet
{
Kind = "error",
Version = "0.9.1"
});
return;
}
Rules rules = Settings.Parse(packet.Rules);
if (rules.Disabled.Any((string id) => !Catalog.Ids.Contains<string>(id, StringComparer.Ordinal)))
{
throw new ArgumentException("Unknown disaster");
}
string text = Hash(Settings.Serialize(rules));
if (!(text != packet.Digest) && packet.Session.Length == 32 && packet.Revision > 0)
{
active = rules;
digest = text;
session = packet.Session;
revision = packet.Revision;
received = true;
clientError = "";
Send(singleton.ServerClientId, new Packet
{
Kind = "ack",
Version = "0.9.1",
Session = session,
Revision = revision,
Digest = text
});
}
}
catch (Exception e)
{
Plugin.Error(e);
}
}
private bool HasRemotePeers()
{
NetworkingManager net = NetworkingManager.Singleton;
if ((Object)(object)net != (Object)null)
{
return ((IEnumerable<NetworkedClient>)net.ConnectedClientsList.ToArray()).Any((NetworkedClient p) => p.ClientId != net.ServerClientId && p.ClientId != net.LocalClientId);
}
return false;
}
private static void Send(ulong peer, Packet packet)
{
//IL_0012: Unknown result type (might be due to invalid IL or missing references)
//IL_0018: Expected O, but got Unknown
byte[] bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(packet));
MemoryStream val = new MemoryStream();
try
{
((Stream)val).Write(new Il2CppStructArray<byte>(bytes), 0, bytes.Length);
((Stream)val).Position = 0L;
CustomMessagingManager.SendNamedMessage("Mikhasan.DisasterControl.v1", peer, (Stream)(object)val, "MLAPI_INTERNAL", (SecuritySendFlags)0);
}
finally
{
((Stream)val).Dispose();
}
}
}
[HarmonyPatch(typeof(GameState), "FullReset")]
internal static class ResetRulesPatch
{
private static void Prefix(GameState __instance)
{
Plugin.Session.Detach(__instance);
}
}
[HarmonyPatch(typeof(GameState), "Exit")]
internal static class ExitRulesPatch
{
private static void Postfix(GameState __instance)
{
Plugin.Session.Detach(__instance);
}
}
[HarmonyPatch(typeof(GameState), "ChangeTurnPhase")]
internal static class SoloBoundaryPatch
{
private static void Postfix()
{
Plugin.Session.PhaseBoundary();
}
}
[HarmonyPatch(typeof(CustomMessagingManager), "InvokeNamedMessage")]
internal static class RuleMessagePatch
{
private static bool Prefix(ulong __0, ulong __1, Stream __2)
{
if (__0 != NetworkedBehaviour.HashMethodName("Mikhasan.DisasterControl.v1"))
{
return true;
}
Plugin.Session.Receive(__1, __2);
return false;
}
}
[HarmonyPatch(typeof(NetworkingManager), "InvokeConnectionApproval")]
internal static class LateJoinGuard
{
private static bool Prefix(ConnectionApprovedDelegate __2)
{
if (!Plugin.Session.IsHost || !Plugin.Session.RejectLateJoin)
{
return true;
}
__2.Invoke(false, new Nullable<ulong>(), false, new Nullable<Vector3>(), new Nullable<Quaternion>());
return false;
}
}
internal sealed class Settings
{
private readonly ConfigFile file;
private readonly ConfigEntry<Preset> preset;
private readonly ConfigEntry<MeteorTargeting> targeting;
private readonly ConfigEntry<ExcessSkullMode> excess;
private readonly ConfigEntry<bool> reroll;
private readonly ConfigEntry<bool> multi;
private readonly ConfigEntry<int> disease;
private readonly ConfigEntry<int> revolt;
private readonly ConfigEntry<int> limit;
private readonly Dictionary<string, ConfigEntry<bool>> events = new Dictionary<string, ConfigEntry<bool>>(StringComparer.Ordinal);
private bool writing;
public Rules Local { get; private set; } = new Rules();
public ConfigEntry<string> Language { get; }
public event Action? Changed;
public Settings(ConfigFile file)
{
//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
//IL_00e1: Expected O, but got Unknown
//IL_0109: Unknown result type (might be due to invalid IL or missing references)
//IL_0113: Expected O, but got Unknown
//IL_0138: Unknown result type (might be due to invalid IL or missing references)
//IL_0142: Expected O, but got Unknown
this.file = file;
preset = file.Bind<Preset>("Rules", "Preset", Preset.Vanilla, "Vanilla on first installation. Manual edits select Custom. Peaceful disables all skull disasters; starvation stays original.");
targeting = file.Bind<MeteorTargeting>("Rules", "MeteorTargeting", MeteorTargeting.VanillaTargeting, "Choose original targeting, full castle footprint protection, or protection with a wider margin.");
excess = file.Bind<ExcessSkullMode>("Rules", "ExcessSkullMode", ExcessSkullMode.Vanilla, "SingleMeteor and NoExcessMeteors retain the first meteor; CyclicDisasters repeats the real threshold range.");
reroll = file.Bind<bool>("Rules", "AllowSkullReroll", false, "Unlock skull dice selection. The native reroll action, cost and face probabilities are preserved.");
multi = file.Bind<bool>("Rules", "ForceMultiMeteor", false, "Force native Multi-Meteor when excess mode is Vanilla. Retained when Chaos becomes Custom.");
disease = file.Bind<int>("Rules", "DiseaseIntensityPercent", 100, new ConfigDescription("Scale the original Disease percentage. Applies to Disease, not the separate Plague event.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 200), Array.Empty<object>()));
revolt = file.Bind<int>("Rules", "RevoltIntensityPercent", 100, new ConfigDescription("Scale the original Revolt loss, capped at 100% actual loss.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 200), Array.Empty<object>()));
limit = file.Bind<int>("Rules", "MeteorLimit", 16, new ConfigDescription("Safety limit for modified Vanilla/Chaos rules; ignored by completely Vanilla rules.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 32), Array.Empty<object>()));
Language = file.Bind<string>("Interface", "Language", "en", "Panel language: en or ru. Does not alter game translations.");
file.SettingChanged += delegate(object? _, SettingChangedEventArgs e)
{
if (!writing && (object)e.ChangedSetting != Language)
{
Read((object)e.ChangedSetting == preset);
}
};
}
public void Discover(IEnumerable<string> ids)
{
writing = true;
try
{
foreach (string id in ids)
{
if (!events.ContainsKey(id))
{
events.Add(id, file.Bind<bool>("Disasters", id, true, "Enable " + id + ". Starvation is independent and is never disabled."));
}
}
}
finally
{
writing = false;
}
Read(usePreset: true);
}
private void Read(bool usePreset)
{
if (!Enum.IsDefined(preset.Value))
{
Set(new Rules());
return;
}
if (usePreset && preset.Value != Preset.Custom)
{
Set(Rules.For(preset.Value, events.Keys));
return;
}
Set(new Rules
{
Preset = Preset.Custom,
Targeting = (Enum.IsDefined(targeting.Value) ? targeting.Value : MeteorTargeting.VanillaTargeting),
Excess = (Enum.IsDefined(excess.Value) ? excess.Value : ExcessSkullMode.Vanilla),
AllowSkullReroll = reroll.Value,
ForceMultiMeteor = multi.Value,
DiseasePercent = Math.Clamp(disease.Value, 0, 200),
RevoltPercent = Math.Clamp(revolt.Value, 0, 200),
MeteorLimit = Math.Clamp(limit.Value, 1, 32),
Disabled = (from e in events
where !e.Value.Value
select e.Key).OrderBy<string, string>((string x) => x, StringComparer.Ordinal).ToArray()
});
}
public void Set(Rules rules)
{
if (!rules.Valid)
{
throw new ArgumentException("Invalid disaster rules");
}
writing = true;
bool saveOnConfigSet = file.SaveOnConfigSet;
file.SaveOnConfigSet = false;
try
{
Local = rules;
preset.Value = rules.Preset;
targeting.Value = rules.Targeting;
excess.Value = rules.Excess;
reroll.Value = rules.AllowSkullReroll;
multi.Value = rules.ForceMultiMeteor;
disease.Value = rules.DiseasePercent;
revolt.Value = rules.RevoltPercent;
limit.Value = rules.MeteorLimit;
foreach (KeyValuePair<string, ConfigEntry<bool>> @event in events)
{
@event.Value.Value = rules.Enabled(@event.Key);
}
file.Save();
}
finally
{
file.SaveOnConfigSet = saveOnConfigSet;
writing = false;
}
this.Changed?.Invoke();
}
public static string Serialize(Rules r)
{
return JsonSerializer.Serialize(r);
}
public static Rules Parse(string json)
{
if (json.Length > 12000)
{
throw new ArgumentException("Rules message too large");
}
Rules? obj = JsonSerializer.Deserialize<Rules>(json) ?? throw new ArgumentException("Missing rules");
if (!obj.Valid)
{
throw new ArgumentException("Invalid rules");
}
return obj;
}
}
}