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 KindredMastery v1.0.4
BepInEx/plugins/KindredMastery/KindredMastery.dll
Decompiled 2 weeks agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using KindredMastery.Commands; using KindredMastery.Configuration; using KindredMastery.Services; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEngine; [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("KindredMastery")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.4.0")] [assembly: AssemblyInformationalVersion("1.0.4+55c6ba452194f646bb2f8addfe7fccaf8ffb1433")] [assembly: AssemblyProduct("KindredMastery")] [assembly: AssemblyTitle("KindredMastery")] [assembly: AssemblyVersion("1.0.4.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 KindredMastery { [BepInPlugin("com.oddlyforged.kindredmastery", "KindredMastery", "1.0.4")] public class KindredMasteryPlugin : BaseUnityPlugin { public const string PluginId = "com.oddlyforged.kindredmastery"; public const string PluginName = "KindredMastery"; public const string PluginVersion = "1.0.4"; private Harmony? _harmony; internal static KindredMasteryCalculator Calculator { get; private set; } internal static KindredMasteryConfig Configuration { get; private set; } internal static ManualLogSource Log { get; private set; } public KindredMasteryPlugin() { Log = ((BaseUnityPlugin)this).Logger; ((BaseUnityPlugin)this).Logger.LogInfo((object)"KindredMasteryPlugin is initializing!"); } private void Awake() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Expected O, but got Unknown Configuration = new KindredMasteryConfig(((BaseUnityPlugin)this).Config, Log); Calculator = new KindredMasteryCalculator(Configuration); _harmony = new Harmony("com.oddlyforged.kindredmastery"); _harmony.PatchAll(); KindredMasteryCommands.Register(Configuration); ((BaseUnityPlugin)this).Logger.LogInfo((object)"KindredMastery is loaded!"); } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } ((BaseUnityPlugin)this).Logger.LogInfo((object)"KindredMastery is unloaded!"); } } internal static class BuildVersion { public const string Value = "1.0.4"; } } namespace KindredMastery.Services { internal static class AffinityFormula { public static float CalculateMultiplier(float targetLevel, float sourceLevel, float affinityStrength, float globalModifier) { float num = Math.Max(0f, sourceLevel - targetLevel); return 1f + num / 100f * affinityStrength * globalModifier; } } internal static class FancyFeedbackProvider { private static readonly Random Random = new Random(); private static readonly Dictionary<(SkillType Source, SkillType Target), string[]> SkillMessages = new Dictionary<(SkillType, SkillType), string[]> { [((SkillType)8, (SkillType)14)] = new string[3] { "It's a bow. With a trigger. How hard could it be?", "YOU'VE INVENTED BOW 2.0!", "Someone finally put a stock on your bow." }, [((SkillType)14, (SkillType)8)] = new string[2] { "You removed the trigger. Bold choice.", "BACK TO BASICS! Who needs mechanical assistance?" }, [((SkillType)1, (SkillType)2)] = new string[3] { "The sword got smaller. Your expertise didn't.", "HALF THE BLADE, ALL THE TALENT!", "Your sword shrank in the wash. Your skills survived." }, [((SkillType)2, (SkillType)1)] = new string[3] { "You added a hilt. Your skill is still sharp.", "The knife grew. Your skill grew sharper.", "Same stabbing principle. Now with personal space." }, [((SkillType)1, (SkillType)7)] = new string[1] { "The blade got heavier. The technique got questionable." }, [((SkillType)7, (SkillType)1)] = new string[1] { "You discovered that blades can do more than chop trees." }, [((SkillType)3, (SkillType)11)] = new string[1] { "The weapon was holding you back. Allegedly." } }; private static readonly string[] GenericMessages = new string[4] { "BY ODIN'S BEARD! Your experience proves useful!", "Your brain has discovered synergy.", "The Viking Accreditation Board approves.", "HR would be proud of your transferable skills." }; internal static string GetMessage(KindredMasteryResult affinity) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) SkillType? sourceSkill = affinity.SourceSkill; if (sourceSkill.HasValue) { SkillType valueOrDefault = sourceSkill.GetValueOrDefault(); if (SkillMessages.TryGetValue((valueOrDefault, affinity.TargetSkill), out string[] value) && value.Length != 0) { return Pick(value); } } return Pick(GenericMessages); } private static string Pick(string[] messages) { return messages[Random.Next(messages.Length)]; } } internal sealed class KindredMasteryCalculator { private readonly KindredMasteryConfig _config; public KindredMasteryCalculator(KindredMasteryConfig config) { _config = config; } public KindredMasteryResult Calculate(Skills skills, SkillType target) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) float skillLevel = skills.GetSkillLevel(target); float num = 1f; float sourceLevel = skillLevel; float affinityStrength = _config.DefaultAffinityStrength.Value; SkillType? sourceSkill = null; string domainName = null; if (_config.Enabled.Value) { foreach (SkillDomain activeDomain in _config.ActiveDomains) { if (!activeDomain.Skills.Contains(target)) { continue; } float num2 = skillLevel; SkillType? val = null; foreach (SkillType skill in activeDomain.Skills) { if (skill != target) { float skillLevel2 = skills.GetSkillLevel(skill); if (skillLevel2 > num2) { num2 = skillLevel2; val = skill; } } } if (val.HasValue) { float effectiveStrength = activeDomain.GetEffectiveStrength(_config.DefaultAffinityStrength.Value); float num3 = AffinityFormula.CalculateMultiplier(skillLevel, num2, effectiveStrength, _config.GlobalModifier.Value); if (num3 > num) { num = num3; sourceLevel = num2; sourceSkill = val; domainName = activeDomain.Name; affinityStrength = effectiveStrength; } } } } return new KindredMasteryResult(target, sourceSkill, skillLevel, sourceLevel, num, domainName, affinityStrength); } public float CalculateMultiplier(Skills skills, SkillType target) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return Calculate(skills, target).Multiplier; } } internal sealed class KindredMasteryResult { public SkillType TargetSkill { get; } public SkillType? SourceSkill { get; } public float CurrentLevel { get; } public float SourceLevel { get; } public float Multiplier { get; } public string? DomainName { get; } public float AffinityStrength { get; } public float BonusPercentage => (Multiplier - 1f) * 100f; public bool HasAffinity { get { if (SourceSkill.HasValue) { return Multiplier > 1f; } return false; } } public KindredMasteryResult(SkillType targetSkill, SkillType? sourceSkill, float currentLevel, float sourceLevel, float multiplier, string? domainName, float affinityStrength) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) TargetSkill = targetSkill; SourceSkill = sourceSkill; CurrentLevel = currentLevel; SourceLevel = sourceLevel; Multiplier = multiplier; DomainName = domainName; AffinityStrength = affinityStrength; } } } namespace KindredMastery.Patches { [HarmonyPatch(typeof(Player), "Message", new Type[] { typeof(MessageType), typeof(string), typeof(int), typeof(Sprite), typeof(bool) })] internal static class PlayerMessagePatch { [HarmonyPrefix] private static void Prefix(ref string msg) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) KindredMasteryResult currentAffinity = SkillsRaiseSkillPatch.CurrentAffinity; if (currentAffinity != null && currentAffinity.HasAffinity && !string.IsNullOrEmpty(msg) && msg.Contains("$msg_skillup")) { msg += (string?)(KindredMasteryPlugin.Configuration.LevelUpFeedback.Value switch { LevelUpFeedbackType.Minimal => " (Kindred Mastery)", LevelUpFeedbackType.Detailed => " (Kindred Mastery: " + $"+{currentAffinity.BonusPercentage:F0}% {currentAffinity.TargetSkill} XP " + $"from {currentAffinity.SourceSkill} in {currentAffinity.DomainName})", LevelUpFeedbackType.Fancy => " (" + FancyFeedbackProvider.GetMessage(currentAffinity) + " " + $"+{currentAffinity.BonusPercentage:F0}% {currentAffinity.TargetSkill} XP " + $"from {currentAffinity.SourceSkill})", _ => string.Empty, }); } } } [HarmonyPatch(typeof(Skills), "RaiseSkill")] internal static class SkillsRaiseSkillPatch { private sealed class RaiseSkillState { public float PreviousLevel { get; set; } public KindredMasteryResult? Affinity { get; set; } } [ThreadStatic] private static KindredMasteryResult? _currentAffinity; internal static KindredMasteryResult? CurrentAffinity => _currentAffinity; [HarmonyPrefix] private static void Prefix(Skills __instance, SkillType skillType, ref float factor) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) KindredMasteryResult kindredMasteryResult = KindredMasteryPlugin.Calculator.Calculate(__instance, skillType); _currentAffinity = (kindredMasteryResult.HasAffinity ? kindredMasteryResult : null); factor *= kindredMasteryResult.Multiplier; } [HarmonyPostfix] private static void Postfix() { _currentAffinity = null; } } } namespace KindredMastery.Configuration { internal static class DomainDefinitionValidator { private const float MaximumStrength = 10f; public static bool TryValidate(string? name, float? affinityStrength, IEnumerable<string>? skillNames, IEnumerable<string> validSkillNames, out ValidatedAffinityDomain? domain, out string error) { domain = null; string text = name?.Trim(); if (string.IsNullOrWhiteSpace(text)) { error = "Domain name cannot be empty."; return false; } if (affinityStrength.HasValue && !IsValidStrength(affinityStrength.Value)) { error = $"Affinity strength must be between 0 and {10f}, inclusive."; return false; } if (skillNames == null) { error = "Skills must be an array of skill names."; return false; } Dictionary<string, string> dictionary = validSkillNames.ToDictionary<string, string, string>((string skill) => skill, (string skill) => skill, StringComparer.OrdinalIgnoreCase); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (string skillName in skillNames) { string text2 = skillName?.Trim(); if (string.IsNullOrWhiteSpace(text2) || !dictionary.TryGetValue(text2, out var value)) { error = "Unknown or empty skill name: '" + (skillName ?? "<null>") + "'."; return false; } hashSet.Add(value); } if (hashSet.Count < 2) { error = "A domain must contain at least two distinct skills."; return false; } domain = new ValidatedAffinityDomain(text, affinityStrength, hashSet.ToArray()); error = string.Empty; return true; } private static bool IsValidStrength(float strength) { if (!float.IsNaN(strength) && !float.IsInfinity(strength) && strength >= 0f) { return strength <= 10f; } return false; } } internal static class DomainJsonParser { private sealed class DomainJsonEntry { [JsonProperty("name")] public string? Name { get; set; } [JsonProperty("affinityStrength")] public float? AffinityStrength { get; set; } [JsonProperty("skills")] public List<string>? Skills { get; set; } } public static DomainJsonParseResult Parse(string json, IEnumerable<string> validSkillNames) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) string[] validSkillNames2 = validSkillNames.Distinct<string>(StringComparer.OrdinalIgnoreCase).ToArray(); JToken obj = JToken.Parse(json); JToken obj2 = ((obj is JArray) ? obj : null) ?? throw new JsonSerializationException("The document must contain a JSON array of domains."); List<ValidatedAffinityDomain> list = new List<ValidatedAffinityDomain>(); List<string> list2 = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (JToken item in (JArray)obj2) { DomainJsonEntry domainJsonEntry; try { domainJsonEntry = item.ToObject<DomainJsonEntry>(); } catch (Exception ex) { list2.Add("Skipping malformed domain entry: " + ex.Message); continue; } if (!DomainDefinitionValidator.TryValidate(domainJsonEntry?.Name, domainJsonEntry?.AffinityStrength, domainJsonEntry?.Skills, validSkillNames2, out ValidatedAffinityDomain domain, out string error)) { string text = (string.IsNullOrWhiteSpace(domainJsonEntry?.Name) ? "<unnamed>" : domainJsonEntry.Name); list2.Add("Skipping domain '" + text + "': " + error); } else if (!hashSet.Add(domain.Name)) { list2.Add("Skipping domain '" + domain.Name + "': Duplicate domain name."); } else { list.Add(domain); } } return new DomainJsonParseResult(list, list2); } } internal sealed class DomainJsonParseResult { public IReadOnlyList<ValidatedAffinityDomain> Domains { get; } public IReadOnlyList<string> Diagnostics { get; } public DomainJsonParseResult(IReadOnlyList<ValidatedAffinityDomain> domains, IReadOnlyList<string> diagnostics) { Domains = domains; Diagnostics = diagnostics; } } internal sealed class ValidatedAffinityDomain { public string Name { get; } public float? AffinityStrength { get; } public IReadOnlyList<string> Skills { get; } public ValidatedAffinityDomain(string name, float? affinityStrength, IReadOnlyList<string> skills) { Name = name; AffinityStrength = affinityStrength; Skills = skills; } } internal sealed class KindredMasteryConfig { private const string DefaultDomainsResourceName = "KindredMastery.Configuration.DefaultDomains.json"; private const float MaximumStrength = 10f; private readonly ConfigFile _config; private readonly ManualLogSource _logger; public ConfigEntry<bool> Enabled { get; } public ConfigEntry<float> GlobalModifier { get; } public ConfigEntry<float> DefaultAffinityStrength { get; } public ConfigEntry<LevelUpFeedbackType> LevelUpFeedback { get; } public IReadOnlyList<SkillDomain> ActiveDomains { get; private set; } = Array.Empty<SkillDomain>(); public string DomainsFilePath { get; } public KindredMasteryConfig(ConfigFile config, ManualLogSource logger) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Expected O, but got Unknown _config = config; _logger = logger; DomainsFilePath = Path.ChangeExtension(config.ConfigFilePath, ".domains.json"); Enabled = config.Bind<bool>("1 - General", "Enabled", true, "Enable or disable the Kindred Mastery plugin."); GlobalModifier = config.Bind<float>("1 - General", "Global Modifier", 1f, new ConfigDescription("Scales every affinity bonus. 1.0 applies the full configured strength.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>())); DefaultAffinityStrength = config.Bind<float>("2 - Affinities", "Default Affinity Strength", 1f, new ConfigDescription("Affinity strength used by domains without an explicit affinityStrength.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>())); LevelUpFeedback = config.Bind<LevelUpFeedbackType>("3 - Feedback", "Level Up Feedback", LevelUpFeedbackType.Detailed, "Controls the detail shown in skill level-up notifications."); InitializeDomainsFile(); LoadDomains(); } private void InitializeDomainsFile() { if (File.Exists(DomainsFilePath)) { return; } try { using Stream stream = typeof(KindredMasteryConfig).Assembly.GetManifestResourceStream("KindredMastery.Configuration.DefaultDomains.json"); if (stream == null) { throw new InvalidOperationException("Embedded default domains resource 'KindredMastery.Configuration.DefaultDomains.json' was not found."); } using StreamReader streamReader = new StreamReader(stream); File.WriteAllText(DomainsFilePath, streamReader.ReadToEnd()); _logger.LogInfo((object)("Created default skill domains file '" + DomainsFilePath + "'.")); } catch (Exception arg) { _logger.LogError((object)$"Could not initialize the domain JSON file '{DomainsFilePath}': {arg}"); } } private void LoadDomains() { DomainJsonParseResult domainJsonParseResult; try { domainJsonParseResult = DomainJsonParser.Parse(File.ReadAllText(DomainsFilePath), Enum.GetNames(typeof(SkillType))); } catch (Exception ex) { _logger.LogError((object)("Could not read skill domains from '" + DomainsFilePath + "'. Keeping the current domains. " + ex.Message)); return; } foreach (string diagnostic in domainJsonParseResult.Diagnostics) { _logger.LogWarning((object)diagnostic); } List<SkillDomain> list = (List<SkillDomain>)(ActiveDomains = domainJsonParseResult.Domains.Select((ValidatedAffinityDomain domain) => new SkillDomain(domain.Name, domain.AffinityStrength, ((IEnumerable<string>)domain.Skills).Select((Func<string, SkillType>)((string name) => (SkillType)Enum.Parse(typeof(SkillType), name))).ToArray())).ToList()); _logger.LogInfo((object)$"KindredMastery: {list.Count} domains loaded, {domainJsonParseResult.Diagnostics.Count} skipped."); foreach (SkillDomain item in list) { string text = (item.AffinityStrength.HasValue ? item.AffinityStrength.Value.ToString("0.##") : "default"); _logger.LogInfo((object)(" [OK] " + item.Name + " (strength: " + text + "): " + string.Join(", ", item.Skills))); } } public void Reload() { _config.Reload(); LoadDomains(); } } public enum LevelUpFeedbackType { None, Minimal, Detailed, Fancy } internal sealed class SkillDomain { public string Name { get; } public float? AffinityStrength { get; } public IReadOnlyList<SkillType> Skills { get; } public SkillDomain(string name, float? affinityStrength, IReadOnlyList<SkillType> skills) { Name = name; AffinityStrength = affinityStrength; Skills = skills; } public float GetEffectiveStrength(float defaultStrength) { return AffinityStrength ?? defaultStrength; } } } namespace KindredMastery.Commands { internal static class KindredMasteryCommands { private static bool _registered; public static void Register(KindredMasteryConfig config) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected O, but got Unknown //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (!_registered) { new ConsoleCommand("kindredmastery", "Manage KindredMastery skill affinities", (ConsoleEvent)delegate(ConsoleEventArgs args) { Execute(args, config); }, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); _registered = true; } } private static void Execute(ConsoleEventArgs args, KindredMasteryConfig config) { switch ((args.Length > 1) ? args[1].ToLowerInvariant() : "help") { case "list": PrintDomains(args.Context, config); break; case "status": PrintStatus(args.Context, config); break; case "reload": config.Reload(); args.Context.AddString("KindredMastery configuration reloaded."); PrintStatus(args.Context, config); break; case "inspect": Inspect(args, config); break; default: args.Context.AddString("KindredMastery Commands:"); args.Context.AddString(" kindredmastery list - List all skill domains and their related skills."); args.Context.AddString(" kindredmastery status - Show the current status of the KindredMastery plugin."); args.Context.AddString(" kindredmastery reload - Reload settings and skill domains."); break; } } private static void PrintDomains(Terminal terminal, KindredMasteryConfig config) { terminal.AddString($"KindredMastery: {config.ActiveDomains.Count} active domains"); foreach (SkillDomain activeDomain in config.ActiveDomains) { string text = (activeDomain.AffinityStrength.HasValue ? activeDomain.AffinityStrength.Value.ToString("0.##") : $"default ({config.DefaultAffinityStrength.Value:0.##})"); terminal.AddString(" " + activeDomain.Name + " [strength: " + text + "]: " + string.Join(", ", activeDomain.Skills)); } } private static void PrintStatus(Terminal terminal, KindredMasteryConfig config) { terminal.AddString("KindredMastery status:"); terminal.AddString($" Enabled: {config.Enabled.Value}"); terminal.AddString($" Global modifier: {config.GlobalModifier.Value:0.##}"); terminal.AddString($" Default affinity strength: {config.DefaultAffinityStrength.Value:0.##}"); terminal.AddString($" Active domains: {config.ActiveDomains.Count}"); terminal.AddString(" Domains file: " + config.DomainsFilePath); } private static void Inspect(ConsoleEventArgs args, KindredMasteryConfig config) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) if (args.Length < 3) { args.Context.AddString("Usage: kindredmastery inspect <skill>"); return; } if (!Enum.TryParse<SkillType>(args[2], ignoreCase: true, out SkillType result) || !Enum.IsDefined(typeof(SkillType), result)) { args.Context.AddString("Unknown skill: " + args[2]); return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { args.Context.AddString("A character must be loaded to inspect skills."); return; } KindredMasteryResult kindredMasteryResult = new KindredMasteryCalculator(config).Calculate(((Character)localPlayer).GetSkills(), result); args.Context.AddString($"KindredMastery - {kindredMasteryResult.TargetSkill}"); args.Context.AddString($" Current level: {kindredMasteryResult.CurrentLevel:F1}"); if (kindredMasteryResult.HasAffinity) { args.Context.AddString(" Strongest domain: " + kindredMasteryResult.DomainName + " " + $"(strength {kindredMasteryResult.AffinityStrength:0.##})"); args.Context.AddString($" Strongest related skill: {kindredMasteryResult.SourceSkill} " + $"({kindredMasteryResult.SourceLevel:F1})"); args.Context.AddString($" Affinity bonus: +{kindredMasteryResult.BonusPercentage:F0}%"); } else { args.Context.AddString(" No active catch-up bonus."); } args.Context.AddString($" Experience multiplier: {kindredMasteryResult.Multiplier:F2}x"); } } }