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 Lost Scrolls II v0.9.1
LostScrollsII.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; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using GUIFramework; using HarmonyLib; using LostScrollsII.Bounty; using LostScrollsII.Companions; using LostScrollsII.Economy; using LostScrollsII.Integration; using LostScrollsII.Ranking; using Microsoft.CodeAnalysis; using Splatform; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; using ValheimServerGuide.Discord; using ValheimServerGuide.Triggers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("LostScrollsII")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.9.1.0")] [assembly: AssemblyInformationalVersion("0.9.1+7b81ffa85b20e1622905f275f7685e83d3fed6dd")] [assembly: AssemblyProduct("LostScrollsII")] [assembly: AssemblyTitle("LostScrollsII")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.9.1.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace LostScrollsII { [BepInPlugin("com.lostscrollsii", "Lost Scrolls II", "0.9.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.lostscrollsii"; public const string PluginName = "Lost Scrolls II"; public const string PluginVersion = "0.9.1"; private Harmony _harmony; public static Plugin Instance { get; private set; } public static ManualLogSource Log { get; private set; } public static ConfigEntry<KeyCode> CommunionKey { get; private set; } public static ConfigEntry<float> CommunionChannelSeconds { get; private set; } public static ConfigEntry<float> CommunionMaxDistance { get; private set; } public static ConfigEntry<bool> CommunionBreakOnDamage { get; private set; } public static ConfigEntry<bool> StaffSealEnabled { get; private set; } public static ConfigEntry<int> SealMinBloodMagic { get; private set; } public static ConfigEntry<int> SealFullSpeedBloodMagic { get; private set; } public static ConfigEntry<float> SealChannelMaxSeconds { get; private set; } public static ConfigEntry<float> SealChannelMinSeconds { get; private set; } public static ConfigEntry<float> SealMaxDistance { get; private set; } public static ConfigEntry<KeyCode> ChoreAssignKey { get; private set; } public static ConfigEntry<float> ChoreAssignRadius { get; private set; } public static ConfigEntry<KeyCode> DuelSelectKey { get; private set; } public static ConfigEntry<KeyCode> PartyDuelKey { get; private set; } public static ConfigEntry<int> MaxPartySize { get; private set; } public static ConfigEntry<KeyCode> StanceCycleKey { get; private set; } public static ConfigEntry<KeyCode> InventoryKey { get; private set; } public static ConfigEntry<bool> ShowMapPins { get; private set; } public static ConfigEntry<string> CompanionPinColor { get; private set; } public static ConfigEntry<float> CompanionPinScale { get; private set; } public static ConfigEntry<bool> ShowDeathMarker { get; private set; } public static ConfigEntry<bool> MoveContainerPanel { get; private set; } public static ConfigEntry<string> ContainerPanelOffset { get; private set; } public static ConfigEntry<string> MenuBarOffset { get; private set; } public static ConfigEntry<int> RankingKFactor { get; private set; } public static ConfigEntry<float> RankingPairCooldown { get; private set; } public static ConfigEntry<bool> ShowRankOnNameTag { get; private set; } public static ConfigEntry<KeyCode> RankingUiKey { get; private set; } public static ConfigEntry<KeyCode> TournamentUiKey { get; private set; } public static ConfigEntry<int> RequiredEntrantLevel { get; private set; } public static ConfigEntry<int> MaxEntrants { get; private set; } public static ConfigEntry<bool> WagersEnabled { get; private set; } public static ConfigEntry<int> TournamentValcoinFee { get; private set; } public static ConfigEntry<int> TournamentValcoinPrize { get; private set; } public static ConfigEntry<int> TournamentCoinFee { get; private set; } public static ConfigEntry<int> TournamentCoinPrize { get; private set; } public static ConfigEntry<int> DuelValcoinStake { get; private set; } public static ConfigEntry<int> DuelCoinStake { get; private set; } public static ConfigEntry<int> WageredBracketSize { get; private set; } public static ConfigEntry<float> WageredRegistrationMinutes { get; private set; } public static ConfigEntry<bool> BountyEnabled { get; private set; } public static ConfigEntry<float> BountySearchMinRadius { get; private set; } public static ConfigEntry<float> BountySearchMaxRadius { get; private set; } public static ConfigEntry<float> BountyLandCheckRadius { get; private set; } public static ConfigEntry<float> BountyWaterMargin { get; private set; } public static ConfigEntry<float> BountyMaxHeightVariance { get; private set; } public static ConfigEntry<int> BountySampleAttempts { get; private set; } public static ConfigEntry<int> BountyTierBlackForest { get; private set; } public static ConfigEntry<int> BountyTierSwamp { get; private set; } public static ConfigEntry<int> BountyTierMountain { get; private set; } public static ConfigEntry<int> BountyTierPlains { get; private set; } public static ConfigEntry<float> BountyHealthBase { get; private set; } public static ConfigEntry<float> BountyHealthGrowth { get; private set; } public static ConfigEntry<int> BountyMaxStarLevel { get; private set; } public static ConfigEntry<int> BountyMinionsBase { get; private set; } public static ConfigEntry<int> BountyMinionsPerTier { get; private set; } public static ConfigEntry<int> BountyMaxMinions { get; private set; } public static ConfigEntry<float> BountyMinionRingRadius { get; private set; } public static ConfigEntry<float> BountyAlertRangeMultiplier { get; private set; } public static ConfigEntry<float> BountyRoamRadius { get; private set; } public static ConfigEntry<int> BountyMaxTierBonus { get; private set; } public static ConfigEntry<float> BountyValcoinBaseChance { get; private set; } public static ConfigEntry<float> BountyValcoinChancePerTier { get; private set; } public static ConfigEntry<float> BountyValcoinRankBonus { get; private set; } public static ConfigEntry<int> BountyValcoinRankDepth { get; private set; } public static ConfigEntry<float> BountyValcoinMaxChance { get; private set; } public static ConfigEntry<int> BountyPointsPerTier { get; private set; } public static ConfigEntry<int> BountyLeaderboardBonusRank { get; private set; } public static ConfigEntry<KeyCode> BountyUiKey { get; private set; } public static ConfigEntry<int> BountyMaxBoardEntries { get; private set; } public static ConfigEntry<int> BountyMaxActivePerPlayer { get; private set; } public static ConfigEntry<float> BountyArrivalRadius { get; private set; } public static ConfigEntry<string> BountyButtonOffset { get; private set; } public static ConfigEntry<float> BountyRefreshHours { get; private set; } public static ConfigEntry<float> BountyEliteChance { get; private set; } public static ConfigEntry<int> BountyEliteRankTopN { get; private set; } private void Awake() { //IL_0a34: Unknown result type (might be due to invalid IL or missing references) //IL_0a3e: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; CommunionKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Recruitment", "CommunionKey", (KeyCode)103, "Key pressed while hovering a recruited companion to feed it a health mead. (The Communion Rite that recruits a subdued Dvergr is now channeled by holding the Block button, not this key.)"); CommunionChannelSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Recruitment", "CommunionChannelSeconds", 5f, "How long you must hold the Communion key to complete the rite. The corruption fights back the whole time — release the key, stray too far, or take a hit and the rite breaks."); CommunionMaxDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Recruitment", "CommunionMaxDistance", 4f, "How far you may drift from the Dvergr mid-rite before the connection snaps and the rite fails."); CommunionBreakOnDamage = ((BaseUnityPlugin)this).Config.Bind<bool>("Recruitment", "CommunionBreakOnDamage", true, "If true, taking damage while channeling the Communion Rite breaks it (the shadow reclaims the Dvergr). Turn off for a more forgiving rite."); StaffSealEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Recruitment", "StaffSealEnabled", true, "Allow sealing a companion into a Communion Totem in the field with an equipped Dead Raiser (StaffSkeleton) + a Wisp, by holding Block on your own Follow-stance ally. The Incinerator ritual is unaffected either way."); SealMinBloodMagic = ((BaseUnityPlugin)this).Config.Bind<int>("Recruitment", "SealMinBloodMagic", 20, "Minimum Blood Magic skill needed to seal a companion with a Dead Raiser. Below this the rite cannot be started."); SealFullSpeedBloodMagic = ((BaseUnityPlugin)this).Config.Bind<int>("Recruitment", "SealFullSpeedBloodMagic", 100, "The Blood Magic skill at which the sealing channel reaches its fastest (SealChannelMinSeconds). Between SealMinBloodMagic and this value the channel time scales smoothly."); SealChannelMaxSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Recruitment", "SealChannelMaxSeconds", 5f, "How long the Dead Raiser sealing channel takes at the MINIMUM Blood Magic skill (the slowest it ever is)."); SealChannelMinSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Recruitment", "SealChannelMinSeconds", 2f, "How long the Dead Raiser sealing channel takes at SealFullSpeedBloodMagic (the fastest it ever is)."); SealMaxDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Recruitment", "SealMaxDistance", 4f, "How far you may drift from the companion mid-sealing before the binding fails."); ChoreAssignKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Chores", "ChoreAssignKey", (KeyCode)104, "Key pressed while hovering a workstation (Smelter-family), a ripe crop, or a tamed animal to assign/unassign the nearest matching-caste companion to that chore."); ChoreAssignRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Chores", "ChoreAssignRadius", 10f, "Max distance from the player to look for a recruited companion to assign to a chore."); DuelSelectKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Duels", "DuelSelectKey", (KeyCode)106, "Press while hovering your own recruited companion to toggle its duel mode. A duel-mode companion fights other players' duel-mode companions and ignores everyone else."); PartyDuelKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Duels", "PartyDuelKey", (KeyCode)107, "Press while hovering your own recruited companion to toggle a party duel: your nearby Follow-stance companions form a team that fights another player's party-duel team."); MaxPartySize = ((BaseUnityPlugin)this).Config.Bind<int>("Duels", "MaxPartySize", 4, "Maximum number of your companions that join a party duel when you toggle it on."); StanceCycleKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Companions", "StanceCycleKey", (KeyCode)101, "Press while hovering your companion to cycle its stance: Follow -> Guard -> Standby."); InventoryKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Companions", "InventoryKey", (KeyCode)121, "Press while hovering your companion to open its inventory (a chest-like panel that also lets you rename it)."); ShowMapPins = ((BaseUnityPlugin)this).Config.Bind<bool>("Companions", "ShowMapPins", true, "Show a live minimap pin at each of your own recruited companions. Pins are client-side — other players never see your companions, and you never see theirs."); CompanionPinColor = ((BaseUnityPlugin)this).Config.Bind<string>("Companions", "CompanionPinColor", "FFB84D", "Hex tint (RRGGBB) for companion map pins, which use the player icon in a smaller size. Distinguishes your allies from your own marker."); CompanionPinScale = ((BaseUnityPlugin)this).Config.Bind<float>("Companions", "CompanionPinScale", 0.7f, "Size multiplier for companion map pins (1 = full player-icon size). Smaller keeps them from crowding your own marker."); ShowDeathMarker = ((BaseUnityPlugin)this).Config.Bind<bool>("Companions", "ShowDeathMarker", true, "Drop a persistent death marker (skull) on your map, labelled with the companion's name, when one of your companions dies."); MoveContainerPanel = ((BaseUnityPlugin)this).Config.Bind<bool>("Interface", "MoveContainerPanel", true, "Let the chest/storage UI be repositioned: it opens at ContainerPanelOffset, and you can drag it anywhere on screen by grabbing an empty part of the panel. Set to false to leave the panel exactly where the game puts it. Automatically ignored when BiomeLords is installed (it moves the same panel itself)."); ContainerPanelOffset = ((BaseUnityPlugin)this).Config.Bind<string>("Interface", "ContainerPanelOffset", "auto", "Where the chest/storage UI sits, as an \"x,y\" pixel offset from the game's own position (+x right, +y up). \"auto\" places it two inventory rows below, so extra rows added by other mods aren't hidden behind it. Dragging the panel writes the new value here; set it back to \"auto\" to restore the default position."); MenuBarOffset = ((BaseUnityPlugin)this).Config.Bind<string>("Interface", "MenuBarOffset", "0,0", "Nudge the row of menu buttons at the top of the inventory screen by \"x,y\" pixels (+x right, +y up). The row holds Rankings, Tournaments and Bounty Board; the matching function keys keep working either way."); RankingKFactor = ((BaseUnityPlugin)this).Config.Bind<int>("Ranking", "EloKFactor", 32, "Elo K-factor for the duel ladder — higher means bigger rating swings per match."); RankingPairCooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Ranking", "PairCooldownSeconds", 300f, "Seconds before the same two companions can move each other's rating again (W/L still count). Anti friend-farming."); ShowRankOnNameTag = ((BaseUnityPlugin)this).Config.Bind<bool>("Ranking", "ShowRankOnNameTag", true, "Append the ladder rank (e.g. #3) to a companion's floating name when it has one."); RankingUiKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Ranking", "RankingUiKey", (KeyCode)287, "Opens the ranking board — a read-only view of the duel and party ladders. Close it with Escape, like reading a runestone. (de_ladder / de_party_ladder still work in the console.)"); TournamentUiKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Tournaments", "TournamentUiKey", (KeyCode)288, "Opens the tournament board — status, registration (lock a companion's Communion Totem into a slot to enter), the bracket, and admin controls. (de_tournament still works in the console.)"); RequiredEntrantLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Tournaments", "RequiredEntrantLevel", 0, "0 = no restriction. When >0, a 1v1 entrant's companion level must match this exactly to be allowed to register (party entries are not level-gated)."); MaxEntrants = ((BaseUnityPlugin)this).Config.Bind<int>("Tournaments", "MaxEntrants", 4, "Hard cap on tournament entrants. `de_tournament start` clamps any size above this, and a size of 0 (or omitted) now means \"use this cap\" instead of unlimited."); WagersEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Wagers", "WagersEnabled", true, "Master switch for wagered events: player-started tournaments and duel invites staked in Coins or Valcoins. Turn off to leave only the free admin-run tournaments."); WageredBracketSize = ((BaseUnityPlugin)this).Config.Bind<int>("Wagers", "BracketSize", 4, "How many entrants a wagered tournament needs. The bracket starts automatically once it is full, and a tournament that never fills is cancelled with every stake refunded."); WageredRegistrationMinutes = ((BaseUnityPlugin)this).Config.Bind<float>("Wagers", "RegistrationMinutes", 30f, "How long a wagered tournament stays open for registration before it is auto-cancelled and every stake refunded. 0 = never expire (the host or an admin must cancel it)."); TournamentCoinFee = ((BaseUnityPlugin)this).Config.Bind<int>("Wagers", "TournamentCoinFee", 100, "Coins it costs to START a Coin tournament, and the same amount each entrant pays to register."); TournamentCoinPrize = ((BaseUnityPlugin)this).Config.Bind<int>("Wagers", "TournamentCoinPrize", 999, "Coins paid to the champion of a Coin tournament."); TournamentValcoinFee = ((BaseUnityPlugin)this).Config.Bind<int>("Wagers", "TournamentValcoinFee", 10, "Valcoins it costs to START a Valcoin tournament, and the same amount each entrant pays to register. Requires the Valheim Donations wallet API on the server."); TournamentValcoinPrize = ((BaseUnityPlugin)this).Config.Bind<int>("Wagers", "TournamentValcoinPrize", 100, "Valcoins the champion of a Valcoin tournament receives. DISPLAY ONLY on this side: the actual payout is minted by the donations mod from its own valcoin_quests.yaml entry `ls_tournament_prize`, which is the only place allowed to price a Valcoin reward. Keep the two numbers in step."); DuelCoinStake = ((BaseUnityPlugin)this).Config.Bind<int>("Wagers", "DuelCoinStake", 100, "Coins staked by each side of a Coin duel invite. The winner takes both stakes."); DuelValcoinStake = ((BaseUnityPlugin)this).Config.Bind<int>("Wagers", "DuelValcoinStake", 10, "Valcoins staked by each side of a Valcoin duel invite. The winner takes both stakes — no Valcoins are created, they only change hands."); BountyEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Bounty", "Enabled", true, "Master switch for bounty hunting. Even when true the feature only runs on a server/host that ALSO has BiomeLords, ValheimServerGuide and Valheim Donations installed — set it to false to switch bounties off on a server that would otherwise qualify. Check the log line \"[bounty] feature gate: ON/OFF\" to see which condition failed."); BountySearchMinRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "SearchMinRadius", 500f, "Closest a bounty may be posted to the world centre, in metres. Keeps targets out of the starting area."); BountySearchMaxRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "SearchMaxRadius", 6000f, "Furthest a bounty may be posted from the world centre, in metres. Keep well inside the world edge (~10000) so candidates don't fall into the edge ocean."); BountyLandCheckRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "LandCheckRadius", 80f, "How far around a candidate spot the ground must be dry land, in metres. Checked as three rings — clearing the outermost means the landmass is at least twice this across, which is what rules out islets and narrow spits. Lower it only if bounties become hard to place."); BountyWaterMargin = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "WaterMargin", 3f, "How far above sea level the ground must sit, in metres, to count as dry. Raise it to push bounties further inland from shorelines."); BountyMaxHeightVariance = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "MaxHeightVariance", 10f, "Largest height swing allowed in the immediate area around a bounty, in metres. Rejects cliff faces and spires where a fight can't happen. Raise it for more mountain bounties."); BountySampleAttempts = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "SampleAttempts", 200, "How many random spots to test before giving up on placing one bounty. Higher is slower but more reliable on worlds with little qualifying land."); BountyTierBlackForest = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "TierBlackForest", 1, "Base difficulty tier (1-5) for bounties posted in the Black Forest."); BountyTierSwamp = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "TierSwamp", 2, "Base difficulty tier (1-5) for bounties posted in the Swamp."); BountyTierMountain = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "TierMountain", 3, "Base difficulty tier (1-5) for bounties posted in the Mountains."); BountyTierPlains = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "TierPlains", 4, "Base difficulty tier (1-5) for bounties posted in the Plains."); BountyHealthBase = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "HealthMultiplierBase", 3f, "Health multiplier for a tier 1 bounty target, over the creature's normal health. Applied on top of its star level."); BountyHealthGrowth = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "HealthMultiplierGrowth", 1.6f, "How much the health multiplier compounds per tier. With the defaults: tier 1 = 3x, tier 2 = 4.8x, tier 3 = 7.7x, tier 4 = 12.3x, tier 5 = 19.7x."); BountyMaxStarLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "MaxStarLevel", 3, "Cap on the vanilla star level given to a bounty target. Stars carry DAMAGE scaling, so raising this makes targets hit far harder, not just survive longer — most of a tier's difficulty is meant to come from health and escort size instead."); BountyMinionsBase = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "MinionsBase", 1, "How many escort minions a tier 1 bounty target has."); BountyMinionsPerTier = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "MinionsPerTier", 1, "Extra escort minions added per tier above 1."); BountyMaxMinions = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "MaxMinions", 6, "Hard cap on escort minions, whatever the tier. Bounds the AI load of one fight."); BountyMinionRingRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "MinionRingRadius", 6f, "How far from the bounty target its escort spawns, in metres."); BountyAlertRangeMultiplier = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "AlertRangeMultiplier", 2f, "How much further than normal a bounty creature notices hunters. Bounty Dvergr hunt on sight rather than waiting to be provoked, so they notice you first."); BountyRoamRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "RoamRadius", 12f, "How far a bounty creature wanders from where it was posted, in metres. Keeps the map pin meaningful; it still chases hunters normally."); BountyMaxTierBonus = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "MaxTierBonus", 1, "How many tiers a top-standing bounty hunter's rewards may be bumped by. The bump selects a higher-tier reward entry, so better hunters get better bundles without a second reward table."); BountyValcoinBaseChance = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "ValcoinBaseChance", 0.1f, "Chance (0-1) that a tier 1 bounty pays Valcoin, before any rank bonus. Valcoin is a REWARD ONLY — nothing in bounty hunting can be bought with it."); BountyValcoinChancePerTier = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "ValcoinChancePerTier", 0.05f, "Extra Valcoin chance (0-1) added per tier above 1."); BountyValcoinRankBonus = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "ValcoinRankBonus", 0.25f, "Extra Valcoin chance (0-1) for the #1 ranked duel/party player, tapering to zero at ValcoinRankDepth. This is what ties the coin payout to the duel and tournament ladders."); BountyValcoinRankDepth = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "ValcoinRankDepth", 10, "How deep the ladder bonus reaches. At rank 1 the full ValcoinRankBonus applies; at this rank and below, none of it does."); BountyValcoinMaxChance = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "ValcoinMaxChance", 0.75f, "Hard cap (0-1) on the Valcoin payout chance, so a payout is never guaranteed however high a player ranks."); BountyPointsPerTier = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "PointsPerTier", 10, "Bounty-ladder points awarded per tier of the bounty answered (tier 3 pays 3x this). Tier-weighting is what stops the board being won by grinding easy postings."); BountyLeaderboardBonusRank = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "LeaderboardBonusRank", 3, "Hunters ranked this high or better on the bounty ladder get +1 reward tier (capped by MaxTierBonus). 0 disables the bonus entirely."); BountyUiKey = ((BaseUnityPlugin)this).Config.Bind<KeyCode>("Bounty", "BountyUiKey", (KeyCode)289, "Opens the Wanted Board — open postings, your active bounty, and the hunter standings. There is also a Bounty Board button in your inventory. Close it with Escape."); BountyMaxBoardEntries = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "MaxBoardEntries", 3, "How many OPEN postings the board keeps available. Postings players have accepted don't count toward this, so the board still offers work while others are out hunting."); BountyMaxActivePerPlayer = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "MaxActiveBountyPerPlayer", 1, "How many bounties one player may hold at a time."); BountyArrivalRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "ArrivalRadius", 80f, "How close a hunter must get before the bounty's creatures are spawned, in metres. They aren't spawned until someone travels there — Valheim doesn't simulate unloaded zones, so a camp placed in advance would sit frozen."); BountyButtonOffset = ((BaseUnityPlugin)this).Config.Bind<string>("Bounty", "InventoryButtonOffset", "0,0", "LEGACY — superseded by Interface/MenuBarOffset. The Bounty Board button is now one of a row of menu buttons, so this nudges the WHOLE row. Left in place because it still works: if you had already dialled a value in here it is honoured and MenuBarOffset is ignored. Leave it at \"0,0\" to use MenuBarOffset instead."); BountyRefreshHours = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "RefreshHours", 24f, "How long an UNCLAIMED posting stays on the board before it's retired and replaced, in real hours. Accepted bounties never expire — a hunter part-way to their mark keeps it. Set to 0 to disable rotation entirely."); BountyEliteChance = ((BaseUnityPlugin)this).Config.Bind<float>("Bounty", "EliteChance", 0.25f, "Chance (0-1) that a newly posted bounty is an ELITE (top-tier) one. Only ever one open at a time, and only hunters ranked high enough may take it — see EliteRankTopN. The top tier is never reached from a biome alone, so this is the only way it appears."); BountyEliteRankTopN = ((BaseUnityPlugin)this).Config.Bind<int>("Bounty", "EliteRankTopN", 10, "How high a player must rank on the duel OR party ladder to answer elite postings. This is the second, non-Valcoin reason to compete: rank buys ACCESS here, where the coin roll only changes a chance. 0 disables the gate (anyone may take them)."); _harmony = new Harmony("com.lostscrollsii"); _harmony.PatchAll(); ((Component)this).gameObject.AddComponent<CompanionMapPins>(); ((Component)this).gameObject.AddComponent<CommunionRite>(); ((Component)this).gameObject.AddComponent<SealingRite>(); ((Component)this).gameObject.AddComponent<CompanionInventoryGui>(); ((Component)this).gameObject.AddComponent<ContainerPanelPositioner>(); ((Component)this).gameObject.AddComponent<TournamentClient>(); ((Component)this).gameObject.AddComponent<TournamentRegistration>(); ((Component)this).gameObject.AddComponent<BountyBoardRunner>(); ((Component)this).gameObject.AddComponent<BountyBoardPanel>(); Log.LogInfo((object)"Lost Scrolls II v0.9.1 loaded."); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void Update() { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_014a: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && !TextInput.IsVisible() && (!((Object)(object)Chat.instance != (Object)null) || !Chat.instance.HasFocus()) && !Console.IsVisible() && !CompanionInventoryGui.IsTyping && (!TournamentRegistration.IsOpen || Input.GetKeyDown(TournamentUiKey.Value)) && (!BountyBoardPanel.IsOpen || Input.GetKeyDown(BountyUiKey.Value))) { if (Input.GetKeyDown(BountyUiKey.Value)) { BountyBoardPanel.Toggle(localPlayer); } if (Input.GetKeyDown(CommunionKey.Value)) { HandleFeedInput(localPlayer); } if (CommunionRite.BlockHeld()) { TryBeginCommune(localPlayer); TryBeginSeal(localPlayer); } if (Input.GetKeyDown(ChoreAssignKey.Value)) { HandleChoreAssignInput(localPlayer); } if (Input.GetKeyDown(DuelSelectKey.Value)) { HandleDuelInput(localPlayer); } if (Input.GetKeyDown(PartyDuelKey.Value)) { HandlePartyDuelInput(localPlayer); } if (Input.GetKeyDown(StanceCycleKey.Value)) { HandleStanceCycleInput(localPlayer); } if (Input.GetKeyDown(InventoryKey.Value)) { HandleInventoryInput(localPlayer); } if (Input.GetKeyDown(RankingUiKey.Value)) { RankingBoard.Open(); } if (Input.GetKeyDown(TournamentUiKey.Value)) { TournamentRegistration.Toggle(localPlayer); } } } private void HandleInventoryInput(Player player) { GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if ((Object)(object)hoverObject == (Object)null) { return; } Character componentInParent = hoverObject.GetComponentInParent<Character>(); DvergrCompanion dvergrCompanion = ((componentInParent != null) ? ((Component)componentInParent).GetComponent<DvergrCompanion>() : null); if ((Object)(object)dvergrCompanion == (Object)null) { return; } if (!dvergrCompanion.IsOwner(player)) { MessageHud.instance.ShowMessage((MessageType)2, "This companion answers to another.", 0, (Sprite)null, false); return; } CompanionInventory companionInventory = ((Component)dvergrCompanion).GetComponent<CompanionInventory>(); if ((Object)(object)companionInventory == (Object)null) { companionInventory = ((Component)dvergrCompanion).gameObject.AddComponent<CompanionInventory>(); } CompanionInventoryGui.Open(dvergrCompanion, companionInventory); } private void HandleFeedInput(Player player) { GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if ((Object)(object)hoverObject == (Object)null) { return; } Character componentInParent = hoverObject.GetComponentInParent<Character>(); if ((Object)(object)componentInParent == (Object)null) { return; } DvergrCompanion component = ((Component)componentInParent).GetComponent<DvergrCompanion>(); if (!((Object)(object)component == (Object)null)) { bool flag = component.IsOwner(player); if (MeadFeedingService.TryFeed(componentInParent, player)) { MessageHud.instance.ShowMessage((MessageType)2, flag ? "Your ally drinks deep." : (component.DisplayName + " drinks deep."), 0, (Sprite)null, false); } else { MessageHud.instance.ShowMessage((MessageType)2, "You have no health mead to offer.", 0, (Sprite)null, false); } } } private void TryBeginCommune(Player player) { if ((Object)(object)CommunionRite.Instance == (Object)null || CommunionRite.Instance.IsActive) { return; } GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if (!((Object)(object)hoverObject == (Object)null)) { Character componentInParent = hoverObject.GetComponentInParent<Character>(); if (!((Object)(object)componentInParent == (Object)null) && !((Object)(object)((Component)componentInParent).GetComponent<DvergrCompanion>() != (Object)null) && CommunionService.IsSubduedDvergr(componentInParent)) { CommunionRite.Instance.Begin(componentInParent, player); } } } private void TryBeginSeal(Player player) { SealingRite instance = SealingRite.Instance; if ((Object)(object)instance == (Object)null || instance.IsActive || ((Object)(object)CommunionRite.Instance != (Object)null && CommunionRite.Instance.IsActive)) { return; } GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if (!((Object)(object)hoverObject == (Object)null)) { Character componentInParent = hoverObject.GetComponentInParent<Character>(); DvergrCompanion dvergrCompanion = (((Object)(object)componentInParent != (Object)null) ? ((Component)componentInParent).GetComponent<DvergrCompanion>() : null); if (!((Object)(object)dvergrCompanion == (Object)null)) { instance.Begin(dvergrCompanion, player); } } } private void HandleChoreAssignInput(Player player) { //IL_029d: Unknown result type (might be due to invalid IL or missing references) //IL_0337: Unknown result type (might be due to invalid IL or missing references) GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if ((Object)(object)hoverObject == (Object)null) { return; } Character componentInParent = hoverObject.GetComponentInParent<Character>(); DvergrCompanion dvergrCompanion = (((Object)(object)componentInParent != (Object)null) ? ((Component)componentInParent).GetComponent<DvergrCompanion>() : null); if ((Object)(object)dvergrCompanion != (Object)null) { if (!dvergrCompanion.IsOwner(player)) { MessageHud.instance.ShowMessage((MessageType)2, "This companion answers to another.", 0, (Sprite)null, false); return; } ChoreAI component = ((Component)dvergrCompanion).GetComponent<ChoreAI>(); if ((Object)(object)component != (Object)null && component.IsAssigned) { component.Unassign(); MessageHud.instance.ShowMessage((MessageType)2, "Ally returns to your side.", 0, (Sprite)null, false); } else { MessageHud.instance.ShowMessage((MessageType)2, "This ally has no chore to leave.", 0, (Sprite)null, false); } return; } Smelter componentInParent2 = hoverObject.GetComponentInParent<Smelter>(); Fermenter val = (((Object)(object)componentInParent2 == (Object)null) ? hoverObject.GetComponentInParent<Fermenter>() : null); CookingStation val2 = (((Object)(object)componentInParent2 == (Object)null && (Object)(object)val == (Object)null) ? hoverObject.GetComponentInParent<CookingStation>() : null); Pickable val3 = (((Object)(object)componentInParent2 == (Object)null && (Object)(object)val == (Object)null && (Object)(object)val2 == (Object)null) ? hoverObject.GetComponentInParent<Pickable>() : null); ItemStand val4 = null; Character val5 = null; Container val6 = null; if ((Object)(object)componentInParent2 == (Object)null && (Object)(object)val == (Object)null && (Object)(object)val2 == (Object)null && (Object)(object)val3 == (Object)null) { ItemStand componentInParent3 = hoverObject.GetComponentInParent<ItemStand>(); if ((Object)(object)componentInParent3 != (Object)null && componentInParent3.HaveAttachment() && componentInParent3.GetAttachedItem() == "Cultivator") { val4 = componentInParent3; } if ((Object)(object)val4 == (Object)null) { Character componentInParent4 = hoverObject.GetComponentInParent<Character>(); if ((Object)(object)componentInParent4 != (Object)null && componentInParent4.IsTamed() && (Object)(object)((Component)componentInParent4).GetComponent<DvergrCompanion>() == (Object)null) { val5 = componentInParent4; } if ((Object)(object)val5 == (Object)null) { val6 = hoverObject.GetComponentInParent<Container>(); } } } if ((Object)(object)componentInParent2 == (Object)null && (Object)(object)val == (Object)null && (Object)(object)val2 == (Object)null && (Object)(object)val3 == (Object)null && (Object)(object)val4 == (Object)null && (Object)(object)val5 == (Object)null && (Object)(object)val6 == (Object)null) { return; } ChoreAI choreAI = ChoreAI.ClaimantOf(((Object)(object)componentInParent2 != (Object)null) ? ((Component)componentInParent2).gameObject : (((Object)(object)val != (Object)null) ? ((Component)val).gameObject : (((Object)(object)val2 != (Object)null) ? ((Component)val2).gameObject : (((Object)(object)val3 != (Object)null) ? ((Component)val3).gameObject : (((Object)(object)val4 != (Object)null) ? ((Component)val4).gameObject : (((Object)(object)val5 != (Object)null) ? ((Component)val5).gameObject : (((Object)(object)val6 != (Object)null) ? ((Component)val6).gameObject : null))))))); if ((Object)(object)choreAI == (Object)null && (Object)(object)val5 != (Object)null) { choreAI = ChoreAI.FeederCovering(((Component)val5).transform.position); } if ((Object)(object)choreAI != (Object)null) { DvergrCompanion component2 = ((Component)choreAI).GetComponent<DvergrCompanion>(); if ((Object)(object)component2 != (Object)null && component2.IsOwner(player)) { choreAI.Unassign(); MessageHud.instance.ShowMessage((MessageType)2, "Ally returns to your side.", 0, (Sprite)null, false); } else { MessageHud.instance.ShowMessage((MessageType)2, choreAI.WorkerName + " is already working here.", 0, (Sprite)null, false); } return; } DvergrCaste? requiredCaste = (((Object)(object)componentInParent2 != (Object)null) ? ChoreRules.RequiredCaste(componentInParent2) : new DvergrCaste?((!((Object)(object)val6 != (Object)null)) ? DvergrCaste.SupportMage : DvergrCaste.Rogue)); DvergrCompanion dvergrCompanion2 = FindNearestRecruitedCompanion(((Component)player).transform.position, ChoreAssignRadius.Value, requiredCaste, player, freeOnly: true); if ((Object)(object)dvergrCompanion2 == (Object)null) { string text = (requiredCaste.HasValue ? ("Only your own free " + ChoreRules.DisplayName(requiredCaste.Value) + " can do this — none nearby.") : "No free ally of yours nearby to assign."); MessageHud.instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false); return; } ChoreAI choreAI2 = ((Component)dvergrCompanion2).GetComponent<ChoreAI>(); if ((Object)(object)choreAI2 == (Object)null) { choreAI2 = ((Component)dvergrCompanion2).gameObject.AddComponent<ChoreAI>(); } if ((Object)(object)componentInParent2 != (Object)null) { choreAI2.AssignToSmelter(componentInParent2); MessageHud.instance.ShowMessage((MessageType)2, "Ally tends the station.", 0, (Sprite)null, false); } else if ((Object)(object)val != (Object)null) { choreAI2.AssignToFermenter(val); MessageHud.instance.ShowMessage((MessageType)2, "Ally tends the brew.", 0, (Sprite)null, false); } else if ((Object)(object)val2 != (Object)null) { choreAI2.AssignToCooking(val2); MessageHud.instance.ShowMessage((MessageType)2, "Ally tends the cookfire.", 0, (Sprite)null, false); } else if ((Object)(object)val3 != (Object)null || (Object)(object)val4 != (Object)null) { choreAI2.AssignToFarm(((Object)(object)val3 != (Object)null) ? ((Component)val3).gameObject : ((Component)val4).gameObject); MessageHud.instance.ShowMessage((MessageType)2, "Ally tends the field.", 0, (Sprite)null, false); } else if ((Object)(object)val5 != (Object)null) { choreAI2.AssignToFeedAnimals(((Component)val5).gameObject); MessageHud.instance.ShowMessage((MessageType)2, "Ally tends the animals.", 0, (Sprite)null, false); } else { choreAI2.AssignToHaul(val6); MessageHud.instance.ShowMessage((MessageType)2, "Ally hauls to this chest.", 0, (Sprite)null, false); } } private void HandleDuelInput(Player player) { GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if (!((Object)(object)hoverObject == (Object)null)) { Character componentInParent = hoverObject.GetComponentInParent<Character>(); DvergrCompanion dvergrCompanion = (((Object)(object)componentInParent != (Object)null) ? ((Component)componentInParent).GetComponent<DvergrCompanion>() : null); if ((Object)(object)dvergrCompanion == (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, "That is not an ally you can pit in a duel.", 0, (Sprite)null, false); } else if (!dvergrCompanion.IsOwner(player)) { MessageHud.instance.ShowMessage((MessageType)2, "Only its owner can send this companion to duel.", 0, (Sprite)null, false); } else if (dvergrCompanion.DuelMode) { dvergrCompanion.ExitDuelMode(DvergrCompanion.DuelExitReason.OwnerStopped); } else if (!dvergrCompanion.EnterDuelMode()) { MessageHud.instance.ShowMessage((MessageType)2, "This companion cannot duel right now — unassign its chore first.", 0, (Sprite)null, false); } } } private void HandlePartyDuelInput(Player player) { //IL_0170: Unknown result type (might be due to invalid IL or missing references) GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if ((Object)(object)hoverObject == (Object)null) { return; } Character componentInParent = hoverObject.GetComponentInParent<Character>(); DvergrCompanion dvergrCompanion = (((Object)(object)componentInParent != (Object)null) ? ((Component)componentInParent).GetComponent<DvergrCompanion>() : null); if ((Object)(object)dvergrCompanion == (Object)null) { MessageHud.instance.ShowMessage((MessageType)2, "That is not an ally you can send to a party duel.", 0, (Sprite)null, false); return; } if (!dvergrCompanion.IsOwner(player)) { MessageHud.instance.ShowMessage((MessageType)2, "Only its owner can send this companion to duel.", 0, (Sprite)null, false); return; } long playerID = player.GetPlayerID(); bool flag = false; foreach (DvergrCompanion item in DvergrCompanion.All) { if ((Object)(object)item != (Object)null && item.OwnerId == playerID && item.PartyDuelMode) { flag = true; break; } } if (flag) { int num = 0; foreach (DvergrCompanion item2 in DvergrCompanion.All) { if ((Object)(object)item2 != (Object)null && item2.OwnerId == playerID && item2.PartyDuelMode) { item2.ExitPartyDuelMode(DvergrCompanion.DuelExitReason.OwnerStopped); num++; } } MessageHud.instance.ShowMessage((MessageType)2, $"Your party ({num}) stands down.", 0, (Sprite)null, false); return; } List<Character> list = new List<Character>(); Character.GetCharactersInRange(((Component)player).transform.position, 25f, list); List<DvergrCompanion> list2 = new List<DvergrCompanion>(); foreach (Character item3 in list) { DvergrCompanion component = ((Component)item3).GetComponent<DvergrCompanion>(); if (!((Object)(object)component == (Object)null) && component.OwnerId == playerID && component.Stance == CompanionStance.Follow) { ChoreAI component2 = ((Component)item3).GetComponent<ChoreAI>(); if ((component2 == null || !component2.IsAssigned) && !component.DuelMode && !component.IsFeral) { list2.Add(component); } } } if (list2.Count == 0) { MessageHud.instance.ShowMessage((MessageType)2, "No free Follow-stance allies of yours nearby to form a party.", 0, (Sprite)null, false); return; } list2.Sort((DvergrCompanion a, DvergrCompanion b) => Vector3.Distance(((Component)player).transform.position, ((Component)a).transform.position).CompareTo(Vector3.Distance(((Component)player).transform.position, ((Component)b).transform.position))); int num2 = Mathf.Max(1, MaxPartySize.Value); int num3 = 0; foreach (DvergrCompanion item4 in list2) { if (num3 >= num2) { break; } if (item4.EnterPartyDuelMode()) { num3++; } } MessageHud.instance.ShowMessage((MessageType)2, $"Your party ({num3}) squares up — waiting for a rival party.", 0, (Sprite)null, false); } private void HandleStanceCycleInput(Player player) { GameObject hoverObject = ((Humanoid)player).GetHoverObject(); if ((Object)(object)hoverObject == (Object)null) { return; } Character componentInParent = hoverObject.GetComponentInParent<Character>(); DvergrCompanion dvergrCompanion = ((componentInParent != null) ? ((Component)componentInParent).GetComponent<DvergrCompanion>() : null); if ((Object)(object)dvergrCompanion == (Object)null) { return; } if (!dvergrCompanion.IsOwner(player)) { MessageHud.instance.ShowMessage((MessageType)2, "This companion answers to another.", 0, (Sprite)null, false); return; } ChoreAI component = ((Component)componentInParent).GetComponent<ChoreAI>(); if ((component != null && component.IsAssigned) || dvergrCompanion.InAnyDuelMode) { MessageHud.instance.ShowMessage((MessageType)2, "Ally is busy — unassign it first.", 0, (Sprite)null, false); return; } CompanionStance companionStance = ((dvergrCompanion.Stance == CompanionStance.Follow) ? CompanionStance.Guard : ((dvergrCompanion.Stance == CompanionStance.Guard) ? CompanionStance.Standby : CompanionStance.Follow)); dvergrCompanion.SetStance(companionStance, ((Component)player).gameObject); MessageHud.instance.ShowMessage((MessageType)2, "Ally stance: " + companionStance switch { CompanionStance.Guard => "Guard", CompanionStance.Follow => "Follow", _ => "Standby", } + ".", 0, (Sprite)null, false); } private static DvergrCompanion FindNearestRecruitedCompanion(Vector3 origin, float radius, DvergrCaste? requiredCaste = null, Player owner = null, bool freeOnly = false) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) List<Character> list = new List<Character>(); Character.GetCharactersInRange(origin, radius, list); DvergrCompanion result = null; float num = float.MaxValue; foreach (Character item in list) { DvergrCompanion component = ((Component)item).GetComponent<DvergrCompanion>(); if ((Object)(object)component == (Object)null || (requiredCaste.HasValue && component.Caste != requiredCaste.Value) || ((Object)(object)owner != (Object)null && !component.IsOwner(owner))) { continue; } if (freeOnly) { ChoreAI component2 = ((Component)item).GetComponent<ChoreAI>(); if (component2 != null && component2.IsAssigned) { continue; } } float num2 = Vector3.Distance(origin, ((Component)item).transform.position); if (num2 < num) { num = num2; result = component; } } return result; } } } namespace LostScrollsII.Ranking { public static class CompetitiveJson { private class Writer { private readonly StringBuilder _sb = new StringBuilder(256); private readonly bool _pretty; private int _depth; private bool _needComma; public Writer(bool pretty) { _pretty = pretty; } public void BeginObj() { Sep(); _sb.Append('{'); _depth++; _needComma = false; } public void EndObj() { _depth--; NewlineIndent(); _sb.Append('}'); _needComma = true; } public void BeginArr() { _sb.Append('['); _depth++; _needComma = false; } public void EndArr() { _depth--; NewlineIndent(); _sb.Append(']'); _needComma = true; } public void Key(string k) { Sep(); _sb.Append('"').Append(k).Append("\":"); if (_pretty) { _sb.Append(' '); } _needComma = false; } public void Field(string k, string v) { Key(k); WriteString(v); _needComma = true; } public void Field(string k, int v) { Key(k); _sb.Append(v.ToString(CultureInfo.InvariantCulture)); _needComma = true; } public void Field(string k, long v) { Key(k); _sb.Append(v.ToString(CultureInfo.InvariantCulture)); _needComma = true; } public void Field(string k, bool v) { Key(k); _sb.Append(v ? "true" : "false"); _needComma = true; } public void Field(string k, float v) { Key(k); _sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); _needComma = true; } public void ArrString(string v) { Sep(); WriteString(v); _needComma = true; } private void Sep() { if (_needComma) { _sb.Append(','); } NewlineIndent(); } private void NewlineIndent() { if (_pretty && _depth != 0) { _sb.Append('\n').Append(' ', _depth * 4); } } private void WriteString(string v) { _sb.Append('"'); string text = v ?? string.Empty; foreach (char c in text) { switch (c) { case '"': _sb.Append("\\\""); continue; case '\\': _sb.Append("\\\\"); continue; case '\n': _sb.Append("\\n"); continue; case '\r': _sb.Append("\\r"); continue; case '\t': _sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = _sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4")); } else { _sb.Append(c); } } _sb.Append('"'); } public override string ToString() { return _sb.ToString(); } } public static string Write(LeaderboardData d, bool pretty = false) { d = d ?? new LeaderboardData(); Writer writer = new Writer(pretty); writer.BeginObj(); writer.Field("seasonId", d.seasonId); writer.Key("companions"); writer.BeginArr(); foreach (CompanionRecord item in d.companions ?? new List<CompanionRecord>()) { WriteCompanion(writer, item); } writer.EndArr(); writer.Key("parties"); writer.BeginArr(); foreach (PartyRecord item2 in d.parties ?? new List<PartyRecord>()) { WriteParty(writer, item2); } writer.EndArr(); writer.EndObj(); return writer.ToString(); } public static string Write(TournamentBook b, bool pretty = false) { b = b ?? new TournamentBook(); Writer writer = new Writer(pretty); writer.BeginObj(); writer.Key("tournaments"); writer.BeginArr(); foreach (TournamentState item in b.tournaments ?? new List<TournamentState>()) { if (item != null) { WriteTournamentObj(writer, item); } } writer.EndArr(); writer.EndObj(); return writer.ToString(); } public static TournamentBook ReadTournamentBook(string json) { TournamentBook tournamentBook = new TournamentBook(); if (!(Parse(json) is Dictionary<string, object> dictionary)) { return tournamentBook; } if (dictionary.ContainsKey("tournaments")) { foreach (object item in Arr(dictionary, "tournaments")) { if (item is Dictionary<string, object> root) { tournamentBook.tournaments.Add(ReadTournamentObj(root)); } } return tournamentBook; } TournamentState tournamentState = ReadTournamentObj(dictionary); tournamentState.key = ""; tournamentBook.tournaments.Add(tournamentState); return tournamentBook; } public static string Write(TournamentState s, bool pretty = false) { Writer writer = new Writer(pretty); WriteTournamentObj(writer, s ?? new TournamentState()); return writer.ToString(); } private static void WriteTournamentObj(Writer w, TournamentState s) { w.BeginObj(); w.Field("key", s.key); w.Field("currency", s.currency); w.Field("entryFee", s.entryFee); w.Field("prize", s.prize); w.Field("hostId", s.hostId); w.Field("hostName", s.hostName); w.Field("openedTicks", s.openedTicks); w.Field("hostCredit", s.hostCredit); w.Field("completedTicks", s.completedTicks); w.Field("active", s.active); w.Field("mode", s.mode); w.Field("phase", s.phase); w.Field("size", s.size); w.Field("bracketSize", s.bracketSize); w.Field("currentRound", s.currentRound); w.Field("championId", s.championId); w.Field("championLabel", s.championLabel); w.Field("seasonId", s.seasonId); w.Field("eliminationType", s.eliminationType); w.Field("totalRounds", s.totalRounds); w.Key("entrants"); w.BeginArr(); foreach (TournamentEntrant item in s.entrants ?? new List<TournamentEntrant>()) { WriteEntrant(w, item); } w.EndArr(); w.Key("matches"); w.BeginArr(); foreach (TournamentMatch item2 in s.matches ?? new List<TournamentMatch>()) { WriteMatch(w, item2); } w.EndArr(); w.EndObj(); } public static string Write(BountyLadderData d, bool pretty = false) { d = d ?? new BountyLadderData(); Writer writer = new Writer(pretty); writer.BeginObj(); writer.Field("seasonId", d.seasonId); writer.Key("hunters"); writer.BeginArr(); foreach (BountyHunterRecord item in d.hunters ?? new List<BountyHunterRecord>()) { if (item != null) { writer.BeginObj(); writer.Field("ownerId", item.ownerId); writer.Field("ownerName", item.ownerName); writer.Field("points", item.points); writer.Field("kills", item.kills); writer.Field("communes", item.communes); writer.Field("bestTier", item.bestTier); writer.Field("lastResolvedTicks", item.lastResolvedTicks); writer.Field("seasonId", item.seasonId); writer.EndObj(); } } writer.EndArr(); writer.EndObj(); return writer.ToString(); } public static BountyLadderData ReadBountyLadder(string json) { Dictionary<string, object> dictionary = Parse(json) as Dictionary<string, object>; BountyLadderData bountyLadderData = new BountyLadderData(); if (dictionary == null) { return bountyLadderData; } bountyLadderData.seasonId = I(dictionary, "seasonId", bountyLadderData.seasonId); foreach (object item in Arr(dictionary, "hunters")) { if (item is Dictionary<string, object> d) { bountyLadderData.hunters.Add(new BountyHunterRecord { ownerId = L(d, "ownerId", 0L), ownerName = S(d, "ownerName"), points = I(d, "points"), kills = I(d, "kills"), communes = I(d, "communes"), bestTier = I(d, "bestTier"), lastResolvedTicks = L(d, "lastResolvedTicks", 0L), seasonId = I(d, "seasonId", 1) }); } } return bountyLadderData; } public static string Write(BountyBoardData d, bool pretty = false) { d = d ?? new BountyBoardData(); Writer writer = new Writer(pretty); writer.BeginObj(); writer.Key("postings"); writer.BeginArr(); foreach (BountyPosting item in d.postings ?? new List<BountyPosting>()) { if (item != null) { writer.BeginObj(); writer.Field("id", item.id); writer.Field("tier", item.tier); writer.Field("biome", item.biome); writer.Field("x", item.x); writer.Field("y", item.y); writer.Field("z", item.z); writer.Field("acceptedBy", item.acceptedBy); writer.Field("acceptedByName", item.acceptedByName); writer.Field("spawned", item.spawned); writer.Field("postedTicks", item.postedTicks); writer.Field("tutorial", item.tutorial); writer.EndObj(); } } writer.EndArr(); writer.EndObj(); return writer.ToString(); } public static BountyBoardData ReadBountyBoard(string json) { Dictionary<string, object> dictionary = Parse(json) as Dictionary<string, object>; BountyBoardData bountyBoardData = new BountyBoardData(); if (dictionary == null) { return bountyBoardData; } foreach (object item in Arr(dictionary, "postings")) { if (item is Dictionary<string, object> d) { bountyBoardData.postings.Add(new BountyPosting { id = S(d, "id"), tier = I(d, "tier", 1), biome = I(d, "biome"), x = F(d, "x"), y = F(d, "y"), z = F(d, "z"), acceptedBy = L(d, "acceptedBy", 0L), acceptedByName = S(d, "acceptedByName"), spawned = B(d, "spawned"), postedTicks = L(d, "postedTicks", 0L), tutorial = B(d, "tutorial") }); } } return bountyBoardData; } public static string Write(ChampionsData d, bool pretty = false) { d = d ?? new ChampionsData(); Writer writer = new Writer(pretty); writer.BeginObj(); writer.Key("champions"); writer.BeginArr(); foreach (ChampionRecord item in d.champions ?? new List<ChampionRecord>()) { writer.BeginObj(); writer.Field("mode", item.mode); writer.Field("championLabel", item.championLabel); writer.Field("ownerName", item.ownerName); writer.Field("bracketSize", item.bracketSize); writer.Field("seasonId", item.seasonId); writer.Field("dateTicks", item.dateTicks); writer.EndObj(); } writer.EndArr(); writer.EndObj(); return writer.ToString(); } private static void WriteCompanion(Writer w, CompanionRecord c) { w.BeginObj(); w.Field("companionId", c.companionId); w.Field("ownerId", c.ownerId); w.Field("ownerName", c.ownerName); w.Field("companionName", c.companionName); w.Field("caste", c.caste); w.Field("wins", c.wins); w.Field("losses", c.losses); w.Field("winStreak", c.winStreak); w.Field("bestStreak", c.bestStreak); w.Field("rating", c.rating); w.Field("lastMatchTicks", c.lastMatchTicks); w.Field("seasonId", c.seasonId); w.EndObj(); } private static void WriteParty(Writer w, PartyRecord p) { w.BeginObj(); w.Field("ownerId", p.ownerId); w.Field("ownerName", p.ownerName); w.Field("partyName", p.partyName); w.Field("wins", p.wins); w.Field("losses", p.losses); w.Field("rating", p.rating); w.Field("bestTeamSize", p.bestTeamSize); w.Field("lastMatchTicks", p.lastMatchTicks); w.Field("seasonId", p.seasonId); w.Key("memberSnapshot"); w.BeginArr(); foreach (PartyMemberSnap item in p.memberSnapshot ?? new List<PartyMemberSnap>()) { w.BeginObj(); w.Field("companionId", item.companionId); w.Field("caste", item.caste); w.Field("level", item.level); w.EndObj(); } w.EndArr(); w.EndObj(); } private static void WriteEntrant(Writer w, TournamentEntrant e) { w.BeginObj(); w.Field("entrantId", e.entrantId); w.Field("ownerId", e.ownerId); w.Field("ownerName", e.ownerName); w.Field("label", e.label); w.Field("caste", e.caste); w.Field("seedRating", e.seedRating); w.Field("eliminated", e.eliminated); w.Field("level", e.level); w.Field("losses", e.losses); w.Field("wins", e.wins); w.Field("totemPayload", e.totemPayload); w.Field("feePaid", e.feePaid); w.Key("teamPayloads"); w.BeginArr(); foreach (string item in e.teamPayloads ?? new List<string>()) { w.ArrString(item); } w.EndArr(); w.EndObj(); } private static void WriteMatch(Writer w, TournamentMatch m) { w.BeginObj(); w.Field("round", m.round); w.Field("aId", m.aId); w.Field("bId", m.bId); w.Field("aLabel", m.aLabel); w.Field("bLabel", m.bLabel); w.Field("aLevel", m.aLevel); w.Field("bLevel", m.bLevel); w.Field("winnerId", m.winnerId); w.Field("bracket", m.bracket); w.Field("aReady", m.aReady); w.Field("bReady", m.bReady); w.Field("activated", m.activated); w.EndObj(); } public static LeaderboardData ReadLeaderboard(string json) { Dictionary<string, object> dictionary = Parse(json) as Dictionary<string, object>; LeaderboardData leaderboardData = new LeaderboardData(); if (dictionary == null) { return leaderboardData; } leaderboardData.seasonId = I(dictionary, "seasonId", leaderboardData.seasonId); foreach (object item in Arr(dictionary, "companions")) { if (item is Dictionary<string, object> d) { leaderboardData.companions.Add(new CompanionRecord { companionId = S(d, "companionId"), ownerId = L(d, "ownerId", 0L), ownerName = S(d, "ownerName"), companionName = S(d, "companionName"), caste = I(d, "caste"), wins = I(d, "wins"), losses = I(d, "losses"), winStreak = I(d, "winStreak"), bestStreak = I(d, "bestStreak"), rating = I(d, "rating", 1000), lastMatchTicks = L(d, "lastMatchTicks", 0L), seasonId = I(d, "seasonId", 1) }); } } foreach (object item2 in Arr(dictionary, "parties")) { if (!(item2 is Dictionary<string, object> d2)) { continue; } PartyRecord partyRecord = new PartyRecord { ownerId = L(d2, "ownerId", 0L), ownerName = S(d2, "ownerName"), partyName = S(d2, "partyName"), wins = I(d2, "wins"), losses = I(d2, "losses"), rating = I(d2, "rating", 1000), bestTeamSize = I(d2, "bestTeamSize"), lastMatchTicks = L(d2, "lastMatchTicks", 0L), seasonId = I(d2, "seasonId", 1) }; foreach (object item3 in Arr(d2, "memberSnapshot")) { if (item3 is Dictionary<string, object> d3) { partyRecord.memberSnapshot.Add(new PartyMemberSnap { companionId = S(d3, "companionId"), caste = I(d3, "caste"), level = I(d3, "level") }); } } leaderboardData.parties.Add(partyRecord); } return leaderboardData; } public static TournamentState ReadTournament(string json) { if (Parse(json) is Dictionary<string, object> root) { return ReadTournamentObj(root); } return new TournamentState(); } private static TournamentState ReadTournamentObj(Dictionary<string, object> root) { TournamentState tournamentState = new TournamentState(); if (root == null) { return tournamentState; } tournamentState.key = S(root, "key"); tournamentState.currency = S(root, "currency"); tournamentState.entryFee = I(root, "entryFee"); tournamentState.prize = I(root, "prize"); tournamentState.hostId = L(root, "hostId", 0L); tournamentState.hostName = S(root, "hostName"); tournamentState.openedTicks = L(root, "openedTicks", 0L); tournamentState.hostCredit = I(root, "hostCredit"); tournamentState.completedTicks = L(root, "completedTicks", 0L); tournamentState.active = B(root, "active"); tournamentState.mode = S(root, "mode", tournamentState.mode); tournamentState.phase = S(root, "phase", tournamentState.phase); tournamentState.size = I(root, "size"); tournamentState.bracketSize = I(root, "bracketSize"); tournamentState.currentRound = I(root, "currentRound"); tournamentState.championId = S(root, "championId"); tournamentState.championLabel = S(root, "championLabel"); tournamentState.seasonId = I(root, "seasonId", 1); tournamentState.eliminationType = S(root, "eliminationType", tournamentState.eliminationType); tournamentState.totalRounds = I(root, "totalRounds"); foreach (object item2 in Arr(root, "entrants")) { if (!(item2 is Dictionary<string, object> d)) { continue; } TournamentEntrant tournamentEntrant = new TournamentEntrant { entrantId = S(d, "entrantId"), ownerId = L(d, "ownerId", 0L), ownerName = S(d, "ownerName"), label = S(d, "label"), caste = I(d, "caste"), seedRating = I(d, "seedRating"), eliminated = B(d, "eliminated"), level = I(d, "level"), losses = I(d, "losses"), wins = I(d, "wins"), totemPayload = S(d, "totemPayload"), feePaid = I(d, "feePaid") }; foreach (object item3 in Arr(d, "teamPayloads")) { if (item3 is string item) { tournamentEntrant.teamPayloads.Add(item); } } tournamentState.entrants.Add(tournamentEntrant); } foreach (object item4 in Arr(root, "matches")) { if (item4 is Dictionary<string, object> d2) { tournamentState.matches.Add(new TournamentMatch { round = I(d2, "round"), aId = S(d2, "aId"), bId = S(d2, "bId"), aLabel = S(d2, "aLabel"), bLabel = S(d2, "bLabel"), aLevel = I(d2, "aLevel"), bLevel = I(d2, "bLevel"), winnerId = S(d2, "winnerId"), bracket = S(d2, "bracket", "W"), aReady = B(d2, "aReady"), bReady = B(d2, "bReady"), activated = B(d2, "activated") }); } } return tournamentState; } public static string Write(DuelInviteBoard b, bool pretty = false) { b = b ?? new DuelInviteBoard(); Writer writer = new Writer(pretty); writer.BeginObj(); writer.Key("invites"); writer.BeginArr(); foreach (DuelInvite item in b.invites ?? new List<DuelInvite>()) { if (item != null) { writer.BeginObj(); writer.Field("id", item.id); writer.Field("currency", item.currency); writer.Field("stake", item.stake); writer.Field("phase", item.phase); writer.Field("hostId", item.hostId); writer.Field("hostName", item.hostName); writer.Field("hostEntrantId", item.hostEntrantId); writer.Field("hostLabel", item.hostLabel); writer.Field("hostLevel", item.hostLevel); writer.Field("hostCaste", item.hostCaste); writer.Field("hostPayload", item.hostPayload); writer.Field("hostReady", item.hostReady); writer.Field("oppId", item.oppId); writer.Field("oppName", item.oppName); writer.Field("oppEntrantId", item.oppEntrantId); writer.Field("oppLabel", item.oppLabel); writer.Field("oppLevel", item.oppLevel); writer.Field("oppCaste", item.oppCaste); writer.Field("oppPayload", item.oppPayload); writer.Field("oppReady", item.oppReady); writer.Field("createdTicks", item.createdTicks); writer.Field("completedTicks", item.completedTicks); writer.EndObj(); } } writer.EndArr(); writer.EndObj(); return writer.ToString(); } public static DuelInviteBoard ReadInvites(string json) { DuelInviteBoard duelInviteBoard = new DuelInviteBoard(); if (!(Parse(json) is Dictionary<string, object> d)) { return duelInviteBoard; } foreach (object item in Arr(d, "invites")) { if (item is Dictionary<string, object> d2) { duelInviteBoard.invites.Add(new DuelInvite { id = S(d2, "id"), currency = S(d2, "currency"), stake = I(d2, "stake"), phase = S(d2, "phase", "open"), hostId = L(d2, "hostId", 0L), hostName = S(d2, "hostName"), hostEntrantId = S(d2, "hostEntrantId"), hostLabel = S(d2, "hostLabel"), hostLevel = I(d2, "hostLevel"), hostCaste = I(d2, "hostCaste"), hostPayload = S(d2, "hostPayload"), hostReady = B(d2, "hostReady"), oppId = L(d2, "oppId", 0L), oppName = S(d2, "oppName"), oppEntrantId = S(d2, "oppEntrantId"), oppLabel = S(d2, "oppLabel"), oppLevel = I(d2, "oppLevel"), oppCaste = I(d2, "oppCaste"), oppPayload = S(d2, "oppPayload"), oppReady = B(d2, "oppReady"), createdTicks = L(d2, "createdTicks", 0L), completedTicks = L(d2, "completedTicks", 0L) }); } } return duelInviteBoard; } public static ChampionsData ReadChampions(string json) { Dictionary<string, object> dictionary = Parse(json) as Dictionary<string, object>; ChampionsData championsData = new ChampionsData(); if (dictionary == null) { return championsData; } foreach (object item in Arr(dictionary, "champions")) { if (item is Dictionary<string, object> d) { championsData.champions.Add(new ChampionRecord { mode = S(d, "mode"), championLabel = S(d, "championLabel"), ownerName = S(d, "ownerName"), bracketSize = I(d, "bracketSize"), seasonId = I(d, "seasonId", 1), dateTicks = L(d, "dateTicks", 0L) }); } } return championsData; } private static string S(Dictionary<string, object> d, string k, string def = "") { if (!d.TryGetValue(k, out var value) || !(value is string result)) { return def; } return result; } private static int I(Dictionary<string, object> d, string k, int def = 0) { if (!d.TryGetValue(k, out var value)) { return def; } if (!(value is long num)) { if (!(value is double num2)) { return def; } return (int)num2; } return (int)num; } private static long L(Dictionary<string, object> d, string k, long def = 0L) { if (!d.TryGetValue(k, out var value)) { return def; } if (value is long) { return (long)value; } if (!(value is double num)) { return def; } return (long)num; } private static bool B(Dictionary<string, object> d, string k, bool def = false) { if (d.TryGetValue(k, out var value) && value is bool) { return (bool)value; } return def; } private static float F(Dictionary<string, object> d, string k, float def = 0f) { if (!d.TryGetValue(k, out var value)) { return def; } if (!(value is double num)) { if (!(value is long num2)) { return def; } return num2; } return (float)num; } private static List<object> Arr(Dictionary<string, object> d, string k) { if (!d.TryGetValue(k, out var value) || !(value is List<object> result)) { return new List<object>(); } return result; } private static object Parse(string json) { if (string.IsNullOrEmpty(json)) { return null; } int i = 0; try { return ParseValue(json, ref i); } catch { return null; } } private static object ParseValue(string s, ref int i) { SkipWs(s, ref i); switch (s[i]) { case '{': return ParseObject(s, ref i); case '[': return ParseArray(s, ref i); case '"': return ParseString(s, ref i); case 't': Expect(s, ref i, "true"); return true; case 'f': Expect(s, ref i, "false"); return false; case 'n': Expect(s, ref i, "null"); return null; default: return ParseNumber(s, ref i); } } private static Dictionary<string, object> ParseObject(string s, ref int i) { Dictionary<string, object> dictionary = new Dictionary<string, object>(); i++; SkipWs(s, ref i); if (s[i] == '}') { i++; return dictionary; } while (true) { SkipWs(s, ref i); string key = ParseString(s, ref i); SkipWs(s, ref i); if (s[i] != ':') { throw new FormatException("expected ':'"); } i++; dictionary[key] = ParseValue(s, ref i); SkipWs(s, ref i); if (s[i] != ',') { break; } i++; } if (s[i] == '}') { i++; return dictionary; } throw new FormatException("expected ',' or '}'"); } private static List<object> ParseArray(string s, ref int i) { List<object> list = new List<object>(); i++; SkipWs(s, ref i); if (s[i] == ']') { i++; return list; } while (true) { list.Add(ParseValue(s, ref i)); SkipWs(s, ref i); if (s[i] != ',') { break; } i++; } if (s[i] == ']') { i++; return list; } throw new FormatException("expected ',' or ']'"); } private static string ParseString(string s, ref int i) { if (s[i] != '"') { throw new FormatException("expected '\"'"); } i++; StringBuilder stringBuilder = new StringBuilder(); while (true) { char c = s[i++]; switch (c) { case '"': return stringBuilder.ToString(); case '\\': switch (s[i++]) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'u': stringBuilder.Append((char)int.Parse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture)); i += 4; break; default: throw new FormatException("bad escape"); } break; default: stringBuilder.Append(c); break; } } } private static object ParseNumber(string s, ref int i) { int num = i; while (i < s.Length && (char.IsDigit(s[i]) || s[i] == '-' || s[i] == '+' || s[i] == '.' || s[i] == 'e' || s[i] == 'E')) { i++; } string text = s.Substring(num, i - num); if (text.IndexOf('.') < 0 && text.IndexOf('e') < 0 && text.IndexOf('E') < 0 && long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return result; } return double.Parse(text, CultureInfo.InvariantCulture); } private static void Expect(string s, ref int i, string word) { if (string.CompareOrdinal(s, i, word, 0, word.Length) != 0) { throw new FormatException("expected '" + word + "'"); } i += word.Length; } private static void SkipWs(string s, ref int i) { while (i < s.Length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) { i++; } } } [Serializable] public class DuelInvite { public string id = ""; public string currency = ""; public int stake; public string phase = "open"; public long hostId; public string hostName = ""; public string hostEntrantId = ""; public string hostLabel = ""; public int hostLevel; public int hostCaste; public string hostPayload = ""; public bool hostReady; public long oppId; public string oppName = ""; public string oppEntrantId = ""; public string oppLabel = ""; public int oppLevel; public int oppCaste; public string oppPayload = ""; public bool oppReady; public long createdTicks; public long completedTicks; public bool Involves(long ownerId) { if (ownerId != 0L) { if (hostId != ownerId) { return oppId == ownerId; } return true; } return false; } public bool HasEntrant(string entrantId) { if (!string.IsNullOrEmpty(entrantId)) { if (!(hostEntrantId == entrantId)) { return oppEntrantId == entrantId; } return true; } return false; } public bool IsHostEntrant(string entrantId) { if (!string.IsNullOrEmpty(entrantId)) { return hostEntrantId == entrantId; } return false; } } [Serializable] public class DuelInviteBoard { public List<DuelInvite> invites = new List<DuelInvite>(); public DuelInvite Find(string id) { if (!string.IsNullOrEmpty(id)) { return invites.Find((DuelInvite i) => i != null && i.id == id); } return null; } public DuelInvite ForOwner(long ownerId) { if (ownerId != 0L) { return invites.Find((DuelInvite i) => i?.Involves(ownerId) ?? false); } return null; } public DuelInvite ForEntrant(string entrantId) { if (!string.IsNullOrEmpty(entrantId)) { return invites.Find((DuelInvite i) => i?.HasEntrant(entrantId) ?? false); } return null; } } public static class DuelInviteService { private static DuelInviteBoard _board = new DuelInviteBoard(); private static DuelInviteBoard _snapshot = new DuelInviteBoard(); private const double CompletionGraceSeconds = 6.0; public static DuelInviteBoard Snapshot => _snapshot; public static IEnumerable<DuelInvite> All => (_snapshot?.invites ?? new List<DuelInvite>()).Where((DuelInvite i) => i != null); public static DuelInvite MineIn(long ownerId) { return _snapshot?.ForOwner(ownerId); } public static DuelInvite FindByEntrant(string entrantId) { return _snapshot?.ForEntrant(entrantId); } private static string BoardPath() { return Path.Combine(LeaderboardStore.DataDir(), "duelinvites." + LeaderboardStore.WorldTag() + ".json"); } public static void LoadForCurrentWorld() { try { string path = BoardPath(); _board = (File.Exists(path) ? (CompetitiveJson.ReadInvites(File.ReadAllText(path)) ?? new DuelInviteBoard()) : new DuelInviteBoard()); if (_board.invites == null) { _board.invites = new List<DuelInvite>(); } Plugin.Log.LogInfo((object)$"[invite] resumed {_board.invites.Count} duel invite(s)."); } catch (Exception ex) { _board = new DuelInviteBoard(); Plugin.Log.LogWarning((object)("[invite] load failed: " + ex.Message)); } _snapshot = _board; } private static void Save() { try { File.WriteAllText(BoardPath(), CompetitiveJson.Write(_board, pretty: true)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[invite] save failed: " + ex.Message)); } } public static string SerializeSnapshot() { return CompetitiveJson.Write(_board ?? new DuelInviteBoard()); } public static void ApplySnapshot(string json) { try { DuelInviteBoard duelInviteBoard = CompetitiveJson.ReadInvites(json) ?? new DuelInviteBoard(); if (duelInviteBoard.invites == null) { duelInviteBoard.invites = new List<DuelInvite>(); } _snapshot = duelInviteBoard; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[invite] snapshot apply failed: " + ex.Message)); } } public static void Post(WagerCurrency currency, long hostId, string hostName, string entrantId, string label, int level, int caste, string payload, int clientPaidCoins, Action<bool, string> done) { if (!WagerService.IsAvailable(currency, out var reason)) { done?.Invoke(arg1: false, reason); return; } if (string.IsNullOrEmpty(entrantId)) { done?.Invoke(arg1: false, "That totem has no ladder identity and can't be staked."); return; } if (string.IsNullOrEmpty(payload)) { done?.Invoke(arg1: false, "No companion totem was locked in."); return; } DuelInvite duelInvite = _board.ForOwner(hostId); if (duelInvite != null) { done?.Invoke(arg1: false, (duelInvite.hostId == hostId) ? "You already have a duel invite posted — withdraw it first." : "You have already accepted someone else's duel invite."); return; } int stake = Wager.DuelStake(currency); WagerService.Charge(currency, hostName, stake, "ls_duel_stake", "posted a duel invite", clientPaidCoins, delegate(bool ok, string msg) { if (!ok) { done?.Invoke(arg1: false, msg ?? "The stake could not be taken."); } else if (_board.ForOwner(hostId) != null) { WagerService.RollBackOwnCharge(currency, hostId, hostName, stake, "you were already in another duel invite"); done?.Invoke(arg1: false, "You joined another duel invite while your stake was being processed — it is returned."); } else { DuelInvite duelInvite2 = new DuelInvite { id = Guid.NewGuid().ToString("N").Substring(0, 12), currency = Wager.Key(currency), stake = stake, phase = "open", hostId = hostId, hostName = (hostName ?? string.Empty), hostEntrantId = entrantId, hostLabel = (label ?? string.Empty), hostLevel = level, hostCaste = caste, hostPayload = payload, createdTicks = DateTime.UtcNow.Ticks }; _board.invites.Add(duelInvite2); Save(); Broadcast(); ServerGuideBridge.AnnounceDiscord("\ud83d\udce3 **" + hostName + "** has posted a duel invite — **" + label + "**" + ((level > 0) ? $" (Lv{level})" : "") + $" · stake **{stake} {Wager.Display(currency)}**, winner takes **{stake * 2}**. " + "Accept it from the Tournament panel (F7)."); Plugin.Log.LogInfo((object)$"[invite] {hostName} posted '{duelInvite2.id}' ({stake} {duelInvite2.currency})."); done?.Invoke(arg1: true, $"Duel invite posted — {stake} {Wager.Display(currency)} staked. " + "The winner takes both stakes."); } }); } public static void Accept(string inviteId, long oppId, string oppName, string entrantId, string label, int level, int caste, string payload, int clientPaidCoins, Action<bool, string> done) { DuelInvite invite = _board.Find(inviteId); if (invite == null || invite.phase != "open") { done?.Invoke(arg1: false, "That duel invite is no longer open."); return; } if (invite.hostId == oppId) { done?.Invoke(arg1: false, "You cannot accept your own invite."); return; } if (_board.ForOwner(oppId) != null) { done?.Invoke(arg1: false, "You are already tied up in a duel invite."); return; } if (string.IsNullOrEmpty(entrantId)) { done?.Invoke(arg1: false, "That totem has no ladder identity and can't be staked."); return; } if (string.IsNullOrEmpty(payload)) { done?.Invoke(arg1: false, "No companion totem was locked in."); return; } WagerCurrency currency = Wager.Parse(invite.currency); if (!WagerService.IsAvailable(currency, out var reason)) { done?.Invoke(arg1: false, reason); return; } int stake = invite.stake; WagerService.Charge(currency, oppName, stake, "ls_duel_stake", "accepted a duel invite", clientPaidCoins, delegate(bool ok, string msg) { if (!ok) { done?.Invoke(arg1: false, msg ?? "The stake could not be taken."); } else { invite = _board.Find(inviteId); if (invite == null || invite.phase != "open" || _board.ForOwner(oppId) != null) { WagerService.RollBackOwnCharge(currency, oppId, oppName, stake, "the invite was already taken"); done?.Invoke(arg1: false, "That duel invite was taken while your stake was being processed."); } else { invite.phase = "matched"; invite.oppId = oppId; invite.oppName = oppName ?? string.Empty; invite.oppEntrantId = entrantId; invite.oppLabel = label ?? string.Empty; invite.oppLevel = level; invite.oppCaste = caste; invite.oppPayload = payload; Save(); Broadcast(); ServerGuideBridge.AnnounceDiscord("\ud83e\udd1d **" + oppName + "** has accepted **" + invite.hostName + "**'s duel invite — " + $"**{invite.hostLabel}** vs **{label}** for **{invite.stake * 2} {Wager.Display(currency)}**. " + "The two of them choose where to fight; the duel starts once both are ready."); done?.Invoke(arg1: true, "Challenge accepted — meet " + invite.hostName + " and press Ready to Fight."); } } }); } public static string SetReady(string inviteId, long ownerId) { DuelInvite duelInvite = _board.Find(inviteId); if (duelInvite == null) { return "that duel invite no longer exists."; } if (duelInvite.phase == "running") { return "the duel is already under way."; } if (duelInvite.phase != "matched") { return "nobody has accepted that invite yet."; } if (duelInvite.hostId == ownerId) { duelInvite.hostReady = true; } else { if (duelInvite.oppId != ownerId) { return "you are not part of that duel."; } duelInvite.oppReady = true; } if (!duelInvite.hostReady || !duelInvite.oppReady) { Save(); Broadcast(); string text = ((duelInvite.hostId == ownerId) ? duelInvite.oppName : duelInvite.hostName); return "Ready. Waiting for " + text + "."; } duelInvite.phase = "running"; Save(); Broadcast(); LeaderboardSync.SummonForMatch(duelInvite.hostId, duelInvite.hostEntrantId, "1v1", duelInvite.oppEntrantId, duelInvite.oppLabel, 0, new List<string> { duelInvite.hostPayload }, "invite"); LeaderboardSync.SummonForMatch(duelInvite.oppId, duelInvite.oppEntrantId, "1v1", duelInvite.hostEntrantId, duelInvite.hostLabel, 0, new List<string> { duelInvite.oppPayload }, "invite"); ServerGuideBridge.AnnounceDiscord("\ud83e\udd4a **The staked duel begins:** " + duelInvite.hostLabel + " (" + duelInvite.hostName + ") vs " + $"{duelInvite.oppLabel} ({duelInvite.oppName}) — **{duelInvite.stake * 2} {Wager.Display(Wager.Parse(duelInvite.currency))}** on the line."); return "Both fighters are ready — the duel begins!"; } public static List<KeyValuePair<long, string>> Withdraw(string inviteId, long ownerId, out string status) { List<KeyValuePair<long, string>> list = new List<KeyValuePair<long, string>>(); status = "that duel invite no longer exists."; DuelInvite duelInvite = _board.Find(inviteId); if (duelInvite == null) { return list; } if (!duelInvite.Involves(ownerId)) { status = "that is not your duel invite."; return list; } if (duelInvite.phase == "running") { status = "the duel is already under way — fight it out."; return list; } WagerCurrency currency = Wager.Parse(duelInvite.currency); if (duelInvite.hostId == ownerId) { if (!string.IsNullOrEmpty(duelInvite.hostPayload)) { list.Add(new KeyValuePair<long, string>(duelInvite.hostId, duelInvite.hostPayload)); } WagerService.Refund(currency, duelInvite.hostId, duelInvite.hostName, duelInvite.stake, "withdrew a duel invite"); if (duelInvite.phase == "matched") { if (!string.IsNullOrEmpty(duelInvite.oppPayload)) { list.Add(new KeyValuePair<long, string>(duelInvite.oppId, duelInvite.oppPayload)); } WagerService.Refund(currency, duelInvite.oppId, duelInvite.oppName, duelInvite.stake, "the poster withdrew"); } _board.invites.Remove(duelInvite); status = "Duel invite withdrawn — your stake and totem are returned."; ServerGuideBridge.AnnounceDiscord("\ud83d\udeab **" + duelInvite.hostName + "** withdrew their duel invite. All stakes returned."); } else { if (!string.IsNullOrEmpty(duelInvite.oppPayload)) { list.Add(new KeyValuePair<long, string>(duelInvite.oppId, duelInvite.oppPayload)); } WagerService.Refund(currency, duelInvite.oppId, duelInvite.oppName, duelInvite.stake, "backed out of a duel"); string oppName = duelInvite.oppName; duelInvite.phase = "open"; duelInvite.oppId = 0L; duelInvite.oppName = ""; duelInvite.oppEntrantId = ""; duelInvite.oppLabel = ""; duelInvite.oppPayload = ""; duelInvite.oppLevel = 0; duelInvite.oppCaste = 0; duelInvite.hostReady = false; duelInvite.oppReady = false; status = "You have backed out — your stake and totem are returned."; ServerGuideBridge.AnnounceDiscord("↩\ufe0f **" + oppName + "** backed out of **" + duelInvite.hostName + "**'s duel invite — it is open again."); } Save(); Broadcast(); return list; } public static bool NotifyDuelResult(string winnerEntrantId, string loserEntrantId) { DuelInvite duelInvite = _board.ForEntrant(winnerEntrantId); if (duelInvite == null || duelInvite.phase != "running") { return false; } if (!duelInvite.HasEntrant(loserEntrantId)) { return false; } bool num = duelInvite.IsHostEntrant(winnerEntrantId); long ownerId = (num ? duelInvite.hostId : duelInvite.oppId); string text = (num ? duelInvite.hostName : duelInvite.oppName); string text2 = (num ? duelInvite.hostLabel : duelInvite.oppLabel); string text3 = (num ? duelInvite.oppName : duelInvite.hostName); string text4 = (num ? duelInvite.oppLabel : duelInvite.hostLabel); WagerCurrency wagerCurrency = Wager.Parse(duelInvite.currency); int num2 = duelInvite.stake * 2; WagerService.Pay(wagerCurrency, ownerId, text, num2, "ls_duel_purse", "won a staked duel"); ServerGuideBridge.AnnounceDiscord("\ud83c\udfc6 **" + text + "** wins the staked duel — **" + text2 + "** defeats **" + text4 + "** (" + text3 + ") " + $"and takes **{num2} {Wager.Display(wagerCurrency)}**."); Plugin.Log.LogInfo((object)$"[invite] '{duelInvite.id}' settled: {text} takes {num2} {duelInvite.currency}."); duelInvite.phase = "complete"; duelInvite.completedTicks = DateTime.UtcNow.Ticks; Save(); Broadcast(); return true; } public static bool UpdateEscrow(string entrantId, List<string> payloads) { if (payloads == null || payloads.Count == 0) { return false; } DuelInvite duelInvite = _board.ForEntrant(entrantId); if (duelInvite == null) { return false; } if (duelInvite.IsHostEntrant(entrantId)) { duelInvite.hostPayload = payloads[0]; } else { duelInvite.oppPayload = payloads[0]; } Save(); return true; } public static void Tick() { if (_board?.invites == null) { return; } bool flag = false; foreach (DuelInvite item in _board.invites.ToList()) { Dictionary<long, List<string>> byOwner; if (item == null) { _board.invites.Remove(item); flag = true; } else if (item.phase == "complete") { if (item.completedTicks <= 0 || !((DateTime.UtcNow - new DateTime(item.completedTicks, DateTimeKind.Utc)).TotalSeconds < 6.0)) { byOwner = new Dictionary<long, List<string>>(); Add(item.hostId, item.hostPayload); Add(item.oppId, item.oppPayload); LeaderboardSync.ReturnEscrowToOwners(byOwner); _board.invites.Remove(item); flag = true; } } else { if (item.phase != "open") { continue; } float value = Plugin.WageredRegistrationMinutes.Value; if (!(value <= 0f) && item.createdTicks > 0 && !((DateTime.UtcNow - new DateTime(item.createdTicks, DateTimeKind.Utc)).TotalMinutes < (double)value)) { WagerService.Refund(Wager.Parse(item.currency), item.hostId, item.hostName, item.stake, "the duel invite expired"); if (!string.IsNullOrEmpty(item.hostPayload)) { LeaderboardSync.ReturnEscrowToOwners(new Dictionary<long, List<string>> { { item.hostId, new List<string> { item.hostPayload } } }); } _board.invites.Remove(item); flag = true; ServerGuideBridge.AnnounceDiscord("⌛ **" + item.hostName + "**'s duel invite expired unanswered — stake and totem returned."); } } void Add(long owner, string payload) { if (owner != 0L && !string.IsNullOrEmpty(payload)) { if (!byOwner.TryGetValue(owner, out var value2)) { value2 = new List<string>(); byOwner[owner] = value2; } value2.Add(payload); } } } if (flag) { Save(); Broadcast(); } } private static void Broadcast() { LeaderboardSync.BroadcastInvites(); } } [Serializable] public class CompanionRecord { public string companionId; public long ownerId; public string ownerName; public string companionName; public int caste; public int wins; public int losses; public int winStreak; public int bestStreak; public int rating = 1000; public long lastMatchTicks; public int seasonId = 1; } [Serializable] public class PartyMemberSnap { public string companionId; public int caste; public int level; } [Serializable] public class PartyRecord { public long ownerId; public string ownerName; public string partyName; public int wins; public int losses; public int rating = 1000; public int bestTeamSize; public List<PartyMemberSnap> memberSnapshot = new List<PartyMemberSnap>(); public long lastMatchTicks; public int seasonId = 1; } [Serializable] public class LeaderboardData { public int seasonId = 1; public List<CompanionRecord> companions = new List<CompanionRecord>(); public List<PartyRecord> parties = new List<PartyRecord>(); } public class PartyDuelResult { public long WinnerOwnerId; public string WinnerOwnerName; public long LoserOwnerId; public string LoserOwnerName; public int MvpCaste; public List<PartyMemberSnap> WinnerMembers = new List<PartyMemberSnap>(); public List<PartyMemberSnap> LoserMembers = new List<PartyMemberSnap>(); public string WinnerPartyName; public string Encode() { return string.Join("|", WinnerOwnerId.ToString(), S(WinnerOwnerName), LoserOwnerId.ToString(), S(LoserOwnerName), MvpCaste.ToString(), Members(WinnerMembers), Members(LoserMembers), S(WinnerPartyName)); static string Members(List<PartyMemberSnap> ms) { if (ms == null || ms.Count == 0) { return string.Empty; } List<string> list = new List<string>(ms.Count); foreach (PartyMemberSnap m in ms) { list.Add($"{m.companionId}:{m.caste}:{m.level}"); } return string.Join(",", list); } static string S(string s) { return (s ?? string.Empty).Replace('|', '/').Replace(',', ' ').Replace(':', ' '); } } public static PartyDuelResult Decode(string s) { if (string.IsNullOrEmpty(s)) { return null; } string[] array = s.Split(new char[1] { '|' }); if (array.Length < 7) { return null; } long.TryParse(array[0], out var result); long.TryParse(array[2], out var result2); int.TryParse(array[4], out var result3); return new PartyDuelResult { WinnerOwnerId = result, WinnerOwnerName = array[1], LoserOwnerId = result2, LoserOwnerName = array[3], MvpCaste = result3, WinnerMembers = ParseMembers(array[5]), LoserMembers = ParseMembers(array[6]), WinnerPartyName = ((array.Length > 7) ? array[7] : string.Empty) }; static List<PartyMemberSnap> ParseMembers(string raw) { List<PartyMemberSnap> list = new List<PartyMemberSnap>(); if (string.IsNullOrEmpty(raw)) { return list; } string[] array2 = raw.Split(new char[1] { ',' }); for (int i = 0; i < array2.Length; i++) { string[] array3 = array2[i].Split(new char[1] { ':' }); if (array3.Length >= 3) { int.TryParse(array3[1], out var result4); int.TryParse(array3[2], out var result5); list.Add(new PartyMemberSnap { companionId = array3[0], caste = result4, level = result5 }); } } return list; } } } public class DuelResult { public string WinnerId; public long WinnerOwnerId; public string WinnerOwnerName; public string WinnerName; public int WinnerCaste; public string LoserId; public long LoserOwnerId; public string LoserOwnerName; public string LoserName; public int LoserCaste; public string Encode() { return string.Join("|", S(WinnerId), WinnerOwnerId.ToString(), S(WinnerOwnerName), S(WinnerName), WinnerCaste.ToString(), S(LoserId), LoserOwnerId.ToString(), S(LoserOwnerName), S(LoserName), LoserCaste.ToString()); static string S(string s) { return (s ?? string.Empty).Replace('|', '/'); } } public static DuelResult Decode(string s) { if (string.IsNullOrEmpty(s)) { return null; } string[] array = s.Split(new char[1] { '|' }); if (array.Length < 10) { return null; } long.TryParse(array[1], out var result); int.TryParse(array[4], out var result2); long.TryParse(array[6], out var result3); int.TryParse(array[9], out var result4); return new DuelResult { WinnerId = array[0], WinnerOwnerId = result, WinnerOwnerName = array[2], WinnerName = array[3], WinnerCaste = result2, LoserId = array[5], LoserOwnerId = result3, LoserOwnerName = array[7], LoserName = array[8], LoserCaste = result4 }; } } public static class LeaderboardStore { private static LeaderboardData _data; private static LeaderboardData _snapshot = new LeaderboardData(); private static readonly Dictionary<string, float> _pairCooldown = new Dictionary<string, float>(); private static string _loadedWorld; public static LeaderboardData Snapshot => _snapshot; public static string DataDir() { string text; try { text = Path.Combine(Utils.GetSaveDataPath((FileSource)1), "LostScrollsII"); } catch { text = Path.Combine(Paths.ConfigPath, "LostScrollsII"); } Directory.CreateDirectory(text); return text; } private static string Sanitize(string s) { if (string.IsNullOrEmpty(s)) { return "world"; } char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { s = s.Replace(oldChar, '_'); } return s; } private static string CurrentWorldName() { try { if ((Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.GetWorldName(); } } catch { } return "world"; } public static string WorldTag() { return Sanitize(CurrentWorldName()); } private static string StorePath(string world) { return Path.Combine(DataDir(), "ladder." + Sanitize(world) + ".json"); } public static void LoadForCurrentWorld() { string text = (_loadedWorld = CurrentWorldName()); string text2 = StorePath(text); try { if (File.Exists(text2)) { _data = CompetitiveJson.ReadLeaderboard(File.ReadAllText(text2)) ?? new LeaderboardData(); if (_data.companions == null) { _data.companions = new List<CompanionRecord>(); } if (_data.parties == null) { _data.parties = new List<PartyRecord>(); } Plugin.Log.LogInfo((object)$"[ladder] loaded {_data.companions.Count} duel + {_data.parties.Count} party record(s) for world '{text}'."); } else { _data = new LeaderboardData(); Plugin.Log.LogInfo((object)("[ladder] no ladder file for world '" + text + "' — starting fresh.")); } } catch (Exception ex) { _data = new LeaderboardData(); Plugin.Log.LogWarning((object)("[ladder] failed to load '" + text2 + "': " + ex.Message + ". Starting fresh.")); } _snapshot = _data; } public static void Save() { if (_data == null) { return; } try { File.WriteAllText(StorePath(_loadedWorld ?? CurrentWorldName()), CompetitiveJson.Write(_data, pretty: true)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ladder] save failed: " + ex.Message)); } } public static string SerializeSnapshot() { return CompetitiveJson.Write(_data ?? new LeaderboardData()); } public static void ApplySnapshot(string json) { try { LeaderboardData leaderboardData = CompetitiveJson.ReadLeaderboard(json) ?? new LeaderboardData(); if (leaderboardData.companions == null) { leaderboardData.companions = new List<CompanionRecord>(); } if (leaderboardData.parties == null) { leaderboardData.parties = new List<PartyRecord>(); } _snapshot = leaderboardData; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ladder] failed to apply snapshot: " + ex.Message)); } } public static List<CompanionRecord> Ranked(int caste = -1) { return (from r in _snapshot?.companions ?? new List<CompanionRecord>() where r != null && (caste < 0 || r.caste == caste) orderby r.rating descending, r.wins descending, r.companionName ?? "" select r).ToList(); } public static int RankOf(string companionId) { if (string.IsNullOrEmpty(companionId)) { return 0; } List<CompanionRecord> list = Ranked(); for (int i = 0; i < list.Count; i++) { if (list[i].companionId == companionId) { return i + 1; } } return 0; } public static CompanionRecord Find(string companionId) { return (_snapshot?.companions ?? new List<CompanionRecord>()).FirstOrDefault((CompanionRecord r) => r != null && r.companionId == companionId); } public static PartyRecord FindParty(long ownerId) { return (_snapshot?.parties ?? new List<PartyRecord>()).FirstOrDefault((PartyRecord r) => r != null && r.ownerId == ownerId); } public static List<PartyRecord> RankedParties() { return (from r in _snapshot?.parties ?? new List<PartyRecord>() where r != null orderby r.rating descending, r.wins descending, r.ownerName ?? "" select r).ToList(); } public static bool ApplyDuel(DuelResult r, int kFactor, float pairCooldownSeconds, out int winnerRank, out int winnerRating, out bool crossedThreshold) { winnerRank = 0; winnerRating = 1000; crossedThreshold = false; if (_data == null) { LoadForCurrentWorld(); } if (r == null || string.IsNullOrEmpty(r.WinnerId) || string.IsNullOrEmpty(r.LoserId)) { return false; } if (r.WinnerId == r.LoserId) { return false; } if (r.WinnerOwnerId == 0L || r.LoserOwnerId == 0L || r.WinnerOwnerId == r.LoserOwnerId) { return false; } CompanionRecord orCreate = GetOrCreate(r.WinnerId, r.WinnerOwnerId, r.WinnerOwnerName, r