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 PraetorisClient v0.1.8
PraetorisClient.dll
Decompiled 2 months ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.Networking; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("PraetorisClient")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PraetorisClient")] [assembly: AssemblyCopyright("Copyright © 2023")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("3A303D15-4E60-4110-8EDF-EDF38560FBE2")] [assembly: AssemblyFileVersion("0.1.8")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.8.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace PraetorisClient { internal static class BotApiClient { public static IEnumerator PostLinkRoutine(LinkRequest link, Action<long, string, bool, string> sendResult) { string linkApiUrl = PraetorisClientPlugin.GetLinkApiUrl(); string botApiKey = PraetorisClientPlugin.GetBotApiKey(); if (string.IsNullOrWhiteSpace(linkApiUrl)) { sendResult(link.Sender, link.RequestId, arg3: false, "Link API URL is not configured on the server."); yield break; } if (string.IsNullOrWhiteSpace(botApiKey)) { sendResult(link.Sender, link.RequestId, arg3: false, "Bot API key is not configured on the server."); yield break; } string s = JsonLinkRequest(link); byte[] bytes = Encoding.UTF8.GetBytes(s); UnityWebRequest request = new UnityWebRequest(linkApiUrl, "POST") { uploadHandler = (UploadHandler)new UploadHandlerRaw(bytes), downloadHandler = (DownloadHandler)new DownloadHandlerBuffer() }; try { request.SetRequestHeader("Content-Type", "application/json"); request.SetRequestHeader("X-API-Key", botApiKey); request.SetRequestHeader("User-Agent", "PraetorisClient/0.1"); yield return request.SendWebRequest(); string text = ((request.downloadHandler != null) ? request.downloadHandler.text : ""); if ((int)request.result != 1 || request.responseCode < 200 || request.responseCode >= 300) { string text2 = ((!string.IsNullOrWhiteSpace(text)) ? text : (string.IsNullOrWhiteSpace(request.error) ? ("HTTP " + request.responseCode) : (request.error + " (HTTP " + request.responseCode + ")"))); PraetorisClientPlugin.Log.LogWarning((object)("Discord link API failed for " + link.PlayerId + ": " + text2)); sendResult(link.Sender, link.RequestId, arg3: false, text2); } else { string arg = (string.IsNullOrWhiteSpace(text) ? "Discord link complete." : text); PraetorisClientPlugin.Log.LogInfo((object)("Discord link API accepted " + link.PlayerId + ".")); sendResult(link.Sender, link.RequestId, arg3: true, arg); } } finally { ((IDisposable)request)?.Dispose(); } } private static string JsonLinkRequest(LinkRequest link) { return "{\"requestId\":\"" + EscapeJson(link.RequestId) + "\",\"code\":\"" + EscapeJson(link.Code) + "\",\"playerId\":\"" + EscapeJson(link.PlayerId) + "\",\"playerName\":\"" + EscapeJson(link.PlayerName) + "\",\"endpoint\":\"" + EscapeJson(link.Endpoint) + "\",\"platformDisplayName\":\"" + EscapeJson(link.PlatformDisplayName) + "\",\"receivedAtUtc\":\"" + EscapeJson(link.ReceivedAtUtc.ToString("O", CultureInfo.InvariantCulture)) + "\"}"; } private static string EscapeJson(string value) { return (value ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r") .Replace("\n", "\\n"); } } internal static class CreativeBiomeOverride { private sealed class OverrideZone { public Vector3 Center { get; } public float Radius { get; } public float RadiusSquared { get; } public float VisualRadius { get; } public float VisualRadiusSquared { get; } public float TerrainRadius { get; } public float TerrainRadiusSquared { get; } public Biome Biome { get; } public bool SuppressSpawns { get; } public bool UseTerrainSource { get; } public Vector3 TerrainSourceCenter { get; } public OverrideZone(Vector3 center, float radius, Biome biome, bool suppressSpawns, bool useTerrainSource, Vector3 terrainSourceCenter) { //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_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) Center = center; Radius = radius; RadiusSquared = radius * radius; VisualRadius = radius + 16f; VisualRadiusSquared = VisualRadius * VisualRadius; TerrainRadius = (useTerrainSource ? (radius + 256f) : radius); TerrainRadiusSquared = TerrainRadius * TerrainRadius; Biome = biome; SuppressSpawns = suppressSpawns; UseTerrainSource = useTerrainSource; TerrainSourceCenter = terrainSourceCenter; } public bool Contains(float x, float z) { return ContainsRadius(x, z, RadiusSquared); } public bool ContainsTerrain(float x, float z) { return ContainsRadius(x, z, TerrainRadiusSquared); } private bool ContainsRadius(float x, float z, float radiusSquared) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) float num = x - Center.x; float num2 = z - Center.z; return num * num + num2 * num2 <= radiusSquared; } public Vector2 MapToSource(float x, float z) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000d: 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_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) return new Vector2(TerrainSourceCenter.x + (x - Center.x), TerrainSourceCenter.z + (z - Center.z)); } public bool ContainsVisual(float x, float z) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) float num = x - Center.x; float num2 = z - Center.z; return num * num + num2 * num2 <= VisualRadiusSquared; } } [HarmonyPatch(typeof(WorldGenerator), "GetBiome", new Type[] { typeof(float), typeof(float), typeof(float), typeof(bool) })] private static class WorldGeneratorGetBiomePatch { private static bool Prefix(float wx, float wy, ref Biome __result) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected I4, but got Unknown if (!TryGetBiome(wx, wy, out var biome)) { return true; } __result = (Biome)(int)biome; return false; } } [HarmonyPatch(typeof(Heightmap), "GetBiome", new Type[] { typeof(Vector3), typeof(float), typeof(bool) })] private static class HeightmapGetBiomePatch { private static bool Prefix(Vector3 point, ref Biome __result) { //IL_0000: 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_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected I4, but got Unknown if (!TryGetBiome(point.x, point.z, out var biome)) { return true; } __result = (Biome)(int)biome; return false; } } [HarmonyPatch(typeof(WorldGenerator), "GetBiomeHeight")] private static class WorldGeneratorGetBiomeHeightPatch { private static void Prefix(ref Biome biome, ref float wx, ref float wy) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected I4, but got Unknown //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) if (TryMapToSource(wx, wy, out var source) && WorldGenerator.instance != null) { _samplingSourceTerrain = true; try { biome = (Biome)(int)WorldGenerator.instance.GetBiome(source.x, source.y, 0.02f, false); } finally { _samplingSourceTerrain = false; } wx = source.x; wy = source.y; } } } [HarmonyPatch(typeof(WorldGenerator), "GetHeight", new Type[] { typeof(float), typeof(float) })] private static class WorldGeneratorGetHeightPatch { private static bool Prefix(float wx, float wy, ref float __result) { //IL_0020: 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) if (!TryMapToSource(wx, wy, out var source) || WorldGenerator.instance == null) { return true; } _samplingSourceTerrain = true; try { __result = WorldGenerator.instance.GetHeight(source.x, source.y); } finally { _samplingSourceTerrain = false; } return false; } } [HarmonyPatch] private static class WorldGeneratorGetHeightWithMaskPatch { private static MethodBase TargetMethod() { return AccessTools.Method(typeof(WorldGenerator), "GetHeight", new Type[3] { typeof(float), typeof(float), typeof(Color).MakeByRefType() }, (Type[])null); } private static bool Prefix(float wx, float wy, ref Color mask, ref float __result) { //IL_0020: 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) if (!TryMapToSource(wx, wy, out var source) || WorldGenerator.instance == null) { return true; } _samplingSourceTerrain = true; try { __result = WorldGenerator.instance.GetHeight(source.x, source.y, ref mask); } finally { _samplingSourceTerrain = false; } return false; } } [HarmonyPatch(typeof(Heightmap), "GetBiomeColor", new Type[] { typeof(float), typeof(float) })] private static class HeightmapGetBiomeColorPatch { private static bool Prefix(Heightmap __instance, float ix, float iy, ref Color __result) { //IL_0026: 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_0066: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)null) { return true; } float num = (float)__instance.m_width * __instance.m_scale * 0.5f; Vector3 position = ((Component)__instance).transform.position; float x = position.x + (ix - 0.5f) * num * 2f; float z = position.z + (iy - 0.5f) * num * 2f; if (!TryGetVisualBiome(x, z, out var biome)) { return true; } __result = Color32.op_Implicit(Heightmap.GetBiomeColor(biome)); return false; } } [HarmonyPatch(typeof(WorldGenerator), "IsAshlands")] private static class WorldGeneratorIsAshlandsPatch { private static bool Prefix(float x, float y, ref bool __result) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 if (!TryGetBiome(x, y, out var biome)) { return true; } __result = (int)biome == 32; return false; } } [HarmonyPatch(typeof(WorldGenerator), "IsDeepnorth")] private static class WorldGeneratorIsDeepnorthPatch { private static bool Prefix(float x, float y, ref bool __result) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 if (!TryGetBiome(x, y, out var biome)) { return true; } __result = (int)biome == 64; return false; } } [HarmonyPatch(typeof(SpawnSystem), "IsSpawnPointGood")] private static class SpawnSystemIsSpawnPointGoodPatch { private static bool Prefix(ref Vector3 spawnPoint, ref bool __result) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) if (!ContainsSpawnBlockedZone(spawnPoint)) { return true; } __result = false; return false; } } private const int ProtocolVersion = 2; private const float VisualBiomeMargin = 16f; private const float TerrainSourcePadding = 256f; private static readonly Dictionary<string, OverrideZone> Zones = new Dictionary<string, OverrideZone>(); private static readonly FieldInfo? HeightmapBuildDataField = AccessTools.Field(typeof(Heightmap), "m_buildData"); private static bool _samplingSourceTerrain; public static void OnOverride(long sender, ZPackage pkg) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_00be: 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_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } try { int num = pkg.ReadInt(); if (num < 1 || num > 2) { PraetorisClientPlugin.Log.LogWarning((object)$"Ignoring creative biome override version {num}; expected 1-{2}."); return; } int num2 = pkg.ReadInt(); if (num2 <= 0) { ClearAll(); return; } for (int i = 0; i < num2; i++) { string text = pkg.ReadString(); bool num3 = pkg.ReadBool(); Vector3 center = pkg.ReadVector3(); float num4 = pkg.ReadSingle(); Biome val = (Biome)pkg.ReadInt(); bool suppressSpawns = ((num2 == 1 && pkg.GetPos() < pkg.Size()) ? pkg.ReadBool() : (!text.StartsWith("siege_", StringComparison.OrdinalIgnoreCase))); bool useTerrainSource = false; Vector3 terrainSourceCenter = Vector3.zero; if (num >= 2 && pkg.GetPos() < pkg.Size()) { useTerrainSource = pkg.ReadBool(); terrainSourceCenter = pkg.ReadVector3(); } if (!num3 || (int)val == 0 || num4 <= 0f) { Remove(text); } else { Set(text, center, num4, val, suppressSpawns, useTerrainSource, terrainSourceCenter); } } } catch (Exception arg) { PraetorisClientPlugin.Log.LogWarning((object)$"Failed to apply creative biome override: {arg}"); } } public static bool TryGetBiome(float x, float z, out Biome biome) { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected I4, but got Unknown //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: 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_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Expected I4, but got Unknown if (_samplingSourceTerrain) { biome = (Biome)0; return false; } foreach (OverrideZone value in Zones.Values) { if (!value.ContainsTerrain(x, z)) { continue; } if (value.UseTerrainSource && WorldGenerator.instance != null) { Vector2 val = value.MapToSource(x, z); _samplingSourceTerrain = true; try { biome = (Biome)(int)WorldGenerator.instance.GetBiome(val.x, val.y, 0.02f, false); } finally { _samplingSourceTerrain = false; } return true; } if (!value.UseTerrainSource) { biome = (Biome)(int)value.Biome; return true; } } biome = (Biome)0; return false; } private static bool TryGetVisualBiome(float x, float z, out Biome biome) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected I4, but got Unknown foreach (OverrideZone value in Zones.Values) { if (value.ContainsVisual(x, z)) { biome = (Biome)(int)value.Biome; return true; } } biome = (Biome)0; return false; } private static bool TryMapToSource(float x, float z, out Vector2 source) { //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_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) source = Vector2.zero; if (_samplingSourceTerrain) { return false; } foreach (OverrideZone value in Zones.Values) { if (value.UseTerrainSource && value.ContainsTerrain(x, z)) { source = value.MapToSource(x, z); return true; } } return false; } public static bool ContainsSpawnBlockedZone(Vector3 point) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) foreach (OverrideZone value in Zones.Values) { if (value.SuppressSpawns && value.Contains(point.x, point.z)) { return true; } } return false; } private static void Set(string zoneId, Vector3 center, float radius, Biome biome, bool suppressSpawns, bool useTerrainSource, Vector3 terrainSourceCenter) { //IL_0015: 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_001c: 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_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) zoneId = NormalizeZoneId(zoneId); OverrideZone value; bool num = Zones.TryGetValue(zoneId, out value); OverrideZone overrideZone = new OverrideZone(center, radius, biome, suppressSpawns, useTerrainSource, terrainSourceCenter); Zones[zoneId] = overrideZone; if (num) { RefreshTerrain(value); } RefreshTerrain(overrideZone); string text = (useTerrainSource ? $", terrainSource={terrainSourceCenter.x:0.##},{terrainSourceCenter.z:0.##}" : string.Empty); PraetorisClientPlugin.Log.LogInfo((object)$"Creative biome override {zoneId}: {biome} at {center.x:0.##},{center.z:0.##} radius {radius:0.##}, suppressSpawns={suppressSpawns}{text}."); } private static void Remove(string zoneId) { zoneId = NormalizeZoneId(zoneId); if (Zones.TryGetValue(zoneId, out OverrideZone value)) { Zones.Remove(zoneId); RefreshTerrain(value); PraetorisClientPlugin.Log.LogInfo((object)("Removed creative biome override " + zoneId + ".")); } } private static void ClearAll() { if (Zones.Count == 0) { return; } List<OverrideZone> list = new List<OverrideZone>(Zones.Values); Zones.Clear(); foreach (OverrideZone item in list) { RefreshTerrain(item); } PraetorisClientPlugin.Log.LogInfo((object)"Cleared creative biome overrides."); } private static void RefreshTerrain(OverrideZone zone) { //IL_006c: Unknown result type (might be due to invalid IL or missing references) foreach (Heightmap allHeightmap in Heightmap.GetAllHeightmaps()) { if (!((Object)(object)allHeightmap == (Object)null) && Intersects(allHeightmap, zone)) { HeightmapBuildDataField?.SetValue(allHeightmap, null); allHeightmap.Poke(false); } } if ((Object)(object)ClutterSystem.instance != (Object)null) { ClutterSystem.instance.ResetGrass(zone.Center, zone.TerrainRadius); } } private static bool Intersects(Heightmap heightmap, OverrideZone zone) { //IL_001b: 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) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) float num = (float)heightmap.m_width * heightmap.m_scale * 0.5f; Vector3 position = ((Component)heightmap).transform.position; float num2 = Math.Max(Math.Abs(position.x - zone.Center.x) - num, 0f); float num3 = Math.Max(Math.Abs(position.z - zone.Center.z) - num, 0f); return num2 * num2 + num3 * num3 <= zone.TerrainRadiusSquared; } private static string NormalizeZoneId(string zoneId) { if (!string.IsNullOrWhiteSpace(zoneId)) { return zoneId.Trim(); } return "creative"; } } internal static class CreativeCommandZoneState { private const int ProtocolVersion = 1; private const string DeniedMessage = "Creative commands can only be used inside your creative zone."; private static bool _active; private static Vector3 _center; private static float _radius; private static long _ownerPlayerId; private static long _playerId; private static string _slotId = string.Empty; private static bool _guardEnabled; private static string _protectedCommandPrefixes = string.Empty; internal static void OnState(long sender, ZPackage package) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00de: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } try { int num = package.ReadInt(); if (num != 1) { PraetorisClientPlugin.Log.LogWarning((object)$"Ignoring creative command zone state protocol {num}; expected {1}."); return; } bool num2 = package.ReadBool(); Vector3 center = package.ReadVector3(); float num3 = package.ReadSingle(); long ownerPlayerId = package.ReadLong(); long num4 = package.ReadLong(); string text = package.ReadString(); bool guardEnabled = package.GetPos() < package.Size() && package.ReadBool(); object obj = ((package.GetPos() < package.Size()) ? package.ReadString() : string.Empty); _guardEnabled = guardEnabled; if (obj == null) { obj = string.Empty; } _protectedCommandPrefixes = (string)obj; if (!num2 || num3 <= 0f || num4 == 0L) { Clear(); return; } _active = true; _center = center; _radius = num3; _ownerPlayerId = ownerPlayerId; _playerId = num4; _slotId = text ?? string.Empty; PraetorisClientPlugin.Log.LogInfo((object)$"Creative command zone active: {_slotId} at {_center.x:0.##},{_center.z:0.##} radius {_radius:0.##}."); } catch (Exception arg) { Clear(); PraetorisClientPlugin.Log.LogWarning((object)$"Failed to apply creative command zone state: {arg}"); } } internal static void Clear() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) _active = false; _center = Vector3.zero; _radius = 0f; _ownerPlayerId = 0L; _playerId = 0L; _slotId = string.Empty; } internal static bool CanRunCommand(ConsoleEventArgs args) { if (args == null || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || !IsProtectedCommand(args.FullLine)) { return true; } if (IsLocalPlayerInsideActiveZone()) { return true; } object obj = ((object)args.Context) ?? ((object)Console.instance); if (obj != null) { ((Terminal)obj).AddString("Creative commands can only be used inside your creative zone."); } return false; } private static bool IsProtectedCommand(string rawCommand) { if (!_guardEnabled) { return false; } string text = NormalizeCommand(rawCommand); if (text.Length == 0) { return false; } foreach (string protectedCommandPrefix in GetProtectedCommandPrefixes()) { if (text.StartsWith(protectedCommandPrefix, StringComparison.Ordinal)) { return true; } } return false; } private static bool IsLocalPlayerInsideActiveZone() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if (!_active || (Object)(object)localPlayer == (Object)null || _radius <= 0f) { return false; } if (_playerId != 0L && localPlayer.GetPlayerID() != _playerId) { return false; } Vector3 position = ((Component)localPlayer).transform.position; float num = position.x - _center.x; float num2 = position.z - _center.z; return num * num + num2 * num2 <= _radius * _radius; } private static IEnumerable<string> GetProtectedCommandPrefixes() { return from value in _protectedCommandPrefixes.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries) select value.Trim().ToLowerInvariant() into value where value.Length > 0 select value; } private static string NormalizeCommand(string rawCommand) { if (string.IsNullOrWhiteSpace(rawCommand)) { return string.Empty; } string[] array = rawCommand.Trim().Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length != 0) { return array[0].ToLowerInvariant(); } return string.Empty; } } internal static class CreativeInventoryRpc { private sealed class CreativeInventorySnapshot { public bool Available { get; set; } public string Error { get; set; } = string.Empty; public long PlayerId { get; set; } public string PlayerName { get; set; } = string.Empty; public int PlayerInventoryCount { get; set; } public bool ExtraSlotsLoaded { get; set; } public bool ExtraSlotsAvailable { get; set; } public int ExtraSlotsCount { get; set; } public int TotalUniqueCount { get; set; } public List<CreativeInventoryItem> Items { get; } = new List<CreativeInventoryItem>(); public static CreativeInventorySnapshot Unavailable(string error) { return new CreativeInventorySnapshot { Available = false, Error = error }; } } private sealed class CreativeInventoryItem { public string Source { get; set; } = string.Empty; public string PrefabName { get; set; } = string.Empty; public string SharedName { get; set; } = string.Empty; public int Stack { get; set; } public int Quality { get; set; } public bool Equipped { get; set; } public int GridX { get; set; } public int GridY { get; set; } public static CreativeInventoryItem FromItem(string source, ItemData item) { return new CreativeInventoryItem { Source = source, PrefabName = (((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : string.Empty), SharedName = (item.m_shared?.m_name ?? string.Empty), Stack = item.m_stack, Quality = item.m_quality, Equipped = item.m_equipped, GridX = item.m_gridPos.x, GridY = item.m_gridPos.y }; } } private sealed class ExtraSlotsReadResult { public bool ModLoaded { get; private set; } public bool Available { get; private set; } public string Error { get; private set; } = string.Empty; public List<ItemData> Items { get; } = new List<ItemData>(); public static ExtraSlotsReadResult NotLoaded() { return new ExtraSlotsReadResult { ModLoaded = false, Available = true }; } public static ExtraSlotsReadResult Loaded(IEnumerable<ItemData> items) { ExtraSlotsReadResult extraSlotsReadResult = new ExtraSlotsReadResult(); extraSlotsReadResult.ModLoaded = true; extraSlotsReadResult.Available = true; extraSlotsReadResult.Items.AddRange(items); return extraSlotsReadResult; } public static ExtraSlotsReadResult Failed(string error) { return new ExtraSlotsReadResult { ModLoaded = true, Available = false, Error = error }; } } private const int ProtocolVersion = 1; private const string ExtraSlotsApiTypeName = "ExtraSlots.API"; public static void OnRequest(long sender, ZPackage pkg) { //IL_0092: 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) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } string text = string.Empty; ZDOID val = ZDOID.None; bool flag = true; try { int num = pkg.ReadInt(); if (num != 1) { SendUnavailable(sender, text, val, $"Unsupported creative inventory protocol version {num}."); return; } text = pkg.ReadString(); val = pkg.ReadZDOID(); flag = pkg.ReadBool(); CreativeInventorySnapshot snapshot = BuildSnapshot(val, flag); SendResponse(sender, text, val, snapshot); } catch (Exception arg) { PraetorisClientPlugin.Log.LogWarning((object)$"Failed to answer creative inventory request {text}: {arg}"); SendUnavailable(sender, text, val, "Failed to read client inventory."); } } private static CreativeInventorySnapshot BuildSnapshot(ZDOID expectedCharacterId, bool includeItems) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || (Object)(object)((Character)localPlayer).m_nview == (Object)null || !((Character)localPlayer).m_nview.IsValid()) { return CreativeInventorySnapshot.Unavailable("Local player is not available."); } ZDO zDO = ((Character)localPlayer).m_nview.GetZDO(); if (zDO == null) { return CreativeInventorySnapshot.Unavailable("Local player character is not available."); } if (!((ZDOID)(ref expectedCharacterId)).IsNone() && zDO.m_uid != expectedCharacterId) { return CreativeInventorySnapshot.Unavailable("Local player character did not match the requested character."); } Inventory inventory = ((Humanoid)localPlayer).GetInventory(); if (inventory == null) { return CreativeInventorySnapshot.Unavailable("Local player inventory is not available."); } List<ItemData> list = (from item in inventory.GetAllItems() where item != null select item).ToList(); ExtraSlotsReadResult extraSlotsReadResult = ReadExtraSlotsItems(); if (!extraSlotsReadResult.Available && extraSlotsReadResult.ModLoaded) { return CreativeInventorySnapshot.Unavailable(extraSlotsReadResult.Error); } List<ItemData> list2 = new List<ItemData>(); AddUnique(list2, list); AddUnique(list2, extraSlotsReadResult.Items); CreativeInventorySnapshot creativeInventorySnapshot = new CreativeInventorySnapshot { Available = true, Error = string.Empty, PlayerId = localPlayer.GetPlayerID(), PlayerName = localPlayer.GetPlayerName(), PlayerInventoryCount = list.Count, ExtraSlotsAvailable = extraSlotsReadResult.Available, ExtraSlotsLoaded = extraSlotsReadResult.ModLoaded, ExtraSlotsCount = extraSlotsReadResult.Items.Count, TotalUniqueCount = list2.Count }; if (includeItems) { creativeInventorySnapshot.Items.AddRange(list.Select((ItemData item) => CreativeInventoryItem.FromItem("player", item))); creativeInventorySnapshot.Items.AddRange(extraSlotsReadResult.Items.Select((ItemData item) => CreativeInventoryItem.FromItem("extraSlots", item))); } return creativeInventorySnapshot; } private static ExtraSlotsReadResult ReadExtraSlotsItems() { Type type = (from assembly in AppDomain.CurrentDomain.GetAssemblies() select assembly.GetType("ExtraSlots.API", throwOnError: false)).FirstOrDefault((Type type2) => type2 != null); if (type == null) { return ExtraSlotsReadResult.NotLoaded(); } MethodInfo method = type.GetMethod("GetAllExtraSlotsItems", BindingFlags.Static | BindingFlags.Public); if (method == null) { return ExtraSlotsReadResult.Failed("ExtraSlots API did not expose GetAllExtraSlotsItems."); } try { if (!(method.Invoke(null, Array.Empty<object>()) is IEnumerable enumerable)) { return ExtraSlotsReadResult.Failed("ExtraSlots API returned no item list."); } List<ItemData> list = new List<ItemData>(); foreach (object item in enumerable) { ItemData val = (ItemData)((item is ItemData) ? item : null); if (val != null) { list.Add(val); } } return ExtraSlotsReadResult.Loaded(list); } catch (Exception ex) { return ExtraSlotsReadResult.Failed("ExtraSlots API read failed: " + ex.Message); } } private static void AddUnique(List<ItemData> target, IEnumerable<ItemData> items) { foreach (ItemData item in items) { if (!target.Contains(item)) { target.Add(item); } } } private static void SendUnavailable(long target, string requestId, ZDOID characterId, string error) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) SendResponse(target, requestId, characterId, CreativeInventorySnapshot.Unavailable(error)); } private static void SendResponse(long target, string requestId, ZDOID characterId, CreativeInventorySnapshot snapshot) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0015: Unknown result type (might be due to invalid IL or missing references) ZPackage val = new ZPackage(); val.Write(1); val.Write(requestId); val.Write(characterId); val.Write(snapshot.Available); val.Write(snapshot.Error); val.Write(snapshot.PlayerId); val.Write(snapshot.PlayerName); val.Write(snapshot.PlayerInventoryCount); val.Write(snapshot.ExtraSlotsLoaded); val.Write(snapshot.ExtraSlotsAvailable); val.Write(snapshot.ExtraSlotsCount); val.Write(snapshot.TotalUniqueCount); val.Write(snapshot.Items.Count); foreach (CreativeInventoryItem item in snapshot.Items) { val.Write(item.Source); val.Write(item.PrefabName); val.Write(item.SharedName); val.Write(item.Stack); val.Write(item.Quality); val.Write(item.Equipped); val.Write(item.GridX); val.Write(item.GridY); } ZRoutedRpc.instance.InvokeRoutedRPC(target, "DiscordTools_CreativeInventoryResponse", new object[1] { val }); } } internal static class LinkCommandHandler { private static readonly Regex CodePattern = new Regex("^[A-Za-z0-9_-]{4,64}$", RegexOptions.Compiled); public static bool TryHandle(Chat chat) { string text = (((Object)(object)((Terminal)chat).m_input != (Object)null) ? ((TMP_InputField)((Terminal)chat).m_input).text : ""); if (string.IsNullOrWhiteSpace(text)) { return false; } string text2 = PraetorisClientPlugin.LinkCommand.Value.Trim(); if (string.IsNullOrWhiteSpace(text2)) { text2 = "!link"; } string text3 = text.Trim(); if (!text3.Equals(text2, StringComparison.OrdinalIgnoreCase) && !text3.StartsWith(text2 + " ", StringComparison.OrdinalIgnoreCase)) { return false; } string text4 = ((text3.Length > text2.Length) ? text3.Substring(text2.Length).Trim() : ""); if (!CodePattern.IsMatch(text4)) { ((Terminal)chat).AddString("Usage: " + text2 + " CODE"); ClearInput(chat); return true; } if (!LinkRpc.TrySendRequest(text4, out string message)) { ((Terminal)chat).AddString(message); ClearInput(chat); return true; } ((Terminal)chat).AddString("Sending Discord link code to the server."); ClearInput(chat); return true; } private static void ClearInput(Chat chat) { if (!((Object)(object)((Terminal)chat).m_input == (Object)null)) { ((TMP_InputField)((Terminal)chat).m_input).text = ""; ((Component)((Terminal)chat).m_input).gameObject.SetActive(false); } } } internal static class LinkRpc { public static bool TrySendRequest(string code, out string message) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Invalid comparison between Unknown and I4 //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Expected O, but got Unknown message = ""; if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null || ZNet.instance.IsServer() || (int)ZNet.GetConnectionStatus() != 2) { message = "You must be connected to a server before linking Discord."; return false; } string text = Guid.NewGuid().ToString("N"); ZPackage val = new ZPackage(); val.Write(text); val.Write(code); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "DiscordTools_LinkRequest", new object[1] { val }); return true; } public static void OnRequest(long sender, ZPackage pkg) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } string requestId = pkg.ReadString(); string code = pkg.ReadString(); ZNetPeer val = PlayerResolver.FindPeerBySender(sender); if (val == null) { SendResult(sender, requestId, success: false, "The server could not identify your Valheim connection."); return; } PraetorisClientPlugin instance = PraetorisClientPlugin.Instance; if ((Object)(object)instance == (Object)null) { SendResult(sender, requestId, success: false, "PraetorisClient is not ready on the server."); return; } LinkRequest link = new LinkRequest { Sender = sender, RequestId = requestId, Code = code, PlayerId = PlayerResolver.StablePlayerId(val), PlayerName = (val.m_playerName ?? ""), Endpoint = PlayerResolver.SafeEndPoint(val), PlatformDisplayName = PlayerResolver.PlatformDisplayName(val), ReceivedAtUtc = DateTime.UtcNow }; PraetorisClientPlugin.Log.LogInfo((object)("Received Discord link code from " + PlayerResolver.DescribePeer(val) + ".")); ((MonoBehaviour)instance).StartCoroutine(BotApiClient.PostLinkRoutine(link, SendResult)); } public static void OnResult(long sender, ZPackage pkg) { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer()) { string text = pkg.ReadString(); bool flag = pkg.ReadBool(); string text2 = pkg.ReadString(); PraetorisClientPlugin.Log.LogInfo((object)("Discord link result " + text + ": " + text2)); Chat instance = Chat.instance; if (instance != null) { ((Terminal)instance).AddString(flag ? text2 : ("Discord link failed: " + text2)); } } } private static void SendResult(long target, string requestId, bool success, string message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(requestId); val.Write(success); val.Write(message); ZRoutedRpc.instance.InvokeRoutedRPC(target, "DiscordTools_LinkResult", new object[1] { val }); } } internal sealed class LinkRequest { public long Sender; public string RequestId = ""; public string Code = ""; public string PlayerId = ""; public string PlayerName = ""; public string Endpoint = ""; public string PlatformDisplayName = ""; public DateTime ReceivedAtUtc; } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class ZNetAwakePatch { private static void Postfix() { CreativeCommandZoneState.Clear(); PraetorisClientRpc.Register(); } } [HarmonyPatch(typeof(Chat), "SendInput")] internal static class ChatSendInputPatch { private static bool Prefix(Chat __instance) { return !LinkCommandHandler.TryHandle(__instance); } } [HarmonyPatch(typeof(Character), "ApplyDamage")] internal static class CharacterApplyDamageTelemetryPatch { private static void Prefix(Character __instance, HitData hit, ref DamageObservationState __state) { __state = ValheimEventsTelemetry.CaptureDamageBefore(__instance, hit); } private static void Postfix(Character __instance, HitData hit, DamageModifier mod, DamageObservationState __state) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) ValheimEventsTelemetry.LogDamageApplied(__instance, hit, mod, __state); } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PlayerDeathTelemetryPatch { private static void Prefix(Player __instance, ref DeathObservationState __state) { __state = ValheimEventsTelemetry.CaptureDeathBefore(__instance); } private static void Postfix(Player __instance, DeathObservationState __state) { ValheimEventsTelemetry.LogPlayerDied(__instance, __state); } } [HarmonyPatch(typeof(Minimap), "Explore", new Type[] { typeof(int), typeof(int) })] internal static class MinimapExploreTelemetryPatch { private static void Postfix(Minimap __instance, int x, int y, bool __result) { if (__result) { ValheimEventsTelemetry.RecordExploredCell(__instance, x, y); } } } [HarmonyPatch(typeof(Game), "Update")] internal static class GameUpdateTelemetryPatch { private static void Postfix() { ValheimEventsTelemetry.Update(); RpcTraceTelemetry.Update(); } } [HarmonyPatch(typeof(Game), "Logout")] internal static class GameLogoutRpcTracePatch { private static bool Prefix(Game __instance, bool save, bool changeToStartScene) { return RpcTraceTelemetry.ShouldAllowLogout(__instance, save, changeToStartScene); } } [HarmonyPatch(typeof(Game), "OnApplicationQuit")] internal static class GameApplicationQuitRpcTracePatch { private static void Prefix() { RpcTraceTelemetry.OnApplicationQuitFallback(); } } [HarmonyPatch(typeof(Menu), "QuitGame")] internal static class MenuQuitGameRpcTracePatch { private static bool Prefix() { return RpcTraceTelemetry.ShouldAllowMenuQuit(); } } [HarmonyPatch(typeof(Menu), "OnQuitYes")] internal static class MenuQuitYesRpcTracePatch { private static bool Prefix() { return RpcTraceTelemetry.ShouldAllowMenuQuit(); } } [HarmonyPatch(typeof(ConsoleCommand), "RunAction")] internal static class ConsoleCommandRunActionCreativeZoneGuardPatch { private static bool Prefix(ConsoleEventArgs args) { return CreativeCommandZoneState.CanRunCommand(args); } } internal static class PlayerResolver { public static List<ZNetPeer> FindPeers(string query) { List<ZNetPeer> list = new List<ZNetPeer>(); if ((Object)(object)ZNet.instance == (Object)null) { return list; } string text = Normalize(query); foreach (ZNetPeer connectedPeer in ZNet.instance.GetConnectedPeers()) { if (connectedPeer.IsReady()) { string value = SafeHostName(connectedPeer); string value2 = StablePlayerId(connectedPeer); if (Normalize(connectedPeer.m_playerName) == text || Normalize(value) == text || Normalize(value2) == text || (DigitsOnly(value) == DigitsOnly(query) && DigitsOnly(query).Length > 0)) { list.Add(connectedPeer); } } } if (list.Count > 0) { return list; } foreach (ZNetPeer connectedPeer2 in ZNet.instance.GetConnectedPeers()) { if (connectedPeer2.IsReady() && Normalize(connectedPeer2.m_playerName).Contains(text)) { list.Add(connectedPeer2); } } return list; } public static ZNetPeer? FindPeerBySender(long sender) { if ((Object)(object)ZNet.instance == (Object)null) { return null; } return ((IEnumerable<ZNetPeer>)ZNet.instance.GetConnectedPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer peer) => peer.m_uid == sender)); } public static string DescribePeer(ZNetPeer peer) { return peer.m_playerName + " (" + StablePlayerId(peer) + ")"; } public static string StablePlayerId(ZNetPeer peer) { string text = SafeHostName(peer); if (!string.IsNullOrWhiteSpace(text)) { return text; } return peer.m_uid.ToString(CultureInfo.InvariantCulture); } public static string SafeHostName(ZNetPeer peer) { try { ISocket socket = peer.m_socket; return ((socket != null) ? socket.GetHostName() : null) ?? ""; } catch { return ""; } } public static string SafeEndPoint(ZNetPeer peer) { try { ISocket socket = peer.m_socket; return ((socket != null) ? socket.GetEndPointString() : null) ?? ""; } catch { return ""; } } public static string PlatformDisplayName(ZNetPeer peer) { try { string value; return (peer.m_serverSyncedPlayerData != null && peer.m_serverSyncedPlayerData.TryGetValue("platformDisplayName", out value)) ? value : ""; } catch { return ""; } } private static string Normalize(string value) { return (value ?? "").Trim().ToLowerInvariant(); } private static string DigitsOnly(string value) { return new string((value ?? "").Where(char.IsDigit).ToArray()); } } [BepInPlugin("warpalicious.PraetorisClient", "PraetorisClient", "0.1.8")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class PraetorisClientPlugin : BaseUnityPlugin { private const string ModName = "PraetorisClient"; private const string ModVersion = "0.1.8"; private const string Author = "warpalicious"; private const string ModGUID = "warpalicious.PraetorisClient"; private const string LinkApiUrlEnv = "PRAETORISCLIENT_LINK_API_URL"; private const string BotApiKeyEnv = "PRAETORISCLIENT_BOT_API_KEY"; private readonly Harmony _harmony = new Harmony("warpalicious.PraetorisClient"); private DateTime _lastReloadTime; private FileSystemWatcher? _configWatcher; private const long ReloadDelayTicks = 10000000L; public static readonly ManualLogSource Log = Logger.CreateLogSource("PraetorisClient"); internal static ConfigEntry<string> LinkApiUrl = null; internal static ConfigEntry<string> BotApiKey = null; internal static ConfigEntry<string> LinkCommand = null; internal static ConfigEntry<bool> ValheimEventsTelemetryEnabled = null; internal static ConfigEntry<bool> CombatTelemetryEnabled = null; internal static ConfigEntry<bool> ExplorationTelemetryEnabled = null; internal static ConfigEntry<float> ExplorationFlushSeconds = null; internal static ConfigEntry<bool> RpcTraceEnabled = null; internal static ConfigEntry<bool> RpcTraceCaptureSendReceive = null; internal static ConfigEntry<string> RpcTraceNameDenyList = null; internal static ConfigEntry<bool> RpcTraceHttpUploadPreferred = null; public static PraetorisClientPlugin? Instance { get; private set; } internal static string GetLinkApiUrl() { string environmentVariable = Environment.GetEnvironmentVariable("PRAETORISCLIENT_LINK_API_URL"); if (!string.IsNullOrWhiteSpace(environmentVariable)) { return environmentVariable.Trim(); } return LinkApiUrl.Value; } internal static string GetBotApiKey() { string environmentVariable = Environment.GetEnvironmentVariable("PRAETORISCLIENT_BOT_API_KEY"); if (!string.IsNullOrWhiteSpace(environmentVariable)) { return environmentVariable.Trim(); } return BotApiKey.Value; } public void Awake() { Instance = this; BindConfig(); SynchronizationManager.OnConfigurationSynchronized += OnConfigurationSynchronized; SiegePortalTestCommand.Register(); RpcTraceTelemetry.Initialize(); _harmony.PatchAll(Assembly.GetExecutingAssembly()); SetupWatcher(); } private void OnDestroy() { SynchronizationManager.OnConfigurationSynchronized -= OnConfigurationSynchronized; try { _configWatcher?.Dispose(); _configWatcher = null; } catch (Exception ex) { Log.LogWarning((object)("Failed to dispose configuration watcher: " + ex.Message)); } try { RpcTraceTelemetry.Shutdown(); } catch (Exception ex2) { Log.LogWarning((object)("Failed to shut down RPC trace telemetry: " + ex2.Message)); } try { ((BaseUnityPlugin)this).Config.Save(); } catch (Exception ex3) { Log.LogWarning((object)("Failed to save configuration during shutdown: " + ex3.Message)); } try { _harmony.UnpatchSelf(); } catch (Exception ex4) { Log.LogWarning((object)("Failed to unpatch PraetorisClient during shutdown: " + ex4.Message)); } if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void BindConfig() { LinkApiUrl = ((BaseUnityPlugin)this).Config.Bind<string>("BotApi", "LinkApiUrl", "", "Compatible bot Valheim link endpoint. Prefer the PRAETORISCLIENT_LINK_API_URL environment variable on dedicated servers."); BotApiKey = ((BaseUnityPlugin)this).Config.Bind<string>("BotApi", "ApiKey", "", "API key sent to the bot in the X-API-Key header. Prefer the PRAETORISCLIENT_BOT_API_KEY environment variable on dedicated servers."); LinkCommand = ((BaseUnityPlugin)this).Config.Bind<string>("Linking", "LinkCommand", "!link", "In-game chat command consumed before it is sent as chat."); ValheimEventsTelemetryEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("ValheimEvents", "Enabled", true, SyncedDescription("Sends client-observed telemetry to the server-side ValheimEvents mod.")); CombatTelemetryEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("ValheimEvents", "CombatTelemetry", true, SyncedDescription("Sends client-observed combat and death telemetry.")); ExplorationTelemetryEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("ValheimEvents", "ExplorationTelemetry", true, SyncedDescription("Sends client-observed minimap exploration telemetry.")); ExplorationFlushSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("ValheimEvents", "ExplorationFlushSeconds", 2f, SyncedDescription("How long newly explored minimap cells are batched before sending.")); RpcTraceEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("RpcTrace", "Enabled", true, SyncedDescription("Sends client-observed routed RPC trace rows to the server-side ValheimTracer receiver.")); RpcTraceCaptureSendReceive = ((BaseUnityPlugin)this).Config.Bind<bool>("RpcTrace", "CaptureSendReceive", true, SyncedDescription("Captures raw routed RPC send and receive points in addition to handled RPC points.")); RpcTraceNameDenyList = ((BaseUnityPlugin)this).Config.Bind<string>("RpcTrace", "RpcNameDenyList", "", SyncedDescription("Comma-separated routed RPC names to exclude from client trace capture.")); RpcTraceHttpUploadPreferred = ((BaseUnityPlugin)this).Config.Bind<bool>("RpcTrace", "HttpUploadPreferred", true, SyncedDescription("Uses ValheimTracer-issued HTTP upload tokens for trace batches when the server supports it.")); } private static ConfigDescription SyncedDescription(string description) { //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) //IL_000d: Expected O, but got Unknown //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown ConfigurationManagerAttributes val = new ConfigurationManagerAttributes { IsAdminOnly = true }; return new ConfigDescription(description, (AcceptableValueBase)null, new object[1] { val }); } private static void OnConfigurationSynchronized(object sender, ConfigurationSynchronizationEventArgs args) { if (args.UpdatedPluginGUIDs != null && args.UpdatedPluginGUIDs.Contains("warpalicious.PraetorisClient")) { string text = (args.InitialSynchronization ? "initial" : "updated"); Log.LogInfo((object)("Jotunn synchronized PraetorisClient configuration (" + text + ").")); } } private void SetupWatcher() { try { _lastReloadTime = DateTime.Now; _configWatcher?.Dispose(); _configWatcher = new FileSystemWatcher(Paths.ConfigPath, "warpalicious.PraetorisClient.cfg"); _configWatcher.Changed += ReadConfigValues; _configWatcher.Created += ReadConfigValues; _configWatcher.Renamed += ReadConfigValues; _configWatcher.IncludeSubdirectories = true; _configWatcher.EnableRaisingEvents = true; } catch (Exception ex) { Log.LogWarning((object)("Failed to start configuration watcher: " + ex.Message)); } } private void ReadConfigValues(object sender, FileSystemEventArgs e) { DateTime now = DateTime.Now; long num = now.Ticks - _lastReloadTime.Ticks; if (File.Exists(Path.Combine(Paths.ConfigPath, "warpalicious.PraetorisClient.cfg")) && num >= 10000000) { try { Log.LogInfo((object)"Reloading configuration."); ((BaseUnityPlugin)this).Config.Reload(); } catch (Exception ex) { Log.LogError((object)("Failed to reload configuration: " + ex.Message)); } _lastReloadTime = now; } } } internal static class PraetorisClientRpc { private static ZRoutedRpc? _registeredRpc; public static void Register() { if (ZRoutedRpc.instance != null && _registeredRpc != ZRoutedRpc.instance) { _registeredRpc = ZRoutedRpc.instance; ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_LinkRequest", (Action<long, ZPackage>)LinkRpc.OnRequest); ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_LinkResult", (Action<long, ZPackage>)LinkRpc.OnResult); ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_CreativeInventoryRequest", (Action<long, ZPackage>)CreativeInventoryRpc.OnRequest); ZRoutedRpc.instance.Register<ZPackage>("DiscordTools_CreativeBiomeOverride", (Action<long, ZPackage>)CreativeBiomeOverride.OnOverride); ZRoutedRpc.instance.Register<ZPackage>("PraetorisClient_CreativeCommandZoneState", (Action<long, ZPackage>)CreativeCommandZoneState.OnState); ZRoutedRpc.instance.Register<ZPackage>("PraetorisClient_RpcTraceClockResponse", (Action<long, ZPackage>)RpcTraceTelemetry.OnClockResponse); ZRoutedRpc.instance.Register<ZPackage>("PraetorisClient_RpcTraceUploadTokenResponse", (Action<long, ZPackage>)RpcTraceUploadTokenClient.OnTokenResponse); PraetorisClientPlugin.Log.LogInfo((object)"Registered PraetorisClient RPC handlers."); } } } internal static class RpcNames { public const string LinkRequest = "DiscordTools_LinkRequest"; public const string LinkResult = "DiscordTools_LinkResult"; public const string CreativeInventoryRequest = "DiscordTools_CreativeInventoryRequest"; public const string CreativeInventoryResponse = "DiscordTools_CreativeInventoryResponse"; public const string CreativeBiomeOverride = "DiscordTools_CreativeBiomeOverride"; public const string CreativeCommandZoneState = "PraetorisClient_CreativeCommandZoneState"; public const string SiegePortalEnter = "DiscordTools_SiegePortalEnter"; public const string ValheimEventsTelemetry = "PraetorisClient_ValheimEventsTelemetry"; public const string RpcTraceClockRequest = "PraetorisClient_RpcTraceClockRequest"; public const string RpcTraceClockResponse = "PraetorisClient_RpcTraceClockResponse"; public const string RpcTraceUploadTokenRequest = "PraetorisClient_RpcTraceUploadTokenRequest"; public const string RpcTraceUploadTokenResponse = "PraetorisClient_RpcTraceUploadTokenResponse"; } internal static class RpcTraceFlushCoordinator { private const float UploadUnavailableLogIntervalSeconds = 30f; private static bool _allowQuit; private static float _nextUploadUnavailableLogTime; internal static void Initialize() { Application.wantsToQuit -= OnWantsToQuit; Application.wantsToQuit += OnWantsToQuit; _allowQuit = false; _nextUploadUnavailableLogTime = 0f; } internal static void Shutdown() { Application.wantsToQuit -= OnWantsToQuit; } internal static void RequestFlush(string reason) { if (RpcTraceTelemetry.IsTracingEnabled()) { if (RpcTraceHttpUploadCoordinator.IsActive()) { RpcTraceHttpUploadCoordinator.RequestFlush(reason); } else { LogHttpUnavailable(reason); } } } internal static void Update() { if (RpcTraceTelemetry.IsTracingEnabled() && !RpcTraceHttpUploadCoordinator.IsActive() && RpcTraceLocalStore.HasPendingFiles()) { LogHttpUnavailable("background"); } } internal static bool ShouldAllowLogout(Game game, bool save, bool changeToStartScene) { if (!RpcTraceTelemetry.IsTracingEnabled()) { return true; } if (RpcTraceHttpUploadCoordinator.IsActive()) { RpcTraceHttpUploadCoordinator.RequestFlush("logout"); } else if (RpcTraceLocalStore.HasPendingFiles()) { LogHttpUnavailable("logout"); } RpcTraceTelemetry.SuppressCaptureUntilDisconnected(); return true; } internal static bool ShouldAllowMenuQuit() { if (!RpcTraceTelemetry.IsTracingEnabled()) { return true; } if (RpcTraceHttpUploadCoordinator.IsActive()) { RpcTraceHttpUploadCoordinator.RequestFlush("quit"); } else if (RpcTraceLocalStore.HasPendingFiles()) { LogHttpUnavailable("quit"); } RpcTraceTelemetry.DisableCaptureForShutdown(); return true; } private static bool OnWantsToQuit() { if (_allowQuit) { return true; } if (!RpcTraceTelemetry.IsTracingEnabled()) { return true; } if (RpcTraceHttpUploadCoordinator.IsActive()) { RpcTraceHttpUploadCoordinator.RequestFlush("quit"); } else if (RpcTraceLocalStore.HasPendingFiles()) { LogHttpUnavailable("quit"); } RpcTraceTelemetry.DisableCaptureForShutdown(); _allowQuit = true; return true; } private static void LogHttpUnavailable(string reason) { if (!(Time.realtimeSinceStartup < _nextUploadUnavailableLogTime)) { _nextUploadUnavailableLogTime = Time.realtimeSinceStartup + 30f; PraetorisClientPlugin.Log.LogWarning((object)("RPC trace HTTP upload is unavailable during " + (string.IsNullOrWhiteSpace(reason) ? "flush" : reason) + "; keeping local trace files for later HTTP retry.")); } } } internal static class RpcTraceHttpUploadCoordinator { private const int MaxHttpRowsPerBatch = 5000; private const float FailureRetrySeconds = 15f; private static readonly object Sync = new object(); private static readonly Queue<string> PendingFiles = new Queue<string>(); private static bool _flushRequested; private static string _flushReason = "background"; private static bool _uploading; private static float _nextUploadTime; internal static void Initialize() { lock (Sync) { PendingFiles.Clear(); _flushRequested = false; _flushReason = "background"; _uploading = false; _nextUploadTime = 0f; } } internal static void Shutdown() { lock (Sync) { PendingFiles.Clear(); _uploading = false; _flushRequested = false; } } internal static bool IsActive() { if (PraetorisClientPlugin.RpcTraceHttpUploadPreferred.Value && RpcTraceUploadTokenClient.HasUsableToken()) { return CanUpload(); } return false; } internal static void RequestFlush(string reason) { lock (Sync) { _flushRequested = true; _flushReason = (string.IsNullOrWhiteSpace(reason) ? "manual" : reason); _nextUploadTime = 0f; } } internal static void Update() { if (!IsActive()) { return; } lock (Sync) { if (_uploading || (!_flushRequested && PendingFiles.Count == 0 && Time.realtimeSinceStartup < _nextUploadTime)) { return; } } PrepareFilesIfNeeded(); StartNextUploadIfReady(); } private static void PrepareFilesIfNeeded() { lock (Sync) { if (PendingFiles.Count > 0 || _uploading) { return; } foreach (string flushableFile in RpcTraceLocalStore.GetFlushableFiles()) { if (new FileInfo(flushableFile).Length == 0L) { RpcTraceLocalStore.DeleteFile(flushableFile); } else { PendingFiles.Enqueue(flushableFile); } } _flushRequested = false; _nextUploadTime = Time.realtimeSinceStartup + RpcTraceUploadTokenClient.FlushIntervalSeconds; } } private static void StartNextUploadIfReady() { string path; string flushReason; lock (Sync) { if (_uploading || PendingFiles.Count == 0) { return; } path = PendingFiles.Dequeue(); flushReason = _flushReason; _uploading = true; } if ((Object)(object)PraetorisClientPlugin.Instance != (Object)null) { ((MonoBehaviour)PraetorisClientPlugin.Instance).StartCoroutine(UploadFile(path, flushReason)); } else { RequeueUpload(path); } } private static IEnumerator UploadFile(string path, string flushReason) { string fileId = RpcTraceLocalStore.BuildFileId(path); int startLine = 0; int batchIndex = 0; while (IsActive() && File.Exists(path)) { bool reachedEnd; List<string> rows = RpcTraceLocalStore.ReadBatch(path, startLine, 5000, out reachedEnd); if (rows.Count == 0 && reachedEnd) { RpcTraceLocalStore.DeleteFile(path); CompleteUpload(success: true); yield break; } bool finalBatch; byte[] array = BuildBodyWithinLimit(rows, reachedEnd, out finalBatch); if (array.Length == 0 || rows.Count == 0) { PraetorisClientPlugin.Log.LogWarning((object)("Skipping oversized HTTP RPC trace row in " + fileId + ".")); startLine++; continue; } string batchId = fileId + "-" + batchIndex.ToString("D6"); UnityWebRequest request = new UnityWebRequest(RpcTraceUploadTokenClient.EndpointUrl, "POST") { uploadHandler = (UploadHandler)new UploadHandlerRaw(array), downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(), timeout = 30 }; try { request.SetRequestHeader("Authorization", "Bearer " + RpcTraceUploadTokenClient.Token); request.SetRequestHeader("Content-Type", "application/x-ndjson"); request.SetRequestHeader("Content-Encoding", "gzip"); request.SetRequestHeader("X-Trace-Batch-Id", batchId); request.SetRequestHeader("X-Trace-File-Id", fileId); request.SetRequestHeader("X-Trace-Batch-Index", batchIndex.ToString()); request.SetRequestHeader("X-Trace-Final-Batch", finalBatch ? "true" : "false"); request.SetRequestHeader("X-Trace-Flush-Reason", flushReason ?? ""); request.SetRequestHeader("User-Agent", "PraetorisClient/0.1.8 ValheimTracerHttpUpload"); yield return request.SendWebRequest(); if ((int)request.result != 1 || request.responseCode < 200 || request.responseCode >= 300) { string text = ((request.downloadHandler != null) ? request.downloadHandler.text : ""); string text2 = (string.IsNullOrWhiteSpace(text) ? request.error : text); if (!RpcTraceUploadTokenClient.ShouldRetryUpload(request.responseCode, text2)) { CompleteUpload(success: false); yield break; } PraetorisClientPlugin.Log.LogWarning((object)$"HTTP RPC trace upload failed for {batchId}: HTTP {request.responseCode} {text2}"); RequeueUpload(path); yield break; } startLine += rows.Count; batchIndex++; if (finalBatch) { PraetorisClientPlugin.Log.LogInfo((object)("Uploaded RPC trace file " + fileId + " over HTTP; deleting local copy.")); RpcTraceLocalStore.DeleteFile(path); CompleteUpload(success: true); yield break; } } finally { ((IDisposable)request)?.Dispose(); } } RequeueUpload(path); } private static byte[] BuildBodyWithinLimit(List<string> rows, bool reachedEnd, out bool finalBatch) { finalBatch = reachedEnd; int num = Math.Max(4096, RpcTraceUploadTokenClient.MaxBatchBytes); while (rows.Count > 0) { byte[] array = GzipRows(rows); if (array.Length <= num) { return array; } if (rows.Count == 1) { rows.Clear(); finalBatch = false; return Array.Empty<byte>(); } rows.RemoveAt(rows.Count - 1); finalBatch = false; } return Array.Empty<byte>(); } private static byte[] GzipRows(IReadOnlyList<string> rows) { using MemoryStream memoryStream = new MemoryStream(); using (GZipStream stream = new GZipStream(memoryStream, CompressionLevel.Fastest, leaveOpen: true)) { using StreamWriter streamWriter = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); foreach (string row in rows) { streamWriter.WriteLine(row); } } return memoryStream.ToArray(); } private static void RequeueUpload(string path) { lock (Sync) { if (File.Exists(path)) { PendingFiles.Enqueue(path); } _uploading = false; _nextUploadTime = Time.realtimeSinceStartup + 15f; } } private static void CompleteUpload(bool success) { lock (Sync) { _uploading = false; _nextUploadTime = Time.realtimeSinceStartup + (success ? RpcTraceUploadTokenClient.FlushIntervalSeconds : 15f); } } private static bool CanUpload() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Invalid comparison between Unknown and I4 if ((Object)(object)ZNet.instance != (Object)null && ZRoutedRpc.instance != null && !ZNet.instance.IsServer()) { return (int)ZNet.GetConnectionStatus() == 2; } return false; } } internal static class RpcTraceLocalStore { private static readonly object Sync = new object(); private static string _pendingDirectory = ""; private static string? _currentPath; private static StreamWriter? _writer; internal static void Initialize() { _pendingDirectory = Path.Combine(Paths.BepInExRootPath, "logs", "PraetorisClient", "RpcTrace", "pending"); Directory.CreateDirectory(_pendingDirectory); } internal static void Append(string line, long localPeerId) { if (string.IsNullOrWhiteSpace(line)) { return; } lock (Sync) { EnsureWriterLocked(localPeerId); if (_writer != null) { _writer.WriteLine(line); _writer.Flush(); } } } internal static void CloseCurrentFile() { lock (Sync) { _writer?.Flush(); _writer?.Dispose(); _writer = null; _currentPath = null; } } internal static List<string> GetFlushableFiles() { lock (Sync) { CloseCurrentFile(); List<string> list = new List<string>(Directory.GetFiles(_pendingDirectory, "*.jsonl")); list.Sort(StringComparer.Ordinal); return list; } } internal static List<string> ReadBatch(string path, int startLine, int maxRows, out bool reachedEnd) { List<string> list = new List<string>(Math.Max(1, maxRows)); reachedEnd = true; using StreamReader streamReader = new StreamReader(path, Encoding.UTF8); int num = 0; while (true) { string text = streamReader.ReadLine(); if (text == null) { break; } if (num++ >= startLine) { if (list.Count >= maxRows) { reachedEnd = false; break; } if (text.Length > 0) { list.Add(text); } } } return list; } internal static void DeleteFile(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { PraetorisClientPlugin.Log.LogWarning((object)("Failed to delete uploaded RPC trace file " + path + ": " + ex.Message)); } } internal static bool HasPendingFiles() { lock (Sync) { if (_writer != null) { return true; } return Directory.Exists(_pendingDirectory) && Directory.GetFiles(_pendingDirectory, "*.jsonl").Length != 0; } } internal static string BuildFileId(string path) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); if (!string.IsNullOrEmpty(fileNameWithoutExtension)) { return fileNameWithoutExtension; } return Guid.NewGuid().ToString("N"); } private static void EnsureWriterLocked(long localPeerId) { if (_writer == null) { Directory.CreateDirectory(_pendingDirectory); long num = ((ZNet.m_world != null) ? ZNet.m_world.m_uid : 0); string text = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss_fff", CultureInfo.InvariantCulture); string path = "rpc_trace_" + text + "_world_" + num.ToString(CultureInfo.InvariantCulture) + "_peer_" + localPeerId.ToString(CultureInfo.InvariantCulture) + "_" + Guid.NewGuid().ToString("N") + ".jsonl"; _currentPath = Path.Combine(_pendingDirectory, path); _writer = new StreamWriter(_currentPath, append: true, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)) { AutoFlush = true }; } } } [HarmonyPatch(typeof(ZRoutedRpc), "InvokeRoutedRPC", new Type[] { typeof(long), typeof(ZDOID), typeof(string), typeof(object[]) })] internal static class RpcTraceInvokeNamePatch { private static void Prefix(string methodName) { RpcTraceTelemetry.RegisterRpcName(methodName); } } [HarmonyPatch(typeof(ZRpc), "Invoke")] internal static class RpcTraceSendPatch { private static void Prefix(ZRpc __instance, string method, object[] parameters) { if (!(method != "RoutedRPC") && parameters != null && parameters.Length != 0) { object obj = parameters[0]; ZPackage val = (ZPackage)((obj is ZPackage) ? obj : null); if (val != null) { RoutedRPCData data = RpcTraceTelemetry.TryReadRoutedRpcData(val); RpcTraceTelemetry.TraceRoutedRpc("rpc_send", data, RpcTraceTelemetry.GetPeerIdForRpc(__instance)); } } } } [HarmonyPatch(typeof(ZRoutedRpc), "RPC_RoutedRPC")] internal static class RpcTraceReceivePatch { private static void Prefix(ZPackage pkg) { RoutedRPCData data = RpcTraceTelemetry.TryReadRoutedRpcData(pkg); RpcTraceTelemetry.TraceRoutedRpc("rpc_receive", data, (ZDOMan.instance != null) ? ZDOMan.GetSessionID() : 0); } } [HarmonyPatch(typeof(ZRoutedRpc), "HandleRoutedRPC")] internal static class RpcTraceHandlePatch { private static void Prefix(RoutedRPCData data) { RpcTraceTelemetry.TraceRoutedRpc("rpc_handle", data, (ZDOMan.instance != null) ? ZDOMan.GetSessionID() : 0); } } internal static class RpcTraceTelemetry { private readonly struct ClockSample { internal double OffsetMs { get; } internal double RoundTripMs { get; } internal double Realtime { get; } internal ClockSample(double offsetMs, double roundTripMs, double realtime) { OffsetMs = offsetMs; RoundTripMs = roundTripMs; Realtime = realtime; } } internal const int ProtocolVersion = 2; internal const string Schema = "valheim.trace.rpc.v1"; private const float ClockSyncIntervalSeconds = 10f; private const int ClockSampleWindow = 9; private const double MaxClockRoundTripMs = 2000.0; private const double MaxClockOffsetJumpMs = 250.0; private static readonly object Sync = new object(); private static readonly Dictionary<int, string> RpcNamesByHash = new Dictionary<int, string>(); private static readonly List<ClockSample> ClockSamples = new List<ClockSample>(); private static HashSet<string> _denyList = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static string _lastDenyListConfig = ""; private static float _nextClockSyncTime; private static long _sequence; private static int _clockSequence; private static double _lastServerMinusClientOffsetMs; private static double _selectedClockRoundTripMs; private static double _lastGoodClockSyncRealtime; private static int _validClockSampleCount; private static bool _hasClockOffset; private static bool _shutdownCapture; private static bool _suppressCaptureUntilDisconnected; internal static void Initialize() { _shutdownCapture = false; _suppressCaptureUntilDisconnected = false; RpcTraceLocalStore.Initialize(); RpcTraceUploadTokenClient.Initialize(); RpcTraceHttpUploadCoordinator.Initialize(); RpcTraceFlushCoordinator.Initialize(); } internal static void Shutdown() { _shutdownCapture = true; RpcTraceHttpUploadCoordinator.Shutdown(); RpcTraceFlushCoordinator.Shutdown(); RpcTraceLocalStore.CloseCurrentFile(); } internal static void DisableCaptureForShutdown() { _shutdownCapture = true; RpcTraceLocalStore.CloseCurrentFile(); } internal static void SuppressCaptureUntilDisconnected() { _suppressCaptureUntilDisconnected = true; RpcTraceLocalStore.CloseCurrentFile(); } internal static void RegisterRpcName(string methodName) { if (string.IsNullOrEmpty(methodName)) { return; } lock (Sync) { RpcNamesByHash[StringExtensionMethods.GetStableHashCode(methodName)] = methodName; } } internal static void TraceRoutedRpc(string eventType, RoutedRPCData? data, long receiverPeerId) { //IL_00e8: Unknown result type (might be due to invalid IL or missing references) if (!CanCapture() || data == null || IsTraceRpc(data) || (!PraetorisClientPlugin.RpcTraceCaptureSendReceive.Value && eventType != "rpc_handle")) { return; } try { RefreshDenyList(); string rpcName = GetRpcName(data.m_methodHash); if (!IsDenied(rpcName)) { long localPeerId = GetLocalPeerId(); TelemetryJson telemetryJson = ObjectWithEnvelope(eventType, localPeerId); telemetryJson.Prop("role", "client"); telemetryJson.Prop("rpcTraceId", BuildRpcTraceId(data.m_senderPeerID, data.m_msgID)); telemetryJson.Prop("msgId", data.m_msgID); telemetryJson.Prop("senderPeerId", data.m_senderPeerID); telemetryJson.Prop("receiverPeerId", receiverPeerId); telemetryJson.Prop("targetPeerId", data.m_targetPeerID); telemetryJson.Prop("methodHash", data.m_methodHash); telemetryJson.Prop("rpcName", rpcName); telemetryJson.Prop("targetZdo", ClientZdoSnapshot.FormatId(data.m_targetZDO)); telemetryJson.Prop("payloadBytes", (data.m_parameters != null) ? data.m_parameters.Size() : 0); telemetryJson.Prop("serverMinusClientOffsetMs", _lastServerMinusClientOffsetMs); telemetryJson.Prop("clockRoundTripMs", _selectedClockRoundTripMs); telemetryJson.Prop("clockOffsetQuality", GetClockOffsetQuality()); telemetryJson.Prop("clockOffsetAgeMs", GetClockOffsetAgeMs()); telemetryJson.Prop("clockSampleCount", _validClockSampleCount); telemetryJson.End(); RpcTraceLocalStore.Append(telemetryJson.ToString(), localPeerId); } } catch (Exception ex) { PraetorisClientPlugin.Log.LogWarning((object)("Failed to capture RPC trace row: " + ex.GetType().Name + ": " + ex.Message)); } } internal static RoutedRPCData? TryReadRoutedRpcData(ZPackage? package) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Expected O, but got Unknown if (package == null) { return null; } try { ZPackage val = new ZPackage(package.GetArray()); RoutedRPCData val2 = new RoutedRPCData(); val2.Deserialize(val); return val2; } catch (Exception ex) { PraetorisClientPlugin.Log.LogWarning((object)("Failed to read routed RPC trace package: " + ex.GetType().Name + ": " + ex.Message)); return null; } } internal static long GetPeerIdForRpc(ZRpc rpc) { if ((Object)(object)ZNet.instance == (Object)null) { return 0L; } foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer != null && peer.m_rpc == rpc) { return peer.m_uid; } } return 0L; } internal static void Update() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 if (!IsTracingEnabled()) { RpcTraceLocalStore.CloseCurrentFile(); return; } if (_suppressCaptureUntilDisconnected && (int)ZNet.GetConnectionStatus() != 2) { _suppressCaptureUntilDisconnected = false; } MaybeSendClockSyncRequest(); RpcTraceUploadTokenClient.Update(); RpcTraceHttpUploadCoordinator.Update(); RpcTraceFlushCoordinator.Update(); } internal static void OnClockResponse(long sender, ZPackage package) { if (!IsTracingEnabled()) { return; } long ticks = DateTime.UtcNow.Ticks; double realtimeSinceStartupAsDouble = Time.realtimeSinceStartupAsDouble; try { int num = package.ReadInt(); int value = package.ReadInt(); long value2 = package.ReadLong(); long value3 = package.ReadLong(); long num2 = package.ReadLong(); double num3 = package.ReadDouble(); long num4 = package.ReadLong(); double num5 = package.ReadDouble(); long num6 = package.ReadLong(); double num7 = package.ReadDouble(); if (num == 2) { double num8 = (realtimeSinceStartupAsDouble - num3) * 1000.0; double num9 = (num7 - num5) * 1000.0; double num10 = num8 - num9; double num11 = (TicksToMilliseconds(num4 - num2) + TicksToMilliseconds(num6 - ticks)) / 2.0; bool value4 = UpdateClockOffset(num11, num10, realtimeSinceStartupAsDouble); long localPeerId = GetLocalPeerId(); TelemetryJson telemetryJson = ObjectWithEnvelope("clock_sync_sample", localPeerId); telemetryJson.Prop("role", "client"); telemetryJson.Prop("senderPeerId", sender); telemetryJson.Prop("clientPeerId", value2); telemetryJson.Prop("serverPeerId", value3); telemetryJson.Prop("clockSequence", value); telemetryJson.Prop("clientSendUtcTicks", num2); telemetryJson.Prop("serverReceiveUtcTicks", num4); telemetryJson.Prop("serverSendUtcTicks", num6); telemetryJson.Prop("clientReceiveUtcTicks", ticks); telemetryJson.Prop("clientSendRealtime", num3); telemetryJson.Prop("serverReceiveRealtime", num5); telemetryJson.Prop("serverSendRealtime", num7); telemetryJson.Prop("clientReceiveRealtime", realtimeSinceStartupAsDouble); telemetryJson.Prop("roundTripMs", num10); telemetryJson.Prop("serverProcessingMs", num9); telemetryJson.Prop("sampleServerMinusClientOffsetMs", num11); telemetryJson.Prop("serverMinusClientOffsetMs", _lastServerMinusClientOffsetMs); telemetryJson.Prop("clockSampleAccepted", value4); telemetryJson.Prop("clockOffsetQuality", GetClockOffsetQuality()); telemetryJson.Prop("clockOffsetAgeMs", GetClockOffsetAgeMs()); telemetryJson.Prop("clockSampleCount", _validClockSampleCount); telemetryJson.End(); RpcTraceLocalStore.Append(telemetryJson.ToString(), localPeerId); } } catch (Exception ex) { PraetorisClientPlugin.Log.LogWarning((object)("Failed to process RPC trace clock response: " + ex.GetType().Name + ": " + ex.Message)); } } internal static bool ShouldAllowLogout(Game game, bool save, bool changeToStartScene) { return RpcTraceFlushCoordinator.ShouldAllowLogout(game, save, changeToStartScene); } internal static bool ShouldAllowMenuQuit() { return RpcTraceFlushCoordinator.ShouldAllowMenuQuit(); } internal static void OnApplicationQuitFallback() { RpcTraceFlushCoordinator.RequestFlush("application_quit"); } internal static bool IsTracingEnabled() { return PraetorisClientPlugin.RpcTraceEnabled.Value; } private static TelemetryJson ObjectWithEnvelope(string eventType, long localPeerId) { DateTime utcNow = DateTime.UtcNow; long value = ++_sequence; TelemetryJson telemetryJson = TelemetryJson.Object(); telemetryJson.Prop("schema", "valheim.trace.rpc.v1"); telemetryJson.Prop("eventType", eventType); telemetryJson.Prop("traceId", "client:" + localPeerId.ToString(CultureInfo.InvariantCulture) + ":" + value.ToString(CultureInfo.InvariantCulture)); telemetryJson.Prop("sequence", value); telemetryJson.Prop("localPeerId", localPeerId); telemetryJson.Prop("timeUtc", utcNow.ToString("o", CultureInfo.InvariantCulture)); telemetryJson.Prop("timeUtcTicks", utcNow.Ticks); telemetryJson.Prop("realtime", Time.realtimeSinceStartupAsDouble); telemetryJson.Prop("worldName", (ZNet.m_world != null) ? ZNet.m_world.m_name : ""); telemetryJson.Prop("worldUid", (ZNet.m_world != null) ? ZNet.m_world.m_uid : 0); return telemetryJson; } private static bool CanCapture() { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Invalid comparison between Unknown and I4 if (IsTracingEnabled() && !_shutdownCapture && !_suppressCaptureUntilDisconnected && (Object)(object)ZNet.instance != (Object)null && ZRoutedRpc.instance != null && !ZNet.instance.IsServer()) { return (int)ZNet.GetConnectionStatus() == 2; } return false; } private static void MaybeSendClockSyncRequest() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown if (CanCapture() && !(Time.realtimeSinceStartup < _nextClockSyncTime) && ZRoutedRpc.instance != null) { _nextClockSyncTime = Time.realtimeSinceStartup + 10f; int num = ++_clockSequence; ZPackage val = new ZPackage(); val.Write(2); val.Write(num); val.Write(GetLocalPeerId()); val.Write(DateTime.UtcNow.Ticks); val.Write(Time.realtimeSinceStartupAsDouble); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "PraetorisClient_RpcTraceClockRequest", new object[1] { val }); } } private static bool IsTraceRpc(RoutedRPCData data) { if (data.m_methodHash != StringExtensionMethods.GetStableHashCode("PraetorisClient_RpcTraceClockRequest") && data.m_methodHash != StringExtensionMethods.GetStableHashCode("PraetorisClient_RpcTraceClockResponse") && data.m_methodHash != StringExtensionMethods.GetStableHashCode("PraetorisClient_RpcTraceUploadTokenRequest")) { return data.m_methodHash == StringExtensionMethods.GetStableHashCode("PraetorisClient_RpcTraceUploadTokenResponse"); } return true; } private static string GetRpcName(int methodHash) { lock (Sync) { string value; return RpcNamesByHash.TryGetValue(methodHash, out value) ? value : ""; } } private static bool IsDenied(string rpcName) { if (!string.IsNullOrEmpty(rpcName)) { return _denyList.Contains(rpcName); } return false; } private static void RefreshDenyList() { string text = PraetorisClientPlugin.RpcTraceNameDenyList.Value ?? ""; if (string.Equals(text, _lastDenyListConfig, StringComparison.Ordinal)) { return; } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); string[] array = text.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length > 0) { hashSet.Add(text2); } } _denyList = hashSet; _lastDenyListConfig = text; } internal static long GetLocalPeerId() { try { return (ZDOMan.instance != null) ? ZDOMan.GetSessionID() : 0; } catch { return 0L; } } private static double TicksToMilliseconds(long ticks) { return (double)ticks / 10000.0; } private static string BuildRpcTraceId(long senderPeerId, long msgId) { long num = ((ZNet.m_world != null) ? ZNet.m_world.m_uid : 0); return num.ToString(CultureInfo.InvariantCulture) + ":" + senderPeerId.ToString(CultureInfo.InvariantCulture) + ":" + msgId.ToString(CultureInfo.InvariantCulture); } private static bool UpdateClockOffset(double offsetMs, double roundTripMs, double realtime) { if (roundTripMs < 0.0 || roundTripMs > 2000.0 || double.IsNaN(offsetMs) || double.IsInfinity(offsetMs)) { return false; } if (ClockSamples.Count >= 3) { double medianOffset = GetMedianOffset(); if (Math.Abs(offsetMs - medianOffset) > 250.0) { return false; } } ClockSamples.Add(new ClockSample(offsetMs, roundTripMs, realtime)); while (ClockSamples.Count > 9) { ClockSamples.RemoveAt(0); } List<ClockSample> list = new List<ClockSample>(ClockSamples); list.Sort((ClockSample left, ClockSample right) => left.RoundTripMs.CompareTo(right.RoundTripMs)); int num = Math.Min(5, list.Count); double num2 = 0.0; for (int num3 = 0; num3 < num; num3++) { num2 += list[num3].OffsetMs; } _lastServerMinusClientOffsetMs = num2 / (double)num; _selectedClockRoundTripMs = list[0].RoundTripMs; _lastGoodClockSyncRealtime = realtime; _validClockSampleCount++; _hasClockOffset = true; return true; } private static double GetMedianOffset() { List<double> list = new List<double>(ClockSamples.Count); foreach (ClockSample clockSample in ClockSamples) { list.Add(clockSample.OffsetMs); } list.Sort(); int num = list.Count / 2; if (list.Count % 2 == 1) { return list[num]; } return (list[num - 1] + list[num]) / 2.0; } private static string GetClockOffsetQuality() { if (!_hasClockOffset) { return "none"; } if (GetClockOffsetAgeMs() > 30000.0) { return "stale"; } if (ClockSamples.Count < 3) { return "warming"; } return "good"; } private static double GetClockOffsetAgeMs() { if (!_hasClockOffset) { return 0.0; } return Math.Max(0.0, (Time.realtimeSinceStartupAsDouble - _lastGoodClockSyncRealtime) * 1000.0); } } internal static class RpcTraceUploadTokenClient { private const float RequestRetrySeconds = 30f; private const float ConfigurationRetrySeconds = 300f; private const long RefreshBeforeExpirySeconds = 60L; private static string _sessionId = ""; private static float _nextRequestTime; private static float _requestDeadlineTime; private static bool _requestPending; internal static bool UploadEnabled { get; private set; } internal static string EndpointUrl { get; private set; } = ""; internal static string Token { get; private set; } = ""; internal static int MaxBatchBytes { get; private set; } = 131072; internal static float FlushIntervalSeconds { get; private set; } = 10f; internal static long TokenExpiresUnixSeconds { get; private set; } internal static string SessionId { get { EnsureSessionId(); return _sessionId; } } internal static void Initialize() { EnsureSessionId(); UploadEnabled = false; EndpointUrl = ""; Token = ""; TokenExpiresUnixSeconds = 0L; _requestPending = false; _nextRequestTime = 0f; } internal static void Update() { if (!PraetorisClientPlugin.RpcTraceHttpUploadPreferred.Value || !RpcTraceTelemetry.IsTracingEnabled()) { return; } if (!CanRequestToken()) { if (_requestPending && Time.realtimeSinceStartup > _requestDeadlineTime) { _requestPending = false; } } else if (!HasUsableToken() && !(Time.realtimeSinceStartup < _nextRequestTime)) { RequestToken(); } } internal static bool HasUsableToken() { long num = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (UploadEnabled && !string.IsNullOrWhiteSpace(EndpointUrl) && !string.IsNullOrWhiteSpace(Token)) { return TokenExpiresUnixSeconds - num > 60; } return false; } internal static bool ShouldRetryUpload(long responseCode, string responseText) { if (!IsConfigurationFailure(responseCode, responseText)) { return true; } UploadEnabled = false; EndpointUrl = ""; Token = ""; TokenExpiresUnixSeconds = 0L; _requestPending = false; _nextRequestTime = Time.realtimeSinceStartup + 300f; PraetorisClientPlugin.Log.LogWarning((object)("HTTP RPC trace upload rejected by receiver configuration: " + (string.IsNullOrWhiteSpace(responseText) ? ("HTTP " + responseCode) : responseText) + ". Keeping local trace files and pausing token requests.")); return false; } internal static void OnTokenResponse(long sender, ZPackage package) { try { int num = package.ReadInt(); bool flag = package.ReadBool(); string text = package.ReadString(); string endpointUrl = package.ReadString(); string token = package.ReadString(); string text2 = package.ReadString(); int val = package.ReadInt(); float val2 = package.ReadSingle(); long tokenExpiresUnixSeconds = package.ReadLong(); if (num != 2) { return; } _requestPending = false; _nextRequestTime = Time.realtimeSinceStartup + 30f; if (!flag) { UploadEnabled = false; EndpointUrl = ""; Token = ""; TokenExpiresUnixSeconds = 0L; if (!string.IsNullOrWhiteSpace(text)) { PraetorisClientPlugin.Log.LogInfo((object)("HTTP RPC trace upload unavailable: " + text)); } return; } if (!string.IsNullOrWhiteSpace(text2)) { _sessionId = text2; } UploadEnabled = true; EndpointUrl = endpointUrl; Token = token; MaxBatchBytes = Math.Max(4096, val); FlushIntervalSeconds = Math.Max(1f, val2); TokenExpiresUnixSeconds = tokenExpiresUnixSeconds; PraetorisClientPlugin.Log.LogInfo((object)("Received HTTP RPC trace upload token for session " + _sessionId + " expiring at " + tokenExpiresUnixSeconds.ToString(CultureInfo.InvariantCulture) + ".")); } catch (Exception ex) { _requestPending = false; PraetorisClientPlugin.Log.LogWarning((object)$"Failed to process RPC trace upload token response from peer {sender}: {ex.Message}"); } } private static void RequestToken() { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance == null) { return; } try { EnsureSessionId(); ZPackage val = new ZPackage(); val.Write(2); val.Write(_sessionId); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "PraetorisClient_RpcTraceUploadTokenRequest", new object[1] { val }); _requestPending = true; _nextRequestTime = Time.realtimeSinceStartup + 30f; _requestDeadlineTime = Time.realtimeSinceStartup + 30f; } catch (Exception ex) { _requestPending = false; _nextRequestTime = Time.realtimeSinceStartup + 30f; PraetorisClientPlugin.Log.LogWarning((object)("Failed to request RPC trace upload token: " + ex.Message)); } } private static bool CanRequestToken() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Invalid comparison between Unknown and I4 if (!_requestPending && (Object)(object)ZNet.instance != (Object)null && ZRoutedRpc.instance != null && !ZNet.instance.IsServer()) { return (int)ZNet.GetConnectionStatus() == 2; } return false; } private static bool IsConfigurationFailure(long responseCode, string responseText) { string text = responseText ?? ""; if (responseCode != 403 && text.IndexOf("missing relay key", StringComparison.OrdinalIgnoreCase) < 0 && text.IndexOf("missing token signing secret", StringComparison.OrdinalIgnoreCase) < 0 && text.IndexOf("unauthorized relay", StringComparison.OrdinalIgnoreCase) < 0 && text.IndexOf("invalid token", StringComparison.OrdinalIgnoreCase) < 0) { return text.IndexOf("failed to read request body", StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static void EnsureSessionId() { if (string.IsNullOrWhiteSpace(_sessionId)) { _sessionId = Guid.NewGuid().ToString("N"); } } } internal static class SiegePortalBridge { private const int ProtocolVersion = 1; internal const string SiegeIdZdoKey = "valheimCreativeSiegeId"; internal const string SiegeTagPrefix = "siege:"; internal static bool TryHandle(TeleportWorld portal, Player player) { //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown //IL_00d4: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer() || ZRoutedRpc.instance == null) { return false; } if ((Object)(object)player == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return false; } ZNetView component = ((Component)portal).GetComponent<ZNetView>(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val == null || !TryGetSiegeId(val, out string siegeId)) { return false; } if ((Object)(object)((Character)player).m_nview == (Object)null || !((Character)player).m_nview.IsValid()) { ((Character)player).Message((MessageType)2, "Siege portal failed: character was not ready.", 0, (Sprite)null); return true; } ZDO zDO = ((Character)player).m_nview.GetZDO(); if (zDO == null) { ((Character)player).Message((MessageType)2, "Siege portal failed: character was not ready.", 0, (Sprite)null); return true; } ZPackage val2 = new ZPackage(); val2.Write(1); val2.Write(zDO.m_uid); val2.Write(siegeId); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "DiscordTools_SiegePortalEnter", new object[1] { val2 }); ((Character)player).Message((MessageType)2, "Entering siege: " + siegeId, 0, (Sprite)null); PraetorisClientPlugin.Log.LogInfo((object)("Requested siege portal enter for " + siegeId + ".")); return true; } private static bool TryGetSiegeId(ZDO portalZdo, out string siegeId) { siegeId = portalZdo.GetString("valheimCreativeSiegeId", "").Trim(); if (!string.IsNullOrWhiteSpace(siegeId)) { return true; } string text = portalZdo.GetString(ZDOVars.s_tag, "").Trim(); if (text.StartsWith("siege:", StringComparison.OrdinalIgnoreCase)) { siegeId = text.Substring("siege:".Length).Trim(); return !string.IsNullOrWhiteSpace(siegeId); } return false; } } [HarmonyPatch(typeof(TeleportWorld), "Teleport")] internal static class TeleportWorldSiegePortalPatch { private static bool Prefix(TeleportWorld __instance, Player player) { return !SiegePortalBridge.TryHandle(__instance, player); } } internal static class SiegePortalTestCommand { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__1_0; public static ConsoleEvent <>9__1_1; internal void <Register>b__1_0(ConsoleEventArgs args) { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) if (args.Length < 2) { args.Context.AddString("Usage: dt_mark_siege_portal <siegeId> [radius]"); return; } string text = args[1].Trim(); if (string.IsNullOrWhiteSpace(text)) { args.Context.AddString("siegeId is required."); return; } float result = 12f; if (args.Length >= 3) { float.TryParse(args[2], NumberStyles.Float, CultureInfo.InvariantCulture, out result); } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { args.Context.AddString("No local player found."); return; } float distance; TeleportWorld val = FindNearestPortal(((Component)localPlayer).transform.position, Mathf.Clamp(result, 1f, 50f), out distance); if ((Object)(object)val == (Object)null) { args.Context.AddString($"No portal found within {result:0.#}m."); return; } ZNetView component = ((Component)val).GetComponent<ZNetView>(); ZDO val2 = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val2 == null) { args.Context.AddString("Nearest portal has no valid ZDO."); return; } val2.Set("valheimCreativeSiegeId", text); val2.Set(ZDOVars.s_tag, "siege:" + text); args.Context.AddString($"Marked nearest portal {val2.m_uid} as siege {text} at {distance:0.#}m."); } internal void <Register>b__1_1(ConsoleEventArgs args) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) float result = 12f; if (args.Length >= 2) { float.TryParse(args[1], NumberStyles.Float, CultureInfo.InvariantCulture, out result); } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { args.Context.AddString("No local player found."); return; } float distance; TeleportWorld val = FindNearestPortal(((Component)localPlayer).transform.position, Mathf.Clamp(result, 1f, 50f), out distance); if ((Object)(object)val == (Object)null) { args.Context.AddString($"No portal found within {result:0.#}m."); return; } bool flag = SiegePortalBridge.TryHandle(val, localPlayer); args.Context.AddString($"Nearest siege portal handled={flag} distance={distance:0.#}m."); } } private static bool _registered; internal static void Register() { //IL_0040: 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_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to i