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 SimpleSell v0.1.15
SimpleSell.dll
Decompiled 13 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using DunGen; using DunGen.Graph; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using SimpleSell.Commands; using SimpleSell.Core; using SimpleSell.Features; using Steamworks; using Steamworks.Data; using TMPro; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("AmazingAssets.TerrainToMesh")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp-firstpass")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: IgnoresAccessChecksTo("ClientNetworkTransform")] [assembly: IgnoresAccessChecksTo("com.olegknyazev.softmask")] [assembly: IgnoresAccessChecksTo("DissonanceVoip")] [assembly: IgnoresAccessChecksTo("DunGen")] [assembly: IgnoresAccessChecksTo("DunGen.Integration.ASPP")] [assembly: IgnoresAccessChecksTo("DunGen.Integration.UnityNav")] [assembly: IgnoresAccessChecksTo("EasyTextEffects")] [assembly: IgnoresAccessChecksTo("Facepunch Transport for Netcode for GameObjects")] [assembly: IgnoresAccessChecksTo("Facepunch.Steamworks.Win64")] [assembly: IgnoresAccessChecksTo("Unity.AI.Navigation")] [assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging")] [assembly: IgnoresAccessChecksTo("Unity.Animation.Rigging.DocCodeExamples")] [assembly: IgnoresAccessChecksTo("Unity.Burst")] [assembly: IgnoresAccessChecksTo("Unity.Burst.Unsafe")] [assembly: IgnoresAccessChecksTo("Unity.Collections")] [assembly: IgnoresAccessChecksTo("Unity.Collections.LowLevel.ILSupport")] [assembly: IgnoresAccessChecksTo("Unity.InputSystem")] [assembly: IgnoresAccessChecksTo("Unity.InputSystem.ForUI")] [assembly: IgnoresAccessChecksTo("Unity.Jobs")] [assembly: IgnoresAccessChecksTo("Unity.Mathematics")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.Common")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.MetricTypes")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStats")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Component")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Configuration")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsMonitor.Implementation")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetStatsReporting")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkProfiler.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.Multiplayer.Tools.NetworkSolutionInterface")] [assembly: IgnoresAccessChecksTo("Unity.Netcode.Components")] [assembly: IgnoresAccessChecksTo("Unity.Netcode.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.Networking.Transport")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Csg")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.KdTree")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Poly2Tri")] [assembly: IgnoresAccessChecksTo("Unity.ProBuilder.Stl")] [assembly: IgnoresAccessChecksTo("Unity.Profiling.Core")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.Core.ShaderLibrary")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Config.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.HighDefinition.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.RenderPipelines.ShaderGraph.ShaderGraphLibrary")] [assembly: IgnoresAccessChecksTo("Unity.Services.Authentication")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Analytics")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Components")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Configuration")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Device")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Environments.Internal")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Internal")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Networking")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Registration")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Scheduler")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Telemetry")] [assembly: IgnoresAccessChecksTo("Unity.Services.Core.Threading")] [assembly: IgnoresAccessChecksTo("Unity.Services.QoS")] [assembly: IgnoresAccessChecksTo("Unity.Services.Relay")] [assembly: IgnoresAccessChecksTo("Unity.TextMeshPro")] [assembly: IgnoresAccessChecksTo("Unity.Timeline")] [assembly: IgnoresAccessChecksTo("Unity.VisualEffectGraph.Runtime")] [assembly: IgnoresAccessChecksTo("Unity.XR.CoreUtils")] [assembly: IgnoresAccessChecksTo("Unity.XR.Management")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.ConformanceAutomation")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MetaQuestSupport")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.MockRuntime")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.OculusQuestSupport")] [assembly: IgnoresAccessChecksTo("Unity.XR.OpenXR.Features.RuntimeDebugger")] [assembly: IgnoresAccessChecksTo("UnityEngine.ARModule")] [assembly: IgnoresAccessChecksTo("UnityEngine.NVIDIAModule")] [assembly: IgnoresAccessChecksTo("UnityEngine.SpatialTracking")] [assembly: IgnoresAccessChecksTo("UnityEngine.UI")] [assembly: IgnoresAccessChecksTo("UnityEngine.XR.LegacyInputHelpers")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.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 SimpleSell { internal static class ChatUtil { internal static bool BroadcastReplies; internal static string InvokerName; internal static void SendPrivate(string message) { bool broadcastReplies = BroadcastReplies; BroadcastReplies = false; try { Send(message); } finally { BroadcastReplies = broadcastReplies; } } internal static void Send(string message, bool broadcast = false) { try { if (BroadcastReplies) { broadcast = true; } HUDManager instance = HUDManager.Instance; if (!((Object)(object)instance == (Object)null)) { string text = "<color=#7ec8ff>SimpleSell</color>: <color=#e8e86a>" + message + "</color>"; if (broadcast) { instance.AddTextMessageServerRpc(text); } else { instance.AddChatMessage(text, "", -1, false); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[ChatUtil.Send] {arg}"); } } internal static void SendRaw(string message, bool broadcast = false) { try { HUDManager instance = HUDManager.Instance; if (!((Object)(object)instance == (Object)null)) { if (broadcast) { instance.AddTextMessageServerRpc(message); } else { instance.AddChatMessage(message, "", -1, false); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[ChatUtil.SendRaw] {arg}"); } } internal static void SendChunked(string message, bool broadcast = false, int maxLen = 120) { if (string.IsNullOrEmpty(message)) { return; } while (message.Length > maxLen) { int num = message.LastIndexOfAny(new char[2] { '·', ',' }, maxLen); if (num < maxLen / 2) { num = message.LastIndexOf(' ', maxLen); } if (num < maxLen / 2) { num = maxLen; } Send(message.Substring(0, num).TrimEnd(), broadcast); message = message.Substring(num).TrimStart(' ', ',', '·'); } if (message.Length > 0) { Send(message, broadcast); } } } internal static class CommandRouter { internal static bool SimpleHQLoaded => Chainloader.PluginInfos.ContainsKey("local.simplehq"); internal static bool IsSenderTheHost(int playerId) { try { if ((Object)(object)NetworkManager.Singleton == (Object)null || !NetworkManager.Singleton.IsHost) { return false; } if (playerId < 0) { Plugin.Logger.LogWarning((object)$"[CommandRouter] playerId={playerId} on host — allowing (local chat path)."); return true; } PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController; if ((Object)(object)val == (Object)null) { return false; } return playerId == (int)val.playerClientId || playerId == (int)val.actualClientId; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[CommandRouter.IsSenderTheHost] {arg}"); return false; } } internal static string CanonicalVerb(string cmd) { switch (cmd) { case "sell": case "s": return "sell"; case "stock": case "st": return "stock"; case "m": case "merge": return "merge"; case "gear": case "g": return "gear"; case "e": case "equipment": return "equipment"; case "o": case "organize": return "organize"; case "time": case "t": return "time"; case "ot": case "overtime": return "overtime"; case "b": case "briefing": return "briefing"; case "l": case "lobby": return "lobby"; case "scrap": return "scrap"; case "players": return "players"; case "kick": return "kick"; case "guard": return "guard"; case "whitelist": return "whitelist"; case "ban": return "ban"; case "unban": return "unban"; case "bans": return "bans"; case "help": return "help"; default: return null; } } internal static bool IsBankSale(string[] parts) { if (parts.Length >= 2 && (parts[0].Equals("sell", StringComparison.OrdinalIgnoreCase) || parts[0].Equals("s", StringComparison.OrdinalIgnoreCase))) { return parts[1].Equals("bank", StringComparison.OrdinalIgnoreCase); } return false; } internal static bool Route(string raw) { try { if (string.IsNullOrWhiteSpace(raw)) { return false; } string[] array = raw.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); string text = array[0].ToLowerInvariant(); if (TournamentGate.Active && text != "help" && CanonicalVerb(text) != null && !IsBankSale(array)) { ChatUtil.Send("Tournament mode: mod commands are off until the run ends."); return true; } switch (text) { case "sell": case "s": if (array.Length >= 2 && array[1].Equals("u", StringComparison.OrdinalIgnoreCase)) { HandleScrapUpdate((array.Length >= 3) ? array[2] : ""); return true; } if (IsBankSale(array)) { if (!SimpleHQLoaded) { ChatUtil.Send("Selling from the bank needs the SimpleHQ mod."); } return true; } return SellCommands.Handle(array); case "stock": case "st": UtilityCommands.Stock(); return true; case "m": case "merge": { string text6 = ItemNameResolver.JoinName(array, 1); if (text6.Equals("auto", StringComparison.OrdinalIgnoreCase)) { ModConfig.AutoMergeOnOrbit.Value = !ModConfig.AutoMergeOnOrbit.Value; ChatUtil.Send(ModConfig.AutoMergeOnOrbit.Value ? "Auto-merge ON — cruiser scrap sweeps to the ship when you reach orbit." : "Auto-merge OFF."); return true; } UtilityCommands.Merge(string.IsNullOrWhiteSpace(text6) ? null : text6); return true; } case "gear": case "g": switch ((array.Length >= 2) ? array[1].ToLowerInvariant() : "") { case "add": UtilityCommands.GearAdd(ItemNameResolver.JoinName(array, 2)); break; case "remove": UtilityCommands.GearRemove(ItemNameResolver.JoinName(array, 2)); break; case "list": UtilityCommands.GearListShow(); break; default: UtilityCommands.Gear(); break; } return true; case "e": case "equipment": return EquipmentCommands.Handle(array); case "o": case "organize": { string text4 = ((array.Length >= 2) ? array[1].ToLowerInvariant() : ""); string arg = ItemNameResolver.JoinName(array, 2).ToLowerInvariant(); switch (text4) { case "": OrganizeService.OrganizeEquipment(""); OrganizeService.OrganizeScrap("", quiet: true); return true; case "scrap": OrganizeService.OrganizeScrap(arg); return true; case "gear": case "equipment": case "e": OrganizeService.OrganizeEquipment(arg); return true; case "set": OrganizeService.SetAnchorAuto(array); return true; case "unset": OrganizeService.UnsetAuto(ItemNameResolver.JoinName(array, 2)); return true; case "shelf": OrganizeService.SetShelfAnchor(array); return true; case "spots": case "list": case "anchors": OrganizeService.ListAnchors(AnchorStore.Equipment, showDefaults: true, "/organize set"); OrganizeService.ListAnchors(AnchorStore.Scrap, showDefaults: false, "/organize set"); return true; default: OrganizeService.OrganizeOne(ItemNameResolver.JoinName(array, 1)); return true; } } case "ot": case "overtime": UtilityCommands.Overtime((array.Length >= 2) ? array[1] : ""); return true; case "time": case "t": if (array.Length >= 2 && (array[1].Equals("update", StringComparison.OrdinalIgnoreCase) || array[1].Equals("u", StringComparison.OrdinalIgnoreCase))) { TimeAnnounceFeature.Handle((array.Length >= 3) ? array[2].ToLowerInvariant() : ""); return true; } ChatUtil.Send("Time: " + TimeUtil.FormatGameTime()); return true; case "b": case "briefing": DayBriefingFeature.HandleToggle((array.Length >= 2) ? array[1].ToLowerInvariant() : ""); return true; case "l": case "lobby": { string text2 = ((array.Length >= 2) ? array[1].ToLowerInvariant() : ""); switch (text2) { case "hide": case "h": LobbyTagFeature.Apply(hide: true); break; case "show": case "s": LobbyTagFeature.Apply(hide: false); break; case "status": LobbyTagFeature.Status(); break; case "name": LobbyTagFeature.Rename(string.Join(" ", array, 2, array.Length - 2).Trim()); break; case "public": case "friends": case "private": LobbyTagFeature.SetVisibility(text2); break; case "late": case "open": { string text3 = ((array.Length >= 3) ? array[2].ToLowerInvariant() : ""); if (text3 == "on" || text3 == "off") { ModConfig.AllowLateJoin.Value = text3 == "on"; LateJoin.ResetPushState(); LateJoin.Refresh(); ChatUtil.Send(LateJoin.Status()); } else { ChatUtil.Send(LateJoin.Status() + " Usage: /lobby late on|off"); } break; } default: ChatUtil.Send("Usage: /lobby hide | show | status | name <new name> | public | friends | private | late on|off"); break; } return true; } case "scrap": switch ((array.Length >= 2) ? array[1].ToLowerInvariant() : "") { case "update": HandleScrapUpdate((array.Length >= 3) ? array[2] : ""); return true; case "set": OrganizeService.SetAnchor(array, AnchorStore.Scrap, allowDefaults: false, "Usage: /scrap set <item name> [pile|stack]"); return true; case "organize": case "o": ChatUtil.Send("Moved in Round 31: use /organize scrap [ship|cruiser|<item>] (or /o scrap)."); return true; case "unset": OrganizeService.Unset(ItemNameResolver.JoinName(array, 2), AnchorStore.Scrap); return true; case "spots": case "anchors": case "list": OrganizeService.ListAnchors(AnchorStore.Scrap, showDefaults: false, "/scrap set"); return true; default: ChatUtil.Send("Usage: /scrap update <mode> | set <item> [pile|stack] | unset <item> | anchors — organizing: /organize scrap"); return true; } case "players": if (array.Length >= 2 && array[1].Equals("ids", StringComparison.OrdinalIgnoreCase)) { GuardCommands.PlayersIds(); } else { GuardCommands.Players(); } return true; case "help": { string text5 = ((array.Length >= 2) ? array[1].ToLowerInvariant() : ""); if (text5 == "") { ChatUtil.Send("SimpleSell — /help sell"); return true; } if (text5 != "sell" && text5 != "simplesell") { return false; } ChatUtil.Send("Selling: /sell <amount|quota|all|item [n|all]> · /sell exclude|include|exclusions"); ChatUtil.Send("Cruiser: /merge [item|auto] · Spots: /organize [scrap|equipment] [ship|cruiser|item], /e set, /scrap set, unset, anchors"); ChatUtil.Send("Info: /stock · /gear [add|remove <item>] · /time [update l|p|o] · /briefing l|p|o · /s u l|p|o|ship|cruiser|both|split"); ChatUtil.Send("Lobby: /lobby hide|show · /players [ids] /kick /guard /whitelist /ban /unban /bans"); return true; } case "kick": GuardCommands.Kick(ItemNameResolver.JoinName(array, 1)); return true; case "guard": GuardCommands.Guard((array.Length >= 2) ? array[1].ToLowerInvariant() : ""); return true; case "whitelist": GuardCommands.Whitelist((array.Length >= 2) ? array[1].ToLowerInvariant() : "", ItemNameResolver.JoinName(array, 2)); return true; case "ban": GuardCommands.Ban(ItemNameResolver.JoinName(array, 1)); return true; case "unban": GuardCommands.Unban(ItemNameResolver.JoinName(array, 1)); return true; case "bans": GuardCommands.Bans(); return true; default: return false; } } catch (Exception arg2) { Plugin.Logger.LogError((object)$"[CommandRouter.Route] {arg2}"); ChatUtil.Send("Command failed — see BepInEx log."); return true; } } private static void HandleScrapUpdate(string modeToken) { switch (modeToken.ToLowerInvariant()) { case "l": case "live": ScrapUpdateFeature.Enable(broadcast: true); break; case "p": case "private": ScrapUpdateFeature.Enable(broadcast: false); break; case "o": case "off": ScrapUpdateFeature.Disable(); break; case "ship": ScrapUpdateFeature.SetDisplayMode(UpdateDisplayMode.Ship); break; case "cruiser": ScrapUpdateFeature.SetDisplayMode(UpdateDisplayMode.Cruiser); break; case "both": ScrapUpdateFeature.SetDisplayMode(UpdateDisplayMode.Combined); break; case "split": ScrapUpdateFeature.SetDisplayMode(UpdateDisplayMode.Breakdown); break; default: ChatUtil.Send("Usage: /s u l (live) | p (private) | o (off) | ship | cruiser | both | split"); break; } } } internal enum UpdateDisplayMode { Ship, Cruiser, Combined, Breakdown } internal enum GuardEnforcement { Warn, Kick } internal static class ModConfig { internal static ConfigEntry<string> CommandPrefix { get; private set; } internal static ConfigEntry<bool> HideLobbyModTag { get; private set; } internal static ConfigEntry<bool> AllowLateJoin { get; private set; } internal static ConfigEntry<bool> FixCrewList { get; private set; } internal static ConfigEntry<string> ExcludedItems { get; private set; } internal static ConfigEntry<UpdateDisplayMode> UpdateMode { get; private set; } internal static ConfigEntry<bool> AutoMergeOnOrbit { get; private set; } internal static ConfigEntry<bool> DefaultExclusionsSeeded { get; private set; } internal static ConfigEntry<bool> LobbyHideDefaultMigrated { get; private set; } internal static ConfigEntry<string> EquipShipAnchor { get; private set; } internal static ConfigEntry<string> EquipCruiserAnchor { get; private set; } internal static ConfigEntry<string> EquipItemAnchors { get; private set; } internal static ConfigEntry<string> ScrapItemAnchors { get; private set; } internal static ConfigEntry<GuardEnforcement> GuardMode { get; private set; } internal static ConfigEntry<string> WhitelistedPlayers { get; private set; } internal static ConfigEntry<string> BannedPlayers { get; private set; } internal static ConfigEntry<string> WhitelistedCommands { get; private set; } internal static ConfigEntry<string> AltCommandPrefix { get; private set; } internal static ConfigEntry<float> DeskGuardRange { get; private set; } internal static ConfigEntry<float> CruiserTransferRange { get; private set; } internal static ConfigEntry<bool> DayBriefing { get; private set; } internal static ConfigEntry<bool> DayBriefingBroadcast { get; private set; } internal static ConfigEntry<bool> ConfirmSellAll { get; private set; } internal static ConfigEntry<bool> AnnounceBans { get; private set; } internal static ConfigEntry<string> MergeExcludeKeywords { get; private set; } internal static ConfigEntry<string> EquipmentNonGearKeywords { get; private set; } internal static ConfigEntry<string> GearItems { get; private set; } internal static ConfigEntry<bool> GearSeeded { get; private set; } internal static ConfigEntry<bool> WeaponExclusionsMigrated { get; private set; } internal static void Init(ConfigFile file) { CommandPrefix = file.Bind<string>("General", "CommandPrefix", "/", "Prefix for chat commands (e.g. /sell)."); DayBriefing = file.Bind<bool>("General", "DayBriefing", false, "Post a summary when the ship lands: interior type, scrap count + value, hives. Off every launch (session toggle). Turn on in-game: /briefing live | private."); DayBriefingBroadcast = file.Bind<bool>("General", "DayBriefingBroadcast", false, "If true the day briefing goes to the whole crew; if false (default), host-only. In-game: /briefing live | private."); ConfirmSellAll = file.Bind<bool>("Selling", "ConfirmSellAll", false, "If true, /sell all shows the count/value and requires /sell confirm within 15s. Default off: /sell all fires immediately."); AnnounceBans = file.Bind<bool>("Lobby", "AnnounceBans", false, "If true, a successful /ban broadcasts 'X was banned by the host.' to the crew. Default off: ban confirmations stay host-only."); MergeExcludeKeywords = file.Bind<string>("Merge", "ExcludeKeywords", "", "Comma-separated name keywords /merge never touches (normalized: lowercase, no spaces — e.g. 'goldbar, pickle'). Empty by default."); EquipmentNonGearKeywords = file.Bind<string>("Equipment", "NonGearKeywords", "clipboard, sticky note", "Comma-separated name keywords treated as NOT equipment (excluded from /gear and /organize equipment). Defaults cover the ship's clipboard and sticky note."); GearItems = file.Bind<string>("Gear", "Items", "", "Comma-separated item names declared as gear (may include scrap). Gear-listed items show in /gear, organize as equipment, and are excluded from selling, /merge, and /organize scrap. Managed in-game via /gear add|remove."); GearSeeded = file.Bind<bool>("Gear", "Seeded", false, "Internal flag — do not edit. (One-time seeding of the four weapons into the gear list.)"); WeaponExclusionsMigrated = file.Bind<bool>("Gear", "WeaponExclusionsMigrated", false, "Internal flag — do not edit. (One-time removal of the Round 25 weapon sell-exclusions; the gear list is their single source of truth now.)"); CruiserTransferRange = file.Bind<float>("General", "CruiserTransferRange", 30f, "Max distance (meters) between cruiser and ship for moving items between them (/e o cross-moves, /merge cruiser sweep). Beyond this the cruiser counts as 'away' and items stay put — no beaming across the map. Magnetted always counts as docked."); FixCrewList = file.Bind<bool>("Lobby", "FixCrewList", true, "Remove leftover names from the pause menu crew list. The game adds a name as soon as someone enters the Steam lobby and never removes it if they're turned away or give up, so public lobbies fill the list with strangers."); AllowLateJoin = file.Bind<bool>("Lobby", "AllowLateJoin", false, "OFF by default and switched back OFF every time the game starts — it reopens the lobby mid-run, which shouldn't happen unless you ask for it. Turn on with /lobby late on when someone needs to join; it lasts until you quit. Lets someone join after the game has started — ONLY while in orbit (ship phase, not leaving, not travelling). Landed or mid-flight stays closed: the joiner would arrive into a generated level that vanilla never expected to sync to a newcomer. Vanilla's version check, kicked-player check and SimpleSell's bans all still apply. Toggle in-game: /lobby late on|off."); HideLobbyModTag = file.Bind<bool>("Lobby", "HideLobbyModTag", true, "If true, newly created lobbies present as vanilla (no mods tag in the browser, no mods warning on join). Can be toggled live with /lobby hide|show."); LobbyHideDefaultMigrated = file.Bind<bool>("Lobby", "HideDefaultMigrated", false, "Internal flag — do not edit. (Round 26 one-time switch to hidden-by-default.)"); ExcludedItems = file.Bind<string>("Selling", "ExcludedItems", "", "Comma-separated item names permanently excluded from selling. Managed in-game via /sell exclude, /sell include, /sell exclusions."); UpdateMode = file.Bind<UpdateDisplayMode>("ScrapUpdate", "DisplayMode", UpdateDisplayMode.Combined, "What the hourly scrap update line shows. Ship = ship only, Cruiser = cruiser only, Combined = one total, Breakdown = ship + cruiser + total. Changeable in-game: /s u ship | cruiser | both | split"); AutoMergeOnOrbit = file.Bind<bool>("Merge", "AutoMergeOnOrbit", false, "OFF by default (Round 65). When on, cruiser scrap is moved to piles by the ship door every time the ship reaches orbit, the same as typing /merge. Toggle in-game: /merge auto. Placement is network-synced (Round 26) — all players see items move."); DefaultExclusionsSeeded = file.Bind<bool>("Selling", "DefaultExclusionsSeeded", false, "Internal flag — do not edit."); EquipShipAnchor = file.Bind<string>("Equipment", "ShipAnchor", "", "Ship gear-stack spot as x,y,z in ship-local space. Set in-game with /equipment set ship."); EquipCruiserAnchor = file.Bind<string>("Equipment", "CruiserAnchor", "", "Cruiser gear-stack spot as x,y,z in the cruiser's item space. Set in-game with /equipment set cruiser."); EquipItemAnchors = file.Bind<string>("Equipment", "ItemAnchors", "", "Per-item gear spots as 'Item Name|ship|x,y,z|pile;...'. Managed in-game via /e set <item> [pile|stack], /e unset <item>, /e anchors."); ScrapItemAnchors = file.Bind<string>("Scrap", "ItemAnchors", "", "Per-item SCRAP spots, same format as Equipment ItemAnchors. Managed in-game via /scrap set <item> [pile|stack], /scrap unset <item>, /scrap anchors. Anchored scrap types are skipped by /merge and moved by /scrap organize."); GuardMode = file.Bind<GuardEnforcement>("Lobby", "GuardMode", GuardEnforcement.Warn, "What happens when a client sends modded network traffic. Warn = private alert to the host only; Kick = auto-kick on first modded packet (also kicks networked cosmetic mods - they are indistinguishable on the wire). The sell-desk beam block is always active regardless of mode. Live toggle: /guard warn | kick."); WhitelistedPlayers = file.Bind<string>("Lobby", "WhitelistedPlayers", "", "Trusted players as 'steamid|name;...' — they may run the WhitelistedCommands from vanilla chat (no mods needed on their end). Managed in-game via /whitelist add|remove."); BannedPlayers = file.Bind<string>("Lobby", "BannedPlayers", "", "Banned players as 'steamid|name;...' — rejected at connection approval, before spawning. Name changes don't help; the Steam ID is what's enforced. Managed in-game via /ban, /unban, /bans."); WhitelistedCommands = file.Bind<string>("Lobby", "WhitelistedCommands", "stock, time, gear, organize, merge", "Comma-separated commands whitelisted players may use (canonical names; aliases resolve automatically). Selling, lobby, guard, and recall stay host-only unless you add them here."); AltCommandPrefix = file.Bind<string>("Lobby", "AltCommandPrefix", "!", "Second prefix whitelisted players may use (e.g. !merge). Friends whose own chat-command mods swallow '/' messages locally can still reach the host this way. Empty disables."); DeskGuardRange = file.Bind<float>("Lobby", "DeskGuardRange", 12f, "Max distance (meters) an item may be from the sell desk when a client places it on the counter. Vanilla physical placement is always within a few meters; modded clients beaming items from the ship get blocked."); if (!LobbyHideDefaultMigrated.Value) { HideLobbyModTag.Value = true; LobbyHideDefaultMigrated.Value = true; } } } [BepInPlugin("local.simplesell", "SimpleSell", "0.1.15")] public class Plugin : BaseUnityPlugin { private readonly Harmony _harmony = new Harmony("local.simplesell"); public static Plugin Instance { get; private set; } internal static ManualLogSource Logger { get; private set; } private void Awake() { Instance = this; Logger = ((BaseUnityPlugin)this).Logger; ModConfig.Init(((BaseUnityPlugin)this).Config); ModConfig.DayBriefing.Value = false; ModConfig.AllowLateJoin.Value = false; _harmony.PatchAll(); Logger.LogInfo((object)"SimpleSell v0.1.15 loaded. Host-only chat commands armed."); } } public static class PluginInfo { public const string Guid = "local.simplesell"; public const string Name = "SimpleSell"; public const string Version = "0.1.15"; } public static class WhitelistInfo { public static bool IsWhitelisted(ulong steamId) { return PersistedIdList.Whitelist.Contains(steamId); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "SimpleSell"; public const string PLUGIN_NAME = "SimpleSell"; public const string PLUGIN_VERSION = "0.1.12"; } } namespace SimpleSell.Patches { [HarmonyPatch(typeof(GameNetworkManager), "ConnectionApproval")] internal static class BanApprovalPatch { [HarmonyPostfix] private static void Postfix(ConnectionApprovalRequest request, ConnectionApprovalResponse response) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) try { if (response != null && response.Approved && PersistedIdList.Bans.Count != 0) { string text = Encoding.ASCII.GetString(request.Payload); string[] array = text.Split(','); if (array.Length >= 2 && ulong.TryParse(array[1], out var result) && PersistedIdList.Bans.Contains(result)) { response.Approved = false; response.Reason = "You are banned from this lobby."; string text2 = PersistedIdList.Bans.NameOf(result); Plugin.Logger.LogInfo((object)$"[Ban] rejected join from {text2} ({result})."); ChatUtil.Send("⚠ Blocked banned player " + text2 + " from joining."); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[BanApprovalPatch] {arg}"); } } } [HarmonyPatch(typeof(HUDManager), "AddPlayerChatMessageServerRpc")] internal static class WhitelistChatPatch { [HarmonyPrefix] private static bool Prefix(string chatMessage, int playerId) { try { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsHost) { return true; } string text = null; if (!string.IsNullOrEmpty(chatMessage)) { if (chatMessage.StartsWith(ModConfig.CommandPrefix.Value)) { text = ModConfig.CommandPrefix.Value; } else if (!string.IsNullOrEmpty(ModConfig.AltCommandPrefix.Value) && chatMessage.StartsWith(ModConfig.AltCommandPrefix.Value)) { text = ModConfig.AltCommandPrefix.Value; } } if (text == null) { return true; } if (CommandRouter.IsSenderTheHost(playerId)) { return true; } StartOfRound instance = StartOfRound.Instance; if (instance?.allPlayerScripts == null || playerId < 0 || playerId >= instance.allPlayerScripts.Length) { return true; } PlayerControllerB val = instance.allPlayerScripts[playerId]; if ((Object)(object)val == (Object)null || !val.isPlayerControlled) { return true; } if (!PersistedIdList.Whitelist.Contains(val.playerSteamId)) { return true; } string text2 = chatMessage.Substring(text.Length).Trim(); if (CommandRouter.SimpleHQLoaded && CommandRouter.IsBankSale(text2.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries))) { return true; } string text3 = text2.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? ""; string text4 = CommandRouter.CanonicalVerb(text3.ToLowerInvariant()); if (text4 == null) { return true; } if (!IsCommandAllowed(text4)) { ChatUtil.Send("'" + text4 + "' is host-only.", broadcast: true); return false; } ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogInfo((object)("[Whitelist] " + val.playerUsername + " ran /" + text2)); } ChatUtil.BroadcastReplies = true; ChatUtil.InvokerName = val.playerUsername; bool flag; try { flag = CommandRouter.Route(text2); } finally { ChatUtil.BroadcastReplies = false; ChatUtil.InvokerName = null; } return !flag; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[WhitelistChatPatch] {arg}"); return true; } } private static bool IsCommandAllowed(string canonical) { string[] array = ModConfig.WhitelistedCommands.Value.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { string text2 = CommandRouter.CanonicalVerb(text.Trim().ToLowerInvariant()) ?? text.Trim().ToLowerInvariant(); if (text2 == canonical) { return true; } } return false; } } [HarmonyPatch(typeof(HUDManager), "AddTextToChatOnServer")] internal static class ChatCommandPatch { [HarmonyPrefix] private static bool Prefix(string chatMessage, int playerId) { try { Plugin.Logger.LogDebug((object)$"[ChatPatch] playerId={playerId} msg={chatMessage}"); if (string.IsNullOrEmpty(chatMessage)) { return true; } if (!chatMessage.StartsWith(ModConfig.CommandPrefix.Value)) { return true; } if (!CommandRouter.IsSenderTheHost(playerId)) { return true; } string raw = chatMessage.Substring(ModConfig.CommandPrefix.Value.Length).Trim(); bool flag = CommandRouter.Route(raw); return !flag; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[ChatCommandPatch] {arg}"); return true; } } } [HarmonyPatch(typeof(TimeOfDay), "Update")] internal static class TimeOfDayUpdatePatch { private static int _lastHour = -1; [HarmonyPostfix] private static void Postfix(TimeOfDay __instance) { try { if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsHost && __instance.hour != _lastHour) { _lastHour = __instance.hour; if (!TournamentGate.Active) { ScrapUpdateFeature.OnHourChanged(); TimeAnnounceFeature.OnHourChanged(); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[TimeOfDayUpdatePatch] {arg}"); } } } [HarmonyPatch(typeof(StartOfRound), "Start")] internal static class StartOfRoundStartPatch { [HarmonyPostfix] private static void Postfix() { try { DefaultExclusionsSeeder.TrySeed(); LobbyGuard.Reset(); PlayerJoinLog.ResetSession(); TournamentGate.Register(); ((MonoBehaviour)CoroutineRunner.Instance).StartCoroutine(StartupLine()); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[StartOfRoundStartPatch] {arg}"); } } private static IEnumerator StartupLine() { yield return (object)new WaitForSeconds(3f); SuiteAnnounce.MaybeAnnounce("local.simplesell"); } } [HarmonyPatch(typeof(StartOfRound), "OnPlayerConnectedClientRpc")] internal static class PlayerConnectedPatch { [HarmonyPostfix] private static void Postfix() { try { if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsHost) { PlayerJoinLog.LogLobbyPlayers(); LobbyGuard.KickAnyBanned(); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[PlayerConnectedPatch] {arg}"); } } } [HarmonyPatch(typeof(RoundManager), "FinishGeneratingNewLevelClientRpc")] internal static class ShipLandedPatch { [HarmonyPostfix] private static void Postfix() { try { if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsHost) { DayBriefingFeature.OnLandingStarted(); LateJoin.Refresh(); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[ShipLandedPatch] {arg}"); } } } [HarmonyPatch(typeof(GiantKiwiAI), "SpawnEggsClientRpc")] internal static class SapsuckerEggsPatch { [HarmonyPostfix] private static void Postfix(GiantKiwiAI __instance, int[] eggScrapValues) { try { if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsHost) { DayBriefingFeature.OnEggsSpawned(((Object)__instance).GetInstanceID(), eggScrapValues); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[SapsuckerEggsPatch] {arg}"); } } } [HarmonyPatch(typeof(StartOfRound), "ShipHasLeft")] internal static class ShipHasLeftPatch { [HarmonyPostfix] private static void Postfix() { try { if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsHost) { ScrapUpdateFeature.OnReachedOrbit(); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[ShipHasLeftPatch] {arg}"); } } } [HarmonyPatch(typeof(ItemDropship), "OpenShipDoorsOnServer")] internal static class DropshipOpenPatch { [HarmonyPrefix] private static void Prefix(ItemDropship __instance, out HashSet<int> __state) { __state = null; try { if (!((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsHost && __instance.shipLanded && !__instance.shipDoorsOpened && __instance.itemsToDeliver.Count != 0) { __state = DropshipDeliveries.SnapshotIds(); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[DropshipOpenPatch.Prefix] {arg}"); } } [HarmonyPostfix] private static void Postfix(HashSet<int> __state) { try { if (__state != null) { DropshipDeliveries.RecordNew(__state); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[DropshipOpenPatch.Postfix] {arg}"); } } } [HarmonyPatch(typeof(GameNetworkManager), "ConnectionApproval")] internal static class LateJoinApprovalPatch { [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(GameNetworkManager __instance, out bool __state) { __state = false; try { if (LateJoin.WindowOpen() && __instance.gameHasStarted) { __state = true; __instance.gameHasStarted = false; Plugin.Logger.LogInfo((object)"[LateJoin] allowing a join attempt (in orbit)."); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoinApprovalPatch.Prefix] {arg}"); } } [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(GameNetworkManager __instance, bool __state) { try { if (__state) { __instance.gameHasStarted = true; } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoinApprovalPatch.Postfix] {arg}"); } } } [HarmonyPatch(typeof(GameNetworkManager), "LeaveLobbyAtGameStart")] internal static class LateJoinKeepLobbyPatch { [HarmonyPrefix] private static bool Prefix() { try { if (!LateJoin.Enabled) { return true; } Plugin.Logger.LogInfo((object)"[LateJoin] keeping the Steam lobby alive past game start."); LateJoin.ResetPushState(); LateJoin.Refresh(); return false; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoinKeepLobbyPatch] {arg}"); return true; } } } [HarmonyPatch(typeof(GameNetworkManager), "SetLobbyJoinable")] internal static class LateJoinJoinablePatch { [HarmonyPrefix] private static void Prefix(ref bool joinable) { try { if (!joinable && LateJoin.WindowOpen()) { joinable = true; } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoinJoinablePatch] {arg}"); } } } [HarmonyPatch(typeof(StartOfRound), "OnPlayerConnectedClientRpc")] internal static class LateJoinWeatherPatch { internal struct Snapshot { internal LevelWeatherType[] Levels; internal LevelWeatherType Current; } [HarmonyPrefix] [HarmonyPriority(800)] private static void Prefix(StartOfRound __instance, ulong clientId, out Snapshot __state) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) __state = default(Snapshot); try { if (!LateJoin.Enabled || (Object)(object)GameNetworkManager.Instance == (Object)null || !GameNetworkManager.Instance.gameHasStarted) { return; } NetworkManager singleton = NetworkManager.Singleton; if ((!((Object)(object)singleton != (Object)null) || singleton.LocalClientId != clientId) && __instance.levels != null) { LevelWeatherType[] array = (LevelWeatherType[])(object)new LevelWeatherType[__instance.levels.Length]; for (int i = 0; i < array.Length; i++) { array[i] = (LevelWeatherType)(((Object)(object)__instance.levels[i] != (Object)null) ? ((int)__instance.levels[i].currentWeather) : (-1)); } __state = new Snapshot { Levels = array, Current = (LevelWeatherType)((!((Object)(object)TimeOfDay.Instance != (Object)null)) ? (-1) : ((int)TimeOfDay.Instance.currentLevelWeather)) }; } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoinWeatherPatch.Prefix] {arg}"); } } [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(StartOfRound __instance, Snapshot __state) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Invalid comparison between Unknown and I4 //IL_0089: 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_0068: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) try { if (__state.Levels == null || __instance.levels == null) { return; } bool flag = false; int num = Math.Min(__state.Levels.Length, __instance.levels.Length); for (int i = 0; i < num; i++) { if (!((Object)(object)__instance.levels[i] == (Object)null) && (int)__instance.levels[i].currentWeather != (int)__state.Levels[i]) { __instance.levels[i].currentWeather = __state.Levels[i]; flag = true; } } if ((Object)(object)TimeOfDay.Instance != (Object)null && TimeOfDay.Instance.currentLevelWeather != __state.Current) { TimeOfDay.Instance.currentLevelWeather = __state.Current; flag = true; } if (flag) { Plugin.Logger.LogInfo((object)"[LateJoin] restored weather after a mid-run join (vanilla re-rolls it on connect)."); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoinWeatherPatch.Postfix] {arg}"); } } } [HarmonyPatch(typeof(StartOfRound), "StartGame")] internal static class LateJoinLandingPatch { [HarmonyPostfix] private static void Postfix() { LateJoin.Refresh(); } } [HarmonyPatch(typeof(StartOfRound), "SetShipReadyToLand")] internal static class LateJoinOrbitPatch { [HarmonyPostfix] private static void Postfix() { LateJoin.Refresh(); } } [HarmonyPatch(typeof(StartOfRound), "ShipLeave")] internal static class LateJoinDepartPatch { [HarmonyPostfix] private static void Postfix() { LateJoin.Refresh(); } } [HarmonyPatch(typeof(CustomMessagingManager), "InvokeNamedMessage")] internal static class NamedMessageGuardPatch { [HarmonyPrefix] private static void Prefix(ulong sender, ulong hash) { try { if (!LobbyGuard.IsSuiteMessage(hash)) { LobbyGuard.OnModdedTraffic(sender, "named message"); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[NamedMessageGuardPatch] {arg}"); } } } [HarmonyPatch(typeof(CustomMessagingManager), "InvokeUnnamedMessage")] internal static class UnnamedMessageGuardPatch { [HarmonyPrefix] private static void Prefix(ulong clientId) { try { LobbyGuard.OnModdedTraffic(clientId, "unnamed message"); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[UnnamedMessageGuardPatch] {arg}"); } } } [HarmonyPatch(typeof(DepositItemsDesk), "AddObjectToDeskServerRpc")] internal static class DeskBeamGuardPatch { [HarmonyPrefix] private static bool Prefix(DepositItemsDesk __instance, NetworkObjectReference grabbableObjectNetObject) { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) try { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsServer) { return true; } if (SaleDisplayAggregator.BatchedSaleInProgress) { return true; } NetworkObject val = default(NetworkObject); if (!((NetworkObjectReference)(ref grabbableObjectNetObject)).TryGet(ref val, (NetworkManager)null) || (Object)(object)val == (Object)null) { return true; } GrabbableObject component = ((Component)val).GetComponent<GrabbableObject>(); if ((Object)(object)component == (Object)null) { return true; } Vector3 val2; if (!((Object)(object)__instance.triggerCollider != (Object)null)) { val2 = ((Component)__instance).transform.position; } else { Bounds bounds = ((Collider)__instance.triggerCollider).bounds; val2 = ((Bounds)(ref bounds)).center; } Vector3 val3 = val2; float num = Vector3.Distance(((Component)component).transform.position, val3); if (num <= ModConfig.DeskGuardRange.Value) { return true; } LobbyGuard.OnDeskBeamBlocked(component, num); return false; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[DeskBeamGuardPatch] {arg}"); return true; } } } [HarmonyPatch(typeof(GameNetworkManager), "SteamMatchmaking_OnLobbyCreated")] internal static class LobbyCreatedPatch { [HarmonyPostfix] private static void Postfix(Result result, Lobby lobby) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_0013: Unknown result type (might be due to invalid IL or missing references) try { if ((int)result == 1 && ModConfig.HideLobbyModTag.Value) { LobbyTagFeature.Apply(hide: true, lobby); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyCreatedPatch] {arg}"); } } } [HarmonyPatch(typeof(DepositItemsDesk), "SellAndDisplayItemProfits")] internal static class SellAndDisplayItemProfitsPatch { [HarmonyPrefix] private static void Prefix(int profit, int newGroupCredits) { if (SaleDisplayAggregator.BatchedSaleInProgress && !((Object)(object)NetworkManager.Singleton == (Object)null) && NetworkManager.Singleton.IsServer) { SaleDisplayAggregator.NoteBatchSold(); SaleDisplayAggregator.Accumulate(profit, newGroupCredits); } } } [HarmonyPatch(typeof(HUDManager), "DisplayCreditsEarning")] internal static class DisplayCreditsEarningPatch { [HarmonyPrefix] private static bool Prefix() { return !SaleDisplayAggregator.ShouldSuppressDisplay(); } } } namespace SimpleSell.Features { internal static class CrewListFix { [HarmonyPatch(typeof(GameNetworkManager), "SteamMatchmaking_OnLobbyMemberJoined")] private static class LobbyMemberJoinedPatch { [HarmonyPostfix] private static void Postfix() { Reconcile(); } } [HarmonyPatch(typeof(GameNetworkManager), "SteamMatchmaking_OnLobbyMemberLeave")] private static class LobbyMemberLeavePatch { [HarmonyPostfix] private static void Postfix() { Reconcile(); } } [HarmonyPatch(typeof(GameNetworkManager), "Singleton_OnClientDisconnectCallback")] private static class ClientDisconnectPatch { [HarmonyPostfix] private static void Postfix() { Reconcile(); } } internal static void Reconcile() { try { if (!ModConfig.FixCrewList.Value) { return; } StartOfRound instance = StartOfRound.Instance; if (instance?.allPlayerScripts == null || instance.ClientPlayerList == null) { return; } QuickMenuManager val = Object.FindObjectOfType<QuickMenuManager>(); if (val?.playerListSlots == null) { return; } HashSet<int> hashSet = new HashSet<int>(instance.ClientPlayerList.Values); int num = Mathf.Min(val.playerListSlots.Length, instance.allPlayerScripts.Length); int num2 = 0; int num3 = 0; for (int i = 0; i < num; i++) { PlayerListSlot val2 = val.playerListSlots[i]; if (val2 == null) { continue; } PlayerControllerB val3 = instance.allPlayerScripts[i]; if (hashSet.Contains(i) && (Object)(object)val3 != (Object)null && val3.playerSteamId != 0L) { if (!val2.isConnected || val2.playerSteamId != val3.playerSteamId) { val.AddUserToPlayerList(val3.playerSteamId, val3.playerUsername, i); num2++; } } else if (val2.isConnected || ((Object)(object)val2.slotContainer != (Object)null && val2.slotContainer.activeSelf)) { val.RemoveUserFromPlayerList(i); num3++; } } if (num2 + num3 > 0) { Plugin.Logger.LogInfo((object)$"[CrewList] tidied the pause menu list: {num2} restored, {num3} leftover name(s) removed."); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[CrewListFix.Reconcile] {arg}"); } } } internal static class DayBriefingFeature { private const float DoorWaitLimit = 60f; private static bool _watching; private static readonly HashSet<int> _eggNestsAnnounced = new HashSet<int>(); internal static void OnLandingStarted() { _eggNestsAnnounced.Clear(); if (ModConfig.DayBriefing.Value && !_watching && !TournamentGate.Active) { _watching = true; Plugin.Logger.LogInfo((object)"[DayBriefing] level ready, watching for the ship doors."); ((MonoBehaviour)CoroutineRunner.Instance).StartCoroutine(BriefWhenDoorsOpen()); } } private static IEnumerator BriefWhenDoorsOpen() { float waited = 0f; float enabledFor = 0f; string how = "time limit"; while (waited < 60f) { StartOfRound instance = StartOfRound.Instance; Animator val = instance?.shipDoorsAnimator; if ((Object)(object)val != (Object)null && !val.GetBool("Closed")) { how = "doors open"; break; } if ((Object)(object)instance != (Object)null && instance.shipDoorsEnabled) { enabledFor += Time.deltaTime; if (enabledFor >= 1.5f) { how = "doors enabled"; break; } } waited += Time.deltaTime; yield return null; } _watching = false; try { Plugin.Logger.LogInfo((object)$"[DayBriefing] briefing ({how}, {waited:0.0}s)."); Broadcast(); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[DayBriefing] {arg}"); } } internal static void OnEggsSpawned(int birdId, int[] values) { if (ModConfig.DayBriefing.Value && values != null && values.Length != 0 && !TournamentGate.Active && _eggNestsAnnounced.Add(birdId)) { int num = values.Sum(); ChatUtil.Send(string.Format("Sapsucker nest: {0} egg{1} (${2})", values.Length, (values.Length == 1) ? "" : "s", num), ModConfig.DayBriefingBroadcast.Value); } } private static void Broadcast() { if (DeskHelper.HasSellDesk()) { return; } List<string> list = new List<string>(); string interiorName = GetInteriorName(); if (interiorName != null) { list.Add(interiorName); } int num = 0; int num2 = 0; int num3 = 0; int num4 = 0; int num5 = 0; int num6 = 0; int num7 = 0; VehicleController vehicle = Object.FindObjectOfType<VehicleController>(); Bounds? vehicleBounds = ScrapScanner.ComputeVehicleBounds(vehicle); GrabbableObject[] array = Object.FindObjectsByType<GrabbableObject>((FindObjectsSortMode)0); GrabbableObject[] array2 = array; foreach (GrabbableObject val in array2) { if ((Object)(object)val == (Object)null || (Object)(object)val.itemProperties == (Object)null || !val.itemProperties.isScrap || val.isInShipRoom || val.isInElevator || val.scrapValue <= 0) { continue; } if (ScrapScanner.ClassifyLocation(val, vehicle, vehicleBounds) != ScrapLocation.Other) { num7++; continue; } string itemName = val.itemProperties.itemName; if (val is KiwiBabyItem) { num5++; num6 += val.scrapValue; } else if (!string.IsNullOrEmpty(itemName) && itemName.ToLowerInvariant().Contains("hive")) { num3++; num4 += val.scrapValue; } else { num++; num2 += val.scrapValue; } } if (num > 0) { list.Add($"{num} scrap (${num2})"); } if (num3 > 0) { list.Add(string.Format("{0} hive{1} (${2})", num3, (num3 == 1) ? "" : "s", num4)); } if (num5 > 0) { list.Add(string.Format("{0} egg{1} (${2})", num5, (num5 == 1) ? "" : "s", num6)); } if ((Object)(object)RoundManager.Instance != (Object)null) { ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogInfo((object)($"[DayBriefing] scan: count={num} value={num2} hives={num3} (${num4}) eggs={num5} (${num6}) " + $"excludedAboard={num7} " + $"(RoundManager.totalScrapValueInLevel={RoundManager.Instance.totalScrapValueInLevel:0})")); } } if (list.Count != 0) { ChatUtil.Send("Today: " + string.Join(" · ", list), ModConfig.DayBriefingBroadcast.Value); } } internal static void HandleToggle(string token) { switch (token) { case "l": case "live": ModConfig.DayBriefing.Value = true; ModConfig.DayBriefingBroadcast.Value = true; ChatUtil.Send("Day briefing: ON (live — whole crew sees it)."); break; case "p": case "private": ModConfig.DayBriefing.Value = true; ModConfig.DayBriefingBroadcast.Value = false; ChatUtil.Send("Day briefing: ON (private — host only)."); break; case "o": case "off": ModConfig.DayBriefing.Value = false; ChatUtil.Send("Day briefing: OFF."); break; case "": { string text = ((!ModConfig.DayBriefing.Value) ? "OFF" : (ModConfig.DayBriefingBroadcast.Value ? "ON (live)" : "ON (private)")); ChatUtil.Send("Day briefing: " + text + ". Usage: /briefing live | private | off"); break; } default: ChatUtil.Send("Usage: /briefing live | private | off (shorthand: /b l | p | o)"); break; } } private static string GetInteriorName() { try { RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.dungeonGenerator == (Object)null) { return null; } IndoorMapType[] dungeonFlowTypes = instance.dungeonFlowTypes; int currentDungeonType = instance.currentDungeonType; object obj; if (dungeonFlowTypes == null || currentDungeonType < 0 || currentDungeonType >= dungeonFlowTypes.Length || !((Object)(object)dungeonFlowTypes[currentDungeonType]?.dungeonFlow != (Object)null)) { DungeonGenerator generator = instance.dungeonGenerator.Generator; if (generator == null) { obj = null; } else { DungeonFlow dungeonFlow = generator.DungeonFlow; obj = ((dungeonFlow != null) ? ((Object)dungeonFlow).name : null); } } else { obj = ((Object)dungeonFlowTypes[currentDungeonType].dungeonFlow).name; } string text = (string)obj; if (string.IsNullOrEmpty(text)) { return null; } string text2 = text.ToLowerInvariant(); if (text2.Contains("level1") || text2.Contains("factory")) { return "Factory"; } if (text2.Contains("level2") || text2.Contains("manor") || text2.Contains("mansion")) { return "Mansion"; } if (text2.Contains("level3") || text2.Contains("mineshaft") || text2.Contains("mine")) { return "Mineshaft"; } return text; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[DayBriefing.GetInteriorName] {arg}"); return null; } } } internal static class DropshipDeliveries { private static readonly HashSet<GrabbableObject> Delivered = new HashSet<GrabbableObject>(); internal static HashSet<int> SnapshotIds() { HashSet<int> hashSet = new HashSet<int>(); GrabbableObject[] array = Object.FindObjectsByType<GrabbableObject>((FindObjectsSortMode)0); foreach (GrabbableObject val in array) { hashSet.Add(((Object)val).GetInstanceID()); } return hashSet; } internal static void RecordNew(HashSet<int> before) { int num = 0; GrabbableObject[] array = Object.FindObjectsByType<GrabbableObject>((FindObjectsSortMode)0); foreach (GrabbableObject val in array) { if (!before.Contains(((Object)val).GetInstanceID())) { Delivered.Add(val); num++; } } Delivered.RemoveWhere((GrabbableObject o) => (Object)(object)o == (Object)null); if (num > 0) { Plugin.Logger.LogInfo((object)$"[Dropship] tracked {num} delivered item(s)"); } } internal static bool IsWaitingDelivery(GrabbableObject o) { if ((Object)(object)o != (Object)null && Delivered.Contains(o) && !o.isHeld) { return !o.heldByPlayerOnServer; } return false; } internal static void Forget(GrabbableObject o) { Delivered.Remove(o); } } internal static class LateJoin { private static bool _lastPushed; internal static bool Enabled { get { if (ModConfig.AllowLateJoin.Value) { return !TournamentGate.Active; } return false; } } internal static bool InOrbit() { try { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return false; } return instance.inShipPhase && !instance.beganLoadingNewLevel && !instance.shipIsLeaving && !instance.travellingToNewLevel; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoin.InOrbit] {arg}"); return false; } } internal static bool WindowOpen() { if (Enabled) { return InOrbit(); } return false; } internal static void Refresh() { try { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null) { return; } bool flag = WindowOpen(); if (flag != _lastPushed) { _lastPushed = flag; instance.SetLobbyJoinable(flag); Plugin.Logger.LogInfo((object)("[LateJoin] lobby " + (flag ? "OPEN (in orbit)" : "closed") + ".")); if (flag) { ChatUtil.Send("Lobby open — someone can join while we're in orbit."); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LateJoin.Refresh] {arg}"); } } internal static void ResetPushState() { _lastPushed = false; } internal static string Status() { if (!Enabled) { return "Late join: OFF. /lobby late on to allow joins while in orbit."; } if (!InOrbit()) { return "Late join: ON — but closed until we're back in orbit."; } return "Late join: ON — lobby is OPEN right now (in orbit)."; } } internal static class LobbyGuard { private static readonly HashSet<ulong> _flagged = new HashSet<ulong>(); private static float _lastBeamAlert; private static int _beamBlockedSinceAlert; private static HashSet<ulong> _suiteHashes; internal static IReadOnlyCollection<ulong> Flagged => _flagged; internal static bool IsFlagged(ulong clientId) { return _flagged.Contains(clientId); } internal static void KickAnyBanned() { try { NetworkManager singleton = NetworkManager.Singleton; StartOfRound instance = StartOfRound.Instance; if ((Object)(object)singleton == (Object)null || !singleton.IsHost || instance?.allPlayerScripts == null || PersistedIdList.Bans.Count == 0) { return; } PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val2 in allPlayerScripts) { if (!((Object)(object)val2 == (Object)null) && val2.isPlayerControlled && !((Object)(object)val2 == (Object)(object)val) && val2.playerSteamId != 0L && PersistedIdList.Bans.Contains(val2.playerSteamId)) { Plugin.Logger.LogInfo((object)$"[Ban] {val2.playerUsername} ({val2.playerSteamId}) is banned: kicking."); ChatUtil.Send("⚠ " + val2.playerUsername + " is banned: kicked."); Kick(val2.actualClientId); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyGuard.KickAnyBanned] {arg}"); } } internal static bool IsSuiteMessage(ulong hash) { try { if (_suiteHashes == null) { _suiteHashes = new HashSet<ulong>(); string[] array = new string[8] { "SimpleSuite.Hello", "SimpleSuite.Verdict", "SimpleSuite.Spray", "SimpleBlackBox.Hello", "SimpleBlackBox.Verdict", "SimpleHQ.Hello", "SimpleWeather.Hello", "SimpleWeather.Verdict" }; foreach (string text in array) { _suiteHashes.Add(XXHash.Hash32(text)); _suiteHashes.Add(XXHash.Hash64(text)); } } return _suiteHashes.Contains(hash); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyGuard.IsSuiteMessage] {arg}"); return false; } } internal static void Reset() { _flagged.Clear(); _beamBlockedSinceAlert = 0; } internal static void OnModdedTraffic(ulong sender, string kind) { try { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsHost || sender == singleton.LocalClientId) { return; } if (_flagged.Add(sender)) { string text = ResolveName(sender); ChatUtil.Send("⚠ " + text + " is running networked mods (" + kind + ")."); if (ModConfig.GuardMode.Value == GuardEnforcement.Kick) { if (Kick(sender)) { ChatUtil.Send("Auto-kicked " + text + " (guard mode: kick)."); } } else { ChatUtil.Send("Guard mode is warn — /kick " + text + " or /guard kick to enforce."); } } else if (ModConfig.GuardMode.Value == GuardEnforcement.Kick) { Kick(sender); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyGuard.OnModdedTraffic] {arg}"); } } internal static void OnDeskBeamBlocked(GrabbableObject obj, float dist) { _beamBlockedSinceAlert++; ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogWarning((object)string.Format("[LobbyGuard] blocked desk beam: {0} from {1:F0}m away.", ((Object)(object)obj != (Object)null) ? ((Object)obj).name : "?", dist)); } if (!(Time.realtimeSinceStartup - _lastBeamAlert < 5f)) { _lastBeamAlert = Time.realtimeSinceStartup; ChatUtil.Send($"⚠ Blocked {_beamBlockedSinceAlert} item(s) beamed to the sell desk by a modded client. /players to review."); _beamBlockedSinceAlert = 0; } } internal static string ResolveName(ulong clientId) { PlayerControllerB val = FindByClientId(clientId); if (!((Object)(object)val != (Object)null) || string.IsNullOrEmpty(val.playerUsername)) { return $"client {clientId}"; } return val.playerUsername; } internal static PlayerControllerB FindByClientId(ulong clientId) { StartOfRound instance = StartOfRound.Instance; if (instance?.allPlayerScripts == null) { return null; } PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if ((Object)(object)val != (Object)null && val.isPlayerControlled && val.actualClientId == clientId) { return val; } } return null; } internal static bool Kick(ulong clientId) { try { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || clientId == singleton.LocalClientId) { return false; } StartOfRound instance = StartOfRound.Instance; if (instance?.allPlayerScripts != null) { for (int i = 0; i < instance.allPlayerScripts.Length; i++) { PlayerControllerB val = instance.allPlayerScripts[i]; if ((Object)(object)val != (Object)null && val.isPlayerControlled && val.actualClientId == clientId) { instance.KickPlayer(i); return true; } } } singleton.DisconnectClient(clientId, "Rejected by host."); return true; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyGuard.Kick] {arg}"); return false; } } } internal static class LobbyTagFeature { private static readonly string[] ModTagKeys = new string[1] { "dmods" }; private static readonly Dictionary<string, string> _saved = new Dictionary<string, string>(); private static string _visibility = "(unchanged)"; internal static bool Hidden { get; private set; } internal static string VisibilityStatus => _visibility; internal static void Apply(bool hide, Lobby? lobbyOverride = null) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) try { Lobby? val = lobbyOverride; Lobby? obj; if (!val.HasValue) { GameNetworkManager instance = GameNetworkManager.Instance; obj = ((instance != null) ? instance.currentLobby : ((Lobby?)null)); } else { obj = val; } Lobby? val2 = obj; if (!val2.HasValue || !val2.HasValue) { ChatUtil.Send("No active lobby."); return; } Lobby value = val2.Value; if (hide) { string[] modTagKeys = ModTagKeys; foreach (string text in modTagKeys) { string data = ((Lobby)(ref value)).GetData(text); if (!string.IsNullOrEmpty(data)) { _saved[text] = data; } ((Lobby)(ref value)).SetData(text, "f"); } Hidden = true; ChatUtil.Send("Lobby now presents as vanilla."); return; } foreach (KeyValuePair<string, string> item in _saved) { ((Lobby)(ref value)).SetData(item.Key, item.Value); } _saved.Clear(); Hidden = false; ChatUtil.Send("Lobby mod tag restored."); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyTagFeature] {arg}"); } } internal static void Status() { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) ChatUtil.Send((Hidden ? "Lobby tag: HIDDEN (presenting as vanilla)." : "Lobby tag: visible (normal modded tag).") + " Visibility: " + _visibility + "."); try { GameNetworkManager instance = GameNetworkManager.Instance; Lobby? val = ((instance != null) ? instance.currentLobby : ((Lobby?)null)); if (!val.HasValue || !val.HasValue) { ChatUtil.Send("⚠ No Steam lobby exists right now — nobody can join or be invited."); Plugin.Logger.LogWarning((object)"[LobbyStatus] currentLobby is null."); return; } Lobby value = val.Value; string data = ((Lobby)(ref value)).GetData("joinable"); string data2 = ((Lobby)(ref value)).GetData("vers"); int memberCount = ((Lobby)(ref value)).MemberCount; string text = ((data == "false") ? "joiners WILL be refused" : ((memberCount >= 4) ? "lobby is FULL (4)" : "a joiner should get in")); ChatUtil.Send($"Steam lobby: joinable=\"{data}\" · members {memberCount}/4 · vers {data2} → {text}."); Plugin.Logger.LogInfo((object)($"[LobbyStatus] id={((Lobby)(ref value)).Id} joinable=\"{data}\" members={memberCount} vers=\"{data2}\" " + $"lateJoin={ModConfig.AllowLateJoin.Value} window={LateJoin.InOrbit()}")); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyTagFeature.Status] {arg}"); } } internal static void SetVisibility(string mode) { //IL_00de: Unknown result type (might be due to invalid IL or missing references) //IL_00e3: Unknown result type (might be due to invalid IL or missing references) try { GameNetworkManager instance = GameNetworkManager.Instance; Lobby? val = ((instance != null) ? instance.currentLobby : ((Lobby?)null)); if (!val.HasValue || !val.HasValue) { HostSettings val2 = GameNetworkManager.Instance?.lobbyHostSettings; if (val2 == null || (mode != "public" && mode != "friends" && mode != "private")) { ChatUtil.Send("No active lobby."); return; } val2.isLobbyPublic = mode == "public"; _visibility = mode; ChatUtil.Send((mode == "public") ? "The Steam lobby is already closed (the game has started), so nobody new can join. Marked as public." : ("The Steam lobby is already closed (the game has started), so nobody new can join. Marked as " + ((mode == "friends") ? "friends only" : "private") + ".")); return; } Lobby value = val.Value; switch (mode) { case "public": ((Lobby)(ref value)).SetPublic(); break; case "friends": ((Lobby)(ref value)).SetFriendsOnly(); break; case "private": ((Lobby)(ref value)).SetPrivate(); break; default: ChatUtil.Send("Usage: /lobby public | friends | private"); return; } _visibility = mode; if (GameNetworkManager.Instance.lobbyHostSettings != null) { GameNetworkManager.Instance.lobbyHostSettings.isLobbyPublic = mode == "public"; } string text = ""; if (GameNetworkManager.Instance.gameHasStarted) { text = ((!ModConfig.AllowLateJoin.Value) ? " The run has started and late join is OFF — /lobby late on to allow joins." : (LateJoin.InOrbit() ? " Late join is on and we're in orbit — a friend can join now." : " Late join is on, but the window only opens in orbit.")); } ChatUtil.Send("Lobby visibility: " + mode + "." + text); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyTagFeature.SetVisibility] {arg}"); } } internal static void Rename(string newName) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) try { if (string.IsNullOrWhiteSpace(newName)) { ChatUtil.Send("Usage: /lobby name <new name>"); return; } GameNetworkManager instance = GameNetworkManager.Instance; Lobby? val = ((instance != null) ? instance.currentLobby : ((Lobby?)null)); if (!val.HasValue || !val.HasValue) { ChatUtil.Send("No active lobby."); return; } Lobby value = val.Value; ((Lobby)(ref value)).SetData("name", newName); GameNetworkManager.Instance.steamLobbyName = newName; ChatUtil.Send("Lobby renamed to \"" + newName + "\" — new browsers see it immediately."); } catch (Exception arg) { Plugin.Logger.LogError((object)$"[LobbyTagFeature.Rename] {arg}"); } } } internal static class PlayerJoinLog { private static readonly string LogPath = Path.Combine(Paths.ConfigPath, "SimpleSell.playerlog.txt"); private static readonly HashSet<ulong> _loggedThisSession = new HashSet<ulong>(); internal static void ResetSession() { _loggedThisSession.Clear(); } internal static void LogLobbyPlayers() { try { StartOfRound instance = StartOfRound.Instance; if (instance?.allPlayerScripts == null) { return; } PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val2 in allPlayerScripts) { if (!((Object)(object)val2 == (Object)null) && val2.isPlayerControlled && !((Object)(object)val2 == (Object)(object)val) && val2.playerSteamId != 0L && _loggedThisSession.Add(val2.playerSteamId)) { File.AppendAllText(LogPath, $"{DateTime.Now:yyyy-MM-dd HH:mm}|{val2.playerSteamId}|{val2.playerUsername}\r\n"); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[PlayerJoinLog] {arg}"); } } internal static List<(ulong SteamId, string Name, string LastSeen)> FindByName(string query) { List<(ulong, string, string)> list = new List<(ulong, string, string)>(); try { if (!File.Exists(LogPath) || string.IsNullOrWhiteSpace(query)) { return list; } string value = Squash(query); HashSet<ulong> hashSet = new HashSet<ulong>(); foreach (string item in File.ReadAllLines(LogPath).Reverse()) { string[] array = item.Split('|'); if (array.Length >= 3 && ulong.TryParse(array[1], out var result) && Squash(array[2]).Contains(value) && hashSet.Add(result)) { list.Add((result, array[2], array[0])); } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[PlayerJoinLog.FindByName] {arg}"); } return list; } private static string Squash(string s) { return new string((s ?? "").ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray()); } internal static List<(ulong SteamId, string Name, string LastSeen)> Recent(int count, HashSet<ulong> exclude) { List<(ulong, string, string)> list = new List<(ulong, string, string)>(); try { if (!File.Exists(LogPath)) { return list; } HashSet<ulong> hashSet = new HashSet<ulong>(); foreach (string item in File.ReadAllLines(LogPath).Reverse()) { string[] array = item.Split('|'); if (array.Length == 3 && ulong.TryParse(array[1], out var result) && result != 0L && (exclude == null || !exclude.Contains(result)) && hashSet.Add(result)) { list.Add((result, array[2], array[0])); if (list.Count >= count) { break; } } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[PlayerJoinLog.Recent] {arg}"); } return list; } } internal static class ScrapUpdateFeature { internal static bool Enabled { get; private set; } internal static bool Broadcast { get; private set; } internal static void Enable(bool broadcast) { Enabled = true; Broadcast = broadcast; ChatUtil.Send(broadcast ? "Scrap Update Enabled (Live)" : "Scrap Update Enabled (Private)"); PostUpdateLine(); } internal static void Disable() { Enabled = false; Broadcast = false; ChatUtil.Send("Scrap Update Disabled"); } internal static void SetDisplayMode(UpdateDisplayMode mode) { ModConfig.UpdateMode.Value = mode; ChatUtil.Send($"Scrap update mode: {mode}"); if (Enabled) { PostUpdateLine(); } } internal static void OnHourChanged() { if (Enabled) { PostUpdateLine(); } } private static void PostUpdateLine() { string text = TimeUtil.FormatGameTime(); VehicleController val = Object.FindObjectOfType<VehicleController>(); bool flag = (Object)(object)val != (Object)null; bool flag2 = flag && val.magnetedToShip; int num = 0; int num2 = 0; try { List<ScrapItem> items = ScrapScanner.ScanAll(shipAndCruiserOnly: true, applyExclusions: false); num = ScrapScanner.SumValue(items, ScrapLocation.Ship); num2 = ScrapScanner.SumValue(items, ScrapLocation.Cruiser); ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogInfo((object)$"[ScrapUpdate] mode={ModConfig.UpdateMode.Value} ship={num} cruiser={num2} magneted={flag2}"); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[ScrapUpdate] value computation failed: {arg}"); ChatUtil.SendRaw("<color=#5fe35f>" + text + " — ship scrap: $?</color>", Broadcast); return; } int total = num + num2; string text2 = FormatValueLine(num, num2, total, flag, flag2); ChatUtil.SendRaw("<color=#5fe35f>" + text + " — " + text2 + "</color>", Broadcast); } private static string FormatValueLine(int ship, int cruiser, int total, bool hasVehicle, bool magnetted) { switch (ModConfig.UpdateMode.Value) { case UpdateDisplayMode.Ship: return $"ship: ${ship}"; case UpdateDisplayMode.Cruiser: if (!hasVehicle) { return "cruiser: no cruiser"; } if (magnetted) { return "cruiser: magnetted (counted in ship)"; } return $"cruiser: ${cruiser}"; case UpdateDisplayMode.Combined: return $"ship scrap: ${total}"; default: if (magnetted) { return $"ship (incl. magnetted cruiser): ${total}"; } if (!hasVehicle) { return $"ship: ${ship} · total: ${total}"; } return $"ship: ${ship} · cruiser: ${cruiser} · total: ${total}"; } } internal static void OnReachedOrbit() { if (ModConfig.AutoMergeOnOrbit.Value && !TournamentGate.Active) { UtilityCommands.Merge(null, quiet: true); OrganizeService.OrganizeScrap("", quiet: true); } } } internal static class TimeAnnounceFeature { internal static bool Enabled { get; private set; } internal static bool Broadcast { get; private set; } internal static void Handle(string token) { switch (token) { case "l": case "live": Enabled = true; Broadcast = true; ChatUtil.Send("Time Update Enabled (Live)"); PostTimeLine(); break; case "p": case "private": Enabled = true; Broadcast = false; ChatUtil.Send("Time Update Enabled (Private)"); PostTimeLine(); break; case "o": case "off": Enabled = false; Broadcast = false; ChatUtil.Send("Time Update Disabled"); break; default: ChatUtil.Send("Usage: /time update l (live) | p (private) | o (off)"); break; } } internal static void OnHourChanged() { if (Enabled) { PostTimeLine(); } } private static void PostTimeLine() { ChatUtil.SendRaw("<color=#6fb7ff>" + TimeUtil.FormatGameTime() + "</color>", Broadcast); } } } namespace SimpleSell.Core { internal sealed class ItemAnchor { internal string ItemName; internal ScrapLocation Loc; internal Vector3 LocalPos; internal bool Stack; internal bool Exact; } internal sealed class AnchorStore { internal static readonly AnchorStore Equipment = new AnchorStore(() => ModConfig.EquipItemAnchors); internal static readonly AnchorStore Scrap = new AnchorStore(() => ModConfig.ScrapItemAnchors); private readonly Func<ConfigEntry<string>> _entry; private Dictionary<string, ItemAnchor> _map; internal IReadOnlyCollection<ItemAnchor> All => Load().Values.ToList(); private AnchorStore(Func<ConfigEntry<string>> entry) { _entry = entry; } private Dictionary<string, ItemAnchor> Load() { //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: Unknown result type (might be due to invalid IL or missing references) if (_map != null) { return _map; } _map = new Dictionary<string, ItemAnchor>(); try { string[] array = _entry().Value.Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { string[] array2 = text.Split('|'); if (array2.Length == 4 || array2.Length == 5) { string text2 = array2[0].Trim(); if (text2.Length != 0 && TryParseVec3(array2[2], out var v)) { _map[text2.ToLowerInvariant()] = new ItemAnchor { ItemName = text2, Loc = ((array2[1].Trim().ToLowerInvariant() == "cruiser") ? ScrapLocation.Cruiser : ScrapLocation.Ship), LocalPos = v, Stack = (array2[3].Trim().ToLowerInvariant() == "stack"), Exact = (array2.Length == 5 && array2[4].Trim().ToLowerInvariant() == "exact") }; } } } } catch (Exception arg) { Plugin.Logger.LogError((object)$"[AnchorStore] {arg}"); } return _map; } internal ItemAnchor Get(string itemName) { if (string.IsNullOrEmpty(itemName)) { return null; } if (!Load().TryGetValue(itemName.ToLowerInvariant(), out var value)) { return null; } return value; } internal void Set(ItemAnchor anchor) { Load()[anchor.ItemName.ToLowerInvariant()] = anchor; Save(); } internal bool Remove(string itemName) { bool flag = Load().Remove(itemName.ToLowerInvariant()); if (flag) { Save(); } return flag; } private void Save() { _entry().Value = string.Join(";", from a in Load().Values orderby a.ItemName select a.ItemName + "|" + ((a.Loc == ScrapLocation.Cruiser) ? "cruiser" : "ship") + "|" + FormatVec3(a.LocalPos) + "|" + (a.Stack ? "stack" : "pile") + (a.Exact ? "|exact" : "")); } internal static string FormatVec3(Vector3 v) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) return string.Format(CultureInfo.InvariantCulture, "{0:0.##},{1:0.##},{2:0.##}", v.x, v.y, v.z); } internal static bool TryParseVec3(string s, out Vector3 v) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) v = Vector3.zero; if (string.IsNullOrWhiteSpace(s)) { return false; } string[] array = s.Split(','); if (array.Length != 3) { return false; } if (float.TryParse(array[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && float.TryParse(array[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result2) && float.TryParse(array[2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var result3)) { v = new Vector3(result, result2, result3); return true; } return false; } } internal static class BankBridge { private static bool _looked; private static PropertyInfo _total; private static PropertyInfo _count; internal static bool TryGet(out int total, out int count) { total = 0; count = 0; try { if (!_looked) { _looked = true; if (Chainloader.PluginInfos.ContainsKey("local.simplehq")) { Type type = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (!(assembly.GetName().Name != "SimpleHQ")) { type = assembly.GetType("SimpleHQ.BankInfo"); if (type != null) { break; } } } _total = type?.GetProperty("Total", BindingFlags.Static | BindingFlags.Public); _count = type?.GetProperty("Count", BindingFlags.Static | BindingFlags.Public); if (_total == null) { Plugin.Logger.LogWarning((object)"[BankBridge] SimpleHQ is loaded but has no BankInfo.Total; bank left out of /stock."); } } } if (_total == null) { return false; } total = (int)_total.GetValue(null); count = ((_count != null) ? ((int)_count.GetValue(null)) : 0); return true; } catch (Exception arg) { Plugin.Logger.LogError((object)$"[BankBridge] {arg}"); _total = null; return false; } } } internal sealed class CoroutineRunner : MonoBehaviour { private static CoroutineRunner _instance; internal static CoroutineRunner Instance { get { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown if ((Object)(object)_instance == (Object)null) { GameObject val = new GameObject("SimpleSell.CoroutineRunner"); ((Object)val).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)val); _instance = val.AddComponent<CoroutineRunner>(); } return _instance; } } } internal static class DefaultExclusionsSeeder { private static readonly (string Query, string FallbackCanonical)[] DefaultWeapons = new(string, string)[4] { ("kitchen knife", "Kitchen knife"), ("double-barrel", "Double-barrel"), ("shotgun shell", "Shotgun shell"), ("homemade flashbang", "Homemade flashbang") }; internal static void TrySeed() { TrySeedGear(); if (ModConfig.DefaultExclusionsSeeded.Value) { return; } StartOfRound instance = StartOfRound.Instance; if (instance?.allItemsList?.itemsList == null || instance.allItemsList.itemsList.Count == 0) { return; } int num = 0; (string, string)[] defaultWeapons = DefaultWeapons; for (int i = 0; i < defaultWeapons.Length; i++) { (string, string) tuple = defaultWeapons[i]; string item = tuple.Item1; string item2 = tuple.Item2; ItemNameResolver.ResolveResult resolveResult = ItemNameResolver.Resolve(item); string text = ((resolveResult.Kind == ItemNameResolver.ResultKind.Ok) ? resolveResult.Name : item2); if (ExclusionList.Add(text)) { num++; ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogInfo((object)("[DefaultExclusions] seeded \"" + text + "\" (query=\"" + item + "\").")); } } else { ManualLogSource logger2 = Plugin.Logger; if (logger2 != null) { logger2.LogInfo((object)("[DefaultExclusions] \"" + text + "\" already excluded.")); } } } ModConfig.DefaultExclusionsSeeded.Value = true; ManualLogSource logger3 = Plugin.Logger; if (logger3 != null) { logger3.LogInfo((object)$"[DefaultExclusions] seed complete — {num} new exclusion(s), {ExclusionList.Count} total."); } } private static void TrySeedGear() { if (ModConfig.GearSeeded.Value) { return; } StartOfRound instance = StartOfRound.Instance; if (instance?.allItemsList?.itemsList == null || instance.allItemsList.itemsList.Count == 0) { return; } int num = 0; (string, string)[] defaultWeapons = DefaultWeapons; for (int i = 0; i < defaultWeapons.Length; i++) { (string, string) tuple = defaultWeapons[i]; string item = tuple.Item1; string item2 = tuple.Item2; ItemNameResolver.ResolveResult resolveResult = ItemNameResolver.Resolve(item); string canonicalName = ((resolveResult.Kind == ItemNameResolver.ResultKind.Ok) ? resolveResult.Name : item2); if (GearList.Add(canonicalName)) { num++; } } ModConfig.GearSeeded.Value = true; ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogInfo((object)$"[GearSeed] seeded {num} weapon(s) into the gear list ({GearList.Count} total)."); } if (ModConfig.WeaponExclusionsMigrated.Value) { return; } int num2 = 0; (string, string)[] defaultWeapons2 = DefaultWeapons; for (int j = 0; j < defaultWeapons2.Length; j++) { (string, string) tuple2 = defaultWeapons2[j]; string item3 = tuple2.Item1; string item4 = tuple2.Item2; ItemNameResolver.ResolveResult resolveResult2 = ItemNameResolver.Resolve(item3); string canonicalName2 = ((resolveResult2.Kind == ItemNameResolver.ResultKind.Ok) ? resolveResult2.Name : item4); if (ExclusionList.Remove(canonicalName2)) { num2++; } } ModConfig.WeaponExclusionsMigrated.Value = true; ManualLogSource logger2 = Plugin.Logger; if (logger2 != null) { logger2.LogInfo((object)$"[GearSeed] lifted {num2} weapon sell-exclusion(s) — gear list governs them now."); } } } internal static class DeskHelper { private struct SoldRecord { internal string Name; internal int Value; internal bool IsBody; internal SaleDisplayAggregator.SoldLine ToDisplayLine() { return new SaleDisplayAggregator.SoldLine { Name = Name, Value = Value }; } } private const int DeskCounterCap = 12; internal static DepositItemsDesk FindDesk() { return Object.FindObjectOfType<DepositItemsDesk>(); } internal static bool HasSellDesk() { return (Object)(object)FindDesk() != (Object)null; } internal static void DepositAndSell(List<ScrapItem> chosen) { DepositItemsDesk val = FindDesk(); if ((Object)(object)val == (Object)null) { throw new InvalidOperationException("No sell desk found."); } int groupCredits = GetGroupCredits(); bool broadcastReplies = ChatUtil.BroadcastReplies; ((MonoBehaviour)CoroutineRunner.Instance).StartCoroutine(SellInBatches(val, chosen, groupCredits, broadcastReplies)); } private static IEnumerator SellInBatches(DepositItemsDesk desk, List<ScrapItem> chosen, int creditsBefore, bool announce) { SaleDisplayAggregator.BatchedSaleInProgress = true; SaleDisplayAggregator.Reset(); List<SoldRecord> sold = new List<SoldRecord>(); int totalBatches = (chosen.Count + 12 - 1) / 12; if (totalBatches == 0) { totalBatches = 1; } int batchNum = 0; bool aborted = false; for (int i = 0; i < chosen.Count; i += 12) { batchNum++; int placed = 0; List<SoldRecord> batchRecords = new List<SoldRecord>(); for (int j = i; j < chosen.Count && j - i < 12; j++) { GrabbableObject val = chosen[j].Object; if (!((Object)(object)val == (Object)null)) { NetworkObject component = ((Component)val).GetComponent<NetworkObject>(); if ((Object)(object)component == (Object)null) { Plugin.Logger.LogWarning((object)("[Sell] " + ((Object)val).name + " has no NetworkObject, skipping.")); continue; } batchRecords.Add(new SoldRecord { Name = (((Object)(object)val.itemProperties != (Object)null) ? val.itemProperties.itemName : ((Object)val).name), Value = val.scrapValue, IsBody = ScrapScanner.IsBody(val) }); desk.AddObjectToDeskServerRpc(NetworkObjectReference.op_Implicit(component)); placed++; } } ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogInfo((object)$"[Sell] batch {batchNum}: placed {placed} item(s) on counter."); } if (placed == 0) { continue; } yield return null; desk.SellItemsOnServer(); float deadline = Time.realtimeSinceStartup + 10f; while (desk.itemsOnCounter.Count > 0 && Time.realtimeSinceStartup < deadline) { yield return (object)new WaitForSeconds(0.25f); } if (desk.itemsOnCounter.Count > 0) { Plugin.Logger.LogWarning((object)"[Sell] desk counter did not clear within 10s; aborting remaining batches."); if (sold.Count > 0) { FlushSaleDisplay(sold, creditsBefore); BroadcastSaleSummary(sold, creditsBefore, partial: true, announce); } ChatUtil.Send("Sale stalled — sold what fit; run the command again for the rest.", announce); aborted = true; break; } sold.AddRange(batchRecords); int num = batchRecords.Sum((SoldRecord r) => r.Value); ChatUtil.Send($"Sold batch {batchNum}/{totalBatches} — {placed} items, ${num}.", announce); } if (!aborted && sold.Count > 0) { FlushSaleDisplay(sold, creditsBefore); BroadcastSaleSummary(sold, creditsBefore, partial: false, announce); } float suppressDeadline = Time.realtimeSinceStartup + 15f; while (SaleDisplayAggregator.PendingDisplaySuppressions > 0 && Time.realtimeSinceStartup < suppressDeadline) { yield return (object)new WaitForSeconds(0.25f); } SaleDisplayAggregator.BatchedSaleInProgress = false; } private static void FlushSaleDisplay(List<SoldRecord> sold, int creditsBefore) { SaleDisplayAggregator.Flush(sold.Select((SoldRecord s) => s.ToDisplayLine()).ToList(), creditsBefore); } private static void BroadcastSaleSummary(List<SoldRecord> sold, int creditsBefore, bool partial, bool announce) { int num = sold.Sum((SoldRecord s) => s.Value); int count = sold.Count; string message = (partial ? $"SOLD (partial): {count} items for ${num} raw" : $"SOLD: {count} items for ${num} raw"); ChatUtil.Send(message, announce); List<SoldRecord> list = sold.Where((SoldRecord s) => s.IsBody).ToList(); if (list.Count > 0) { int num2 = list.Sum((SoldRecord s) => s.Value); ChatUtil.Send($"Bodies x{list.Count} (${num2})", announce); } var list2 = (from s in sold where !s.IsBody group s by s.Name into g select new { Name = g.Key, Count = g.Count(), Total = g.Sum((SoldRecord x) => x.Value) } into g orderby g.Total descending select g).ToList(); if (list2.Count > 0) { List<string> values = list2.Select(g => $"{g.Name} x{g.Count} (${g.Total})").ToList(); ChatUtil.SendChunked(string.Join(" · ", values), announce); } float num3 = (((Object)(object)StartOfRound.Instance != (Object)null) ? Mathf.Max(0f, StartOfRound.Instance.companyBuyingRate) : 1f); int num4 = Mathf.FloorToInt((float)num * num3); int groupCredits = GetGroupCredits(); int num5 = groupCredits - creditsBefore; int num6 = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.quotaFulfilled : 0); int num7 = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0); string text = ((num5 > 0) ? $"+${num5}" : $"+${num4} (est)"); if (num5 > 0 && num5 != num4) { ManualLogSource logger = Plugin.Logger; if (logger != null) { logger.LogInfo((object)$"[Sell] credits est={num4} actual={num5}"); } } ChatUtil.Send($"Quota: {num6}/{num7} · Credits: {text} ({Mathf.RoundToInt(num3 * 100f)}% rate)", announce); } private static int GetGroupCredits() { Terminal val = Object.FindObjectOfType<Terminal>(); if (!((Object)(object)val != (Object)null)) { return 0; } return val.groupCredits; } } internal static class ExclusionList { private static HashSet<string> _set; internal static IReadOnlyCollection<string> All => Load(); internal static int Count => Load().Count; private static HashSet<string> Load() { if (_set != null) { return _set; } _set = new HashSet<string>(from s in ModConfig.ExcludedItems.Value.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries) select s.Trim().ToLowerInvariant()); return _set; } internal static bool Contains(string itemName) { if (string.IsNullOrEmpty(itemName)) { return false; } return Load().Contains(itemName.ToLowerInvariant()); } internal static bool Add(string canonicalName) { bool flag = Load().Add(canonicalName.ToLowerInvariant()); if (flag) { Save(); } return flag; } internal static bool Remove(string canonicalName) { bool flag = Load().Remove(canonicalName.ToLowerInvariant()); if (flag) { Save(); } return flag; } internal static void Reload() { _set = null; Load(); } private static void Save() { ModConfig.ExcludedItems.Value = string.Join(",", from s in Load() orderby s select s); } } internal static class GearList { private static HashSet<string> _set; internal static IReadOnlyCollection<string> All => Load(); internal static int Count => Load().Count; private static HashSet<string> Load() { if (_set != null) { return _set; } _set = new HashSet<string>(from s in ModConfig.GearItems.Value.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries) select s.Trim().ToLowerInvariant()); return _set; } internal static bool Contains(string itemName) { if (string.IsNullOrEmpty(itemName)) { return false; } return Load().Contains(itemName.ToLowerInvariant()); } internal static bool Add(string canonicalName) { bool flag = Load().Add(canonicalName.ToLowerInvariant()); if (flag) { Save(); } return flag; } internal static bool Remove(string canonicalName) { bool flag = Load().Remove(canonicalName.ToLowerInvariant()); if (flag) { Save(); } return flag; } private static void Save() { ModConfig.GearItems.Value = string.Join(",", from s in Load() orderby s select s); } } internal static class ItemNameResolver { internal enum ResultKind { Ok, Ambiguous, Unknown, Empty } internal readonly struct ResolveResult { internal ResultKind Kind { get; } internal string Name { get; } internal IReadOnlyList<string> Candidates { get; } internal static ResolveResult Empty => new ResolveResult(ResultKind.Empty, null, null); internal ResolveResult(ResultKind kind, string name, IReadOnlyList<string> candidates) { Kind = kind; Name = name; Candidates = candidates; } internal static ResolveResult Ok(string name) { return new ResolveResult(ResultKind.Ok, name, null); } internal static ResolveResult Ambiguous(IReadOnlyList<string> candidates) { return new ResolveResult(ResultKind.Ambiguous, null, candidates); } internal static ResolveResult Unknown(string raw) { return new ResolveResult(ResultKind.Unknown, raw, null); } } private static readonly Dictionary<string, string> Aliases = new Dictionary<string, string> { { "shotgunshell", "Ammo" }, { "shotgunshells", "Ammo" }, { "shell", "Ammo" }, { "shells", "Ammo" }, { "shotgunammo", "Ammo" } }; internal static string JoinName(string[] parts, int startIndex) { if (parts == null || startIndex >= parts.Length) { return string.Empty; } string text = string.Join(" ", parts, startIndex, parts.Length - startIndex).Trim(); if (text.Length >= 2 && text[0] == '"' && text[text.Length - 1] == '"') { text = text.Substring(1, text.Length - 2).Trim(); } return text; } internal static string ApplyAlias(string query) { if (string.IsNullOrWhiteSpace(query)) { return query; } string norm = Normalize(query.Trim()); if (!Aliases.TryGetValue(norm, out var canonical)) { return query; } List<string> allItemNames = GetAllItemNames(); string singular = Singularize(norm); if (allItemNames.Any((string n) => Normalize(n).Contains(norm) || Normalize(n).Contains(singular))) { return query; } if (!allItemNames.Any((string n) => n.Equals(canonical, StringComparison.OrdinalIgnoreCase))) { return query; } return canonical; } internal static ResolveResult Resolve(string input) { string query = ApplyAlias(JoinName(new string[1] { input }, 0)); if (string.IsNullOrWhiteSpace(query)) { return ResolveResult.Empty; } List<string> allItemNames = GetAllItemNames(); string text = allItemNames.FirstOrDefault((string n) => n.Equals(query, StringComparison.OrdinalIgnoreCase)); if (text != null) { return ResolveResult.Ok(text); } string norm = Normalize(query); List<string> list = allItemNames.Where((string n) => Normalize(n).Contains(norm)).ToList(); if (list.Count == 0) { string singular = Singularize(norm); if (singular != nor