diff --git a/cheats/MovementAndInputHelpers.cs b/cheats/MovementAndInputHelpers.cs
index 8935d64..5553a94 100644
--- a/cheats/MovementAndInputHelpers.cs
+++ b/cheats/MovementAndInputHelpers.cs
@@ -122,6 +122,17 @@ private static string SanitizeSpoofFriendCode(string input)
return string.IsNullOrWhiteSpace(input) ? "" : input.Trim();
}
+ [HideFromIl2Cpp]
+ internal static bool CanSendSpoofFriendCode(string input)
+ {
+ string value = SanitizeSpoofFriendCode(input);
+ if (string.IsNullOrEmpty(value)) return false;
+
+ int hash = value.LastIndexOf('#');
+ string username = hash > 0 ? value.Substring(0, hash) : value;
+ return username.Length <= 10;
+ }
+
private static string SanitizeHexColor(string input, string fallback)
{
string value = (input ?? string.Empty).Trim();
diff --git a/features/FriendCodeSpoofPatch.cs b/features/FriendCodeSpoofPatch.cs
index e874a7b..ff1db58 100644
--- a/features/FriendCodeSpoofPatch.cs
+++ b/features/FriendCodeSpoofPatch.cs
@@ -53,6 +53,11 @@ public static bool Prefix(
string value = (ElysiumModMenuGUI.spoofFriendCodeInput ?? string.Empty).Trim();
if (string.IsNullOrEmpty(value)) return true;
+ if (!ElysiumModMenuGUI.CanSendSpoofFriendCode(value))
+ {
+ ElysiumModMenuGUI.ShowNotification("[SPOOF FC] Server limit: 10 characters.");
+ return false;
+ }
int hash = value.LastIndexOf('#');
if (hash <= 0)
@@ -154,6 +159,11 @@ public static bool Prefix(EditAccountUsername __instance)
string value = (ElysiumModMenuGUI.spoofFriendCodeInput ?? string.Empty).Trim();
if (string.IsNullOrEmpty(value)) return true;
+ if (!ElysiumModMenuGUI.CanSendSpoofFriendCode(value))
+ {
+ ElysiumModMenuGUI.ShowNotification("[SPOOF FC] Server limit: 10 characters.");
+ return false;
+ }
try
{
@@ -170,8 +180,9 @@ public static bool Prefix(EditAccountUsername __instance)
mgr.SetFriendCode(value, callback);
return false;
}
- catch
+ catch (Exception ex)
{
+ Plugin.Instance?.Log?.LogWarning((object)$"[SPOOF FC] Submit failed: {ex.Message}");
return true;
}
}
diff --git a/features/PlayerHistoryStore.cs b/features/PlayerHistoryStore.cs
index 89c89db..9fe25c9 100644
--- a/features/PlayerHistoryStore.cs
+++ b/features/PlayerHistoryStore.cs
@@ -514,7 +514,8 @@ private static void RebuildPlayerHistoryViewCache()
Identity = $"Lv: {e.Level} | FC: {e.FriendCode} | PUID: {e.Puid}",
Times = $"Joined: {e.FirstSeenUtc:HH:mm:ss} | Left: {(e.LeftUtc.HasValue ? e.LeftUtc.Value.ToString("HH:mm:ss") : "online")}",
Platform = $"Platform: {FormatPlatformHistory(e)}",
- Rpc = $"RPC: {FormatRpcHistory(e)}"
+ Rpc = $"RPC: {FormatRpcHistory(e)}",
+ Puid = e.Puid
});
}
diff --git a/menu/MenuInputAndTabs.cs b/menu/MenuInputAndTabs.cs
index 86885c2..d65544e 100644
--- a/menu/MenuInputAndTabs.cs
+++ b/menu/MenuInputAndTabs.cs
@@ -490,6 +490,7 @@ private sealed class PlayerHistoryViewRow
public string Times;
public string Platform;
public string Rpc;
+ public string Puid;
}
private static readonly Dictionary playerHistoryEntryLookup = new Dictionary();
@@ -498,7 +499,7 @@ private sealed class PlayerHistoryViewRow
private static bool playerHistoryViewDirty = true;
-private const float PlayerHistoryRowHeight = 92f;
+private const float PlayerHistoryRowHeight = 108f;
private static bool playerHistoryLoaded = false;
diff --git a/uimenu/PlayerAccounts.cs b/uimenu/PlayerAccounts.cs
new file mode 100644
index 0000000..b21933f
--- /dev/null
+++ b/uimenu/PlayerAccounts.cs
@@ -0,0 +1,525 @@
+#nullable disable
+using Epic.OnlineServices;
+using Epic.OnlineServices.Connect;
+using Il2CppInterop.Runtime.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ElysiumModMenu
+{
+ public partial class ElysiumModMenuGUI
+ {
+ private sealed class PlayerAccountEntry
+ {
+ public ExternalAccountType type;
+ public string name;
+ public string id;
+ public string lastLogin;
+ }
+
+ private readonly Dictionary> playerAccountsByPuid = new Dictionary>();
+ private readonly Dictionary playerAccountTextByPuid = new Dictionary();
+ private readonly HashSet playerAccountLoaded = new HashSet();
+ private readonly HashSet playerAccountFailed = new HashSet();
+ private readonly HashSet playerAccountQueued = new HashSet();
+ private readonly Queue playerAccountQueue = new Queue();
+ private string playerAccountLookupPuid;
+ private int playerAccountPendingId;
+ private float playerAccountRequestAt;
+
+ [HideFromIl2Cpp]
+ private string GetPlayerHistoryAccountText(string puid)
+ {
+ UpdatePlayerAccountLookup();
+
+ if (string.IsNullOrWhiteSpace(puid) || puid == "Unknown")
+ return L("Accounts: unavailable", "Аккаунты: недоступны");
+
+ puid = puid.Trim();
+ if (playerAccountLoaded.Contains(puid))
+ {
+ if (playerAccountTextByPuid.TryGetValue(puid, out string text))
+ return $"{L("Accounts:", "Аккаунты:")} {text}";
+
+ return playerAccountFailed.Contains(puid)
+ ? L("Accounts: cache unavailable", "Аккаунты: кэш недоступен")
+ : L("Accounts: not cached", "Аккаунты: нет в кэше");
+ }
+
+ LoadCachedPlayerAccounts(puid);
+ UpdatePlayerAccountLookup();
+ if (!playerAccountLoaded.Contains(puid))
+ return L("Accounts: searching...", "Аккаунты: поиск...");
+
+ if (playerAccountTextByPuid.TryGetValue(puid, out string accountText))
+ return $"{L("Accounts:", "Аккаунты:")} {accountText}";
+
+ return playerAccountFailed.Contains(puid)
+ ? L("Accounts: cache unavailable", "Аккаунты: кэш недоступен")
+ : L("Accounts: not cached", "Аккаунты: нет в кэше");
+ }
+
+ [HideFromIl2Cpp]
+ private void LoadCachedPlayerAccounts(string puid)
+ {
+ if (playerAccountLoaded.Contains(puid) || playerAccountQueued.Contains(puid) || playerAccountLookupPuid == puid) return;
+
+ playerAccountsByPuid[puid] = new List();
+ ReadLocalPlayerAccounts(puid);
+ ReadCachedPlayerAccounts(puid);
+ playerAccountQueued.Add(puid);
+ playerAccountQueue.Enqueue(puid);
+ }
+
+ [HideFromIl2Cpp]
+ private void ReadLocalPlayerAccounts(string puid)
+ {
+ try
+ {
+ EOSManager eos = EOSManager.Instance;
+ ProductUserId local = eos?.userId;
+ if (local == null || local.ToString() != puid) return;
+
+ if (eos.linkedExternalAccounts != null)
+ {
+ for (int i = 0; i < eos.linkedExternalAccounts.Count; i++)
+ {
+ try
+ {
+ AddPlayerAccount(puid, eos.linkedExternalAccounts[i]);
+ }
+ catch (Exception ex)
+ {
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] Local account {i} read failed: {ex.Message}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ playerAccountFailed.Add(puid);
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] Local cache read failed: {ex.Message}");
+ }
+ }
+
+ [HideFromIl2Cpp]
+ private void ReadCachedPlayerAccounts(string puid)
+ {
+ try
+ {
+ EOSManager eos = EOSManager.Instance;
+ var connect = eos?.PlatformInterface?.GetConnectInterface();
+ ProductUserId targetPuid = ProductUserId.FromString(puid);
+ if (connect == null || targetPuid == null || !targetPuid.IsValid())
+ {
+ playerAccountFailed.Add(puid);
+ return;
+ }
+
+ var countOptions = new GetProductUserExternalAccountCountOptions
+ {
+ TargetUserId = targetPuid
+ };
+ uint count = connect.GetProductUserExternalAccountCount(ref countOptions);
+ for (uint i = 0; i < count; i++)
+ {
+ try
+ {
+ var copyOptions = new CopyProductUserExternalAccountByIndexOptions
+ {
+ TargetUserId = targetPuid,
+ ExternalAccountInfoIndex = i
+ };
+ Result result = connect.CopyProductUserExternalAccountByIndex(ref copyOptions, out var info);
+ if (result == Result.Success && info.HasValue)
+ AddPlayerAccount(puid, info.Value);
+ else if (result != Result.Success)
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] EOS account {i}/{count} copy failed: {result}");
+ }
+ catch (Exception ex)
+ {
+ playerAccountFailed.Add(puid);
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] EOS account {i}/{count} read failed: {ex.Message}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ playerAccountFailed.Add(puid);
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] EOS cache read failed: {ex.Message}");
+ }
+ }
+
+ [HideFromIl2Cpp]
+ private void UpdatePlayerAccountLookup()
+ {
+ if (playerAccountPendingId != 0)
+ {
+ if (!PlayerAccountNative.TryTake(playerAccountPendingId, out string puid, out int result))
+ {
+ if (UnityEngine.Time.realtimeSinceStartup - playerAccountRequestAt < 12f)
+ return;
+
+ PlayerAccountNative.Forget(playerAccountPendingId);
+ puid = playerAccountLookupPuid;
+ result = -1;
+ }
+
+ playerAccountPendingId = 0;
+ if (result == (int)Result.Success)
+ ReadCachedPlayerAccounts(puid);
+ else
+ {
+ playerAccountFailed.Add(puid);
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] PUID mapping failed for {puid}: {result}");
+ }
+
+ FinishPlayerAccountLookup(puid);
+ playerAccountQueued.Remove(puid);
+ playerAccountLookupPuid = null;
+ }
+
+ if (string.IsNullOrEmpty(playerAccountLookupPuid))
+ {
+ if (playerAccountQueue.Count == 0) return;
+ playerAccountLookupPuid = playerAccountQueue.Dequeue();
+ }
+
+ try
+ {
+ EOSManager eos = EOSManager.Instance;
+ var connect = eos?.PlatformInterface?.GetConnectInterface();
+ ProductUserId local = eos?.userId;
+ ProductUserId target = ProductUserId.FromString(playerAccountLookupPuid);
+ if (connect == null || local == null || target == null || !local.IsValid() || !target.IsValid())
+ {
+ playerAccountFailed.Add(playerAccountLookupPuid);
+ FinishPlayerAccountLookup(playerAccountLookupPuid);
+ playerAccountQueued.Remove(playerAccountLookupPuid);
+ playerAccountLookupPuid = null;
+ return;
+ }
+
+ playerAccountPendingId = PlayerAccountNative.Start(
+ connect.InnerHandle,
+ local.InnerHandle,
+ target.InnerHandle,
+ playerAccountLookupPuid);
+ playerAccountRequestAt = UnityEngine.Time.realtimeSinceStartup;
+ }
+ catch (Exception ex)
+ {
+ playerAccountFailed.Add(playerAccountLookupPuid);
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] Native PUID lookup failed: {ex.Message}");
+ FinishPlayerAccountLookup(playerAccountLookupPuid);
+ playerAccountQueued.Remove(playerAccountLookupPuid);
+ playerAccountLookupPuid = null;
+ playerAccountPendingId = 0;
+ }
+ }
+
+ [HideFromIl2Cpp]
+ private void AddPlayerAccount(string puid, ExternalAccountInfo account)
+ {
+ if (account == null) return;
+
+ ExternalAccountType type = account.AccountIdType;
+ string name = string.Empty;
+ string id = string.Empty;
+ string lastLogin = string.Empty;
+
+ try
+ {
+ if (account.DisplayName != null)
+ name = account.DisplayName.ToString();
+ }
+ catch (Exception ex)
+ {
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] {type} name read failed: {ex.Message}");
+ }
+
+ try
+ {
+ if (account.AccountId != null)
+ id = account.AccountId.ToString();
+ }
+ catch (Exception ex)
+ {
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] {type} id read failed: {ex.Message}");
+ }
+
+ try
+ {
+ var login = account.LastLoginTime;
+ if (login.HasValue)
+ lastLogin = login.Value.ToString("dd.MM.yyyy HH:mm:ss");
+ }
+ catch (Exception ex)
+ {
+ Plugin.Instance?.Log?.LogWarning((object)$"[ACCOUNTS] {type} login time read failed: {ex.Message}");
+ }
+
+ AddPlayerAccount(puid, type, name, id, lastLogin);
+ }
+
+ [HideFromIl2Cpp]
+ private void AddPlayerAccount(string puid, ExternalAccountType type, string name, string id, string lastLogin)
+ {
+ if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(id)) return;
+
+ if (!playerAccountsByPuid.TryGetValue(puid, out List accounts))
+ {
+ accounts = new List();
+ playerAccountsByPuid[puid] = accounts;
+ }
+
+ PlayerAccountEntry entry = accounts.Find(x =>
+ x.type == type &&
+ ((!string.IsNullOrWhiteSpace(id) && x.id == id) ||
+ (!string.IsNullOrWhiteSpace(name) && x.name == name &&
+ (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(x.id)))));
+ if (entry == null)
+ {
+ entry = new PlayerAccountEntry();
+ accounts.Add(entry);
+ }
+
+ entry.type = type;
+ if (!string.IsNullOrWhiteSpace(name)) entry.name = name;
+ if (!string.IsNullOrWhiteSpace(id)) entry.id = id;
+ if (!string.IsNullOrWhiteSpace(lastLogin)) entry.lastLogin = lastLogin;
+ }
+
+ [HideFromIl2Cpp]
+ private void FinishPlayerAccountLookup(string puid)
+ {
+ if (playerAccountsByPuid.TryGetValue(puid, out List accounts) && accounts.Count > 0)
+ {
+ string[] names = new string[accounts.Count];
+ var typeCounts = new Dictionary();
+ for (int i = 0; i < accounts.Count; i++)
+ {
+ PlayerAccountEntry account = accounts[i];
+ string name = string.IsNullOrWhiteSpace(account.name) ? account.id : account.name;
+ typeCounts.TryGetValue(account.type, out int count);
+ count++;
+ typeCounts[account.type] = count;
+ string type = AccountTypeName(account.type);
+ if (count > 1) type += $" ({count})";
+ names[i] = string.IsNullOrWhiteSpace(account.lastLogin)
+ ? $"{type}: {name}"
+ : $"{type}: {name} [{account.lastLogin}]";
+ }
+ playerAccountTextByPuid[puid] = string.Join(" | ", names);
+ }
+
+ playerAccountLoaded.Add(puid);
+ }
+
+ private void ClearPlayerAccountCache()
+ {
+ if (playerAccountPendingId != 0)
+ PlayerAccountNative.Forget(playerAccountPendingId);
+
+ playerAccountsByPuid.Clear();
+ playerAccountTextByPuid.Clear();
+ playerAccountLoaded.Clear();
+ playerAccountFailed.Clear();
+ playerAccountQueued.Clear();
+ playerAccountQueue.Clear();
+ playerAccountLookupPuid = null;
+ playerAccountPendingId = 0;
+ playerAccountRequestAt = 0f;
+ }
+
+ private static string AccountTypeName(ExternalAccountType type)
+ {
+ switch (type)
+ {
+ case ExternalAccountType.Epic: return "Epic";
+ case ExternalAccountType.Steam: return "Steam";
+ case ExternalAccountType.Psn: return "PlayStation";
+ case ExternalAccountType.Xbl: return "Microsoft / Xbox";
+ case ExternalAccountType.Discord: return "Discord";
+ case ExternalAccountType.Gog: return "GOG";
+ case ExternalAccountType.Nintendo: return "Nintendo";
+ case ExternalAccountType.Uplay: return "Ubisoft";
+ case ExternalAccountType.Openid: return "OpenID";
+ case ExternalAccountType.Apple: return "Apple";
+ case ExternalAccountType.Google: return "Google";
+ case ExternalAccountType.Oculus: return "Oculus";
+ case ExternalAccountType.Itchio: return "Itch";
+ case ExternalAccountType.Amazon: return "Amazon";
+ case ExternalAccountType.Viveport: return "Viveport";
+ default: return type.ToString();
+ }
+ }
+ }
+
+ internal static class PlayerAccountNative
+ {
+ [StructLayout(LayoutKind.Sequential)]
+ private struct QueryOptions
+ {
+ public int ApiVersion;
+ public IntPtr LocalUserId;
+ public int AccountIdTypeDeprecated;
+ public IntPtr ProductUserIds;
+ public uint ProductUserIdCount;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct QueryCallbackInfo
+ {
+ public int ResultCode;
+ public IntPtr ClientData;
+ public IntPtr LocalUserId;
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate void QueryCallback(IntPtr data);
+
+ [UnmanagedFunctionPointer(CallingConvention.Winapi)]
+ private delegate void QueryProductUserIdMappings(
+ IntPtr handle,
+ ref QueryOptions options,
+ IntPtr clientData,
+ QueryCallback completion);
+
+ private sealed class QueryState
+ {
+ public string puid;
+ public IntPtr ids;
+ public bool complete;
+ public bool discard;
+ public int result;
+ }
+
+ private static readonly object sync = new object();
+ private static readonly Dictionary queries = new Dictionary();
+ private static readonly QueryCallback callback = OnQueryComplete;
+ private static QueryProductUserIdMappings queryMappings;
+ private static IntPtr eosLibrary;
+ private static int nextId;
+
+ private static void LoadEos()
+ {
+ if (queryMappings != null) return;
+
+ lock (sync)
+ {
+ if (queryMappings != null) return;
+
+ bool is64 = IntPtr.Size == 8;
+ string path = System.IO.Path.Combine(UnityEngine.Application.dataPath,
+ "Plugins", is64 ? "x86_64" : "x86",
+ is64 ? "EOSSDK-Win64-Shipping.dll" : "EOSSDK-Win32-Shipping.dll");
+ string entry = is64
+ ? "EOS_Connect_QueryProductUserIdMappings"
+ : "_EOS_Connect_QueryProductUserIdMappings@16";
+ eosLibrary = NativeLibrary.Load(path);
+ IntPtr ptr = NativeLibrary.GetExport(eosLibrary, entry);
+ queryMappings = Marshal.GetDelegateForFunctionPointer(ptr);
+ }
+ }
+
+ internal static int Start(IntPtr connect, IntPtr local, IntPtr target, string puid)
+ {
+ LoadEos();
+
+ IntPtr ids = Marshal.AllocHGlobal(IntPtr.Size);
+ Marshal.WriteIntPtr(ids, target);
+
+ int id;
+ lock (sync)
+ {
+ id = ++nextId;
+ if (id == 0) id = ++nextId;
+ queries[id] = new QueryState
+ {
+ puid = puid,
+ ids = ids
+ };
+ }
+
+ var options = new QueryOptions
+ {
+ ApiVersion = 2,
+ LocalUserId = local,
+ AccountIdTypeDeprecated = 0,
+ ProductUserIds = ids,
+ ProductUserIdCount = 1
+ };
+
+ try
+ {
+ queryMappings(connect, ref options, new IntPtr(id), callback);
+ return id;
+ }
+ catch
+ {
+ lock (sync)
+ queries.Remove(id);
+ Marshal.FreeHGlobal(ids);
+ throw;
+ }
+ }
+
+ internal static bool TryTake(int id, out string puid, out int result)
+ {
+ lock (sync)
+ {
+ if (queries.TryGetValue(id, out QueryState state) && state.complete)
+ {
+ puid = state.puid;
+ result = state.result;
+ queries.Remove(id);
+ return true;
+ }
+ }
+
+ puid = null;
+ result = 0;
+ return false;
+ }
+
+ internal static void Forget(int id)
+ {
+ lock (sync)
+ {
+ if (!queries.TryGetValue(id, out QueryState state)) return;
+ if (state.complete)
+ queries.Remove(id);
+ else
+ state.discard = true;
+ }
+ }
+
+ private static void OnQueryComplete(IntPtr data)
+ {
+ try
+ {
+ if (data == IntPtr.Zero) return;
+ QueryCallbackInfo info = Marshal.PtrToStructure(data);
+ int id = info.ClientData.ToInt32();
+
+ lock (sync)
+ {
+ if (!queries.TryGetValue(id, out QueryState state)) return;
+ state.result = info.ResultCode;
+ state.complete = true;
+ if (state.ids != IntPtr.Zero)
+ {
+ Marshal.FreeHGlobal(state.ids);
+ state.ids = IntPtr.Zero;
+ }
+ if (state.discard)
+ queries.Remove(id);
+ }
+ }
+ catch { }
+ }
+ }
+
+}
diff --git a/uimenu/PlayersHistoryTab.cs b/uimenu/PlayersHistoryTab.cs
index ea73772..e0ad180 100644
--- a/uimenu/PlayersHistoryTab.cs
+++ b/uimenu/PlayersHistoryTab.cs
@@ -57,7 +57,17 @@ private void DrawPlayersHistoryTab()
GUILayout.Label($"Entries: {playerHistoryEntries.Count}", historyInfoStyle, GUILayout.MinWidth(128), GUILayout.ExpandWidth(false), GUILayout.Height(24));
GUILayout.Label(L("File: ElysiumPlayerHistory.txt", "Файл: ElysiumPlayerHistory.txt"), historyInfoStyle, GUILayout.MinWidth(220), GUILayout.ExpandWidth(false), GUILayout.Height(24));
GUILayout.FlexibleSpace();
- if (GUILayout.Button(L("Clear History", "ОЧИСТИТЬ ИСТОРИЮ"), btnStyle, GUILayout.Width(136), GUILayout.Height(24)))
+ }
+ finally { GUILayout.EndHorizontal(); }
+
+ GUILayout.BeginHorizontal();
+ try
+ {
+ GUILayout.FlexibleSpace();
+ if (GUILayout.Button(L("Refresh Accounts", "ОБНОВИТЬ АККАУНТЫ"), btnStyle, GUILayout.Width(154), GUILayout.Height(24)))
+ ClearPlayerAccountCache();
+ GUILayout.Space(4);
+ if (GUILayout.Button(L("Clear History", "ОЧИСТИТЬ ИСТОРИЮ"), btnStyle, GUILayout.Width(136), GUILayout.Height(24)))
{
playerHistoryEntries.Clear();
playerHistoryEntryLookup.Clear();
@@ -65,6 +75,7 @@ private void DrawPlayersHistoryTab()
playerHistoryKeysById.Clear();
playerHistoryKeysByClientId.Clear();
playerHistoryLoaded = true;
+ ClearPlayerAccountCache();
InvalidatePlayerHistoryViewCache();
WritePlayerHistoryFile();
}
@@ -85,7 +96,7 @@ private void DrawPlayersHistoryTab()
int rowCount = playerHistoryViewRows.Count;
int firstIndex = Mathf.Clamp(Mathf.FloorToInt(playersHistoryScroll.y / PlayerHistoryRowHeight), 0, Mathf.Max(0, rowCount - 1));
- int visibleRows = Mathf.Clamp(Mathf.CeilToInt(Mathf.Max(180f, windowRect.height - 170f) / PlayerHistoryRowHeight) + 3, 6, 30);
+ int visibleRows = Mathf.Clamp(Mathf.CeilToInt(Mathf.Max(180f, windowRect.height - 198f) / PlayerHistoryRowHeight) + 3, 6, 30);
int endIndex = Mathf.Min(rowCount, firstIndex + visibleRows);
GUILayout.Space(firstIndex * PlayerHistoryRowHeight);
@@ -100,6 +111,7 @@ private void DrawPlayersHistoryTab()
GUILayout.Label(row.Times, historyLineStyle, GUILayout.Height(16));
GUILayout.Label(row.Platform, historyWrapStyle, GUILayout.Height(16));
GUILayout.Label(row.Rpc, historyWrapStyle, GUILayout.Height(16));
+ GUILayout.Label(GetPlayerHistoryAccountText(row.Puid), historyWrapStyle, GUILayout.Height(16));
}
finally { GUILayout.EndVertical(); }
GUILayout.Space(2);
diff --git a/uimenu/SelfSpoofVisualsTab.cs b/uimenu/SelfSpoofVisualsTab.cs
index cfe5983..d5d338c 100644
--- a/uimenu/SelfSpoofVisualsTab.cs
+++ b/uimenu/SelfSpoofVisualsTab.cs
@@ -234,6 +234,11 @@ private void ApplyPublicFriendCode()
{
spoofFriendCodeInput = SanitizeSpoofFriendCode(spoofFriendCodeInput);
if (string.IsNullOrEmpty(spoofFriendCodeInput)) return;
+ if (!CanSendSpoofFriendCode(spoofFriendCodeInput))
+ {
+ ShowNotification("[SPOOF FC] Server limit: 10 characters.");
+ return;
+ }
try
{