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 ValheimPlayerCards v0.2.1
ValheimPlayerCards.dll
Decompiled 19 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.Rendering; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ValheimPlayerCards")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.1.0")] [assembly: AssemblyInformationalVersion("0.2.1+1c5c4e8003d2c8c3266ddc9574b80a219acbfce3")] [assembly: AssemblyProduct("ValheimPlayerCards")] [assembly: AssemblyTitle("ValheimPlayerCards")] [assembly: AssemblyVersion("0.2.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ValheimPlayerCards { internal sealed class CardCommand : ConsoleCommand { public override string Name => "playercard"; public override string Help => "Take a new portrait of your character and send your player card to the server now."; public override void Run(string[] args) { ClientSide.ForceNext(); Console instance = Console.instance; if (instance != null) { instance.Print("ValheimPlayerCards: your card will be sent in a moment (the server accepts one every few seconds)."); } } } internal static class ClientSide { private static readonly FieldRef<Humanoid, ItemData> RightItem = Field("m_rightItem"); private static readonly FieldRef<Humanoid, ItemData> LeftItem = Field("m_leftItem"); private static readonly FieldRef<Humanoid, ItemData> ChestItem = Field("m_chestItem"); private static readonly FieldRef<Humanoid, ItemData> LegItem = Field("m_legItem"); private static readonly FieldRef<Humanoid, ItemData> HelmetItem = Field("m_helmetItem"); private static readonly FieldRef<Humanoid, ItemData> ShoulderItem = Field("m_shoulderItem"); private static readonly FieldRef<Humanoid, ItemData> UtilityItem = Field("m_utilityItem"); private static readonly FieldRef<Humanoid, ItemData> TrinketItem = Field("m_trinketItem"); private static readonly FieldRef<Humanoid, ItemData> HiddenLeftItem = Field("m_hiddenLeftItem"); private static readonly FieldRef<Humanoid, ItemData> HiddenRightItem = Field("m_hiddenRightItem"); private static readonly FieldRef<Humanoid, string> BeardItem = AccessTools.FieldRefAccess<Humanoid, string>("m_beardItem"); private static readonly FieldRef<Humanoid, string> HairItem = AccessTools.FieldRefAccess<Humanoid, string>("m_hairItem"); private const float FirstDelay = 8f; private const float Debounce = 3f; private static ZNetPeer _serverPeer; private static bool _serverReady; private static int _serverMaxBytes; private static int _serverMinInterval; private static int _serverMaxSide; private static readonly HashSet<string> ServerIcons = new HashSet<string>(); private static Player _player; private static float _playerSeenAt; private static string _sentSignature; private static string _pendingSignature; private static float _pendingSince; private static float _lastCapture = -10000f; private static float _nextCheck; private static bool _busy; private static bool _forced; private static int _uploadId = Environment.TickCount & 0x3FFFFFFF; private static FieldRef<Humanoid, ItemData> Field(string name) { return AccessTools.FieldRefAccess<Humanoid, ItemData>(name); } public static void RegisterPeer(ZNetPeer peer) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown if (!Plugin.IsHeadless) { _serverPeer = peer; _serverReady = false; peer.m_rpc.Register<ZPackage>("ValheimPlayerCards_Ready", (Action<ZRpc, ZPackage>)OnReady); ZPackage val = new ZPackage(); val.Write(1); peer.m_rpc.Invoke("ValheimPlayerCards_Hello", new object[1] { val }); } } public static void Reset() { _serverPeer = null; _serverReady = false; _player = null; _sentSignature = null; _pendingSignature = null; } private static void OnReady(ZRpc rpc, ZPackage pkg) { try { if (_serverPeer != null && rpc == _serverPeer.m_rpc && pkg.ReadInt() == 1) { _serverMaxBytes = pkg.ReadInt(); _serverMinInterval = pkg.ReadInt(); _serverMaxSide = pkg.ReadInt(); ServerIcons.Clear(); int num = Math.Min(pkg.ReadInt(), 2000); for (int i = 0; i < num; i++) { ServerIcons.Add(pkg.ReadString()); } _serverReady = true; _sentSignature = null; Plugin.Log.LogInfo((object)$"Server accepts player cards (up to {_serverMaxBytes / 1024} KB, one every {_serverMinInterval} s)."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Bad answer from server: " + ex.Message)); } } private static bool CanSend() { if ((Object)(object)ZNet.instance == (Object)null) { return false; } if (ZNet.instance.IsServer()) { return PlayerStore.Active; } if (_serverReady && _serverPeer != null) { return _serverPeer.m_rpc != null; } return false; } private static int Cooldown() { int num = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? Plugin.ServerMinIntervalSeconds.Value : _serverMinInterval); return Math.Max(Plugin.MinIntervalSeconds.Value, num + 2); } private static int ByteLimit() { int num = (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) ? (Plugin.MaxUploadKB.Value * 1024) : _serverMaxBytes); return Math.Min(Plugin.TargetKB.Value * 1024, Math.Max(32768, num - 131072)); } public static void ForceNext() { _forced = true; _sentSignature = null; } public static void Tick() { if (Plugin.IsHeadless || !Plugin.ClientEnabled.Value || _busy) { return; } float time = Time.time; if (time < _nextCheck) { return; } _nextCheck = time + 1f; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || !CanSend()) { return; } if ((Object)(object)localPlayer != (Object)(object)_player) { _player = localPlayer; _playerSeenAt = time; _sentSignature = null; _pendingSignature = null; } if (time - _playerSeenAt < 8f || ((Character)localPlayer).IsDead() || ((Character)localPlayer).IsTeleporting()) { return; } string text; try { text = Signature(localPlayer); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Cannot read equipment: " + ex.Message)); _nextCheck = time + 60f; return; } if (!(text == _sentSignature)) { if (text != _pendingSignature) { _pendingSignature = text; _pendingSince = time; } if ((_forced || !(time - _pendingSince < 3f)) && (_forced || !(time - _lastCapture < (float)Cooldown()))) { _forced = false; _busy = true; _lastCapture = time; ((MonoBehaviour)Plugin.Instance).StartCoroutine(CaptureAndSend(localPlayer, text)); } } } private static string Signature(Player p) { //IL_01b2: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); foreach (var (text, item) in Equipped(p)) { switch (text) { case "right": case "left": case "backRight": case "backLeft": continue; } stringBuilder.Append(text).Append('=').Append(ItemKey(item)) .Append(';'); } List<string> list = new List<string> { ItemKey(RightItem.Invoke((Humanoid)(object)p)), ItemKey(LeftItem.Invoke((Humanoid)(object)p)), ItemKey(HiddenRightItem.Invoke((Humanoid)(object)p)), ItemKey(HiddenLeftItem.Invoke((Humanoid)(object)p)) }; list.Sort(StringComparer.Ordinal); stringBuilder.Append("weapons=").Append(string.Join(",", list)).Append(';'); stringBuilder.Append("hair=").Append(HairItem.Invoke((Humanoid)(object)p)).Append(";beard=") .Append(BeardItem.Invoke((Humanoid)(object)p)) .Append(';'); ZNetView component = ((Component)p).GetComponent<ZNetView>(); ZDO val = ((component != null) ? component.GetZDO() : null); if (val != null) { stringBuilder.Append("model=").Append(val.GetInt(ZDOVars.s_modelIndex, 0)).Append(';'); stringBuilder.Append("skin=").Append(val.GetVec3(ZDOVars.s_skinColor, Vector3.one)).Append(';'); stringBuilder.Append("haircolor=").Append(val.GetVec3(ZDOVars.s_hairColor, Vector3.one)).Append(';'); } return stringBuilder.ToString(); } private static string ItemKey(ItemData item) { if (item == null) { return ""; } string arg = (((Object)(object)item.m_dropPrefab != (Object)null) ? ((Object)item.m_dropPrefab).name : item.m_shared?.m_name); return $"{arg}:{item.m_quality}:{item.m_variant}"; } private static IEnumerable<(string, ItemData)> Equipped(Player p) { yield return ("helmet", HelmetItem.Invoke((Humanoid)(object)p)); yield return ("chest", ChestItem.Invoke((Humanoid)(object)p)); yield return ("legs", LegItem.Invoke((Humanoid)(object)p)); yield return ("cape", ShoulderItem.Invoke((Humanoid)(object)p)); yield return ("utility", UtilityItem.Invoke((Humanoid)(object)p)); yield return ("trinket", TrinketItem.Invoke((Humanoid)(object)p)); yield return ("right", RightItem.Invoke((Humanoid)(object)p)); yield return ("left", LeftItem.Invoke((Humanoid)(object)p)); yield return ("backRight", HiddenRightItem.Invoke((Humanoid)(object)p)); yield return ("backLeft", HiddenLeftItem.Invoke((Humanoid)(object)p)); } private static IEnumerator CaptureAndSend(Player player, string sig) { try { byte[] png = null; yield return PortraitRenderer.Render(player, ByteLimit(), (_serverMaxSide > 0) ? _serverMaxSide : 2048, delegate(byte[] result) { png = result; }); _sentSignature = sig; if (png == null || (Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer || !CanSend()) { yield break; } CardPayload cardPayload = BuildPayload(player, png); AddIcons(player, cardPayload); byte[] array = Protocol.Serialize(cardPayload); if (Plugin.SaveLocalCopy.Value) { SaveLocal(png); } Send(player, array); foreach (IconInfo icon in cardPayload.Icons) { ServerIcons.Add(icon.Key); } Plugin.Log.LogInfo((object)$"Card sent ({array.Length / 1024} KB: portrait {png.Length / 1024} KB, {cardPayload.Items.Count} items, {cardPayload.Skills.Count} skills, {cardPayload.Stats.Count} stats, {cardPayload.Icons.Count} icons)."); } finally { _busy = false; } } private static CardPayload BuildPayload(Player p, byte[] png) { //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01ba: Unknown result type (might be due to invalid IL or missing references) //IL_032f: Unknown result type (might be due to invalid IL or missing references) //IL_0339: Invalid comparison between Unknown and I4 //IL_033d: Unknown result type (might be due to invalid IL or missing references) //IL_0347: Invalid comparison between Unknown and I4 //IL_0357: Unknown result type (might be due to invalid IL or missing references) //IL_035c: Unknown result type (might be due to invalid IL or missing references) CardPayload cardPayload = new CardPayload { Png = png, Hair = (HairItem.Invoke((Humanoid)(object)p) ?? ""), Beard = (BeardItem.Invoke((Humanoid)(object)p) ?? "") }; foreach (var (slot, val) in Equipped(p)) { if (val != null) { string text = val.m_shared?.m_name ?? ""; cardPayload.Items.Add(new ItemInfo { Slot = slot, Prefab = (((Object)(object)val.m_dropPrefab != (Object)null) ? ((Object)val.m_dropPrefab).name : ""), Name = ((Localization.instance != null) ? Localization.instance.Localize(text) : text), Token = text, Quality = val.m_quality, Variant = val.m_variant }); } } ZNetView component = ((Component)p).GetComponent<ZNetView>(); ZDO val2 = ((component != null) ? component.GetZDO() : null); if (val2 != null) { cardPayload.Model = val2.GetInt(ZDOVars.s_modelIndex, 0); Vector3 vec = val2.GetVec3(ZDOVars.s_skinColor, Vector3.one); Vector3 vec2 = val2.GetVec3(ZDOVars.s_hairColor, Vector3.one); cardPayload.SkinColor = new float[3] { vec.x, vec.y, vec.z }; cardPayload.HairColor = new float[3] { vec2.x, vec2.y, vec2.z }; } if (Plugin.ShareSkills.Value) { foreach (Skill skill in ((Character)p).GetSkills().GetSkillList()) { if (skill?.m_info != null) { string text2 = ((object)Unsafe.As<SkillType, SkillType>(ref skill.m_info.m_skill)/*cast due to .constrained prefix*/).ToString(); string text3 = "$skill_" + text2.ToLowerInvariant(); string text4 = ((Localization.instance != null) ? Localization.instance.Localize(text3) : text2); cardPayload.Skills.Add(new SkillInfo { Id = text2, Name = (text4.StartsWith("[") ? text2 : text4), Level = skill.m_level }); } } } PlayerProfile val3 = (((Object)(object)Game.instance != (Object)null) ? Game.instance.GetPlayerProfile() : null); if (Plugin.ShareStats.Value && val3?.m_playerStats != null && val3.m_playerStats.Length != 0 && val3.m_playerStats[0] != null) { foreach (KeyValuePair<PlayerStatType, float> stat in val3.m_playerStats[0].m_stats) { if (stat.Value != 0f && (int)stat.Key != 205 && (int)stat.Key != 206) { cardPayload.Stats.Add(new StatInfo { Id = ((object)stat.Key/*cast due to .constrained prefix*/).ToString(), Value = stat.Value }); if (cardPayload.Stats.Count >= 512) { break; } } } } return cardPayload; } private static void AddIcons(Player p, CardPayload payload) { foreach (var item2 in Equipped(p)) { ItemData item = item2.Item2; if (item == null || (Object)(object)item.m_dropPrefab == (Object)null || payload.Icons.Count >= 24) { continue; } string key = PlayerStore.IconKey(((Object)item.m_dropPrefab).name, item.m_variant); if (ServerIcons.Contains(key) || payload.Icons.Any((IconInfo i) => i.Key == key)) { continue; } try { byte[] array = IconRenderer.Render(item.GetIcon()); if (array != null && array.Length <= 49152) { payload.Icons.Add(new IconInfo { Key = key, Png = array }); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Cannot draw the icon of " + ((Object)item.m_dropPrefab).name + ": " + ex.Message)); } } } private static void Send(Player player, byte[] bytes) { //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Expected O, but got Unknown if (ZNet.instance.IsServer()) { ServerSide.Save(player.GetPlayerName(), bytes); return; } int num = _uploadId++; int num2 = Protocol.ChunkCount(bytes.Length); for (int i = 0; i < num2; i++) { int num3 = i * 60000; int num4 = Math.Min(60000, bytes.Length - num3); byte[] array = new byte[num4]; Buffer.BlockCopy(bytes, num3, array, 0, num4); ZPackage val = new ZPackage(); val.Write(1); val.Write(num); val.Write(i); val.Write(num2); val.Write(bytes.Length); val.Write(array); _serverPeer.m_rpc.Invoke("ValheimPlayerCards_Upload", new object[1] { val }); } } private static void SaveLocal(byte[] png) { try { string text = Path.Combine(Paths.BepInExRootPath, "ValheimPlayerCards"); Directory.CreateDirectory(text); File.WriteAllBytes(Path.Combine(text, "last-portrait.png"), png); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Cannot save local copy: " + ex.Message)); } } } internal static class IconRenderer { private const int Size = 64; public static byte[] Render(Sprite sprite) { //IL_0021: 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_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0123: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Expected O, but got Unknown //IL_0155: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)sprite == (Object)null || (Object)(object)sprite.texture == (Object)null) { return null; } Texture texture = (Texture)(object)sprite.texture; Rect textureRect = sprite.textureRect; Rect val = default(Rect); ((Rect)(ref val))..ctor(((Rect)(ref textureRect)).x / (float)texture.width, ((Rect)(ref textureRect)).y / (float)texture.height, ((Rect)(ref textureRect)).width / (float)texture.width, ((Rect)(ref textureRect)).height / (float)texture.height); float num = ((Rect)(ref textureRect)).width / ((Rect)(ref textureRect)).height; Rect val2 = ((num >= 1f) ? new Rect(0f, (64f - 64f / num) / 2f, 64f, 64f / num) : new Rect((64f - 64f * num) / 2f, 0f, 64f * num, 64f)); RenderTexture temporary = RenderTexture.GetTemporary(64, 64, 0, (RenderTextureFormat)0); RenderTexture active = RenderTexture.active; Texture2D val3 = null; try { RenderTexture.active = temporary; GL.Clear(true, true, Color.clear); GL.PushMatrix(); GL.LoadPixelMatrix(0f, 64f, 64f, 0f); Graphics.DrawTexture(val2, texture, val, 0, 0, 0, 0); GL.PopMatrix(); val3 = new Texture2D(64, 64, (TextureFormat)4, false); val3.ReadPixels(new Rect(0f, 0f, 64f, 64f), 0, 0); val3.Apply(false); return ImageConversion.EncodeToPNG(val3); } finally { RenderTexture.active = active; RenderTexture.ReleaseTemporary(temporary); if ((Object)(object)val3 != (Object)null) { Object.Destroy((Object)(object)val3); } } } } internal static class Json { public static string Write(object value, bool pretty = false) { StringBuilder stringBuilder = new StringBuilder(); WriteValue(stringBuilder, value, pretty, 0); return stringBuilder.ToString(); } private static void WriteValue(StringBuilder sb, object v, bool pretty, int depth) { if (v != null) { if (!(v is string s)) { if (!(v is bool flag)) { if (!(v is int num)) { if (!(v is long num2)) { if (!(v is float num3)) { if (!(v is double d)) { if (!(v is IDictionary<string, object> dictionary)) { if (v is IEnumerable enumerable) { sb.Append('['); bool flag2 = true; foreach (object item in enumerable) { if (!flag2) { sb.Append(','); } flag2 = false; NewLine(sb, pretty, depth + 1); WriteValue(sb, item, pretty, depth + 1); } if (!flag2) { NewLine(sb, pretty, depth); } sb.Append(']'); } else { WriteString(sb, Convert.ToString(v, CultureInfo.InvariantCulture)); } return; } sb.Append('{'); bool flag3 = true; foreach (KeyValuePair<string, object> item2 in dictionary) { if (!flag3) { sb.Append(','); } flag3 = false; NewLine(sb, pretty, depth + 1); WriteString(sb, item2.Key); sb.Append(pretty ? ": " : ":"); WriteValue(sb, item2.Value, pretty, depth + 1); } if (!flag3) { NewLine(sb, pretty, depth); } sb.Append('}'); } else { WriteNumber(sb, d); } } else { WriteNumber(sb, num3); } } else { sb.Append(num2.ToString(CultureInfo.InvariantCulture)); } } else { sb.Append(num.ToString(CultureInfo.InvariantCulture)); } } else { sb.Append(flag ? "true" : "false"); } } else { WriteString(sb, s); } } else { sb.Append("null"); } } private static void NewLine(StringBuilder sb, bool pretty, int depth) { if (pretty) { sb.Append('\n').Append(' ', depth * 2); } } private static void WriteNumber(StringBuilder sb, double d) { if (double.IsNaN(d) || double.IsInfinity(d)) { d = 0.0; } if (Math.Abs(d % 1.0) < 1E-09 && Math.Abs(d) < 1000000000000000.0) { sb.Append(((long)Math.Round(d)).ToString(CultureInfo.InvariantCulture)); } else { sb.Append(d.ToString("0.###", CultureInfo.InvariantCulture)); } } private static void WriteString(StringBuilder sb, string s) { sb.Append('"'); string text = s ?? ""; foreach (char c in text) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ' || c == '<' || c == '>' || c == '&' || c == '\u2028' || c == '\u2029') { StringBuilder stringBuilder = sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4")); } else { sb.Append(c); } } sb.Append('"'); } public static object Parse(string text) { int pos = 0; object result = ParseValue(text, ref pos, 0); SkipSpace(text, ref pos); if (pos != text.Length) { throw new FormatException("trailing characters"); } return result; } private static object ParseValue(string s, ref int pos, int depth) { if (depth > 32) { throw new FormatException("too deep"); } SkipSpace(s, ref pos); if (pos >= s.Length) { throw new FormatException("unexpected end"); } switch (s[pos]) { case '{': { pos++; Dictionary<string, object> dictionary = new Dictionary<string, object>(); SkipSpace(s, ref pos); if (Peek(s, pos) == '}') { pos++; return dictionary; } while (true) { SkipSpace(s, ref pos); string key = ParseString(s, ref pos); SkipSpace(s, ref pos); Expect(s, ref pos, ':'); dictionary[key] = ParseValue(s, ref pos, depth + 1); SkipSpace(s, ref pos); if (Peek(s, pos) != ',') { break; } pos++; } Expect(s, ref pos, '}'); return dictionary; } case '[': { pos++; List<object> list = new List<object>(); SkipSpace(s, ref pos); if (Peek(s, pos) == ']') { pos++; return list; } while (true) { list.Add(ParseValue(s, ref pos, depth + 1)); SkipSpace(s, ref pos); if (Peek(s, pos) != ',') { break; } pos++; } Expect(s, ref pos, ']'); return list; } case '"': return ParseString(s, ref pos); default: { if (Match(s, ref pos, "true")) { return true; } if (Match(s, ref pos, "false")) { return false; } if (Match(s, ref pos, "null")) { return null; } int num = pos; while (pos < s.Length && "+-0123456789.eE".IndexOf(s[pos]) >= 0) { pos++; } if (pos == num) { throw new FormatException("unexpected character"); } return double.Parse(s.Substring(num, pos - num), NumberStyles.Float, CultureInfo.InvariantCulture); } } } private static string ParseString(string s, ref int pos) { Expect(s, ref pos, '"'); StringBuilder stringBuilder = new StringBuilder(); while (pos < s.Length) { char c = s[pos++]; switch (c) { case '"': return stringBuilder.ToString(); default: stringBuilder.Append(c); continue; case '\\': break; } if (pos >= s.Length) { break; } char c2 = s[pos++]; switch (c2) { case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'u': if (pos + 4 > s.Length) { throw new FormatException("bad escape"); } stringBuilder.Append((char)int.Parse(s.Substring(pos, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture)); pos += 4; break; default: stringBuilder.Append(c2); break; } } throw new FormatException("unterminated string"); } private static void SkipSpace(string s, ref int pos) { while (pos < s.Length && char.IsWhiteSpace(s[pos])) { pos++; } } private static char Peek(string s, int pos) { if (pos >= s.Length) { return '\0'; } return s[pos]; } private static void Expect(string s, ref int pos, char c) { if (Peek(s, pos) != c) { throw new FormatException($"expected '{c}'"); } pos++; } private static bool Match(string s, ref int pos, string word) { if (string.CompareOrdinal(s, pos, word, 0, word.Length) != 0) { return false; } pos += word.Length; return true; } public static string Str(IDictionary<string, object> o, string key) { if (o == null || !o.TryGetValue(key, out var value) || !(value is string result)) { return ""; } return result; } public static double Num(IDictionary<string, object> o, string key) { if (o != null && o.TryGetValue(key, out var value) && value is double) { return (double)value; } return 0.0; } public static long Long(IDictionary<string, object> o, string key) { return (long)Num(o, key); } public static int Int(IDictionary<string, object> o, string key) { return (int)Num(o, key); } public static IEnumerable<IDictionary<string, object>> Objects(IDictionary<string, object> o, string key) { if (o == null || !o.TryGetValue(key, out var value) || !(value is List<object> list)) { yield break; } foreach (object item in list) { if (item is IDictionary<string, object> dictionary) { yield return dictionary; } } } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class ZNet_Awake { private static void Postfix(ZNet __instance) { try { if (__instance.IsServer()) { ServerSide.Start(); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Server start: {arg}"); } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] internal static class ZNet_OnNewConnection { private static void Postfix(ZNet __instance, ZNetPeer __0) { try { if (__instance.IsServer()) { ServerSide.RegisterPeer(__0); } else { ClientSide.RegisterPeer(__0); } } catch (Exception arg) { Plugin.Log.LogError((object)$"OnNewConnection: {arg}"); } } } [HarmonyPatch(typeof(ZNet), "RPC_CharacterID")] internal static class ZNet_RPC_CharacterID { private static void Postfix(ZNet __instance, ZRpc __0) { try { if (!__instance.IsServer()) { return; } foreach (ZNetPeer peer in __instance.GetPeers()) { if (peer.m_rpc == __0) { PlayerStore.OnPlayerJoined(peer); break; } } } catch (Exception arg) { Plugin.Log.LogError((object)$"RPC_CharacterID: {arg}"); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] internal static class ZNet_Disconnect { private static void Prefix(ZNet __instance, ZNetPeer peer) { try { if (__instance.IsServer()) { ServerSide.OnPeerLeft(peer); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Disconnect: {arg}"); } } } [HarmonyPatch(typeof(ZNet), "OnDestroy")] internal static class ZNet_OnDestroy { private static void Prefix() { ServerSide.Stop(); ClientSide.Reset(); } } internal class ItemRecord { public string slot = ""; public string prefab = ""; public string name = ""; public string token = ""; public string icon = ""; public int quality; public int variant; } internal class SkillRecord { public string id = ""; public string name = ""; public float level; } internal class StatRecord { public string id = ""; public float value; } internal class SessionRecord { public long start; public long end; } internal class PlayerRecord { public string id = ""; public string name = ""; public long firstSeen; public long lastSeen; public double playSeconds; public int sessions; public long openSince; public List<SessionRecord> recent = new List<SessionRecord>(); public long cardUpdated; public int portraitWidth; public int portraitHeight; public int model; public string hair = ""; public string beard = ""; public string skinColor = ""; public string hairColor = ""; public List<ItemRecord> items = new List<ItemRecord>(); public List<SkillRecord> skills = new List<SkillRecord>(); public List<StatRecord> stats = new List<StatRecord>(); public Dictionary<string, object> ToJson() { return new Dictionary<string, object> { ["id"] = id, ["name"] = name, ["firstSeen"] = firstSeen, ["lastSeen"] = lastSeen, ["playSeconds"] = playSeconds, ["sessions"] = sessions, ["openSince"] = openSince, ["recent"] = recent.Select((SessionRecord r) => new Dictionary<string, object> { ["start"] = r.start, ["end"] = r.end }).ToList(), ["cardUpdated"] = cardUpdated, ["portraitWidth"] = portraitWidth, ["portraitHeight"] = portraitHeight, ["model"] = model, ["hair"] = hair, ["beard"] = beard, ["skinColor"] = skinColor, ["hairColor"] = hairColor, ["items"] = items.Select((ItemRecord i) => new Dictionary<string, object> { ["slot"] = i.slot, ["prefab"] = i.prefab, ["name"] = i.name, ["token"] = i.token, ["icon"] = i.icon, ["quality"] = i.quality, ["variant"] = i.variant }).ToList(), ["skills"] = skills.Select((SkillRecord k) => new Dictionary<string, object> { ["id"] = k.id, ["name"] = k.name, ["level"] = k.level }).ToList(), ["stats"] = stats.Select((StatRecord k) => new Dictionary<string, object> { ["id"] = k.id, ["value"] = k.value }).ToList() }; } public static PlayerRecord FromJson(IDictionary<string, object> o) { return new PlayerRecord { id = Json.Str(o, "id"), name = Json.Str(o, "name"), firstSeen = Json.Long(o, "firstSeen"), lastSeen = Json.Long(o, "lastSeen"), playSeconds = Json.Num(o, "playSeconds"), sessions = Json.Int(o, "sessions"), openSince = Json.Long(o, "openSince"), recent = (from r in Json.Objects(o, "recent") select new SessionRecord { start = Json.Long(r, "start"), end = Json.Long(r, "end") }).ToList(), cardUpdated = Json.Long(o, "cardUpdated"), portraitWidth = Json.Int(o, "portraitWidth"), portraitHeight = Json.Int(o, "portraitHeight"), model = Json.Int(o, "model"), hair = Json.Str(o, "hair"), beard = Json.Str(o, "beard"), skinColor = Json.Str(o, "skinColor"), hairColor = Json.Str(o, "hairColor"), items = (from i in Json.Objects(o, "items") select new ItemRecord { slot = Json.Str(i, "slot"), prefab = Json.Str(i, "prefab"), name = Json.Str(i, "name"), token = Json.Str(i, "token"), icon = Json.Str(i, "icon"), quality = Json.Int(i, "quality"), variant = Json.Int(i, "variant") }).ToList(), skills = (from k in Json.Objects(o, "skills") select new SkillRecord { id = Json.Str(k, "id"), name = Json.Str(k, "name"), level = (float)Json.Num(k, "level") }).ToList(), stats = (from k in Json.Objects(o, "stats") select new StatRecord { id = Json.Str(k, "id"), value = (float)Json.Num(k, "value") }).ToList() }; } } internal sealed class Snapshot { public string Directory = ""; public Dictionary<string, byte[]> Json = new Dictionary<string, byte[]>(); public HashSet<string> Portraits = new HashSet<string>(); public HashSet<string> Icons = new HashSet<string>(); } internal static class PlayerStore { private sealed class Online { public string Id; public long Start; } private sealed class Features { public bool PlayTime; public bool Equipment; public bool ItemIcons; public bool Skills; public bool Stats; } private const int RecentSessions = 30; private const float RefreshSeconds = 30f; private const float HeartbeatSeconds = 60f; private static readonly Dictionary<string, PlayerRecord> Players = new Dictionary<string, PlayerRecord>(); private static readonly Dictionary<long, Online> OnlineByPeer = new Dictionary<long, Online>(); private static readonly HashSet<string> IconKeys = new HashSet<string>(); private static string _dir; private static bool _dirty; private static float _nextRefresh; private static float _nextHeartbeat; public static volatile Snapshot Current; public static bool Active => _dir != null; public static IEnumerable<string> KnownIcons => IconKeys; private static long Now => DateTimeOffset.UtcNow.ToUnixTimeSeconds(); public static void Start() { _dir = ResolveDirectory(); Directory.CreateDirectory(Path.Combine(_dir, "players")); Directory.CreateDirectory(Path.Combine(_dir, "portraits")); Directory.CreateDirectory(Path.Combine(_dir, "icons")); Players.Clear(); OnlineByPeer.Clear(); IconKeys.Clear(); string[] files = Directory.GetFiles(Path.Combine(_dir, "players"), "*.json"); foreach (string path in files) { try { PlayerRecord playerRecord = PlayerRecord.FromJson(Json.Parse(File.ReadAllText(path, Encoding.UTF8)) as IDictionary<string, object>); if (playerRecord != null && !string.IsNullOrEmpty(playerRecord.id) && !(playerRecord.id != Path.GetFileNameWithoutExtension(path))) { if (playerRecord.openSince > 0) { CloseSession(playerRecord, Math.Max(playerRecord.openSince, playerRecord.lastSeen)); Save(playerRecord); } Players[playerRecord.id] = playerRecord; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Cannot read " + Path.GetFileName(path) + ": " + ex.Message)); } } files = Directory.GetFiles(Path.Combine(_dir, "icons"), "*.png"); foreach (string path2 in files) { IconKeys.Add(Path.GetFileNameWithoutExtension(path2)); } _dirty = true; Rebuild(); Plugin.Log.LogInfo((object)$"Player cards in {_dir}: {Players.Count} players, {IconKeys.Count} item icons."); } public static void Stop() { if (_dir == null) { return; } long now = Now; foreach (Online value2 in OnlineByPeer.Values) { if (Players.TryGetValue(value2.Id, out var value)) { CloseSession(value, now); Save(value); } } OnlineByPeer.Clear(); _dir = null; Current = null; } public static void Tick() { if (_dir == null) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (OnlineByPeer.Count > 0 && realtimeSinceStartup >= _nextHeartbeat) { _nextHeartbeat = realtimeSinceStartup + 60f; long now = Now; foreach (Online value2 in OnlineByPeer.Values) { if (Players.TryGetValue(value2.Id, out var value)) { value.lastSeen = now; Save(value); } } } if (_dirty || realtimeSinceStartup >= _nextRefresh) { Rebuild(); } } private static string ResolveDirectory() { string value = Plugin.DataDirectory.Value; if (string.IsNullOrWhiteSpace(value)) { string name = ((ZNet.World != null) ? ZNet.World.m_name : "world"); return Path.Combine(Paths.ConfigPath, "ValheimPlayerCards", SafeName(name)); } if (!Path.IsPathRooted(value)) { return Path.Combine(Paths.GameRootPath, value); } return value; } public static void OnPlayerJoined(ZNetPeer peer) { if (_dir != null && peer != null && !string.IsNullOrEmpty(peer.m_playerName) && !OnlineByPeer.ContainsKey(peer.m_uid)) { long now = Now; PlayerRecord orCreate = GetOrCreate(peer.m_playerName); if (orCreate.firstSeen == 0L) { orCreate.firstSeen = now; } if (orCreate.openSince > 0) { CloseSession(orCreate, Math.Max(orCreate.openSince, orCreate.lastSeen)); } orCreate.openSince = now; orCreate.lastSeen = now; orCreate.sessions++; OnlineByPeer[peer.m_uid] = new Online { Id = orCreate.id, Start = now }; Save(orCreate); _dirty = true; } } public static void OnPeerLeft(ZNetPeer peer) { if (_dir != null && peer != null && OnlineByPeer.TryGetValue(peer.m_uid, out var value)) { OnlineByPeer.Remove(peer.m_uid); if (Players.TryGetValue(value.Id, out var value2)) { CloseSession(value2, Now); Save(value2); } _dirty = true; } } private static void CloseSession(PlayerRecord rec, long end) { if (rec.openSince <= 0) { return; } long openSince = rec.openSince; if (end > openSince) { rec.playSeconds += end - openSince; rec.recent.Add(new SessionRecord { start = openSince, end = end }); if (rec.recent.Count > 30) { rec.recent.RemoveRange(0, rec.recent.Count - 30); } } rec.lastSeen = Math.Max(rec.lastSeen, end); rec.openSince = 0L; } public static void SaveCard(string playerName, CardPayload p, int width, int height) { if (_dir == null) { return; } PlayerRecord orCreate = GetOrCreate(playerName); long now = Now; if (orCreate.firstSeen == 0L) { orCreate.firstSeen = now; } orCreate.lastSeen = Math.Max(orCreate.lastSeen, now); WriteAtomic(Path.Combine(_dir, "portraits", orCreate.id + ".png"), p.Png); int num = 0; HashSet<string> hashSet = new HashSet<string>(p.Items.Select((ItemInfo i) => IconKey(i.Prefab, i.Variant))); foreach (IconInfo icon in p.Icons) { string text = SafeName(icon.Key); if (!IconKeys.Contains(text) && hashSet.Contains(text) && Protocol.TryReadPngSize(icon.Png, out var width2, out var height2) && width2 <= 128 && height2 <= 128) { WriteAtomic(Path.Combine(_dir, "icons", text + ".png"), icon.Png); IconKeys.Add(text); num++; } } orCreate.cardUpdated = now; orCreate.portraitWidth = width; orCreate.portraitHeight = height; orCreate.model = p.Model; orCreate.hair = p.Hair; orCreate.beard = p.Beard; orCreate.skinColor = Hex(p.SkinColor); orCreate.hairColor = Hex(p.HairColor); orCreate.items = p.Items.Select((ItemInfo i) => new ItemRecord { slot = i.Slot, prefab = i.Prefab, name = i.Name, token = i.Token, icon = IconKey(i.Prefab, i.Variant), quality = i.Quality, variant = i.Variant }).ToList(); orCreate.skills = p.Skills.Select((SkillInfo s) => new SkillRecord { id = s.Id, name = s.Name, level = s.Level }).ToList(); orCreate.stats = p.Stats.Select((StatInfo s) => new StatRecord { id = s.Id, value = s.Value }).ToList(); Save(orCreate); _dirty = true; Plugin.Log.LogInfo((object)$"Saved card of {playerName}: portrait {p.Png.Length / 1024} KB {width}x{height}, {p.Items.Count} items, {p.Skills.Count} skills, {p.Stats.Count} stats, {num} new icons."); } public static string IconKey(string prefab, int variant) { return SafeName((variant != 0) ? $"{prefab}-{variant}" : prefab); } private static PlayerRecord GetOrCreate(string name) { string text = SafeName(name); if (!Players.TryGetValue(text, out var value)) { value = new PlayerRecord { id = text }; Players[text] = value; } value.name = name; return value; } private static void Save(PlayerRecord rec) { try { WriteAtomic(Path.Combine(_dir, "players", rec.id + ".json"), Encoding.UTF8.GetBytes(Json.Write(rec.ToJson(), pretty: true))); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Cannot save " + rec.id + ".json: " + ex.Message)); } } private static void Rebuild() { _dirty = false; _nextRefresh = Time.realtimeSinceStartup + 30f; try { Snapshot snapshot = new Snapshot { Directory = _dir }; HashSet<string> hashSet = new HashSet<string>(from s in Plugin.HiddenPlayers.Value.Split(',') select s.Trim() into s where s.Length > 0 select s, StringComparer.OrdinalIgnoreCase); long now = Now; Features features = new Features { PlayTime = Plugin.ShowPlayTime.Value, Equipment = Plugin.ShowEquipment.Value, ItemIcons = (Plugin.ShowEquipment.Value && Plugin.ShowItemIcons.Value), Skills = Plugin.ShowSkills.Value, Stats = Plugin.ShowStats.Value }; List<string> list = (from s in Plugin.StatList.Value.Split(',') select s.Trim() into s where s.Length > 0 select s).ToList(); Dictionary<string, long> dictionary = new Dictionary<string, long>(); foreach (Online value2 in OnlineByPeer.Values) { dictionary[value2.Id] = value2.Start; } List<object> list2 = new List<object>(); int num = 0; foreach (PlayerRecord item in Players.Values.OrderBy<PlayerRecord, string>((PlayerRecord r) => r.name, StringComparer.OrdinalIgnoreCase)) { if (!hashSet.Contains(item.name)) { bool flag = item.cardUpdated > 0 && File.Exists(Path.Combine(_dir, "portraits", item.id + ".png")); if (flag) { snapshot.Portraits.Add(item.id); } if (features.PlayTime && dictionary.ContainsKey(item.id)) { num++; } Dictionary<string, object> dictionary2 = ToApi(item, features, list, dictionary, now, flag); list2.Add(dictionary2); snapshot.Json["api/players/" + item.id + ".json"] = Encoding.UTF8.GetBytes(Json.Write(dictionary2)); } } Dictionary<string, object> value = new Dictionary<string, object> { ["title"] = (string.IsNullOrWhiteSpace(Plugin.PageTitle.Value) ? ServerName() : Plugin.PageTitle.Value.Trim()), ["world"] = ((ZNet.World != null) ? ZNet.World.m_name : ""), ["version"] = "0.2.1", ["language"] = Plugin.PageLanguage.Value, ["now"] = now, ["online"] = num, ["features"] = new Dictionary<string, object> { ["playTime"] = features.PlayTime, ["equipment"] = features.Equipment, ["itemIcons"] = features.ItemIcons, ["skills"] = features.Skills, ["stats"] = features.Stats }, ["statList"] = list }; snapshot.Json["api/players.json"] = Encoding.UTF8.GetBytes(Json.Write(new Dictionary<string, object> { ["server"] = value, ["players"] = list2 })); if (features.ItemIcons) { foreach (string iconKey in IconKeys) { snapshot.Icons.Add(iconKey); } } Current = snapshot; } catch (Exception arg) { Plugin.Log.LogError((object)$"Cannot build the API data: {arg}"); } } private static Dictionary<string, object> ToApi(PlayerRecord rec, Features f, List<string> statList, Dictionary<string, long> onlineSince, long now, bool hasPortrait) { long value; bool flag = f.PlayTime && onlineSince.TryGetValue(rec.id, out value) && value > 0; long num = (flag ? onlineSince[rec.id] : 0); List<object> list = new List<object>(); if (f.Equipment) { foreach (ItemRecord item in rec.items) { list.Add(new Dictionary<string, object> { ["slot"] = item.slot, ["prefab"] = item.prefab, ["name"] = item.name, ["icon"] = ((f.ItemIcons && IconKeys.Contains(item.icon)) ? ("icons/" + item.icon + ".png") : ""), ["quality"] = item.quality, ["variant"] = item.variant }); } } List<object> list2 = new List<object>(); if (f.Skills) { foreach (SkillRecord item2 in rec.skills.OrderByDescending((SkillRecord k) => k.level)) { list2.Add(new Dictionary<string, object> { ["id"] = item2.id, ["name"] = item2.name, ["level"] = item2.level }); } } List<object> list3 = new List<object>(); if (f.Stats) { Dictionary<string, float> dictionary = new Dictionary<string, float>(); foreach (StatRecord stat in rec.stats) { dictionary[stat.id] = stat.value; } foreach (string stat2 in statList) { if (dictionary.TryGetValue(stat2, out var value2)) { list3.Add(new Dictionary<string, object> { ["id"] = stat2, ["value"] = value2 }); } } } List<object> list4 = new List<object>(); if (f.PlayTime) { foreach (SessionRecord item3 in rec.recent) { list4.Add(new Dictionary<string, object> { ["start"] = item3.start, ["end"] = item3.end }); } } return new Dictionary<string, object> { ["id"] = rec.id, ["name"] = rec.name, ["portrait"] = (hasPortrait ? $"portraits/{rec.id}.png?v={rec.cardUpdated}" : ""), ["portraitWidth"] = (hasPortrait ? rec.portraitWidth : 0), ["portraitHeight"] = (hasPortrait ? rec.portraitHeight : 0), ["cardUpdated"] = rec.cardUpdated, ["online"] = flag, ["onlineSince"] = num, ["firstSeen"] = (f.PlayTime ? rec.firstSeen : 0), ["lastSeen"] = ((!f.PlayTime) ? 0 : (flag ? now : rec.lastSeen)), ["playSeconds"] = (f.PlayTime ? (rec.playSeconds + (double)(flag ? Math.Max(0L, now - num) : 0)) : 0.0), ["sessions"] = (f.PlayTime ? rec.sessions : 0), ["recent"] = list4, ["model"] = rec.model, ["skinColor"] = rec.skinColor, ["hairColor"] = rec.hairColor, ["items"] = list, ["skills"] = list2, ["stats"] = list3 }; } private static string ServerName() { try { if (AccessTools.Field(typeof(ZNet), "m_ServerName")?.GetValue(null) is string { Length: >0 } text) { return text; } } catch (Exception) { } if (ZNet.World == null) { return "Valheim"; } return ZNet.World.m_name; } internal static string SafeName(string name) { string text = (name ?? "").Normalize(NormalizationForm.FormD); StringBuilder stringBuilder = new StringBuilder(); string text2 = text; foreach (char c in text2) { if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) { char c2 = char.ToLowerInvariant(c); if ((c2 >= 'a' && c2 <= 'z') || (c2 >= '0' && c2 <= '9') || c2 == '-' || c2 == '_') { stringBuilder.Append(c2); } else if (stringBuilder.Length > 0 && stringBuilder[stringBuilder.Length - 1] != '_') { stringBuilder.Append('_'); } if (stringBuilder.Length >= 64) { break; } } } string text3 = stringBuilder.ToString().Trim('_'); if (text3.Length != 0) { return text3; } return "player"; } private static void WriteAtomic(string path, byte[] bytes) { string text = path + ".tmp"; File.WriteAllBytes(text, bytes); if (File.Exists(path)) { File.Delete(path); } File.Move(text, path); } private static string Hex(float[] c) { return $"#{B(c[0]):x2}{B(c[1]):x2}{B(c[2]):x2}"; static int B(float v) { return Mathf.Clamp(Mathf.RoundToInt(v * 255f), 0, 255); } } } [BepInPlugin("Manke.ValheimPlayerCards", "ValheimPlayerCards", "0.2.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string Guid = "Manke.ValheimPlayerCards"; public const string Name = "ValheimPlayerCards"; public const string Version = "0.2.1"; internal const string DefaultStats = "Deaths,EnemyKills,BossKills,PlayerKills,Builds,Crafts,TreeChops,MineHits,FishCaught,CreatureTamed,FoodEaten,PortalsUsed,DistanceTraveled,DistanceSail,ArrowsShot,Jumps"; internal static Plugin Instance; internal static ManualLogSource Log; internal static readonly bool IsHeadless = (int)SystemInfo.graphicsDeviceType == 4; internal static ConfigEntry<bool> ServerEnabled; internal static ConfigEntry<string> DataDirectory; internal static ConfigEntry<int> MaxUploadKB; internal static ConfigEntry<int> ServerMinIntervalSeconds; internal static ConfigEntry<string> HiddenPlayers; internal static ConfigEntry<bool> WebEnabled; internal static ConfigEntry<string> WebHost; internal static ConfigEntry<int> WebPort; internal static ConfigEntry<string> AllowOrigin; internal static ConfigEntry<string> PageLanguage; internal static ConfigEntry<string> PageTitle; internal static ConfigEntry<string> CustomPageDirectory; internal static ConfigEntry<bool> ShowPlayTime; internal static ConfigEntry<bool> ShowEquipment; internal static ConfigEntry<bool> ShowItemIcons; internal static ConfigEntry<bool> ShowSkills; internal static ConfigEntry<bool> ShowStats; internal static ConfigEntry<string> StatList; internal static ConfigEntry<bool> ClientEnabled; internal static ConfigEntry<bool> ShareSkills; internal static ConfigEntry<bool> ShareStats; internal static ConfigEntry<int> MinIntervalSeconds; internal static ConfigEntry<int> Width; internal static ConfigEntry<int> Height; internal static ConfigEntry<int> Supersampling; internal static ConfigEntry<int> TargetKB; internal static ConfigEntry<bool> SaveLocalCopy; internal static ConfigEntry<float> Yaw; internal static ConfigEntry<float> Pitch; internal static ConfigEntry<float> Ambient; internal static ConfigEntry<float> KeyLight; internal static ConfigEntry<float> RimLight; private Harmony _harmony; private void Awake() { //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Expected O, but got Unknown //IL_0342: Unknown result type (might be due to invalid IL or missing references) //IL_034c: Expected O, but got Unknown //IL_037f: Unknown result type (might be due to invalid IL or missing references) //IL_0389: Expected O, but got Unknown //IL_03bc: Unknown result type (might be due to invalid IL or missing references) //IL_03c6: Expected O, but got Unknown //IL_03ed: Unknown result type (might be due to invalid IL or missing references) //IL_03f7: Expected O, but got Unknown //IL_0427: Unknown result type (might be due to invalid IL or missing references) //IL_0431: Expected O, but got Unknown //IL_0484: Unknown result type (might be due to invalid IL or missing references) //IL_048e: Expected O, but got Unknown //IL_04c1: Unknown result type (might be due to invalid IL or missing references) //IL_04cb: Expected O, but got Unknown //IL_04fe: Unknown result type (might be due to invalid IL or missing references) //IL_0508: Expected O, but got Unknown //IL_053b: Unknown result type (might be due to invalid IL or missing references) //IL_0545: Expected O, but got Unknown //IL_0578: Unknown result type (might be due to invalid IL or missing references) //IL_0582: Expected O, but got Unknown //IL_058d: Unknown result type (might be due to invalid IL or missing references) //IL_0597: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; ServerEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("1 - Server", "Enabled", true, "Accept player cards (portrait, equipment, skills, stats) from players. Only used when this game instance is the server."); DataDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("1 - Server", "DataDirectory", "", "Where cards are stored. Empty = BepInEx/config/ValheimPlayerCards/<world name>. A relative path starts from the Valheim folder."); MaxUploadKB = ((BaseUnityPlugin)this).Config.Bind<int>("1 - Server", "MaxUploadKB", 700, new ConfigDescription("Largest upload accepted (portrait, item icons and data).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(64, 2048), Array.Empty<object>())); ServerMinIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("1 - Server", "MinIntervalSeconds", 20, new ConfigDescription("Minimum time between two uploads from the same player.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(5, 3600), Array.Empty<object>())); HiddenPlayers = ((BaseUnityPlugin)this).Config.Bind<string>("1 - Server", "HiddenPlayers", "", "Comma-separated player names that never appear on the page or in the API."); WebEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("2 - Web", "Enabled", true, "Serve the player cards page and the JSON API. Only on the server."); WebHost = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "Host", "*", "Address to listen on. * = all addresses, 127.0.0.1 = only this machine (for a reverse proxy on the same host)."); WebPort = ((BaseUnityPlugin)this).Config.Bind<int>("2 - Web", "Port", 3010, new ConfigDescription("TCP port of the web page and API.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 65535), Array.Empty<object>())); AllowOrigin = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "AllowOrigin", "*", "Value of the Access-Control-Allow-Origin header, so other websites can read the API. * = any site, empty = only this page."); PageLanguage = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "Language", "auto", "Language of the page: auto (from the visitor's browser) or a language code: en, de, fr, es, pt, ru, pl, it."); PageTitle = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "Title", "", "Title shown on the page. Empty = the server name."); CustomPageDirectory = ((BaseUnityPlugin)this).Config.Bind<string>("2 - Web", "CustomPageDirectory", "", "Folder with your own index.html, app.js, lang.js or style.css; files found there replace the built-in ones."); ShowPlayTime = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowPlayTime", true, "Online now, time played, sessions, first and last seen. Tracked by the server."); ShowEquipment = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowEquipment", true, "List of the equipped items."); ShowItemIcons = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowItemIcons", true, "Game icons next to the equipped items."); ShowSkills = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowSkills", true, "Skill levels."); ShowStats = ((BaseUnityPlugin)this).Config.Bind<bool>("3 - Card", "ShowStats", true, "Character statistics from the game (deaths, kills, builds...). They count all worlds the character has played."); StatList = ((BaseUnityPlugin)this).Config.Bind<string>("3 - Card", "StatList", "Deaths,EnemyKills,BossKills,PlayerKills,Builds,Crafts,TreeChops,MineHits,FishCaught,CreatureTamed,FoodEaten,PortalsUsed,DistanceTraveled,DistanceSail,ArrowsShot,Jumps", "Statistics shown, in this order. Names are Valheim's PlayerStatType values."); ClientEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "Enabled", true, "Render your character and send your card to servers that have ValheimPlayerCards."); ShareSkills = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "ShareSkills", true, "Send your skill levels with the card."); ShareStats = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "ShareStats", true, "Send your character statistics with the card."); MinIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "MinIntervalSeconds", 30, new ConfigDescription("Minimum time between two cards. The server can ask for a longer wait.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 3600), Array.Empty<object>())); Width = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "Width", 512, new ConfigDescription("Portrait width in pixels.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(128, 1024), Array.Empty<object>())); Height = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "Height", 768, new ConfigDescription("Portrait height in pixels.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(128, 1536), Array.Empty<object>())); Supersampling = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "Supersampling", 2, new ConfigDescription("Render at this many times the size and scale down, for smoother edges.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 3), Array.Empty<object>())); TargetKB = ((BaseUnityPlugin)this).Config.Bind<int>("4 - Client", "TargetKB", 300, new ConfigDescription("If the portrait PNG is larger, it is scaled down until it fits.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(32, 1024), Array.Empty<object>())); SaveLocalCopy = ((BaseUnityPlugin)this).Config.Bind<bool>("4 - Client", "SaveLocalCopy", true, "Also save the last portrait as BepInEx/ValheimPlayerCards/last-portrait.png on your PC."); Yaw = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "Yaw", -5f, new ConfigDescription("Character rotation in degrees. The idle stance already turns the body about 35 degrees, so around -5 gives a 3/4 view.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-180f, 180f), Array.Empty<object>())); Pitch = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "Pitch", 0f, new ConfigDescription("Forward tilt in degrees.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(-30f, 30f), Array.Empty<object>())); Ambient = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "Ambient", 0.5f, new ConfigDescription("Ambient light (0 = black, 1 = white).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1.5f), Array.Empty<object>())); KeyLight = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "KeyLight", 0.6f, new ConfigDescription("Light from the upper front side.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>())); RimLight = ((BaseUnityPlugin)this).Config.Bind<float>("5 - Look", "RimLight", 0.5f, new ConfigDescription("Light from behind that outlines the character.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>())); _harmony = new Harmony("Manke.ValheimPlayerCards"); _harmony.PatchAll(typeof(Plugin).Assembly); if (!IsHeadless) { CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new CardCommand()); } Log.LogInfo((object)"ValheimPlayerCards 0.2.1 loaded."); } private void Update() { ClientSide.Tick(); PlayerStore.Tick(); } private void OnDestroy() { ServerSide.Stop(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class PortraitRenderer { private sealed class PortraitLighting : IDisposable { private readonly bool _fog; private readonly AmbientMode _ambientMode; private readonly Color _ambientLight; private readonly float _ambientIntensity; private readonly List<Light> _disabled = new List<Light>(); private readonly List<GameObject> _created = new List<GameObject>(); public PortraitLighting() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: 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_0038: 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_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) _fog = RenderSettings.fog; _ambientMode = RenderSettings.ambientMode; _ambientLight = RenderSettings.ambientLight; _ambientIntensity = RenderSettings.ambientIntensity; RenderSettings.fog = false; RenderSettings.ambientMode = (AmbientMode)3; float value = Plugin.Ambient.Value; RenderSettings.ambientLight = new Color(value, value, value, 1f); RenderSettings.ambientIntensity = 1f; Light[] array = Object.FindObjectsByType<Light>((FindObjectsSortMode)0); foreach (Light val in array) { if (((Behaviour)val).enabled && (val.cullingMask & int.MinValue) != 0 && val.cullingMask != int.MinValue) { ((Behaviour)val).enabled = false; _disabled.Add(val); } } AddLight(Quaternion.Euler(35f, 150f, 0f), Plugin.KeyLight.Value); AddLight(Quaternion.Euler(20f, -20f, 0f), Plugin.RimLight.Value); } private void AddLight(Quaternion rotation, float intensity) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown //IL_001a: Unknown result type (might be due to invalid IL or missing references) if (!(intensity <= 0f)) { GameObject val = new GameObject("PlayerCardLight"); val.transform.rotation = rotation; Light obj = val.AddComponent<Light>(); obj.type = (LightType)1; obj.intensity = intensity; obj.shadows = (LightShadows)0; obj.cullingMask = int.MinValue; _created.Add(val); } } public void Dispose() { //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) foreach (GameObject item in _created) { Object.DestroyImmediate((Object)(object)item); } foreach (Light item2 in _disabled) { if ((Object)(object)item2 != (Object)null) { ((Behaviour)item2).enabled = true; } } RenderSettings.fog = _fog; RenderSettings.ambientMode = _ambientMode; RenderSettings.ambientLight = _ambientLight; RenderSettings.ambientIntensity = _ambientIntensity; } } private const int RenderLayer = 31; private const float SettleSeconds = 1.5f; private const float MaxTextureWait = 5f; private const int FastForwardSteps = 40; private static readonly MethodInfo SetupVisEquipment = AccessTools.Method(typeof(Humanoid), "SetupVisEquipment", (Type[])null, (Type[])null); private static readonly MethodInfo UpdateVisuals = AccessTools.Method(typeof(VisEquipment), "UpdateVisuals", (Type[])null, (Type[])null); private static readonly int StateF = Animator.StringToHash("statef"); private static readonly int StateI = Animator.StringToHash("statei"); private static readonly (string, bool)[] StandingBools = new(string, bool)[12] { ("wakeup", false), ("onGround", true), ("inWater", false), ("encumbered", false), ("flying", false), ("crouching", false), ("blocking", false), ("dead", false), ("falling", false), ("skating", false), ("skategliding", false), ("slipping", false) }; private static readonly string[] StandingFloats = new string[4] { "forward_speed", "sideway_speed", "turn_speed", "tilt" }; private static readonly int SkinColor = Shader.PropertyToID("_SkinColor"); public static IEnumerator Render(Player player, int maxBytes, int maxSide, Action<byte[]> done) { GameObject holder = null; byte[] png = null; List<Texture2D> textures = null; try { holder = CreateClone(player); if ((Object)(object)holder == (Object)null) { yield break; } textures = RequestFullTextures(holder); float start = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - start < 1.5f || (Time.realtimeSinceStartup - start < 5f && !textures.All(LoadedOrGone))) { yield return null; if ((Object)(object)holder == (Object)null || (Object)(object)player == (Object)null) { yield break; } } try { png = Capture(holder, maxBytes, maxSide); } catch (Exception arg) { Plugin.Log.LogError((object)$"Could not render the portrait: {arg}"); } } finally { if ((Object)(object)holder != (Object)null) { Object.Destroy((Object)(object)holder); } if (textures != null) { foreach (Texture2D item in textures) { if ((Object)(object)item != (Object)null) { item.ClearRequestedMipmapLevel(); } } } done(png); } } private static List<Texture2D> RequestFullTextures(GameObject root) { HashSet<Texture2D> hashSet = new HashSet<Texture2D>(); Renderer[] componentsInChildren = root.GetComponentsInChildren<Renderer>(true); for (int i = 0; i < componentsInChildren.Length; i++) { Material[] sharedMaterials = componentsInChildren[i].sharedMaterials; foreach (Material val in sharedMaterials) { if ((Object)(object)val == (Object)null) { continue; } int[] texturePropertyNameIDs = val.GetTexturePropertyNameIDs(); foreach (int num in texturePropertyNameIDs) { Texture texture = val.GetTexture(num); Texture2D val2 = (Texture2D)(object)((texture is Texture2D) ? texture : null); if (val2 != null && val2.streamingMipmaps && hashSet.Add(val2)) { val2.requestedMipmapLevel = 0; } } } } return hashSet.ToList(); } private static bool LoadedOrGone(Texture2D t) { if (!((Object)(object)t == (Object)null)) { return t.IsRequestedMipmapLevelLoaded(); } return true; } private static GameObject CreateClone(Player player) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Expected O, but got Unknown //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) GameObject val = null; try { GameObject val2 = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab("Player") : null); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)"Player prefab not found, no portrait."); return null; } val = new GameObject("PlayerCardPortrait"); val.SetActive(false); val.transform.position = new Vector3(0f, 6000f, 0f); GameObject val3 = Object.Instantiate<GameObject>(val2, val.transform, false); ((Object)val3).name = "PlayerCardClone"; StripComponents(val3); SetLayer(val3.transform, 31); ZNetView.m_forceDisableInit = true; try { val.SetActive(true); } finally { ZNetView.m_forceDisableInit = false; } VisEquipment component = val3.GetComponent<VisEquipment>(); if ((Object)(object)component == (Object)null) { Plugin.Log.LogWarning((object)"Player prefab has no VisEquipment, no portrait."); Object.Destroy((Object)(object)val); return null; } SetupVisEquipment.Invoke(player, new object[2] { component, false }); ZNetView component2 = ((Component)player).GetComponent<ZNetView>(); ZDO val4 = ((component2 != null) ? component2.GetZDO() : null); if (val4 != null) { component.SetModel(val4.GetInt(ZDOVars.s_modelIndex, 0)); component.SetSkinColor(val4.GetVec3(ZDOVars.s_skinColor, Vector3.one)); component.SetHairColor(val4.GetVec3(ZDOVars.s_hairColor, Vector3.one)); } UpdateVisuals.Invoke(component, null); SetLayer(val3.transform, 31); PoseStanding(val3, ((Component)player).GetComponentInChildren<Animator>(), 40); return val; } catch (Exception arg) { Plugin.Log.LogError((object)$"Could not build the character copy: {arg}"); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } return null; } } private static void PoseStanding(GameObject clone, Animator source, int steps) { Animator[] componentsInChildren = clone.GetComponentsInChildren<Animator>(); Animator[] array = componentsInChildren; foreach (Animator val in array) { val.cullingMode = (AnimatorCullingMode)0; val.updateMode = (AnimatorUpdateMode)0; val.applyRootMotion = false; (string, bool)[] standingBools = StandingBools; for (int j = 0; j < standingBools.Length; j++) { var (text, flag) = standingBools[j]; if (HasParameter(val, Animator.StringToHash(text), (AnimatorControllerParameterType)4)) { val.SetBool(text, flag); } } string[] standingFloats = StandingFloats; foreach (string text2 in standingFloats) { if (HasParameter(val, Animator.StringToHash(text2), (AnimatorControllerParameterType)1)) { val.SetFloat(text2, 0f); } } if ((Object)(object)source != (Object)null && HasParameter(source, StateF, (AnimatorControllerParameterType)1) && HasParameter(val, StateF, (AnimatorControllerParameterType)1)) { val.SetFloat(StateF, source.GetFloat(StateF)); } if ((Object)(object)source != (Object)null && HasParameter(source, StateI, (AnimatorControllerParameterType)3) && HasParameter(val, StateI, (AnimatorControllerParameterType)3)) { val.SetInteger(StateI, source.GetInteger(StateI)); } } for (int k = 0; k < steps; k++) { array = componentsInChildren; for (int i = 0; i < array.Length; i++) { array[i].Update(0.2f); } } } private static bool HasParameter(Animator anim, int hash, AnimatorControllerParameterType type) { //IL_000e: 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) return anim.parameters.Any((AnimatorControllerParameter p) => p.nameHash == hash && p.type == type); } private static bool Keep(Component c) { if (c is Transform || c is Renderer || c is MeshFilter || c is Animator || c is LODGroup || c is VisEquipment || c is ZNetView || c is ParticleSystem) { return true; } return (((object)c).GetType().Namespace ?? "").StartsWith("MagicaCloth", StringComparison.Ordinal); } private static void StripComponents(GameObject root) { Func<Component, int>[] array = new Func<Component, int>[4] { (Component c) => (!(c is MonoBehaviour)) ? 99 : 0, (Component c) => (!((object)c).GetType().Name.Contains("Joint")) ? 99 : 0, (Component c) => 0, (Component c) => 0 }; foreach (Func<Component, int> func in array) { Component[] componentsInChildren = root.GetComponentsInChildren<Component>(true); foreach (Component val in componentsInChildren) { if (!((Object)(object)val == (Object)null) && !Keep(val) && func(val) == 0) { try { Object.DestroyImmediate((Object)(object)val); } catch (Exception) { } } } } } private static void SetLayer(Transform t, int layer) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown ((Component)t).gameObject.layer = layer; foreach (Transform item in t) { SetLayer(item, layer); } } private static void DisableLowerLods(GameObject root) { LODGroup[] componentsInChildren = root.GetComponentsInChildren<LODGroup>(true); for (int i = 0; i < componentsInChildren.Length; i++) { LOD[] lODs = componentsInChildren[i].GetLODs(); if (lODs.Length < 2) { continue; } HashSet<Renderer> hashSet = new HashSet<Renderer>(lODs[0].renderers.Where((Renderer r) => (Object)(object)r != (Object)null)); for (int num = 1; num < lODs.Length; num++) { Renderer[] renderers = lODs[num].renderers; foreach (Renderer val in renderers) { if ((Object)(object)val != (Object)null && !hashSet.Contains(val)) { val.enabled = false; } } } } } private static void BakePropertyBlocks(GameObject root) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) MaterialPropertyBlock val = new MaterialPropertyBlock(); Renderer[] componentsInChildren = root.GetComponentsInChildren<Renderer>(true); foreach (Renderer val2 in componentsInChildren) { if (!val2.HasPropertyBlock()) { continue; } val2.GetPropertyBlock(val); if (!val.HasColor(SkinColor)) { continue; } Color color = val.GetColor(SkinColor); Material[] materials = val2.materials; foreach (Material val3 in materials) { if ((Object)(object)val3 != (Object)null && val3.HasProperty(SkinColor)) { val3.SetColor(SkinColor, color); } } } } private static byte[] Capture(GameObject holder, int maxBytes, int maxSide) { //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) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Expected O, but got Unknown GameObject gameObject = ((Component)holder.transform.GetChild(0)).gameObject; DisableLowerLods(gameObject); BakePropertyBlocks(gameObject); PoseStanding(gameObject, ((Object)(object)Player.m_localPlayer != (Object)null) ? ((Component)Player.m_localPlayer).GetComponentInChildren<Animator>() : null, 10); int value = Plugin.Supersampling.Value; int num = Mathf.Min(Plugin.Width.Value, maxSide); int num2 = Mathf.Min(Plugin.Height.Value, maxSide); RenderRequest val = new RenderRequest(gameObject) { Width = num * value, Height = num2 * value, Rotation = Quaternion.Euler(Plugin.Pitch.Value, Plugin.Yaw.Value, 0f), ParticleSimulationTime = 1f }; Sprite val2 = null; using (new PortraitLighting()) { val2 = RenderManager.Instance.Render(val); } if ((Object)(object)val2 == (Object)null || (Object)(object)val2.texture == (Object)null) { Plugin.Log.LogWarning((object)"The renderer returned nothing."); return null; } Texture2D texture = val2.texture; Color32[] pixels; try { pixels = texture.GetPixels32(); } finally { Object.Destroy((Object)(object)val2); Object.Destroy((Object)(object)texture); } float num3 = 1f; for (int i = 0; i < 6; i++) { int num4 = Mathf.Max(64, Mathf.RoundToInt((float)num * num3)); int num5 = Mathf.Max(64, Mathf.RoundToInt((float)num2 * num3)); byte[] array = Encode(Resample(pixels, num * value, num2 * value, num4, num5), num4, num5); if (array.Length <= maxBytes) { return array; } Plugin.Log.LogInfo((object)$"Portrait {num4}x{num5} is {array.Length / 1024} KB, trying smaller."); num3 *= 0.8f; } Plugin.Log.LogWarning((object)"Could not make the portrait small enough, not sent."); return null; } private static byte[] Encode(Color32[] pixels, int w, int h) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Expected O, but got Unknown Texture2D val = new Texture2D(w, h, (TextureFormat)4, false); try { val.SetPixels32(pixels); val.Apply(false); return ImageConversion.EncodeToPNG(val); } finally { Object.Destroy((Object)(object)val); } } private static Color32[] Resample(Color32[] src, int sw, int sh, int dw, int dh) { //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0119: 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_0087: 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_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) //IL_0129: Unknown result type (might be due to invalid IL or missing references) if (sw == dw && sh == dh) { return src; } Color32[] array = (Color32[])(object)new Color32[dw * dh]; for (int i = 0; i < dh; i++) { int num = i * sh / dh; int num2 = Math.Max(num + 1, (i + 1) * sh / dh); for (int j = 0; j < dw; j++) { int num3 = j * sw / dw; int num4 = Math.Max(num3 + 1, (j + 1) * sw / dw); long num5 = 0L; long num6 = 0L; long num7 = 0L; long num8 = 0L; long num9 = 0L; for (int k = num; k < num2; k++) { int num10 = k * sw; for (int l = num3; l < num4; l++) { Color32 val = src[num10 + l]; num5 += val.r * val.a; num6 += val.g * val.a; num7 += val.b * val.a; num8 += val.a; num9++; } } array[i * dw + j] = ((num8 == 0L) ? new Color32((byte)0, (byte)0, (byte)0, (byte)0) : new Color32((byte)(num5 / num8), (byte)(num6 / num8), (byte)(num7 / num8), (byte)(num8 / num9))); } } return array; } } internal sealed class ItemInfo { public string Slot = ""; public string Prefab = ""; public string Name = ""; public string Token = ""; public int Quality; public int Variant; } internal sealed class SkillInfo { public string Id = ""; public string Name = ""; public float Level; } internal sealed class StatInfo { public string Id = ""; public float Value; } internal sealed class IconInfo { public string Key = ""; public byte[] Png = Array.Empty<byte>(); } internal sealed class CardPayload { public readonly List<ItemInfo> Items = new List<ItemInfo>(); public readonly List<SkillInfo> Skills = new List<SkillInfo>(); public readonly List<StatInfo> Stats = new List<StatInfo>(); public readonly List<IconInfo> Icons = new List<IconInfo>(); public int Model; public string Hair = ""; public string Beard = ""; public float[] SkinColor = new float[3] { 1f, 1f, 1f }; public float[] HairColor = new float[3] { 1f, 1f, 1f }; public byte[] Png = Array.Empty<byte>(); } internal static class Protocol { public const int Version = 1; public const string RpcHello = "ValheimPlayerCards_Hello"; public const string RpcReady = "ValheimPlayerCards_Ready"; public const string RpcUpload = "ValheimPlayerCards_Upload"; public const int ChunkSize = 60000; public const int HardMaxBytes = 2097152; public const int MaxItems = 24; public const int MaxSkills = 96; public const int MaxStats = 512; public const int MaxIcons = 24; public const int MaxIconBytes = 49152; public const int MaxIconSide = 128; public const int MaxKnownIcons = 2000; public const int MaxStringLength = 128; public const int MaxImageSide = 2048; private static readonly byte[] PngSignature = new byte[8] { 137, 80, 78, 71, 13, 10, 26, 10 }; public static int ChunkCount(int total) { return (total + 60000 - 1) / 60000; } public static byte[] Serialize(CardPayload p) { using MemoryStream memoryStream = new MemoryStream(); using (BinaryWriter binaryWriter = new BinaryWriter(memoryStream, Encoding.UTF8, leaveOpen: true)) { binaryWriter.Write(1); int num = Math.Min(p.Items.Count, 24); binaryWriter.Write(num); for (int i = 0; i < num; i++) { ItemInfo itemInfo = p.Items[i]; binaryWriter.Write(Clip(itemInfo.Slot)); binaryWriter.Write(Clip(itemInfo.Prefab)); binaryWriter.Write(Clip(itemInfo.Name)); binaryWriter.Write(Clip(itemInfo.Token)); binaryWriter.Write(itemInfo.Quality); binaryWriter.Write(itemInfo.Variant); } binaryWriter.Write(p.Model); binaryWriter.Write(Clip(p.Hair)); binaryWriter.Write(Clip(p.Beard)); for (int j = 0; j < 3; j++) { binaryWriter.Write(p.SkinColor[j]); } for (int k = 0; k < 3; k++) { binaryWriter.Write(p.HairColor[k]); } int num2 = Math.Min(p.Skills.Count, 96); binaryWriter.Write(num2); for (int l = 0; l < num2; l++) { binaryWriter.Write(Clip(p.Skills[l].Id)); binaryWriter.Write(Clip(p.Skills[l].Name)); binaryWriter.Write(p.Skills[l].Level); } int num3 = Math.Min(p.Stats.Count, 512); binaryWriter.Write(num3); for (int m = 0; m < num3; m++) { binaryWriter.Write(Clip(p.Stats[m].Id)); binaryWriter.Write(p.Stats[m].Value); } int num4 = Math.Min(p.Icons.Count, 24); binaryWriter.Write(num4); for (int n = 0; n < num4; n++) { binaryWriter.Write(Clip(p.Icons[n].Key)); binaryWriter.Write(p.Icons[n].Png.Length); binaryWriter.Write(p.Icons[n].Png); } binaryWriter.Write(p.Png.Length); binaryWriter.Write(p.Png); } return memoryStream.ToArray(); } public static CardPayload Deserialize(byte[] data) { using BinaryReader binaryReader = new BinaryReader(new MemoryStream(data, writable: false), Encoding.UTF8); if (binaryReader.ReadInt32() != 1) { throw new InvalidDataException("unknown payload version"); } CardPayload cardPayload = new CardPayload(); int num = ReadCount(binaryReader, 24, "items"); for (int i = 0; i < num; i++) { cardPayload.Items.Add(new ItemInfo { Slot = ReadString(binaryReader), Prefab = ReadString(binaryReader), Name = ReadString(binaryReader), Token = ReadString(binaryReader), Quality = binaryReader.ReadInt32(), Variant = binaryReader.ReadInt32() }); } cardPayload.Model = binaryReader.ReadInt32(); cardPayload.Hair = ReadString(binaryReader); cardPayload.Beard = ReadString(binaryReader); for (int j = 0; j < 3; j++) { cardPayload.SkinColor[j] = binaryReader.ReadSingle(); } for (int k = 0; k < 3; k++) { cardPayload.HairColor[k] = binaryReader.ReadSingle(); } int num2 = ReadCount(binaryReader, 96, "skills"); for (int l = 0; l < num2; l++) { cardPayload.Skills.Add(new SkillInfo { Id = ReadString(binaryReader), Name = ReadString(binaryReader), Level = Finite(binaryReader.ReadSingle()) }); } int num3 = ReadCount(binaryReader, 512, "stats"); for (int m = 0; m < num3; m++) { cardPayload.Stats.Add(new StatInfo { Id = ReadString(binaryReader), Value = Finite(binaryReader.ReadSingle()) }); } int num4 = ReadCount(binaryReader, 24, "icons"); for (int n = 0; n < num4; n++) { string key = ReadString(binaryReader); cardPayload.Icons.Add(new IconInfo { Key = key, Png = ReadBytes(binaryReader, 49152) }); } cardPayload.Png = ReadBytes(binaryReader, data.Length); return cardPayload; } public static bool TryReadPngSize(byte[] png, out int width, out int height) { width = (height = 0); if (png == null || png.Length < 33) { return false; } for (int i = 0; i < PngSignature.Length; i++) { if (png[i] != PngSignature[i]) { return false; } } if (png[12] != 73 || png[13] != 72 || png[14] != 68 || png[15] != 82) { return false; } width = (png[16] << 24) | (png[17] << 16) | (png[18] << 8) | png[19]; height = (png[20] << 24) | (png[21] << 16) | (png[22] << 8) | png[23]; if (width > 0) { return height > 0; } return false; } private static int ReadCount(BinaryReader r, int max, string what) { int num = r.ReadInt32(); if (num < 0 || num > max) { throw new InvalidDataException("too many " + what); } return num; } private static byte[] ReadBytes(BinaryReader r, int max) { int num = r.ReadInt32(); if (num <= 0 || num > max) { throw new InvalidDataException("bad image length"); } byte[] array = r.ReadBytes(num); if (array.Length != num) { throw new InvalidDataException("truncated image"); } return array; } private static float Finite(float v) { if (!float.IsNaN(v) && !float.IsInfinity(v)) { return v; } return 0f; } private static string ReadString(BinaryReader r) { string text = r.ReadString(); if (text.Length > 128) { throw new InvalidDataException("string too long"); } StringBuilder stringBuilder = new StringBuilder(text.Length); string text2 = text; foreach (char c in text2) { if (!char.IsControl(c)) { stringBuilder.Append(c); } } return stringBuilder.ToString(); } private static string Clip(string s) { s = s ?? ""; if (s.Length <= 128) { return s; } return s.Substring(0, 128); } } internal static class ServerSide { private sealed class Upload { public int Id; public int Count; public int Next; public byte[] Buffer; public int Written; public float Started; } private static readonly Dictionary<long, Upload> Uploads = new Dictionary<long, Upload>(); private static readonly Dictionary<string, float> LastStart = new Dictionary<string, float>(); private static WebServer _web; private static int MaxBytes => Plugin.MaxUploadKB.Value * 1024; public static void Start() { if (Plugin.ServerEnabled.Value) { try { PlayerStore.Start(); } catch (Exception arg) { Plugin.Log.LogError((object)$"Cannot open the player cards folder: {arg}"); return; } if (Plugin.WebEnabled.Value) { _web = new WebServer(); _web.Start(Plugin.WebHost.Value, Plugin.WebPort.Value); } } } public static void Stop() { _web?.Stop(); _web = null; PlayerStore.Stop(); Uploads.Clear(); LastStart.Clear(); } public static void RegisterPeer(ZNetPeer peer) { peer.m_rpc.Register<ZPackage>("ValheimPlayerCards_Hello", (Action<ZRpc, ZPackage>)OnHello); peer.m_rpc.Register<ZPackage>("ValheimPlayerCards_Upload", (Action<ZRpc, ZPackage>)OnUpload); } public static void OnPeerLeft(ZNetPeer peer) { if (peer != null) { Uploads.Remove(peer.m_uid); PlayerStore.OnPeerLeft(peer); } } private static ZNetPeer FindPeer(ZRpc rpc) { ZNet instance = ZNet.instance; if (instance == null) { return null; } return ((IEnumerable<ZNetPeer>)instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.m_rpc == rpc)); } private static void OnHello(ZRpc rpc, ZPackage pkg) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown try { if (!PlayerStore.Active) { return; } int num = pkg.ReadInt(); ZNetPeer val = FindPeer(rpc); string text = ((val != null && !string.IsNullOrEmpty(val.m_playerName)) ? val.m_playerName : "a player"); if (num != 1) { Plugin.Log.LogWarning((object)$"{text} uses ValheimPlayerCards protocol {num}, this server uses {1}."); return; } ZPackage val2 = new ZPackage(); val2.Write(1); val2.Write(MaxBytes); val2.Write(Plugin.ServerMinIntervalSeconds.Value); val2.Write(2048); List<string> list = PlayerStore.KnownIcons.Take(2000).ToList(); val2.Write(list.Count); foreach (string item in list) { val2.Write(item); } rpc.Invoke("ValheimPlayerCards_Ready", new object[1] { val2 }); Plugin.Log.LogInfo((object)(text + " has ValheimPlayerCards.")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Bad hello: " + ex.Message)); } } private static void OnUpload(ZRpc rpc, ZPackage pkg) { ZNetPeer val = FindPeer(rpc); if (val == null || !PlayerStore.Active) { return; } try { if (pkg.ReadInt() != 1) { return; } int num = pkg.ReadInt(); int num2 = pkg.ReadInt(); int num3 = pkg.ReadInt(); int num4 = pkg.ReadInt(); byte[] array = pkg.ReadByteArray(); if (num2 == 0) { Uploads.Remove(val.m_uid); if (string.IsNullOrEmpty(val.m_playerName)) { Plugin.Log.LogWarning((object)"Card from a connection without a player name, ignored."); return; } if (num4 <= 0 || num4 > MaxBytes) { Plugin.Log.LogWarning((object)$"Card from {val.m_playerName} is {num4 / 1024} KB, limit is {Plugin.MaxUploadKB.Value} KB. Ignored."); return; } if (num3 != Protocol.ChunkCount(num4)) { return; } string key = PlayerStore.SafeName(val.m_playerName); float realtimeSinceStartup = Time.realtimeSinceStartup; if (LastStart.TryGetValue(key, out var value) && realtimeSinceStartup - value < (float)Plugin.ServerMinIntervalSeconds.Value) { Plugin.Log.LogInfo((object)("Card from " + val.m_playerName + " came too soon after the previous one, ignored.")); return; } LastStart[key] = realtimeSinceStartup; Uploads[val.m_uid] = new Upload { Id = num, Count = num3, Buffer = new byte[num4], Started = realtimeSinceStartup }; } if (!Uploads.TryGetValue(val.m_uid, out var value2) || value2.Id != num) { return; } bool flag = num2 == value2.Count - 1; int num5 = (flag ? (value2.Buffer.Length - value2.Written) : 60000); if (num2 != value2.Next || array.Length != num5 || Time.realtimeSinceStartup - value2.Started > 120f) { Uploads.Remove(val.m_uid); Plugin.Log.LogWarning((object)("Card from " + val.m_playerName + " arrived broken, ignored.")); return; } Buffer.BlockCopy(array, 0, value2.Buffer, value2.Written, array.Length); value2.Written += array.Length; value2.Next++; if (flag) { Uploads.Remove(val.m_uid); Save(val.m_playerName, value2.Buffer); } } catch (Exception ex) { Uploads.Remove(val.m_uid); Plugin.Log.LogWarning((object)("Bad card from " + val.m_playerName + ": " + ex.Message)); } } public static void Save(string playerName, byte[] payload) { if (PlayerStore.Active) { CardPayload cardPayload; try { cardPayload = Protocol.Deserialize(payload); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Card from " + playerName + " could not be read: " + ex.Message)); return; } if (!Protocol.TryReadPngSize(cardPayload.Png, out var width, out var height) || width > 2048 || height > 2048) { Plugin.Log.LogWarning((object)("Portrait from " + playerName + " is not a valid PNG, ignored.")); } else { PlayerStore.SaveCard(playerName, cardPayload, width, height); } } } } internal sealed class WebServer { private const int MaxConnections = 32; private const int MaxRequestBytes = 8192; private static readonly Dictionary<string, string> PageFiles = new Dictionary<string, string> { { "index.html", "text/html; charset=utf-8" }, { "app.js", "text/javascript; charset=utf-8" }, { "lang.js", "text/javascript; charset=utf-8" }, { "style.css", "text/css; charset=utf-8" } }; private readonly Dictionary<string, byte[]> _builtIn = new Dictionary<string, byte[]>(); private TcpListener _listener; private Thread _thread; private volatile bool _running; private int _connections; private string _allowOrigin; private string _customDir; public void Start(string host, int port) { foreach (string key in PageFiles.Keys) { using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("web/" + key); if (stream != null) { MemoryStream memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); _builtIn[key] = memoryStream.ToArray(); } } _allowOrigin = (Plugin.AllowOrigin.Value ?? "").Trim(); _customDir = (Plugin.CustomPageDirectory.Value ?? "").Trim(); IPAddress address = IPAddress.Any; if (!string.IsNullOrWhiteSpace(host) && host.Trim() != "*" && !IPAddress.TryParse(host.Trim(), out address)) { Plugin.Log.LogError((object)("Web Host '" + host + "' is not an IP address; the page is not served.")); return; } try { _listener = new TcpListener(address, port); _listener.Start(); } catch (Exception ex) { Plugin.Log.LogError((object)$"Cannot listen on {address}:{port} ({ex.Message}); the page is not served."); _listener = null; return; } _running = true; _thread = new Thread(AcceptLoop) { IsBackground = true, Name = "ValheimPlayerCards web" }; _thread.Start(); Plugin.Log.LogInfo((object)string.Format("Player cards page on http://{0}:{1}/", address.Equals(IPAddress.Any) ? "*" : address.ToString(), port)); } public void Stop() { _running = false; try { _listener?.Stop(); } catch (Exception) { } _listener = null; } private void AcceptLoop() { while (_running) { TcpClient client; try { client = _listener.AcceptTcpClient(); } catch (Exception) { if (!_running) { break; } Thread.Sleep(100); continue; } if (Interlocked.Increment(ref _connections) > 32) { Interlocked.Decrement(ref _connections); client.Close(); continue; } ThreadPool.QueueUserWorkItem(delegate { try { Handle(client); } catch (Exception) { } finally { client.Close(); Interlocked.Decrement(ref _connections); } }); } } private void Handle(TcpClient client) { client.ReceiveTimeout = 5000; client.SendTimeout = 10000; NetworkStream stream = client.GetStream(); string text = ReadHead(stream); if (text == null) { return; } string[] array = text.Split(new string[1] { "\r\n" }, StringSplitOptions.None)[0].Split(' '); if (array.Length < 3) { Send(stream, 400, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Bad request"), headOnly: false, null); return; } string text2 = array[0]; string text3 = array[1]; bool flag = text2 == "HEAD"; if (text2 == "OPTIONS") { Send(stream, 204, null, Array.Empty<byte>(), headOnly: true, null); return; } if (text2 != "GET" && !flag) { Send(stream, 405, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Method not allowed"), headOnly: false, null); return; } int num = text3.IndexOfAny(new char[2] { '?', '#' }); string text4 = Uri.UnescapeDataString((num >= 0) ? text3.Substring(0, num) : text3).TrimStart('/'); if (text4.Length == 0) { text4 = "index.html"; } Snapshot current = PlayerStore.Current; if (current == null) { Send(stream, 503, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Starting"), flag, "no-store"); return; } if (PageFiles.TryGetValue(text4, out var value)) { byte[] value2; byte[] array2 = Custom(text4) ?? (_builtIn.TryGetValue(text4, out value2) ? value2 : null); if (array2 != null) { Send(stream, 200, value, array2, flag, "no-cache"); return; } } else { if (current.Json.TryGetValue(text4, out var value3)) { Send(stream, 200, "application/json; charset=utf-8", value3, flag, "no-cache"); return; } if (TryImage(current, text4, "portraits/", current.Portraits, out var file) || TryImage(current, text4, "icons/", current.Icons, out file)) { byte[] body = File.ReadAllBytes(file); Send(stream, 200, "image/png", body, flag, "public, max-age=300"); return; } } Send(stream, 404, "text/plain; charset=utf-8", Encoding.UTF8.GetBytes("Not found"), flag, "no-cache"); } private static bool TryImage(Snapshot snap, string path, string prefix, HashSet<string> known, out string file) { file = null; if (!path.StartsWith(prefix, StringComparison.Ordinal) || !path.EndsWith(".png", StringComparison.Ordinal)) { return false; } string text = path.Substring(prefix.Length, path.Length - prefix.Length - 4); if (!known.Contains(text)) { return false; } file = Path.Combine(snap.Directory, prefix.TrimEnd('/'), text + ".png"); return File.Exists(file); } private byte[] Custom(string name) { if (_customDir.Length == 0) { return null; } try { string path = Path.Combine(_customDir, name); return File.Exists(path) ? File.ReadAllBytes(path) : null; } catch (Exception) { return null; } } private static string ReadHead(NetworkStream stream) { byte[] array = new byte[8192]; int num = 0; while (num < array.Length) { int num2 = stream.Read(array, num, array.Length - num); if (num2 <= 0) { return null; } num += num2; string text = Encoding.ASCII.GetString(array, 0, num); int num3 = text.IndexOf("\r\n\r\n", StringComparison.Ordinal); if (num3 >= 0) { return text.Substring(0, num3); } } return null; } private void Send(NetworkStream stream, int status, string type, byte[] body, bool headOnly, string cache) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("HTTP/1.1 ").Append(status).Append(' ') .Append(Reason(status)) .Append("\r\n"); if (type != null) { stringBuilder.Append("Content-Type: ").Append(type).Append("\r\n"); } stringBuilder.Append("Content-Length: ").Append(body.Length).Append("\r\n"); if (cache != null) { stringBuilder.Append("Cache-Control: ").Append(cache).Append("\r\n"); } if (_allowOrigin.Length > 0) { stringBuilder.Append("Access-Control-Allow-Origin: ").Append(_allowOrigin).Append("\r\n"); stringBuilder.Append("Access-Control-Allow-Methods: GET, HEAD, OPTIONS\r\n"); } stringBuilder.Append("X-Content-Type-Options: nosniff\r\n"); stringBuilder.Append("Server: ValheimPlayerCards\r\n"); stringBuilder.Append("Connection: close\r\n\r\n"); byte[] bytes = Encoding.ASCII.GetBytes(stringBuilder.ToString()); stream.Write(bytes, 0, bytes.Length); if (!headOnly && body.Length != 0) { stream.Write(body, 0, body.Length); } stream.Flush(); } private static string Reason(int status) { return status switch { 200 => "OK", 204 => "No Content", 40