Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Saga v0.1.17
plugins/Saga/Saga.Core.dll
Decompiled 10 hours ago
The result has been truncated due to the large size, download it to view full contents!
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.Text; using System.Threading; using ExtraPotions.Saga.Config; using ExtraPotions.Saga.Conversion; using ExtraPotions.Saga.Integration; using ExtraPotions.Saga.Powers; using ExtraPotions.Saga.Progression; using ExtraPotions.Saga.Skills; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ExtraPotions")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (C) 2026 expDARE (ExtraPotions Meadworks)")] [assembly: AssemblyFileVersion("0.1.17.0")] [assembly: AssemblyInformationalVersion("0.1.17+fatal: not a git repository (or any of the parent directories): .git")] [assembly: AssemblyProduct("Saga.Core")] [assembly: AssemblyTitle("Saga.Core")] [assembly: AssemblyVersion("0.1.17.0")] [module: RefSafetyRules(11)] 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; } } [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 ExtraPotions.Saga { public static class SagaCore { public const string PluginGuid = "com.meadworks.saga"; public const string ProductName = "Saga"; public const string ProductVersion = "0.1.17"; public const string Family = "Fang"; public const string TokenNamespace = "saga"; public const string TokenPrefix = "meadworks_saga_"; public const string DataPrefix = "saga."; public const string SupportedGameVersion = "1.0.17"; } } namespace ExtraPotions.Saga.Skills { public sealed class BackstabReceipt { public const int MaxBytes = 1024; private static readonly UTF8Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); public long World { get; } public string Epoch { get; } public long VictimUser { get; } public uint VictimId { get; } public int Prefab { get; } public long Owner { get; } public long AttackerUser { get; } public uint AttackerId { get; } public long Player { get; } public string Event { get; } public BackstabReceipt(long world, string epoch, long victimUser, uint victimId, int prefab, long owner, long attackerUser, uint attackerId, long player, string id) { if (world == 0L || string.IsNullOrWhiteSpace(epoch) || epoch.Length > 128 || Utf8.GetByteCount(epoch) > 512 || epoch.IndexOfAny(new char[3] { '\0', '\r', '\n' }) >= 0 || victimUser == 0L || victimId == 0 || prefab == 0 || owner == 0L || attackerUser == 0L || attackerId == 0 || player == 0L || !Guid.TryParseExact(id, "N", out var result) || result == Guid.Empty || result.ToString("N") != id) { throw new FormatException("Invalid backstab receipt."); } World = world; Epoch = epoch; VictimUser = victimUser; VictimId = victimId; Prefab = prefab; Owner = owner; AttackerUser = attackerUser; AttackerId = attackerId; Player = player; Event = id; } public bool Same(BackstabReceipt other) { if (other != null && World == other.World && Epoch == other.Epoch && VictimUser == other.VictimUser && VictimId == other.VictimId && Prefab == other.Prefab && Owner == other.Owner && AttackerUser == other.AttackerUser && AttackerId == other.AttackerId && Player == other.Player) { return Event == other.Event; } return false; } public byte[] Encode() { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Utf8, leaveOpen: true)) { binaryWriter.Write((byte)1); binaryWriter.Write(World); byte[] bytes = Utf8.GetBytes(Epoch); binaryWriter.Write((ushort)bytes.Length); binaryWriter.Write(bytes); binaryWriter.Write(VictimUser); binaryWriter.Write(VictimId); binaryWriter.Write(Prefab); binaryWriter.Write(Owner); binaryWriter.Write(AttackerUser); binaryWriter.Write(AttackerId); binaryWriter.Write(Player); binaryWriter.Write(Guid.ParseExact(Event, "N").ToByteArray()); } return memoryStream.ToArray(); } public static BackstabReceipt Decode(byte[] bytes) { if (bytes == null || bytes.Length > 1024) { throw new FormatException("Backstab packet size."); } try { using MemoryStream memoryStream = new MemoryStream(bytes, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, Utf8); if (binaryReader.ReadByte() != 1) { throw new FormatException("Backstab packet version."); } long world = binaryReader.ReadInt64(); int num = binaryReader.ReadUInt16(); if (num < 1 || num > 512 || num > memoryStream.Length - memoryStream.Position) { throw new FormatException("Backstab epoch size."); } string epoch = Utf8.GetString(binaryReader.ReadBytes(num)); long victimUser = binaryReader.ReadInt64(); uint victimId = binaryReader.ReadUInt32(); int prefab = binaryReader.ReadInt32(); long owner = binaryReader.ReadInt64(); long attackerUser = binaryReader.ReadInt64(); uint attackerId = binaryReader.ReadUInt32(); long player = binaryReader.ReadInt64(); byte[] array = binaryReader.ReadBytes(16); if (array.Length != 16 || memoryStream.Position != memoryStream.Length) { throw new FormatException("Backstab packet boundary."); } return new BackstabReceipt(world, epoch, victimUser, victimId, prefab, owner, attackerUser, attackerId, player, new Guid(array).ToString("N")); } catch (EndOfStreamException innerException) { throw new FormatException("Truncated backstab packet.", innerException); } catch (DecoderFallbackException innerException2) { throw new FormatException("Invalid backstab UTF8.", innerException2); } } } public enum BackstabAdmission { Accepted, Duplicate, Conflict, WrongSession, InvalidAuthority, RateLimited, Capacity } public sealed class BackstabReceipts { private sealed class Entry { internal BackstabReceipt Receipt; internal byte Stages; } public const int Capacity = 4096; public const int PerSecond = 64; private readonly Dictionary<string, Entry> receipts = new Dictionary<string, Entry>(StringComparer.Ordinal); private long world; private string epoch = ""; private double window = double.NaN; private int calls; public int Count => receipts.Count; public void Session(long currentWorld, string currentEpoch) { if (world != currentWorld || !(epoch == currentEpoch)) { world = currentWorld; epoch = currentEpoch; receipts.Clear(); window = double.NaN; calls = 0; } } public BackstabAdmission Admit(BackstabReceipt receipt, long currentWorld, string currentEpoch, bool verified, double now, bool recipient = false) { if (currentWorld == 0L || string.IsNullOrEmpty(currentEpoch) || receipt.World != currentWorld || receipt.Epoch != currentEpoch) { return BackstabAdmission.WrongSession; } Session(currentWorld, currentEpoch); if (double.IsNaN(now) || double.IsInfinity(now) || (!double.IsNaN(window) && now < window)) { return BackstabAdmission.InvalidAuthority; } if (double.IsNaN(window) || now - window >= 1.0) { window = now; calls = 0; } if (calls >= 64) { return BackstabAdmission.RateLimited; } calls++; if (!verified) { return BackstabAdmission.InvalidAuthority; } byte b = (byte)((!recipient) ? 1 : 2); if (receipts.TryGetValue(receipt.Event, out Entry value)) { if (!value.Receipt.Same(receipt)) { return BackstabAdmission.Conflict; } if ((value.Stages & b) != 0) { return BackstabAdmission.Duplicate; } value.Stages |= b; return BackstabAdmission.Accepted; } if (receipts.Count >= 4096) { return BackstabAdmission.Capacity; } receipts.Add(receipt.Event, new Entry { Receipt = receipt, Stages = b }); return BackstabAdmission.Accepted; } public bool Contains(BackstabReceipt receipt) { if (receipts.TryGetValue(receipt.Event, out Entry value) && (value.Stages & 1) != 0) { return value.Receipt.Same(receipt); } return false; } } public static class CraftingMarks { public const string MakerKey = "grindstone_maker"; public const string BuilderKey = "grindstone_builder"; public const float JourneymanLevel = 75f; public const float ThriftyChanceAt100 = 0.1f; public static float Share(float level) { return 0.25f * RunningRules.Factor(level); } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } public static string FormatLevel(float level) { return ((Finite(level) && level > 0f) ? level : 0f).ToString("0.##", CultureInfo.InvariantCulture); } public static float ReadMakerShare(IDictionary<string, string>? data) { if (data == null || !data.TryGetValue("grindstone_maker", out string value) || !float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return 0f; } return Share(result); } public static Dictionary<string, string> WithMakerLevel(IDictionary<string, string>? data, float level) { Dictionary<string, string> obj = ((data == null) ? new Dictionary<string, string>() : new Dictionary<string, string>(data)); obj["grindstone_maker"] = FormatLevel(level); return obj; } public static float MaximumFromBase(float maximum, float level) { if (!Finite(maximum) || maximum <= 0f) { return maximum; } float num = maximum * (1f + Share(level)); if (!Finite(num)) { return maximum; } return num; } public static float HealthFraction(float maximum, float? storedHealth) { if (!Finite(maximum) || maximum <= 0f) { return 0f; } if (!storedHealth.HasValue) { return 1f; } float value = storedHealth.Value; if (!Finite(value)) { return 0f; } return Math.Max(0f, Math.Min(1f, value / maximum)); } public static float ThriftyChance(float level) { return 0.1f * RunningRules.Factor(level); } public static bool RollThrifty(float level, float roll) { if (Finite(roll) && roll >= 0f) { return roll < ThriftyChance(level); } return false; } public static bool JourneymanUnlocked(float level) { return RunningRules.Factor(level) >= 0.75f; } } public enum DefenseMilestone { Riposte, ShieldWall, Hardened, LastStand } public sealed class DefenseValues { public float MaxHealthAt100; public float FoodHealthAt100; public float DamageReductionAt100; public float RegenerationAt100; public float PoiseAt100; public float ParryWindowAt100; public float BlockStaminaReductionAt100; public float DodgeStaminaReductionAt100; public float ExperienceMultiplier; public float BlockExperience; public float ParryMultiplier; public float WeaponBlockShare; public float HitTakenExperience; public float HitSizeDamage; public float MinHitSize; public float MaxHitSize; public float FirstBlockBonus; public int RiposteLevel; public int ShieldWallLevel; public int HardenedLevel; public int LastStandLevel; public int HardenedMaxStacks; public float HardenedPerStack; public float ShieldWallReduction; public float DesperationHealth; public float DesperationMultiplier; public static DefenseValues Defaults() { return new DefenseValues { MaxHealthAt100 = SagaSettings.Defense.MaxHealthAt100.Default, FoodHealthAt100 = SagaSettings.Defense.FoodHealthAt100.Default, DamageReductionAt100 = SagaSettings.Defense.DamageReductionAt100.Default, RegenerationAt100 = SagaSettings.Defense.RegenerationAt100.Default, PoiseAt100 = SagaSettings.Defense.PoiseAt100.Default, ParryWindowAt100 = SagaSettings.Defense.ParryWindowAt100.Default, BlockStaminaReductionAt100 = SagaSettings.Defense.BlockStaminaReductionAt100.Default, DodgeStaminaReductionAt100 = SagaSettings.Defense.DodgeStaminaReductionAt100.Default, ExperienceMultiplier = SagaSettings.Defense.ExperienceMultiplier.Default, BlockExperience = SagaSettings.Defense.BlockExperience.Default, ParryMultiplier = SagaSettings.Defense.ParryMultiplier.Default, WeaponBlockShare = SagaSettings.Defense.WeaponBlockShare.Default, HitTakenExperience = SagaSettings.Defense.HitTakenExperience.Default, HitSizeDamage = SagaSettings.Defense.HitSizeDamage.Default, MinHitSize = SagaSettings.Defense.MinHitSize.Default, MaxHitSize = SagaSettings.Defense.MaxHitSize.Default, FirstBlockBonus = SagaSettings.Defense.FirstBlockBonus.Default, RiposteLevel = SagaSettings.Defense.RiposteLevel.Default, ShieldWallLevel = SagaSettings.Defense.ShieldWallLevel.Default, HardenedLevel = SagaSettings.Defense.HardenedLevel.Default, LastStandLevel = SagaSettings.Defense.LastStandLevel.Default, HardenedMaxStacks = SagaSettings.Defense.HardenedMaxStacks.Default, HardenedPerStack = SagaSettings.Defense.HardenedPerStack.Default, ShieldWallReduction = SagaSettings.Defense.ShieldWallReduction.Default, DesperationHealth = SagaSettings.Defense.DesperationHealth.Default, DesperationMultiplier = SagaSettings.Defense.DesperationMultiplier.Default }; } } public readonly struct DefenseHit { public bool Blocked { get; } public bool Parried { get; } public bool WithShield { get; } public bool FirstOfKind { get; } public float Taken { get; } public float RawDamage { get; } public DefenseHit(bool blocked, bool parried, bool withShield, bool firstOfKind, float taken, float rawDamage) { Blocked = blocked; Parried = parried; WithShield = withShield; FirstOfKind = firstOfKind; Taken = taken; RawDamage = rawDamage; } } public sealed class ExperienceClock { private float last = float.NegativeInfinity; public bool TryCredit(float now, float cooldown) { if (now - last < Math.Max(0f, cooldown)) { return false; } last = now; return true; } public void Reset() { last = float.NegativeInfinity; } } public static class DefenseRules { public const float GameParryWindow = 0.25f; private const float MaxEach = 0.9f; public static float BonusHealth(DefenseValues v, float level) { return Math.Max(0f, v.MaxHealthAt100) * SkillCurve.Factor(level); } public static float FoodHealth(DefenseValues v, float level, float foodHealth) { return Math.Max(0f, foodHealth) * SkillCurve.Share(v.FoodHealthAt100, level); } public static float Toughness(DefenseValues v, float level, bool desperate) { float num = SkillCurve.Share(v.DamageReductionAt100, level); if (desperate) { num *= Math.Max(1f, v.DesperationMultiplier); } return Math.Min(0.9f, num); } public static float Keep(DefenseValues v, float level, bool desperate, int hardenedStacks, bool sheltered) { return (1f - Toughness(v, level, desperate)) * (1f - Math.Min(0.9f, (float)Math.Max(0, hardenedStacks) * Math.Max(0f, v.HardenedPerStack) / 100f)) * (1f - Math.Min(0.9f, sheltered ? Math.Min(1f, Math.Max(0f, v.ShieldWallReduction / 100f)) : 0f)); } public static bool Desperate(DefenseValues v, float level, float health, float maxHealth) { float num = v.DesperationHealth / 100f; if (num <= 0f || v.DesperationMultiplier <= 1f || SkillCurve.Share(v.DamageReductionAt100, level) <= 0f) { return false; } if (maxHealth > 0f && health > 0f) { return health / maxHealth < num; } return false; } public static float BlockStaminaFactor(DefenseValues v, float level) { return 1f - Math.Min(1f, SkillCurve.Share(v.BlockStaminaReductionAt100, level)); } public static float DodgeStaminaFactor(DefenseValues v, float level) { return 1f - Math.Min(1f, SkillCurve.Share(v.DodgeStaminaReductionAt100, level)); } public static float ParryWindow(DefenseValues v, float level) { return 0.25f + Math.Max(0f, v.ParryWindowAt100) * SkillCurve.Factor(level); } public static float PoiseFactor(DefenseValues v, float level) { return 1f + SkillCurve.Share(v.PoiseAt100, level); } public static float RegenerationHeal(DefenseValues v, float level, float maxHealth, float regenMultiplier, bool outOfCombat) { if (!outOfCombat) { return 0f; } return Math.Max(0f, maxHealth) * SkillCurve.Share(v.RegenerationAt100, level) * Math.Max(0f, regenMultiplier); } public static int MilestoneLevel(DefenseValues v, DefenseMilestone milestone) { return milestone switch { DefenseMilestone.Riposte => v.RiposteLevel, DefenseMilestone.ShieldWall => v.ShieldWallLevel, DefenseMilestone.Hardened => v.HardenedLevel, DefenseMilestone.LastStand => v.LastStandLevel, _ => throw new ArgumentException("Unknown milestone.", "milestone"), }; } public static bool Unlocked(DefenseValues v, DefenseMilestone milestone, float level) { return SkillCurve.Reached(level, MilestoneLevel(v, milestone)); } public static int NextHardenedStacks(DefenseValues v, int current) { return Math.Min(Math.Max(0, current) + 1, Math.Max(1, v.HardenedMaxStacks)); } public static float HitSize(DefenseValues v, float rawDamage) { float num = Math.Max(1f, v.HitSizeDamage); float val = Math.Max(0f, v.MinHitSize); return Math.Min(Math.Max(val, v.MaxHitSize), Math.Max(val, (float)Math.Sqrt(Math.Max(0f, rawDamage) / num))); } public static float Experience(DefenseValues v, DefenseHit hit) { float num; if (hit.Blocked) { num = Math.Max(0f, v.BlockExperience); if (hit.Parried) { num *= Math.Max(0f, v.ParryMultiplier); } if (!hit.WithShield) { num *= Math.Min(1f, Math.Max(0f, v.WeaponBlockShare / 100f)); } if (num > 0f && hit.FirstOfKind) { num *= Math.Max(1f, v.FirstBlockBonus); } } else { num = ((hit.Taken > 0f) ? Math.Max(0f, v.HitTakenExperience) : 0f); } return num * HitSize(v, hit.RawDamage) * Math.Max(0f, v.ExperienceMultiplier); } } public static class DodgingRules { public const float CounterLevel = 50f; public const float HeavyLevel = 75f; public static float ExtraShare(float level) { return 0.3f * RunningRules.Factor(level); } public static float HeavyCost(float current, bool lifted) { if (!lifted) { return current; } return current * 2f; } } public sealed class RollWindow { private float start = float.NegativeInfinity; private float until = float.NegativeInfinity; public float Last { get; private set; } public bool Extended => until > float.NegativeInfinity; public void Start(float now) { start = now; until = float.NegativeInfinity; } public bool Mortal(float now, float level, bool wasInvincible) { float num = now - start; start = float.NegativeInfinity; if (!wasInvincible || num <= 0f || num > 1.5f || float.IsNaN(num) || float.IsInfinity(num)) { return false; } Last = num; float num2 = num * DodgingRules.ExtraShare(level); if (num2 <= 0f) { return false; } until = now + num2; return true; } public bool Expire(float now, bool active, bool nativeRolling) { if (now - start > 1.5f) { start = float.NegativeInfinity; } if (!Extended || (active && nativeRolling && now < until)) { return false; } until = float.NegativeInfinity; return nativeRolling; } public bool Reset(bool nativeRolling = false) { bool result = Extended && nativeRolling; start = (until = float.NegativeInfinity); Last = 0f; return result; } } public sealed class DodgeCounterWindow { private float until = float.NegativeInfinity; private object? chosen; public void Arm(float now, float level) { if (level >= 50f) { until = now + 2f; } } public void Started(object? attack, bool melee, float now) { chosen = ((melee && attack != null && now < until) ? attack : null); if (melee) { until = float.NegativeInfinity; } } public bool Applies(object? current, bool ranged) { if (chosen != null && chosen == current) { return !ranged; } return false; } public void Reset() { until = float.NegativeInfinity; chosen = null; } } public static class MeleeRules { public const float Level = 50f; public const float UnarmedShare = 1.5f; public static float SwordShare(bool staggering) { if (!staggering) { return 1f; } return 1.25f; } public static float KnifePoison(float total, float chop, float pickaxe) { return Math.Max(0f, total - chop - pickaxe) * 0.2f; } public static float ClubStagger(float current) { if (!(current * 1.3f < 100f)) { return current; } return current * 1.3f; } public static float PolearmPush(float current) { return current * 1.3f; } public static float SpearShare(bool ranged) { if (!ranged) { return 1f; } return 1.25f; } } public sealed class AxeChain { private readonly int[] targets = new int[8]; private readonly int[] steps = new int[8]; private readonly float[] times = new float[8]; private readonly bool[] used = new bool[8]; public float Hit(int target, float now) { int num = -1; int num2 = 0; for (int i = 0; i < targets.Length; i++) { if (used[i] && targets[i] == target) { num = i; break; } if (!used[i]) { num2 = i; } else if (used[num2] && times[i] < times[num2]) { num2 = i; } } if (num < 0) { num = num2; } bool flag = used[num] && targets[num] == target && now >= times[num] && now - times[num] <= 3f; steps[num] = (flag ? Math.Min(5, steps[num] + 1) : 0); used[num] = true; targets[num] = target; times[num] = now; return 1f + 0.05f * (float)steps[num]; } public void Reset() { Array.Clear(used, 0, used.Length); } } public enum Biome { None, Meadows, BlackForest, Swamp, Mountain, Plains, Mistlands, AshLands, DeepNorth, Ocean } internal static class Pct { public static float Of(float value) { return Math.Max(0f, value) / 100f; } } public sealed class WoodValues { public float Multiplier; public float TierExperience; public float SmallWoodHealth; public float SmallWoodExperience; public float Fell; public float Split; public float Discovery; public bool TierScaling; public static WoodValues Defaults() { return new WoodValues { Multiplier = SagaSettings.Woodcutting.ExperienceMultiplier.Default, TierScaling = SagaSettings.Woodcutting.TierScaling.Default, TierExperience = SagaSettings.Woodcutting.TierExperience.Default, SmallWoodHealth = SagaSettings.Woodcutting.SmallWoodHealth.Default, SmallWoodExperience = SagaSettings.Woodcutting.SmallWoodExperience.Default, Fell = SagaSettings.Woodcutting.FellExperience.Default, Split = SagaSettings.Woodcutting.SplitExperience.Default, Discovery = SagaSettings.Woodcutting.DiscoveryMultiplier.Default }; } } public static class WoodExperience { public static float TierFactor(WoodValues v, int tier) { if (!v.TierScaling) { return 1f; } return 1f + Pct.Of(v.TierExperience) * (float)Math.Max(0, tier); } public static float SwingScale(WoodValues v, int tier, bool canCut, float health) { float num = Math.Max(0f, v.Multiplier) * (canCut ? TierFactor(v, tier) : 1f); if (!(health < v.SmallWoodHealth)) { return num; } return num * Math.Min(1f, Pct.Of(v.SmallWoodExperience)); } public static float FellAmount(WoodValues v, int tier) { return Math.Max(0f, v.Fell) * TierFactor(v, tier); } public static float SplitAmount(WoodValues v, int tier, bool dropsWood) { if (!dropsWood) { return 0f; } return Math.Max(0f, v.Split) * TierFactor(v, tier); } public static float Credit(WoodValues v, float amount, bool fell, bool firstOfKind) { float num = Math.Max(0f, amount) * Math.Max(0f, v.Multiplier); if (!(num > 0f && fell && firstOfKind)) { return num; } return num * Math.Max(1f, v.Discovery); } } public sealed class MineValues { public float Multiplier; public float BiomeStep; public float OreBonus; public float CleanStrikeExperience; public float DiscoveryExperience; public static MineValues Defaults() { return new MineValues { Multiplier = SagaSettings.Mining.ExperienceMultiplier.Default, BiomeStep = SagaSettings.Mining.BiomeStep.Default, OreBonus = SagaSettings.Mining.OreBonus.Default, CleanStrikeExperience = SagaSettings.Mining.CleanStrikeExperience.Default, DiscoveryExperience = SagaSettings.Mining.DiscoveryExperience.Default }; } } public static class MineExperience { public static int BiomeStep(Biome biome) { switch (biome) { case Biome.BlackForest: return 1; case Biome.Swamp: return 2; case Biome.Mountain: return 3; case Biome.Plains: return 4; case Biome.Mistlands: return 5; case Biome.AshLands: case Biome.DeepNorth: return 6; default: return 0; } } public static float Scale(MineValues v, Biome biome, bool ore, bool canMine) { if (!canMine) { return 1f; } float num = 1f + Pct.Of(v.BiomeStep) * (float)BiomeStep(biome); if (!ore) { return num; } return num * (1f + Pct.Of(v.OreBonus)); } public static float Credit(float setting, MineValues v, float scale) { return Math.Max(0f, setting) * scale * Math.Max(0f, v.Multiplier); } } public sealed class ForageValues { public float PerPick; public float BiomeStep; public float Discovery; public float Planted; public static ForageValues Defaults() { return new ForageValues { PerPick = SagaSettings.Foraging.ExperiencePerPick.Default, BiomeStep = SagaSettings.Foraging.BiomeStep.Default, Discovery = SagaSettings.Foraging.DiscoveryMultiplier.Default, Planted = SagaSettings.Foraging.PlantedExperience.Default }; } } public static class ForageExperience { public static float Scale(ForageValues v, float itemFactor, Biome biome, bool firstOfKind, bool planted = false) { float num = PlantedShare(v.Planted, planted) * Math.Max(0f, v.PerPick) * Math.Max(0f, itemFactor) * (1f + Pct.Of(v.BiomeStep) * (float)MineExperience.BiomeStep(biome)); if (!(num > 0f && firstOfKind)) { return num; } return num * Math.Max(1f, v.Discovery); } public static float PlantedShare(float plantedPercent, bool planted) { if (!planted) { return 1f; } return Math.Min(1f, Math.Max(0f, Pct.Of(plantedPercent))); } } public sealed class FishValues { public float Multiplier; public float EmptyReel; public float FightReel; public float Catch; public float SizeBonus; public float Discovery; public float NewSize; public static FishValues Defaults() { return new FishValues { Multiplier = SagaSettings.Fishing.ExperienceMultiplier.Default, EmptyReel = SagaSettings.Fishing.EmptyReelExperience.Default, FightReel = SagaSettings.Fishing.FightExperience.Default, Catch = SagaSettings.Fishing.CatchExperience.Default, SizeBonus = SagaSettings.Fishing.SizeBonus.Default, Discovery = SagaSettings.Fishing.DiscoveryExperience.Default, NewSize = SagaSettings.Fishing.NewSizeExperience.Default }; } } public static class FishExperience { public static float ReelScale(FishValues v, bool fishOnLine) { return Pct.Of(fishOnLine ? v.FightReel : v.EmptyReel) * Math.Max(0f, v.Multiplier); } public static float Catch(FishValues v, float speciesScale, int fishLevel) { return Math.Max(0f, v.Catch) * Math.Max(0f, speciesScale) * (1f + Pct.Of(v.SizeBonus) * (float)Math.Max(0, fishLevel - 1)) * Math.Max(0f, v.Multiplier); } public static float Discovery(FishValues v, float speciesScale, bool newSpecies) { return Math.Max(0f, newSpecies ? v.Discovery : v.NewSize) * Math.Max(0f, speciesScale) * Math.Max(0f, v.Multiplier); } } public static class WoodCreditRules { public const int MaxToolTier = 4; public static float Cap(WoodValues values, bool fell) { if (!fell) { return WoodExperience.SplitAmount(values, 4, dropsWood: true); } return WoodExperience.FellAmount(values, 4); } public static float Clamp(float amount, float cap) { if (float.IsNaN(amount) || float.IsInfinity(amount) || amount <= 0f) { return 0f; } if (float.IsNaN(cap) || float.IsInfinity(cap) || cap <= 0f) { return 0f; } if (!(amount < cap)) { return cap; } return amount; } public static int ClampChain(int chain, int maxChain) { if (maxChain < 0) { maxChain = 0; } if (chain < 0) { return 0; } if (chain <= maxChain) { return chain; } return maxChain; } public static bool KindMatches(bool sourceIsTree, bool sourceIsLog, bool fell) { if (sourceIsTree == sourceIsLog) { return false; } if (!fell) { return sourceIsLog; } return sourceIsTree; } public static bool ServerAccepts(bool server, bool senderOwnsSource, bool kindMatchesSource, bool firstForSource) { return server && senderOwnsSource && kindMatchesSource && firstForSource; } public static bool ClientAccepts(bool senderIsServer) { return senderIsServer; } } public static class CleanStrikeRules { public static bool Keep(bool active, bool ownsRock, bool minerPresent, bool senderOwnsMiner, int area) { if (active && ownsRock && minerPresent && senderOwnsMiner) { return area >= 0; } return false; } } public static class JumpingRules { public const float RollLevel = 50f; public static float CostShare(float level) { return 1f - 0.5f * SkillCurve.Factor(level); } public static float DamageShare(float level) { return 1f - 0.5f * SkillCurve.Factor(level); } public static float SafeHeight(float level) { return 4f + 4f * SkillCurve.Factor(level); } public static float FallDamage(float height, float level) { return Math.Max(0f, Math.Min(1f, (height - SafeHeight(level)) / 16f)) * 100f * DamageShare(level); } public static float FreeRollHeight(float level) { return 2f * SafeHeight(level); } public static bool Forward(float moveLength, float lookLengthSquared, float dot) { if (moveLength >= 0.1f && lookLengthSquared >= 0.0001f) { return dot >= 0.5f; } return false; } public static float ApplyFall(float nativeBase, float modified, float height, float level, bool rolled) { if (nativeBase <= 0f || modified <= 0f) { return modified; } float num = FallDamage(height, level); if (rolled) { num = ((height <= FreeRollHeight(level)) ? 0f : (num * 0.5f)); } return modified * (num / nativeBase); } } public static class RangedRules { public const float Level = 50f; public const float FreeArrowChanceAt100 = 0.25f; public const float EitrRegenBonus = 0.25f; public const float HealthShare = 0.75f; public static bool Unlocked(float level) { return RunningRules.Factor(level) >= 0.5f; } internal static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } public static float FreeArrowChance(float level) { return 0.25f * RunningRules.Factor(level); } public static bool FreeArrow(float level, float roll) { if (Finite(roll) && roll >= 0f) { return roll < FreeArrowChance(level); } return false; } public static float ExtraEitrRegen(float level, bool holdsElementalStaff) { if (!holdsElementalStaff || !Unlocked(level)) { return 0f; } return 0.25f; } public static float BloodHealthCost(float current, float level, bool trainsBloodMagic) { if (!(current > 0f && Finite(current) && trainsBloodMagic) || !Unlocked(level)) { return current; } return current * 0.75f; } } public sealed class ReloadMemo { public const float Window = 30f; private object? weapon; private float progress; private float at; public bool HasValue => weapon != null; public bool Remember(object? currentWeapon, float currentProgress, float now, float level, bool trainsCrossbows) { if (currentWeapon == null || !trainsCrossbows || !RangedRules.Unlocked(level) || !RangedRules.Finite(currentProgress) || currentProgress <= 0f || !RangedRules.Finite(now)) { return false; } weapon = currentWeapon; progress = currentProgress; at = now; return true; } public float Resume(object? queuedWeapon, float queuedProgress, float duration, float now, float level, bool trainsCrossbows) { if (queuedWeapon == null) { return queuedProgress; } object obj = weapon; float val = progress; float num = now - at; Reset(); if (obj == null || queuedWeapon != obj || !trainsCrossbows || !RangedRules.Unlocked(level) || !RangedRules.Finite(now) || !RangedRules.Finite(num) || num < 0f || num > 30f || !RangedRules.Finite(queuedProgress) || queuedProgress > 0f || !RangedRules.Finite(duration) || duration < 0f) { return queuedProgress; } return Math.Min(val, duration); } public void Reset() { weapon = null; progress = (at = 0f); } } public static class RunningRules { public const float BurdenLevel = 50f; public const float WindLevel = 75f; public const float WindStamina = 30f; public const float WindCooldown = 180f; public static float Factor(float level) { if (!float.IsNaN(level) && !float.IsInfinity(level)) { return Math.Max(0f, Math.Min(1f, level / 100f)); } return 0f; } public static float Carry(float level, float worldRate) { return 50f * Factor(level) * worldRate; } public static float EncumberedDrain(float current, float level) { if (!(level >= 50f)) { return current; } return current * 0.5f; } } public sealed class RunningWind { private object? actor; private float readyAt = float.NegativeInfinity; public void Sync(object? current, bool active) { if (!active || actor != current) { readyAt = float.NegativeInfinity; actor = (active ? current : null); } } public float Remaining(float now) { return Math.Max(0f, readyAt - now); } public bool TryUse(float now, float level, bool hadStamina, bool hasStamina, bool running) { if (actor == null || float.IsNaN(level) || float.IsInfinity(level) || level < 75f || !hadStamina || hasStamina || running || now < readyAt || float.IsNaN(now) || float.IsInfinity(now)) { return false; } readyAt = now + 180f; return true; } } public enum SagaSkill { Defense, Sneak, WoodCutting, Pickaxes, Foraging, Fishing } public sealed class SkillInfo { public SagaSkill Skill { get; } public string Name { get; } public int GameType { get; } public bool IsCustom => Identity.Length > 0; public string Identity { get; } public string PublishKey { get { if (!IsCustom) { return ""; } return Identity + "_level"; } } public string ImportFrom { get; } public int ImportType { get { if (ImportFrom.Length <= 0) { return 0; } return SkillHash.CustomType(ImportFrom); } } public string TextKey { get { if (!IsCustom) { return ""; } return "skill_" + Skill.ToString().ToLowerInvariant(); } } public SagaModule Module { get; } public string IconResource { get; } public string Description { get; } internal SkillInfo(SagaSkill skill, string name, int gameType, string identity, string importFrom, SagaModule module, string icon, string description) { Skill = skill; Name = name; GameType = gameType; Identity = identity; ImportFrom = importFrom; Module = module; IconResource = icon; Description = description; } public override string ToString() { return Name + " (" + GameType + ")"; } } public static class SagaSkills { public const int DefenseType = 210799442; public const int ForagingType = 1181927485; private static readonly SkillInfo[] skills = new SkillInfo[6] { new SkillInfo(SagaSkill.Defense, "Defense", 210799442, "meadworks_defense", "grindstone_defense", SagaModule.Defense, "Saga.skill_defense.png", "Toughness: more health and food health, less damage taken, recovery out of combat, poise, a wider parry window and cheaper blocks and dodges. Trained by blocking and by taking hits."), new SkillInfo(SagaSkill.Sneak, "Sneak", 101, "", "", SagaModule.Sneak, "", "Moving unseen: faster crouched movement and lower visibility as the skill grows. Trained by sneaking and by backstabs."), new SkillInfo(SagaSkill.WoodCutting, "Woodcutting", 13, "", "", SagaModule.Woodcutting, "Saga.skill_woodcutting.png", "Felling and chopping: stronger falls, safer logs, domino falls, clean fells and splits, replanting and finds in felled trees."), new SkillInfo(SagaSkill.Pickaxes, "Pickaxes", 12, "", "", SagaModule.Mining, "Saga.skill_pickaxes.png", "Mining: seams and clean strikes, starred veins, extra ore, splash damage and finds in the rock."), new SkillInfo(SagaSkill.Foraging, "Foraging", 1181927485, "meadworks_foraging", "grindstone_foraging", SagaModule.Foraging, "Saga.skill_foraging.png", "Gathering wild plants: extra yield, best-time picks and sweep picking around the plant. Trained by every wild pick."), new SkillInfo(SagaSkill.Fishing, "Fishing", 104, "", "", SagaModule.Fishing, "Saga.skill_fishing.png", "Fishing: the line fight, better bites, big and legendary fish, bonus catches, bait saving and longer casts.") }; public static IReadOnlyList<SkillInfo> All { get; } = Array.AsReadOnly(skills); public static SkillInfo Get(SagaSkill skill) { return skills.FirstOrDefault((SkillInfo s) => s.Skill == skill) ?? throw new ArgumentException("Unknown skill.", "skill"); } public static bool TryGetByType(int gameType, out SkillInfo info) { info = skills.FirstOrDefault((SkillInfo s) => s.GameType == gameType); return info != null; } } public static class SkillCurve { public const float MaxLevel = 100f; public static float Factor(float level) { if (!float.IsNaN(level)) { return Math.Min(1f, Math.Max(0f, level / 100f)); } return 0f; } public static float Share(float percentAt100, float level) { return Math.Max(0f, percentAt100) / 100f * Factor(level); } public static bool Reached(float level, int milestone) { if ((float)milestone <= 100f) { return level >= (float)milestone; } return false; } public static float Lerp(float at0, float at100, float level) { return at0 + (at100 - at0) * Factor(level); } } public static class SkillHash { public static int Of(string text) { if (text == null) { throw new ArgumentNullException("text"); } int num = 5381; int num2 = num; for (int i = 0; i < text.Length && text[i] != 0; i += 2) { num = ((num << 5) + num) ^ text[i]; if (i == text.Length - 1 || text[i + 1] == '\0') { break; } num2 = ((num2 << 5) + num2) ^ text[i + 1]; } return num + num2 * 1566083941; } public static int CustomType(string identity) { return Of(identity) & 0x7FFFFFFF; } } public enum SneakTick { Direct, Burning, Poison } public static class SneakRules { private static float Clamp(float value, float min, float max) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(min, Math.Min(max, value)); } return min; } public static float SpeedMultiplier(float level, float at100 = 1.5f) { return 1f + (Clamp(at100, 1f, 2.5f) - 1f) * Clamp(level / 100f, 0f, 1f); } public static float Visibility(float level, float light, float effect = 2f) { light = Clamp(light, 0f, 1f); return Clamp(0.5f + light * 0.5f - (0.3f + light * 0.1f) * Clamp(level / 100f, 0f, 1f) * Clamp(effect, 1f, 2f), 0.1f, 1f); } public static float NoiseRange(float range, float level) { if (!(range > 0f) || float.IsInfinity(range)) { return range; } return range * (1f - 0.5f * Clamp(level / 100f, 0f, 1f)); } public static float BackstabBonus(float bonus, float level, bool hasAi) { if (!hasAi || !(bonus > 1f) || float.IsInfinity(bonus)) { return bonus; } float num = bonus * (1f + 0.5f * Clamp(level / 100f, 0f, 1f)); if (!float.IsInfinity(num)) { return num; } return bonus; } public static bool KeepCrouching(SneakTick tick, bool hasAttacker, bool otherDamage, bool positiveTick, bool lethal, bool stagger, bool knockback) { if (tick != SneakTick.Direct && !hasAttacker && !otherDamage && positiveTick && !lethal && !stagger) { return !knockback; } return false; } } public static class SwimmingRules { public static float WetTime(float duration, float level) { if (!(duration > 0f)) { return duration; } return duration * (1f - 0.5f * SkillCurve.Factor(level)); } public static float BreathDelay(float level) { return 3f * SkillCurve.Factor(level); } public static float DrowningShare(float level) { return 1f - 0.5f * SkillCurve.Factor(level); } public static float PickupReach(float range, float level) { if (!(range > 0f)) { return range; } return range * (1f + SkillCurve.Factor(level)); } } public sealed class BreathlessSpell { private object? actor; private float since = float.NegativeInfinity; private float last = float.NegativeInfinity; public void Sync(object? current, bool active) { if (!active || actor != current) { actor = (active ? current : null); since = (last = float.NegativeInfinity); } } public bool Hold(float now, float level, bool hasStamina) { if (actor == null || float.IsNaN(now) || float.IsInfinity(now)) { return false; } if (hasStamina) { since = (last = float.NegativeInfinity); return false; } if (now - last > 1f) { since = now; } last = now; return now - since < SwimmingRules.BreathDelay(level); } } public static class RidingRules { private static float Factor(float value) { if (!float.IsNaN(value) && !float.IsInfinity(value)) { return Math.Max(0f, Math.Min(1f, value)); } return 0f; } public static float RunMultiplier(float riderFactor) { return 1f + 0.15f * Factor(riderFactor); } public static float DamageShare(float riderFactor) { return 1f - 0.25f * Factor(riderFactor); } } } namespace ExtraPotions.Saga.Progression { public readonly struct DeathPlan { public bool Skip { get; } public float LowerFactor { get; } public bool KeepProgress { get; } public DeathPlan(bool skip, float lowerFactor, bool keepProgress) { Skip = skip; LowerFactor = lowerFactor; KeepProgress = keepProgress; } } public static class DeathPenalty { public static DeathPlan Plan(float lossPercent, bool loseProgress) { float num = (float.IsNaN(lossPercent) ? 0f : (Math.Min(100f, Math.Max(0f, lossPercent)) / 100f)); bool flag = !loseProgress; return new DeathPlan(num <= 0f && flag, num, flag); } public static (float Level, float Accumulator) Apply(float level, float accumulator, DeathPlan plan, float worldReductionRate) { if (plan.Skip) { return (Level: level, Accumulator: accumulator); } float num = plan.LowerFactor * Math.Max(0f, worldReductionRate); return (Level: level - level * num, Accumulator: plan.KeepProgress ? accumulator : 0f); } } public enum LineKind { Heading, Perk, Locked, Note, Breakdown } public sealed class PageLine { public string Key { get; } public LineKind Kind { get; } public int UnlockLevel { get; } public IReadOnlyList<string> Args { get; } internal PageLine(string key, LineKind kind, int unlockLevel, params string[] args) { Key = key; Kind = kind; UnlockLevel = unlockLevel; Args = Array.AsReadOnly(args); } public override string ToString() { return Kind.ToString() + " " + Key + "(" + string.Join(", ", Args) + ")"; } } public readonly struct LevelBonus { public string Source { get; } public float Levels { get; } public LevelBonus(string source, float levels) { Source = source ?? ""; Levels = levels; } } public sealed class BookValues { public DefenseValues Defense = DefenseValues.Defaults(); public float SneakSpeedAt100; public float SneakVisibilityFactor; public float StaminaRefundAt100; public float AxeWearAt100; public float CleanSplitAt100; public float ReplantingAt100; public float WoodFindAt0; public float WoodFindAt100; public float SeamChanceAt0; public float SeamChanceAt100; public float SeamWindowAt0; public float SeamWindowAt100; public float ExtraOreAt100; public float PickaxeWearAt100; public float MineFindAt0; public float MineFindAt100; public int ReadTheRockLevel; public int EchoLevel; public int UnbrokenLevel; public float ExtraYieldAt100; public float SweepRadiusAt100; public int SweepLevel; public float StrikeWindowAt0; public float StrikeWindowAt100; public float BigOneAt100; public float BonusItemAt100; public float BaitSaverAt100; public float CastDistanceAt100; public int SpeciesSenseLevel; public int SizeSenseLevel; public int WaterSenseLevel; public int GraceLevel; public int LegendaryLevel; public int DoubleBonusLevel; public static BookValues Defaults() { return new BookValues { SneakSpeedAt100 = SagaSettings.Sneak.SpeedAt100.Default, SneakVisibilityFactor = SagaSettings.Sneak.VisibilitySkillFactor.Default, StaminaRefundAt100 = SagaSettings.Woodcutting.StaminaRefundAt100.Default, AxeWearAt100 = SagaSettings.Woodcutting.AxeWearReductionAt100.Default, CleanSplitAt100 = SagaSettings.Woodcutting.CleanSplitChanceAt100.Default, ReplantingAt100 = SagaSettings.Woodcutting.ReplantingAt100.Default, WoodFindAt0 = SagaSettings.Woodcutting.FindChanceAt0.Default, WoodFindAt100 = SagaSettings.Woodcutting.FindChanceAt100.Default, SeamChanceAt0 = SagaSettings.Mining.SeamChanceAt0.Default, SeamChanceAt100 = SagaSettings.Mining.SeamChanceAt100.Default, SeamWindowAt0 = SagaSettings.Mining.SeamWindowAt0.Default, SeamWindowAt100 = SagaSettings.Mining.SeamWindowAt100.Default, ExtraOreAt100 = SagaSettings.Mining.ExtraOreChanceAt100.Default, PickaxeWearAt100 = SagaSettings.Mining.PickaxeWearReductionAt100.Default, MineFindAt0 = SagaSettings.Mining.FindChanceAt0.Default, MineFindAt100 = SagaSettings.Mining.FindChanceAt100.Default, ReadTheRockLevel = SagaSettings.Mining.ReadTheRockLevel.Default, EchoLevel = SagaSettings.Mining.EchoLevel.Default, UnbrokenLevel = SagaSettings.Mining.UnbrokenLevel.Default, ExtraYieldAt100 = SagaSettings.Foraging.ExtraYieldChanceAt100.Default, SweepRadiusAt100 = SagaSettings.Foraging.SweepRadiusAt100.Default, SweepLevel = SagaSettings.Foraging.SweepLevel.Default, StrikeWindowAt0 = SagaSettings.Fishing.StrikeWindowAt0.Default, StrikeWindowAt100 = SagaSettings.Fishing.StrikeWindowAt100.Default, BigOneAt100 = SagaSettings.Fishing.BigOneChanceAt100.Default, BonusItemAt100 = SagaSettings.Fishing.BonusItemChanceAt100.Default, BaitSaverAt100 = SagaSettings.Fishing.BaitSaverAt100.Default, CastDistanceAt100 = SagaSettings.Fishing.CastDistanceAt100.Default, SpeciesSenseLevel = SagaSettings.Bites.SpeciesSenseLevel.Default, SizeSenseLevel = SagaSettings.Bites.SizeSenseLevel.Default, WaterSenseLevel = SagaSettings.Bites.WaterSenseLevel.Default, GraceLevel = SagaSettings.Fishing.GraceLevel.Default, LegendaryLevel = SagaSettings.Fishing.LegendaryLevel.Default, DoubleBonusLevel = SagaSettings.Fishing.DoubleBonusLevel.Default }; } } public static class SkillBook { private sealed class Builder { private readonly List<PageLine> lines = new List<PageLine>(); private readonly float level; private bool milestones; public Builder(float level) { this.level = level; lines.Add(new PageLine("page_perks", LineKind.Heading, 0)); } public Builder Perk(string key, float value) { lines.Add(new PageLine(key, LineKind.Perk, 0, F(value))); return this; } public Builder Percent(string key, float percentAt100) { return Perk(key, SkillCurve.Share(percentAt100, level) * 100f); } public Builder Milestone(string key, int unlock) { if ((float)unlock > 100f) { return this; } if (!milestones) { milestones = true; lines.Add(new PageLine("page_milestones", LineKind.Heading, 0)); } lines.Add(new PageLine(key, SkillCurve.Reached(level, unlock) ? LineKind.Perk : LineKind.Locked, unlock, unlock.ToString(CultureInfo.InvariantCulture))); return this; } public IReadOnlyList<PageLine> Note(string key) { lines.Add(new PageLine(key, LineKind.Note, 0)); return lines.AsReadOnly(); } } public const string HeadingPerks = "page_perks"; public const string HeadingMilestones = "page_milestones"; public const string BreakdownBase = "breakdown_base"; public const string BreakdownBonus = "breakdown_bonus"; public const string BreakdownEffective = "breakdown_effective"; public static readonly string[] PerkKeys = new string[27] { "perk_max_health", "perk_food_health", "perk_damage_reduction", "perk_regeneration", "perk_poise", "perk_parry_window", "perk_block_stamina", "perk_dodge_stamina", "perk_sneak_speed", "perk_sneak_visibility", "perk_stamina_refund", "perk_axe_wear", "perk_clean_split", "perk_replanting", "perk_wood_find", "perk_seam_chance", "perk_seam_window", "perk_extra_ore", "perk_pickaxe_wear", "perk_mine_find", "perk_extra_yield", "perk_sweep_radius", "perk_strike_window", "perk_big_one", "perk_bonus_item", "perk_bait_saver", "perk_cast_distance" }; public static readonly string[] MilestoneKeys = new string[14] { "milestone_riposte", "milestone_shield_wall", "milestone_hardened", "milestone_last_stand", "milestone_read_the_rock", "milestone_echo", "milestone_unbroken", "milestone_sweep", "milestone_species_sense", "milestone_size_sense", "milestone_water_sense", "milestone_grace", "milestone_legendary", "milestone_double_bonus" }; public static readonly string[] NoteKeys = new string[6] { "note_defense_experience", "note_sneak_experience", "note_wood_experience", "note_mine_experience", "note_forage_experience", "note_fish_experience" }; private static string F(float value) { return value.ToString("0.##", CultureInfo.InvariantCulture); } public static IReadOnlyList<PageLine> Page(SagaSkill skill, float level, BookValues v) { if (v == null) { throw new ArgumentNullException("v"); } Builder builder = new Builder(level); DefenseValues defense = v.Defense; return skill switch { SagaSkill.Defense => builder.Perk("perk_max_health", DefenseRules.BonusHealth(defense, level)).Percent("perk_food_health", defense.FoodHealthAt100).Percent("perk_damage_reduction", defense.DamageReductionAt100) .Percent("perk_regeneration", defense.RegenerationAt100) .Percent("perk_poise", defense.PoiseAt100) .Perk("perk_parry_window", DefenseRules.ParryWindow(defense, level)) .Percent("perk_block_stamina", defense.BlockStaminaReductionAt100) .Percent("perk_dodge_stamina", defense.DodgeStaminaReductionAt100) .Milestone("milestone_riposte", defense.RiposteLevel) .Milestone("milestone_shield_wall", defense.ShieldWallLevel) .Milestone("milestone_hardened", defense.HardenedLevel) .Milestone("milestone_last_stand", defense.LastStandLevel) .Note("note_defense_experience"), SagaSkill.Sneak => builder.Perk("perk_sneak_speed", SkillCurve.Lerp(1f, v.SneakSpeedAt100, level)).Perk("perk_sneak_visibility", v.SneakVisibilityFactor).Note("note_sneak_experience"), SagaSkill.WoodCutting => builder.Percent("perk_stamina_refund", v.StaminaRefundAt100).Percent("perk_axe_wear", v.AxeWearAt100).Percent("perk_clean_split", v.CleanSplitAt100) .Percent("perk_replanting", v.ReplantingAt100) .Perk("perk_wood_find", SkillCurve.Lerp(v.WoodFindAt0, v.WoodFindAt100, level)) .Note("note_wood_experience"), SagaSkill.Pickaxes => builder.Perk("perk_seam_chance", SkillCurve.Lerp(v.SeamChanceAt0, v.SeamChanceAt100, level)).Perk("perk_seam_window", SkillCurve.Lerp(v.SeamWindowAt0, v.SeamWindowAt100, level)).Percent("perk_extra_ore", v.ExtraOreAt100) .Percent("perk_pickaxe_wear", v.PickaxeWearAt100) .Perk("perk_mine_find", SkillCurve.Lerp(v.MineFindAt0, v.MineFindAt100, level)) .Milestone("milestone_read_the_rock", v.ReadTheRockLevel) .Milestone("milestone_echo", v.EchoLevel) .Milestone("milestone_unbroken", v.UnbrokenLevel) .Note("note_mine_experience"), SagaSkill.Foraging => builder.Percent("perk_extra_yield", v.ExtraYieldAt100).Perk("perk_sweep_radius", v.SweepRadiusAt100 * SkillCurve.Factor(level)).Milestone("milestone_sweep", v.SweepLevel) .Note("note_forage_experience"), SagaSkill.Fishing => builder.Perk("perk_strike_window", SkillCurve.Lerp(v.StrikeWindowAt0, v.StrikeWindowAt100, level)).Percent("perk_big_one", v.BigOneAt100).Percent("perk_bonus_item", v.BonusItemAt100) .Percent("perk_bait_saver", v.BaitSaverAt100) .Percent("perk_cast_distance", v.CastDistanceAt100) .Milestone("milestone_species_sense", v.SpeciesSenseLevel) .Milestone("milestone_size_sense", v.SizeSenseLevel) .Milestone("milestone_water_sense", v.WaterSenseLevel) .Milestone("milestone_grace", v.GraceLevel) .Milestone("milestone_legendary", v.LegendaryLevel) .Milestone("milestone_double_bonus", v.DoubleBonusLevel) .Note("note_fish_experience"), _ => throw new ArgumentException("Unknown skill.", "skill"), }; } public static IReadOnlyList<PageLine> Breakdown(float baseLevel, IEnumerable<LevelBonus> bonuses) { List<PageLine> list = new List<PageLine>(); list.Add(new PageLine("breakdown_base", LineKind.Breakdown, 0, F(baseLevel))); List<PageLine> list2 = list; float num = baseLevel; foreach (LevelBonus item in bonuses ?? Array.Empty<LevelBonus>()) { if (item.Levels != 0f && !float.IsNaN(item.Levels)) { num += item.Levels; list2.Add(new PageLine("breakdown_bonus", LineKind.Breakdown, 0, item.Source, item.Levels.ToString("+0.##;-0.##", CultureInfo.InvariantCulture))); } } list2.Add(new PageLine("breakdown_effective", LineKind.Breakdown, 0, F(Math.Max(0f, num)))); return list2.AsReadOnly(); } } public static class SkillNotices { public static bool ShouldShow(SkillGainNotices mode, bool sagaSkill) { if (mode != SkillGainNotices.All) { return mode == SkillGainNotices.SagaSkills && sagaSkill; } return true; } } public static class SkillLossOverrides { public static IReadOnlyDictionary<string, float> Parse(string? text) { Dictionary<string, float> dictionary = new Dictionary<string, float>(StringComparer.Ordinal); foreach (var (name, value) in Entries(text)) { dictionary[Key(name)] = value; } return dictionary; } public static string Normalize(string? text) { List<string> list = new List<string>(); Dictionary<string, (string Name, float Percent)> latest = new Dictionary<string, (string, float)>(StringComparer.Ordinal); foreach (var item3 in Entries(text)) { string item = item3.Name; float item2 = item3.Percent; string text2 = Key(item); if (!latest.ContainsKey(text2)) { list.Add(text2); } latest[text2] = (item, item2); } return string.Join(", ", list.Select((string k) => latest[k].Name + "=" + latest[k].Percent.ToString("0.###", CultureInfo.InvariantCulture))); } public static float LossPercent(IReadOnlyDictionary<string, float> overrides, IEnumerable<string?> names, float globalPercent) { if (!TryFind(overrides, names, out var percent)) { return Clamp(globalPercent); } return percent; } public static bool Lists(IReadOnlyDictionary<string, float> overrides, IEnumerable<string?> names) { float percent; return TryFind(overrides, names, out percent); } private static bool TryFind(IReadOnlyDictionary<string, float> overrides, IEnumerable<string?> names, out float percent) { percent = 0f; if (overrides == null || names == null) { return false; } foreach (string name in names) { if (!string.IsNullOrWhiteSpace(name) && overrides.TryGetValue(Key(name), out percent)) { return true; } } return false; } public static float LevelAfterDeath(float level, float lossPercent, float worldRate) { float num = Clamp(lossPercent) / 100f * Math.Max(0f, worldRate); return Math.Max(0f, level - level * num); } private static IEnumerable<(string Name, float Percent)> Entries(string? text) { string[] array = (text ?? "").Split(',', ';', '\n'); foreach (string text2 in array) { int num = text2.IndexOfAny(new char[2] { '=', ':' }); if (num > 0) { string text3 = text2.Substring(0, num).Trim(); if (Key(text3).Length != 0 && float.TryParse(text2.Substring(num + 1).Trim().TrimEnd('%') .Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && !float.IsNaN(result) && !float.IsInfinity(result)) { yield return (Name: text3, Percent: Clamp(result)); } } } } private static string Key(string name) { return new string(name.Where((char c) => !char.IsWhiteSpace(c) && c != '_' && c != '-').Select(char.ToLowerInvariant).ToArray()); } private static float Clamp(float percent) { if (!float.IsNaN(percent)) { return Math.Min(100f, Math.Max(0f, percent)); } return 0f; } } public sealed class SkillRecovery { public const string Key = "saga.best_levels"; public const string ImportKey = "saga.imported.best_levels"; private readonly Dictionary<int, int> best = new Dictionary<int, int>(); public static SkillRecovery Load(IDictionary<string, string> data, IEnumerable<KeyValuePair<int, float>> current, IReadOnlyDictionary<int, int> aliases) { SkillRecovery skillRecovery = new SkillRecovery(); if (data.TryGetValue("saga.best_levels", out string value)) { skillRecovery.Merge(value); } if (!data.ContainsKey("saga.imported.best_levels") && data.TryGetValue("grindstone_best_levels", out string value2)) { skillRecovery.Merge(value2, aliases); } foreach (KeyValuePair<int, float> item in current) { skillRecovery.Note(item.Key, item.Value); } data["saga.best_levels"] = skillRecovery.Save(); data["saga.imported.best_levels"] = "1"; return skillRecovery; } public int Best(int skill) { if (!best.TryGetValue(skill, out var value)) { return 0; } return value; } public static int Base(float level) { if (!float.IsNaN(level) && !float.IsInfinity(level)) { return (int)Math.Max(0f, Math.Min(100f, level)); } return 0; } public bool Note(int skill, float level) { int num = Base(level); if (skill <= 0 || skill == 999 || num <= Best(skill)) { return false; } best[skill] = num; return true; } public void Forget(int skill) { best.Remove(skill); } public void ForgetAll() { best.Clear(); } public void Merge(string text, IReadOnlyDictionary<int, int>? aliases = null) { if (text.Length > 131072) { throw new FormatException("Recovery history exceeds 128 KiB."); } string[] array = text.Split(','); if (array.Length > 4096) { throw new FormatException("Recovery history exceeds 4096 entries."); } string[] array2 = array; for (int i = 0; i < array2.Length; i++) { string[] array3 = array2[i].Split(':'); if (array3.Length == 2 && int.TryParse(array3[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && int.TryParse(array3[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { if (aliases != null && aliases.TryGetValue(result, out var value)) { result = value; } Note(result, result2); } } } public string Save() { return string.Join(",", from p in best orderby p.Key select p.Key.ToString(CultureInfo.InvariantCulture) + ":" + p.Value.ToString(CultureInfo.InvariantCulture)); } public static bool IsWeapon(int skill) { if (skill < 1 || skill > 11 || skill == 6) { return skill == 14; } return true; } public float Rate(int skill, float storedLevel, int highestWeapon, bool catchUp, bool weaponSwitch) { int num = Base(storedLevel); if (!catchUp || num >= Best(skill)) { if (!weaponSwitch || !IsWeapon(skill) || num >= highestWeapon) { return 1f; } return 1.5f; } return 2f; } } public sealed class RecoveryProviders : IDisposable { private readonly Dictionary<int, IDisposable> owned = new Dictionary<int, IDisposable>(); public void Reconcile(IEnumerable<int> skills, Func<int, IDisposable> register) { HashSet<int> wanted = new HashSet<int>(skills.Where((int id) => id > 0 && id != 999)); int[] array = owned.Keys.Where((int id) => !wanted.Contains(id)).ToArray(); foreach (int key in array) { owned[key].Dispose(); owned.Remove(key); } foreach (int item in wanted) { if (!owned.ContainsKey(item)) { owned.Add(item, register(item)); } } } public void Dispose() { foreach (IDisposable value in owned.Values) { value.Dispose(); } owned.Clear(); } } } namespace ExtraPotions.Saga.Powers { public enum AttuneKind { Attune, Replace, Release } public readonly struct AttunePreview { public AttuneKind Kind { get; } public Boss? Replaced { get; } public AttunePreview(AttuneKind kind, Boss? replaced) { Kind = kind; Replaced = replaced; } } public sealed class Attunement { private sealed class ReadMemo { internal readonly string? Attuned; internal readonly string? Aside; internal readonly int Limit; internal readonly Attunement State; internal ReadMemo(string? attuned, string? aside, int limit, Attunement state) { Attuned = attuned; Aside = aside; Limit = limit; State = state; } } public const string AttunedKey = "saga.powers.attuned"; public const string AsideKey = "saga.powers.aside"; private static ReadMemo? lastRead; public static Attunement Empty { get; } = new Attunement(Array.Empty<Boss>(), Array.Empty<Boss>()); public IReadOnlyList<Boss> Attuned { get; } public IReadOnlyList<Boss> Aside { get; } public string AttunedText => Write(Attuned); public string AsideText => Write(Aside); private Attunement(IReadOnlyList<Boss> attuned, IReadOnlyList<Boss> aside) { Attuned = attuned; Aside = aside; } private static int Clamp(int limit) { return Math.Min(8, Math.Max(1, limit)); } public Attunement Limit(int limit) { List<Boss> list = Aside.Concat(Attuned).ToList(); int num = Math.Min(list.Count, Clamp(limit)); return new Attunement(list.Skip(list.Count - num).ToList(), list.Take(list.Count - num).ToList()); } public AttunePreview Preview(Boss stone, int limit) { Attunement attunement = Limit(limit); if (attunement.Attuned.Contains(stone)) { return new AttunePreview(AttuneKind.Release, null); } if (attunement.Attuned.Count < Clamp(limit)) { return new AttunePreview(AttuneKind.Attune, null); } return new AttunePreview(AttuneKind.Replace, attunement.Attuned[0]); } public Attunement Use(Boss stone, int limit) { List<Boss> list = Limit(limit).Attuned.ToList(); if (list.Remove(stone)) { return new Attunement(list, Array.Empty<Boss>()); } if (list.Count >= Clamp(limit)) { list.RemoveAt(0); } list.Add(stone); return new Attunement(list, Array.Empty<Boss>()); } private static string Write(IEnumerable<Boss> bosses) { return string.Join(",", bosses.Select(PowerCatalog.Name)); } public static Attunement ReadLimited(string? attuned, string? aside, int limit) { ReadMemo readMemo = lastRead; if (readMemo != null && readMemo.Limit == limit && string.Equals(readMemo.Attuned, attuned, StringComparison.Ordinal) && string.Equals(readMemo.Aside, aside, StringComparison.Ordinal)) { return readMemo.State; } Attunement attunement = Read(attuned, aside).Limit(limit); lastRead = new ReadMemo(attuned, aside, limit, attunement); return attunement; } public static Attunement Read(string? attuned, string? aside) { List<Boss> active = Parse(attuned); return new Attunement(active, (from b in Parse(aside) where !active.Contains(b) select b).ToList()); } private static List<Boss> Parse(string? text) { List<Boss> list = new List<Boss>(); string[] array = (text ?? "").Split(','); foreach (string text2 in array) { if (PowerCatalog.TryParse(text2, out var boss) && text2.Trim() == PowerCatalog.Name(boss) && !list.Contains(boss)) { list.Add(boss); } } return list; } } public sealed class KillCounts { public const string Key = "saga.powers.kills"; public const string SeededKey = "saga.powers.seeded"; public const string SeededKallKey = "saga.powers.seeded.kall"; private readonly Dictionary<Boss, int> counts; public static KillCounts Empty => new KillCounts(new Dictionary<Boss, int>()); public IReadOnlyDictionary<Boss, int> Values => counts; public string Text => string.Join(",", from p in counts where p.Value > 0 orderby p.Key select p.Key.ToString() + ":" + p.Value.ToString(CultureInfo.InvariantCulture)); private KillCounts(Dictionary<Boss, int> counts) { this.counts = counts; } public int Of(Boss boss) { if (!counts.TryGetValue(boss, out var value)) { return 0; } return value; } public KillCounts Add(Boss boss, int amount) { if (amount < 0) { throw new ArgumentOutOfRangeException("amount"); } return new KillCounts(new Dictionary<Boss, int>(counts) { [boss] = (int)Math.Min(2147483647L, (long)Of(boss) + (long)amount) }); } public KillCounts Reset(Boss boss) { Dictionary<Boss, int> dictionary = new Dictionary<Boss, int>(counts); dictionary.Remove(boss); return new KillCounts(dictionary); } public KillCounts ResetAll() { return Empty; } public static KillCounts Read(string? text) { Dictionary<Boss, int> dictionary = new Dictionary<Boss, int>(); string[] array = (text ?? "").Split(','); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Trim().Split(':'); if (array2.Length == 2 && PowerCatalog.TryParse(array2[0], out var boss) && int.TryParse(array2[1], NumberStyles.None, CultureInfo.InvariantCulture, out var result) && result >= 0) { dictionary[boss] = result; } } return new KillCounts(dictionary); } } public readonly struct CreditCandidate { public long Id { get; } public float X { get; } public float Y { get; } public float Z { get; } public bool AliveAndLoaded { get; } public CreditCandidate(long id, float x, float y, float z, bool aliveAndLoaded) { Id = id; X = x; Y = y; Z = z; AliveAndLoaded = aliveAndLoaded; } } public static class KillCredit { public static IReadOnlyList<long> Eligible(float x, float y, float z, float range, IEnumerable<CreditCandidate> players) { return (from p in players where p.AliveAndLoaded && (range == 0f || (range > 0f && ((double)p.X - (double)x) * (double)(p.X - x) + ((double)p.Y - (double)y) * (double)(p.Y - y) + ((double)p.Z - (double)z) * (double)(p.Z - z) <= (double)(range * range))) select p.Id).Distinct().ToArray(); } public static KillCounts Seed(KillCounts existing, IEnumerable<Boss> defeated) { KillCounts killCounts = existing; foreach (Boss item in defeated.Distinct()) { if (killCounts.Of(item) == 0) { killCounts = killCounts.Add(item, 1); } } return killCounts; } } public static class BossKillRules { public static bool ServerAccepts(bool senderOwnsBoss, bool knownPrefab, bool dead, bool first) { return senderOwnsBoss && knownPrefab && dead && first; } public static bool ClientAccepts(bool senderIsServer, bool knownBoss, bool inRange, bool first) { return senderIsServer && knownBoss && inRange && first; } } public enum Boss { Eikthyr, TheElder, Bonemass, Moder, Yagluth, Queen, Fader, Kall } public enum FeatureKind { Fraction, CarryWeight, Armor, SkillLevels, HealthTick, Resistance, Toggle } public enum Resistance { Normal, Resistant, Weak, Immune, Ignore, VeryResistant, VeryWeak, SlightlyResistant, SlightlyWeak } public enum PowerEffect { RunStaminaCost, JumpStaminaCost, SwimStaminaCost, DodgeStaminaCost, AttackStaminaCost, BlockStaminaCost, BlockStaminaFlat, SneakStaminaCost, ToolStaminaCost, MovementSpeed, SwimSpeed, JumpHeight, JumpDistance, MaxFallSpeed, WindMovement, WindRunStaminaCost, FallDamage, HealthRegen, StaminaRegen, EitrRegen, Armor, ArmorMultiplier, CarryWeight, Noise, Stealth, AdrenalineGain, StaggerTaken, TimedBlockBonus, ColdResistance, SailingPower, DoubleImpactDamage, TamingBoost, Resistance, DamageDealt, SkillLevel, SkillGain, AttackDamage, HealthTick } public readonly struct EffectValue : IEquatable<EffectValue> { public PowerEffect Effect { get; } public float Number { get; } public string Target { get; } public Resistance Modifier { get; } public bool Flag { get; } public EffectValue(PowerEffect effect, float number, string target = "", Resistance modifier = Resistance.Normal, bool flag = false) { Effect = effect; Number = number; Target = target ?? ""; Modifier = modifier; Flag = flag; } public bool Equals(EffectValue other) { if (Effect == other.Effect && Number.Equals(other.Number) && Target == other.Target && Modifier == other.Modifier) { return Flag == other.Flag; } return false; } public override bool Equals(object? obj) { if (obj is EffectValue other) { return Equals(other); } return false; } public override int GetHashCode() { return ((int)Effect * 397) ^ Number.GetHashCode() ^ Target.GetHashCode(); } public override string ToString() { return Effect.ToString() + ((Target.Length > 0) ? ("." + Target) : "") + "=" + ((Effect == PowerEffect.Resistance) ? Modifier.ToString() : ((Effect != PowerEffect.ColdResistance && Effect != PowerEffect.SailingPower && Effect != PowerEffect.DoubleImpactDamage && Effect != PowerEffect.TamingBoost) ? Number.ToString("R", CultureInfo.InvariantCulture) : (Flag ? "true" : "false"))); } } public sealed class PowerFeature { private readonly Func<object, EffectValue[]> effects; public Boss Power { get; } public int Index { get; } public string Name { get; } public FeatureKind Kind { get; } public object Default { get; } public string Key => "Feature " + Index + " - " + Name; internal PowerFeature(Boss power, int index, string name, FeatureKind kind, object defaultValue, Func<object, EffectValue[]> effects) { Power = power; Index = index; Name = name; Kind = kind; Default = defaultValue; this.effects = effects; } public IReadOnlyList<EffectValue> Effects(object? value) { object obj = ((value != null && value.GetType() == Default.GetType()) ? value : Default); if (obj is float num && (num == 0f || float.IsNaN(num) || float.IsInfinity(num))) { return Array.Empty<EffectValue>(); } if (obj is Resistance && (Resistance)obj == Resistance.Normal) { return Array.Empty<EffectValue>(); } if (obj is bool && !(bool)obj) { return Array.Empty<EffectValue>(); } return effects(obj); } public override string ToString() { return Power.ToString() + " " + Key; } } public static class PowerCatalog { private const FeatureKind F = FeatureKind.Fraction; private static readonly PowerFeature[] features = new PowerFeature[56] { Number(Boss.Eikthyr, 1, "Run stamina cost", FeatureKind.Fraction, -0.3f, PowerEffect.RunStaminaCost), Number(Boss.Eikthyr, 2, "Jump stamina cost", FeatureKind.Fraction, -0.3f, PowerEffect.JumpStaminaCost), Number(Boss.Eikthyr, 3, "Swim stamina cost", FeatureKind.Fraction, -0.3f, PowerEffect.SwimStaminaCost), Number(Boss.Eikthyr, 4, "Movement speed", FeatureKind.Fraction, 0.03f, PowerEffect.MovementSpeed), Number(Boss.Eikthyr, 5, "Stamina regeneration", FeatureKind.Fraction, 0.15f, PowerEffect.StaminaRegen), Number(Boss.Eikthyr, 6, "Fall damage", FeatureKind.Fraction, -0.35f, PowerEffect.FallDamage), Number(Boss.Eikthyr, 7, "Dodge stamina cost", FeatureKind.Fraction, -0.25f, PowerEffect.DodgeStaminaCost), Number(Boss.TheElder, 1, "Health regeneration", FeatureKind.Fraction, 0.15f, PowerEffect.HealthRegen), Number(Boss.TheElder, 2, "Chopping damage", FeatureKind.Fraction, 0.3f, PowerEffect.DamageDealt, "Chop"), Number(Boss.TheElder, 3, "Pickaxe damage", FeatureKind.Fraction, 0.3f, PowerEffect.DamageDealt, "Pickaxe"), Number(Boss.TheElder, 4, "Carry weight", FeatureKind.CarryWeight, 75f, PowerEffect.CarryWeight), Number(Boss.TheElder, 5, "Woodcutting skill", FeatureKind.SkillLevels, 12f, PowerEffect.SkillLevel, "WoodCutting"), Number(Boss.TheElder, 6, "Pickaxes skill", FeatureKind.SkillLevels, 12f, PowerEffect.SkillLevel, "Pickaxes"), Number(Boss.TheElder, 7, "Regeneration tick", FeatureKind.HealthTick, 1f, PowerEffect.HealthTick), Resist(Boss.Bonemass, 1, "Blunt", Resistance.SlightlyResistant), Resist(Boss.Bonemass, 2, "Slash", Resistance.SlightlyResistant), Resist(Boss.Bonemass, 3, "Pierce", Resistance.SlightlyResistant), Number(Boss.Bonemass, 4, "Block stamina cost", FeatureKind.Fraction, -0.5f, PowerEffect.BlockStaminaCost), Resist(Boss.Bonemass, 5, "Poison", Resistance.Resistant), Number(Boss.Bonemass, 6, "Armor", FeatureKind.Armor, 6f, PowerEffect.Armor), Number(Boss.Bonemass, 7, "Stagger taken", FeatureKind.Fraction, -0.25f, PowerEffect.StaggerTaken), Resist(Boss.Moder, 1, "Frost", Resistance.Resistant), Toggle(Boss.Moder, 2, "Cold immunity", PowerEffect.ColdResistance), Toggle(Boss.Moder, 3, "Sailing tailwind", PowerEffect.SailingPower), Number(Boss.Moder, 4, "Carry weight", FeatureKind.CarryWeight, 150f, PowerEffect.CarryWeight), Number(Boss.Moder, 5, "Movement speed", FeatureKind.Fraction, 0.05f, PowerEffect.MovementSpeed), new PowerFeature(Boss.Moder, 6, "Wind at your back", FeatureKind.Fraction, 0.25f, (object v) => new EffectValue[2] { new EffectValue(PowerEffect.WindMovement, (float)v), new EffectValue(PowerEffect.WindRunStaminaCost, 0f - (float)v) }), Number(Boss.Moder, 7, "Swim speed", FeatureKind.Fraction, 0.25f, PowerEffect.SwimSpeed), Resist(Boss.Yagluth, 1, "Lightning", Resistance.Resistant), Number(Boss.Yagluth, 2, "Melee damage", FeatureKind.Fraction, 0.05f, PowerEffect.DamageDealt, "Melee"), Number(Boss.Yagluth, 3, "Farming skill", FeatureKind.SkillLevels, 12f, PowerEffect.SkillLevel, "Farming"), Number(Boss.Yagluth, 4, "Elemental damage", FeatureKind.Fraction, 0.05f, PowerEffect.DamageDealt, "Elemental"), Number(Boss.Yagluth, 5, "Skill gain rate", FeatureKind.Fraction, 0.15f, PowerEffect.SkillGain, "All"), Number(Boss.Yagluth, 6, "Armor multiplier", FeatureKind.Fraction, 0.1f, PowerEffect.ArmorMultiplier), Number(Boss.Yagluth, 7, "All damage", FeatureKind.Fraction, 0.05f, PowerEffect.DamageDealt, "All"), Resist(Boss.Queen, 1, "Poison", Resistance.Resistant), Number(Boss.Queen, 2, "Sneak stamina cost", FeatureKind.Fraction, -0.5f, PowerEffect.SneakStaminaCost), Number(Boss.Queen, 3, "Eitr regeneration", FeatureKind.Fraction, 0.5f, PowerEffect.EitrRegen), Number(Boss.Queen, 4, "Stealth", FeatureKind.Fraction, 0.25f, PowerEffect.Stealth), Number(Boss.Queen, 5, "Noise", FeatureKind.Fraction, -0.25f, PowerEffect.Noise), Number(Boss.Queen, 6, "Elemental Magic skill", FeatureKind.SkillLevels, 12f, PowerEffect.SkillLevel, "ElementalMagic"), Number(Boss.Queen, 7, "Blood Magic skill", FeatureKind.SkillLevels, 12f, PowerEffect.SkillLevel, "BloodMagic"), Resist(Boss.Fader, 1, "Fire", Resistance.Resistant), Number(Boss.Fader, 2, "Adrenaline gain", FeatureKind.Fraction, 0.5f, PowerEffect.AdrenalineGain), Number(Boss.Fader, 3, "Stagger taken", FeatureKind.Fraction, -0.25f, PowerEffect.StaggerTaken), Number(Boss.Fader, 4, "Attack stamina cost", FeatureKind.Fraction, -0.2f, PowerEffect.AttackStaminaCost), Number(Boss.Fader, 5, "Armor", FeatureKind.Armor, 8f, PowerEffect.Armor), Number(Boss.Fader, 6, "Timed block bonus", FeatureKind.Fraction, 0.5f, PowerEffect.TimedBlockBonus), Number(Boss.Fader, 7, "Health regeneration", FeatureKind.Fraction, 0.2f, PowerEffect.HealthRegen), Resist(Boss.Kall, 1, "Frost", Resistance.VeryResistant), Number(Boss.Kall, 2, "Frost damage", FeatureKind.Fraction, 0.15f, PowerEffect.DamageDealt, "Frost"), Number(Boss.Kall, 3, "Health regeneration", FeatureKind.Fraction, 0.15f, PowerEffect.HealthRegen), Number(Boss.Kall, 4, "Movement speed", FeatureKind.Fraction, 0.05f, PowerEffect.MovementSpeed), Number(Boss.Kall, 5, "Armor", FeatureKind.Armor, 10f, PowerEffect.Armor), Number(Boss.Kall, 6, "Stamina regeneration", FeatureKind.Fraction, 0.2f, PowerEffect.StaminaRegen), Number(Boss.Kall, 7, "All damage", FeatureKind.Fraction, 0.05f, PowerEffect.DamageDealt, "All") }; public static IReadOnlyList<PowerFeature> Features { get; } = Array.AsReadOnly(features); public static string Name(Boss boss) { return boss.ToString(); } public static string DisplayName(Boss boss) { return boss switch { Boss.Kall => "Kall Fimbulbringer", Boss.Queen => "The Queen", Boss.TheElder => "The Elder", _ => boss.ToString(), }; } public static string Prefab(Boss boss) { return boss switch { Boss.Eikthyr => "Eikthyr", Boss.TheElder => "gd_king", Boss.Bonemass => "Bonemass", Boss.Moder => "Dragon", Boss.Yagluth => "GoblinKing", Boss.Queen => "SeekerQueen", Boss.Fader => "Fader", Boss.Kall => "FrozenKing_p3", _ => throw new ArgumentException("Unknown boss.", "boss"), }; } public static bool TryParse(string? text, out Boss boss) { foreach (Boss value in Enum.GetValues(typeof(Boss))) { if (string.Equals(text?.Trim(), Name(value), StringComparison.OrdinalIgnoreCase) || string.Equals(text?.Trim(), Prefab(value), StringComparison.Ordinal)) { boss = value; return true; } } boss = Boss.Eikthyr; return false; } public static string Section(Boss boss) { return "4 - Power " + (int)(boss + 1) + " - " + DisplayName(boss); } private static PowerFeature Number(Boss p, int i, string name, FeatureKind kind, float value, PowerEffect effect, string target = "") { return new PowerFeature(p, i, name, kind, value, (object v) => new EffectValue[1] { new EffectValue(effect, (float)v, target) }); } private static PowerFeature Resist(Boss p, int i, string type, Resistance value) { return new PowerFeature(p, i, type + " resistance", FeatureKind.Resistance, value, (object v) => new EffectValue[1] { new EffectValue(PowerEffect.Resistance, 0f, type, (Resistance)v) }); } private static PowerFeature Toggle(Boss p, int i, string name, PowerEffect effect) { return new PowerFeature(p, i, name, FeatureKind.Toggle, true, (object v) => new EffectValue[1] { new EffectValue(effect, 0f, "", Resistance.Normal, flag: true) }); } public static IReadOnlyList<PowerFeature> For(Boss boss) { return features.Where((PowerFeature f) => f.Power == boss).ToList(); } public static (float Min, float Max)? Range(FeatureKind kind) { return kind switch { FeatureKind.Fraction => (-1f, 5f), FeatureKind.CarryWeight => (0f, 1000f), FeatureKind.Armor => (0f, 200f), FeatureKind.SkillLevels => (0f, 100f), FeatureKind.HealthTick => (-50f, 50f), _ => null, }; } } public readonly struct PowerCommand { public readonly long Player; public readonly string Boss; public readonly int Count; public readonly bool Reset; public PowerCommand(long player, string boss, int count, bool reset) { if (player == 0L || string.IsNullOrWhiteSpace(boss) || boss.Length > 64 || boss.IndexOfAny(new char[4] { '\r', '\n', ',', ':' }) >= 0 || count < 0 || count > 1000000 || (!reset && count == 0)) { throw new ArgumentException("Invalid power command"); } Player = player; Boss = boss; Count = count; Reset = reset; } public byte[] Encode() { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true); binaryWriter.Write((byte)1); binaryWriter.Write(Player); binaryWriter.Write(Boss); binaryWriter.Write(Count); binaryWriter.Write(Reset); binaryWriter.Flush(); return memoryStream.ToArray(); } public static PowerCommand Decode(byte[] bytes) { if (bytes == null || bytes.Length > 256) { throw new FormatException("Power command size"); } using MemoryStream memoryStream = new MemoryStream(bytes, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, Encoding.UTF8, leaveOpen: true); if (binaryReader.ReadByte() != 1) { throw new FormatException("Power command version"); } PowerCommand result = new PowerCommand(binaryReader.ReadInt64(), binaryReader.ReadString(), binaryReader.ReadInt32(), binaryReader.ReadBoolean()); if (memoryStream.Position != memoryStream.Length) { throw new FormatException("Trailing power command bytes"); } return result; } } public enum PowerRoute { Unavailable, ThisMachine, Owner } public static class PowerCommandRoute { public static PowerRoute Choose(bool avatarHere, bool avatarIsMine, bool zdoKnown) { if (!avatarHere) { if (!zdoKnown) { return PowerRoute.Unavailable; } return PowerRoute.Owner; } if (!avatarIsMine) { return PowerRoute.Owner; } return PowerRoute.ThisMachine; } } public static class PowerEffects { public static IReadOnlyList<EffectValue> FromFeatures(Boss boss, int mastery, Func<PowerFeature, object?> value) { return (from f in PowerCatalog.For(boss) where f.Index <= mastery select f).SelectMany((PowerFeature f) => f.Effects(value(f))).ToArray(); } public static int EffectiveMastery(int earned, int? forced, int levels) { return Math.Min(Math.Min(100, Math.Max(0, levels)), Math.Max(0, forced ?? earned)); } } public sealed class PowerDefinitions { private static readonly HashSet<string> Skills = new HashSet<string>("Swords Knives Clubs Polearms Spears Blocking Axes Bows ElementalMagic BloodMagic Unarmed Pickaxes WoodCutting Crossbows Jump Sneak Run Swim Fishing Cooking Farming Crafting Dodge Ride All".Split(' '), StringComparer.Ordinal); private static readonly HashSet<string> Damage = new HashSet<string>("Blunt Slash Pierce Chop Pickaxe Fire Frost Lightning Poison Spirit Pure Melee Elemental All".Split(' '), StringComparer.Ordinal); private readonly Dictionary<Boss, SortedDictionary<int, List<EffectValue>>> powers; private PowerDefinitions(Dictionary<Boss, SortedDictionary<int, List<EffectValue>>> powers) { this.powers = powers; } public bool Lists(Boss boss) { return powers.ContainsKey(boss); } public int LevelCount(Boss boss) { if (!powers.TryGetValue(boss, out SortedDictionary<int, List<EffectValue>> value) || value.Count <= 0) { return 0; } return value.Keys.Max(); } public IReadOnlyList<EffectValue> Effects(Boss boss, int mastery) { if (!powers.TryGetValue(boss, out SortedDictionary<int, List<EffectValue>> value)) { return Array.Empty<EffectValue>(); } return value.Where((KeyValuePair<int, List<EffectValue>> p) => p.Key <= mastery).SelectMany((KeyValuePair<int, List<EffectValue>> p) => p.Value).ToArray(); } public static PowerDefinitions Parse(string? text, out List<string> errors) { errors = new List<string>(); YamlNode yamlNode = MiniYaml.Parse(text, errors); Dictionary<Boss, SortedDictionary<int, List<EffectValue>>> dictionary = new Dictionary<Boss, SortedDictionary<int, List<EffectValue>>>(); foreach (YamlEntry entry in yamlNode.Entries) { if (!PowerCatalog.TryParse(entry.Key, out var boss) || entry.Key != PowerCatalog.Name(boss)) { errors.Add("Unknown power: " + entry.Key); continue; } SortedDictionary<int, List<EffectValue>> sortedDictionary = (dictionary[boss] = new SortedDictionary<int, List<EffectValue>>()); Dictionary<PowerEffect, HashSet<string>> dictionary2 = new Dictionary<PowerEffect, HashSet<string>>(); foreach (YamlEntry entry2 in entry.Value.Entries) { if (!int.TryParse(entry2.Key, out var result) || result < 1 || result > 100) { errors.Add("Invalid level: " + entry2.Key); continue; } List<EffectValue> list = (sortedDictionary[result] = new List<EffectValue>()); foreach (YamlEntry entry3 in entry2.Value.Entries) { if (!Enum.TryParse<PowerEffect>(entry3.Key, out var result2) || !Enum.IsDefined(typeof(PowerEffect), result2)) { errors.Add("Unknown effect: " + entry3.Key); continue; } switch (result2) { case PowerEffect.HealthTick: { bool flag = false; foreach (YamlEntry entry4 in entry3.Value.Entries) { float number2; if (entry4.Key != "Health" && entry4.Key != "Interval" && entry4.Key != "MinHealth") { errors.Add("Invalid health tick field: " + entry4.Key); } else if (!Number(entry4.Value.Scalar, out number2) || (entry4.Key == "Interval" && number2 <= 0f) || (entry4.Key == "MinHealth" && (number2 < 0f || number2 > 1f))) { errors.Add("Invalid health tick value: " + entry4.Key); } else if (entry4.Key == "Health") { flag = true; if (number2 != 0f) { list.Add(new EffectValue(result2, number2)); } } else if ((entry4.Key == "Interval") ? (number2 != 10f) : (number2 != 0f)) { list.Add(new EffectValue(result2, number2, entry4.Key)); } } if (!flag) { errors.Add("HealthTick requires Health"); } break; } case PowerEffect.Resistance: case PowerEffect.DamageDealt: case PowerEffect.SkillLevel: case PowerEffect.SkillGain: case PowerEffect.AttackDamage: foreach (YamlEntry entry5 in entry3.Value.Entries) { bool flag2 = result2 == PowerEffect.SkillLevel || result2 == PowerEffect.SkillGain || result2 == PowerEffect.AttackDamage; float number3; if (!(flag2 ? Skills : Damage).Contains(entry5.Key) || (result2 == PowerEffect.Resistance && entry5.Key == "Pure")) { errors.Add("Invalid target: " + entry5.Key); } else if (result2 == PowerEffect.Resistance) { if (!Enum.TryParse<Resistance>(entry5.Value.Scalar, out var result4) || !Enum.IsDefined(typeof(Resistance), result4)) { errors.Add("Invalid resistance: " + entry5.Value.Scalar); } else if (result4 != Resistance.Normal) { list.Add(new EffectValue(result2, 0f, entry5.Key, result4)); } } else if (!Number(entry5.Value.Scalar, out number3)) { errors.Add("Invalid number: " + entry5.Value.Scalar); } else { if (number3 == 0f) { continue; } if (flag2) { if (!dictionary2.TryGetValue(result2, out var value)) { value = (dictionary2[result2] = new HashSet<string>()); } if (!value.Contains(entry5.Key) && value.Count >= ((result2 != PowerEffect.SkillLevel) ? 1 : 2)) { errors.Add("Too many skill targets for " + result2); continue; } value.Add(entry5.Key); } list.Add(new EffectValue(result2, number3, entry5.Key)); } } break; case PowerEffect.ColdResistance: case PowerEffect.SailingPower: case PowerEffect.DoubleImpactDamage: case PowerEffect.TamingBoost: { if (!bool.TryParse(entry3.Value.Scalar, out var result3)) { errors.Add("Invalid toggle: " + entry3.Key); } else if (result3) { list.Add(new EffectValue(result2, 0f, "", Resistance.Normal, flag: true)); } break; } default: { if (!Number(entry3.Value.Scalar, out var number)) { errors.Add("Invalid number: " + entry3.Key); } else if (number != 0f) { list.Add(new EffectValue(result2, number)); } break; } } } } } return new PowerDefinitions(dictionary); } private static bool Number(string? text, out float number) { if (float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out number) && !float.IsNaN(number)) { return !float.IsInfinity(number); } return false; } } public static class PowerRecharge { private static float Valid(float seconds) { if (!float.IsNaN(seconds) && !float.IsInfinity(seconds) && !(seconds < 0f)) { return seconds; } return 0f; } public static float Shared(float first, float next) { return Math.Max(Valid(first), Valid(next)); } public static string TimeLeft(float seconds) { double num = Math.Ceiling(Valid(seconds)); return Math.Floor(num / 60.0).ToString("0", CultureInfo.InvariantCulture) + ":" + (num % 60.0).ToString("00", CultureInfo.InvariantCulture); } } public sealed class PowerRequirements { public const int MaxLevels = 100; public const int DefaultLevels = 7; private readonly Dictionary<Boss, SortedDictionary<int, List<(Boss Boss, int Count)>>> table; private readonly Dictionary<Boss, SortedDictionary<int, List<(string Name, int Count)>>> additional = new Dictionary<Boss, SortedDictionary<int, List<(string, int)>>>(); private static readonly Boss[] LaterPowers = new Boss[1] { Boss.Kall }; public static PowerRequirements Defaults { get; } = Build(new Dictionary<Boss, (Boss, int)[]> { [Boss.Eikthyr] = new(Boss, int)[7] { (Boss.Eikthyr, 1), (Boss.TheElder, 1), (Boss.Bonemass, 1), (Boss.Moder, 1), (Boss.Yagluth, 1), (Boss.Queen, 1), (Boss.Fader, 1) }, [Boss.TheElder] = new(Boss, int)[7] { (Boss.TheElder, 1), (Boss.Bonemass, 1), (Boss.Moder, 1), (Boss.Yagluth, 1), (Boss.Queen, 1), (Boss.Fader, 1), (Boss.TheElder, 2) }, [Boss.Bonemass] = new(Boss, int)[7] { (Boss.Bonemass, 1), (Boss.Moder, 1), (Boss.Yagluth, 1), (Boss.Queen, 1), (Boss.Fader, 1), (Boss.Bonemass, 2), (Boss.Fader, 2) }, [Boss.Moder] = new(Boss, int)[7] { (Boss.Moder, 1), (Boss.Yagluth, 1), (Boss.Queen, 1), (Boss.Fader, 1), (Boss.Moder, 2), (Boss.Fader, 2), (Boss.Moder, 3) }, [Boss.Yagluth] = new(Boss, int)[7] { (Boss.Yagluth, 1), (Boss.Queen, 1), (Boss.Fader, 1), (Boss.Yagluth, 2), (Boss.Fader, 2), (Boss.Yagluth, 3), (Boss.Fader, 3) }, [Boss.Queen] = new(Boss, int)[7] { (Boss.Queen, 1), (Boss.Fader, 1), (Boss.Queen, 2), (Boss.Fader, 2), (Boss.Queen, 3), (Boss.Fader, 3), (Boss.Queen, 4) }, [Boss.Fader] = new(Boss, int)[7] { (Boss.Fader, 1), (Boss.Fader, 2), (Boss.Fader, 3), (Boss.Fader, 4), (Boss.Fader, 5), (Boss.Fader, 6), (Boss.Fader, 7) }, [Boss.Kall] = new(Boss, int)[7] { (Boss.Kall, 1), (Boss.Kall, 2), (Boss.Kall, 3), (Boss.Kall, 4), (Boss.Kall, 5), (Boss.Kall, 6), (Boss.Kall, 7) } }); private PowerRequirements(Dictionary<Boss, SortedDictionary<int, List<(Boss, int)>>> table) { this.table = table; } private static PowerRequirements Build(Dictionary<Boss, (Boss, int)[]> levels) { Dictionary<Boss, SortedDictionary<int, List<(Boss, int)>>> dictionary = new Dictionary<Boss, SortedDictionary<int, List<(Boss, int)>>>(); foreach (KeyValuePair<Boss, (Boss, int)[]> level in levels) { SortedDictionary<int, List<(Boss, int)>> sortedDictionary = new SortedDictionary<int, List<(Boss, int)>>(); for (int i = 0; i < level.Value.Length; i++) { sortedDictionary[i + 1] = new List<(Boss, int)> { level.Value[i] }; } dictionary[level.Key] = sortedDictionary; } return new PowerRequirements(dictionary); } public static PowerRequirements Parse(string? text, out List<string> errors) { return Parse(text, Array.Empty<string>(), out errors); } public static PowerRequirements Parse(string? text, IEnumerable<string> registeredBosses, out List<string> errors) { errors = new List<string>(); YamlNode yamlNode = MiniYaml.Parse(text, errors); Dictionary<Boss, SortedDictionary<int, List<(Boss, int)>>> dictionary = new Dictionary<Boss, SortedDictionary<int, List<(Boss, int)>>>(); PowerRequirements powerRequirements = new PowerRequirements(dictionary); HashSet<string> hashSet = new HashSet<string>(registeredBosses ?? Array.Empty<string>(), StringComparer.Ordinal); foreach (YamlEntry entry in yamlNode.Entries) { if (!PowerCatalog.TryParse(entry.Key, out var boss) || entry.Key != PowerCatalog.Name(boss)) { errors.Add("line " + entry.Line + ": unknown power '" + entry.Key + "'"); continue; } if (!dictionary.TryGetValue(boss, out var value)) { value = (dictionary[boss] = new SortedDictionary<int, List<(Boss, int)>>()); } foreach (YamlEntry entry2 in entry.Value.Entries) { if (!int.TryParse(entry2.Key, NumberStyles.None, CultureInfo.InvariantCulture, out var result) || result < 1 || result > 100) { errors.Add("line " + entry2.Line + ": " + boss.ToString() + " level must be 1 to " + 100 + ", got '" + entry2.Key + "'"); continue; } List<(Boss, int)> list = new List<(Boss, int)>(); foreach (YamlEntry entry3 in entry2.Value.Entries) { int result3; if (!PowerCatalog.TryParse(entry3.Key, out var boss2) || entry3.Key != PowerCatalog.Name(boss2)) { if (hashSet.Contains(entry3.Key) && int.TryParse(entry3.Value.Scalar, NumberStyles.None, CultureInfo.InvariantCulture, out var result2) && result2 >= 0) { if (!powerRequirements.additional.TryGetValue(boss, out SortedDictionary<int, List<(string, int)>> value2)) { value2 = (powerRequirements.additional[boss] = new SortedDictionary<int, List<(string, int)>>()); } if (!value2.TryGetValue(result, out var value3)) { value3 = (value2[result] = new List<(string, int)>()); } value3.Add((entry3.Key, result2)); } else { errors.Add("line " + entry2.Line + ": " + boss.ToString() + " level " + result + ": unknown boss '" + entry3.Key + "'"); } } else if (!int.TryParse(entry3.Value.Scalar, NumberStyles.None, CultureInfo.InvariantCulture, out result3) || result3 < 0) { errors.Add("line " + entry2.Line + ": " + boss.ToString() + " level " + result + ": " + boss2.ToString() + " needs a whole number of kills"); } else { list.Add((boss2, result3)); } } value[result] = list; } } Boss[] laterPowers = LaterPowers; foreach (Boss key in laterPowers) { if (!dictionary.ContainsKey(key)) { dictionary[key] = new SortedDictionary<int, List<(Boss, int)>>(Defaults.table[key].ToDictionary<KeyValuePair<int, List<(Boss, int)>>, int, List<(Boss, int)>>((KeyValuePair<int, List<(Boss Boss, int Count)>> p) => p.Key, (KeyValuePair<int, List<(Boss Boss, int Count)>> p) => p.Value.ToList())); } } return powerRequirements; } private IReadOnlyList<(Boss Boss, int Count)> Level(Boss power, int level) { if (!table.TryGetValue(power, out SortedDictionary<int, List<(Boss, int)>> value) || !value.TryGetValue(level, out var value2)) { return Array.Empty<(Boss, int)>(); } return value2; } public string Describe(Boss power, int level) { IEnumerable<string> first = from n in Level(power, level) select PowerCatalog.Name(n.Boss) + " " + n.Count.ToString(CultureInfo.InvariantCulture); IEnumerable<string> second; if (!additional.TryGetValue(power, out SortedDictionary<int, List<(string, int)>> value) || !value.TryGetValue(level, out var value2)) { IEnumerable<string> enumerable = Array.Empty<string>(); second = enumerable; } else { second = value2.Select<(string, int), string>(((string Name, int Count) n) => n.Name + " " + n.Count.ToString(CultureInfo.InvariantCulture)); } return string.Join(", ", first.Concat(second)); } public IReadOnlyDictionary<string, int> NeededNamed(Boss power, int level, IReadOnlyDictionary<string, int> kills) { Dictionary<string, int> dictionary = new Dictionary<string, int>(); for (int i = 1; i <= Math.Min(100, level); i++) { foreach (var item in Level(power, i)) { string key = PowerCatalog.Name(item.Boss); dictionary[key] = Math.Max(dictionary.TryGetValue(key, out var value) ? value : 0, item.Count); } if (!additional.TryGetValue(power, out SortedDictionary<int, List<(string, int)>> value2) || !value2.TryGetValue(i, out var value3)) { continue; } foreach (var item2 in value3) { dictionary[item2.Item1] = Math.Max(dictionary.TryGetValue(item2.Item1, out var value4) ? value4 : 0, item2.Item2); } } int value5; return (from p in dictionary select new KeyValuePair<string, int>(p.Key, p.Value - (kills.TryGetValue(p.Key, out value5) ? value5 : 0)) into p where p.Value > 0 select p).ToDictionary((KeyValuePair<string, int> p) => p.Key, (KeyValuePair<string, int> p) => p.Value); } private static int Have(IReadOnlyDictionary<Boss, int> kills, Boss boss) { if (kills == null || !kills.TryGetValue(boss, out var value)) { return 0; } return value; } public int Mastery(Boss power, IReadOnlyDictionary<Boss, int> kills, int levelCount) { int num = Math.Min(100, Math.Max(0, levelCount)); for (int i = 1; i <= num; i++) { if (Level(power, i).Any(((Boss Boss, int Count) n) => Have(kills, n.Boss) < n.Count)) { return i - 1; } } return num; } public int MasteryNamed(Boss power, IReadOnlyDictionary<string, int> kills, int levelCount) { Dictionary<Boss, int> dictionary = new Dictionary<Boss, int>(); foreach (Boss value4 in Enum.GetValues(typeof(Boss))) { dictionary[value4] = (kills.TryGetValue(PowerCatalog.Name(value4), out var value) ? value : 0); } int num = Mastery(power, dictionary, levelCount); if (additional.TryGetValue(power, out SortedDictionary<int, List<(string, int)>> value2)) { foreach (KeyValuePair<int, List<(string, int)>> item in value2) { if (item.Key <= num && item.Value.Any<(string, int)>(((string Name, int Count) n) => !kills.TryGetValue(n.Name, out var value3) || value3 < n.Count)) { return item.Key - 1; } } } return num; } public IReadOnlyDictionary<Boss, int> Needed(Boss power, int level, IReadOnlyDictionary<Boss, int> kills) { Dictionary<Boss, int> dictionary = new Dictionary<Boss, int>(); for (int i = 1; i <= Math.Min(100, level); i++) { foreach (var item3 in Level(power, i)) { Boss item = item3.Boss; int item2 = item3.Count;
plugins/Saga/Saga.dll
Decompiled 10 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExtraPotions.Meadworks.Crucible; using ExtraPotions.Meadworks.Crucible.UI; using ExtraPotions.Saga.Config; using ExtraPotions.Saga.Conversion; using ExtraPotions.Saga.Integration; using ExtraPotions.Saga.Powers; using ExtraPotions.Saga.Progression; using ExtraPotions.Saga.Runtime.Adopted.Grindstone; using ExtraPotions.Saga.Skills; using HarmonyLib; using Jotunn.Utils; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.Rendering; using UnityEngine.UI; using YamlConfig; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.ObjectPool; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_guiutils")] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ExtraPotions")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (C) 2026 expDARE (ExtraPotions Meadworks)")] [assembly: AssemblyFileVersion("0.1.17.0")] [assembly: AssemblyInformationalVersion("0.1.17+fatal: not a git repository (or any of the parent directories): .git")] [assembly: AssemblyProduct("Saga")] [assembly: AssemblyTitle("Saga")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.17.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] 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; } } [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 YamlConfig { public sealed class YamlGroupTable { private readonly Dictionary<string, List<string>> members = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, List<string>> parents = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); public IReadOnlyCollection<string> Names => members.Keys; public bool IsGroup(string name) { return members.ContainsKey(name); } public void Add(string group, IEnumerable<string> names) { if (!members.TryGetValue(group, out List<string> value)) { value = new List<string>(); members.Add(group, value); } foreach (string name in names) { value.Add(name); if (!parents.TryGetValue(name, out List<string> value2)) { value2 = new List<string>(); parents.Add(name, value2); } value2.Add(group); } } public IReadOnlyList<string> Resolve(string name) { List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); Queue<string> queue = new Queue<string>(); queue.Enqueue(name); while (queue.Count > 0) { if (!parents.TryGetValue(queue.Dequeue(), out List<string> value)) { continue; } foreach (string item in value) { if (hashSet.Add(item)) { list.Add(item); queue.Enqueue(item); } } } return list; } public IReadOnlyList<string> Expand(string group) { List<string> result = new List<string>(); HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase); Expand(group, seen, result); return result; } private void Expand(string name, HashSet<string> seen, List<string> result) { if (!seen.Add(name)) { return; } if (!members.TryGetValue(name, out List<string> value)) { result.Add(name); return; } foreach (string item in value) { Expand(item, seen, result); } } } public abstract class YamlModel { private static readonly IDeserializer Parser = new DeserializerBuilder().Build(); private string? currentFile; public List<string> Errors { get; } = new List<string>(); public List<string> Warnings { get; } = new List<string>(); protected YamlGroupTable Groups { get; } = new YamlGroupTable(); public void Load(string yamlText) { ReadText(yamlText); Verify(); } public void LoadAll(IEnumerable<KeyValuePair<string, string>> files) { foreach (KeyValuePair<string, string> file in files) { currentFile = Path.GetFileName(file.Key); ReadText(file.Value); } currentFile = null; Verify(); } public IReadOnlyList<string> ResolveGroups(string name) { return Groups.Resolve(name); } protected abstract void Read(YamlNode root); protected virtual void Verify() { } protected void ReadGroups(YamlNode root, string key = "groups") { foreach (KeyValuePair<string, YamlNode> entry in root.Get(key).Entries) { if (entry.Value.TryStringList(out List<string> items)) { Groups.Add(entry.Key, items); } } } internal void AddError(string message) { Errors.Add(WithFile(message)); } internal void AddWarning(string message) { Warnings.Add(WithFile(message)); } private string WithFile(string message) { if (currentFile != null) { return currentFile + ": " + message; } return message; } private void ReadText(string yamlText) { if (TryParse(yamlText, out object graph) && graph != null) { if (!(graph is IDictionary<object, object>)) { AddError("the file must be a map of keys at the top level, not " + ((graph is IList<object>) ? "a list" : "a single value")); return; } YamlNode yamlNode = new YamlNode(this, graph, ""); Read(yamlNode); yamlNode.WarnUnknownKeys(); } } private bool TryParse(string yamlText, out object? graph) { try { graph = Parser.Deserialize<object>(yamlText.TrimStart('\ufeff')); return true; } catch (Exception ex) { AddError("invalid YAML: " + ex.Message + ((ex.InnerException == null) ? "" : (" (" + ex.InnerException.Message + ")"))); graph = null; return false; } } } public enum YamlNodeKind { Scalar, List, Map, Null, Missing } public sealed class YamlNode { private readonly YamlModel model; private readonly object? value; private List<KeyValuePair<string, YamlNode>>? mapEntries; private Dictionary<string, YamlNode>? mapLookup; private List<YamlNode>? listItems; private HashSet<string>? askedKeys; private bool visited; private bool shapeReported; public string Path { get; } public YamlNodeKind Kind { get; } public string? Text { get { if (Kind != YamlNodeKind.Scalar) { return null; } return Convert.ToString(value, CultureInfo.InvariantCulture); } } public int Count => Kind switch { YamlNodeKind.Map => MapEntries.Count, YamlNodeKind.List => ListItems.Count, _ => 0, }; public IReadOnlyList<KeyValuePair<string, YamlNode>> Entries { get { if (!ExpectShape(YamlNodeKind.Map, "a map")) { return Array.Empty<KeyValuePair<string, YamlNode>>(); } visited = true; AskedKeys.UnionWith(MapEntries.Select<KeyValuePair<string, YamlNode>, string>((KeyValuePair<string, YamlNode> e) => e.Key)); return MapEntries; } } public IReadOnlyList<YamlNode> Items { get { if (!ExpectShape(YamlNodeKind.List, "a list")) { return Array.Empty<YamlNode>(); } return ListItems; } } private HashSet<string> AskedKeys => askedKeys ?? (askedKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase)); private List<KeyValuePair<string, YamlNode>> MapEntries { get { if (mapEntries == null) { BuildMap(); } return mapEntries; } } private Dictionary<string, YamlNode> MapLookup { get { if (mapLookup == null) { BuildMap(); } return mapLookup; } } private List<YamlNode> ListItems { get { if (listItems == null) { IList<object> source = (IList<object>)value; listItems = source.Select((object item, int i) => new YamlNode(model, item, $"{Path}[{i}]")).ToList(); } return listItems; } } internal YamlNode(YamlModel model, object? value, string path, YamlNodeKind kind) { this.model = model; this.value = value; Path = path; Kind = kind; } internal YamlNode(YamlModel model, object? value, string path) : this(model, value, path, KindOf(value)) { } public YamlNode Get(string key) { string path = (string.IsNullOrEmpty(Path) ? key : (Path + "." + key)); if (!ExpectShape(YamlNodeKind.Map, "a map")) { return new YamlNode(model, null, path, YamlNodeKind.Missing); } visited = true; AskedKeys.Add(key); if (!MapLookup.TryGetValue(key, out YamlNode result)) { return new YamlNode(model, null, path, YamlNodeKind.Missing); } return result; } public bool Is(string keyword) { if (Kind == YamlNodeKind.Scalar) { return string.Equals(Text.Trim(), keyword, StringComparison.OrdinalIgnoreCase); } return false; } public void Error(string message) { model.AddError(Prefixed(message)); } public void Warn(string message) { model.AddWarning(Prefixed(message)); } public void WarnUnknownKeys() { if (Kind == YamlNodeKind.Map && visited) { List<string> list = (from e in MapEntries select e.Key into k where !AskedKeys.Contains(k) select k).ToList(); if (list.Count > 0) { Warn("unknown keys " + string.Join(", ", list)); } { foreach (KeyValuePair<string, YamlNode> mapEntry in MapEntries) { mapEntry.Value.WarnUnknownKeys(); } return; } } if (Kind == YamlNodeKind.List && listItems != null) { listItems.ForEach(delegate(YamlNode item) { item.WarnUnknownKeys(); }); } } private string Prefixed(string message) { if (!string.IsNullOrEmpty(Path)) { return Path + ": " + message; } return message; } private void BuildMap() { mapEntries = new List<KeyValuePair<string, YamlNode>>(); mapLookup = new Dictionary<string, YamlNode>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<object, object> item in (IDictionary<object, object>)value) { string text = Convert.ToString(item.Key, CultureInfo.InvariantCulture) ?? ""; string path = (string.IsNullOrEmpty(Path) ? text : (Path + "." + text)); YamlNode yamlNode = new YamlNode(model, item.Value, path); if (mapLookup.ContainsKey(text)) { yamlNode.Warn("duplicate key (differs only in case), ignored"); continue; } mapLookup.Add(text, yamlNode); mapEntries.Add(new KeyValuePair<string, YamlNode>(text, yamlNode)); } } private bool ExpectShape(YamlNodeKind wanted, string description) { if (Kind == wanted) { return true; } if (Kind != YamlNodeKind.Missing && !shapeReported) { shapeReported = true; Error((Kind == YamlNodeKind.Null) ? ("expected " + description + " but the key has no value") : ("expected " + description + ", found " + Describe(Kind))); } return false; } private static string Describe(YamlNodeKind kind) { return kind switch { YamlNodeKind.Scalar => "a single value", YamlNodeKind.List => "a list", YamlNodeKind.Map => "a map", _ => "nothing", }; } private static YamlNodeKind KindOf(object? value) { if (value != null) { if (!(value is IDictionary<object, object>)) { if (value is IList<object>) { return YamlNodeKind.List; } return YamlNodeKind.Scalar; } return YamlNodeKind.Map; } return YamlNodeKind.Null; } public bool TryInt(out int result) { return TryScalar<int>(out result); } public bool TryFloat(out float result) { return TryScalar<float>(out result); } public bool TryBool(out bool result) { return TryScalar<bool>(out result); } public bool TryString(out string result) { return TryScalar<string>(out result); } public bool TryEnum<T>(out T result) where T : struct, Enum { return TryScalar<T>(out result); } public bool TryList<T>(out List<T> items) { items = new List<T>(); if (!ExpectShape(YamlNodeKind.List, "a list of " + YamlScalarParser.Plural<T>())) { return false; } bool result = true; foreach (YamlNode listItem in ListItems) { if (listItem.TryScalar<T>(out var result2)) { items.Add(result2); } else { result = false; } } return result; } public bool TryStringList(out List<string> items) { if (Kind == YamlNodeKind.Scalar) { items = new List<string> { Text }; return true; } return TryList(out items); } private bool TryScalar<T>(out T result) { result = default(T); if (!ExpectShape(YamlNodeKind.Scalar, YamlScalarParser.Describe(typeof(T)))) { return false; } if (YamlScalarParser.TryParse(Text, typeof(T), out object result2)) { result = (T)result2; return true; } Error("'" + Text + "' is not " + YamlScalarParser.Describe(typeof(T))); return false; } } internal static class YamlScalarParser { private static readonly string[] TrueWords = new string[4] { "true", "yes", "on", "1" }; private static readonly string[] FalseWords = new string[4] { "false", "no", "off", "0" }; public static bool TryParse(string text, Type type, out object? result) { string text2 = text.Trim(); result = null; if (type == typeof(string)) { result = text; return true; } if (type == typeof(int)) { int result3; bool result2 = int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out result3); result = result3; return result2; } if (type == typeof(float)) { float result5; bool result4 = float.TryParse(text2, NumberStyles.Float, CultureInfo.InvariantCulture, out result5); result = result5; return result4; } if (!(type == typeof(bool))) { return TryParseEnum(text2, type, out result); } return TryParseBool(text2, out result); } public static string Describe(Type type) { if (type == typeof(int)) { return "an integer"; } if (type == typeof(float)) { return "a number"; } if (type == typeof(bool)) { return "true or false"; } if (!type.IsEnum) { return "text"; } return "one of " + string.Join(", ", Enum.GetNames(type)); } public static string Plural<T>() { if (!(typeof(T) == typeof(int))) { if (!(typeof(T) == typeof(float))) { if (!(typeof(T) == typeof(bool))) { if (!typeof(T).IsEnum) { return "text values"; } return "names (" + string.Join(", ", Enum.GetNames(typeof(T))) + ")"; } return "true/false values"; } return "numbers"; } return "integers"; } private static bool TryParseBool(string text, out object? result) { if (TrueWords.Contains<string>(text, StringComparer.OrdinalIgnoreCase)) { result = true; return true; } result = false; return FalseWords.Contains<string>(text, StringComparer.OrdinalIgnoreCase); } private static bool TryParseEnum(string text, Type type, out object? result) { if (!type.IsEnum) { throw new NotSupportedException("YamlNode cannot read values of type " + type.Name); } string text2 = Enum.GetNames(type).FirstOrDefault((string n) => string.Equals(n, text, StringComparison.OrdinalIgnoreCase)); result = ((text2 == null) ? null : Enum.Parse(type, text2)); return text2 != null; } } } namespace ExtraPotions.Saga.Runtime { internal static class PresetPersistence { internal static void Save(IReadOnlyList<ConfigEntryBase> entries) { //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Expected O, but got Unknown //IL_0228: Unknown result type (might be due to invalid IL or missing references) //IL_022f: Expected O, but got Unknown if (entries == null) { throw new ArgumentNullException("entries"); } if (entries.Count == 0) { throw new ArgumentException("A preset save needs at least one entry.", "entries"); } ConfigEntryBase obj = entries[0]; ConfigFile val = ((obj != null) ? obj.ConfigFile : null) ?? throw new ArgumentException("A preset entry is missing.", "entries"); HashSet<ConfigEntryBase> hashSet = new HashSet<ConfigEntryBase>(); List<(ConfigEntryBase, object, string)> list = new List<(ConfigEntryBase, object, string)>(); foreach (ConfigEntryBase entry in entries) { if (entry == null || entry.ConfigFile != val || !hashSet.Add(entry) || !val.ContainsKey(entry.Definition) || val[entry.Definition] != entry) { throw new ArgumentException("Preset entries must be distinct current entries from one configuration file.", "entries"); } list.Add((entry, entry.BoxedValue, TomlTypeConverter.ConvertToString(entry.BoxedValue, entry.SettingType))); } string fullPath = Path.GetFullPath(val.ConfigFilePath); string text = fullPath + ".preset-" + Guid.NewGuid().ToString("N") + ".tmp"; byte[] array = (File.Exists(fullPath) ? File.ReadAllBytes(fullPath) : null); bool flag = false; try { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { flag = true; if (array != null) { fileStream.Write(array, 0, array.Length); } } ConfigFile val2 = new ConfigFile(text, false) { SaveOnConfigSet = false }; MethodInfo methodInfo = typeof(ConfigFile).GetMethods(BindingFlags.Instance | BindingFlags.Public).Single((MethodInfo method) => method.Name == "Bind" && method.IsGenericMethodDefinition && method.GetParameters().Length == 3 && method.GetParameters()[0].ParameterType == typeof(ConfigDefinition)); List<(ConfigEntryBase, string)> list2 = new List<(ConfigEntryBase, string)>(); foreach (var item2 in list) { ConfigEntryBase item = item2.Item1; ConfigEntryBase val3 = (ConfigEntryBase)methodInfo.MakeGenericMethod(item.SettingType).Invoke(val2, new object[3] { item.Definition, item.DefaultValue, item.Description }); val3.BoxedValue = item2.Item2; if (TomlTypeConverter.ConvertToString(val3.BoxedValue, val3.SettingType) != item2.Item3) { throw new IOException("The configuration serializer changed a preset value."); } list2.Add((val3, item2.Item3)); } val2.Save(); using (FileStream fileStream2 = new FileStream(text, FileMode.Open, FileAccess.Write, FileShare.None)) { fileStream2.Flush(flushToDisk: true); } val2.Reload(); foreach (var item3 in list2) { if (TomlTypeConverter.ConvertToString(item3.Item1.BoxedValue, item3.Item1.SettingType) != item3.Item2) { throw new IOException("A saved preset value failed readback."); } } byte[] array2 = File.ReadAllBytes(text); bool num; if (array != null) { if (!File.Exists(fullPath)) { goto IL_0367; } num = !((ReadOnlySpan<byte>)array).SequenceEqual((ReadOnlySpan<byte>)File.ReadAllBytes(fullPath)); } else { num = File.Exists(fullPath); } if (!num) { if (array == null) { File.Move(text, fullPath); } else { File.Replace(text, fullPath, null); } flag = false; if (!((ReadOnlySpan<byte>)array2).SequenceEqual((ReadOnlySpan<byte>)File.ReadAllBytes(fullPath))) { throw new IOException("The published preset configuration failed readback."); } return; } goto IL_0367; IL_0367: throw new IOException("The configuration changed on disk during the preset edit."); } finally { if (flag && File.Exists(text)) { File.Delete(text); } } } } public sealed class SagaConfig { private const string Product = "com.meadworks.saga"; private readonly ConfigFile file; private readonly Dictionary<SagaSetting, ConfigEntryBase> entries = new Dictionary<SagaSetting, ConfigEntryBase>(); public ConfigEntry<KeyboardShortcut> WheelKey => (ConfigEntry<KeyboardShortcut>)(object)Entry((SagaSetting)(object)Powers.PowerWheelKey); public int? MasteryOverride => SagaCheats.MasteryOverride(CheatLocked(), ((ConfigEntry<bool>)(object)Entry((SagaSetting)(object)Cheats.MasteryOverride)).Value, Get<int>(Cheats.MasteryOverrideLevel)); private SagaConfig(ConfigFile file) { this.file = file; bool saveOnConfigSet = file.SaveOnConfigSet; file.SaveOnConfigSet = false; try { foreach (SagaSetting item in SagaSettings.All) { BindSetting(item); } ReconcilePresetLabels(); } finally { file.SaveOnConfigSet = saveOnConfigSet; } file.Save(); } public static SagaConfig Bind(ConfigFile file, MeadworksLog log) { //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) if (file == null) { throw new ArgumentNullException("file"); } if (log == null) { throw new ArgumentNullException("log"); } SagaConfig sagaConfig = new SagaConfig(file); foreach (KeyValuePair<SagaSetting, ConfigEntryBase> item in sagaConfig.entries.Where<KeyValuePair<SagaSetting, ConfigEntryBase>>((KeyValuePair<SagaSetting, ConfigEntryBase> p) => (int)p.Key.Scope != 0 && !p.Key.Hidden)) { MeadworksBlockedOptions.Track("com.meadworks.saga", item.Value, Map(item.Key.Scope), true); } log.Verbose("Configuration bound: " + Count((SettingScope)0) + " Local, " + Count((SettingScope)1) + " ServerAuthoritative, " + Count((SettingScope)2) + " Cheat, " + SagaSettings.All.Count((SagaSetting s) => s.Hidden) + " hidden."); return sagaConfig; } public static ConfigScope Map(SettingScope scope) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected I4, but got Unknown return (ConfigScope)((int)scope switch { 0 => 0, 1 => 1, 2 => 2, _ => throw new ArgumentException("Unknown scope.", "scope"), }); } public ConfigEntryBase Entry(SagaSetting setting) { if (!entries.TryGetValue(setting ?? throw new ArgumentNullException("setting"), out ConfigEntryBase value)) { throw new ArgumentException("Not a Saga setting.", "setting"); } return value; } internal bool TryMatch(ConfigEntryBase entry, out SagaSetting setting) { foreach (KeyValuePair<SagaSetting, ConfigEntryBase> entry2 in entries) { if (entry2.Value == entry) { setting = entry2.Key; return true; } } setting = null; return false; } public object GetBoxed(SagaSetting setting) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) if (setting is SagaSetting<bool> setting2) { return Get<bool>(setting2); } if (setting is SagaSetting<float> setting3) { return Get<float>(setting3); } if (setting is SagaSetting<int> setting4) { return Get<int>(setting4); } if (setting is SagaSetting<string> setting5) { return Get<string>(setting5); } if (setting is SagaSetting<SkillGainNotices> setting6) { return Get<SkillGainNotices>(setting6); } if (setting is SagaSetting<PowerDisplayMode> setting7) { return Get<PowerDisplayMode>(setting7); } if (setting is SagaSetting<PowerWheelPad> setting8) { return Get<PowerWheelPad>(setting8); } if (setting is SagaSetting<SagaPreset> setting9) { return Get<SagaPreset>(setting9); } if (setting is SagaSetting<Resistance> setting10) { return Get<Resistance>(setting10); } throw new ArgumentException("Unsupported Saga setting " + setting.Id); } public T Get<T>(SagaSetting<T> setting) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Invalid comparison between Unknown and I4 //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Invalid comparison between Unknown and I4 if (((SagaSetting)setting).IsShortcut) { return setting.Normalize((T)(object)ShortcutText((SagaSetting)(object)setting)); } ConfigEntry<T> val = (ConfigEntry<T>)(object)Entry((SagaSetting)(object)setting); SettingScope scope = ((SagaSetting)setting).Scope; if ((int)scope != 1) { if ((int)scope == 2) { return SagaCheats.Effective<T>(setting, CheatLocked(), val.Value); } return setting.Normalize(val.Value); } return setting.Normalize(MeadworksConfig.Effective<T>(val, "com.meadworks.saga", (ConfigScope)1, setting.Default, true)); } public bool TryUseCheat(SagaSetting cheat, string? label = null) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Invalid comparison between Unknown and I4 if (cheat == null || (int)cheat.Scope != 2) { throw new ArgumentException("Expected a Saga cheat.", "cheat"); } return MeadworksBlockedOptions.TryUse("com.meadworks.saga", cheat.Id, (ConfigScope)2, label, true); } public void Shutdown() { foreach (ConfigEntryBase value in entries.Values) { MeadworksBlockedOptions.Untrack(value); } entries.Clear(); } private static bool CheatLocked() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 return (int)MeadworksAuthority.Decide("com.meadworks.saga", (ConfigScope)2, true).Source == 2; } private void BindSetting(SagaSetting setting) { //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0122: Unknown result type (might be due to invalid IL or missing references) ConfigEntryBase value; if (!(setting is SagaSetting<bool> setting2)) { if (!(setting is SagaSetting<int> val)) { if (!(setting is SagaSetting<float> val2)) { if (!(setting is SagaSetting<string> val3)) { if (!(setting is SagaSetting<SkillGainNotices> setting3)) { if (!(setting is SagaSetting<PowerDisplayMode> setting4)) { if (!(setting is SagaSetting<PowerWheelPad> setting5)) { if (!(setting is SagaSetting<SagaPreset> setting6)) { if (!(setting is SagaSetting<Resistance> setting7)) { throw new NotSupportedException(setting.Id + ": unsupported value type " + setting.ValueType.Name + "."); } value = (ConfigEntryBase)(object)Bind<Resistance>(setting7, null); } else { value = (ConfigEntryBase)(object)Bind<SagaPreset>(setting6, null); } } else { value = (ConfigEntryBase)(object)Bind<PowerWheelPad>(setting5, null); } } else { value = (ConfigEntryBase)(object)Bind<PowerDisplayMode>(setting4, null); } } else { value = (ConfigEntryBase)(object)Bind<SkillGainNotices>(setting3, null); } } else if (((SagaSetting)val3).IsShortcut) { value = (ConfigEntryBase)(object)MeadworksConfig.Bind<KeyboardShortcut>(file, ((SagaSetting)val3).Section, ((SagaSetting)val3).Key, KeyboardShortcut.Empty, Describe((SagaSetting)(object)val3, null), Map(((SagaSetting)val3).Scope)); } else { SagaSetting<string> setting8 = val3; value = (ConfigEntryBase)(object)Bind<string>(setting8, null); } } else { value = (ConfigEntryBase)(object)Bind<float>(val2, (AcceptableValueBase?)(object)new AcceptableValueRange<float>((float)((SagaSetting)val2).Minimum, (float)((SagaSetting)val2).Maximum)); } } else { value = (ConfigEntryBase)(object)Bind<int>(val, (AcceptableValueBase?)(object)new AcceptableValueRange<int>((int)((SagaSetting)val).Minimum, (int)((SagaSetting)val).Maximum)); } } else { value = (ConfigEntryBase)(object)Bind<bool>(setting2, null); } entries.Add(setting, value); } private ConfigDescription Describe(SagaSetting setting, AcceptableValueBase? acceptable) { //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Expected O, but got Unknown //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Expected O, but got Unknown //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown ConfigDependency val = null; if ((object)setting == Powers.MaxAttunedPowers || (object)setting == Powers.KillCreditRange || (object)setting == Powers.BlockPowerActivation || (object)setting == Powers.ShowLockedFeatures || (object)setting == Powers.ActivateAllAttuned || (object)setting == Powers.PowerWheelKey || (object)setting == Powers.PowerWheelGamepad || (object)setting == Powers.PowerStrength) { val = new ConfigDependency((Func<bool>)(() => Get<bool>(General.Enabled) && Get<bool>(General.PowersEnabled)), (Func<string>)(() => "Requires General: Enabled and Powers Enabled."), (Func<bool>)null); } if ((object)setting == Sneak.SpeedAt100 || (object)setting == Sneak.VisibilitySkillFactor) { val = new ConfigDependency((Func<bool>)(() => Get<bool>(General.Enabled) && Get<bool>(General.SneakEnabled)), (Func<string>)(() => "Requires General: Enabled and Sneak Enabled."), (Func<bool>)null); } return new ConfigDescription(setting.Description, acceptable, (val == null) ? Array.Empty<object>() : new object[1] { val }); } private ConfigEntry<T> Bind<T>(SagaSetting<T> setting, AcceptableValueBase? acceptable) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Invalid comparison between Unknown and I4 //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) if ((int)((SagaSetting)setting).Scope == 2) { return MeadworksConfig.BindCheat<T>(file, ((SagaSetting)setting).Section, ((SagaSetting)setting).Key, setting.Safe, ((SagaSetting)setting).Description, acceptable); } if (((SagaSetting)setting).Hidden) { return MeadworksConfig.BindHidden<T>(file, ((SagaSetting)setting).Section, ((SagaSetting)setting).Key, setting.Default, ((SagaSetting)setting).Description, Map(((SagaSetting)setting).Scope), acceptable); } return MeadworksConfig.Bind<T>(file, ((SagaSetting)setting).Section, ((SagaSetting)setting).Key, setting.Default, Describe((SagaSetting)(object)setting, acceptable), Map(((SagaSetting)setting).Scope)); } private string ShortcutText(SagaSetting setting) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = ((ConfigEntry<KeyboardShortcut>)(object)Entry(setting)).Value; if ((int)((KeyboardShortcut)(ref value)).MainKey != 0) { return ((KeyboardShortcut)(ref value)).Serialize(); } return ""; } private static int Count(SettingScope scope) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) return SagaSettings.All.Count((SagaSetting s) => s.Scope == scope && !s.Hidden); } private void ReconcilePresetLabels() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //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_003f: 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) //IL_0049: Unknown result type (might be due to invalid IL or missing references) foreach (SagaPresetGroup group in SagaPresets.Groups) { ConfigEntry<SagaPreset> val = (ConfigEntry<SagaPreset>)(object)Entry((SagaSetting)(object)group.Selector); SagaPreset val2 = group.OnLoad(val.Value, (Func<SagaSetting, object>)((SagaSetting setting) => entries[setting].BoxedValue)); if (val2 != val.Value) { val.Value = val2; } } } } internal sealed class SagaPresetRuntime : IDisposable { private readonly ConfigFile file; private readonly SagaConfig config; private readonly MeadworksLog log; private readonly SagaPresetChanges pending = new SagaPresetChanges(); private readonly Dictionary<SagaPresetGroup, SagaPreset> acknowledged = new Dictionary<SagaPresetGroup, SagaPreset>(); private bool writing; private bool reloaded; private long generation; private SessionRole role; private bool Remote { get { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if (Plugin.IsReady) { return (int)Plugin.Session.Role == 3; } return false; } } internal SagaPresetRuntime(ConfigFile file, SagaConfig config, MeadworksLog log) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown this.file = file ?? throw new ArgumentNullException("file"); this.config = config ?? throw new ArgumentNullException("config"); this.log = log ?? throw new ArgumentNullException("log"); Remember(); if (SagaPresets.Retired.Length != 0) { log.Info("Saga retains but ignores obsolete configuration keys: " + string.Join(", ", SagaPresets.Retired) + "."); } ReconcileLabels(); file.SettingChanged += Changed; file.ConfigReloaded += Reloaded; } private object Read(SagaSetting setting) { return config.Entry(setting).BoxedValue; } private void Remember() { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) generation = (Plugin.IsReady ? Plugin.Session.Generation : (-1)); role = (SessionRole)(Plugin.IsReady ? ((int)Plugin.Session.Role) : 0); } private void Changed(object? sender, SettingChangedEventArgs args) { if (!writing && ((args != null) ? args.ChangedSetting : null) != null && config.TryMatch(args.ChangedSetting, out SagaSetting setting)) { pending.Observe(setting, Remote); } } private void Reloaded(object? sender, EventArgs args) { if (!writing) { pending.Clear(); reloaded = true; } } internal void Tick() { //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_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) long num = generation; SessionRole val = role; Remember(); if (reloaded || num != generation || val != role) { pending.Clear(); reloaded = false; ReconcileLabels(); return; } try { pending.Flush(Remote, (Func<SagaSetting, object>)Read, (Action<SagaPresetGroup, IReadOnlyDictionary<SagaSetting, object>>)Apply); } catch (AggregateException ex) { foreach (Exception innerException in ex.InnerExceptions) { log.Warning(innerException.Message); } } } private void Apply(SagaPresetGroup group, IReadOnlyDictionary<SagaSetting, object> values) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) if (values.All<KeyValuePair<SagaSetting, object>>((KeyValuePair<SagaSetting, object> pair) => object.Equals(Read(pair.Key), pair.Value))) { acknowledged[group] = (SagaPreset)values[(SagaSetting)(object)group.Selector]; return; } ConfigEntryBase[] array = values.Keys.Select(config.Entry).ToArray(); ConfigAmendResult val; try { val = MeadworksConfig.AmendBatch("com.meadworks.saga", (IReadOnlyList<ConfigEntryBase>)array, (Action)delegate { Write(values); }, "Saga"); } catch (Exception ex) { Restore(group); throw new InvalidOperationException("Preset was not applied: " + ((SagaSetting)group.Selector).Id + ". " + ex.Message, ex); } if (!val.Applied) { Restore(group); throw new InvalidOperationException("Preset was not applied: " + ((SagaSetting)group.Selector).Id + "."); } acknowledged[group] = (SagaPreset)values[(SagaSetting)(object)group.Selector]; } private void ReconcileLabels() { //IL_003b: 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_0052: 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_0083: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) foreach (SagaPresetGroup group in SagaPresets.Groups) { if (SagaPresets.CanWrite(group, Remote)) { ConfigEntry<SagaPreset> val = (ConfigEntry<SagaPreset>)(object)config.Entry((SagaSetting)(object)group.Selector); SagaPreset val2 = group.OnLoad(val.Value, (Func<SagaSetting, object>)Read); if (val2 != val.Value) { Write(new Dictionary<SagaSetting, object> { [(SagaSetting)(object)group.Selector] = val2 }); } acknowledged[group] = val2; } } } private void Restore(SagaPresetGroup group) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: 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_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_0038: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) SagaPreset value; SagaPreset val = (SagaPreset)((!acknowledged.TryGetValue(group, out value)) ? 3 : ((int)value)); SagaPreset val2 = group.OnLoad(val, (Func<SagaSetting, object>)Read); try { Write(new Dictionary<SagaSetting, object> { [(SagaSetting)(object)group.Selector] = val2 }); acknowledged[group] = val2; } catch (Exception ex) { bool saveOnConfigSet = file.SaveOnConfigSet; writing = true; file.SaveOnConfigSet = false; try { config.Entry((SagaSetting)(object)group.Selector).BoxedValue = val2; acknowledged[group] = val2; } finally { file.SaveOnConfigSet = saveOnConfigSet; writing = false; } log.Warning("Preset selector restored in memory but could not be saved: " + ((SagaSetting)group.Selector).Id + ". " + ex.Message); } } private void Write(IReadOnlyDictionary<SagaSetting, object> changes) { ConfigEntryBase[] array = changes.Keys.Select(config.Entry).ToArray(); Dictionary<ConfigEntryBase, object> dictionary = array.ToDictionary((ConfigEntryBase entry) => entry, (ConfigEntryBase entry) => entry.BoxedValue); bool saveOnConfigSet = file.SaveOnConfigSet; writing = true; file.SaveOnConfigSet = false; try { foreach (KeyValuePair<SagaSetting, object> change in changes) { config.Entry(change.Key).BoxedValue = change.Value; } PresetPersistence.Save(array); } catch { foreach (KeyValuePair<ConfigEntryBase, object> item in dictionary) { item.Key.BoxedValue = item.Value; } throw; } finally { file.SaveOnConfigSet = saveOnConfigSet; writing = false; } } public void Dispose() { file.SettingChanged -= Changed; file.ConfigReloaded -= Reloaded; pending.Clear(); } } internal static class NativeRpcCleanup { private sealed class ViewWrite { public WeakReference<ZNetView> View; public string Name = ""; public object Handler; } [HarmonyPatch(typeof(ZNet), "Awake")] private static class Net { [HarmonyPriority(0)] private static void Postfix() { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null) { return; } IDictionary functions = instance.m_functions; string[] array = new string[8] { "saga.WoodCredit", "saga.WoodReport", "saga.WoodCallout", "saga.FishCallout", "saga.FishAnnounce", "saga.MineCallout", "saga.BossDeath", "saga.BossKill" }; for (int i = 0; i < array.Length; i++) { int stableHashCode = StringExtensionMethods.GetStableHashCode(array[i]); if (functions.Contains(stableHashCode)) { routed.Add(Tuple.Create(functions, stableHashCode, functions[stableHashCode])); } } } } private static readonly List<ViewWrite> views = new List<ViewWrite>(); private static readonly List<Tuple<IDictionary, int, object>> routed = new List<Tuple<IDictionary, int, object>>(); public static void Track(ZNetView view, string name) { int stableHashCode = StringExtensionMethods.GetStableHashCode(name); if (views.Count % 128 == 127) { views.RemoveAll((ViewWrite write) => !write.View.TryGetTarget(out ZNetView target) || (Object)(object)target == (Object)null); } if ((Object)(object)view != (Object)null && view.m_functions.TryGetValue(stableHashCode, out var value)) { views.Add(new ViewWrite { View = new WeakReference<ZNetView>(view), Name = name, Handler = value }); } } public static void Shutdown() { foreach (ViewWrite view in views) { if (view.View.TryGetTarget(out ZNetView target) && (Object)(object)target != (Object)null && target.m_functions.TryGetValue(StringExtensionMethods.GetStableHashCode(view.Name), out var value) && value == view.Handler) { target.Unregister(view.Name); } } views.Clear(); foreach (Tuple<IDictionary, int, object> item in routed) { if (item.Item1.Contains(item.Item2) && item.Item1[item.Item2] == item.Item3) { item.Item1.Remove(item.Item2); } } routed.Clear(); } } public sealed class SagaImportPreview { internal ConfigConverter Converter; internal readonly Dictionary<string, string> Sources = new Dictionary<string, string>(); internal string Source = ""; public ConversionPreview Config { get; internal set; } public IReadOnlyDictionary<string, string> Yaml { get; internal set; } public IReadOnlyList<string> Warnings { get; internal set; } } public static class SagaConverter { [HarmonyPatch(typeof(Player), "Load")] private static class CharacterImport { private static void Postfix(Player __instance) { if ((Object)(object)__instance == (Object)(object)Player.m_localPlayer) { ImportCharacter(__instance); } } } private sealed class ImportSnapshot { internal readonly ConfigFile Owner = config; internal readonly string Source; internal readonly string SourceHash; internal readonly long Generation = Plugin.Session.Generation; internal readonly long Lifecycle = lifecycle; internal readonly SessionRole Role = Plugin.Session.Role; internal string TargetHash; internal bool Consumed; internal ImportSnapshot(string source) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) Source = source; SourceHash = FilesHash(SourcePaths(source)); TargetHash = TargetHash(this); } } private sealed class ImportTarget : IValidatedConversionTarget, IConversionTarget { private readonly SagaImportPreview preview; private readonly ImportSnapshot snapshot; private readonly IConversionTarget target; private readonly Dictionary<string, object> previous = new Dictionary<string, object>(StringComparer.Ordinal); private bool backupRequested; internal bool SavedLocally { get; private set; } internal bool PublicationFailed { get; private set; } public int SchemaVersion => target.SchemaVersion; internal ImportTarget(SagaImportPreview preview, ImportSnapshot snapshot) { this.preview = preview; this.snapshot = snapshot; target = MeadworksConversion.Target(config); } private ConfigEntryBase[] Entries(IReadOnlyDictionary<string, object> values) { List<ConfigEntryBase> list = new List<ConfigEntryBase>(); ConfigEntryMetadata val2 = default(ConfigEntryMetadata); foreach (KeyValuePair<string, object> value in values) { ConfigDefinition[] array = config.Keys.Where((ConfigDefinition key) => key.Section + "." + key.Key == value.Key).Take(2).ToArray(); if (array.Length != 1) { throw new ArgumentException(SagaText.Text("import_wrong_source")); } ConfigEntryBase val = config[array[0]]; if (!MeadworksConfig.TryGetMetadata(val, ref val2) || val2.ProductId != "com.meadworks.saga" || !val2.Decision.Allowed || val2.Decision.RequiresServerAmendment) { throw new InvalidOperationException(SagaText.Text("import_authority")); } object obj = (val.SettingType.IsInstanceOfType(value.Value) ? value.Value : TomlTypeConverter.ConvertToValue(Convert.ToString(value.Value, CultureInfo.InvariantCulture), val.SettingType)); if (val.Description.AcceptableValues != null && !val.Description.AcceptableValues.IsValid(obj)) { throw new ArgumentException(SagaText.Text("import_wrong_source")); } list.Add(val); } return list.Distinct().ToArray(); } public void Validate(IReadOnlyDictionary<string, object> values) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) if (SavedLocally) { throw new InvalidOperationException(SagaText.Text("import_changed")); } Admission(preview, snapshot); ((IValidatedConversionTarget)target).Validate(values); Entries(values); } public void Backup() { Admission(preview, snapshot); if (!File.Exists(config.ConfigFilePath)) { throw new IOException(SagaText.Text("import_confirm")); } backupRequested = true; } public void WriteAtomic(IReadOnlyDictionary<string, object> values) { Validate(values); if (!backupRequested) { throw new InvalidOperationException(SagaText.Text("import_confirm")); } ConfigEntryBase[] array = Entries(values); if (array.Length == 0) { return; } ConfigEntryBase[] array2 = array; foreach (ConfigEntryBase val in array2) { previous.Add(val.Definition.Section + "." + val.Definition.Key, val.BoxedValue); } try { if (!MeadworksConfig.AmendBatch("com.meadworks.saga", (IReadOnlyList<ConfigEntryBase>)array, (Action)delegate { Admission(preview, snapshot); Entries(values); target.Backup(); target.WriteAtomic(values); SavedLocally = true; snapshot.TargetHash = TargetHash(snapshot); }, "Saga import").Applied || !SavedLocally) { throw new InvalidOperationException(SagaText.Text("import_authority")); } } catch (Exception innerException) when (SavedLocally) { PublicationFailed = true; throw new IOException(SagaText.Text("import_saved_local"), innerException); } } internal void Rollback() { Admission(preview, snapshot); ConfigEntryBase[] array = Entries(previous); if (array.Length == 0) { return; } try { if (!MeadworksConfig.AmendBatch("com.meadworks.saga", (IReadOnlyList<ConfigEntryBase>)array, (Action)delegate { Admission(preview, snapshot); Entries(previous); target.WriteAtomic((IReadOnlyDictionary<string, object>)previous); snapshot.TargetHash = TargetHash(snapshot); }, "Saga import recovery").Applied) { throw new InvalidOperationException(SagaText.Text("import_authority")); } } catch (Exception innerException) { throw new IOException(SagaText.Text("import_recovery_incomplete"), innerException); } } } private static ConfigFile config = null; private static readonly List<IDisposable> leases = new List<IDisposable>(); private static readonly Dictionary<string, string> identities = new Dictionary<string, string> { { "ProgressivePowers", "MidnightsFX.ProgressivePowers" }, { "SNEAKer", "blacks7ar.SNEAKer" }, { "SecondaryAttacks", "sighsorry.SecondaryAttacks" }, { "GrindstoneSkills", "com.GrindstoneSkills" } }; private static bool active; private static int thread; private static long lifecycle; private static readonly ConditionalWeakTable<SagaImportPreview, ImportSnapshot> snapshots = new ConditionalWeakTable<SagaImportPreview, ImportSnapshot>(); public static void Initialize(ConfigFile target) { //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Expected O, but got Unknown config = target; active = true; lifecycle++; thread = Thread.CurrentThread.ManagedThreadId; foreach (KeyValuePair<string, string> identity in identities) { string source = identity.Key; leases.Add(MeadworksConversion.Register(new ConversionRegistration("saga." + source.ToLowerInvariant(), "com.meadworks.saga", identity.Value, source, (Func<bool>)(() => File.Exists(Path.Combine(Paths.ConfigPath, identities[source] + ".cfg"))), (Func<object>)(() => Preview(source)), (Func<object, IReadOnlyDictionary<string, string>, bool, object>)((object preview, IReadOnlyDictionary<string, string> choices, bool confirmed) => Apply(source, preview, choices, confirmed))))); } } private static string Read(SagaImportPreview preview, string path) { FileInfo fileInfo = new FileInfo(path); if (!fileInfo.Exists) { return ""; } if (fileInfo.Length > 1048576) { throw new InvalidDataException(SagaText.Text("import_source_large")); } string text = File.ReadAllText(fileInfo.FullName); preview.Sources.Add(fileInfo.FullName, text); return text; } private unsafe static SagaImportPreview Preview(string source) { //IL_0227: Unknown result type (might be due to invalid IL or missing references) //IL_025c: Unknown result type (might be due to invalid IL or missing references) //IL_0198: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Expected O, but got Unknown //IL_0405: Unknown result type (might be due to invalid IL or missing references) //IL_040a: Unknown result type (might be due to invalid IL or missing references) //IL_0411: Unknown result type (might be due to invalid IL or missing references) //IL_0461: Unknown result type (might be due to invalid IL or missing references) //IL_046b: Expected O, but got Unknown //IL_0466: Unknown result type (might be due to invalid IL or missing references) //IL_0470: Expected O, but got Unknown //IL_0345: Unknown result type (might be due to invalid IL or missing references) CheckOwner(); ImportSnapshot importSnapshot = new ImportSnapshot(source); SagaImportPreview sagaImportPreview = new SagaImportPreview { Source = source }; string path = Path.Combine(Paths.ConfigPath, identities[source] + ".cfg"); IReadOnlyList<KeyValuePair<string, string>> readOnlyList = CfgText.Read(Read(sagaImportPreview, path), source); List<ConversionRule> list = new List<ConversionRule>(); List<KeyValuePair<string, string>> list2 = new List<KeyValuePair<string, string>>(); List<string> list3 = new List<string>(); Dictionary<string, string> dictionary = new Dictionary<string, string>(); foreach (KeyValuePair<string, string> item in readOnlyList) { string text = item.Key.Substring(source.Length + 1); int num = text.LastIndexOf('.'); if (num >= 0) { string text2 = text.Substring(0, num); string text3 = text.Substring(num + 1); SagaSetting val = SagaImportRules.Target(source, text2, text3); if (val == null) { list2.Add(item); continue; } string text4 = ((val.Minimum != null) ? SagaText.Format("import_range", Convert.ToString(val.Minimum, CultureInfo.InvariantCulture), Convert.ToString(val.Maximum, CultureInfo.InvariantCulture)) : string.Join(",", val.Choices)); list.Add(new ConversionRule(item.Key, val.Id, val.ValueType.IsEnum ? val.ValueType.Name : val.ValueType.Name, text4, false, false)); list2.Add(new KeyValuePair<string, string>(item.Key, SagaImportRules.Normalize(source, val, item.Value, ((Character)(Player.m_localPlayer?)).m_crouchSpeed ?? 2f))); } } if (source == "GrindstoneSkills") { DataFamily[] array = new DataFamily[4]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); DataFamily[] array2 = (DataFamily[])(object)array; for (int i = 0; i < array2.Length; i++) { DataFamily val2 = array2[i]; string path2 = Path.Combine(Paths.ConfigPath, "GrindstoneSkills." + ((object)(*(DataFamily*)(&val2))/*cast due to .constrained prefix*/).ToString() + ".yml"); if (File.Exists(path2)) { dictionary.Add(SagaDataFiles.BaseName(val2), Read(sagaImportPreview, path2)); } } } if (source == "ProgressivePowers") { Tuple<string, DataFamily, string>[] array3 = new Tuple<string, DataFamily, string>[2] { Tuple.Create<string, DataFamily, string>("Levels.yaml", (DataFamily)0, "Level Requirements"), Tuple.Create<string, DataFamily, string>("Powers.yaml", (DataFamily)1, "Advanced Powers") }; foreach (Tuple<string, DataFamily, string> pair in array3) { string path3 = Path.Combine(Paths.ConfigPath, "ProgressivePowers", pair.Item1); string text5 = (File.Exists(path3) ? Read(sagaImportPreview, path3) : readOnlyList.FirstOrDefault((KeyValuePair<string, string> p) => p.Key == source + ".General." + pair.Item3).Value); if (!string.IsNullOrEmpty(text5)) { dictionary.Add(SagaDataFiles.BaseName(pair.Item2), text5.Replace("\\r\\n", "\n").Replace("\\n", "\n")); } } list3.Add(SagaText.Text("import_unsupported_powers")); } if (source == "SNEAKer" && readOnlyList.Any((KeyValuePair<string, string> p) => p.Key.EndsWith(".Enable Exp Multiplier") && p.Value == "On")) { list3.Add(SagaText.Text("import_sneak_larder")); } foreach (KeyValuePair<string, string> item2 in dictionary) { DataFamily value = SagaDataFiles.Classify(item2.Key).Value; IReadOnlyList<string> readOnlyList2 = NativeSaga.Data.ValidateImport(value, item2.Value); if (readOnlyList2.Count > 0) { throw new InvalidDataException(string.Join("; ", readOnlyList2)); } } sagaImportPreview.Converter = new ConfigConverter(new ConversionMapping(1, source, 0, (IEnumerable<ConversionRule>)list)); sagaImportPreview.Config = sagaImportPreview.Converter.Preview((IEnumerable<KeyValuePair<string, string>>)list2); sagaImportPreview.Yaml = new ReadOnlyDictionary<string, string>(dictionary); sagaImportPreview.Warnings = list3.AsReadOnly(); Unchanged(sagaImportPreview, importSnapshot); snapshots.Add(sagaImportPreview, importSnapshot); return sagaImportPreview; } private static object Apply(string source, object raw, IReadOnlyDictionary<string, string> choices, bool confirmed) { //IL_0094: Unknown result type (might be due to invalid IL or missing references) if (!confirmed) { throw new InvalidOperationException(SagaText.Text("import_confirm")); } if (!(raw is SagaImportPreview sagaImportPreview) || sagaImportPreview.Source != source) { throw new ArgumentException(SagaText.Text("import_wrong_source")); } if (!snapshots.TryGetValue(sagaImportPreview, out ImportSnapshot value)) { throw new ArgumentException(SagaText.Text("import_wrong_source")); } Admission(sagaImportPreview, value); foreach (KeyValuePair<string, string> item in sagaImportPreview.Yaml) { IReadOnlyList<string> readOnlyList = NativeSaga.Data.ValidateImport(SagaDataFiles.Classify(item.Key).Value, item.Value); if (readOnlyList.Count > 0) { throw new InvalidDataException(string.Join("; ", readOnlyList)); } } if (sagaImportPreview.Config.Errors.Count > 0) { throw new InvalidDataException(string.Join("; ", sagaImportPreview.Config.Errors)); } ImportTarget importTarget = new ImportTarget(sagaImportPreview, value); Dictionary<string, string> dictionary = new Dictionary<string, string>(); Dictionary<string, string> dictionary2 = new Dictionary<string, string>(); HashSet<string> committed = new HashSet<string>(StringComparer.OrdinalIgnoreCase); try { foreach (KeyValuePair<string, string> item2 in sagaImportPreview.Yaml) { string fullPath = Path.GetFullPath(Path.Combine(Paths.ConfigPath, item2.Key)); dictionary[fullPath] = (File.Exists(fullPath) ? File.ReadAllText(fullPath) : null); string text = fullPath + ".import-" + Guid.NewGuid().ToString("N") + ".tmp"; File.WriteAllText(text, item2.Value); dictionary2.Add(fullPath, text); } ConversionResult result = sagaImportPreview.Converter.Apply(sagaImportPreview.Config, (IConversionTarget)(object)importTarget, choices, (IReadOnlyDictionary<string, string>)null); foreach (KeyValuePair<string, string> item3 in dictionary2) { Admission(sagaImportPreview, value); if (File.Exists(item3.Key)) { File.Copy(item3.Key, item3.Key + ".import-" + DateTime.UtcNow.Ticks + ".bak", overwrite: false); File.Replace(item3.Value, item3.Key, null); } else { File.Move(item3.Value, item3.Key); } committed.Add(item3.Key); value.TargetHash = TargetHash(value); } Admission(sagaImportPreview, value); ConfigEntry<string> val = (ConfigEntry<string>)(object)Plugin.Settings.Entry((SagaSetting)(object)Import.ImportedSources); val.Value = string.Join(",", (from v in val.Value.Split(',') where v.Length > 0 select v).Concat(new string[1] { source }).Distinct()); value.Consumed = true; return result; } catch (Exception ex) { if (importTarget.PublicationFailed) { value.Consumed = true; throw; } try { if (importTarget.SavedLocally) { importTarget.Rollback(); } foreach (KeyValuePair<string, string> item4 in dictionary.Where((KeyValuePair<string, string> item) => committed.Contains(item.Key))) { Admission(sagaImportPreview, value); if (item4.Value == null) { if (File.Exists(item4.Key)) { File.Delete(item4.Key); } } else { File.WriteAllText(item4.Key, item4.Value); } value.TargetHash = TargetHash(value); } } catch (Exception ex2) { value.Consumed = true; throw new IOException(SagaText.Text("import_recovery_incomplete"), new AggregateException(ex, ex2)); } if (importTarget.SavedLocally || committed.Count > 0) { value.Consumed = true; } throw; } finally { foreach (string value2 in dictionary2.Values) { if (File.Exists(value2)) { File.Delete(value2); } } } } public static void ImportCharacter(Player __instance) { if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer || __instance.m_customData.ContainsKey("saga.imported.discoveries")) { return; } foreach (KeyValuePair<string, string> characterKey in SagaImportRules.CharacterKeys) { if (!__instance.m_customData.ContainsKey(characterKey.Value) && __instance.m_customData.TryGetValue(characterKey.Key, out var value)) { __instance.m_customData[characterKey.Value] = value; } } __instance.m_customData["saga.imported.discoveries"] = "1"; } public static void Shutdown() { active = false; lifecycle++; foreach (IDisposable lease in leases) { lease.Dispose(); } leases.Clear(); } private static void CheckOwner() { if (!active || !Plugin.Ready || !Plugin.IsReady || Thread.CurrentThread.ManagedThreadId != thread) { throw new InvalidOperationException(SagaText.Text("import_authority")); } } private unsafe static IEnumerable<string> SourcePaths(string source) { yield return Path.Combine(Paths.ConfigPath, identities[source] + ".cfg"); if (source == "GrindstoneSkills") { DataFamily[] array = new DataFamily[4]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); DataFamily[] array2 = (DataFamily[])(object)array; for (int i = 0; i < array2.Length; i++) { DataFamily val = array2[i]; yield return Path.Combine(Paths.ConfigPath, "GrindstoneSkills." + ((object)(*(DataFamily*)(&val))/*cast due to .constrained prefix*/).ToString() + ".yml"); } } if (source == "ProgressivePowers") { string[] array3 = new string[2] { "Levels.yaml", "Powers.yaml" }; foreach (string path in array3) { yield return Path.Combine(Paths.ConfigPath, "ProgressivePowers", path); } } } private static IEnumerable<string> TargetPaths(string source) { if (source == "GrindstoneSkills") { DataFamily[] array = new DataFamily[4]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); DataFamily[] array2 = (DataFamily[])(object)array; foreach (DataFamily val in array2) { yield return Path.Combine(Paths.ConfigPath, SagaDataFiles.BaseName(val)); } } if (source == "ProgressivePowers") { DataFamily[] array2 = (DataFamily[])(object)new DataFamily[2] { default(DataFamily), (DataFamily)1 }; foreach (DataFamily val2 in array2) { yield return Path.Combine(Paths.ConfigPath, SagaDataFiles.BaseName(val2)); } } } private static string Hash(byte[] bytes) { using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(bytes)); } private static string FileHash(string path) { if (!File.Exists(path)) { return "missing"; } using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); if (fileStream.Length > 1048576) { throw new InvalidDataException(SagaText.Text("import_source_large")); } using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(fileStream)); } private static string FilesHash(IEnumerable<string> paths) { return Hash(Encoding.UTF8.GetBytes(string.Join("\n", from path in paths.Select(Path.GetFullPath).OrderBy<string, string>((string path) => path, StringComparer.Ordinal) select path.Length + ":" + path + "=" + FileHash(path)))); } private static string TargetHash(ImportSnapshot snapshot) { return Hash(Encoding.UTF8.GetBytes(FilesHash(TargetPaths(snapshot.Source).Append(config.ConfigFilePath)) + "\n" + string.Join("\n", from key in config.Keys.OrderBy<ConfigDefinition, string>((ConfigDefinition key) => key.Section, StringComparer.Ordinal).ThenBy<ConfigDefinition, string>((ConfigDefinition key) => key.Key, StringComparer.Ordinal) select key.Section.Length + ":" + key.Section + key.Key.Length + ":" + key.Key + "=" + TomlTypeConverter.ConvertToString(config[key].BoxedValue, config[key].SettingType)))); } private static void Admission(SagaImportPreview preview, ImportSnapshot snapshot) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Invalid comparison between Unknown and I4 //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) CheckOwner(); SessionSnapshot session = Plugin.Session; AuthorityDecision val = MeadworksAuthority.Decide("com.meadworks.saga", (ConfigScope)1, true); if (snapshot.Consumed || snapshot.Lifecycle != lifecycle || config != snapshot.Owner || preview.Source != snapshot.Source || (int)session.Role == 3 || session.Role != snapshot.Role || session.Generation != snapshot.Generation || !val.Allowed || val.RequiresServerAmendment) { throw new InvalidOperationException(SagaText.Text("import_authority")); } Unchanged(preview, snapshot); } private static void Unchanged(SagaImportPreview preview, ImportSnapshot snapshot) { if (snapshot.SourceHash != FilesHash(SourcePaths(snapshot.Source)) || snapshot.TargetHash != TargetHash(snapshot) || preview.Sources.Any<KeyValuePair<string, string>>((KeyValuePair<string, string> item) => !File.Exists(item.Key) || File.ReadAllText(item.Key) != item.Value)) { throw new InvalidOperationException(SagaText.Text("import_changed")); } } } public static class SagaText { private static MeadworksTranslations? catalog; public static ProductTranslations Catalog { get; private set; } public static string Language { get { MeadworksTranslations? obj = catalog; return ((obj != null) ? obj.Language : null) ?? "English"; } } public static void Initialize(ManualLogSource log) { //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Expected O, but got Unknown //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Expected O, but got Unknown Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); Assembly assembly = typeof(SagaText).Assembly; foreach (string item in MeadworksLanguages.Supported) { using Stream stream = assembly.GetManifestResourceStream("Saga.localization." + item + ".json") ?? throw new InvalidOperationException("Missing Saga language " + item); using StreamReader streamReader = new StreamReader(stream); dictionary.Add(item, streamReader.ReadToEnd()); } Catalog = new ProductTranslations("saga", (IReadOnlyDictionary<string, string>)dictionary); catalog = MeadworksTranslations.Register(new BepInPlugin("com.meadworks.saga", "Saga", "0.1.17"), Catalog, (string)null, log); } public static string Text(string key) { MeadworksTranslations? obj = catalog; return ((obj != null) ? obj.Text(key) : null) ?? key; } public static string Format(string key, params object[] args) { MeadworksTranslations? obj = catalog; return ((obj != null) ? obj.Format(key, args) : null) ?? key; } public static string Packet(string key, params object[] args) { return CalloutText.Encode(key, args.Select((object value) => Convert.ToString(value, CultureInfo.InvariantCulture) ?? "").ToArray()); } public static string Resolve(string text) { string key = default(string); IReadOnlyList<string> source = default(IReadOnlyList<string>); if (!CalloutText.TryDecode(text, ref key, ref source)) { if (!text.StartsWith("~saga1:", StringComparison.Ordinal)) { return text; } return ""; } return Format(key, ((IEnumerable<string>)source).Select((Func<string, object>)delegate(string value) { Localization instance = Localization.instance; return ((instance != null) ? instance.Localize(value) : null) ?? value; }).ToArray()); } public static void Tick() { MeadworksTranslations? obj = catalog; if (obj != null) { obj.Update(); } } public static void Shutdown() { MeadworksTranslations? obj = catalog; if (obj != null) { obj.Dispose(); } catalog = null; } } [BepInPlugin("com.meadworks.saga", "Saga", "0.1.17")] [BepInDependency("com.meadworks.crucible", "0.3.33")] [BepInDependency("com.meadworks.crucible.ui", "0.3.22")] [BepInDependency("com.jotunn.jotunn", "2.30.2")] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "com.meadworks.saga"; public const string Name = "Saga"; public const string Version = "0.1.17"; public const string Family = "Fang"; private ProductRegistration? registration; private MeadworksLog? log; private SagaPresetRuntime? presets; public static bool Ready { get; private set; } public static bool AllowsUi { get; private set; } public static SagaConfig? Settings { get; private set; } private void Awake() { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Expected O, but got Unknown log = MeadworksLog.For(((BaseUnityPlugin)this).Logger, "Saga"); try { if (!Plugin.IsReady) { throw new InvalidOperationException("Required Crucible is not ready"); } registration = Plugin.Services.Register(new ProductMetadata("com.meadworks.saga", "Saga", new System.Version("0.1.17"), new System.Version(0, 1), new System.Version(0, 1), (MultiplayerPolicy)2, "Fang")); AllowsUi = MeadworksHeadless.AllowsUi; Settings = SagaConfig.Bind(((BaseUnityPlugin)this).Config, log); presets = new SagaPresetRuntime(((BaseUnityPlugin)this).Config, Settings, log); NativeSaga.Initialize(((BaseUnityPlugin)this).Logger); SagaConverter.Initialize(((BaseUnityPlugin)this).Config); PowerCommands.Initialize(registration); BackstabCredit.Initialize(registration); registration.AddCapability<IReadOnlyDictionary<string, Delegate>>("meadworks.saga.progression", new System.Version(1, 0), NativeBook.Capability()); registration.AddCapability<IReadOnlyDictionary<string, Delegate>>("meadworks.saga.powers", new System.Version(1, 0), NativePowers.Capability()); registration.AddCapability<Func<string, string, bool>>("meadworks.saga.register_boss", new System.Version(1, 0), (Func<string, string, bool>)NativePowers.RegisterBoss); registration.AddCapability<Func<IReadOnlyList<string>>>("meadworks.diagnostics.com.meadworks.saga", new System.Version(1, 0), (Func<IReadOnlyList<string>>)NativeSaga.Describe); Ready = true; log.Info(AllowsUi ? "Saga registered." : "Saga registered on a headless process; no UI is created."); } catch (Exception ex) { log.Error((object)("Saga initialization rejected: " + ex.GetType().Name + ": " + ex.Message)); Shutdown(); } } private void OnDestroy() { Shutdown(); } private void Start() { if (!Ready) { return; } try { NativeSaga.Start(); } catch (Exception ex) { MeadworksLog? obj = log; if (obj != null) { obj.Error((object)("Saga modules rejected: " + ex)); } Shutdown(); } } private void Update() { if (Ready) { presets?.Tick(); NativeSaga.Tick(); } } private void Shutdown() { Ready = false; presets?.Dispose(); presets = null; NativeSaga.Shutdown(); SagaConverter.Shutdown(); PowerCommands.Shutdown(); BackstabCredit.Shutdown(); Settings?.Shutdown(); Settings = null; ProductRegistration? obj = registration; if (obj != null) { obj.Dispose(); } registration = null; } } public static class BossKillCredit { private sealed class Pending { public long Sender; public int Prefab; public string Boss = ""; public ZDOID Id; public Vector3 Position; public float Until; } [HarmonyPatch(typeof(ZNet), "Awake")] private static class NetAwake { private static void Postfix() { Clear(); ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.Register<ZPackage>("saga.BossDeath", (Action<long, ZPackage>)OnReport); } ZRoutedRpc instance2 = ZRoutedRpc.instance; if (instance2 != null) { instance2.Register<ZPackage>("saga.BossKill", (Action<long, ZPackage>)OnCredit); } } } public const string ReportRpc = "saga.BossDeath"; public const string CreditRpc = "saga.BossKill"; private const float WaitSeconds = 3f; private const int MaxPending = 32; private static readonly HashSet<ZDOID> seen = new HashSet<ZDOID>(); private static readonly List<Pending> pending = new List<Pending>(); public static void Clear() { seen.Clear(); pending.Clear(); } public static void Poll() { try { Drain(); } catch (Exception error) { NativeSaga.Report(error, "boss credit"); } } public static void Report(Character character) { try { ReportInner(character); } catch (Exception error) { NativeSaga.Report(error, "boss credit"); } } private static void ReportInner(Character character) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Expected O, but got Unknown //IL_00f9: 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) if (!NativeSaga.Active("powers") || (Object)(object)character == (Object)null || (Object)(object)character.m_nview == (Object)null || !character.m_nview.IsValid() || !character.m_nview.IsOwner() || character.GetHealth() > 0f) { return; } ZDO zDO = character.m_nview.GetZDO(); if (zDO == null) { return; } string prefabName = Utils.GetPrefabName(((Component)character).gameObject); if (!NativePowers.KnownBossPrefab(prefabName, out string _)) { return; } int stableHashCode = StringExtensionMethods.GetStableHashCode(prefabName); if (zDO.GetPrefab() != stableHashCode) { return; } ZDOID uid = zDO.m_uid; Vector3 position = ((Component)character).transform.position; ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null) { return; } if (instance.IsServer()) { Accept(ZNet.GetUID(), stableHashCode, uid, position); return; } long num = instance.GetServerPeer()?.m_uid ?? 0; if (num != 0L) { ZPackage val = new ZPackage(); val.Write(stableHashCode); val.Write(uid); val.Write(position); instance2.InvokeRoutedRPC(num, "saga.BossDeath", new object[1] { val }); } } private static void OnReport(long sender, ZPackage pkg) { //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_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) try { int prefab = pkg.ReadInt(); ZDOID id = pkg.ReadZDOID(); Vector3 pos = pkg.ReadVector3(); Accept(sender, prefab, id, pos); } catch (Exception error) { NativeSaga.Report(error, "boss credit"); } } private static void Accept(long sender, int prefab, ZDOID id, Vector3 pos) { //IL_0025: 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_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || ((ZDOID)(ref id)).IsNone() || seen.Contains(id)) { return; } ZDO val = ((ZDOMan.instance != null) ? ZDOMan.instance.GetZDO(id) : null); if (val != null && val.IsValid()) { bool flag = val.GetOwner() == sender; string boss = ""; bool flag2 = val.GetPrefab() == prefab && NativePowers.KnownBossHash(prefab, out boss); bool flag3 = val.GetFloat(ZDOVars.s_health, 1f) <= 0f; if (BossKillRules.ServerAccepts(flag, flag2, flag3, true)) { seen.Add(id); Forget(id); Broadcast(boss, id, pos); } else if (flag && flag2) { Hold(sender, prefab, boss, id, pos); } } } private static void Hold(long sender, int prefab, string boss, ZDOID id, Vector3 pos) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < pending.Count; i++) { if (((ZDOID)(ref pending[i].Id)).Equals(id)) { return; } } if (pending.Count < 32) { pending.Add(new Pending { Sender = sender, Prefab = prefab, Boss = boss, Id = id, Position = pos, Until = Time.unscaledTime + 3f }); } } private static void Drain() { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_015d: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || BossKillCredit.pending.Count == 0) { return; } if (!NativeSaga.Active("powers")) { BossKillCredit.pending.Clear(); return; } float unscaledTime = Time.unscaledTime; for (int num = BossKillCredit.pending.Count - 1; num >= 0; num--) { Pending pending = BossKillCredit.pending[num]; if (seen.Contains(pending.Id) || unscaledTime >= pending.Until) { BossKillCredit.pending.RemoveAt(num); } else { ZDO val = ((ZDOMan.instance != null) ? ZDOMan.instance.GetZDO(pending.Id) : null); if (val == null || !val.IsValid()) { if (BossKillRules.ServerAccepts(true, NativePowers.IsKnownBoss(pending.Boss), true, true)) { seen.Add(pending.Id); Broadcast(pending.Boss, pending.Id, pending.Position); } BossKillCredit.pending.RemoveAt(num); } else { bool flag = val.GetOwner() == pending.Sender; bool flag2 = val.GetPrefab() == pending.Prefab && NativePowers.IsKnownBoss(pending.Boss); bool flag3 = val.GetFloat(ZDOVars.s_health, 1f) <= 0f; if (BossKillRules.ServerAccepts(flag, flag2, flag3, true)) { seen.Add(pending.Id); Broadcast(pending.Boss, pending.Id, pending.Position); BossKillCredit.pending.RemoveAt(num); } else if (!flag || !flag2) { BossKillCredit.pending.RemoveAt(num); } } } } } private static void Forget(ZDOID id) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) for (int num = pending.Count - 1; num >= 0; num--) { if (((ZDOID)(ref pending[num].Id)).Equals(id)) { pending.RemoveAt(num); } } } private static void Broadcast(string boss, ZDOID id, Vector3 pos) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_01c3: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01cf: 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_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_01fe: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Expected O, but got Unknown //IL_020f: 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_010c: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) ZRoutedRpc instance = ZRoutedRpc.instance; if (instance == null || string.IsNullOrEmpty(boss)) { return; } float num = ((Plugin.Settings != null) ? Plugin.Settings.Get<float>(Powers.KillCreditRange) : Powers.KillCreditRange.Default); List<CreditCandidate> list = new List<CreditCandidate>(); Dictionary<long, long> dictionary = new Dictionary<long, long>(); HashSet<long> hashSet = new HashSet<long>(); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { Vector3 position = ((Component)localPlayer).transform.position; list.Add(new CreditCandidate(localPlayer.GetPlayerID(), position.x, position.y, position.z, !((Character)localPlayer).IsDead())); dictionary[localPlayer.GetPlayerID()] = ZNet.GetUID(); hashSet.Add(localPlayer.GetPlayerID()); } if ((Object)(object)ZNet.instance != (Object)null && ZDOMan.instance != null) { foreach (PlayerInfo player in ZNet.instance.GetPlayerList()) { ZDOID characterID = player.m_characterID; if (((ZDOID)(ref characterID)).IsNone()) { continue; } ZDO zDO = ZDOMan.instance.GetZDO(player.m_characterID); if (zDO == null || !zDO.IsValid()) { continue; } long num2 = zDO.GetLong(ZDOVars.s_playerID, 0L); if (num2 != 0L && hashSet.Add(num2)) { Vector3 position2 = zDO.GetPosition(); bool flag = zDO.GetFloat(ZDOVars.s_health, 1f) > 0f; list.Add(new CreditCandidate(num2, position2.x, position2.y, position2.z, flag)); long owner = zDO.GetOwner(); if (owner != 0L) { dictionary[num2] = owner; } } } } foreach (long item in KillCredit.Eligible(pos.x, pos.y, pos.z, num, (IEnumerable<CreditCandidate>)list)) { if (dictionary.TryGetValue(item, out var value) && value != 0L) { ZPackage val = new ZPackage(); val.Write(boss); val.Write(id); val.Write(pos); instance.InvokeRoutedRPC(value, "saga.BossKill", new object[1] { val }); } } } private static void OnCredit(long sender, ZPackage pkg) { //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_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0030: 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) try { string boss = pkg.ReadString(); ZDOID id = pkg.ReadZDOID(); Vector3 pos = pkg.ReadVector3(); bool flag = NativePowers.IsKnownBoss(boss); bool flag2 = !NativePowers.AlreadyCredited(id); if (BossKillRules.ClientAccepts(sender == ServerPeer(), flag, InRange(pos), flag2)) { NativePowers.ApplyKill(boss, id); } } catch (Exception error) { NativeSaga.Report(error, "boss credit"); } } private static bool InRange(Vector3 pos) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: 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_0042: 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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || ((Character)localPlayer).IsDead() || Plugin.Settings == null) { return false; } Vector3 position = ((Component)localPlayer).transform.position; float num = Plugin.Settings.Get<float>(Powers.KillCreditRange); return KillCredit.Eligible(pos.x, pos.y, pos.z, num, (IEnumerable<CreditCandidate>)(object)new CreditCandidate[1] { new CreditCandidate(localPlayer.GetPlayerID(), position.x, position.y, position.z, true) }).Count > 0; } private static long ServerPeer() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return 0L; } if (instance.IsServer()) { return ZNet.GetUID(); } return instance.GetServerPeer()?.m_uid ?? 0; } } public static class NativePowers { private sealed class HudRow { public Boss Boss; public GameObject Root; public TMP_Text Title; public TMP_Text? Details; public Image Icon; public bool IconOnly; } [HarmonyPatch(typeof(Player), "GetGuardianPowerHUD")] private static class GuardianHud { private static bool Prefix(Player __instance, ref StatusEffect se, ref float cooldown) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance != (Object)(object)Player.m_localPlayer || !NativeSaga.Active("powers")) { return true; } Attunement val = Attuned(__instance); cooldown = __instance.m_guardianPowerCooldown; se = ((val.Attuned.Count == 0) ? null : (((Character)__instance).GetSEMan().GetStatusEffect(PowerHash(val.Attuned.Last())) ?? PowerSource(val.Attuned.Last()))); return false; } } [HarmonyPatch(typeof(Hud), "UpdateGuardianPower")] private static class GuardianLabel { private static void Postfix(Hud __instance, Player player) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.AllowsUi || (Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer || !NativeSaga.Active("powers") || ((Character)player).IsDead()) { NativeNameVisible(visible: true); RestoreNativeTooltip(); return; } Attunement val = Attuned(player); if (val.Attuned.Count == 0) { NativeNameVisible(visible: true); RestoreNativeTooltip(); return; } Boss boss = val.Attuned.Last(); if ((Object)(object)__instance.m_gpName != (Object)null) { __instance.m_gpName.text = BossLabel(boss); ((Behaviour)__instance.m_gpName).enabled = !foldNativeName; } if ((Object)(object)__instance.m_gpCooldown != (Object)null) { __instance.m_gpCooldown.text = ((player.m_guardianPowerCooldown > 0f) ? PowerRecharge.TimeLeft(player.m_guardianPowerCooldown) : MasteryText(player, boss)); } NativeTooltip(__instance, player, boss); } } [HarmonyPatch(typeof(Character), "OnDeath")] private static class Died { private static void Prefix(Character __instance) { BossKillCredit.Report(__instance); } } [HarmonyPatch(typeof(ItemStand), "Interact")] private static class Stone { private static bool Prefix(ItemStand __instance, Humanoid user, bool hold, ref bool __result) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0071: 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_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Invalid comparison between Unknown and I4 //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Invalid comparison between Unknown and I4 //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) if (!(!NativeSaga.Active("powers") || hold)) { Player val = (Player)(object)((user is Player) ? user : null); if (val != null && !((Object)(object)val != (Object)(object)Player.m_localPlayer) && __instance.HaveAttachment()) { StatusEffect guardianPower = __instance.m_guardianPower; if (Power(((guardianPower != null) ? ((Object)guardianPower).name : null) ?? "", out var boss)) { AttunePreview val2 = Attuned(val).Preview(boss, Plugin.Settings.Get<int>(Powers.MaxAttunedPowers)); Save(val, Attuned(val).Use(boss, Plugin.Settings.Get<int>(Powers.MaxAttunedPowers))); val.SetGuardianPower(((Object)__instance.m_guardianPower).name); ((Character)val).Message((MessageType)2, ((int)((AttunePreview)(ref val2)).Kind == 2) ? SagaText.Format("power_release", BossLabel(boss)) : (((int)((AttunePreview)(ref val2)).Kind == 1) ? SagaText.Format("power_replace", BossLabel(((AttunePreview)(ref val2)).Replaced.Value), BossLabel(boss)) : SagaText.Format("power_attune", BossLabel(boss))), 0, (Sprite)null, false); next = 0f; __result = true; return false; } } } return true; } } [HarmonyPatch(typeof(ItemStand), "GetHoverText")] private static class Hover { private static void Postfix(ItemStand __instance, ref string __result) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Invalid comparison between Unknown and I4 //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Invalid comparison between Unknown and I4 //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && NativeSaga.Active("powers") && __instance.HaveAttachment()) { StatusEffect guardianPower = __instance.m_guardianPower; if (Power(((guardianPower != null) ? ((Object)guardianPower).name : null) ?? "", out var boss)) { AttunePreview val = Attuned(localPlayer).Preview(boss, Plugin.Settings.Get<int>(Powers.MaxAttunedPowers)); __result = __result + "\n" + (((int)((AttunePreview)(ref val)).Kind == 2) ? SagaText.Format("power_release", BossLabel(boss)) : (((int)((AttunePreview)(ref val)).Kind == 1) ? SagaText.Format("power_replace", BossLabel(((AttunePreview)(ref val)).Replaced.Value), BossLabel(boss)) : SagaText.Format("power_attune", BossLabel(boss)))) + "\n" + Tooltip(localPlayer, boss); } } } } [HarmonyPatch(typeof(OfferingBowl), "Interact")] private static class KallAltar { private static bool Prefix(OfferingBowl __instance, Humanoid user, bool hold, bool alt, ref bool __result) { if (!(!alt || hold)) { Player val = (Player)(object)((user is Player) ? user : null); if (val != null && !((Object)(object)val != (Object)(object)Player.m_localPlayer) && NativeSaga.Active("powers") && IsKallAltar(__instance)) { __result = AttuneAtAltar(val); return false; } } return true; } } [HarmonyPatch(typeof(OfferingBowl), "GetHoverText")] private static class KallAltarHover { private static void Postfix(OfferingBowl __instance, ref string __result) { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Invalid comparison between Unknown and I4 //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Invalid comparison between Unknown and I4 //IL_00de: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && NativeSaga.Active("powers") && IsKallAltar(__instance)) { if (!KallUnlocked(localPlayer)) { __result = __result + "\n" + SagaText.Format("power_altar_locked", BossLabel((Boss)7)); return; } AttunePreview val = Attuned(localPlayer).Preview((Boss)7, Plugin.Settings.Get<int>(Powers.MaxAttunedPowers)); __result = __result + "\n" + Localization.instance.Localize("[<color=yellow><b>$KEY_AltPlace + $KEY_Use</b></color>] ") + (((int)((AttunePreview)(ref val)).Kind == 2) ? SagaText.Format("power_release", BossLabel((Boss)7)) : (((int)((AttunePreview)(ref val)).Kind == 1) ? SagaText.Format("power_replace", BossLabel(((AttunePreview)(ref val)).Replaced.Value), BossLabel((Boss)7)) : SagaText.Format("power_attune", BossLabel((Boss)7)))) + "\n" + Tooltip(localPlayer, (Boss)7); } } } [HarmonyPatch(typeof(Player), "StartGuardianPower")] private static class BlockStart { private static bool Prefix(Player __instance, ref bool __result) { if (!OwnPowers(__instance)) { return true; } if (((Character)__instance).IsDead() || Plugin.Settings.Get<bool>(Powers.BlockPowerActivation)) { __result = false; return false; } StatusEffect[] array = ActivationSources(__instance); if (array.Length == 0) { __result = false; return false; } __instance.SetGuardianPower(((Object)array[0]).name); return true; } } [HarmonyPatch(typeof(Player), "ActivateGuardianPower")] private s