using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Logging; using HarmonyLib; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("IronLabs.ScatteredVikings")] [assembly: AssemblyDescription("Gives every Viking a personal Meadows spawn.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("IronLabs")] [assembly: AssemblyProduct("IronLabs.ScatteredVikings")] [assembly: AssemblyCopyright("Copyright © 2026 End3rbyte")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("3EF3F0B9-0D06-4746-AAE4-B5FAFD1F0F95")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.0")] namespace IronLabs.ScatteredVikings; [HarmonyPatch(typeof(Game), "FindSpawnPoint")] internal static class GameFindSpawnPointPatch { private static bool Prefix(Game __instance, ref bool __result, ref Vector3 point, ref bool usedLogoutPoint, bool ___m_respawnAfterDeath) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) PlayerProfile playerProfile = __instance.GetPlayerProfile(); if (ShouldUseVanillaSpawn(playerProfile, ___m_respawnAfterDeath)) { return true; } point = SpawnPointSelector.Get(playerProfile); usedLogoutPoint = false; ZNet.instance.SetReferencePosition(point); __result = TryPrepareSpawnPoint(ref point); return false; } private static bool ShouldUseVanillaSpawn(PlayerProfile profile, bool respawnAfterDeath) { if (!profile.HaveCustomSpawnPoint()) { if (!respawnAfterDeath) { return profile.HaveLogoutPoint(); } return false; } return true; } private static bool TryPrepareSpawnPoint(ref Vector3 point) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) if (!ZNetScene.instance.IsAreaReady(point)) { return false; } float num = default(float); if (!ZoneSystem.instance.GetGroundHeight(point, ref num)) { ModLog.LogWarning($"No ground was found at scattered spawn {point}; waiting for terrain."); return false; } point.y = num + 0.25f; return true; } } internal static class ModLog { private static ManualLogSource _logger; private static string _pluginName; internal static void Initialize(ManualLogSource logger, string pluginName) { _logger = logger; _pluginName = pluginName; } internal static void Clear() { _logger = null; _pluginName = null; } internal static void LogDebug(object message) { Write((LogLevel)32, message); } internal static void LogInfo(object message) { Write((LogLevel)16, message); } internal static void LogWarning(object message) { Write((LogLevel)4, message); } private static void Write(LogLevel level, object message) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) ManualLogSource logger = _logger; if (logger != null) { logger.Log(level, message); } } } [BepInPlugin("IronLabs.ScatteredVikings", "IronLabs.ScatteredVikings", "1.0.0")] public sealed class ScatteredVikingsPlugin : BaseUnityPlugin { private const string PluginGuid = "IronLabs.ScatteredVikings"; private const string PluginName = "IronLabs.ScatteredVikings"; private const string PluginVersion = "1.0.0"; private readonly Harmony _harmony = new Harmony("IronLabs.ScatteredVikings"); private static bool _patchesApplied; private void Awake() { ModLog.Initialize(((BaseUnityPlugin)this).Logger, "IronLabs.ScatteredVikings"); PatchOwnNamespace(); ModLog.LogInfo("IronLabs.ScatteredVikings 1.0.0 is loaded."); } private void PatchOwnNamespace() { if (_patchesApplied) { ModLog.LogDebug("Harmony patches are already active; skipping registration."); return; } string text = typeof(ScatteredVikingsPlugin).Namespace; Type[] types = Assembly.GetExecutingAssembly().GetTypes(); foreach (Type type in types) { if (type.Namespace == text) { _harmony.CreateClassProcessor(type).Patch(); } } _patchesApplied = true; ModLog.LogDebug("Harmony patches were applied for the plugin namespace."); } private void OnDestroy() { SpawnPointSelector.Clear(); if (_patchesApplied) { _harmony.UnpatchSelf(); _patchesApplied = false; } ModLog.Clear(); } } internal static class SpawnPointSelector { private const float MaximumRadius = 1000f; private const float MinimumHeightAboveWater = 2f; private const int MaximumAttempts = 10000; internal static Vector3 Get(PlayerProfile profile) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0023: 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_0036: 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_001b: Unknown result type (might be due to invalid IL or missing references) long worldUID = ZNet.instance.GetWorldUID(); if (SpawnPointStore.TryGet(profile.GetPlayerID(), worldUID, out var point)) { return point; } point = FindSpawnPoint(profile); SpawnPointStore.Set(profile.GetPlayerID(), worldUID, point); ModLog.LogDebug($"Selected scattered spawn {point} for player {profile.GetPlayerID()}."); return point; } internal static void Clear() { SpawnPointStore.ClearCache(); } private static Vector3 FindSpawnPoint(PlayerProfile profile) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) Random random = new Random(CreateSeed(profile)); for (int i = 0; i < 10000; i++) { Vector3 val = CreateCandidate(random); if (IsValid(val)) { val.y = WorldGenerator.instance.GetHeight(val.x, val.z); return val; } } ModLog.LogWarning("No random Meadows spawn was found; using the Meadows world center."); return new Vector3(0f, WorldGenerator.instance.GetHeight(0f, 0f), 0f); } private static int CreateSeed(PlayerProfile profile) { long worldUID = ZNet.instance.GetWorldUID(); long playerID = profile.GetPlayerID(); return (((int)worldUID ^ (int)(worldUID >> 32)) * 397) ^ (int)playerID ^ (int)(playerID >> 32); } private static Vector3 CreateCandidate(Random random) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) double num = random.NextDouble() * Math.PI * 2.0; float num2 = 1000f * Mathf.Sqrt((float)random.NextDouble()); return new Vector3(num2 * (float)Math.Cos(num), 0f, num2 * (float)Math.Sin(num)); } private static bool IsValid(Vector3 candidate) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Invalid comparison between Unknown and I4 //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) WorldGenerator instance = WorldGenerator.instance; if ((int)instance.GetBiome(candidate) != 1 || (int)instance.GetBiomeArea(candidate) != 2) { return false; } return instance.GetHeight(candidate.x, candidate.z) >= ZoneSystem.instance.m_waterLevel + 2f; } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class PlayerOnSpawnedPatch { private static void Postfix(Player __instance) { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)Minimap.instance == (Object)null) && !((Object)(object)Game.instance == (Object)null) && !((Object)(object)ZNet.instance == (Object)null)) { long playerID = Game.instance.GetPlayerProfile().GetPlayerID(); long worldUID = ZNet.instance.GetWorldUID(); if (SpawnPointStore.NeedsPin(playerID, worldUID) && SpawnPointStore.TryGet(playerID, worldUID, out var point)) { Minimap.instance.AddPin(point, (PinType)3, "START", true, false, 0L, default(PlatformUserID)); SpawnPointStore.MarkPinCreated(playerID, worldUID); ModLog.LogDebug($"Added removable start pin at {point}."); } } } } internal static class SpawnPointStore { private struct SpawnRecord { internal readonly Vector3 Point; internal readonly bool PinCreated; internal SpawnRecord(Vector3 point, bool pinCreated) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) Point = point; PinCreated = pinCreated; } } private const string Header = "IronLabs.ScatteredVikings:1"; private static readonly Dictionary> Cache = new Dictionary>(); internal static bool TryGet(long playerId, long worldId, out Vector3 point) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) SpawnRecord value; bool flag = GetPlayerSpawns(playerId).TryGetValue(worldId, out value); point = (flag ? value.Point : Vector3.zero); return flag; } internal static void Set(long playerId, long worldId, Vector3 point) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) Dictionary playerSpawns = GetPlayerSpawns(playerId); playerSpawns[worldId] = new SpawnRecord(point, pinCreated: false); Save(playerId, playerSpawns); } internal static bool NeedsPin(long playerId, long worldId) { if (GetPlayerSpawns(playerId).TryGetValue(worldId, out var value)) { return !value.PinCreated; } return false; } internal static void MarkPinCreated(long playerId, long worldId) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) Dictionary playerSpawns = GetPlayerSpawns(playerId); if (playerSpawns.TryGetValue(worldId, out var value)) { playerSpawns[worldId] = new SpawnRecord(value.Point, pinCreated: true); Save(playerId, playerSpawns); } } internal static void ClearCache() { Cache.Clear(); } private static Dictionary GetPlayerSpawns(long playerId) { if (Cache.TryGetValue(playerId, out var value)) { return value; } value = Load(playerId); Cache.Add(playerId, value); return value; } private static Dictionary Load(long playerId) { Dictionary dictionary = new Dictionary(); string path = GetPath(playerId); if (!File.Exists(path)) { return dictionary; } try { ParseLines(File.ReadAllLines(path), dictionary); ModLog.LogDebug($"Loaded {dictionary.Count} scattered spawn(s) for player {playerId}."); } catch (Exception ex) { ModLog.LogWarning("Failed to load scattered spawns from " + path + ": " + ex.Message); } return dictionary; } private static void ParseLines(string[] lines, Dictionary spawns) { if (lines.Length == 0 || lines[0] != "IronLabs.ScatteredVikings:1") { throw new InvalidDataException("Unsupported scattered spawn file format."); } for (int i = 1; i < lines.Length; i++) { if (TryParse(lines[i], out var worldId, out var record)) { spawns[worldId] = record; } else { ModLog.LogWarning("Ignored malformed scattered spawn entry: " + lines[i]); } } } private static bool TryParse(string line, out long worldId, out SpawnRecord record) { //IL_008d: Unknown result type (might be due to invalid IL or missing references) string[] array = line.Split(new char[1] { '|' }); CultureInfo invariantCulture = CultureInfo.InvariantCulture; worldId = 0L; float result = 0f; float result2 = 0f; float result3 = 0f; bool result4 = false; bool result5 = array.Length == 5 && long.TryParse(array[0], NumberStyles.Integer, invariantCulture, out worldId) && float.TryParse(array[1], NumberStyles.Float, invariantCulture, out result) && float.TryParse(array[2], NumberStyles.Float, invariantCulture, out result2) && float.TryParse(array[3], NumberStyles.Float, invariantCulture, out result3) && bool.TryParse(array[4], out result4); record = new SpawnRecord(new Vector3(result, result2, result3), result4); return result5; } private static void Save(long playerId, Dictionary spawns) { string path = GetPath(playerId); string text = path + ".new"; try { Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllLines(text, CreateLines(spawns)); ReplaceFile(text, path); ModLog.LogDebug($"Saved {spawns.Count} scattered spawn(s) for player {playerId}."); } catch (Exception ex) { ModLog.LogWarning("Failed to save scattered spawns to " + path + ": " + ex.Message); DeleteTemporaryFile(text); } } private static string[] CreateLines(Dictionary spawns) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) List list = new List { "IronLabs.ScatteredVikings:1" }; foreach (KeyValuePair spawn in spawns) { SpawnRecord value = spawn.Value; list.Add(string.Join("|", spawn.Key, Format(value.Point.x), Format(value.Point.y), Format(value.Point.z), value.PinCreated)); } return list.ToArray(); } private static string Format(float value) { return value.ToString("R", CultureInfo.InvariantCulture); } private static void ReplaceFile(string temporaryPath, string path) { if (File.Exists(path)) { File.Replace(temporaryPath, path, null); } else { File.Move(temporaryPath, path); } } private static void DeleteTemporaryFile(string path) { if (File.Exists(path)) { File.Delete(path); } } private static string GetPath(long playerId) { return Path.Combine(Paths.ConfigPath, "IronLabs.ScatteredVikings", $"{playerId}.spawns"); } }