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 Dedicated Simulation v1.18.3
plugins/SarkasticGG_Dedicated_Simulation.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
#define TRACE using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using FeaturesLib; using HarmonyLib; using Mono.Cecil.Cil; using MonoMod.Cil; using PatchingLib; using PluginConfiguration; using Requirements; using SoftReferenceableAssets; using Steamworks; using Unity.Jobs.LowLevel.Unsafe; using UnityEngine; using Valheim_Serverside; using Valheim_Serverside.Features; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Sarkastic.gg Dedicated Simulation")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Sarkastic.gg Dedicated Simulation")] [assembly: AssemblyCopyright("Copyright © 2021")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("118598c3-bcc2-4a92-a60e-f77be92f1305")] [assembly: AssemblyFileVersion("1.5.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.5.0.0")] [module: UnverifiableCode] namespace PluginConfiguration { public class Configuration { public static ConfigEntry<bool> modEnabled; public static ConfigEntry<bool> maxObjectsPerFrameEnabled; public static ConfigEntry<int> maxObjectsPerFrame; public static ConfigEntry<bool> networkingEnabled; public static ConfigEntry<int> networkQueueSizeKB; public static ConfigEntry<int> networkSendRateMinKB; public static ConfigEntry<int> networkSendRateMaxKB; public static ConfigEntry<int> networkStatsMinutes; public static ConfigEntry<bool> consoleCommandsEnabled; public static ConfigEntry<int> unityJobWorkers; public static ConfigEntry<string> cpuAffinity; public static ConfigEntry<string> processPriority; public static ConfigEntry<int> sendIntervalMs; public static ConfigEntry<int> maxCatchUpMs; public static ConfigEntry<int> maxZonesPerTick; public static ConfigEntry<int> performanceStatsMinutes; public static ConfigEntry<int> serverTargetFps; public static ConfigEntry<int> maxGiveAmount; public static ConfigEntry<int> consoleInputCodePage; public static ConfigEntry<bool> fixSaveClientChanges; public static ConfigEntry<bool> fixDungeonLoadGuard; public static ConfigEntry<bool> compatVcpSpawnQueue; public static ConfigEntry<bool> compatVcpUnload; public static ConfigEntry<bool> fixTeleportGhosts; public static ConfigEntry<bool> fixServerSmoke; public static ConfigEntry<bool> characterGuardEnabled; public static ConfigEntry<CharacterGuard.NewCharacterPolicy> characterGuardNewCharacters; public static ConfigEntry<CharacterGuard.Action> characterGuardNewCharacterAction; public static ConfigEntry<CharacterGuard.Action> characterGuardChangedAway; public static ConfigEntry<bool> characterGuardExemptAdmins; public static ConfigEntry<string> characterGuardFreshEvent; public static ConfigEntry<string> characterGuardNewMessage; public static ConfigEntry<string> characterGuardChangedMessage; public static ConfigEntry<ItemLedger.Mode> itemLedgerMode; public static ConfigEntry<string> itemLedgerItems; public static ConfigEntry<string> itemLedgerExtra; public static ConfigEntry<string> itemLedgerExclude; public static ConfigEntry<bool> itemLedgerExemptAdmins; public static ConfigEntry<float> itemLedgerGraceHours; public static ConfigEntry<string> itemLedgerAltarMessage; public static ConfigEntry<bool> itemLedgerLogAll; public static ConfigEntry<string> itemLedgerMessage; public static ConfigEntry<bool> nightSpawnGuardEnabled; public static ConfigEntry<bool> portalLockEnabled; public static ConfigEntry<bool> portalLockExemptAdmins; public static ConfigEntry<string> portalLockPublicPrefix; public static ConfigEntry<float> portalLockExtraReach; public static ConfigEntry<string> portalLockMessage; public static ConfigEntry<string> portalLockPairingMessage; public static ConfigEntry<bool> guardianStonesEnabled; public static ConfigEntry<bool> welcomeEnabled; public static ConfigEntry<string> welcomeTitle; public static ConfigEntry<string> welcomeLines; public static ConfigEntry<bool> locationIconsEnabled; public static ConfigEntry<float> locationIconsDiscoverRadius; public static ConfigEntry<bool> guardianStonesExemptAdmins; public static ConfigEntry<string> guardianStonesMessage; public static ConfigEntry<bool> fireControlEnabled; public static ConfigEntry<float> fireControlMaxRadius; public static ConfigEntry<int> fireControlMaxSpread; public static ConfigEntry<bool> fireControlFireplaceIgnition; public static ConfigEntry<bool> fireControlOnlyProjectiles; public static void Load(ConfigFile config) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Expected O, but got Unknown //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Expected O, but got Unknown //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Expected O, but got Unknown //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Expected O, but got Unknown //IL_0226: Unknown result type (might be due to invalid IL or missing references) //IL_0230: Expected O, but got Unknown //IL_0257: Unknown result type (might be due to invalid IL or missing references) //IL_0261: Expected O, but got Unknown //IL_0288: Unknown result type (might be due to invalid IL or missing references) //IL_0292: Expected O, but got Unknown //IL_02b5: Unknown result type (might be due to invalid IL or missing references) //IL_02bf: Expected O, but got Unknown //IL_02e6: Unknown result type (might be due to invalid IL or missing references) //IL_02f0: Expected O, but got Unknown //IL_0537: Unknown result type (might be due to invalid IL or missing references) //IL_0541: Expected O, but got Unknown //IL_077d: Unknown result type (might be due to invalid IL or missing references) //IL_0787: Expected O, but got Unknown //IL_07aa: Unknown result type (might be due to invalid IL or missing references) //IL_07b4: Expected O, but got Unknown modEnabled = config.Bind<bool>("General", "Enabled", true, "Enable or disable the mod"); maxObjectsPerFrameEnabled = config.Bind<bool>("MaxObjectsPerFrame", "Enabled", true, "Enable or disable the feature"); maxObjectsPerFrame = config.Bind<int>("MaxObjectsPerFrame", "MaxObjects", 100, new ConfigDescription("Maximum number of objects the server can create per frame. A vanilla dedicated server creates 100 (the game's loading-screen rate: a server has no player of its own); lower values make areas fill in more slowly.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 10000), Array.Empty<object>())); networkingEnabled = config.Bind<bool>("Networking", "Enabled", true, "Raise the limits on how fast the server sends world data to each player (server-side part of BetterNetworking). Needs a restart."); networkQueueSizeKB = config.Bind<int>("Networking", "QueueSizeKB", 48, new ConfigDescription("Data queued per player before the server stops sending world updates for that tick. Valheim: 10. At 20 ticks/s, 48 KB is ~960 KB/s, just under SteamSendRateMaxKB; more needs a higher send rate too. A fuller queue delays new updates behind it. Above 80 Steam starts failing.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 80), Array.Empty<object>())); networkSendRateMinKB = config.Bind<int>("Networking", "SteamSendRateMinKB", 256, new ConfigDescription("Minimum rate Steam attempts to send to each player, KB/s. Valheim: 150. Keep it below the server's upload speed divided by the number of players.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(64, 4096), Array.Empty<object>())); networkSendRateMaxKB = config.Bind<int>("Networking", "SteamSendRateMaxKB", 1024, new ConfigDescription("Maximum rate Steam sends to each player, KB/s. Valheim: 150.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(64, 4096), Array.Empty<object>())); networkStatsMinutes = config.Bind<int>("Networking", "StatsIntervalMinutes", 5, "Every this many minutes, log per player how often their send queue was full. 0 disables."); consoleCommandsEnabled = config.Bind<bool>("Server", "ConsoleCommands", true, "Read commands from standard input: save, stop, players, give <item> <amount> <player>, broadcast <text>, event <name> <player>, events, help; anything else goes to the game's own console (kick, ban, unban, banned, stopevent, devcommands, skiptime ...). In AMP set App.HasWriteableConsole=True to type them into its console, and App.ExitMethod=String with App.ExitString=stop to shut down cleanly. On Windows also set [Logging.Console] Enabled = false in BepInEx.cfg, or BepInEx's own console takes over standard input."); maxGiveAmount = config.Bind<int>("Server", "MaxGiveAmount", 1000, "Most items one give command may drop."); consoleInputCodePage = config.Bind<int>("Server", "ConsoleInputCodePage", 1250, new ConfigDescription("How to read a console line that is not valid UTF-8 (UTF-8 is always tried first). Panels on Windows may write their code page instead: 1250 Central European (Czech), 852 Central European DOS, 1252 Western, 65001 UTF-8 only. The log says which bytes arrived the first time this is needed.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[4] { 1250, 852, 1252, 65001 }), Array.Empty<object>())); unityJobWorkers = config.Bind<int>("Server", "UnityJobWorkers", 8, "Upper limit on Unity job worker threads. Unity starts one per CPU core, and on many-core hosts the idle ones still use CPU. Only ever lowers the count. 0 leaves Unity's default."); cpuAffinity = config.Bind<string>("Server", "CpuAffinity", "", "Processors the server runs on. Empty: wherever the system puts it (as before). auto: one chiplet -- on AMD EPYC and Ryzen a die with its own L3 cache (and on the first EPYC generation its own memory) -- the one least busy over one second at startup, read from the system's own CPU topology, so the server's threads never move between dies. Or a list of processor numbers, e.g. 16-31 or 0-7,32-39. Processors above 63 are not used. Needs a restart."); processPriority = config.Bind<string>("Server", "ProcessPriority", "Normal", new ConfigDescription("Priority of the server process against everything else on the host: Normal (as before), AboveNormal or High. When the host is busy, a higher priority lets the server's frames go first. Needs a restart.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[3] { "Normal", "AboveNormal", "High" }), Array.Empty<object>())); sendIntervalMs = config.Bind<int>("Performance", "SendIntervalMs", 100, new ConfigDescription("How often each player is sent world updates, in real milliseconds. Valheim sends to one player per frame, so each player waits players+1 frames: at 15 FPS with 4 players ~330 ms. Every send builds that player's list of nearby objects, so shorter intervals cost server CPU. 0 keeps Valheim's behaviour.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 1000), Array.Empty<object>())); maxCatchUpMs = config.Bind<int>("Performance", "MaxCatchUpMs", 100, new ConfigDescription("Longest frame the server counts in full (Unity's maximum allowed timestep). After a slow frame Unity runs physics and every creature's fixed update again for each 20 ms it fell behind; Valheim allows 200 ms (10 steps), 100 caps it at 5, so one slow frame does not make the next one slow too. Game time runs slightly slower during such frames. Needs a restart. 0 keeps the game's setting.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 1000), Array.Empty<object>())); maxZonesPerTick = config.Bind<int>("Performance", "MaxZonesPerTick", 1, new ConfigDescription("Most new zones the server generates per zone tick (10 ticks a second), shared by all players in turn. A new zone is generated in full in one frame, so several players exploring at once used to cost one zone each in the same frame. 0 = one per player per tick, as before.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>())); serverTargetFps = config.Bind<int>("Performance", "ServerTargetFps", 60, new ConfigDescription("Frame rate the server aims for. The game sets a dedicated server to 30, so even with time to spare a frame waits 33 ms; every reaction to a player (a hit, a felled tree) waits for a server frame. 60 halves that wait whenever the server has the headroom, and costs nothing when it has not. Needs a restart. 0 keeps the game's 30.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 240), Array.Empty<object>())); performanceStatsMinutes = config.Bind<int>("Performance", "StatsIntervalMinutes", 5, "Every this many minutes, log frame times, physics steps per frame, the cost of sending world updates and of generating zones. 0 disables."); fixSaveClientChanges = config.Bind<bool>("Fixes", "SaveClientChanges", true, "Mark a world chunk as changed when a player's own change to an object arrives, so the next save writes it. Valheim 1.0 only rewrites changed chunks and does not count changes received from players, so what a player just built or moved could be missing after a restart."); fixDungeonLoadGuard = config.Bind<bool>("Fixes", "DungeonLoadGuard", true, "Keep a dungeon whose room bundle fails to load from wedging its zone for good. When Unity refuses a bundle, the game still reports the load as done and then throws, the dungeon never hears back and its zone stays flagged as loading. A vanilla dedicated server never loads dungeons, this mod does. The guard takes over a bundle Unity says is already loaded, reports a load that really failed as failed, and lets such a dungeon go so its zone keeps working and it is tried again next time."); compatVcpSpawnQueue = config.Bind<bool>("Compat", "ValheimCommunityPatchSpawnQueue", false, "Only matters with ValheimCommunityPatch installed. Its spawn queue replaces ZNetScene.CreateObjectsSorted and orders new objects by the server's reference position, which on a dedicated server is the world origin, so this mod's ordering by the nearest player never runs. Off: that patch of ValheimCommunityPatch is removed when the world starts. On: it is kept."); compatVcpUnload = config.Bind<bool>("Compat", "ValheimCommunityPatchUnload", false, "Only matters with ValheimCommunityPatch installed. Its zone-diff unload replaces ZNetScene.RemoveObjects and, whenever the object lists look untouched, drops everything outside the simulation distance of the server's reference position -- the world origin on a dedicated server. With players whose areas do not overlap, objects around them are then destroyed and created again on every pass (dungeons reloading dozens of times a second, doors, beds and items that cannot be used). Off: that patch of ValheimCommunityPatch is removed when the world starts. On: it is kept, and this mod marks its object lists as edited on every pass so that patch takes the game's own unload check."); characterGuardEnabled = config.Bind<bool>("CharacterGuard", "Enabled", false, "Check the characters players join with. A vanilla client keeps its character to itself, so the server cannot see or replace what a character carries; it sees what it wears and, from the raids it is ready for, how far it has come. A character this world has not seen must be fresh (NewCharacters), and a known one that comes back wearing something new or with other progress than it left with was played elsewhere (ChangedAway). Characters that built something or own a bed or tombstone here count as known. The list is kept next to the world save (<world>.characters.txt); `allow <name>` in the console lets a character in."); characterGuardNewCharacters = config.Bind<CharacterGuard.NewCharacterPolicy>("CharacterGuard", "NewCharacters", CharacterGuard.NewCharacterPolicy.RequireFresh, "RequireFresh: a character this world has not seen must still be ready for Eikthyr's raid (knows nothing past the first boss) and wear nothing beyond a level 1 workbench. Allow: every new character is let in and remembered."); characterGuardNewCharacterAction = config.Bind<CharacterGuard.Action>("CharacterGuard", "NewCharacterAction", CharacterGuard.Action.Kick, "What happens to a new character that is not fresh: Kick (after a message on their screen), Log, or Ignore."); characterGuardChangedAway = config.Bind<CharacterGuard.Action>("CharacterGuard", "ChangedAway", CharacterGuard.Action.Log, "What happens to a known character that comes back wearing something new, or with other progress than it had when it left: Log, Kick, or Ignore."); characterGuardExemptAdmins = config.Bind<bool>("CharacterGuard", "ExemptAdmins", true, "Admins (adminlist.txt) are never checked."); characterGuardFreshEvent = config.Bind<string>("CharacterGuard", "FreshEvent", "army_eikthyr", "The raid a fresh character is still ready for. The game stops a character being ready for army_eikthyr once it knows the antler, bronze or iron pickaxe, hard antler or Eikthyr's trophy."); characterGuardNewMessage = config.Bind<string>("CharacterGuard", "NewCharacterMessage", "This character brings progress from another world. Please join with a new character, or ask an admin.", "Shown in the middle of the screen before a new character that is not fresh is kicked."); characterGuardChangedMessage = config.Bind<string>("CharacterGuard", "ChangedAwayMessage", "This character was played on another world since it left here. Please ask an admin.", "Shown in the middle of the screen before a changed character is kicked (ChangedAway = Kick)."); itemLedgerMode = config.Bind<ItemLedger.Mode>("ItemLedger", "Mode", ItemLedger.Mode.Off, "Off, LogOnly or On. Keep an account per character of the items that lock progress (Items): what it got on this server -- picked up, taken out of containers -- and what it put back into the world -- dropped, put into containers, on stands, into smelters, kilns, cooking stations and fermenters, offered at boss altars, built with, died carrying -- and what it wears. More than it ever got here, and could have crafted from it, came from another world: that is written to <world>.guard.log next to the world save. With On it is also taken away (a dropped stack is cut down, a container or stand loses it once nobody uses it, a smelter drops it from its queue, a boss altar refuses to summon) and the player is told; what was built with or is worn is only logged. Only characters whose account is exact are acted on: a new, fresh character from the start, any other after its first death here, when the tombstone shows its whole bag."); itemLedgerItems = config.Bind<string>("ItemLedger", "Items", "auto", "Items to keep accounts of: auto, or item prefab names, comma separated. auto takes the items that lock progress, from the game's own data: what the game does not let through a portal (ores, metals, dragon eggs), what bosses drop and what summons them, every material that all recipes and pieces using it need more than a level 1 workbench for, and every item that all its recipes need more for. The list, by reason, is written to the guard log at startup. Items traders sell are always left out."); itemLedgerExtra = config.Bind<string>("ItemLedger", "ExtraItems", "", "More item prefab names to keep accounts of, comma separated."); itemLedgerExclude = config.Bind<string>("ItemLedger", "ExcludeItems", "", "Item prefab names never to keep accounts of, comma separated."); itemLedgerLogAll = config.Bind<bool>("ItemLedger", "LogAllMovements", false, "Also write every movement of a tracked item to the guard log, and every craft the ledger counts, not only what cannot be accounted for."); itemLedgerExemptAdmins = config.Bind<bool>("ItemLedger", "ExemptAdmins", true, "Admins (adminlist.txt) get no account and are never checked."); itemLedgerGraceHours = config.Bind<float>("ItemLedger", "GraceHours", 168f, new ConfigDescription("For this many real hours after the ledger first runs on a world, no character counts as new: the players who are already around come back with what they had, and their accounts start as unknown.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 8760f), Array.Empty<object>())); itemLedgerMessage = config.Bind<string>("ItemLedger", "Message", "{0} x {1} did not come from this world and was taken away.", "Shown in the middle of the player's screen when items are taken away (Mode = On). {0} is the amount, {1} the item."); itemLedgerAltarMessage = config.Bind<string>("ItemLedger", "AltarMessage", "{0} x {1} did not come from this world; the altar refuses them.", "Shown when a boss altar refuses items from another world (Mode = On). {0} is the amount, {1} the item."); nightSpawnGuardEnabled = config.Bind<bool>("NightSpawnGuard", "Enabled", false, "Hold back the ambient spawns that come with boss progress (Greydwarfs at night after Eikthyr, Draugr and Greydwarf Elites and Shamans after the Elder, Skeletons after Bonemass, Goblins after Yagluth, Seekers and Ticks after the Queen, Charred after Fader) around a group of players until every player in that spawn zone has got that far themselves, as their own client's raid list shows it. Until then the zone behaves as if the boss were still alive. In a group the least progressed player decides. Raids are not affected (-setkey playerevents decides those per player)."); welcomeEnabled = config.Bind<bool>("CharacterGuard", "Welcome", false, "Greet a character the first time it joins this world: WelcomeTitle in the middle of the screen, then each of WelcomeLines in the top left corner. A vanilla client shows it; nothing to install."); welcomeTitle = config.Bind<string>("CharacterGuard", "WelcomeTitle", "Welcome to Sarkastic.gg", "Shown in the middle of the screen a few seconds after a new character is let in. Empty = none."); welcomeLines = config.Bind<string>("CharacterGuard", "WelcomeLines", "Build a workbench to keep your area as it is: without one, a zone grows back as new after 72 h with nobody near.|A zone growing back never removes your buildings, chests or portals.|Fire spreads only from fire arrows, staffs, meteors and lava.|More at sarkastic.gg", "Shown one after another in the top left corner after WelcomeTitle. Separated by |."); locationIconsEnabled = config.Bind<bool>("LocationIcons", "Enabled", false, "Show a location's map icon (the traders Haldor, Hildir and the Bog Witch, Hildir's dungeons, the Deep North boss room, the ancient upgrade station) only to players who have been near it themselves. Vanilla sends every player the icon as soon as anyone's presence has generated the location's zone, and the map shows it whether the player has explored there or not. Found icons are remembered per character in <world>.icons.txt. The start temple's icon stays for everyone."); locationIconsDiscoverRadius = config.Bind<float>("LocationIcons", "DiscoverRadius", 50f, "How near (metres, horizontally) a player must have been to a location for its icon to appear on their map."); portalLockEnabled = config.Bind<bool>("PortalLock", "Enabled", false, "Let a portal be used only by those it belongs to. The server pairs portals by name alone, so anyone who builds a portal with the name of an unpaired one walks into that base; and nothing stops a player from stepping into any paired portal. Here a pair of portals is one thing: it may be used by the builder of either end, and by the owner and the players written on a ward that covers either end (one ward at one end decides for the pair; wards at both ends: a player has to be on both lists). A name that starts with PublicPrefix is open to everyone. Two portals are paired only if they have a person in common. A player who may not use a pair sees the portal unconnected. The ward's own list is used: its owner presses E on it as before. Existing pairs keep working for the builders of both ends. See the README."); portalLockExemptAdmins = config.Bind<bool>("PortalLock", "ExemptAdmins", true, "Admins (adminlist.txt) may use and rename every portal."); portalLockPublicPrefix = config.Bind<string>("PortalLock", "PublicPrefix", "*", "A portal whose name starts with this is open to everyone, as in the game: at a spawn crossroads, say. Inside someone else's ward it counts only if the ward's owner built it. Its name can still only be changed by those who manage it. Empty: no public portals."); portalLockExtraReach = config.Bind<float>("PortalLock", "ExtraReach", 0f, "Metres added to a ward's own radius (32 m for the game's ward, the circle seen around it) for deciding whether it covers a portal."); portalLockMessage = config.Bind<string>("PortalLock", "Message", "This portal is private. Ask its owner to add you to the ward next to it.", "Shown in the middle of the screen of a player who stands at a portal they may not use, or tries to rename one, at most once a quarter of an hour per portal. Empty: no message."); portalLockPairingMessage = config.Bind<string>("PortalLock", "PairingMessage", "No portal of this name can be linked to yours: its builder has to be on a ward next to your portal, or you on one next to theirs.", "Shown to the builder standing at an unconnected portal that has a partner of the same name it may not be paired with, at most once a quarter of an hour per portal. Empty: no message."); guardianStonesEnabled = config.Bind<bool>("GuardianStones", "Enabled", false, "Show a boss's trophy on its guardian stone, and so offer its power, only to players who have beaten that boss themselves, as their own client's raid list shows it (the same reading as the night spawn guard). To everyone else's game the stone is empty: a vanilla client cannot take a power it cannot see, and the server could not take one back. Players who may not use a stone cannot hang a trophy on it either."); guardianStonesExemptAdmins = config.Bind<bool>("GuardianStones", "ExemptAdmins", true, "Admins see every stone as it is."); guardianStonesMessage = config.Bind<string>("GuardianStones", "Message", "This power is yours once you have beaten {0} yourself.", "Shown in the middle of the screen of a player who stands at a stone holding a trophy they may not use, at most once a quarter of an hour per stone. {0} is the boss. Empty: no message."); fireControlEnabled = config.Bind<bool>("FireControl", "Enabled", false, "Limit how far fire spreads (the Fire world modifier, and the Ashlands, which always burn). See the README for how far each source reaches in the game."); fireControlMaxRadius = config.Bind<float>("FireControl", "MaxRadius", -1f, new ConfigDescription("A fire starts only within this many metres (straight line) of the source its chain began at: a lit fireplace, or where a fire arrow, fireball or meteor came down. Sparks still fly, but one that lands farther away starts nothing. -1: no limit in metres (vanilla).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-1f, 100f), Array.Empty<object>())); fireControlMaxSpread = config.Bind<int>("FireControl", "MaxSpread", -1, new ConfigDescription("How many further generations of sparks any fire may set off. The game gives a bonfire, hearth and iron fire pit 4, a campfire 2, braziers, torches and candles 1, fire arrows and fireballs 4. 0: nothing throws sparks except an arrow or fireball where it lands, and nothing that catches fire spreads it further. -1: as in the game.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(-1, 10), Array.Empty<object>())); fireControlFireplaceIgnition = config.Bind<bool>("FireControl", "FireplaceIgnition", true, "A lit hearth, bonfire, campfire, iron fire pit, brazier or standing torch sets alight whatever burnable touches its flame (vanilla). Off: no fireplace ever does -- a hearth, brazier or torch then cannot start a fire at all, a bonfire or campfire still throws sparks."); fireControlOnlyProjectiles = config.Bind<bool>("FireControl", "OnlyProjectiles", false, "Only projectiles start fires: a fire arrow, a Staff of Embers fireball, a meteor or a lava rock, and what they set alight burns and spreads as before (within MaxRadius and MaxSpread). Nothing a base has -- no fireplace, bonfire, campfire, hearth, brazier or torch, no fire they lit -- throws a spark or sets anything alight, so a player's own build never sets it on fire. Surtlings' and fuling shamans' fireballs never set anything alight in the game."); fixTeleportGhosts = config.Bind<bool>("Fixes", "TeleportGhosts", true, "Tell the players near the old spot to drop a player who teleported away. Valheim 1.0 checks whether an object left their area before it stores the new position, so the teleported player stayed there for them, frozen, until they next crossed a zone line."); fixServerSmoke = config.Bind<bool>("Fixes", "ServerSmoke", true, "Let fires and fireplaces make smoke on the server while a player is within 64 m, as they do on that player's computer in vanilla. A smoke source only puffs while the local player is that close, and a dedicated server has none, so the server never saw smoke: a fireplace in a room full of smoke, which every player sees go out, kept burning on the server -- using fuel, throwing sparks and setting what touches it alight -- and a fire under a roof never choked."); } } } namespace FeaturesLib { public interface IFeature { bool FeatureEnabled(); } public class AvailableFeatures { public readonly List<IFeature> _features; public AvailableFeatures() { _features = new List<IFeature>(); } public AvailableFeatures(List<IFeature> features) { _features = features; } public AvailableFeatures AddFeature(IFeature feature) { _features.Add(feature); return this; } public List<IFeature> EnabledFeatures() { return _features.Where((IFeature feature) => feature.FeatureEnabled()).ToList(); } public Type[] GetAllNestedTypes() { return (from feature in EnabledFeatures() select feature.GetType().GetNestedTypes() into list from item in list select item).ToArray(); } } } namespace Requirements { public class PatchRequirement { public class DebugBuild : IPatchRequirement { public const string name = "DebugBuild"; string IPatchRequirement.Name => "DebugBuild"; Func<bool> IPatchRequirement.Checker => Utilities.IsDebugBuild; } } } namespace PatchingLib { public interface IPatchRequirement { string Name { get; } Func<bool> Checker { get; } } public class PatchRequirements { private readonly Dictionary<string, Func<bool>> _requirements; public PatchRequirements() { _requirements = new Dictionary<string, Func<bool>>(); } public PatchRequirements(Dictionary<string, Func<bool>> requirements) { _requirements = requirements; } public PatchRequirements AddRequirement(IPatchRequirement patchRequirement) { _requirements.Add(patchRequirement.Name, patchRequirement.Checker); return this; } public bool IsAllowed(string requirement_name) { _requirements.TryGetValue(requirement_name, out var value); return value?.Invoke() ?? false; } } [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] public class PatchRequiresAttribute : Attribute { public readonly string requirement_name; public PatchRequiresAttribute(string requirement_name) { this.requirement_name = requirement_name; } } public class HarmonyFeaturesPatcher { private readonly PatchRequirements _patchRequirements; public HarmonyFeaturesPatcher(PatchRequirements availableFeatures) { _patchRequirements = availableFeatures; } public void PatchAll(Type[] types, Harmony harmony_instance) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) foreach (Type type in types) { if (!type.GetCustomAttributes<PatchRequiresAttribute>().ToList().Any((PatchRequiresAttribute attribute) => !_patchRequirements.IsAllowed(attribute.requirement_name))) { ZLog.Log((object)("Patching: " + type.ToString())); new PatchClassProcessor(harmony_instance, type).Patch(); } else { ZLog.Log((object)("Patch disabled: " + type.ToString())); } } } } } namespace Valheim_Serverside { public class Utilities { public static bool IsDebugBuild() { return false; } } public static class AdminCommands { private static Dictionary<string, GameObject> s_items; public static ZNetPeer FindPlayer(string name, out string problem) { problem = null; List<ZNetPeer> list = (from p in ZNet.instance.GetPeers() where p.IsReady() && !string.IsNullOrEmpty(p.m_playerName) select p).ToList(); ZNetPeer val = ((IEnumerable<ZNetPeer>)list).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => string.Equals(p.m_playerName, name, StringComparison.OrdinalIgnoreCase))); if (val != null) { return val; } List<ZNetPeer> list2 = list.Where((ZNetPeer p) => p.m_playerName.StartsWith(name, StringComparison.OrdinalIgnoreCase)).ToList(); List<ZNetPeer> list3 = ((list2.Count > 0) ? list2 : list.Where((ZNetPeer p) => p.m_playerName.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0).ToList()); if (list3.Count == 1) { return list3[0]; } problem = ((list3.Count == 0) ? ("No player '" + name + "' online. Online: " + Names(list)) : ("'" + name + "' matches several players: " + Names(list3))); return null; } public static string Players() { List<ZNetPeer> list = (from p in ZNet.instance.GetPeers() where p.IsReady() select p).ToList(); if (list.Count == 0) { return "No players online"; } return string.Join("; ", list.Select(delegate(ZNetPeer p) { //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_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) Vector3 refPos = p.GetRefPos(); return $"{p.m_playerName} ({p.m_socket.GetHostName()}) at {refPos.x:0},{refPos.z:0}"; })); } public static string Give(ZNetPeer target, string itemName, int amount, string byWhom) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) GameObject val = FindItem(itemName); if (!Object.op_Implicit((Object)(object)val)) { return "No item '" + itemName + "'." + Suggest(itemName); } amount = Mathf.Clamp(amount, 1, Mathf.Max(1, Configuration.maxGiveAmount.Value)); ZDO zDO = ZDOMan.instance.GetZDO(target.m_characterID); Vector3 val2 = ((zDO != null) ? zDO.GetPosition() : target.GetRefPos()) + Vector3.up * 1.5f; int num = Mathf.Max(1, val.GetComponent<ItemDrop>().m_itemData.m_shared.m_maxStackSize); int num2 = 0; for (int num3 = amount; num3 > 0; num3 -= num) { Vector2 val3 = Random.insideUnitCircle * 0.75f; Vector3 val4 = val2 + new Vector3(val3.x, 0.25f * (float)num2, val3.y); Object.Instantiate<GameObject>(val, val4, Quaternion.identity).GetComponent<ItemDrop>().SetStack(Mathf.Min(num3, num)); num2++; } string text = $"{byWhom} gave {amount} x {((Object)val).name} to {target.m_playerName} in {num2} stack(s) at {val2.x:0},{val2.z:0}"; ServersidePlugin.logger.LogInfo((object)("Admin: " + text)); return text; } public static string Save(string byWhom) { if (!Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer()) { return "No world loaded, nothing to save"; } bool flag = default(bool); if (!ZNet.instance.EnoughDiskSpaceAvailable(ref flag, false, (Action<bool>)null)) { return "Not enough disk space, world not saved"; } ServersidePlugin.logger.LogInfo((object)("Admin: " + byWhom + " requested a world save")); ZNet.instance.Save(false, true, true); return "Saving world"; } public static string Broadcast(string text, string byWhom) { ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ShowMessage", new object[2] { 2, text }); ServersidePlugin.logger.LogInfo((object)("Admin: " + byWhom + " broadcast: " + text)); return $"shown to {ZNet.instance.GetNrOfPlayers()} player(s): {text}"; } public static string StartEvent(string name, ZNetPeer target, string byWhom) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0058: 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_008a: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)RandEventSystem.instance) || !RandEventSystem.instance.HaveEvent(name)) { return "No event '" + name + "'. Events: " + Events(); } ZDO zDO = ZDOMan.instance.GetZDO(target.m_characterID); Vector3 val = ((zDO != null) ? zDO.GetPosition() : target.GetRefPos()); RandEventSystem.instance.SetRandomEventByName(name, val); string text = $"{byWhom} started event {name} at {target.m_playerName} ({val.x:0},{val.z:0})"; ServersidePlugin.logger.LogInfo((object)("Admin: " + text)); return text; } public static string Events() { if (!Object.op_Implicit((Object)(object)RandEventSystem.instance)) { return "none (no world loaded)"; } return string.Join(", ", RandEventSystem.instance.m_events.Select((RandomEvent e) => e.m_name)); } public static bool TryParseAmount(string text, out int amount) { return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out amount); } private static string Names(List<ZNetPeer> peers) { if (peers.Count != 0) { return string.Join(", ", peers.Select((ZNetPeer p) => p.m_playerName)); } return "nobody"; } private static GameObject FindItem(string name) { if (s_items == null) { s_items = new Dictionary<string, GameObject>(StringComparer.OrdinalIgnoreCase); foreach (GameObject prefab in ZNetScene.instance.m_prefabs) { if (Object.op_Implicit((Object)(object)prefab) && Object.op_Implicit((Object)(object)prefab.GetComponent<ItemDrop>()) && !s_items.ContainsKey(((Object)prefab).name)) { s_items[((Object)prefab).name] = prefab; } } } if (!s_items.TryGetValue(name, out var value)) { return null; } return value; } private static string Suggest(string name) { List<string> list = new List<string>(); foreach (string key in s_items.Keys) { if (key.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0) { list.Add(key); if (list.Count == 8) { break; } } } if (list.Count <= 0) { return ""; } return " Similar: " + string.Join(", ", list); } } public static class ServerSafe { private static readonly Dictionary<string, MethodInfo> Replacements = new Dictionary<string, MethodInfo> { { "GetZDOID", AccessTools.Method(typeof(ServerSafe), "LocalPlayerZDOID", (Type[])null, (Type[])null) }, { "GetPlayerID", AccessTools.Method(typeof(ServerSafe), "LocalPlayerID", (Type[])null, (Type[])null) }, { "GetPlayerName", AccessTools.Method(typeof(ServerSafe), "LocalPlayerName", (Type[])null, (Type[])null) } }; public static ZDOID LocalPlayerZDOID() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)Player.m_localPlayer)) { return ZDOID.None; } return ((Character)Player.m_localPlayer).GetZDOID(); } public static long LocalPlayerID() { if (!Object.op_Implicit((Object)(object)Player.m_localPlayer)) { return 0L; } return Player.m_localPlayer.GetPlayerID(); } public static string LocalPlayerName() { if (!Object.op_Implicit((Object)(object)Player.m_localPlayer)) { return ""; } return Player.m_localPlayer.GetPlayerName(); } public static IEnumerable<CodeInstruction> NullSafeLocalPlayer(IEnumerable<CodeInstruction> instructions, MethodBase original, int expected) { //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Expected O, but got Unknown FieldInfo fieldInfo = AccessTools.Field(typeof(Player), "m_localPlayer"); List<CodeInstruction> list = instructions.ToList(); int num = 0; for (int i = 0; i < list.Count - 1; i++) { if (CodeInstructionExtensions.LoadsField(list[i], fieldInfo, false) && !(list[i + 1].opcode != OpCodes.Callvirt) && list[i + 1].operand is MethodInfo methodInfo && methodInfo.DeclaringType.IsAssignableFrom(typeof(Player)) && Replacements.TryGetValue(methodInfo.Name, out var value)) { CodeInstruction val = new CodeInstruction(OpCodes.Call, (object)value); val.labels.AddRange(list[i].labels); val.labels.AddRange(list[i + 1].labels); val.blocks.AddRange(list[i].blocks); val.blocks.AddRange(list[i + 1].blocks); list[i] = val; list.RemoveAt(i + 1); num++; } } if (num != expected) { ServersidePlugin.logger.LogWarning((object)$"{original.DeclaringType.Name}.{original.Name}: replaced {num} Player.m_localPlayer call(s), expected {expected}. The game changed this method; the patch needs reviewing."); } return list; } } public static class ServerConsole { private const string Commands = "save | stop | players | give <item> <amount> <player> | broadcast <text> | event <name> <player> | events | characters | allow <character> | help -- anything else goes to the game console: kick, ban, unban, banned, stopevent, devcommands, skiptime ..."; private static readonly ConcurrentQueue<string> s_commands = new ConcurrentQueue<string>(); private static bool s_capturing; private static int s_captured; private static Harmony s_capture; public static void Start() { Thread thread = new Thread(ReadLoop); thread.IsBackground = true; thread.Name = "Dedicated Simulation console"; thread.Start(); ServersidePlugin.logger.LogInfo((object)"Console commands enabled on standard input: save | stop | players | give <item> <amount> <player> | broadcast <text> | event <name> <player> | events | characters | allow <character> | help -- anything else goes to the game console: kick, ban, unban, banned, stopevent, devcommands, skiptime ..."); } private static void ReadLoop() { try { ConsoleInput.ReadLines(() => Configuration.consoleInputCodePage.Value, delegate(string line, string notice) { if (notice != null) { s_commands.Enqueue("\u0001" + notice); } line = line.Trim(); if (line.Length > 0) { s_commands.Enqueue(line); } }); } catch (Exception ex) { s_commands.Enqueue("\0" + ex.Message); } } private static void Reply(string text) { ServersidePlugin.logger.LogInfo((object)("Console: " + text)); WriteOut(text); } private static void WriteOut(string text) { try { Console.Out.WriteLine("Console: " + text); Console.Out.Flush(); } catch (Exception) { } } public static void ProcessPending() { string result; while (s_commands.TryDequeue(out result)) { Execute(result); } } private static void Execute(string command) { if (command[0] == '\0') { ServersidePlugin.logger.LogWarning((object)("Console commands unavailable, cannot read standard input: " + command.Substring(1))); return; } if (command[0] == '\u0001') { ServersidePlugin.logger.LogWarning((object)command.Substring(1)); WriteOut(command.Substring(1)); return; } string[] array = command.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); bool flag = Object.op_Implicit((Object)(object)ZNet.instance) && ZNet.instance.IsServer(); try { switch (array[0].ToLowerInvariant()) { case "save": Reply(AdminCommands.Save("console")); break; case "stop": case "quit": case "shutdown": Reply("saving world and shutting down"); Application.Quit(); break; case "players": Reply(flag ? AdminCommands.Players() : "no world loaded"); break; case "give": { if (!flag) { Reply("no world loaded"); break; } if (array.Length < 4 || !AdminCommands.TryParseAmount(array[2], out var amount)) { Reply("usage: give <item> <amount> <player>, e.g. give Copper 120 Ulf"); break; } string problem; ZNetPeer val = AdminCommands.FindPlayer(string.Join(" ", array, 3, array.Length - 3), out problem); Reply((val == null) ? problem : AdminCommands.Give(val, array[1], amount, "console")); break; } case "broadcast": case "say": if (!flag) { Reply("no world loaded"); } else if (array.Length < 2) { Reply("usage: broadcast <text>"); } else { Reply(AdminCommands.Broadcast(command.Substring(array[0].Length).Trim(), "console")); } break; case "event": { if (!flag) { Reply("no world loaded"); break; } if (array.Length < 3) { Reply("usage: event <name> <player>, e.g. event army_eikthyr Ulf; `events` lists the names"); break; } string problem2; ZNetPeer val2 = AdminCommands.FindPlayer(string.Join(" ", array, 2, array.Length - 2), out problem2); Reply((val2 == null) ? problem2 : AdminCommands.StartEvent(array[1], val2, "console")); break; } case "events": Reply(flag ? ("events: " + AdminCommands.Events()) : "no world loaded"); break; case "characters": Reply(flag ? CharacterGuard.Describe() : "no world loaded"); break; case "allow": Reply((!flag) ? "no world loaded" : ((array.Length < 2) ? "usage: allow <character>" : CharacterGuard.Allow(string.Join(" ", array, 1, array.Length - 1)))); break; case "help": Reply("commands: save | stop | players | give <item> <amount> <player> | broadcast <text> | event <name> <player> | events | characters | allow <character> | help -- anything else goes to the game console: kick, ban, unban, banned, stopevent, devcommands, skiptime ..."); break; default: RunGameCommand(command, array[0]); break; } } catch (Exception ex) { ServersidePlugin.logger.LogWarning((object)$"Console: '{command}' failed: {ex}"); Reply("'" + command + "' failed: " + ex.Message); } } private static void RunGameCommand(string command, string word) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Expected O, but got Unknown if (!Object.op_Implicit((Object)(object)Console.instance)) { Reply("'" + word + "' is not one of ours and the game console is not up yet. Commands: save | stop | players | give <item> <amount> <player> | broadcast <text> | event <name> <player> | events | characters | allow <character> | help -- anything else goes to the game console: kick, ban, unban, banned, stopevent, devcommands, skiptime ..."); return; } if (s_capture == null) { s_capture = new Harmony("MVP.Valheim_Serverside_Simulations.ServerConsole"); s_capture.Patch((MethodBase)AccessTools.Method(typeof(Terminal), "AddString", new Type[1] { typeof(string) }, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(ServerConsole), "AddStringPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } s_capturing = true; s_captured = 0; try { ((Terminal)Console.instance).TryRunCommand(command, false, true); } finally { s_capturing = false; } if (s_captured == 0) { Reply("game console ran '" + word + "' without output"); } } public static void AddStringPostfix(Terminal __instance, string text) { if (s_capturing && (Object)(object)__instance == (Object)(object)Console.instance) { s_captured++; WriteOut(text); } } } public static class ConsoleInput { public delegate void LineHandler(string line, string notice); private static readonly UTF8Encoding s_utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private const string Cp1250 = "€\ufffd‚\ufffd„…†‡\ufffd‰Š‹ŚŤŽŹ\ufffd‘’“”•–—\ufffd™š›śťžź\u00a0ˇ\u02d8٤Ą¦§\u00a8©Ş«¬\u00ad®Ż°±\u02dbł\u00b4µ¶·\u00b8ąş»Ľ\u02ddľżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ\u02d9"; private const string Cp852 = "ÇüéâäůćçłëŐőîŹÄĆÉĹĺôöĽľŚśÖÜŤťŁ×čáíóúĄąŽžĘ꬟Ⱥ«»░▒▓│┤ÁÂĚŞ╣║╗╝Żż┐└┴┬├─┼Ăă╚╔╩╦╠═╬¤đĐĎËďŇÍÎě┘┌█▄ŢŮ▀ÓßÔŃńňŠšŔÚŕŰýÝţ\u00b4\u00ad\u02dd\u02dbˇ\u02d8§÷\u00b8°\u00a8\u02d9űŘř■\u00a0"; public static void ReadLines(Func<int> codePage, LineHandler handler) { bool noticed = false; using Stream stream = Console.OpenStandardInput(); byte[] array = new byte[4096]; List<byte> list = new List<byte>(); int num; while ((num = stream.Read(array, 0, array.Length)) > 0) { for (int i = 0; i < num; i++) { if (array[i] == 10) { Emit(list, codePage(), handler, ref noticed); list.Clear(); } else { list.Add(array[i]); } } } Emit(list, codePage(), handler, ref noticed); } private static void Emit(List<byte> line, int codePage, LineHandler handler, ref bool noticed) { if (line.Count > 0 && line[line.Count - 1] == 13) { line.RemoveAt(line.Count - 1); } if (line.Count != 0) { byte[] array = line.ToArray(); string notice = null; bool fellBack; string line2 = Decode(array, codePage, out fellBack); if (fellBack && !noticed) { noticed = true; notice = string.Format("Console input is not UTF-8 (bytes: {0}); decoded with code page {1}.", BitConverter.ToString(array).Replace("-", " "), codePage) + " If accents come out wrong, set [Server] ConsoleInputCodePage to what the panel sends (1250, 852, 1252 or 65001)."; } handler(line2, notice); } } public static string Decode(byte[] data, int codePage, out bool fellBack) { fellBack = false; bool flag = true; for (int i = 0; i < data.Length; i++) { if (data[i] >= 128) { flag = false; break; } } if (flag) { return Encoding.ASCII.GetString(data); } if (codePage != 65001) { try { return s_utf8.GetString(data); } catch (DecoderFallbackException) { fellBack = true; } } return SingleByte(data, codePage); } private static string SingleByte(byte[] data, int codePage) { string text = codePage switch { 1250 => "€\ufffd‚\ufffd„…†‡\ufffd‰Š‹ŚŤŽŹ\ufffd‘’“”•–—\ufffd™š›śťžź\u00a0ˇ\u02d8٤Ą¦§\u00a8©Ş«¬\u00ad®Ż°±\u02dbł\u00b4µ¶·\u00b8ąş»Ľ\u02ddľżŔÁÂĂÄĹĆÇČÉĘËĚÍÎĎĐŃŇÓÔŐÖ×ŘŮÚŰÜÝŢßŕáâăäĺćçčéęëěíîďđńňóôőö÷řůúűüýţ\u02d9", 852 => "ÇüéâäůćçłëŐőîŹÄĆÉĹĺôöĽľŚśÖÜŤťŁ×čáíóúĄąŽžĘ꬟Ⱥ«»░▒▓│┤ÁÂĚŞ╣║╗╝Żż┐└┴┬├─┼Ăă╚╔╩╦╠═╬¤đĐĎËďŇÍÎě┘┌█▄ŢŮ▀ÓßÔŃńňŠšŔÚŕŰýÝţ\u00b4\u00ad\u02dd\u02dbˇ\u02d8§÷\u00b8°\u00a8\u02d9űŘř■\u00a0", _ => null, }; char[] array = new char[data.Length]; for (int i = 0; i < data.Length; i++) { byte b = data[i]; array[i] = (char)((b < 128) ? b : (((int?)text?[b - 128]) ?? ((int)b))); } return new string(array); } } public static class VanillaDrift { public const string ReviewedForGameVersion = "1.0.15"; private static readonly Dictionary<MethodBase, string[]> Expected = new Dictionary<MethodBase, string[]> { { AccessTools.Method(typeof(ZoneSystem), "Update", (Type[])null, (Type[])null), new string[15] { "CinematicsManager::IsPlaying", "TextViewer::IsShowingIntro", "Utils::GetTimeBudgetForTargetFrameRate", "ZNet::GetConnectionStatus", "ZNet::GetPeers", "ZNet::GetReferencePosition", "ZNet::IsServer", "ZNet::get_instance", "ZNetPeer::GetRefPos", "ZoneSystem::CreateGhostZones", "ZoneSystem::CreateLocalZones", "ZoneSystem::TimeSinceStart", "ZoneSystem::UpdatePrefabLifetimes", "ZoneSystem::UpdateTTL", "ZoneSystem::get_LocationsGenerated" } }, { AccessTools.Method(typeof(ZoneSystem), "IsActiveAreaLoaded", (Type[])null, (Type[])null), new string[7] { "SimulationDistance::get_IsClassic", "SimulationDistance::get_NearSimulationDistance", "Vector2s::.ctor", "ZNet::GetReferencePosition", "ZNet::get_instance", "ZoneSystem::GetZone", "ZoneSystem::ZonesWithinRadius" } }, { AccessTools.Method(typeof(ZNetScene), "CreateDestroyObjects", (Type[])null, (Type[])null), new string[8] { "ZDOMan::FindSectorObjects", "ZDOMan::get_instance", "ZNet::GetReferencePosition", "ZNet::GetSyncedSimulationDistance", "ZNet::get_instance", "ZNetScene::CreateObjects", "ZNetScene::RemoveObjects", "ZoneSystem::GetZone" } }, { AccessTools.Method(typeof(ZDOMan), "ReleaseNearbyZDOS", (Type[])null, (Type[])null), new string[14] { "SimulationDistance::.ctor", "SimulationDistance::get_IsClassic", "SimulationDistance::get_NearSimulationDistance", "ZDO::GetOwner", "ZDO::GetPosition", "ZDO::HasOwner", "ZDO::SetOwner", "ZDO::get_Persistent", "ZDOMan::FindSectorObjects", "ZDOMan::IsInPeerActiveArea", "ZNet::GetSyncedSimulationDistance", "ZNet::get_instance", "ZNetScene::InActiveArea", "ZoneSystem::GetZone" } }, { AccessTools.Method(typeof(ZDOMan), "SendZDOToPeers2", (Type[])null, (Type[])null), new string[1] { "ZDOMan::SendZDOs" } }, { AccessTools.Method(typeof(ZNetScene), "OutsideActiveArea", new Type[1] { typeof(Vector3) }, (Type[])null), new string[4] { "ZNet::GetReferencePosition", "ZNet::get_instance", "ZNetScene::OutsideActiveArea", "ZoneSystem::GetZone" } }, { AccessTools.Method(typeof(Ship), "UpdateOwner", (Type[])null, (Type[])null), new string[8] { "Ship::GetNewOwnerID", "Ship::IsPlayerInBoat", "Ship::RefreshPlayerList", "ZDO::SetOwner", "ZLog::Log", "ZNetView::GetZDO", "ZNetView::IsOwner", "ZNetView::IsValid" } } }; public static void Check(ManualLogSource logger) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) string text = ((object)Version.CurrentVersion/*cast due to .constrained prefix*/).ToString(); int num = 0; foreach (KeyValuePair<MethodBase, string[]> item in Expected) { if (item.Key == null) { logger.LogError((object)"Vanilla drift check: a replaced method no longer exists in this game version."); num++; continue; } HashSet<string> hashSet = GameCalls(item.Key); List<string> list = (from s in hashSet.Except(item.Value) orderby s select s).ToList(); List<string> list2 = (from s in item.Value.Except(hashSet) orderby s select s).ToList(); if (list.Count != 0 || list2.Count != 0) { num++; logger.LogWarning((object)("Vanilla " + item.Key.DeclaringType.Name + "." + item.Key.Name + " changed since this mod's replacement was reviewed (game 1.0.15, running " + text + "). Now calls: [" + string.Join(", ", list) + "]. No longer calls: [" + string.Join(", ", list2) + "]. The replacement may be dropping new vanilla behaviour.")); } } if (num == 0) { logger.LogInfo((object)string.Format("Vanilla drift check passed ({0} replaced methods, reviewed for {1}, running {2}).", Expected.Count, "1.0.15", text)); } } private static HashSet<string> GameCalls(MethodBase method) { HashSet<string> hashSet = new HashSet<string>(); foreach (CodeInstruction originalInstruction in PatchProcessor.GetOriginalInstructions(method, (ILGenerator)null)) { if ((originalInstruction.opcode == OpCodes.Call || originalInstruction.opcode == OpCodes.Callvirt || originalInstruction.opcode == OpCodes.Newobj) && originalInstruction.operand is MethodBase methodBase && methodBase.DeclaringType != null) { string name = methodBase.DeclaringType.Assembly.GetName().Name; if (name == "assembly_valheim" || name == "assembly_utils") { hashSet.Add(methodBase.DeclaringType.Name + "::" + methodBase.Name); } } } return hashSet; } } public static class ProcessPlacement { private static readonly bool s_windows = Environment.OSVersion.Platform == PlatformID.Win32NT; private const int RelationNumaNode = 1; private const int RelationCache = 2; private const int RelationProcessorDie = 5; public static void Apply(string affinity, string priority, Action<string> log) { affinity = (affinity ?? "").Trim(); priority = (priority ?? "").Trim(); if (priority.Length > 0 && !priority.Equals("Normal", StringComparison.OrdinalIgnoreCase)) { try { log(SetPriority(priority)); } catch (Exception ex) { log("Process priority failed, left as it was: " + ex.Message); } } if (affinity.Length == 0) { return; } if (affinity.Equals("auto", StringComparison.OrdinalIgnoreCase)) { Thread thread = new Thread((ThreadStart)delegate { try { log(PlaceOnChiplet(out var placed)); KeepPlaced(placed, log); } catch (Exception ex3) { log("Processor placement failed, left as it was: " + ex3.Message); } }); thread.IsBackground = true; thread.Name = "ProcessPlacement"; thread.Start(); return; } try { ulong mask = ParseList(affinity, strict: true); ulong num = AllProcessors(); if (num != 0L && (mask & ~num) != 0L) { log("Processor placement: '" + affinity + "' names processors this system does not have (" + Describe(num) + "); left as it was."); return; } bool flag = SetAffinity(mask); log(flag ? ("Processor placement: processors " + Describe(mask) + ", as configured.") : "Processor placement: the system refused the affinity; left as it was."); if (flag && !s_windows) { Thread thread2 = new Thread((ThreadStart)delegate { KeepPlaced(mask, log); }); thread2.IsBackground = true; thread2.Name = "ProcessPlacement"; thread2.Start(); } } catch (Exception ex2) { log("Processor placement: '" + affinity + "' cannot be used (" + ex2.Message + "); left as it was."); } } private static void KeepPlaced(ulong mask, Action<string> log) { if (s_windows || mask == 0L) { return; } int[] array = new int[2] { 30, 270 }; for (int i = 0; i < array.Length; i++) { Thread.Sleep(array[i] * 1000); int num = 0; ulong[] array2 = new ulong[16]; foreach (int item in EveryThread()) { Array.Clear(array2, 0, array2.Length); if (sched_getaffinity(item, new IntPtr(8 * array2.Length), array2) == 0 && (array2[0] & ~mask) != 0L && sched_setaffinity(item, new IntPtr(8), new ulong[1] { mask }) == 0) { num++; } } if (num > 0) { log($"Processor placement: {num} thread(s) started since with processors of their own, moved back to {Describe(mask)}."); } } } private static string PlaceOnChiplet(out ulong placed) { placed = 0uL; string source; List<ulong> list = Chiplets(out source); ulong allowed = Allowed(); if (list != null && allowed != 0L) { list = (from c in list select c & allowed into c where c != 0 select c).Distinct().ToList(); } if (list == null || list.Count < 2) { return "Processor placement: the system reports " + ((list == null) ? "no" : "one") + " chiplet (" + source + ((allowed != 0L && allowed != AllProcessors()) ? (", of processors " + Describe(allowed) + " it may use") : "") + "); left as it was."; } double[] array = Busy(1000); if (array == null) { return "Processor placement: the system does not say how busy its processors are; left as it was."; } int num = -1; double num2 = double.MaxValue; for (int num3 = 0; num3 < list.Count; num3++) { double num4 = Average(array, list[num3]); if (num4 <= num2) { num2 = num4; num = num3; } } ulong num5 = list[num]; if (!SetAffinity(num5)) { return "Processor placement: the system refused the affinity; left as it was."; } placed = num5; return $"Processor placement: chiplet {num + 1} of {list.Count} ({source}), processors {Describe(num5)}, {100.0 * num2:0}% busy over 1 s, the least of {list.Count}."; } private static double Average(double[] busy, ulong mask) { double num = 0.0; int num2 = 0; for (int i = 0; i < 64 && i < busy.Length; i++) { if ((mask & (ulong)(1L << i)) != 0L) { num += busy[i]; num2++; } } if (num2 <= 0) { return 1.0; } return num / (double)num2; } private static List<ulong> Chiplets(out string source) { List<ulong> list; if (s_windows) { list = WindowsGroups(5, 0); if (Several(list)) { source = "processor dies"; return list; } list = WindowsGroups(1, 0); if (Several(list)) { source = "NUMA nodes"; return list; } list = WindowsGroups(2, 3); source = "processors sharing an L3 cache"; return list; } list = LinuxDies(); if (Several(list)) { source = "processor dies"; return list; } list = LinuxList("/sys/devices/system/node", "node", "cpulist"); if (Several(list)) { source = "NUMA nodes"; return list; } list = LinuxL3(); source = "processors sharing an L3 cache"; return list; } private static bool Several(List<ulong> groups) { if (groups != null) { return groups.Distinct().Count() > 1; } return false; } [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetLogicalProcessorInformationEx(int relationship, IntPtr buffer, ref uint returnedLength); private static List<ulong> WindowsGroups(int relationship, int cacheLevel) { uint returnedLength = 0u; GetLogicalProcessorInformationEx(relationship, IntPtr.Zero, ref returnedLength); if (returnedLength == 0) { return null; } IntPtr intPtr = Marshal.AllocHGlobal((int)returnedLength); try { if (!GetLogicalProcessorInformationEx(relationship, intPtr, ref returnedLength)) { return null; } List<ulong> list = new List<ulong>(); int num2; for (int i = 0; i + 8 <= returnedLength; i += num2) { IntPtr ptr = IntPtr.Add(intPtr, i); int num = Marshal.ReadInt32(ptr, 0); num2 = Marshal.ReadInt32(ptr, 4); if (num2 <= 0) { break; } if (num != relationship || (num == 2 && Marshal.ReadByte(ptr, 8) != cacheLevel)) { continue; } int num3 = ((num == 2) ? 38 : 30); int num4 = Math.Max(1, (int)Marshal.ReadInt16(ptr, num3)); ulong num5 = 0uL; for (int j = 0; j < num4; j++) { int num6 = num3 + 2 + 16 * j; if (Marshal.ReadInt16(ptr, num6 + 8) == 0) { num5 |= (ulong)Marshal.ReadInt64(ptr, num6); } } if (num5 != 0L) { list.Add(num5); } } return list; } finally { Marshal.FreeHGlobal(intPtr); } } private static List<ulong> LinuxDies() { Dictionary<string, ulong> dictionary = new Dictionary<string, ulong>(); foreach (int item in LinuxCpus()) { string text = $"/sys/devices/system/cpu/cpu{item}/topology/"; if (!File.Exists(text + "die_id")) { return null; } string text2 = File.ReadAllText(text + "die_id").Trim(); string key = File.ReadAllText(text + "physical_package_id").Trim() + "/" + text2; dictionary[key] = (dictionary.TryGetValue(key, out var value) ? value : 0) | (ulong)((item < 64) ? (1L << item) : 0); } return dictionary.Values.ToList(); } private static List<ulong> LinuxL3() { HashSet<ulong> hashSet = new HashSet<ulong>(); foreach (int item in LinuxCpus()) { string path = $"/sys/devices/system/cpu/cpu{item}/cache"; if (!Directory.Exists(path)) { continue; } string[] directories = Directory.GetDirectories(path, "index*"); foreach (string text in directories) { if (File.Exists(text + "/level") && File.ReadAllText(text + "/level").Trim() == "3") { hashSet.Add(ParseList(File.ReadAllText(text + "/shared_cpu_list").Trim())); } } } return hashSet.ToList(); } private static List<ulong> LinuxList(string dir, string prefix, string file) { if (!Directory.Exists(dir)) { return null; } return (from d in Directory.GetDirectories(dir, prefix + "*") where File.Exists(Path.Combine(d, file)) select ParseList(File.ReadAllText(Path.Combine(d, file)).Trim()) into m where m != 0 select m).ToList(); } private static IEnumerable<int> LinuxCpus() { IEnumerable<int> enumerable = from n in (from d in Directory.GetDirectories("/sys/devices/system/cpu", "cpu*") select Path.GetFileName(d).Substring(3) into n where n.Length > 0 && n.All(char.IsDigit) select n).Select(int.Parse) orderby n select n; if (!File.Exists("/sys/devices/system/cpu/online")) { return enumerable; } ulong mask = ParseList(File.ReadAllText("/sys/devices/system/cpu/online").Trim()); return enumerable.Where((int cpu) => cpu >= 64 || (mask & (ulong)(1L << cpu)) != 0); } [DllImport("ntdll.dll")] private static extern int NtQuerySystemInformation(int informationClass, IntPtr information, int length, out int returnLength); private static double[] Busy(int milliseconds) { ulong[,] array = Times(); if (array == null) { return null; } Thread.Sleep(milliseconds); ulong[,] array2 = Times(); if (array2 == null || array2.GetLength(0) != array.GetLength(0)) { return null; } double[] array3 = new double[array.GetLength(0)]; for (int i = 0; i < array3.Length; i++) { double num = array2[i, 0] - array[i, 0]; double num2 = array2[i, 1] - array[i, 1]; array3[i] = ((num > 0.0) ? Math.Max(0.0, Math.Min(1.0, 1.0 - num2 / num)) : 0.0); } return array3; } private static ulong[,] Times() { if (s_windows) { int num = 3072; IntPtr intPtr = Marshal.AllocHGlobal(num); try { if (NtQuerySystemInformation(8, intPtr, num, out var returnLength) != 0 || returnLength < 48) { return null; } int num2 = returnLength / 48; ulong[,] array = new ulong[num2, 2]; for (int i = 0; i < num2; i++) { ulong num3 = (ulong)Marshal.ReadInt64(intPtr, 48 * i); ulong num4 = (ulong)Marshal.ReadInt64(intPtr, 48 * i + 8); ulong num5 = (ulong)Marshal.ReadInt64(intPtr, 48 * i + 16); array[i, 0] = num4 + num5; array[i, 1] = num3; } return array; } finally { Marshal.FreeHGlobal(intPtr); } } if (!File.Exists("/proc/stat")) { return null; } List<string> list = (from l in File.ReadAllLines("/proc/stat") where l.StartsWith("cpu") && l.Length > 3 && char.IsDigit(l[3]) select l).ToList(); int num6 = list.Select((string l) => int.Parse(l.Split(new char[1] { ' ' })[0].Substring(3))).DefaultIfEmpty(-1).Max() + 1; if (num6 <= 0) { return null; } ulong[,] array2 = new ulong[num6, 2]; foreach (string item in list) { string[] array3 = item.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); int num7 = int.Parse(array3[0].Substring(3)); ulong[] array4 = (from x in array3.Skip(1).Take(8) select ulong.Parse(x, CultureInfo.InvariantCulture)).ToArray(); array2[num7, 0] = array4.Aggregate(0uL, (ulong a, ulong b) => a + b); array2[num7, 1] = array4[3] + ((array4.Length > 4) ? array4[4] : 0); } return array2; } [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetProcessAffinityMask(IntPtr process, UIntPtr mask); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetProcessAffinityMask(IntPtr process, out UIntPtr processMask, out UIntPtr systemMask); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetPriorityClass(IntPtr process, uint priorityClass); [DllImport("libc", SetLastError = true)] private static extern int sched_setaffinity(int pid, IntPtr cpusetsize, ulong[] mask); [DllImport("libc", SetLastError = true)] private static extern int setpriority(int which, int who, int prio); [DllImport("libc", SetLastError = true)] private static extern int sched_getaffinity(int pid, IntPtr cpusetsize, ulong[] mask); private static ulong Allowed() { if (s_windows) { if (!GetProcessAffinityMask(GetCurrentProcess(), out var processMask, out var _)) { return 0uL; } return (ulong)processMask; } ulong[] array = new ulong[16]; if (sched_getaffinity(0, new IntPtr(8 * array.Length), array) != 0) { return 0uL; } return array[0]; } private static ulong AllProcessors() { if (s_windows) { if (!GetProcessAffinityMask(GetCurrentProcess(), out var _, out var systemMask)) { return 0uL; } return (ulong)systemMask; } return (from c in LinuxCpus() where c < 64 select c).Aggregate(0uL, (ulong m, int c) => m | (ulong)(1L << c)); } private static bool SetAffinity(ulong mask) { if (s_windows) { return SetProcessAffinityMask(GetCurrentProcess(), new UIntPtr(mask)); } bool result = false; foreach (int item in EveryThread()) { if (sched_setaffinity(item, new IntPtr(8), new ulong[1] { mask }) == 0) { result = true; } } return result; } private static IEnumerable<int> EveryThread() { HashSet<int> seen = new HashSet<int>(); for (int pass = 0; pass < 20; pass++) { bool found = false; string[] directories = Directory.GetDirectories("/proc/self/task"); for (int i = 0; i < directories.Length; i++) { if (int.TryParse(Path.GetFileName(directories[i]), out var result) && seen.Add(result)) { found = true; yield return result; } } if (!found) { break; } } } private static string SetPriority(string priority) { string text; if (s_windows) { text = priority.ToLowerInvariant(); uint priorityClass; if (!(text == "abovenormal")) { if (!(text == "high")) { return "Process priority: '" + priority + "' is not Normal, AboveNormal or High; left as it was."; } priorityClass = 128u; } else { priorityClass = 32768u; } if (!SetPriorityClass(GetCurrentProcess(), priorityClass)) { return "Process priority: the system refused it; left as it was."; } return "Process priority: " + priority + "."; } text = priority.ToLowerInvariant(); int num; if (!(text == "abovenormal")) { if (!(text == "high")) { return "Process priority: '" + priority + "' is not Normal, AboveNormal or High; left as it was."; } num = -10; } else { num = -5; } int num2 = 0; int num3 = 0; foreach (int item in EveryThread()) { num3++; if (setpriority(0, item, num) == 0) { num2++; } } if (num2 <= 0) { return "Process priority: the system refused it (raising priority on Linux needs CAP_SYS_NICE); left as it was."; } return $"Process priority: {priority} (nice {num}) on {num2} of {num3} thread(s)."; } public static ulong ParseList(string list, bool strict = false) { ulong num = 0uL; string[] array = list.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (string text in array) { string[] array2 = text.Trim().Split(new char[1] { '-' }); if (array2.Length > 2) { throw new FormatException("'" + text.Trim() + "' is not a processor or a range of them"); } int num2 = int.Parse(array2[0].Trim(), CultureInfo.InvariantCulture); int num3 = ((array2.Length > 1) ? int.Parse(array2[1].Trim(), CultureInfo.InvariantCulture) : num2); if (strict && (num2 > num3 || num3 > 63)) { throw new FormatException((num2 > num3) ? ("'" + text.Trim() + "' runs backwards") : ("'" + text.Trim() + "' goes past processor 63, the last one this can use")); } for (int j = num2; j <= num3 && j < 64; j++) { num |= (ulong)(1L << j); } } if (strict && num == 0L) { throw new FormatException("it names no processor"); } return num; } public static string Describe(ulong mask) { List<string> list = new List<string>(); for (int i = 0; i < 64; i++) { if ((mask & (ulong)(1L << i)) != 0L) { int j; for (j = i; j + 1 < 64 && (mask & (ulong)(1L << j + 1)) != 0L; j++) { } list.Add((j > i) ? $"{i}-{j}" : i.ToString()); i = j; } } if (list.Count <= 0) { return "none"; } return string.Join(",", list); } } [Harmony] [BepInPlugin("MVP.Valheim_Serverside_Simulations", "Sarkastic.gg Dedicated Simulation", "1.18.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class ServersidePlugin : BaseUnityPlugin { public const string PluginGUID = "MVP.Valheim_Serverside_Simulations"; public const string PluginName = "Sarkastic.gg Dedicated Simulation"; public const string PluginVersion = "1.18.0"; private static ServersidePlugin context; public static Configuration configuration; public static Harmony harmony; public const string ValheimPlusPluginId = "org.bepinex.plugins.valheim_plus"; public static ManualLogSource logger; private static bool consoleStarted; private static bool installed; private int tickErrors; private float tickErrorsSince; public static ServersidePlugin instance => context; private void Awake() { //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Expected O, but got Unknown context = this; logger = ((BaseUnityPlugin)this).Logger; Configuration.Load(((BaseUnityPlugin)this).Config); if (!ModIsEnabled()) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Sarkastic.gg Dedicated Simulation is disabled. (configuration)"); return; } if (!IsDedicated()) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Sarkastic.gg Dedicated Simulation is disabled. (not a dedicated server)"); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)"Installing Sarkastic.gg Dedicated Simulation"); LimitJobWorkers(Configuration.unityJobWorkers.Value); ProcessPlacement.Apply(Configuration.cpuAffinity.Value, Configuration.processPriority.Value, delegate(string message) { ((BaseUnityPlugin)this).Logger.LogInfo((object)message); }); LimitPhysicsCatchUp(Configuration.maxCatchUpMs.Value); if (Configuration.consoleCommandsEnabled.Value) { ServerConsole.Start(); consoleStarted = true; } harmony = new Harmony("MVP.Valheim_Serverside_Simulations"); AvailableFeatures availableFeatures = new AvailableFeatures(); availableFeatures.AddFeature(new Core()); availableFeatures.AddFeature(new MaxObjectsPerFrame()); availableFeatures.AddFeature(new Networking()); availableFeatures.AddFeature(new Performance()); availableFeatures.AddFeature(new Fixes()); availableFeatures.AddFeature(new Debugging()); availableFeatures.AddFeature(new Compat_ValheimPlus()); availableFeatures.AddFeature(new Compat_ValheimCommunityPatch()); availableFeatures.AddFeature(new DungeonLoadGuard()); availableFeatures.AddFeature(new CharacterGuard()); availableFeatures.AddFeature(new ItemLedger()); availableFeatures.AddFeature(new FireControl()); availableFeatures.AddFeature(new NightSpawnGuard()); availableFeatures.AddFeature(new GuardianStones()); availableFeatures.AddFeature(new LocationIcons()); availableFeatures.AddFeature(new PortalLock()); PatchRequirements patchRequirements = new PatchRequirements(); patchRequirements.AddRequirement(new PatchRequirement.DebugBuild()); if (PatchFeatures(availableFeatures, new HarmonyFeaturesPatcher(patchRequirements))) { VanillaDrift.Check(((BaseUnityPlugin)this).Logger); installed = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Sarkastic.gg Dedicated Simulation installed"); } } private void Update() { if (consoleStarted) { ServerConsole.ProcessPending(); } if (installed) { PerformanceStats.Frame(); Safely(CharacterGuard.Tick, "Character guard"); Safely(ItemLedger.Tick, "Item ledger"); Safely(FireControl.Tick, "Fire control"); Safely(GuardianStones.Tick, "Guardian stones"); Safely(LocationIcons.Tick, "Location icons"); Safely(PortalLock.Tick, "Portal lock"); } } private void Safely(Action tick, string feature) { try { tick(); } catch (Exception arg) { if (Time.realtimeSinceStartup - tickErrorsSince > 900f) { tickErrorsSince = Time.realtimeSinceStartup; tickErrors = 0; } if (tickErrors++ < 20) { ((BaseUnityPlugin)this).Logger.LogWarning((object)$"{feature}: {arg}"); } } } private void FixedUpdate() { if (installed) { PerformanceStats.FixedStep(); } } private void LimitJobWorkers(int limit) { int jobWorkerCount = JobsUtility.JobWorkerCount; if (limit > 0 && limit < jobWorkerCount) { JobsUtility.JobWorkerCount = limit; ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Unity job worker threads: {jobWorkerCount} -> {JobsUtility.JobWorkerCount}"); } } private void LimitPhysicsCatchUp(int milliseconds) { if (milliseconds > 0) { float maximumDeltaTime = Time.maximumDeltaTime; Time.maximumDeltaTime = Mathf.Max((float)milliseconds / 1000f, Time.fixedDeltaTime); ((BaseUnityPlugin)this).Logger.LogInfo((object)($"Physics catch-up: at most {Mathf.RoundToInt(Time.maximumDeltaTime / Time.fixedDeltaTime)} fixed steps per frame " + $"(fixed step {1000f * Time.fixedDeltaTime:0} ms, longest frame counted {1000f * maximumDeltaTime:0} -> {1000f * Time.maximumDeltaTime:0} ms)")); } } private bool PatchFeatures(AvailableFeatures availableFeatures, HarmonyFeaturesPatcher patcher) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown List<Harmony> list = new List<Harmony>(); foreach (IFeature item in availableFeatures.EnabledFeatures()) { string name = item.GetType().Name; Harmony val = new Harmony("MVP.Valheim_Serverside_Simulations." + name); try { patcher.PatchAll(item.GetType().GetNestedTypes(), val); list.Add(val); } catch (Exception arg) { val.UnpatchSelf(); if (item is Core) { ((BaseUnityPlugin)this).Logger.LogError((object)string.Format("Core patches failed to apply; {0} is disabled and the server runs vanilla. {1}", "Sarkastic.gg Dedicated Simulation", arg)); foreach (Harmony item2 in list) { item2.UnpatchSelf(); } harmony.UnpatchSelf(); return false; } ((BaseUnityPlugin)this).Logger.LogError((object)$"Feature {name} failed to apply and is disabled. {arg}"); } } return true; } public bool ModIsEnabled() { return Configuration.modEnabled.Value; } public static bool IsDedicated() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return new ZNet().IsDedicated(); } } } namespace Valheim_Serverside.Features { public class Compat_ValheimPlus : IFeature { public class ZNetScene_Awake_Patch { private static bool triedToPatchSmelter; public static void Postfix() { if (!triedToPatchSmelter) { triedToPatchSmelter = true; try { TryPatchSmelter(); } catch (Exception arg) { ServersidePlugin.logger.LogError((object)$"ValheimPlus compatibility could not be applied (Smelter): {arg}"); } } } } public bool haveValheimPlus; public Compat_ValheimPlus() { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown haveValheimPlus = Chainloader.PluginInfos.ContainsKey("org.bepinex.plugins.valheim_plus"); if (haveValheimPlus) { try { TryPatchInventoryAssistant(); ServersidePlugin.harmony.Patch((MethodBase)AccessTools.Method(typeof(ZNetScene), "Awake", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(ZNetScene_Awake_Patch), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } catch (Exception arg) { ServersidePlugin.logger.LogError((object)$"ValheimPlus compatibility could not be applied (InventoryAssistant): {arg}"); } } } public bool FeatureEnabled() { return haveValheimPlus; } private void TryPatchInventoryAssistant() { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown Type type = Type.GetType("ValheimPlus.InventoryAssistant, ValheimPlus"); if (type != null) { ServersidePlugin.logger.LogInfo((object)"Patching ValheimPlus.InventoryAssistant.GetNearbyChests"); ServersidePlugin.harmony.Patch((MethodBase)AccessTools.Method(type, "GetNearbyChests", (Type[])null, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(Compat_ValheimPlus), "Transpile_InventoryAssistant_GetNearbyChests", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); } else { ServersidePlugin.logger.LogError((object)"Couldn't find ValheimPlus.InventoryAssistant"); } } private static void TryPatchSmelter() { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown Type type = Type.GetType("ValheimPlus.GameClasses.Smelter_UpdateSmelter_Patch, ValheimPlus"); if (type != null) { ServersidePlugin.logger.LogInfo((object)"Patching ValheimPlus.GameClasses.Smelter_UpdateSmelter_Patch.Prefix"); ServersidePlugin.harmony.Patch((MethodBase)AccessTools.Method(type, "Prefix", (Type[])null, (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(Compat_ValheimPlus), "Transpile_Smelter_FixedUpdate_Patch", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null); } else { ServersidePlugin.logger.LogError((object)"Couldn't find ValheimPlus.GameClasses.Smelter_UpdateSmelter_Patch"); } } private static IEnumerable<CodeInstruction> Transpile_InventoryAssistant_GetNearbyChests(IEnumerable<CodeInstruction> instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Expected O, but got Unknown //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Expected O, but got Unknown //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Expected O, but got Unknown //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Expected O, but got Unknown return new CodeMatcher(instructions, (ILGenerator)null).MatchForward(false, (CodeMatch[])(object)new CodeMatch[3] { new CodeMatch((OpCode?)OpCodes.Ldsfld, (object)AccessTools.Field(typeof(Player), "m_localPlayer"), (string)null), new CodeMatch((OpCode?)OpCodes.Callvirt, (object)AccessTools.Method(typeof(Player), "GetPlayerID", (Type[])null, (Type[])null), (string)null), new CodeMatch((OpCode?)OpCodes.Callvirt, (object)AccessTools.Method(typeof(Container), "CheckAccess", (Type[])null, (Type[])null), (string)null) }).RemoveInstructions(3).Insert((CodeInstruction[])(object)new CodeInstruction[1] { new CodeInstruction(OpCodes.Ldc_I4_1, (object)null) }) .MatchForward(true, (CodeMatch[])(object)new CodeMatch[2] { new CodeMatch((OpCode?)OpCodes.Stloc_S, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Ldarg_2, (object)null, (string)null) }) .SetInstruction(new CodeInstruction(OpCodes.Ldc_I4_0, (object)null)) .InstructionEnumeration(); } private static IEnumerable<CodeInstruction> Transpile_Smelter_FixedUpdate_Patch(IEnumerable<CodeInstruction> instructions) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Expected O, but got Unknown //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Expected O, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown return new CodeMatcher(instructions, (ILGenerator)null).MatchForward(true, (CodeMatch[])(object)new CodeMatch[3] { new CodeMatch((OpCode?)OpCodes.Ldsfld, (object)null, (string)null), new CodeMatch((OpCode?)OpCodes.Call, (object)AccessTools.Method(typeof(Object), "op_Implicit", (Type[])null, (Type[])null), (string)null), new CodeMatch((OpCode?)OpCodes.Brfalse, (object)null, (string)null) }).SetOpcodeAndAdvance(OpCodes.Brtrue).InstructionEnumeration(); } } public class Compat_ValheimCommunityPatch : IFeature { [HarmonyPatch(typeof(ZNetScene), "Awake")] public static class ZNetScene_Awake_Patch { private static bool s_done; private static void Postfix() { if (!s_done) { s_done = true; TakeOver("CreateObjectsSorted", Configuration.compatVcpSpawnQueue, "spawn queue", "new objects are created in order of distance from the world origin, not from the nearest player."); TakeOver("RemoveObjects", Configuration.compatVcpUnload, "zone-diff unload", "objects around players far from the world origin can be destroyed and created again on every pass."); } } } public const string PluginId = "MidnightsFX.ValheimCommunityPatch"; public static bool UnloadKept; public bool FeatureEnabled() { return Chainloader.PluginInfos.ContainsKey("MidnightsFX.ValheimCommunityPatch"); } private static void TakeOver(string method, ConfigEntry<bool> keep, string what, string cost) { MethodBase methodBase = AccessTools.DeclaredMethod(typeof(ZNetScene), method, (Type[])null, (Type[])null); Patches val = ((methodBase != null) ? Harmony.GetPatchInfo(methodBase) : null); if (val == null || !val.Prefixes.Any((Patch patch) => patch.owner == "MidnightsFX.ValheimCommunityPatch")) { ServersidePlugin.logger.LogInfo((object)("ValheimCommunityPatch has no " + what + " on ZNetScene." + method + "; nothing to take over.")); } else if (keep.Value) { if (method == "RemoveObjects") { UnloadKept = true; ServersidePlugin.logger.LogInfo((object)("Keeping ValheimCommunityPatch's " + what + " on ZNetScene." + method + " ([Compat] " + ((ConfigEntryBase)keep).Definition.Key + " = true); the object lists are marked as edited on every pass so it uses the game's own unload check.")); } else { ServersidePlugin.logger.LogWarning((object)("Keeping ValheimCommunityPatch's " + what + " on ZNetScene." + method + " ([Compat] " + ((ConfigEntryBase)keep).Definition.Key + " = true): " + cost)); } } else { ServersidePlugin.harmony.Unpatch(methodBase, (HarmonyPatchType)1, "MidnightsFX.ValheimCommunityPatch"); Patches patchInfo = Harmony.GetPatchInfo(methodBase); if (patchInfo != null && patchInfo.Prefixes.Any((Patch patch) => patch.owner == "MidnightsFX.ValheimCommunityPatch")) { ServersidePlugin.logger.LogWarning((object)("Could not remove ValheimCommunityPatch's " + what + " from ZNetScene." + method + ": " + cost)); } else { ServersidePlugin.logger.LogInfo((object)("Removed ValheimCommunityPatch's " + what + " from ZNetScene." + method + ": it works around the server's reference position, the world origin on a dedicated server, not around the players. Set [Compat] " + ((ConfigEntryBase)keep).Definition.Key + " = true to keep it.")); } } } } public class DungeonLoadGuard : IFeature { [HarmonyPatch(typeof(BundleLoader), "OnLoadAsyncCompleted", new Type[] { })] public static class BundleLoader_OnLoadAsyncCompleted_Patch { private static bool Prefix(ref BundleLoader __instance) { AssetBundleCreateRequestWrapper asyncLoadRequest = __instance.m_asyncLoadRequest; if (asyncLoadRequest == null || (Object)(object)asyncLoadRequest.AssetBundle != (Object)null) { return true; } AssetBundle val = (__instance.m_bundle = FindLoaded(__instance.m_bundleName, __instance.m_bundlePath)); __instance.m_asyncLoadRequest = null; LogOnce("bundle " + __instance.m_bundleName, ((Object)(object)val != (Object)null) ? ("Unity refused to load bundle " + __instance.m_bundleName + " again because it is already loaded; using the one it holds, so what needs it still loads.") : ("Unity returned no bundle for " + __instance.m_bundleName + "; reporting its assets as failed rather than letting the game throw on it.")); if (__instance.m_shouldBeLoaded) { ((BundleLoader)(ref __instance)).InvokeCallbacks((LoadResult)(!((Object)(object)val != (Object)null))); } else if (BundleLoader.s_tickOnAsyncOperationComplete) { ((BundleLoader)(ref __instance)).TickAsync(); } return false; } } [HarmonyPatch(typeof(AssetLoader), "LoadAssetFromBundleAsync")] public static class AssetLoader_LoadAssetFromBundleAsync_Patch { private unsafe static bool Prefix(ref AssetLoader __instance) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) AssetBundleLoader instance = AssetBundleLoader.Instance; if (instance == null || (Object)(object)((BundleLoader)(ref instance.m_bundleLoaders[__instance.m_bundleLoaderIndex])).Bundle != (Object)null) { return true; } AssetID assetID = __instance.m_assetID; LogOnce("asset " + ((object)(*(AssetID*)(&assetID))/*cast due to .constrained prefix*/).ToString(), $"Asset {__instance.m_assetID} ({__instance.m_assetPathInBundle}) has no bundle to load from; reporting it as failed."); ((AssetLoader)(ref __instance)).InvokeCallbacks((LoadResult)1); return false; } } [HarmonyPatch(typeof(DungeonGenerator), "OnRoomLoaded")] [HarmonyAfter(new string[] { "MidnightsFX.ValheimCommunityPatch" })] public static class DungeonGenerator_OnRoomLoaded_Patch { private static bool Prefix(DungeonGenerator __instance, LoadResult result, bool __runOriginal) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) if (!__runOriginal || (int)result == 0 || (Object)(object)__instance == (Object)null || !Object.op_Implicit((Object)(object)__instance)) { return true; } Vector3 position = ((Component)__instance).transform.position; LogOnce("dungeon " + ((object)ZoneSystem.GetZone(position)/*cast due to .constrained prefix*/).ToString(), $"Dungeon {((Object)__instance).name} at {position:F0}: a room failed to load ({result}). Letting it go so its zone keeps working; it is tried again the next time it is created."); __instance.m_roomsToLoad = int.MaxValue; __instance.ReleaseHeldReferences(); return false; } } private static readonly HashSet<string> s_logged = new HashSet<string>(); public bool FeatureEnabled() { return Configuration.fixDungeonLoadGuard.Value; } private static void LogOnce(string key, string text) { if (s_logged.Add(key)) { ServersidePlugin.logger.LogWarning((object)text); } } private static AssetBundle FindLoaded(string bundleName, string bundlePath) { string text = (string.IsNullOrEmpty(bundlePath) ? null : Path.GetFileName(bundlePath)); foreach (AssetBundle allLoadedAssetBundle in AssetBundle.GetAllLoadedAssetBundles()) { if ((Object)(object)allLoadedAssetBundle != (Object)null && (((Object)allLoadedAssetBundle).name == bundleName || (text != null && ((Object)allLoadedAssetBundle).name == text))) { return allLoadedAssetBundle; } } return null; } } public class CharacterGuard : IFeature { public enum NewCharacterPolicy { Allow, RequireFresh } public enum Action { Ignore, Log, Kick } private class Record { public long id; public string name = ""; public string account = ""; public string reason = ""; public DateTime firstSeen; public DateTime lastSeen; public string[] wearing; public string[] progress; } private class Session { public float since; public float zdoSince = -1f; public bool decided; public long id; public float kickAt = -1f; public bool kicked; public float welcomeAt = -1f; } [HarmonyPatch(typeof(ZNet), "Save")] public static class ZNet_Save_Patch { private static void Prefix() { SnapshotOnServer("save"); } } [HarmonyPatch(typeof(ZNet), "Shutdown")] public static class ZNet_Shutdown_Patch { private static void Prefix() { SnapshotOnServer("shutdown"); } } [HarmonyPatch(typeof(ZNet), "Disconnect")] public static class ZNet_Disconnect_Patch { private static void Prefix(ZNetPeer peer) { try { if (Object.op_Implicit((Object)(object)ZNet.instance) && ZNet.instance.IsServer() && ZDOMan.instance != null && Object.op_Implicit((Object)(object)ObjectDB.instance) && Object.op_Implicit((Object)(object)RandEventSystem.instance) && peer != null && s_sessions.TryGetValue(peer, out var value) && value.decided && !value.kicked && s_records.ContainsKey(value.id)) { Snapshot(peer, value); Save(); } } catch (Exception e) { Warn("could not note what " + peer?.m_playerName + " wore when leaving", e); } } } private static readonly Dictionary<long, Record> s_records = new Dictionary<long, Record>(); private static readonly Dictionary<ZNetPeer, Session> s_sessions = new Dictionary<ZNetPeer, Session>(); private static readonly Dictionary<string, Record> s_rejected = new Dictionary<string, Record>(StringComparer.OrdinalIgnoreCase); private static HashSet<long> s_footprint; private static bool s_footprintStarted; private static string s_path; private static Dictionary<int, bool> s_advanced; private static HashSet<string> s_progressEvents; private static float s_nextTick; private static float s_nextSnapshot; private static bool s_failed; private static int s_warnings; private static float s_warningsSince; private const float SettleSeconds = 3f; private const float EventsWaitSeconds = 20f; private const float KickDelaySeconds = 8f; private const float WelcomeDelaySeconds = 6f; private const float SnapshotSeconds = 60f; private static readonly int[] Worn = new int[10] { ZDOVars.s_rightItem, ZDOVars.s_leftItem, ZDOVars.s_rightBackItem, ZDOVars.s_leftBackItem, ZDOVars.s_chestItem, ZDOVars.s_legItem, ZDOVars.s_helmetItem, ZDOVars.s_shoulderItem, ZDOVars.s_utilityItem, ZDOVars.s_trinketItem }; private static readonly Dictionary<int, int> WornQuality = new Dictionary<int, int> { { ZDOVars.s_rightItem, ZDOVars.s_rightItemQuality }, { ZDOVars.s_leftItem, ZDOVars.s_leftItemQuality }, { ZDOVars.s_rightBackItem, ZDOVars.s_rightBackItemQuality }, { ZDOVars.s_leftBackItem, ZDOVars.s_leftBackItemQuality } }; public bool FeatureEnabled() { return Configuration.characterGuardEnabled.Value; } public static void EnsureFootprint() { if (s_failed) { return; } if (s_path == null) { try { Load(); } catch (Exception ex) { s_failed = true; ServersidePlugin.logger.LogError((object)$"Character guard: could not read the known characters, so it stays off until the server restarts: {ex}"); GuardLog.Write("Character guard: could not read the known characters (" + ex.GetType().Name + ": " + ex.Message + "), so it stays off until the server restarts"); return; } } if (!s_footprintStarted) { s_footprintStarted = true; ((MonoBehaviour)ServersidePlugin.instance).StartCoroutine(CollectFootprint()); } } public static bool IsBrandNew(ZNetPeer peer, ZDO character, long id) { if (s_failed || s_footprint == null || s_footprint.Contains(id) || (s_records.TryGetValue(id, out var value) && value.reason != "new and fresh") || !peer.m_serverSyncedPlayerData.TryGetValue("possibleEvents", out var value2)) { return false; } if (value != null && (DateTime.UtcNow - value.firstSeen).TotalMinutes > 10.0) { return false; } return FreshnessProblems(Wearing(character), value2, haveEvents: true).Count == 0; } public static void Tick() { if (!Configuration.characterGuardEnabled.Value || !Object.op_Implicit((Object)(object)ZNet.instance) || !ZNet.instance.IsServer() || ZNet.World == null || ZDOMan.instance == null || !Object.op_Implicit((Object)(object)ObjectDB.instance) || !Object.op_Implicit((Object)(object)RandEventSyste