using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using BoneLib.BoneMenu;
using FusionFriends;
using FusionFriends.Core;
using LabFusion;
using LabFusion.Data;
using LabFusion.Network;
using LabFusion.Network.Proxy;
using LabFusion.Player;
using MelonLoader;
using MelonLoader.Utils;
using Microsoft.CodeAnalysis;
using Steamworks;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: MelonInfo(typeof(FriendMod), "Fusion Friends", "0.1.0", "Local Custom Mods", null)]
[assembly: MelonGame("Stress Level Zero", "BONELAB")]
[assembly: MelonAdditionalDependencies(new string[] { "BoneLib", "LabFusion" })]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("FusionFriends")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0")]
[assembly: AssemblyProduct("FusionFriends")]
[assembly: AssemblyTitle("FusionFriends")]
[assembly: AssemblyVersion("0.1.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
internal sealed class NullableAttribute : Attribute
{
public readonly byte[] NullableFlags;
public NullableAttribute(byte P_0)
{
NullableFlags = new byte[1] { P_0 };
}
public NullableAttribute(byte[] P_0)
{
NullableFlags = P_0;
}
}
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
internal sealed class NullableContextAttribute : Attribute
{
public readonly byte Flag;
public NullableContextAttribute(byte P_0)
{
Flag = P_0;
}
}
[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 FusionFriends
{
public sealed class FriendMod : MelonMod
{
private static readonly Color Accent = new Color(0.2f, 0.85f, 1f);
private readonly ConcurrentQueue<Action> _mainThread = new ConcurrentQueue<Action>();
private FriendStore _store;
private Page _root;
private Page _players;
private Page _friends;
private Page _detail;
private FunctionElement _status;
private NetworkLayer _observedLayer;
private ulong _observedLocalId;
private bool _observedLoggedIn;
private int _requestSerial;
private int _transportSerial;
private IMatchmaker _busyMatchmaker;
private bool _transportBusy;
private bool _searching;
private bool _ready;
private bool _stopped;
private DateTime _searchStarted;
private DateTime _joinCooldownUntil;
public override void OnInitializeMelon()
{
string text = Path.Combine(MelonEnvironment.UserDataDirectory, "FusionFriends", "friends.json");
_store = new FriendStore(text);
((MelonBase)this).LoggerInstance.Msg("Fusion Friends 0.1.0 loaded. Friend list: " + text);
if (_store.IsReadOnly)
{
((MelonBase)this).LoggerInstance.Warning("Friend list was preserved and is read-only: " + _store.LoadError);
}
}
public override void OnUpdate()
{
if (_stopped)
{
return;
}
if (!_ready && Page.Root != null)
{
CreateMenu();
_ready = true;
}
NetworkLayer layer = NetworkLayerManager.Layer;
ulong localPlatformID = PlayerIDManager.LocalPlatformID;
bool loggedIn = NetworkLayerManager.LoggedIn;
if (layer != _observedLayer || localPlatformID != _observedLocalId || loggedIn != _observedLoggedIn)
{
if (_searching)
{
CancelSearch("Search canceled: Fusion connection changed.");
}
else
{
_requestSerial++;
}
_observedLayer = layer;
_observedLocalId = localPlatformID;
_observedLoggedIn = loggedIn;
}
if (_searching && DateTime.UtcNow - _searchStarted > TimeSpan.FromSeconds(45.0))
{
CancelSearch("Search timed out. If a new search is blocked, log out and back into Fusion.");
}
for (int i = 0; i < 16; i++)
{
if (!_mainThread.TryDequeue(out var result))
{
break;
}
try
{
result();
}
catch (Exception ex)
{
Fail("Friend action failed", ex);
}
}
}
public override void OnDeinitializeMelon()
{
_stopped = true;
_requestSerial++;
Menu.OnPageOpened -= OnPageOpened;
}
private void CreateMenu()
{
//IL_000b: Unknown result type (might be due to invalid IL or missing references)
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_0062: Unknown result type (might be due to invalid IL or missing references)
//IL_007f: Unknown result type (might be due to invalid IL or missing references)
//IL_009c: Unknown result type (might be due to invalid IL or missing references)
//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
//IL_00ac: Expected O, but got Unknown
//IL_00b7: Unknown result type (might be due to invalid IL or missing references)
_root = Page.Root.CreatePage("Fusion Friends", Accent, 0, true);
_status = _root.CreateFunction("Save each other once to join without codes", Color.white, (Action)delegate
{
Tell("Fusion Friends", "Join the same lobby once. In Add from lobby, each of you selects the other person's name. Your lists save automatically on your own devices.");
});
_players = _root.CreatePage("Add from lobby", Accent, 0, true);
_friends = _root.CreatePage("Saved friends", Accent, 0, true);
_detail = new Page(_root, "Friend", Accent, 0);
_root.CreateFunction("How joining works", Color.white, (Action)delegate
{
Tell("Joining friends", "Join current lobby searches listed lobbies, even when your friend is not the host. Join hosted lobby connects directly when your friend is hosting. Normal lobby permissions and capacity still apply.");
});
Menu.OnPageOpened += OnPageOpened;
RefreshPlayers();
RefreshFriends();
}
private void OnPageOpened(Page page)
{
try
{
if (page == _players)
{
RefreshPlayers();
}
else if (page == _friends)
{
RefreshFriends();
}
}
catch (Exception ex)
{
Fail("Menu refresh failed", ex);
}
}
private bool CheckConnection()
{
if (!NetworkLayerManager.LoggedIn || !IsSupported(NetworkLayerManager.Layer) || PlayerIDManager.LocalPlatformID == 0L)
{
Tell("Connect to Fusion", "Use SteamVR on PC or Proxy SteamVR with Fusion Helper on Quest, then log in to Fusion. This build supports that matching pair of network layers.");
return false;
}
return true;
}
private static bool IsSupported(NetworkLayer layer)
{
SteamNetworkLayer val = (SteamNetworkLayer)(object)((layer is SteamNetworkLayer) ? layer : null);
if (val == null)
{
ProxyNetworkLayer val2 = (ProxyNetworkLayer)(object)((layer is ProxyNetworkLayer) ? layer : null);
if (val2 != null)
{
return val2.ApplicationID == 250820;
}
return false;
}
return val.ApplicationID == 250820;
}
private void RefreshPlayers()
{
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
//IL_004b: Unknown result type (might be due to invalid IL or missing references)
//IL_0148: Unknown result type (might be due to invalid IL or missing references)
//IL_0141: Unknown result type (might be due to invalid IL or missing references)
//IL_017f: Unknown result type (might be due to invalid IL or missing references)
//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
_players.RemoveAll();
_players.CreateFunction("Refresh players", Color.white, (Action)RefreshPlayers);
if (!NetworkInfo.HasServer || !IsSupported(NetworkLayerManager.Layer))
{
_players.CreateFunction("Join a Fusion lobby together first", Color.gray, (Action)delegate
{
Tell("Meet once", "Join the same Fusion lobby once, then select your friend's name here. Each of you saves the other person on your own device.");
});
return;
}
int num = 0;
PlayerID[] array = PlayerIDManager.PlayerIDs.ToArray();
foreach (PlayerID val in array)
{
if (val != null && val.IsValid && !val.IsMe)
{
ulong platformId = val.PlatformID;
string name;
try
{
name = new PlayerInfo(val).Username;
}
catch
{
name = "Player";
}
name = FriendStore.SanitizeName(name);
bool flag = _store.Entries.Any((FriendEntry x) => x.PlatformId == platformId);
_players.CreateFunction((flag ? "Saved: " : "Add: ") + name, flag ? Color.gray : Accent, (Action)delegate
{
AddFriend(platformId, name);
});
num++;
}
}
if (num == 0)
{
_players.CreateFunction("No other players in this lobby", Color.gray, (Action)RefreshPlayers);
}
}
private void AddFriend(ulong platformId, string name)
{
if (CheckConnection())
{
if (!NetworkInfo.HasServer || !PlayerIDManager.HasPlayerID(platformId))
{
Tell("Player left", "Refresh players and select someone who is still in your lobby.");
return;
}
if (!_store.Add(platformId, name, PlayerIDManager.LocalPlatformID))
{
Tell("Friend not added", _store.LastError ?? "This player is already saved.");
return;
}
RefreshPlayers();
RefreshFriends();
SetStatus("Saved " + name);
Tell("Friend saved", name + " is saved on this device. Have them save you too so they can join you.");
}
}
private void RefreshFriends()
{
//IL_004a: 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_00e3: Unknown result type (might be due to invalid IL or missing references)
//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
_friends.RemoveAll();
foreach (FriendEntry entry in _store.Entries)
{
FriendEntry friend = entry;
_friends.CreateFunction(friend.Name, Accent, (Action)delegate
{
OpenFriend(friend);
});
}
if (_store.Entries.Count == 0)
{
_friends.CreateFunction("Add each other in the same lobby first", Color.gray, (Action)delegate
{
Menu.OpenPage(_players);
});
}
if (_store.IsReadOnly)
{
_friends.CreateFunction("Friend file needs recovery", Color.yellow, (Action)delegate
{
Tell("Friend file preserved", _store.LoadError);
});
}
_friends.CreateFunction("Reload saved file", Color.white, (Action)delegate
{
_requestSerial++;
_searching = false;
bool flag = _store.Reload();
RefreshFriends();
Tell(flag ? "Friends reloaded" : "Friend file preserved", flag ? "The saved list was reloaded from this device." : _store.LoadError);
});
}
private void OpenFriend(FriendEntry friend)
{
//IL_0063: Unknown result type (might be due to invalid IL or missing references)
//IL_0085: 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_00dc: Unknown result type (might be due to invalid IL or missing references)
if (_searching)
{
CancelSearch("Search canceled: another friend was selected.");
}
else
{
_requestSerial++;
}
_detail.RemoveAll();
_detail.Name = friend.Name;
_detail.CreateFunction("Join current lobby", Accent, (Action)delegate
{
FindCurrentLobby(friend);
});
_detail.CreateFunction("Join hosted lobby", Accent, (Action)delegate
{
JoinHostedLobby(friend);
});
_detail.CreateFunction("What is the difference?", Color.white, (Action)delegate
{
Tell("Join options", "Current lobby searches for your friend in public or visible friends-only lobbies. Hosted lobby connects to your friend's own server, including a private server they are hosting. If they are in somebody else's private lobby, request an invitation or code from its host.");
});
_detail.CreateFunction("Remove saved friend", Color.red, (Action)delegate
{
Menu.DisplayDialog("Remove friend?", "Remove " + friend.Name + " from this device's saved list?", (Texture2D)null, (Action)delegate
{
RemoveFriend(friend);
}, (Action)delegate
{
});
});
Menu.OpenPage(_detail);
}
private void RemoveFriend(FriendEntry friend)
{
if (!_store.Remove(friend.PlatformId))
{
Tell("Friend not removed", _store.LastError ?? "The friend could not be removed.");
return;
}
_requestSerial++;
_searching = false;
RefreshFriends();
SetStatus("Friend removed");
Menu.OpenPage(_friends);
}
private bool CanJoin(FriendEntry friend)
{
if (!CheckConnection())
{
return false;
}
if (!_store.Entries.Any((FriendEntry x) => x.PlatformId == friend.PlatformId))
{
Tell("Friend removed", "Choose a friend from your saved list.");
return false;
}
if (friend.PlatformId == PlayerIDManager.LocalPlatformID)
{
Tell("Same Steam account", "This saved ID is your current Steam account. You each need a separate Steam account, including the account used by Fusion Helper.");
return false;
}
if (NetworkInfo.HasServer && PlayerIDManager.HasPlayerID(friend.PlatformId))
{
Tell("Already together", friend.Name + " is already in your current lobby.");
return false;
}
if (DateTime.UtcNow < _joinCooldownUntil)
{
Tell("Connecting", "Wait for Fusion's connection attempt before trying again.");
return false;
}
return true;
}
private void FindCurrentLobby(FriendEntry friend)
{
//IL_00f5: Unknown result type (might be due to invalid IL or missing references)
if (!CanJoin(friend))
{
return;
}
if (_searching)
{
Tell("Search in progress", "Wait for the current lobby search to finish.");
return;
}
NetworkLayer layer = NetworkLayerManager.Layer;
IMatchmaker matchmaker = layer.Matchmaker;
if (matchmaker == null)
{
Tell("Matchmaking unavailable", "Finish logging in to Fusion and check Fusion Helper.");
return;
}
if (_transportBusy && matchmaker == _busyMatchmaker)
{
Tell("Previous search finishing", "Wait for the previous Fusion search to return. If Fusion Helper timed out, log out and back into Fusion before searching again.");
return;
}
int serial = ++_requestSerial;
int transportSerial = ++_transportSerial;
_busyMatchmaker = matchmaker;
_transportBusy = true;
_searching = true;
_searchStarted = DateTime.UtcNow;
SetStatus("Searching for " + friend.Name + "...");
try
{
matchmaker.RequestLobbies(MatchmakerFilters.Empty, (Action<MatchmakerCallbackInfo>)delegate(MatchmakerCallbackInfo result)
{
//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)
_mainThread.Enqueue(delegate
{
//IL_005b: Unknown result type (might be due to invalid IL or missing references)
if (transportSerial == _transportSerial)
{
_transportBusy = false;
}
CompleteSearch(serial, layer, friend, result);
});
});
}
catch (Exception ex)
{
CancelSearch("Lobby search failed");
Fail("Lobby search failed", ex);
}
}
private void CompleteSearch(int serial, NetworkLayer layer, FriendEntry friend, MatchmakerCallbackInfo result)
{
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
//IL_0122: Unknown result type (might be due to invalid IL or missing references)
//IL_0127: Unknown result type (might be due to invalid IL or missing references)
//IL_0128: 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)
//IL_012e: Unknown result type (might be due to invalid IL or missing references)
//IL_013a: Unknown result type (might be due to invalid IL or missing references)
//IL_0140: Invalid comparison between Unknown and I4
//IL_0144: Unknown result type (might be due to invalid IL or missing references)
//IL_014a: Invalid comparison between Unknown and I4
//IL_01bd: Unknown result type (might be due to invalid IL or missing references)
//IL_01c3: Invalid comparison between Unknown and I4
//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
//IL_01ee: Unknown result type (might be due to invalid IL or missing references)
if (_stopped || serial != _requestSerial || layer != NetworkLayerManager.Layer)
{
return;
}
_searching = false;
if (!CanJoin(friend))
{
return;
}
LobbyInfo[] array = (from x in (result.Lobbies ?? Array.Empty<LobbyInfo>()).Where((LobbyInfo x) => ((LobbyMetadataInfo)(ref x.Metadata)).HasLobbyOpen && ((LobbyMetadataInfo)(ref x.Metadata)).LobbyInfo != null).Where(delegate(LobbyInfo x)
{
if (((LobbyMetadataInfo)(ref x.Metadata)).LobbyInfo.LobbyID != friend.PlatformId)
{
PlayerList playerList = ((LobbyMetadataInfo)(ref x.Metadata)).LobbyInfo.PlayerList;
if (playerList == null)
{
return false;
}
return playerList.Players?.Any((PlayerInfo p) => p != null && p.PlatformID == friend.PlatformId) == true;
}
return true;
})
group x by ((LobbyMetadataInfo)(ref x.Metadata)).LobbyInfo.LobbyID into g
select g.First()).ToArray();
if (array.Length == 0)
{
SetStatus("No listed lobby found for " + friend.Name);
Tell("No listed lobby found", "Your friend may be offline, hosting privately, or in an unlisted lobby. If they are hosting, select Join hosted lobby. If somebody else hosts a private lobby, ask that host for an invitation or code.");
return;
}
if (array.Length > 1)
{
SetStatus("Lobby listing is updating");
Tell("Multiple listings", "Fusion returned more than one lobby for this player. Wait a few seconds and search again.");
return;
}
LobbyInfo val = array[0];
LobbyMetadataInfo metadata = val.Metadata;
LobbyInfo lobbyInfo = ((LobbyMetadataInfo)(ref metadata)).LobbyInfo;
if ((int)((LobbyMetadataInfo)(ref metadata)).Privacy == 1 || (int)((LobbyMetadataInfo)(ref metadata)).Privacy == 3)
{
Tell("Lobby is restricted", "Request an invitation or code from the host.");
return;
}
if (((LobbyMetadataInfo)(ref metadata)).Full || lobbyInfo.PlayerCount >= lobbyInfo.MaxPlayers)
{
Tell("Lobby full", "Wait for a place to open in your friend's lobby.");
return;
}
if (((LobbyMetadataInfo)(ref metadata)).VersionMajor != FusionMod.Version.Major || ((LobbyMetadataInfo)(ref metadata)).VersionMinor != FusionMod.Version.Minor)
{
Tell("Fusion versions differ", "Your friend's lobby uses a different Fusion version. Use compatible Fusion versions before joining.");
return;
}
if ((int)((LobbyMetadataInfo)(ref metadata)).Privacy == 2 && !layer.IsFriend(lobbyInfo.LobbyID))
{
Tell("Steam friendship required", "This lobby is friends-only. Its host must add the Steam account you use for Fusion, including the account used by Fusion Helper. Saving someone here does not change Steam's friends list.");
return;
}
Action action = ((val.Lobby == null) ? null : ((LobbyMetadataInfo)(ref metadata)).CreateJoinDelegate(val.Lobby));
if (action == null)
{
Tell("Join unavailable", "The current network layer did not provide a join action.");
return;
}
ConfirmAndJoin(friend, layer, action, "Join " + friend.Name + " in " + FriendStore.SanitizeName(lobbyInfo.LobbyName) + "?");
}
private void JoinHostedLobby(FriendEntry friend)
{
if (!CanJoin(friend))
{
return;
}
if (_searching)
{
CancelSearch("Search canceled for direct join");
}
else
{
_requestSerial++;
}
NetworkLayer layer = NetworkLayerManager.Layer;
SteamNetworkLayer val = (SteamNetworkLayer)(object)((layer is SteamNetworkLayer) ? layer : null);
Action action;
if (val == null)
{
ProxyNetworkLayer val2 = (ProxyNetworkLayer)(object)((layer is ProxyNetworkLayer) ? layer : null);
action = ((val2 == null) ? null : ((Action)delegate
{
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
val2.JoinServer(SteamId.op_Implicit(friend.PlatformId));
}));
}
else
{
action = delegate
{
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
val.JoinServer(SteamId.op_Implicit(friend.PlatformId));
};
}
Action action2 = action;
if (action2 != null)
{
ConfirmAndJoin(friend, layer, action2, "Connect to " + friend.Name + "'s hosted lobby? They must be online and hosting. If they joined another person's lobby, use Join current lobby instead.", alwaysConfirm: true);
}
}
private void ConfirmAndJoin(FriendEntry friend, NetworkLayer layer, Action join, string message, bool alwaysConfirm = false)
{
int serial = _requestSerial;
ulong localId = PlayerIDManager.LocalPlatformID;
if (alwaysConfirm || NetworkInfo.HasServer)
{
if (NetworkInfo.IsHost)
{
message += " Leaving your hosted lobby will disconnect its other players.";
}
else if (NetworkInfo.HasServer)
{
message += " This leaves your current lobby.";
}
Menu.DisplayDialog("Join friend?", message, (Texture2D)null, (Action)Connect, (Action)delegate
{
});
}
else
{
Connect();
}
void Connect()
{
if (_stopped || serial != _requestSerial || localId != PlayerIDManager.LocalPlatformID || layer != NetworkLayerManager.Layer || !CanJoin(friend))
{
return;
}
try
{
_requestSerial++;
_searching = false;
_joinCooldownUntil = DateTime.UtcNow.AddSeconds(10.0);
join();
SetStatus("Connection requested for " + friend.Name);
}
catch (Exception ex)
{
Fail("Connection request failed", ex);
}
}
}
private void CancelSearch(string message)
{
_requestSerial++;
_searching = false;
SetStatus(message);
}
private void SetStatus(string message)
{
if (_status != null)
{
((Element)_status).ElementName = message;
}
((MelonBase)this).LoggerInstance.Msg(message);
}
private static void Tell(string title, string message)
{
Menu.DisplayDialog(title, message ?? "No details available.", (Texture2D)null, (Action)delegate
{
}, (Action)delegate
{
});
}
private void Fail(string title, Exception ex)
{
((MelonBase)this).LoggerInstance.Error(title + ": " + ex.GetType().Name + ": " + ex.Message);
_searching = false;
SetStatus(title);
Tell(title, "Check the MelonLoader log for details. No join was confirmed by this mod.");
}
}
}
namespace FusionFriends.Core
{
public sealed class FriendEntry
{
public ulong PlatformId { get; }
public string Name { get; }
public FriendEntry(ulong platformId, string name)
{
PlatformId = platformId;
Name = name;
}
}
public sealed class FriendStore
{
public const int CurrentVersion = 1;
public const int MaximumFriends = 128;
public const int MaximumNameLength = 64;
public const int MaximumFileBytes = 262144;
private readonly object _gate = new object();
private readonly string _path;
private List<FriendEntry> _entries = new List<FriendEntry>();
private byte[]? _expectedHash;
public string FilePath => _path;
public string BackupPath => _path + ".bak";
public bool IsReadOnly { get; private set; }
public string? LoadError { get; private set; }
public string? LastError { get; private set; }
public IReadOnlyList<FriendEntry> Entries
{
get
{
lock (_gate)
{
return _entries.ToArray();
}
}
}
public FriendStore(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("A friends file path is required.", "path");
}
_path = Path.GetFullPath(path);
Reload();
}
public bool Reload()
{
lock (_gate)
{
try
{
byte[] array = ReadExistingFile();
List<FriendEntry> entries = ((array == null) ? new List<FriendEntry>() : Parse(array));
_entries = entries;
_expectedHash = ((array == null) ? null : SHA256.HashData(array));
IsReadOnly = false;
LoadError = null;
LastError = null;
return true;
}
catch (Exception ex) when (IsStorageOrDataError(ex))
{
IsReadOnly = true;
LoadError = "The friends file could not be loaded; it has been preserved. " + ex.Message;
LastError = LoadError;
return false;
}
}
}
public bool Add(ulong platformId, string? name, ulong localPlatformId)
{
lock (_gate)
{
LastError = null;
if (IsReadOnly)
{
return Reject("The friends file is read-only because loading failed or it changed externally.");
}
if (platformId == 0L || localPlatformId == 0L)
{
return Reject("A valid local and friend platform ID is required.");
}
if (platformId == localPlatformId)
{
return Reject("You cannot add yourself.");
}
if (_entries.Any((FriendEntry entry) => entry.PlatformId == platformId))
{
return Reject("That player is already saved.");
}
if (_entries.Count >= 128)
{
return Reject("The friends list is full (128 entries).");
}
List<FriendEntry> candidate = new List<FriendEntry>(_entries)
{
new FriendEntry(platformId, SanitizeName(name))
};
return Commit(candidate);
}
}
public bool Remove(ulong platformId)
{
lock (_gate)
{
LastError = null;
if (IsReadOnly)
{
return Reject("The friends file is read-only because loading failed or it changed externally.");
}
if (!_entries.Any((FriendEntry entry) => entry.PlatformId == platformId))
{
return Reject("That player is not saved.");
}
return Commit(_entries.Where((FriendEntry entry) => entry.PlatformId != platformId).ToList());
}
}
public static string SanitizeName(string? name)
{
StringBuilder stringBuilder = new StringBuilder(64);
string text = name ?? string.Empty;
foreach (char c in text)
{
if (!char.IsControl(c) && c != '<' && c != '>')
{
stringBuilder.Append(c);
}
if (stringBuilder.Length >= 64)
{
break;
}
}
if (stringBuilder.Length > 0 && char.IsHighSurrogate(stringBuilder[stringBuilder.Length - 1]))
{
stringBuilder.Length--;
}
string text2 = stringBuilder.ToString().Trim();
if (text2.Length != 0)
{
return text2;
}
return "Player";
}
private List<FriendEntry> Parse(byte[] bytes)
{
using JsonDocument jsonDocument = JsonDocument.Parse(bytes, new JsonDocumentOptions
{
MaxDepth = 8
});
JsonElement rootElement = jsonDocument.RootElement;
if (rootElement.ValueKind != JsonValueKind.Object || !rootElement.TryGetProperty("version", out var value) || !value.TryGetInt32(out var value2) || value2 != 1)
{
throw new InvalidDataException("Unsupported or missing friends-file version.");
}
if (!rootElement.TryGetProperty("friends", out var value3) || value3.ValueKind != JsonValueKind.Array)
{
throw new InvalidDataException("The friends array is missing.");
}
if (value3.GetArrayLength() > 128)
{
throw new InvalidDataException("The friends file exceeds the 128-entry limit.");
}
List<FriendEntry> list = new List<FriendEntry>();
HashSet<ulong> hashSet = new HashSet<ulong>();
foreach (JsonElement item in value3.EnumerateArray())
{
if (item.ValueKind != JsonValueKind.Object || !item.TryGetProperty("platformId", out var value4) || !value4.TryGetUInt64(out var value5) || value5 == 0L || !item.TryGetProperty("name", out var value6) || value6.ValueKind != JsonValueKind.String)
{
throw new InvalidDataException("A friend entry contains an invalid identity or name.");
}
if (!hashSet.Add(value5))
{
throw new InvalidDataException("The friends file contains duplicate identities.");
}
list.Add(new FriendEntry(value5, SanitizeName(value6.GetString())));
}
return list;
}
private byte[]? ReadExistingFile()
{
try
{
using FileStream fileStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read);
if (fileStream.Length > 262144)
{
throw new InvalidDataException("The friends file is too large.");
}
using MemoryStream memoryStream = new MemoryStream();
byte[] array = new byte[4096];
int num;
while ((num = fileStream.Read(array, 0, array.Length)) > 0)
{
if (memoryStream.Length + num > 262144)
{
throw new InvalidDataException("The friends file is too large.");
}
memoryStream.Write(array, 0, num);
}
return memoryStream.ToArray();
}
catch (FileNotFoundException)
{
return null;
}
catch (DirectoryNotFoundException)
{
return null;
}
}
private bool Commit(List<FriendEntry> candidate)
{
string text = null;
try
{
byte[] array = ReadExistingFile();
if (!SameHash((array == null) ? null : SHA256.HashData(array), _expectedHash))
{
IsReadOnly = true;
LoadError = "The friends file changed externally. Reload it before saving.";
return Reject(LoadError);
}
Directory.CreateDirectory(Path.GetDirectoryName(_path));
text = _path + ".tmp-" + Guid.NewGuid().ToString("N");
byte[] array2;
using (MemoryStream memoryStream = new MemoryStream())
{
using (Utf8JsonWriter utf8JsonWriter = new Utf8JsonWriter(memoryStream, new JsonWriterOptions
{
Indented = true
}))
{
utf8JsonWriter.WriteStartObject();
utf8JsonWriter.WriteNumber("version", 1);
utf8JsonWriter.WriteStartArray("friends");
foreach (FriendEntry item in candidate)
{
utf8JsonWriter.WriteStartObject();
utf8JsonWriter.WriteNumber("platformId", item.PlatformId);
utf8JsonWriter.WriteString("name", item.Name);
utf8JsonWriter.WriteEndObject();
}
utf8JsonWriter.WriteEndArray();
utf8JsonWriter.WriteEndObject();
}
array2 = memoryStream.ToArray();
}
using (FileStream fileStream = new FileStream(text, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
fileStream.Write(array2, 0, array2.Length);
fileStream.Flush(flushToDisk: true);
}
if (_expectedHash == null)
{
File.Move(text, _path);
}
else
{
File.Replace(text, _path, BackupPath, ignoreMetadataErrors: false);
}
text = null;
_entries = candidate;
_expectedHash = SHA256.HashData(array2);
LastError = null;
return true;
}
catch (Exception ex) when (IsStorageOrDataError(ex))
{
return Reject("The friends list was not saved; the current list was kept. " + ex.Message);
}
finally
{
if (text != null)
{
try
{
File.Delete(text);
}
catch (Exception ex2) when (IsStorageOrDataError(ex2))
{
}
}
}
}
private bool Reject(string message)
{
LastError = message;
return false;
}
private static bool SameHash(byte[]? first, byte[]? second)
{
if (first != null)
{
if (second != null)
{
return ((ReadOnlySpan<byte>)first).SequenceEqual((ReadOnlySpan<byte>)second);
}
return false;
}
return second == null;
}
private static bool IsStorageOrDataError(Exception ex)
{
if (!(ex is IOException) && !(ex is InvalidDataException) && !(ex is UnauthorizedAccessException) && !(ex is JsonException) && !(ex is InvalidOperationException) && !(ex is ArgumentException))
{
return ex is NotSupportedException;
}
return true;
}
}
}