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 Inscribe v0.2.8
plugins/Inscribe/Inscribe.Core.dll
Decompiled a day 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.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using ExtraPotions.Inscribe.Config; using ExtraPotions.Inscribe.Rarity; using ExtraPotions.Inscribe.Runes; 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.2.8.0")] [assembly: AssemblyInformationalVersion("0.2.8+source.31CDC68E1D064A47")] [assembly: AssemblyProduct("Inscribe.Core")] [assembly: AssemblyTitle("Inscribe.Core")] [assembly: AssemblyVersion("0.2.8.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.Inscribe { public static class InscribeCore { public const string PluginGuid = "com.meadworks.inscribe"; public const string ProductName = "Inscribe"; public const string ProductVersion = "0.2.8"; public const string Family = "Fang"; public const string TokenNamespace = "inscribe"; public const string TokenPrefix = "meadworks_inscribe_"; public const string SupportedGameVersion = "1.0.17"; } public interface IDice { float Value(); } public sealed class FixedDice : IDice { private readonly float[] values; private int next; public FixedDice(params float[] values) { this.values = ((values == null || values.Length == 0) ? new float[1] : values); } public float Value() { float result = values[(next < values.Length) ? next : (values.Length - 1)]; next++; return result; } } } namespace ExtraPotions.Inscribe.Runes { public sealed class BiomeEssence { public string Id { get; } public string Biome { get; } public string IconItem { get; } public (float R, float G, float B) Color { get; } public IReadOnlyList<string> Inscriptions { get; } public string TextKey => "essence_" + Id; public BiomeEssence(string id, string biome, string iconItem, float r, float g, float b, params string[] inscriptions) { Id = id; Biome = biome; IconItem = iconItem; Color = (R: r, G: g, B: b); Inscriptions = inscriptions; } } public static class BiomeEssences { public const int OriginalCount = 5; public static readonly string[] OriginalIds = new string[5] { "beast", "stone", "elemental", "spirit", "grave" }; public static readonly BiomeEssence[] All = new BiomeEssence[3] { new BiomeEssence("mist", "Mistlands", "Carapace", 0.56f, 0.78f, 0.86f, "mistbane", "huginns_eye", "windstep", "ravens_glide", "spring_heeled", "light_leap", "soft_landing", "mountain_goat", "marshstrider", "fleetfoot", "fleet", "stride", "momentum", "evaders_fury", "nimble", "elusive", "ghostwalk", "shadowmeld", "soft_tread"), new BiomeEssence("ash", "Ashlands", "CharredBone", 1f, 0.55f, 0.32f, "battle_rush", "blood_up", "lone_blade", "steel_rhythm", "godslayer", "unshaken", "skirmisher", "easy_draw", "thrifty_quiver", "throwing_grip", "returning", "bifrost_step"), new BiomeEssence("frost", "DeepNorth", "FrostCore", 0.82f, 0.93f, 1f, "hearthbound", "hearthlight", "hearthglow", "oilskin", "sealegs", "hearty_appetite", "well_fed", "deep_rest", "gourmand", "long_brew", "potent_brew", "bottomless_flask", "brewers_haste", "swift_draught", "steady_breath", "hardy", "hale", "warding", "purifying", "strong_swimmer") }; public static int Offered(bool biomeEssences) { return 5 + (biomeEssences ? All.Length : 0); } public static bool IsOffered(int index, bool biomeEssences) { if (index >= 0) { return index < Offered(biomeEssences); } return false; } } public static class BossPhases { private static readonly HashSet<string> Earlier = new HashSet<string>(StringComparer.Ordinal) { "FrozenKing", "FrozenKing_p2" }; public static IReadOnlyCollection<string> EarlierPhases => Earlier; public static bool IsEarlierPhase(string? prefab) { if (prefab != null) { return Earlier.Contains(prefab); } return false; } } public enum RecastFocus { Unrestricted, Offense, Defense, Utility } public enum RecastSide { Prefix, Suffix } public enum RuneVerb { Promote, Add, Strip, Corrupt, Reroll, Hone } public enum SerpentOutcome { SealOnly, AddInscription, ChaoticReroll } public readonly struct OutcomeWeight { public SerpentOutcome Outcome { get; } public float Weight { get; } public OutcomeWeight(SerpentOutcome outcome, float weight) { Outcome = outcome; Weight = weight; } } public sealed class RuneDef { public string Id { get; } public string Prefab { get; } public RuneVerb Verb { get; } public IReadOnlyList<string> AppliesTo { get; } public bool Confirm { get; } public int Overflow { get; } public IReadOnlyList<OutcomeWeight> Outcomes { get; } public IReadOnlyDictionary<string, int> Cost { get; } public bool Enabled { get; } public int Stack { get; } public float ItemWeight { get; } public RuneDef(string id, RuneVerb verb, IReadOnlyList<string> appliesTo, bool confirm = false, int overflow = 1, IReadOnlyList<OutcomeWeight>? outcomes = null, IReadOnlyDictionary<string, int>? cost = null, bool enabled = true, int stack = 50, float itemWeight = 0.2f) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentException("Expected a rune id.", "id"); } Id = id; Prefab = PrefabOf(id); Verb = verb; AppliesTo = appliesTo?.ToArray() ?? Array.Empty<string>(); Confirm = confirm; Overflow = Math.Max(0, overflow); Outcomes = outcomes?.ToArray() ?? Array.Empty<OutcomeWeight>(); Cost = cost ?? new Dictionary<string, int>(); Enabled = enabled; Stack = stack; ItemWeight = itemWeight; } public static string PrefabOf(string id) { return string.Concat("ECF_", string.Concat(from part in id.Split(new char[1] { '_' }, StringSplitOptions.RemoveEmptyEntries) select char.ToUpperInvariant(part[0]) + part.Substring(1))); } public bool AppliesToRarity(string rarityId) { return AppliesTo.Contains(rarityId); } public int CostFor(string rarityId) { if (!Cost.TryGetValue(rarityId, out var value)) { return 1; } return Math.Max(value, 0); } public RuneDef WithEnabled(bool enabled) { return new RuneDef(Id, Verb, AppliesTo, Confirm, Overflow, Outcomes, Cost, enabled, Stack, ItemWeight); } public override string ToString() { return Id; } } public sealed class RuneCatalog { private readonly RuneDef[] runes; public static RuneCatalog Default { get; } = new RuneCatalog(new RuneDef[5] { new RuneDef("ascension", RuneVerb.Promote, new string[3] { "normal", "magic", "rare" }), new RuneDef("recasting", RuneVerb.Reroll, new string[1] { "magic" }), new RuneDef("honing", RuneVerb.Hone, new string[4] { "magic", "rare", "mw_epic", "mw_legendary" }), new RuneDef("cleansing", RuneVerb.Strip, new string[4] { "magic", "rare", "mw_epic", "mw_legendary" }, confirm: true), new RuneDef("serpent", RuneVerb.Corrupt, new string[4] { "magic", "rare", "mw_epic", "mw_legendary" }, confirm: true, 1, new OutcomeWeight[3] { new OutcomeWeight(SerpentOutcome.SealOnly, 35f), new OutcomeWeight(SerpentOutcome.AddInscription, 35f), new OutcomeWeight(SerpentOutcome.ChaoticReroll, 30f) }) }); public IReadOnlyList<RuneDef> Runes => runes; public RuneCatalog(IEnumerable<RuneDef> runes) { this.runes = runes.ToArray(); if (this.runes.Select((RuneDef r) => r.Id).Distinct<string>(StringComparer.Ordinal).Count() != this.runes.Length) { throw new ArgumentException("Duplicate rune id.", "runes"); } } public RuneDef? Find(string? id) { if (id != null) { return runes.FirstOrDefault((RuneDef r) => r.Id == id); } return null; } public RuneDef? ByPrefab(string? prefab) { if (prefab != null) { return runes.FirstOrDefault((RuneDef r) => r.Prefab == prefab); } return null; } } public static class LegacyRunes { public static readonly IReadOnlyDictionary<string, string> Successors = new Dictionary<string, string>(StringComparer.Ordinal) { ["awakening"] = "ascension", ["tempering"] = "honing" }; private static readonly Dictionary<string, string> PrefabSuccessors = Successors.ToDictionary<KeyValuePair<string, string>, string, string>((KeyValuePair<string, string> p) => RuneDef.PrefabOf(p.Key), (KeyValuePair<string, string> p) => RuneDef.PrefabOf(p.Value), StringComparer.Ordinal); public static IEnumerable<string> Ids => Successors.Keys; public static bool IsLegacy(string? id) { if (id != null) { return Successors.ContainsKey(id); } return false; } public static string? SuccessorOf(string? id) { if (id == null || !Successors.TryGetValue(id, out string value)) { return null; } return value; } public static string? SuccessorPrefab(string? prefab) { if (prefab == null || !PrefabSuccessors.TryGetValue(prefab, out string value)) { return null; } return value; } public static int MoveCounts(IDictionary<string, int> counts) { if (counts == null) { throw new ArgumentNullException("counts"); } int num = 0; foreach (KeyValuePair<string, string> successor in Successors) { if (counts.TryGetValue(successor.Key, out var value) && value > 0) { counts.TryGetValue(successor.Value, out var value2); counts[successor.Value] = value2 + value; counts[successor.Key] = 0; num += value; } } return num; } } public static class Weighted { public static int Pick(IReadOnlyList<float> weights, IDice dice) { double num = 0.0; for (int i = 0; i < weights.Count; i++) { num += (double)Math.Max(weights[i], 0f); } if (num <= 0.0) { return -1; } double num2 = (double)dice.Value() * num; int result = -1; for (int j = 0; j < weights.Count; j++) { float num3 = Math.Max(weights[j], 0f); if (!(num3 <= 0f)) { result = j; if (num2 < (double)num3) { return j; } num2 -= (double)num3; } } return result; } } public interface IInscriptionSource { IReadOnlyList<Inscription>? Add(ItemState state, RarityDef rarity, int count, int overflow); IReadOnlyList<Inscription>? Fresh(ItemState state, RarityDef rarity); IReadOnlyList<Inscription>? Chaotic(ItemState state, RarityDef rarity); } public sealed class RuneJob { public RuneDef? Rune { get; } public ItemState State { get; set; } public bool TargetInOwnInventory { get; set; } public bool RuneWithinReach { get; set; } public bool IsMagicBase { get; set; } public bool IsEquipped { get; set; } public bool ModifyEquippedItems { get; set; } = true; public int RuneStack { get; set; } public RuneJob(RuneDef? rune, ItemState state) { Rune = rune; State = state ?? throw new ArgumentNullException("state"); } } public sealed class RuneResult { public string? Refusal { get; } public string? Feedback { get; } public ItemState? State { get; } public int Cost { get; } public IReadOnlyList<string> Words { get; } public bool Refused => Refusal != null; private RuneResult(string? refusal, string? feedback, ItemState? state, int cost, string[] words) { Refusal = refusal; Feedback = feedback; State = state; Cost = cost; Words = words; } public static RuneResult Refuse(string id, params string[] words) { return new RuneResult(id, null, null, 0, words); } public static RuneResult Success(ItemState state, int cost, string feedback, params string[] words) { return new RuneResult(null, feedback, state, cost, words); } } public static class RunePipeline { public static RuneResult Evaluate(RuneJob job, RarityLadder ladder, IInscriptionSource source, IDice dice) { if (job == null) { throw new ArgumentNullException("job"); } if (ladder == null) { throw new ArgumentNullException("ladder"); } if (source == null) { throw new ArgumentNullException("source"); } if (dice == null) { throw new ArgumentNullException("dice"); } if (!job.TargetInOwnInventory) { return RuneResult.Refuse("not_own_inventory"); } if (!job.RuneWithinReach) { return RuneResult.Refuse("stone_out_of_reach"); } if (!job.IsMagicBase) { return RuneResult.Refuse("not_magic_base"); } if (job.State.IsNewerFormat) { return RuneResult.Refuse("newer_format"); } RuneDef rune = job.Rune; if (rune == null || !rune.Enabled) { return RuneResult.Refuse("stone_disabled", rune?.Id ?? ""); } if (job.State.IsSealed) { return RuneResult.Refuse("sealed"); } if (job.IsEquipped && !job.ModifyEquippedItems) { return RuneResult.Refuse("equipped"); } RarityDef rarityDef = ladder.Of(job.State); if (rarityDef == null) { return RuneResult.Refuse("unknown_rarity"); } if (rune.Verb == RuneVerb.Promote && AtTop(ladder, rarityDef)) { return RuneResult.Refuse("ascension_top", rarityDef.Id); } if (!rune.AppliesToRarity(rarityDef.Id)) { return RuneResult.Refuse("wrong_rarity", rune.Id, rarityDef.Id); } int num = rune.CostFor(rarityDef.Id); if (job.RuneStack < num) { return RuneResult.Refuse("not_enough_stones", num.ToString(CultureInfo.InvariantCulture), rune.Id); } return Verb(job, rune, rarityDef, num, ladder, source, dice); } private static RuneResult Verb(RuneJob job, RuneDef rune, RarityDef rarity, int cost, RarityLadder ladder, IInscriptionSource source, IDice dice) { ItemState state = job.State; switch (rune.Verb) { case RuneVerb.Promote: { RarityDef rarityDef = ladder.Next(rarity); if (rarityDef == null || rarityDef.SourceOnly) { return RuneResult.Refuse("ascension_top", rarity.Id); } ItemState itemState = state.WithRarity(rarityDef.Id); int num = ladder.PromotionAdds(state.Count, rarityDef); IReadOnlyList<Inscription> readOnlyList3; if (num != 0) { readOnlyList3 = source.Add(itemState, rarityDef, num, 0); } else { IReadOnlyList<Inscription> readOnlyList4 = Array.Empty<Inscription>(); readOnlyList3 = readOnlyList4; } IReadOnlyList<Inscription> readOnlyList5 = readOnlyList3; if (readOnlyList5 != null) { return RuneResult.Success(itemState.Adding(readOnlyList5), cost, "promoted", rarityDef.Id); } return RuneResult.Refuse("no_eligible_affix"); } case RuneVerb.Add: { if (state.Count >= rarity.MaxInscriptions) { return RuneResult.Refuse("affixes_full"); } IReadOnlyList<Inscription> readOnlyList2 = source.Add(state, rarity, 1, 0); if (readOnlyList2 != null) { return RuneResult.Success(state.Adding(readOnlyList2), cost, "affix_added", readOnlyList2[0].Id); } return RuneResult.Refuse("no_eligible_affix"); } case RuneVerb.Reroll: { if (rarity.IsBase) { return RuneResult.Refuse("wrong_rarity", rune.Id, rarity.Id); } IReadOnlyList<Inscription> readOnlyList = source.Fresh(state, rarity); if (readOnlyList != null) { return RuneResult.Success(state.WithInscriptions(readOnlyList), cost, "rerolled"); } return RuneResult.Refuse("no_eligible_affix"); } case RuneVerb.Strip: return RuneResult.Success(new ItemState(null, null, null, state.IsNewerFormat), cost, "stripped"); case RuneVerb.Corrupt: return Corrupt(state, rune, rarity, cost, source, dice); default: return RuneResult.Refuse("stone_disabled", rune.Id); } } public static bool AtTop(RarityLadder ladder, RarityDef rarity) { if (rarity.IsBase) { return false; } return ladder.Next(rarity)?.SourceOnly ?? true; } private static RuneResult Corrupt(ItemState state, RuneDef rune, RarityDef rarity, int cost, IInscriptionSource source, IDice dice) { int num = Weighted.Pick(rune.Outcomes.Select((OutcomeWeight o) => o.Weight).ToList(), dice); SerpentOutcome serpentOutcome = ((num >= 0) ? rune.Outcomes[num].Outcome : SerpentOutcome.SealOnly); if (serpentOutcome == SerpentOutcome.AddInscription && !rarity.IsBase && state.Count < rarity.MaxInscriptions + rune.Overflow) { IReadOnlyList<Inscription> readOnlyList = source.Add(state, rarity, 1, rune.Overflow); if (readOnlyList != null) { return RuneResult.Success(state.Adding(readOnlyList).Sealed("serpent"), cost, "corrupt_add", readOnlyList[0].Id); } } if (serpentOutcome == SerpentOutcome.ChaoticReroll) { IReadOnlyList<Inscription> readOnlyList2 = source.Chaotic(state, rarity); if (readOnlyList2 != null && readOnlyList2.Count > 0) { return RuneResult.Success(state.WithInscriptions(readOnlyList2).Sealed("serpent"), cost, "corrupt_chaos"); } } return RuneResult.Success(state.Sealed("serpent"), cost, "corrupt_seal"); } } public enum ConfirmDecision { Commit, AskDialog, RefuseConfirmRequired } public static class RuneConfirm { public static ConfirmDecision Decide(bool runeConfirms, ConfirmMode mode, bool shiftHeld, bool padInput, bool dialogAvailable) { if (!runeConfirms || mode == ConfirmMode.Off) { return ConfirmDecision.Commit; } bool flag = mode == ConfirmMode.Dialog || (mode == ConfirmMode.HoldShift && padInput && !shiftHeld); if (!flag && shiftHeld) { return ConfirmDecision.Commit; } if (flag && dialogAvailable) { return ConfirmDecision.AskDialog; } if (!shiftHeld) { return ConfirmDecision.RefuseConfirmRequired; } return ConfirmDecision.Commit; } } public enum SocketBase { None, Weapon, Staff, Armour } public static class SocketStones { public const string ChiselId = "dvergr_chisel"; public const string BackpackClass = "backpack"; public const string PickaxeClass = "pickaxe"; public const int MaxSockets = 3; public const float BossGemChance = 50f; public const float BossChiselChance = 25f; private static readonly Dictionary<string, (string? Weapon, string? Staff, string? Armour)> Stats = new Dictionary<string, (string, string, string)>(StringComparer.Ordinal) { ["gem_surtr"] = ("emberbrand", "primal_fury", "flameward"), ["gem_ymir"] = ("rimebrand", "seidr_thrift", "frostward"), ["gem_thor"] = ("stormbrand", "thors_chain", "stormward"), ["gem_nidhogg"] = ("venombrand", "lingering_wounds", "venomward"), ["gem_hel"] = ("spiritbrand", "soul_reaper", "spiritward"), ["gem_tyr"] = ("honed_might", "grave_command", "resolute"), ["gem_freyja"] = ("blood_drinker", "blood_thrift", "vigor"), ["gem_odin"] = ("seidr_siphon", "seidr_flow", "wellspring"), ["gem_skadi"] = ("wind_siphon", "grave_vigor", "endurance"), ["gem_heimdall"] = ("keen_eye", "swift_casting", "mist_veil"), ["gem_sleipnir"] = (null, null, "fleetfoot") }; private static readonly float[] MagicDrop = new float[4] { 80f, 15f, 4f, 1f }; private static readonly float[] RareDrop = new float[4] { 60f, 25f, 11f, 4f }; public static IReadOnlyList<string> GemIds { get; } = new string[11] { "gem_surtr", "gem_ymir", "gem_thor", "gem_nidhogg", "gem_hel", "gem_tyr", "gem_freyja", "gem_odin", "gem_skadi", "gem_heimdall", "gem_sleipnir" }; public static IReadOnlyList<string> Bosses { get; } = new string[8] { "Eikthyr", "gd_king", "Bonemass", "Dragon", "GoblinKing", "SeekerQueen", "Fader", "FrozenKing_p3" }; public static bool IsGem(string? id) { if (id != null) { return Stats.ContainsKey(id); } return false; } public static bool IsSocketStone(string? id) { if (!(id == "dvergr_chisel")) { return IsGem(id); } return true; } public static SocketBase BaseOf(string? group, string? classId) { switch (group) { case "onehand": case "twohand": case "ranged": return SocketBase.Weapon; case "magic": return SocketBase.Staff; case "armour": return SocketBase.Armour; case "offhand": if (!(classId == "light")) { return SocketBase.Armour; } return SocketBase.None; case "jewel": if (!(classId == "backpack")) { return SocketBase.None; } return SocketBase.Armour; case "tools": if (!(classId == "pickaxe")) { return SocketBase.None; } return SocketBase.Weapon; default: return SocketBase.None; } } public static string? StatFor(string gemId, SocketBase socketBase) { if (!Stats.TryGetValue(gemId, out (string, string, string) value)) { return null; } return socketBase switch { SocketBase.Armour => value.Item3, SocketBase.Staff => value.Item2, SocketBase.Weapon => value.Item1, _ => null, }; } public static float[] DropWeights(string? rarityId) { return ((rarityId == "rare") ? RareDrop : MagicDrop).ToArray(); } public static double CutChance(string? rarityId, int sockets) { float[] array = CutWeights(rarityId); if (sockets >= 1 && sockets <= 3) { return 100.0 * (double)array[sockets - 1] / (double)array.Sum(); } return 0.0; } public static int Cut(string? rarityId, IDice dice) { return 1 + Math.Max(0, Weighted.Pick(CutWeights(rarityId), dice)); } public static int BossGemRolls(int stars) { return 1 + Math.Max(0, stars); } private static float[] CutWeights(string? rarityId) { return DropWeights(rarityId).Skip(1).ToArray(); } } public static class SalvageRune { public const float Chance = 0.25f; public static string? RuneFor(RuneCatalog runes, RarityLadder ladder, string? rarityId) { RarityDef rarity = ladder.Find(rarityId); if (rarity == null || rarity.Index == 0) { return null; } RarityDef below = ladder.Rarities.LastOrDefault((RarityDef r) => r.Index < rarity.Index); if (below == null) { return null; } return runes.Runes.FirstOrDefault((RuneDef r) => r.Enabled && r.Verb == RuneVerb.Promote && r.AppliesToRarity(below.Id))?.Id; } public static bool Rolls(IDice dice) { return dice.Value() < 0.25f; } } public static class TargetedRules { public static bool CanHone(bool exists, float weight, int unlockLevel, int shown, int itemLevel, int closed) { if (exists && weight > 0f && unlockLevel <= itemLevel) { return shown > closed; } return false; } } } namespace ExtraPotions.Inscribe.Rarity { public enum InscriptionKind { Prefix, Suffix } public sealed class Inscription { public string Id { get; } public InscriptionKind Kind { get; } public int Grade { get; } public float Value { get; } public bool Dormant { get; } public Inscription(string id, InscriptionKind kind, int grade, float value, bool dormant = false) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentException("Expected an inscription id.", "id"); } Id = id; Kind = kind; Grade = grade; Value = value; Dormant = dormant; } public override string ToString() { return Id; } } public sealed class ItemState { public static ItemState Normal { get; } = new ItemState(null, null, null); public string? RarityId { get; } public IReadOnlyList<Inscription> Inscriptions { get; } public string? SealedReason { get; } public bool IsSealed => SealedReason != null; public bool IsNewerFormat { get; } public int Count => Inscriptions.Count; public ItemState(string? rarityId, IReadOnlyList<Inscription>? inscriptions, string? sealedReason, bool newerFormat = false) { RarityId = (string.IsNullOrEmpty(rarityId) ? null : rarityId); Inscriptions = inscriptions?.ToArray() ?? Array.Empty<Inscription>(); SealedReason = (string.IsNullOrEmpty(sealedReason) ? null : sealedReason); IsNewerFormat = newerFormat; } public ItemState WithRarity(string? rarityId) { return new ItemState(rarityId, Inscriptions, SealedReason, IsNewerFormat); } public ItemState WithInscriptions(IEnumerable<Inscription> inscriptions) { return new ItemState(RarityId, inscriptions.ToArray(), SealedReason, IsNewerFormat); } public ItemState Adding(IEnumerable<Inscription> added) { return WithInscriptions(Inscriptions.Concat(added)); } public ItemState Sealed(string reason) { return new ItemState(RarityId, Inscriptions, reason, IsNewerFormat); } } public sealed class InscriptionLimits { private readonly int maxPrefixes; private readonly int maxSuffixes; private int prefixes; private int suffixes; private InscriptionLimits(int maxPrefixes, int maxSuffixes) { this.maxPrefixes = maxPrefixes; this.maxSuffixes = maxSuffixes; } public static InscriptionLimits For(ItemState state, RarityDef? rarity, int overflow) { overflow = Math.Max(overflow, 0); InscriptionLimits inscriptionLimits = ((rarity == null) ? new InscriptionLimits(1073741823, 1073741823) : new InscriptionLimits(rarity.MaxPrefixes + overflow, rarity.MaxSuffixes + overflow)); foreach (Inscription inscription in state.Inscriptions) { inscriptionLimits.Count(inscription.Kind); } return inscriptionLimits; } public bool Allows(InscriptionKind kind) { if (kind != InscriptionKind.Prefix) { return suffixes < maxSuffixes; } return prefixes < maxPrefixes; } public void Count(InscriptionKind kind) { if (kind == InscriptionKind.Prefix) { prefixes++; } else { suffixes++; } } } public static class MagicBase { public static bool Is(int maxStack, bool classRolls, bool isRune, bool vetoed) { if (maxStack == 1 && classRolls && !isRune) { return !vetoed; } return false; } public static IReadOnlyList<string> StackableWarnings(IEnumerable<(string Prefab, int MaxStack, bool ClassRolls)> items) { return (from i in items where i.MaxStack > 1 && i.ClassRolls select i.Prefab).ToList(); } } public sealed class RarityDef { public int TierFloor { get; } public bool SourceOnly { get; } public string Id { get; } public int Index { get; } public string Color { get; } public bool Glow { get; } public int MinInscriptions { get; } public int MaxInscriptions { get; } public int MaxPrefixes { get; } public int MaxSuffixes { get; } public float DropWeight { get; } public bool IsBase => Index == 0; public RarityDef(string id, int index, string color, bool glow, int minInscriptions, int maxInscriptions, int? maxPrefixes = null, int? maxSuffixes = null, float dropWeight = 1f, int tierFloor = 0, bool sourceOnly = false) { if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentException("Expected a rarity id.", "id"); } if (minInscriptions < 0 || maxInscriptions < minInscriptions) { throw new ArgumentException(id + ": expected 0 <= min <= max.", "minInscriptions"); } Id = id; Index = index; Color = color; Glow = index != 0 && glow; MinInscriptions = minInscriptions; MaxInscriptions = maxInscriptions; MaxPrefixes = maxPrefixes ?? maxInscriptions; MaxSuffixes = maxSuffixes ?? maxInscriptions; DropWeight = Math.Max(0f, dropWeight); TierFloor = Math.Max(0, tierFloor); SourceOnly = sourceOnly; } public int Limit(InscriptionKind kind) { if (kind != InscriptionKind.Prefix) { return MaxSuffixes; } return MaxPrefixes; } public override string ToString() { return Id; } } public sealed class RollingSettings { public float AllowedClosedFraction { get; } public int PromoteAddsAtLeast { get; } public IReadOnlyDictionary<string, IReadOnlyDictionary<int, float>> CountWeights { get; } public RollingSettings(float allowedClosedFraction = 0.334f, int promoteAddsAtLeast = 1, IReadOnlyDictionary<string, IReadOnlyDictionary<int, float>>? countWeights = null) { AllowedClosedFraction = allowedClosedFraction; PromoteAddsAtLeast = Math.Max(0, promoteAddsAtLeast); CountWeights = countWeights ?? new Dictionary<string, IReadOnlyDictionary<int, float>>(); } } public sealed class RarityLadder { private readonly RarityDef[] rarities; public static RarityLadder Default { get; } = new RarityLadder(new RarityDef[5] { new RarityDef("normal", 0, "#FFFFFF", glow: false, 0, 0, null, null, 0f), new RarityDef("magic", 1, "#1EFF00", glow: true, 2, 2, 1, 1), new RarityDef("rare", 2, "#0070DD", glow: true, 3, 3, 2, 2), new RarityDef("mw_epic", 3, "#A335EE", glow: true, 4, 4, 2, 2, 1f, 4), new RarityDef("mw_legendary", 4, "#FFB400", glow: true, 5, 5, 3, 3, 0f, 2, sourceOnly: true) }, new RollingSettings()); public IReadOnlyList<RarityDef> Rarities => rarities; public RollingSettings Rolling { get; } public RarityDef Base => rarities[0]; public RarityLadder(IEnumerable<RarityDef> rarities, RollingSettings rolling) { this.rarities = (rarities ?? throw new ArgumentNullException("rarities")).OrderBy((RarityDef r) => r.Index).ToArray(); if (this.rarities.Length == 0 || this.rarities[0].Index != 0) { throw new ArgumentException("A ladder starts with its base rarity at index 0.", "rarities"); } if (this.rarities.Select((RarityDef r) => r.Id).Distinct<string>(StringComparer.Ordinal).Count() != this.rarities.Length) { throw new ArgumentException("Duplicate rarity id.", "rarities"); } Rolling = rolling ?? throw new ArgumentNullException("rolling"); } public RarityDef? Find(string? id) { if (id != null) { return rarities.FirstOrDefault((RarityDef r) => r.Id == id); } return null; } public RarityDef? Of(ItemState state) { if (state.RarityId != null) { return Find(state.RarityId); } return Base; } public RarityDef? Next(RarityDef rarity) { int num = Array.IndexOf(rarities, rarity); if (num < 0 || num + 1 >= rarities.Length) { return null; } return rarities[num + 1]; } public int PromotionAdds(int count, RarityDef to) { return Math.Min(Math.Max(to.MinInscriptions - count, Rolling.PromoteAddsAtLeast), Math.Max(to.MaxInscriptions - count, 0)); } } public static class RarityTiers { public const string Epic = "mw_epic"; public const string Legendary = "mw_legendary"; public static readonly string[] Ids = new string[5] { "normal", "magic", "rare", "mw_epic", "mw_legendary" }; private static readonly float[] UncommonSockets = new float[4] { 80f, 15f, 4f, 1f }; private static readonly float[] RareSockets = new float[4] { 60f, 25f, 11f, 4f }; private static readonly float[] EpicSockets = new float[4] { 35f, 35f, 22f, 8f }; private static readonly float[] LegendarySockets = new float[4] { 0f, 40f, 40f, 20f }; public static float[] SocketWeights(string? rarityId) { return rarityId switch { "rare" => RareSockets, "mw_epic" => EpicSockets, "mw_legendary" => LegendarySockets, _ => UncommonSockets, }; } } public static class LegendarySources { public const string Boss = "boss"; public const string Raid = "raid"; public const string Capability = "meadworks.inscribe.legendary_reward"; public static readonly Version Version = new Version(1, 0); public static bool Allows(string? source, bool bosses, bool raids) { if (!string.Equals(source, "boss", StringComparison.Ordinal)) { return string.Equals(source, "raid", StringComparison.Ordinal) && raids; } return bosses; } public static bool Rolls(float percent, double unit) { if (percent > 0f) { return unit * 100.0 < (double)percent; } return false; } } public static class GemRecovery { public static readonly float[] Defaults = new float[5] { 100f, 90f, 75f, 50f, 25f }; public static int TierOf(string? rarityId) { if (rarityId == null) { return 0; } int num = Array.IndexOf<string>(RarityTiers.Ids, rarityId); if (num < 0) { return RarityTiers.Ids.Length - 1; } return num; } public static float PercentFor(string? rarityId, float[] percents) { return Math.Max(0f, Math.Min(100f, percents[Math.Min(TierOf(rarityId), percents.Length - 1)])); } public static bool Returns(float percent, double unit) { if (!(percent >= 100f)) { if (percent > 0f) { return unit * 100.0 < (double)percent; } return false; } return true; } } } namespace ExtraPotions.Inscribe.Localization { public readonly struct TextKey { public string Key { get; } public int Arguments { get; } public TextKey(string key, int arguments) { Key = key; Arguments = arguments; } public override string ToString() { return Key; } } public static class InscribeTextKeys { public const string OffNoCrucible = "off_no_crucible"; public const string OffNoInscribe = "off_no_inscribe"; public const string ImportDone = "import_done"; public const string ImportSyncIncomplete = "import_sync_incomplete"; public const string ImportRecoveryIncomplete = "import_recovery_incomplete"; public const string CheatItemCommands = "cheat_item_commands"; public const string StackableGear = "stackable_gear"; public const string EnhancementConverted = "enhancement_converted"; public const string ScrollsConverted = "scrolls_converted"; public const string ConversionNoRoom = "enhancement_convert_no_room"; public const string ConversionDropped = "enhancement_convert_dropped"; internal static readonly TextKey[] Fixed = new TextKey[35] { new TextKey("off_no_crucible", 0), new TextKey("off_no_inscribe", 0), new TextKey("import_done", 2), new TextKey("import_sync_incomplete", 0), new TextKey("import_recovery_incomplete", 0), new TextKey("cheat_item_commands", 0), new TextKey("stackable_gear", 2), new TextKey("recast_focus_label", 0), new TextKey("recast_focus_any", 0), new TextKey("recast_focus_offense", 0), new TextKey("recast_focus_defense", 0), new TextKey("recast_focus_utility", 0), new TextKey("recast_focus_help", 0), new TextKey("recast_side_prefix", 0), new TextKey("recast_side_suffix", 0), new TextKey("recast_side_help", 0), new TextKey("step_aside_rarity", 1), new TextKey("step_aside_enhancement", 1), new TextKey("enhancement_converted", 4), new TextKey("scrolls_converted", 2), new TextKey("enhancement_convert_no_room", 0), new TextKey("enhancement_convert_dropped", 0), new TextKey("step_aside_all", 0), new TextKey("confirm_title", 1), new TextKey("confirm_body", 2), new TextKey("tooltip_sealed", 0), new TextKey("tooltip_dormant", 0), new TextKey("tooltip_item_level", 1), new TextKey("tooltip_quality", 1), new TextKey("tooltip_inscriptions", 1), new TextKey("tooltip_sockets", 2), new TextKey("gem_salvage_lost", 1), new TextKey("gem_replace_chance", 1), new TextKey("pick_inscription_title", 1), new TextKey("pick_inscription_body", 2) }; internal static readonly (string Id, int Arguments)[] RuneMessages = new(string, int)[26] { ("not_own_inventory", 0), ("stone_out_of_reach", 0), ("not_magic_base", 0), ("newer_format", 0), ("stone_disabled", 1), ("sealed", 0), ("equipped", 0), ("unknown_rarity", 0), ("wrong_rarity", 2), ("not_enough_stones", 2), ("no_eligible_affix", 0), ("affixes_full", 0), ("confirm_required", 1), ("affix_at_top", 0), ("affix_is_flag", 0), ("gem_swap_kept", 4), ("gem_swap_lost", 4), ("honed", 2), ("ascension_top", 1), ("promoted", 2), ("affix_added", 2), ("rerolled", 1), ("stripped", 1), ("corrupt_seal", 1), ("corrupt_add", 2), ("corrupt_chaos", 1) }; public static readonly string[] MeadworksAffixes = new string[11] { "unshaken", "steady_breath", "hardy", "elusive", "warding", "hale", "fleet", "hearthglow", "purifying", "gleaner", "seafarer" }; public static readonly string[] SettingTexts = BuildSettingTexts(); public static IReadOnlyList<TextKey> All { get; } = Fixed.Concat(RarityLadder.Default.Rarities.Select((RarityDef r) => new TextKey(RarityName(r.Id), 0))).Concat(RuneCatalog.Default.Runes.SelectMany((RuneDef r) => new TextKey[2] { new TextKey(RuneName(r.Id), 0), new TextKey(RuneDescription(r.Id), 0) })).Concat(LegacyRunes.Ids.SelectMany((string id) => new TextKey[2] { new TextKey(RuneName(id), 0), new TextKey(RuneDescription(id), 0) })) .Concat(RuneMessages.Select(((string Id, int Arguments) m) => new TextKey(RuneMessage(m.Id), m.Arguments))) .Concat(Enumerable.Range(1, 4).SelectMany((int t) => new TextKey[2] { new TextKey(ScrollName(t), 0), new TextKey(ScrollDescription(t), 0) })) .Concat(SettingTexts.Select((string k) => new TextKey(k, 0))) .Concat(BiomeEssences.OriginalIds.Select((string id) => new TextKey("essence_" + id, 0))) .Concat(BiomeEssences.All.Select((BiomeEssence e) => new TextKey(e.TextKey, 0))) .Concat(MeadworksAffixes.SelectMany((string a) => new TextKey[3] { new TextKey(AffixName(a), 0), new TextKey(AffixLine(a), 1), new TextKey(AffixShort(a), 1) })) .ToArray(); public static string RarityName(string rarityId) { return "rarity_" + rarityId; } public static string RuneName(string runeId) { return "rune_" + runeId; } public static string RuneDescription(string runeId) { return "rune_" + runeId + "_desc"; } public static string RuneMessage(string id) { return "rune_msg_" + id; } public static string ScrollName(int tier) { return "scroll_tier" + tier; } public static string ScrollDescription(int tier) { return "scroll_tier" + tier + "_desc"; } public static string AffixName(string id) { return "affix_" + id; } public static string AffixLine(string id) { return "affix_" + id + "_line"; } public static string AffixShort(string id) { return "affix_" + id + "_short"; } public static string SettingSlug(string text) { return Regex.Replace(text.ToLowerInvariant(), "[^a-z0-9]+", "_").Trim('_'); } private static string[] BuildSettingTexts() { List<string> list = new List<string>(); foreach (InscribeSetting item in InscribeSettings.All) { string text = "cfg_" + SettingSlug(item.Section); if (!list.Contains(text + "_heading")) { list.Add(text + "_heading"); } list.Add(text + "__" + SettingSlug(item.Key) + "_name"); list.Add(text + "__" + SettingSlug(item.Key) + "_help"); } return list.ToArray(); } } } namespace ExtraPotions.Inscribe.Items { public static class IconBadges { public static bool Glint(bool enabled, string? rarityId) { if (enabled) { return string.Equals(rarityId, "mw_legendary", StringComparison.Ordinal); } return false; } } public static class ItemKeys { public const string EcPrefix = "ecf_"; public const string EcVersion = "ecf_v"; public const string EcRarity = "ecf_rarity"; public const string EcInscriptions = "ecf_inscriptions"; public const string EcLegacyInscriptions = "ecf_affixes"; public const string EcSealed = "ecf_sealed"; public const string EcTier = "ecf_tier"; public const string EcChestFilled = "ecf_filled"; public const int EcCurrentFormat = 2; public const string EcSealedSerpent = "serpent"; public const string EnhancementLevel = "ValheimEnhancement_Level"; public const string EnhancementCrafter = "ValheimEnhancement_Crafter"; public const string EnhancementConverted = "ecf_enhancement_converted"; public static IReadOnlyList<string> EcRetired { get; } = new string[6] { "ecf_bound", "ecf_refine", "ecf_sigil", "ecf_sockets", "ecf_gems", "ecf_catalyst" }; } public static class UpgradeData { public static Dictionary<string, string>? Capture(IReadOnlyDictionary<string, string> source) { Dictionary<string, string> dictionary = null; foreach (KeyValuePair<string, string> item in source) { if (item.Key.StartsWith("ecf_", StringComparison.Ordinal) || !(item.Key != "ValheimEnhancement_Level") || !(item.Key != "ValheimEnhancement_Crafter")) { if (dictionary == null) { dictionary = new Dictionary<string, string>(StringComparer.Ordinal); } dictionary.Add(item.Key, item.Value); } } return dictionary; } } } namespace ExtraPotions.Inscribe.Interop { public static class DecorationV1 { public const string Capability = "meadworks.items.decoration"; public static readonly Version Version = new Version(1, 0); public static Type ContractType => typeof(Tuple<Func<object, string>, Func<object, string>, Func<object, Tuple<string, string, float, string, string, string>[]>>); } public static class CapabilityIds { public const string ItemClasses = "meadworks.items.classes"; public const string Decoration = "meadworks.items.decoration"; public static readonly Version Version1 = new Version(1, 0); public static IReadOnlyList<string> All { get; } = new string[6] { "meadworks.items.classes", "meadworks.items.decoration", "meadworks.items.sockets", "meadworks.crafting.salvage_rune", "meadworks.crafting.salvage_gems", "meadworks.inscribe.legendary_reward" }; } public static class ItemClassesV1 { public static Type ContractType { get; } = typeof(Tuple<Func<string, string, bool>, Func<string, int, bool>, Func<string, bool>>); public static Tuple<Func<string, string, bool>, Func<string, int, bool>, Func<string, bool>> Create(ItemRegistry registry) { if (registry == null) { throw new ArgumentNullException("registry"); } return Tuple.Create<Func<string, string, bool>, Func<string, int, bool>, Func<string, bool>>((string cls, string prefab) => Guard(() => registry.ClaimItem(cls, prefab)), (string prefab, int level) => Guard(() => registry.SetItemLevel(prefab, level)), (string json) => Guard(() => registry.SetCreatureLoot(json))); } private static bool Guard(Func<bool> call) { try { return call(); } catch (Exception) { return false; } } } public sealed class ItemRegistry { public const int MaxLevel = 8; private readonly HashSet<string> classIds; private readonly Dictionary<string, string> claims = new Dictionary<string, string>(StringComparer.Ordinal); private readonly Dictionary<string, int> levels = new Dictionary<string, int>(StringComparer.Ordinal); private readonly Dictionary<string, LootProfile> loot = new Dictionary<string, LootProfile>(StringComparer.Ordinal); public static IReadOnlyList<string> DefaultClassIds { get; } = new string[34] { "unused", "throwable", "tankard", "practice", "cosmetic", "helmet", "chest", "legs", "cape", "trinket", "utility", "buckler", "tower", "shield", "light", "tool", "fishing", "pickaxe", "crossbow", "bow", "staff_elemental", "staff_blood", "knife", "spear", "atgeir", "fist", "greatsword", "sword_1h", "dual_axe", "battleaxe", "axe_1h", "sledge", "mace_1h", "backpack" }; public IReadOnlyDictionary<string, LootProfile> Loot => loot; public IReadOnlyList<string> LastWarnings { get; private set; } = Array.Empty<string>(); public IReadOnlyList<string> LastErrors { get; private set; } = Array.Empty<string>(); public event Action? Changed; public ItemRegistry(IEnumerable<string>? classIds = null) { this.classIds = new HashSet<string>(classIds ?? DefaultClassIds, StringComparer.Ordinal); } public string? ClassOf(string prefab) { if (prefab == null || !claims.TryGetValue(prefab, out string value)) { return null; } return value; } public int LevelOf(string prefab) { if (prefab == null || !levels.TryGetValue(prefab, out var value)) { return 0; } return value; } public bool ClaimItem(string? classId, string? prefab) { if (string.IsNullOrEmpty(classId) || string.IsNullOrEmpty(prefab) || !classIds.Contains(classId)) { return Refuse("ClaimItems: unknown class '" + classId + "' or no prefab"); } claims[prefab] = classId; return Accept(); } public bool SetItemLevel(string? prefab, int level) { if (string.IsNullOrEmpty(prefab) || level < 1 || level > 8) { return Refuse("SetItemLevel: '" + prefab + "' " + level + " refused (a prefab name and a level from 1 to 8)"); } levels[prefab] = level; return Accept(); } public bool SetCreatureLoot(string? json) { IReadOnlyList<string> errors; IReadOnlyList<string> warnings; LootProfile lootProfile = LootProfile.Parse(json, out errors, out warnings); LastWarnings = warnings; if (lootProfile == null) { LastErrors = errors; return false; } loot[lootProfile.Prefab] = lootProfile; return Accept(); } private bool Refuse(string error) { LastErrors = new string[1] { error }; return false; } private bool Accept() { LastErrors = Array.Empty<string>(); this.Changed?.Invoke(); return true; } } public sealed class LootBonus { public string Rune { get; } public float Chance { get; } public int Amount { get; } internal LootBonus(string rune, float chance, int amount) { Rune = rune; Chance = chance; Amount = amount; } } public sealed class LootProfile { public const int Tiers = 8; private static readonly string[] Keys = new string[11] { "prefab", "boss", "tier", "multiplier", "rune_multiplier", "gear_multiplier", "rune_rolls", "gear_rolls", "bonus", "scroll_rolls", "scroll_multiplier" }; private static readonly Dictionary<string, string> Successors = new Dictionary<string, string>(StringComparer.Ordinal) { ["shaping"] = "recasting", ["consecrated"] = "ascension" }; public string Prefab { get; private set; } = ""; public bool Boss { get; private set; } public int Tier { get; private set; } public float Multiplier { get; private set; } = 1f; public float RuneMultiplier { get; private set; } = 1f; public float GearMultiplier { get; private set; } = 1f; public float ScrollMultiplier { get; private set; } = 1f; public int RuneRolls { get; private set; } public int GearRolls { get; private set; } public int ScrollRolls { get; private set; } public IReadOnlyList<LootBonus> Bonus { get; private set; } = Array.Empty<LootBonus>(); private LootProfile() { } public static LootProfile? Parse(string? json, out IReadOnlyList<string> errors, out IReadOnlyList<string> warnings) { List<string> list = new List<string>(); List<string> list2 = new List<string>(); errors = list; warnings = list2; if (string.IsNullOrWhiteSpace(json)) { list.Add("no definition given"); return null; } object obj; try { obj = MiniJson.Parse(json); } catch (FormatException ex) { list.Add("the definition does not parse: " + ex.Message); return null; } if (!(obj is Dictionary<string, object> dictionary)) { list.Add("the definition should be a JSON object"); return null; } foreach (string item in dictionary.Keys.Where((string k) => Array.IndexOf(Keys, k) < 0)) { list2.Add(item + ": unknown key, ignored"); } LootProfile lootProfile = new LootProfile(); if (!(Get(dictionary, "prefab") is string { Length: not 0 } text)) { list.Add("prefab: is required: the creature's prefab name"); return null; } lootProfile.Prefab = text; lootProfile.Boss = Bool(dictionary, "boss", fallback: false, list); lootProfile.Tier = Int(dictionary, "tier", lootProfile.Boss ? 1 : 0, 1, 8, list); lootProfile.Multiplier = Float(dictionary, "multiplier", 1f, 0f, float.MaxValue, list); lootProfile.RuneMultiplier = Float(dictionary, "rune_multiplier", 1f, 0f, float.MaxValue, list); lootProfile.GearMultiplier = Float(dictionary, "gear_multiplier", 1f, 0f, float.MaxValue, list); lootProfile.ScrollMultiplier = Float(dictionary, "scroll_multiplier", 1f, 0f, float.MaxValue, list); int runeRolls = Int(dictionary, "rune_rolls", 0, 0, int.MaxValue, list); int gearRolls = Int(dictionary, "gear_rolls", 0, 0, int.MaxValue, list); lootProfile.ScrollRolls = Int(dictionary, "scroll_rolls", 0, 0, int.MaxValue, list); if (lootProfile.Boss) { lootProfile.RuneRolls = runeRolls; lootProfile.GearRolls = gearRolls; } else if (dictionary.ContainsKey("rune_rolls") || dictionary.ContainsKey("gear_rolls")) { list2.Add("rune_rolls: rune_rolls and gear_rolls are for bosses (\"boss\": true); ignored"); } lootProfile.Bonus = Bonuses(dictionary, list, list2); if (list.Count != 0) { return null; } return lootProfile; } private static IReadOnlyList<LootBonus> Bonuses(Dictionary<string, object?> map, List<string> e, List<string> w) { object obj = Get(map, "bonus"); if (obj == null) { return Array.Empty<LootBonus>(); } if (!(obj is List<object> list)) { e.Add("bonus: should be a list of { rune, chance, amount }"); return Array.Empty<LootBonus>(); } List<LootBonus> list2 = new List<LootBonus>(); foreach (object item in list) { if (!(item is Dictionary<string, object> dictionary)) { e.Add("bonus: a bonus row looks like { rune: ascension, chance: 100, amount: 1 }"); continue; } foreach (string item2 in dictionary.Keys.Where((string k) => k != "rune" && k != "chance" && k != "amount")) { w.Add("bonus." + item2 + ": unknown key, ignored"); } string text = Get(dictionary, "rune") as string; if (text != null && Successors.TryGetValue(text, out string value)) { w.Add("bonus: '" + text + "' was removed in EliteCrafting " + ((text == "shaping") ? "0.8.0" : "0.9.0") + "; paying '" + value + "' instead"); text = value; } else { string text2 = LegacyRunes.SuccessorOf(text); if (text2 != null) { w.Add("bonus: '" + text + "' was folded into '" + text2 + "'; paying '" + text2 + "' instead"); text = text2; } } if (text == null || (RuneCatalog.Default.Find(text) == null && !SocketStones.IsSocketStone(text))) { e.Add("bonus: '" + text + "' is not a rune, chisel or gem id (" + string.Join(", ", RuneCatalog.Default.Runes.Select((RuneDef r) => r.Id)) + ", dvergr_chisel, gem_*)"); } else { list2.Add(new LootBonus(text, Float(dictionary, "chance", 100f, 0f, 100f, e), Int(dictionary, "amount", 1, 1, int.MaxValue, e))); } } return list2; } private static object? Get(Dictionary<string, object?> map, string key) { if (!map.TryGetValue(key, out object value)) { return null; } return value; } private static bool Bool(Dictionary<string, object?> map, string key, bool fallback, List<string> e) { if (!map.ContainsKey(key)) { return fallback; } object obj = map[key]; if (obj is bool) { return (bool)obj; } e.Add(key + ": should be true or false"); return fallback; } private static int Int(Dictionary<string, object?> map, string key, int fallback, int min, int max, List<string> e) { if (!map.ContainsKey(key)) { return fallback; } if (map[key] is double num && num == Math.Floor(num) && num >= (double)min && num <= (double)max) { return (int)num; } e.Add(key + ": should be a whole number from " + min + ((max == int.MaxValue) ? " up" : (" to " + max))); return fallback; } private static float Float(Dictionary<string, object?> map, string key, float fallback, float min, float max, List<string> e) { if (!map.ContainsKey(key)) { return fallback; } if (map[key] is double num && num >= (double)min && num <= (double)max) { return (float)num; } e.Add(key + ": should be a number from " + min + ((max == float.MaxValue) ? " up" : (" to " + max))); return fallback; } } public static class MiniJson { public static object? Parse(string text) { if (text == null) { throw new FormatException("No JSON text."); } int i = 0; object? result = Value(text, ref i, 0); Skip(text, ref i); if (i != text.Length) { throw new FormatException("Unexpected text at " + i + "."); } return result; } private static object? Value(string s, ref int i, int depth) { if (depth > 32) { throw new FormatException("Nesting too deep."); } Skip(s, ref i); if (i >= s.Length) { throw new FormatException("Unexpected end."); } char c = s[i]; switch (c) { case '{': return Object(s, ref i, depth); case '[': return Array(s, ref i, depth); case '"': return String(s, ref i); case '-': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return Number(s, ref i); default: if (Literal(s, ref i, "true")) { return true; } if (Literal(s, ref i, "false")) { return false; } if (Literal(s, ref i, "null")) { return null; } throw new FormatException("Unexpected '" + c + "' at " + i + "."); } } private static Dictionary<string, object?> Object(string s, ref int i, int depth) { Dictionary<string, object> dictionary = new Dictionary<string, object>(StringComparer.Ordinal); i++; Skip(s, ref i); if (i < s.Length && s[i] == '}') { i++; return dictionary; } while (true) { Skip(s, ref i); if (i >= s.Length || s[i] != '"') { throw new FormatException("Expected a key at " + i + "."); } string key = String(s, ref i); Skip(s, ref i); if (i >= s.Length || s[i] != ':') { throw new FormatException("Expected ':' at " + i + "."); } i++; dictionary[key] = Value(s, ref i, depth + 1); Skip(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == '}') { i++; return dictionary; } throw new FormatException("Expected ',' or '}' at " + i + "."); } private static List<object?> Array(string s, ref int i, int depth) { List<object> list = new List<object>(); i++; Skip(s, ref i); if (i < s.Length && s[i] == ']') { i++; return list; } while (true) { list.Add(Value(s, ref i, depth + 1)); Skip(s, ref i); if (i >= s.Length || s[i] != ',') { break; } i++; } if (i < s.Length && s[i] == ']') { i++; return list; } throw new FormatException("Expected ',' or ']' at " + i + "."); } private static string String(string s, ref int i) { StringBuilder stringBuilder = new StringBuilder(); i++; while (i < s.Length) { char c = s[i++]; if (c == '"') { return stringBuilder.ToString(); } if (c < ' ') { throw new FormatException("Control character in a string."); } if (c != '\\') { stringBuilder.Append(c); continue; } if (i >= s.Length) { break; } char c2 = s[i++]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'u': { if (i + 4 > s.Length || !int.TryParse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { throw new FormatException("Bad \\u escape."); } stringBuilder.Append((char)result); i += 4; break; } default: throw new FormatException("Bad escape \\" + c2 + "."); } } throw new FormatException("Unterminated string."); } private static double Number(string s, ref int i) { int num = i; if (s[i] == '-') { i++; } while (i < s.Length && (char.IsDigit(s[i]) || s[i] == '.' || s[i] == 'e' || s[i] == 'E' || s[i] == '+' || s[i] == '-')) { i++; } if (!double.TryParse(s.Substring(num, i - num), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || double.IsInfinity(result)) { throw new FormatException("Bad number at " + num + "."); } return result; } private static bool Literal(string s, ref int i, string word) { if (string.CompareOrdinal(s, i, word, 0, word.Length) != 0) { return false; } i += word.Length; return true; } private static void Skip(string s, ref int i) { while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) { i++; } } } public static class SecondaryCooldown { public const string Capability = "meadworks.inscribe.secondary_cooldown"; public static readonly Version Version = new Version(1, 0); public const float MinMultiplier = 0.5f; public static float Multiplier(float attackSpeed, float castSpeed) { float num = Clean(attackSpeed) + Clean(castSpeed); if (!(num <= 0f)) { return Math.Max(0.5f, 1f / (1f + num)); } return 1f; } private static float Clean(float share) { if (!float.IsNaN(share) && !float.IsInfinity(share)) { return Math.Max(0f, share); } return 0f; } } public static class SocketsV1 { public const string Capability = "meadworks.items.sockets"; public static readonly Version Version = new Version(1, 0); public static Type ContractType => typeof(Tuple<Func<object, int>, Func<object, Tuple<string, string, string, string>[]>>); } public static class SalvageRuneV1 { public const string Capability = "meadworks.crafting.salvage_rune"; public static readonly Version Version = new Version(1, 0); public static Type ContractType => typeof(Tuple<Func<object, string>, Func<object, float>>); } public static class SalvageGemsV1 { public const string Capability = "meadworks.crafting.salvage_gems"; public static readonly Version Version = new Version(1, 0); public static Type ContractType => typeof(Tuple<Func<object, string[]>, Func<object, float>, Func<int, string>>); } } namespace ExtraPotions.Inscribe.Integration { public enum InscribeModule { Rarity, Enhancement, Drops, Display, Cheats } public sealed class ModuleSource { public string Guid { get; } public string Name { get; } internal ModuleSource(string guid, string name) { Guid = guid; Name = name; } } public readonly struct StepAsideNotice { public string Key { get; } public string SourceNames { get; } public string SourceGuid { get; } public string ModuleId => Key.Substring("step_aside_".Length); public StepAsideNotice(string key, string sourceNames, string sourceGuid) { Key = key; SourceNames = sourceNames; SourceGuid = sourceGuid; } } public sealed class StepAsideDecision { public bool EliteCrafting { get; } public bool ItemEnhancements { get; } public bool ProductOff { get { if (EliteCrafting) { return ItemEnhancements; } return false; } } public bool Any { get { if (!EliteCrafting) { return ItemEnhancements; } return true; } } public bool RuneAndGearDrops => !EliteCrafting; public bool RarityDisplay => !EliteCrafting; public bool RunePrefabs => !EliteCrafting; public bool ScrollPrefabs => !ItemEnhancements; public bool StatPatchesAfterItemEnhancements { get { if (ItemEnhancements) { return !EliteCrafting; } return false; } } public IReadOnlyList<StepAsideNotice> Notices { get; } internal StepAsideDecision(bool eliteCrafting, bool itemEnhancements) { EliteCrafting = eliteCrafting; ItemEnhancements = itemEnhancements; List<StepAsideNotice> list = new List<StepAsideNotice>(); if (ProductOff) { list.Add(new StepAsideNotice("step_aside_all", ModuleStepAside.EliteCrafting.Name + " and " + ModuleStepAside.ItemEnhancements.Name, ModuleStepAside.EliteCrafting.Guid)); } else if (eliteCrafting) { list.Add(new StepAsideNotice("step_aside_rarity", ModuleStepAside.EliteCrafting.Name, ModuleStepAside.EliteCrafting.Guid)); } else if (itemEnhancements) { list.Add(new StepAsideNotice("step_aside_enhancement", ModuleStepAside.ItemEnhancements.Name, ModuleStepAside.ItemEnhancements.Guid)); } Notices = list; } public bool SteppedAside(InscribeModule module) { if (ProductOff) { return true; } return module switch { InscribeModule.Rarity => EliteCrafting, InscribeModule.Enhancement => ItemEnhancements, _ => false, }; } } public static class ModuleStepAside { public static readonly ModuleSource EliteCrafting = new ModuleSource("com.EliteCrafting", "EliteCrafting"); public static readonly ModuleSource ItemEnhancements = new ModuleSource("ckforgame.ItemEnhancements", "ItemEnhancements"); public static IReadOnlyList<ModuleSource> Sources { get; } = Array.AsReadOnly(new ModuleSource[2] { EliteCrafting, ItemEnhancements }); public static StepAsideDecision Decide(IEnumerable<string> installedGuids) { if (installedGuids == null) { throw new ArgumentNullException("installedGuids"); } HashSet<string> hashSet = new HashSet<string>(installedGuids.Where((string g) => g != null), StringComparer.Ordinal); return new StepAsideDecision(hashSet.Contains(EliteCrafting.Guid), hashSet.Contains(ItemEnhancements.Guid)); } } } namespace ExtraPotions.Inscribe.Enhancement { public readonly struct TierRange { public int Grade { get; } public float Min { get; } public float Max { get; } public int Decimals { get; } public TierRange(int grade, float min, float max, int decimals) { Grade = grade; Min = Math.Min(min, max); Max = Math.Max(min, max); Decimals = Math.Max(0, Math.Min(decimals, 6)); } } public sealed class ConvertibleInscription { public TierRange Current { get; } public float Value { get; } public IReadOnlyList<TierRange> Higher { get; } public ConvertibleInscription(TierRange current, float value, IReadOnlyList<TierRange>? higher) { Current = current; Value = value; Higher = higher ?? Array.Empty<TierRange>(); } } public readonly struct RaisedInscription { public int Index { get; } public int Grade { get; } public float Value { get; } public RaisedInscription(int index, int grade, float value) { Index = index; Grade = grade; Value = value; } } public sealed class ItemConversion { public int Level { get; } public int Steps { get; } public IReadOnlyList<RaisedInscription> Raised { get; } public int TiersRaised { get; } public int HoningRunes { get; } public string Marker => "v1;level=" + Level.ToString(CultureInfo.InvariantCulture) + ";steps=" + Steps.ToString(CultureInfo.InvariantCulture) + ";raised=" + TiersRaised.ToString(CultureInfo.InvariantCulture) + ";runes=" + HoningRunes.ToString(CultureInfo.InvariantCulture); internal ItemConversion(int level, int steps, IReadOnlyList<RaisedInscription> raised, int tiersRaised, int honingRunes) { Level = level; Steps = steps; Raised = raised; TiersRaised = tiersRaised; HoningRunes = honingRunes; } } public readonly struct ScrollConversion { public int HoningRunes { get; } public int Essence { get; } public bool IsEmpty { get { if (HoningRunes == 0) { return Essence == 0; } return false; } } public ScrollConversion(int honingRunes, int essence) { HoningRunes = honingRunes; Essence = essence; } } public static class EnhancementConversion { public const int MaxLevel = 20; public const int ScrollTiers = 4; public const int NinthsPerHoningRune = 9; public const int EssencePerNinth = 2; public const string MarkerVersion = "v1"; public static IReadOnlyList<string> ScrollPrefabs { get; } = Array.AsReadOnly(new string[4] { "ScrollEnhance_Tier1", "ScrollEnhance_Tier2", "ScrollEnhance_Tier3", "ScrollEnhance_Tier4" }); public static int ScrollTier(string? prefab) { for (int i = 0; i < ScrollPrefabs.Count; i++) { if (string.Equals(prefab, ScrollPrefabs[i], StringComparison.Ordinal)) { return i + 1; } } return 0; } public static int Ninths(int tier) { if (tier >= 1 && tier <= 4) { return (int)Math.Pow(3.0, tier - 1); } return 0; } public static int TierForLevel(int level) { if (level > 5) { if (level > 10) { if (level > 15) { return 4; } return 3; } return 2; } return 1; } public static int ReadLevel(IReadOnlyDictionary<string, string>? data) { if (data == null || !data.TryGetValue("ValheimEnhancement_Level", out string value)) { return 0; } if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return 0; } return Math.Min(Math.Max(result, 0), 20); } public static bool IsConverted(IReadOnlyDictionary<string, string>? data) { if (data != null && data.TryGetValue("ecf_enhancement_converted", out string value)) { return !string.IsNullOrEmpty(value); } return false; } public static bool MayRun(bool moduleActive, bool authorityReady) { return moduleActive && authorityReady; } public static bool NeedsConversion(IReadOnlyDictionary<string, string>? data) { if (ReadLevel(data) > 0) { return !IsConverted(data); } return false; } public static int Steps(int level) { level = Math.Min(Math.Max(level, 0), 20); if (level == 0) { return 0; } int num = 0; for (int i = 1; i <= level; i++) { num += Ninths(TierForLevel(i)); } return Math.Max(1, (num + 9 - 1) / 9); } public static ItemConversion PlanItem(int level, IReadOnlyList<ConvertibleInscription?>? inscriptions) { level = Math.Min(Math.Max(level, 0), 20); int num = Steps(level); IReadOnlyList<ConvertibleInscription> readOnlyList = inscriptions ?? Array.Empty<ConvertibleInscription>(); int[] array = new int[readOnlyList.Count]; int num2 = num; bool flag = true; while (num2 > 0 && flag) { flag = false; for (int i = 0; i < readOnlyList.Count; i++) { if (num2 <= 0) { break; } if (readOnlyList[i] != null && array[i] < readOnlyList[i].Higher.Count) { array[i]++; num2--; flag = true; } } } List<RaisedInscription> list = new List<RaisedInscription>(); for (int j = 0; j < readOnlyList.Count; j++) { if (array[j] != 0) { ConvertibleInscription convertibleInscription = readOnlyList[j]; TierRange to = convertibleInscription.Higher[array[j] - 1]; list.Add(new RaisedInscription(j, to.Grade, Carry(convertibleInscription.Value, convertibleInscription.Current, to))); } } return new ItemConversion(level, num, list, num - num2, num2); } public static float Carry(float value, TierRange from, TierRange to) { double val = ((from.Max > from.Min) ? ((double)(value - from.Min) / (double)(from.Max - from.Min)) : 1.0); val = Math.Min(1.0, Math.Max(0.0, val)); return (float)Math.Min(Math.Max(Math.Max(Math.Round((double)to.Min + val * (double)(to.Max - to.Min), to.Decimals, MidpointRounding.AwayFromZero), value), to.Min), to.Max); } public static ScrollConversion PlanScrolls(IReadOnlyList<int>? counts) { long num = 0L; int num2 = 1; while (counts != null && num2 <= Math.Min(counts.Count, 4)) { num += (long)Math.Max(0, counts[num2 - 1]) * (long)Ninths(num2); num2++; } long val = num / 9; long num3 = num % 9 * 2; return new ScrollConversion((int)Math.Min(val, 2147483647L), (int)num3); } public static void Mark(IDictionary<string, string> data, ItemConversion conversion) { if (data == null) { throw new ArgumentNullException("data"); } if (conversion == null) { throw new ArgumentNullException("conversion"); } data["ecf_enhancement_converted"] = conversion.Marker; } public static (int Level, int Steps, int Raised, int Runes)? ReadMarker(IReadOnlyDictionary<string, string>? data) { if (data == null || !data.TryGetValue("ecf_enhancement_converted", out string value) || string.IsNullOrEmpty(value)) { return null; } string[] array = value.Split(';'); if (array.Length == 0 || array[0] != "v1") { return null; } int result; Dictionary<string, int> values = (from p in array.Skip(1) select p.Split('=') into p where p.Length == 2 select p).ToDictionary<string[], string, int>((string[] p) => p[0], (string[] p) => (!int.TryParse(p[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) ? (-1) : result, StringComparer.Ordinal); return (Get("level"), Get("steps"), Get("raised"), Get("runes")); int Get(string key) { if (!values.TryGetValue(key, out var value2)) { return -1; } return value2; } } } public sealed class OpenContainerConversion { private int? waiting; public int? Waiting => waiting; public bool Opened(int id, bool owned) { waiting = (owned ? ((int?)null) : new int?(id)); return owned; } public bool Frame(int? openId, bool owned) { if (!waiting.HasValue) { return false; } if (openId != waiting) { waiting = null; return false; } if (!owned) { return false; } waiting = null; return true; } public void Reset() { waiting = null; } } } namespace ExtraPotions.Inscribe.Conversion { public enum ImportSource { EliteCraftingConfig } public enum ImportNoteKind { Skipped, Unknown, Clamped, Fallback } public sealed class ImportNote { public ImportNoteKind Kind { get; } public string Source { get; } public string Text { get; } internal ImportNote(ImportNoteKind kind, string source, string text) { Kind = kind; Source = source; Text = text; } public override string ToString() { return Kind.ToString() + " " + Source + ": " + Text; } } public sealed class ImportValue { public string SettingId { get; } public object Value { get; } public string Source { get; } internal ImportValue(string settingId, object value, string source) { SettingId = settingId; Value = value; Source = source; } } public sealed class ImportPlan { public ImportSource Source { get; } public IReadOnlyList<ImportValue> Values { get; } public IReadOnlyList<ImportNote> Notes { get; } public string? LogOverride { get; } internal ImportPlan(ImportSource source, IReadOnlyList<ImportValue> values, IReadOnlyList<ImportNote> notes, string? logOverride) { Source = source; Values = values; Notes = notes; LogOverride = logOverride; } public string Report() { return string.Join("\n", new string[1] { "Source: " + Source }.Concat(Values.Select((ImportValue v) => "Set " + v.SettingId + " = " + Format(v.Value) + " (from " + v.Source + ")")).Concat((LogOverride == null) ? ((IEnumerable<string>)Array.Empty<string>()) : ((IEnumerable<string>)new string[1] { "Log override " + LogOverride })).Concat(Notes.Select((ImportNote n) => n.ToString()))); } internal static string Format(object value) { string text; if (!(value is float num)) { if (value is bool) { if (!(bool)value) { return "false"; } return "true"; } text = Convert.ToString(value, CultureInfo.InvariantCulture); if (text == null) { return ""; } } else { text = num.ToString("R", CultureInfo.InvariantCulture); } return text; } } public readonly struct SourceEntry { public string Section { get; } public string Key { get; } public string Value { get; } public SourceEntry(string section, string key, string value) { Section = section; Key = key; Value = value; } } public static class SourceConfig { public static IReadOnlyList<SourceEntry> Parse(string? text) { List<SourceEntry> list = new List<SourceEntry>(); string section = ""; string[] array = (text ?? "").Split('\n'); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0 || text2[0] == '#') { continue; } if (text2[0] == '[' && text2[text2.Length - 1] == ']') { section = text2.Substring(1, text2.Length - 2).Trim(); continue; } int num = text2.IndexOf('='); if (num > 0) { string text3 = text2.Substring(0, num).Trim(); if (text3.Length != 0) { list.Add(new SourceEntry(section, text3, text2.Substring(num + 1).Trim())); } } } return list; } } public readonly struct YamlCopy { public string From { get; } public string To { get; } public YamlCopy(string from, string to) { From = from; To = to; } } public static class SourceImport { private const string EcGeneral = "1 - General"; private const string EcRunes = "2 - Runes"; private const string EcDrops = "3 - Drops"; private const string EcCommands = "4 - Commands"; private const string EcDisplay = "5 - Display (per player)"; private const string EcGlow = "6 - Ground glow (per player)"; private const string EcDiagnostics = "7 - Diagnostics"; private const string EcEcr = "8 - Elite Creatures Reborn"; private static readonly Dictionary<string, InscribeSetting> Ec = new Dictionary<string, InscribeSetting>(StringComparer.Ordinal) { { "1 - General.Inscription effects", InscribeSettings.General.InscriptionEffects }, { "2 - Runes.Modify equipped items", InscribeSettings.Runes.ModifyEquippedItems }, { "2 - Runes.Confirm destructive runes", InscribeSettings.Runes.ConfirmDestructiveRunes }, { "3 - Drops.Rune drops", InscribeSettings.Drops.RuneDrops }, { "3 - Drops.Magic item drops", InscribeSettings.Drops.MagicItemDrops }, { "4 - Commands.Read-only commands for everyone", InscribeSettings.Commands.ReadOnlyForEveryone }, { "5 - Display (per player).Colored item names", InscribeSettings.Display.ColoredItemNames }, { "5 - Display (per player).Tooltip detail", InscribeSettings.Display.TooltipDetail }, { "5 - Display (per player).Show dormant inscriptions", InscribeSettings.Display.ShowDormant }, { "6 - Ground glow (per player).Ground glow", InscribeSettings.Glow.GroundGlow }, { "6 - Ground glow (per player).Glow intensity", InscribeSettings.Glow.Intensity }, { "6 - Ground glow (per player).Glow range", InscribeSettings.Glow.Range }, { "6 - Ground glow (per player).Glow max lights", InscribeSettings.Glow.MaxLights }, { "6 - Ground glow (per player).Glow refresh seconds", InscribeSettings.Glow.RefreshSeconds }, { "6 - Ground glow (per player).Glow runes", InscribeSettings.Glow.GlowRunes }, { "6 - Ground glow (per player).Loot beam", InscribeSettings.Glow.LootBeam }, { "8 - Elite Creatures Reborn.Synergy", InscribeSettings.Drops.WildbloodStarSynergy } }; private static readonly Dictionary<string, string> EcSkips = new Dictionary<string, string>(StringComparer.Ordinal) { { "1 - General.Lock Configuration", "Crucible decides who may change settings (C01)" }, { "2 - Runes.Runes from salvage", "EliteCrafting 0.8+ salvage runes are outside Inscribe's 0.7.0 gameplay" }, { "2 - Runes.Rune Table", "EliteCrafting 0.8+ Rune Table is outside Inscribe's 0.7.0 gameplay" }, { "2 - Runes.Gems and sockets", "EliteCrafting 0.8+ gems and sockets are outside Inscribe's 0.7.0 gameplay" } }; private static readonly Dictionary<string, int> BiomeTiers = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) { { "Meadows", 1 }, { "BlackForest", 2 }, { "Swamp", 3 }, { "Mountain", 4 }, { "Plains", 5 }, { "Mistlands", 6 }, { "AshLands", 7 }, { "DeepNorth", 8 }, { "Ocean", 4 } }; public static string FileName(ImportSource source) { if (source == ImportSource.EliteCraftingConfig) { return "com.EliteCrafting.cfg"; } throw new ArgumentException("Unknown source.", "source"); } public static int? BiomeTier(string biome) { if (biome == null || !BiomeTiers.TryGetValue(biome, out var value)) { return null; } return value; } public static ImportPlan Plan(ImportSource source, string? text) { if (source != ImportSource.EliteCraftingConfig) { throw new ArgumentException("Unknown source.", "source"); } List<ImportValue> values = new List<ImportValue>(); List<ImportNote> list = new List<ImportNote>(); Dictionary<string, InscribeSetting> ec = Ec; bool flag = false; bool flag2 = false; foreach (SourceEntry item in SourceConfig.Parse(text)) { string text2 = item.Section + "." + item.Key; string value; InscribeSetting value2; if (source == ImportSource.EliteCraftingConfig && item.Section == "7 - Diagnostics") { if (item.Key == "Log rolls") { flag = Eq(item.Value, "true"); } else if (item.Key == "Log effect rebuilds") { flag2 = Eq(item.Value, "true"); } else { list.Add(new ImportNote(ImportNoteKind.Unknown, text2, "not an Inscribe setting")); } } else if (source == ImportSource.EliteCraftingConfig && EcSkips.TryGetValue(text2, out value)) { list.Add(new ImportNote(ImportNoteKind.Skipped, text2, value)); } else if (!ec.TryGetValue(text2, out value2)) { list.Add(new ImportNote(ImportNoteKind.Unknown, text2, "not an Inscribe setting")); } else { Add(value2, item.Value, text2, values, list); } } string logOverride = (flag ? "inscribe=Verbose" : (flag2 ? "inscribe=Debug" : null)); return new ImportPlan(source, values, list, logOverride); } public static IReadOnlyList<YamlCopy> YamlCopies(IEnumerable<string> fileNames) { List<YamlCopy> list = new List<YamlCopy>(); foreach (string item in fileNames.OrderBy<string, string>((string n) => n, StringComparer.Ordinal)) { if (!item.EndsWith(".yml", StringComparison.Ordinal)) { continue; } string[] array = new string[2] { "economy", "inscriptions" }; foreach (string text in array) { string text2 = "EliteCrafting_" + text; if (item.StartsWith(text2, StringComparison.Ordinal)) { list.Add(new YamlCopy(item, "Inscribe." + text + item.Substring(text2.Length))); } } } return list; } public static string CopyHeader(string sourceName) { return "# Imported by Meadworks from " + sourceName + " (EliteCrafting 0.7.0 format 2); the source file is unchanged."; } private static void Add(InscribeSetting target, string text, string from, List<ImportValue> values, List<ImportNote> notes) { if (values.Any((ImportValue v) => v.SettingId == target.Id)) { notes.Add(new ImportNote(ImportNoteKind.Skipped, from, "merged into " + target.Id + " (first value kept)")); } else { values.Add(new ImportValue(target.Id, Convert(target, text, from, notes), from)); } } private static object Convert(InscribeSetting target, string text, string from, List<ImportNote> notes) { if (!(target is InscribeSetting<bool>)) { if (!(target is InscribeSetting<float>)) { if (!(target is InscribeSetting<int>)) { if (target is InscribeSetting<string>) { return Checked(text); } if (target.ValueType.IsEnum && target.Choices.Any((string c) => Eq(c, text.Replace(" ", "")))) { return Enum.Parse(target.ValueType, text.Replace(" ", ""), ignoreCase: true); } return Fallback("unknown value \"" + text + "\""); } if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return Checked(result); } if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result2) || (double)result2 != Math.Floor(result2) || !(Math.Abs(result2) < 2.1474836E+09f)) { return Fallback("unreadable \"" + text + "\""); } return Checked((int)result2); } if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result3) || float.IsNaN(result3) || float.IsInfinity(result3)) { return Fallback("unreadable \"" + text + "\""); } return Checked(result3); } if (Eq(text, "true")) { return true; } if (Eq(text, "false")) { return false; } return Fallback("unreadable \"" + text + "\""); object Checked(object value) { object obj = target.NormalizeBoxed(value); if (!object.Equals(obj, value)) { notes.Add(new ImportNote(ImportNoteKind.Clamped, from, ImportPlan.Format(value) + " changed to " + ImportPlan.Format(obj))); } return obj; } object Fallback(string why) { notes.Add(new ImportNote(ImportNoteKind.Fallback, from, why + "; Inscribe default " + ImportPlan.Format(target.DefaultValue))); return target.DefaultValue; } } private static bool Eq(string a, string b) { return string.Equals(a?.Trim(), b, StringComparison.OrdinalIgnoreCase); } } } namespace ExtraPotions.Inscribe.Config { public enum SettingScope { Local, ServerAuthoritative, Cheat } public abstract class InscribeSetting { private static readonly IReadOnlyList<string> NoChoices = Array.Empty<string>(); public string Section { get; } public string Key { get; } public string Id => Section + "." + Key; public SettingScope Scope { get; } public string Description { get; } public IReadOnlyList<string> Choices { get; } public abstract Type ValueType { get; } public abstract object DefaultValue { get; } public abstract object? SafeValue { get; } public abstract object? Minimum { get; } public abstract object? Maximum { get; } private protected InscribeSetting(string section, string key, SettingScope scope, string description, IReadOnlyList<string>? choices) { if (string.IsNullOrWhiteSpace(section) || section.Trim() != section) { throw new ArgumentException("Expected a trimmed section name.", "section"); } if (string.IsNullOrWhiteSpace(key) || key.Trim() != key) { throw new ArgumentException("Expected a trimmed key.", "key"); } if (!Enum.IsDefined(typeof(SettingScope), scope)) { throw new ArgumentException("Unknown scope.", "scope"); } if (string.IsNullOrWhiteSpace(description)) { throw new ArgumentException("Expected a description.", "description"); } Section = section; Key = key; Scope = scope; Description = description; Choices = choices ?? NoChoices; } public abstract object NormalizeBoxed(object? value); public override string ToString() { return Id + " (" + Scope.ToString() + ")"; } public static InscribeSetting<bool> Toggle(string section, string key, bool defaultValue, SettingScope scope, string description, bool? safe = null) { return new InscribeSetting<bool>(section, key, scope, description, defaultValue, safe, null, (bool v) => v); } public static InscribeSetting<int> Integer(string section, string key, int defaultValue, int min, int max, SettingScope scope, string description) { if (min > max) { throw new ArgumentException("Minimum above maximum.", "min"); } return new InscribeSetting<int>(section, key, scope, description, defaultValue, null, (min, max), (int v) => Math.Min(max, Math.Max(min, v))); } public static InscribeSetting<float> Number(string section, string key, float defaultValue, float min, float max, SettingScope scope, string description) { if (!(min <= max) || float.IsInfinity(min) || float.IsInfinity(max)) { throw new ArgumentException("Expected a finite range.", "min"); } return new InscribeSetting<float>(section, key, scope, description, defaultValue, null, (min, max), (float v) => (!float.IsNaN(v) && !float.IsInfinity(v)) ? Math.Min(max, Math.Max(min, v)) : defaultValue); } public static InscribeSetting<TEnum> Choice<TEnum>(string section, string key, TEnum defaultValue, SettingScope scope, string description) where TEnum : struct, Enum { return new InscribeSetting<TEnum>(section, key, scope, description, defaultValue, null, null, (TEnum v) => (!Enum.IsDefined(typeof(TEnum), v)) ? defaultValue : v, Enum.GetNames(typeof(TEnum))); } public static InscribeSetting<string> Text(string section, string key, string defaultValue, SettingScope scope, string description, Func<string, string> normalize) { if (normalize == null) { throw new ArgumentNullException("normalize"); } return new InscribeSetting<string>(section, key, scope, description, defaultValue, null, null, normalize); } } public sealed class InscribeSetting<T> : InscribeSetting { private readonly Func<T, T> normalize; private readonly bool hasSafe; private readonly (T Min, T Max)? range; public T Default { get; } public T Safe { get; } public override Type ValueType => typeof(T); public override object DefaultValue => Default; public override object? SafeValue { get { if (!hasSafe) { return null; } return Safe; } } public override object? Minimum { get { if (!range.HasValue) { return null; } return range.Value.Min; } } public override object? Maximum { get { if (!range.HasValue) { return null; } return range.Value.Max; } } internal InscribeSetting(string section, string key, SettingScope scope, string description, T defaultValue, object? safe, (T, T)? range, Func<T, T> normalize, IReadOnlyList<string>? choices = null) : base(section, key, scope, description, choices) { this.normalize = normalize ?? throw new ArgumentNullException("normalize"); this.range = range; if (!EqualityComparer<T>.Default.Equals(normalize(defaultValue), defaultValue)) { throw new ArgumentException(base.Id + ": the default is outside its own rule.", "defaultValue"); } Default = defaultValue; if (scope == SettingScope.Cheat) { if (safe == null) { throw new ArgumentException(base.Id + ": a cheat needs a safe value.", "safe"); } if (!EqualityComparer<T>.Default.Equals(defaultValue, (T)safe)) { throw new ArgumentException(base.Id + ": a cheat must default to its safe value.", "defaultValue"); } Safe = (T)safe; hasSafe = true; } else { if (safe != null) { throw new ArgumentException(base.Id + ": only cheats have a safe value.", "safe"); } Safe = defaultValue; } } public T Normalize(T value) { return normalize(value); } public override object NormalizeBoxed(object? value) { return (value is T value2) ? Normalize(value2) : Default; } } public enum ConfirmMode { HoldShift, Dialog, Off } public enum TooltipDetail { Compact, Standard, Full } public static class InscribeSettings { public static class General { public const string Section = "1 - General"; public static readonly InscribeSetting<bool> Enabled = InscribeSetting.Toggle("1 - General", "Enabled", defaultValue: true, SettingScope.ServerAuthoritative, "Whole product. Off: items keep and show their state; nothing rolls, drops or applies."); public static readonly InscribeSetting<bool> RarityEnabled = InscribeSetting.Toggle("1 - General", "Rarity Enabled", defaultValue: true, SettingScope.ServerAuthoritative, "Item rarity, inscriptions and runes. Off: nothing new rolls or drops and inscriptions stop applying; existing item data is kept and shown."); public static readonly InscribeSetting<bool> InscriptionEffects = InscribeSetting.Toggle("1 - General", "Inscription Effects", defaultValue: true, SettingScope.ServerAuthoritative, "Off: inscriptions are kept and shown, but none applies."); public static readonly InscribeSetting<bool> AccessoryRarity = InscribeSetting.Toggle("1 - General", "Accessory Rarity", defaultValue: true, SettingScope.ServerAuthoritative, "Rings and belts (Mantle) and trinkets can drop with a rarity and take runes. Off: new ones stay Common and runes refuse them; inscriptions they already have are kept and still apply. Host or server; default on. For one kind only, set rolls: false on the ring, belt or trinket class in Inscribe.economy.yml."); } public static class Runes { public const string Section = "2 - Runes"; public static readonly InscribeSetting<bool> ModifyEquippedItems = InscribeSetting.Toggle("2 - Runes", "Modify Equipped Items", defaultValue: true, SettingScope.ServerAuthoritative, "Runes may be applied to equipped items."); public static readonly InscribeSetting<ConfirmMode> ConfirmDestructiveRunes = InscribeSetting.Choice("2 - Runes", "Confirm Destructive Runes", ConfirmMode.HoldShift, SettingScope.Local, "How confirm-gated runes (Cleansing, Sealed) ask first: HoldShift, a yes/no Dialog, or Off."); public static readonly InscribeSetting<bool> RunesFromSalvage = InscribeSetting.Toggle("2 - Runes", "Runes From Salvage", defaultValue: true, SettingScope.ServerAuthoritative, "Salvaging an Uncommon, Rare or Epic item (Kindle's Salvage) may give back the Ascension Rune that raised it, one time in four. Legendary items give none."); public static readonly InscribeSetting<bool> RuneTable = InscribeSetting.Toggle("2 - Runes", "Rune Table", defaultValue: true, SettingScope.ServerAuthoritative, "The Rune Table (hammer, Crafting tab): holds runes, turns trophies into essence and applies runes; an essence chooses what the Ascension Rune adds. Off: it cannot be built or used; what it holds is kept."); public static readonly InscribeSetting<bool> BiomeEssences = InscribeSetting.Toggle("2 - Runes", "Biome Essences", defaultValue: true, SettingScope.ServerAuthoritative, "The Rune Table also offers the Mist (Mistlands), Ash (Ashlands) and Frost (Deep North) essences, which steer the Ascension Rune toward movement and stealth; fury, ammunition and thrown weapons; or warmth, food, meads and endurance. Off: only the first five essences are offered; essence in the pool and inscriptions already rolled are kept. Host or server; default on."); public static readonly InscribeSetting<bool> GemsAndSockets = InscribeSetting.Toggle("2 - Runes", "Gems And Sockets", defaultValue: true, SettingScope.ServerAuthoritative, "Weapons, staves, armor, shields and backpacks hold up to three sockets: the Dvergr Chisel cuts them and the eleven gems fill them. Off: the chisel and gems neither drop nor work; socketed gems keep their stats."); public static readonly InscribeSetting<float> GemRecoveryCommon = InscribeSetting.Number("2 - Runes", "Gem Recovery Common", 100f, 0f, 100f, SettingScope.ServerAuthoritative, "Percent chance a gem comes back when it is replaced in a Common item or the item is salvaged; otherwise the gem is lost. Host or server; default 100."); public static readonly InscribeSetting<float> GemRecoveryUncommon = InscribeSetting.Number("2 - Runes", "Gem Recovery Uncommon", 90f, 0f, 100f, SettingScope.ServerAuthoritative, "Percent chance a gem comes back when it is replaced in a Uncommon item or the item is salvaged; otherwise the gem is lost. Host or server; default 90."); public static readonly InscribeSetting<float> GemRecoveryRare = InscribeSetting.Number("2 - Runes", "Gem Recovery Rare", 75f, 0f, 100f, SettingScope.ServerAuthoritative, "Percent chance a gem comes back when it is replaced in a Rare item or the item is salvaged; otherwise the gem is lost. Host or server; default 75."); public static readonly InscribeSetting<float> GemRecoveryEpic = InscribeSetting.Number("2 - Runes", "Gem Recovery Epic", 50f, 0f, 100f, SettingScope.ServerAuthoritative, "Percent chance a gem comes back when it is replaced in a Epic item or the item is salvaged; otherwise the gem is lost. Host or server; default 50."); public static readonly InscribeSetting<float> GemRecoveryLegendary = InscribeSetting.Number("2 - Runes", "Gem Recovery Legendary", 25f, 0f, 100f, SettingScope.ServerAuthoritative, "Percent chance a gem comes back when it is replaced in a Legendary item or the item is salvaged; otherwise the gem is lost. Host or server; default 25."); public static InscribeSetting<float>[] GemRecovery => new InscribeSetting<float>[5] { GemRecoveryCommon, GemRecoveryUncommon, GemRecoveryRare, GemRecoveryEpic, GemRecoveryLegendary }; } public static class Drops { public const string Section = "4 - Drops"; public static readonly InscribeSetting<bool> RuneDrops = InscribeSetting.Toggle("4 - Drops", "Rune Drops", defaultValue: true, SettingScope.ServerAuthoritative, "Creatures and bosses drop runes."); public static readonly InscribeSetting<bool> MagicItemDrops = InscribeSetting.Toggle("4 - Drops", "Magic Item Drops", defaultValue: true, SettingScope.ServerAuthoritative, "Creatures and bosses drop gear of Uncommon rarity or better."); public static readonly InscribeSetting<bool> ChestDrops = InscribeSetting.Toggle("4 - Drops", "Chest Drops", defaultValue: true, SettingScope.ServerAuthoritative, "World chests roll runes and gear once."); public static readonly InscribeSetting<bool> WildbloodStarSynergy = InscribeSetting.Toggle("4 - Drops", "Wildblood Star Synergy", defaultValue: false, SettingScope.ServerAuthoritative, "Wildblood's stars raise Inscribe's drop chances."); public static readonly InscribeSetting<float> UncommonWeight = InscribeSetting.Number("4 - Drops", "Uncommon Drop Weight", 1f, 0f, 5f, SettingScope.ServerAuthoritative, "Multiplies how often dropped gear is Uncommon (green), against the drop tables. 1: as the tables say; 0: never. Host or server; the next drop uses it."); public static readonly InscribeSetting<float> RareWeight = InscribeSetting.Number("4 - Drops", "Rare Drop Weight", 1f, 0f, 5f, SettingScope.ServerAuthoritative, "Multiplies how often dropped gear is Rare (blue), against the drop tables. 1: as the tables say; 0: never. Host or server; the next drop uses it."); public static readonly InscribeSetting<float> EpicWeight = InscribeSetting.Number("4 - Drops", "Epic Drop Weight", 1f, 0f, 5f, SettingScope.ServerAuthoritative, "Multiplies how often dropped gear is Epic (purple), against the drop tables. 1: as the tables say; 0: never. Host or server; the next drop uses it."); public static readonly InscribeSetting<bool> LegendaryFromBosses = InscribeSetting.Toggle("4 - Drops", "Legendary From Wildblood Bosses", defaultValue: true, SettingScope.ServerAuthoritative, "A boss kill may drop one Legendary (gold) item, when Wildblood is installed. Legendary never drops from other creatures, chests, crafting or runes. Host or server."); public static readonly InscribeSetting<bool> LegendaryFromRaids = InscribeSetting.Toggle("4 - Drops", "Legendary From Wildblood Raids", defaultValue: true, SettingScope.ServerAuthoritative, "A won Wildblood raid may drop one Legendary (gold) item at the raid site. Host or server."); public static readonly InscribeSetting<float> LegendaryBossChance = InscribeSetting.Number("4 - Drops", "Legendary Boss Chance", 20f, 0f, 100f, SettingScope.ServerAuthoritative, "Percent chance a boss kill drops a Legendary item, while Legendary From Wildblood Bosses is on. Host or server."); public static readonly InscribeSetting<float> LegendaryRaidChance = InscribeSetting.Number("4 - Drops", "Legendary Raid Chance", 10f, 0f, 100f, SettingScope.ServerAuthoritative, "Percent chance a won raid drops a Legendary item, while Legendary From Wildblood Raids is on. Host or server."); } public static class Commands { public const string Section = "5 - Commands"; public static readonly InscribeSetting<bool> ReadOnlyForEveryone = InscribeSetting.Toggle("5 - Commands", "Read-Only Commands For Everyone", defaultValue: true, SettingScope.ServerAuthoritative, "Everyone may use inscribe inspect, stats, list and help."); } public static class Display { public const string Section = "6 - Display"; public static readonly InscribeSetting<bool> ColoredItemNames = InscribeSetting.Toggle("6 - Display", "Colored Item Names", defaultValue: true, SettingScope.Local, "Item names show in their rarity color."); public static readonly InscribeSetting<TooltipDetail> TooltipDetail = InscribeSetting.Choice("6 - Display", "Tooltip Detail", ExtraPotions.Inscribe.Config.TooltipDetail.Standard, SettingScope.Local, "Tooltip block: Compact, Standard or Full."); public static readonly InscribeSetting<bool> ShowDormant = InscribeSetting.Toggle("6 - Display", "Show Dormant Inscriptions", defaultValue: true, SettingScope.Local, "Inscriptions whose definition is gone show grayed."); } public static class Glow { public const string Section = "7 - Ground Glow"; public static readonly InscribeSetting<bool> GroundGlow = InscribeSetting.Toggle("7 - Ground Glow", "Ground Glow", defaultValue: true, SettingScope.Local, "Dropped Uncommon or better gear glows in its rarity color."); public static readonly InscribeSetting<float> Intensity = InscribeSetting.Number("7 - Ground Glow", "Glow Intensity", 1f, 0f, 3f, SettingScope.Local, "Brightness of the ground glow."); public static readonly InscribeSetting<float> Range = InscribeSetting.Number("7 - Ground Glow", "Glow Range", 2f, 0.5f, 6f, SettingScope.Local, "Light range in meters."); public static readonly InscribeSetting<int> MaxLights = InscribeSetting.Integer("7 - Ground Glow", "Glow Max Lights", 25, 0, 100, SettingScope.Local, "Only the nearest this many items hold a live light."); public static readonly InscribeSetting<float> RefreshSeconds = InscribeSetting.Number("7 - Ground Glow", "Glow Refresh Seconds", 1f, 0.25f, 5f, SettingScope.Local, "How often the nearest items are chosen again."); public static readonly InscribeSetting<bool> GlowRunes = InscribeSetting.Toggle("7 - Ground Glow", "Glow Runes", defaultValue: false, SettingScope.Local, "Dropped runes glow as well."); public static readonly InscribeSetting<bool> LootBeam = InscribeSetting.Toggle("7 - Ground Glow", "Loot Beam", defaultValue: true, SettingScope.Local, "Glowing dropped gear also shows a beam of light in its rarity color."); public static readonly InscribeSetting<bool> LegendaryGlint = InscribeSetting.Toggle("7 - Ground Glow", "Legendary Glint", defaultValue: true, SettingScope.Local, "Dropped Legendary gear also shows a small bronze and ivory glint on the ground. Other rarities never glint. This client only."); } public static class Interface { public const string Section = "8 - Interface"; public static readonly InscribeSetting<bool> StationButton = InscribeSetting.Toggle("8 - Interface", "Station Button", defaultValue: true, SettingScope.Local, "Retained legacy preference for the removed station button; no effect."); public static readonly InscribeSetting<RecastFocus> RecastingFocus = InscribeSetting.Choice("8 - Interface", "Recasting Focus", RecastFocus.Unrestricted, SettingScope.Local, "Your saved Rune Table category for one new inscription: Unrestricted, Offense, Defense or Utility. Recasting still replaces all inscriptions at the same rune cost and uses the host's rules. Your choice is saved on this computer."); public static readonly InscribeSetting<RecastSide> RecastingSide = InscribeSetting.Choice("8 - Interface", "Recasting Side", RecastSide.Prefix, SettingScope.Local, "Your saved prefix or suffix choice for the focused inscription. Applies only when a Recasting category is selected; the other inscriptions still reroll normally. Your choice is saved on this computer."); } public static class Cheats { public const string Section = "9 - Cheats"; public static readonly InscribeSetting<bool> ItemCommands = InscribeSetting.Toggle("9 - Cheats", "Item Commands", defaultValue: false, SettingScope.Cheat, "Cheat. Enables inscribe give, roll, reroll and cleanse console commands.", false); } private static readonly Lazy<InscribeSetting[]> all = new Lazy<InscribeSetting[]>(() => new InscribeSetting[41] { General.Enabled, General.RarityEn