using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using GameSaver.Asset; using GameSaver.Component; using GameSaver.Menu; using GameSaver.Network; using GameSaver.Patches; using GameSaver.Util; using HarmonyLib; using Jotunn.Utils; using Microsoft.CodeAnalysis; using ModdingUtils.Utils; using Photon.Pun; using Photon.Realtime; using RWF; using Steamworks; using TMPro; using UnboundLib; using UnboundLib.Extensions; using UnboundLib.GameModes; using UnboundLib.Networking; using UnboundLib.Utils; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyFileVersion("1.1.1")] [assembly: AssemblyInformationalVersion("1.1.1")] [assembly: IgnoresAccessChecksTo("RoundsWithFriends")] [assembly: TargetFramework(".NETFramework,Version=v4.8.1", FrameworkDisplayName = ".NET Framework 4.8.1")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.1.1.0")] [module: UnverifiableCode] [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 GameSaver { [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInPlugin("ot.dan.rounds.gamesaver", "GameSaver", "1.1.1")] [BepInProcess("Rounds.exe")] public class GameSaver : BaseUnityPlugin { private const string ModId = "ot.dan.rounds.gamesaver"; private const string ModName = "Game Saver"; public const string Version = "1.1.1"; public const string ModInitials = "GS"; private const string CompatibilityModName = "GameSaver"; public static GameSaver Instance { get; private set; } internal static bool AssetsFailed { get; private set; } internal void Awake() { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown Instance = this; ConfigManager.Initialize(((BaseUnityPlugin)this).Config); try { AssetsFailed = !AssetManager.Initialize(); } catch (Exception arg) { AssetsFailed = true; LogError($"asset initialisation threw: {arg}"); } if (AssetsFailed) { LogError("asset bundle failed to load - GameSaver is disabled for this session."); return; } SaveManager.Initialize(); OpeningPickPatch.Apply(new Harmony("ot.dan.rounds.gamesaver")); } internal void Start() { if (!AssetsFailed) { GameModeManager.AddHook("GameStart", (Func)SaveManager.GameStart, 0); GameModeManager.AddHook("GameEnd", (Func)SaveManager.GameEnd); GameModeManager.AddHook("RoundStart", (Func)SaveManager.RoundStart); GameModeManager.AddHook("RoundEnd", (Func)SaveManager.RoundEnd); GameModeManager.AddHook("PickEnd", (Func)SaveManager.PickEnd); ((Component)this).gameObject.AddComponent(); } } public void Log(string debug) { ((BaseUnityPlugin)this).Logger.LogInfo((object)debug); } internal static void LogInfo(string message) { if ((Object)(object)Instance != (Object)null) { ((BaseUnityPlugin)Instance).Logger.LogInfo((object)message); } else { Debug.Log("[GameSaver] " + message); } } internal static void LogWarning(string message) { if ((Object)(object)Instance != (Object)null) { ((BaseUnityPlugin)Instance).Logger.LogWarning((object)message); } else { Debug.LogWarning("[GameSaver] " + message); } } internal static void LogError(string message) { if ((Object)(object)Instance != (Object)null) { ((BaseUnityPlugin)Instance).Logger.LogError((object)message); } else { Debug.LogError("[GameSaver] " + message); } } } } namespace GameSaver.Util { internal static class ConfigManager { internal static ConfigEntry SaveEnabled { get; private set; } internal static ConfigEntry SaveAsHostOnly { get; private set; } internal static void Initialize(ConfigFile config) { SaveEnabled = config.Bind("General", "SaveEnabled", true, "Automatically save the state of the match after every card pick."); SaveAsHostOnly = config.Bind("General", "SaveAsHostOnly", false, "Only write save files when you are the host. Only the host can load a save, so turning this on stops non-hosts filling their disk with saves they can never use."); } } public class SaveManager { private class Match { public PlayerData Data; public Player Player; } public class GameInfoData { public GameData gameData; public List gameSaves; private int _rounds = -1; public string FilePath { get; } public int rounds { get { if (_rounds == -1) { _rounds = ((gameSaves.Count != 0) ? gameSaves.OrderByDescending((SaveData save) => save.round).First().round : 0); } return _rounds; } } internal void InvalidateRounds() { _rounds = -1; } public GameInfoData(string filePath, GameData gameData, List gameSaves) { FilePath = filePath; this.gameData = gameData; this.gameSaves = gameSaves; } } [Serializable] public class GameData { [NonSerialized] public GameObject button; public long _serializedStartTime; public int playerAmount; public string gameMode; public GameType gameType; public DateTime StartTime => DateTime.FromBinary(_serializedStartTime); public GameData(DateTime startTime, int playerAmount, string gameMode, GameType gameType) { _serializedStartTime = startTime.ToBinary(); this.playerAmount = playerAmount; this.gameMode = gameMode; this.gameType = gameType; } } [Serializable] public class SaveData { [NonSerialized] public GameObject button; [NonSerialized] public GameObject display; [NonSerialized] public TextMeshProUGUI loaded; public long _serializedDateTime; public SaveType saveType; public int round; public int pointsToWinRound = 2; public int pointsToWin; public List seralizedPlayers; private List _players; public DateTime Time => DateTime.FromBinary(_serializedDateTime); public List players { get { if (_players != null) { return _players; } List list = new List(); foreach (string item in seralizedPlayers ?? new List()) { try { PlayerData playerData = JsonUtility.FromJson(item); if (playerData != null) { list.Add(playerData); } } catch (Exception ex) { GameSaver.LogWarning("skipping unreadable player entry in save: " + ex.Message); } } return _players = list; } set { _players = value; } } internal void InvalidateCards() { if (_players == null) { return; } foreach (PlayerData player in _players) { player?.InvalidateCards(); } } public SaveData(DateTime time, SaveType saveType, int round, int pointsToWinRound, int pointsToWin, List players) { _serializedDateTime = time.ToBinary(); this.saveType = saveType; this.round = round; this.pointsToWinRound = pointsToWinRound; this.pointsToWin = pointsToWin; seralizedPlayers = players; } } [Serializable] public class PlayerData { [NonSerialized] public GameObject display; private List _cards; public int serializedTeamId; public string name; public int serializedColor; public List serializedCards; public int rounds; public int points; public bool host; public ulong steamId; public List Cards { get { if (_cards != null) { return _cards; } List list = new List(); foreach (string item in serializedCards ?? new List()) { list.Add(ResolveCard(item)); } return _cards = list; } } public PlayerSkin Color => PlayerSkinBank.GetPlayerSkinColors(serializedColor); public int TeamId => serializedTeamId - 1; public bool HasTeamId => serializedTeamId > 0; internal void InvalidateCards() { _cards = null; } private static CardInfo ResolveCard(string objectName) { if (string.IsNullOrEmpty(objectName)) { return null; } try { CardInfo cardWithObjectName = Cards.instance.GetCardWithObjectName(objectName); if (Object.op_Implicit((Object)(object)cardWithObjectName)) { return cardWithObjectName; } } catch (Exception ex) { GameSaver.LogWarning("card lookup for '" + objectName + "' failed: " + ex.GetType().Name + ": " + ex.Message); } try { CardInfo cardInfoWithName = CardManager.GetCardInfoWithName(objectName); if (Object.op_Implicit((Object)(object)cardInfoWithName)) { return cardInfoWithName; } } catch (Exception ex2) { GameSaver.LogWarning("card '" + objectName + "' is unresolvable on this client (mod not installed, or a random-card seed that was not generated this session): " + ex2.GetType().Name); } try { string b = Regex.Replace(objectName, "\\s+", ""); ReadOnlyCollection hiddenCards = Cards.instance.HiddenCards; IEnumerable second; if (!((Object)(object)CardChoice.instance != (Object)null)) { IEnumerable enumerable = (IEnumerable)(object)new CardInfo[0]; second = enumerable; } else { IEnumerable enumerable = CardChoice.instance.cards; second = enumerable; } foreach (CardInfo item in hiddenCards.Concat(second)) { if (Object.op_Implicit((Object)(object)item) && Object.op_Implicit((Object)(object)((Component)item).gameObject) && string.Equals(Regex.Replace(((Object)((Component)item).gameObject).name, "\\s+", ""), b, StringComparison.OrdinalIgnoreCase)) { return item; } } } catch (Exception ex3) { GameSaver.LogWarning("fuzzy card lookup for '" + objectName + "' failed: " + ex3.GetType().Name + ": " + ex3.Message); } return null; } public PlayerData(string name, int color, int teamId, List cards, int points, int rounds, bool host, ulong steamId) { this.name = name; serializedColor = color; serializedTeamId = teamId + 1; serializedCards = new List(); foreach (CardInfo card in cards) { if (Object.op_Implicit((Object)(object)card) && Object.op_Implicit((Object)(object)((Component)card).gameObject)) { serializedCards.Add(((Object)((Component)card).gameObject).name); } } this.points = points; this.rounds = rounds; this.host = host; this.steamId = steamId; } } public enum SaveType { PickStart, PickEnd, CardPicked, RoundStart, RoundEnd, Manual } public enum GameType { Local, Online } public static readonly string SavesPath = Path.Combine(Paths.ConfigPath, "Saves"); private static string _gameSavesPath; private static int _round; private static Guid _gameGuid; public static List _games = new List(); public static SaveData _selectedSave; private static GameObject _savingObject; public static List orderedGames => (from game in _games where game != null && game.gameData != null orderby game.gameData._serializedStartTime descending select game).ToList(); internal static bool SuppressOpeningPick { get; private set; } internal static void Initialize() { Directory.CreateDirectory(SavesPath); LoadGames(); } private static IEnumerator RunGuarded(IEnumerator routine, string what) { while (true) { object current; try { if (!routine.MoveNext()) { break; } current = routine.Current; } catch (Exception arg) { GameSaver.LogError($"{what} failed: {arg}"); break; } yield return current; } } public static void LoadGames() { string[] directories; try { directories = Directory.GetDirectories(SavesPath); } catch (Exception ex) { GameSaver.LogError("could not enumerate saves folder: " + ex.Message); return; } string[] array = directories; foreach (string directoryPath in array) { if (!Path.GetFileName(directoryPath).StartsWith("Game-", StringComparison.Ordinal)) { continue; } string path = Path.Combine(directoryPath, "game.json"); if (!File.Exists(path)) { continue; } GameData gameData; try { gameData = JsonUtility.FromJson(File.ReadAllText(path)); } catch (Exception ex2) { GameSaver.LogWarning("skipping unreadable game.json in '" + Path.GetFileName(directoryPath) + "': " + ex2.Message); continue; } if (gameData == null) { continue; } string[] files; try { files = Directory.GetFiles(directoryPath); } catch (Exception ex3) { GameSaver.LogWarning("could not read save folder '" + Path.GetFileName(directoryPath) + "': " + ex3.Message); continue; } List list = new List(); string[] array2 = files; foreach (string path2 in array2) { if (!Path.GetFileName(path2).Contains("save")) { continue; } try { SaveData saveData = JsonUtility.FromJson(File.ReadAllText(path2)); if (saveData != null) { list.Add(saveData); } } catch (Exception ex4) { GameSaver.LogWarning("skipping unreadable save '" + Path.GetFileName(path2) + "': " + ex4.Message); } } GameInfoData gameInfoData = _games.FirstOrDefault((GameInfoData g) => g?.gameData != null && string.Equals(g.FilePath, directoryPath, StringComparison.OrdinalIgnoreCase)); if (gameInfoData != null) { HashSet known = new HashSet(from s in gameInfoData.gameSaves where s != null select s._serializedDateTime); int num = 0; foreach (SaveData item in list.Where((SaveData save) => !known.Contains(save._serializedDateTime))) { gameInfoData.gameSaves.Add(item); num++; } if (num > 0) { gameInfoData.InvalidateRounds(); } } else { _games.Add(new GameInfoData(directoryPath, gameData, list)); } } } internal static IEnumerator PreSave() { string savesPath = SavesPath; Guid gameGuid = _gameGuid; _gameSavesPath = Path.Combine(savesPath, "Game-" + gameGuid); Directory.CreateDirectory(_gameSavesPath); string currentHandlerID = GameModeManager.CurrentHandlerID; GameType gameType = ((!PhotonNetwork.OfflineMode && PhotonNetwork.CurrentRoom != null) ? GameType.Online : GameType.Local); GameData gameData = new GameData(DateTime.Now, PlayerManager.instance.players.Count, currentHandlerID, gameType); File.WriteAllText(Path.Combine(_gameSavesPath, "game.json"), JsonUtility.ToJson((object)gameData)); yield return null; } private static List MatchPlayers(SaveData save) { List list = new List(); List list2 = new List(PlayerManager.instance.players.Where((Player p) => (Object)(object)p?.data?.view != (Object)null)); Room room = (PhotonNetwork.OfflineMode ? null : PhotonNetwork.CurrentRoom); List list3 = new List(save.players.Where((PlayerData p) => p != null)); foreach (PlayerData data in list3.ToList()) { if (data.steamId > 1) { ulong value; Player val = ((IEnumerable)list2).FirstOrDefault((Func)((Player p) => SteamManager.steamIds.TryGetValue(p.data.view.OwnerActorNr, out value) && value == data.steamId)); if (!((Object)(object)val == (Object)null)) { list.Add(new Match { Data = data, Player = val }); list2.Remove(val); list3.Remove(data); } } } foreach (PlayerData data2 in list3.ToList()) { if (room == null) { break; } Player val2 = ((IEnumerable)list2).FirstOrDefault((Func)delegate(Player p) { Player player = room.GetPlayer(p.data.view.OwnerActorNr); return player != null && player.NickName == data2.name; }); if (!((Object)(object)val2 == (Object)null)) { list.Add(new Match { Data = data2, Player = val2 }); list2.Remove(val2); list3.Remove(data2); } } if (room == null && save.players.Count == PlayerManager.instance.players.Count) { foreach (PlayerData item in list3.ToList()) { int num = save.players.IndexOf(item); if (num >= 0 && num < PlayerManager.instance.players.Count) { Player val3 = PlayerManager.instance.players[num]; if (!((Object)(object)val3 == (Object)null) && list2.Contains(val3)) { list.Add(new Match { Data = item, Player = val3 }); list2.Remove(val3); list3.Remove(item); GameSaver.LogInfo($"matched saved player '{item.name}' by position {num} (no steam id or nickname match)."); } } } } foreach (PlayerData item2 in list3) { GameSaver.LogWarning("saved player '" + item2.name + "' could not be matched to anyone in this lobby - their cards and score were not restored."); } foreach (Player item3 in list2) { GameSaver.LogWarning("player '" + PlayerName(item3) + "' is in this lobby but not in the save - they start fresh."); } return list; } private static string PlayerName(Player player) { try { Room currentRoom = PhotonNetwork.CurrentRoom; if (currentRoom == null) { return $"player {player.playerID}"; } Player player2 = currentRoom.GetPlayer(player.data.view.OwnerActorNr); return ((player2 != null) ? player2.NickName : null) ?? $"player {player.playerID}"; } catch { return $"player {player.playerID}"; } } private static void RestorePlayerCards(Match match) { try { List cards = match.Data.Cards; CardInfo[] array = cards.Where((CardInfo c) => Object.op_Implicit((Object)(object)c)).ToArray(); if (array.Length != cards.Count) { GameSaver.LogWarning($"{match.Data.name}: {cards.Count - array.Length} of {cards.Count} cards could not be resolved on this client and were skipped."); } if (array.Length != 0) { Cards.instance.AddCardsToPlayer(match.Player, array, true, (string[])null, (float[])null, (float[])null, true); } } catch (Exception arg) { GameSaver.LogError($"failed to restore cards for '{match.Data.name}': {arg}"); } } private static void ApplyTeamScores(List matches) { foreach (IGrouping item in from m in matches group m by m.Player.teamID) { List source = item.ToList(); List list = (from m in source where m.Data.HasTeamId select m.Data.TeamId).Distinct().ToList(); if (list.Count > 1) { GameSaver.LogWarning($"team {item.Key} now contains players who were on {list.Count} different teams " + "(" + string.Join(", ", source.Select((Match m) => m.Data.name).ToArray()) + "); applying the highest score of the group."); } Match match = (from m in source orderby m.Data.rounds descending, m.Data.points descending select m).First(); ShareSaveTeamSettings(item.Key, match.Data.points, match.Data.rounds); } } internal static IEnumerator LoadSave() { if (_selectedSave == null) { yield break; } SaveData save = _selectedSave; _selectedSave = null; _round = save.round; ShareSaveGameSettings(save.pointsToWin, save.pointsToWinRound); yield return null; List matches = MatchPlayers(save); foreach (Match item in matches) { RestorePlayerCards(item); yield return null; } ApplyTeamScores(matches); SuppressOpeningPick = true; GameSaver.LogInfo($"restored round {save.round} for {matches.Count} player(s); " + "skipping the opening pick so the match resumes straight into the round."); } public static void ShareSaveTeamSettings(int teamId, int points, int rounds) { NetworkingManager.RPC(typeof(SaveManager), "LoadSaveTeamSettings", new object[3] { teamId, points, rounds }); } public static void ShareSaveGameSettings(int pointsToWin, int pointsToWinRound) { NetworkingManager.RPC(typeof(SaveManager), "LoadSaveGameSettings", new object[3] { pointsToWin, pointsToWinRound, _round }); } [UnboundRPC] private static void LoadSaveTeamSettings(int teamId, int points, int rounds) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) try { GameModeManager.CurrentHandler.SetTeamScore(teamId, new TeamScore(points, rounds)); if ((Object)(object)UIHandler.instance != (Object)null) { if ((Object)(object)UIHandler.instance.roundCounter != (Object)null) { ExtensionMethods.InvokeMethod((object)UIHandler.instance.roundCounter, "ReDraw", Array.Empty()); } if ((Object)(object)UIHandler.instance.roundCounterSmall != (Object)null) { ExtensionMethods.InvokeMethod((object)UIHandler.instance.roundCounterSmall, "ReDraw", Array.Empty()); } } } catch (Exception arg) { GameSaver.LogError($"could not apply team score for team {teamId}: {arg}"); } } [UnboundRPC] private static void LoadSaveGameSettings(int pointsToWin, int pointsToWinRound, int currentRound) { try { GameModeManager.CurrentHandler.ChangeSetting("roundsToWinGame", (object)pointsToWin); GameModeManager.CurrentHandler.ChangeSetting("pointsToWinRound", (object)pointsToWinRound); _round = currentRound; } catch (Exception arg) { GameSaver.LogError($"could not apply game settings: {arg}"); } } private static void EnsureSavingObject() { if ((Object)(object)_savingObject != (Object)null) { return; } try { if ((Object)(object)AssetManager.Saving != (Object)null) { _savingObject = Object.Instantiate(AssetManager.Saving); } } catch (Exception ex) { GameSaver.LogWarning("could not create the saving indicator: " + ex.Message); } } private static void GameStartPrologue() { try { if ((Object)(object)SaveLoadMenu.instance != (Object)null) { SaveLoadMenu.instance.Reset(); } EnsureSavingObject(); } catch (Exception arg) { GameSaver.LogError($"preparing a new game failed: {arg}"); } } internal static IEnumerator GameStart(IGameModeHandler gm) { GameStartPrologue(); _round = 1; _gameGuid = Guid.NewGuid(); yield return RunGuarded(LoadSave(), "restoring a save"); yield return RunGuarded(PreSave(), "creating the save folder"); } internal static IEnumerator GameEnd(IGameModeHandler gm) { if ((Object)(object)_savingObject != (Object)null) { Object.Destroy((Object)(object)_savingObject); _savingObject = null; } _selectedSave = null; SuppressOpeningPick = false; yield return null; } internal static void SelectSave(GameData gameData, SaveData selectedSave) { _selectedSave = selectedSave; GameModeManager.SetGameMode(gameData.gameMode); ExtensionMethods.InvokeMethod((object)PrivateRoomHandler.instance, "UnreadyAllPlayers", Array.Empty()); ObjectExtensions.ExecuteAfterGameModeInitialized((MonoBehaviour)(object)PrivateRoomHandler.instance, gameData.gameMode, (Action)delegate { SyncMethodStatic.SyncMethod(typeof(PrivateRoomHandler), "SetGameSettings", (int[])null, new object[2] { GameModeManager.CurrentHandlerID, GameModeManager.CurrentHandler.Settings }); ExtensionMethods.InvokeMethod((object)PrivateRoomHandler.instance, "HandleTeamRules", Array.Empty()); }); } internal static IEnumerator RoundStart(IGameModeHandler gm) { if ((Object)(object)_savingObject != (Object)null) { _savingObject.SetActive(false); } yield return null; } internal static IEnumerator RoundEnd(IGameModeHandler gm) { _round++; yield return null; } internal static IEnumerator PickEnd(IGameModeHandler gm) { SuppressOpeningPick = false; yield return RunGuarded(Save(SaveType.PickEnd), "auto-saving after the pick phase"); } internal static IEnumerator Save(SaveType saveType) { if ((ConfigManager.SaveEnabled != null && !ConfigManager.SaveEnabled.Value) || (ConfigManager.SaveAsHostOnly != null && ConfigManager.SaveAsHostOnly.Value && !PhotonNetwork.OfflineMode && !PhotonNetwork.IsMasterClient) || string.IsNullOrEmpty(_gameSavesPath)) { yield break; } EnsureSavingObject(); if ((Object)(object)_savingObject != (Object)null) { _savingObject.SetActive(true); ExtensionMethods.GetOrAddComponent(_savingObject, false); } yield return null; List list = new List(); bool flag = PhotonNetwork.OfflineMode || PhotonNetwork.CurrentRoom == null; int num = 1; foreach (Player item in PlayerManager.instance.players.Where((Player player) => (Object)(object)player != (Object)null)) { Player val = (flag ? null : PhotonNetwork.CurrentRoom.GetPlayer(item.data.view.OwnerActorNr)); string name = (flag ? (SteamFriends.GetPersonaName() + "-" + num) : (((val != null) ? val.NickName : null) ?? ("Player " + num))); List cards = new List(item.data.currentCards); TeamScore teamScore = GameModeManager.CurrentHandler.GetTeamScore(item.teamID); bool host = (flag ? (num == 1) : (val != null && val.IsMasterClient)); ulong value; ulong steamId = (flag ? 1 : (SteamManager.steamIds.TryGetValue(item.data.view.OwnerActorNr, out value) ? value : 1)); list.Add(JsonUtility.ToJson((object)new PlayerData(name, PlayerExtensions.colorID(item), item.teamID, cards, teamScore.points, teamScore.rounds, host, steamId))); num++; } int pointsToWinRound = (int)GameModeManager.CurrentHandler.Settings["pointsToWinRound"]; int pointsToWin = (int)GameModeManager.CurrentHandler.Settings["roundsToWinGame"]; SaveData saveData = new SaveData(DateTime.Now, saveType, _round, pointsToWinRound, pointsToWin, list); try { File.WriteAllText(Path.Combine(_gameSavesPath, "save" + DateTime.Now.ToBinary() + ".json"), JsonUtility.ToJson((object)saveData, true)); } catch (Exception ex) { GameSaver.LogError("could not write save file: " + ex.Message); } yield return null; } public static void DeleteGameSave(GameInfoData gameInfoData) { _games.Remove(gameInfoData); if ((Object)(object)SaveLoadMenu.instance != (Object)null) { SaveLoadMenu.instance.RemoveGameSaveButtons(gameInfoData); } try { if (Directory.Exists(gameInfoData.FilePath)) { Directory.Delete(gameInfoData.FilePath, recursive: true); } } catch (Exception ex) { GameSaver.LogError("could not delete save folder: " + ex.Message); } } } internal static class ShareCodec { [Serializable] private class Payload { public SaveManager.GameData game; public SaveManager.SaveData save; } internal static string Encode(SaveManager.GameData game, SaveManager.SaveData save) { string s = JsonUtility.ToJson((object)new Payload { game = game, save = save }); return TextCodec.Frame(TextCodec.Deflate(Encoding.UTF8.GetBytes(s))); } internal static bool TryDecode(string code, out SaveManager.GameData game, out SaveManager.SaveData save, out string error) { game = null; save = null; if (!TextCodec.TryUnframe(code, out var compressed, out error)) { return false; } try { Payload payload = JsonUtility.FromJson(Encoding.UTF8.GetString(TextCodec.Inflate(compressed))); if (payload?.game == null || payload.save == null) { error = "CODE UNREADABLE"; return false; } game = payload.game; save = payload.save; return true; } catch (Exception ex) { GameSaver.LogWarning("could not decode share code: " + ex.Message); error = "CODE UNREADABLE"; return false; } } } internal static class ShareManager { internal class ExportResult { public bool Success; public string Code; public string FilePath; public string Error; public bool FitsInOneMessage { get { if (Code != null) { return Code.Length <= 2000; } return false; } } } internal class ImportResult { public bool Success; public string Error; public int Round; public bool AlreadyHad; } internal const int ChatMessageLimit = 2000; internal static string SharedFolder => Path.Combine(SaveManager.SavesPath, "_shared"); internal static ExportResult Export(SaveManager.GameData game, SaveManager.SaveData save) { ExportResult exportResult = new ExportResult(); try { exportResult.Code = ShareCodec.Encode(game, save); try { GUIUtility.systemCopyBuffer = exportResult.Code; } catch (Exception ex) { GameSaver.LogWarning("could not copy the code to the clipboard: " + ex.Message); } try { Directory.CreateDirectory(SharedFolder); string text = $"round{save.round}-{game.gameMode}-{save.Time:yyyyMMdd-HHmmss}.txt"; char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { text = text.Replace(oldChar, '_'); } exportResult.FilePath = Path.Combine(SharedFolder, text); File.WriteAllText(exportResult.FilePath, exportResult.Code); } catch (Exception ex2) { exportResult.FilePath = null; GameSaver.LogWarning("could not write the share file: " + ex2.Message); } exportResult.Success = true; GameSaver.LogInfo($"exported round {save.round} as a {exportResult.Code.Length} character code" + ((exportResult.FilePath != null) ? (" (also saved to " + exportResult.FilePath + ")") : "")); } catch (Exception arg) { exportResult.Error = "EXPORT FAILED"; GameSaver.LogError($"could not export the save: {arg}"); } return exportResult; } internal static ImportResult Import(string code) { ImportResult importResult = new ImportResult(); if (!ShareCodec.TryDecode(code, out var game, out var save, out var error)) { importResult.Error = error; return importResult; } try { string text = FindFolderForGame(game) ?? CreateFolderForGame(game); string path = "save" + save._serializedDateTime + ".json"; string path2 = Path.Combine(text, path); if (File.Exists(path2)) { importResult.Success = true; importResult.AlreadyHad = true; importResult.Round = save.round; return importResult; } File.WriteAllText(path2, JsonUtility.ToJson((object)save, true)); SaveManager.LoadGames(); importResult.Success = true; importResult.Round = save.round; GameSaver.LogInfo($"imported round {save.round} ({game.gameMode}) into {text}"); } catch (Exception arg) { importResult.Error = "IMPORT FAILED"; GameSaver.LogError($"could not import the save: {arg}"); } return importResult; } private static string FindFolderForGame(SaveManager.GameData game) { if (!Directory.Exists(SaveManager.SavesPath)) { return null; } string[] directories = Directory.GetDirectories(SaveManager.SavesPath); foreach (string text in directories) { if (!Path.GetFileName(text).StartsWith("Game-", StringComparison.Ordinal)) { continue; } string path = Path.Combine(text, "game.json"); if (!File.Exists(path)) { continue; } try { SaveManager.GameData gameData = JsonUtility.FromJson(File.ReadAllText(path)); if (gameData != null && gameData._serializedStartTime == game._serializedStartTime) { return text; } } catch (Exception) { } } return null; } private static string CreateFolderForGame(SaveManager.GameData game) { string text = Path.Combine(SaveManager.SavesPath, "Game-" + Guid.NewGuid()); Directory.CreateDirectory(text); File.WriteAllText(Path.Combine(text, "game.json"), JsonUtility.ToJson((object)game)); return text; } } internal class SteamManager { public static Dictionary steamIds = new Dictionary(); public static Player GetPlayerFromSteamId(ulong steamId) { if (steamId <= 1 || steamIds.Count == 0) { return null; } foreach (Player player in PlayerManager.instance.players) { if (!((Object)(object)player?.data?.view == (Object)null) && steamIds.TryGetValue(player.data.view.OwnerActorNr, out var value) && value == steamId) { return player; } } return null; } } internal static class TextCodec { internal const string Prefix = "GS1"; private static uint[] _crcTable; internal static string Frame(byte[] compressed) { return string.Format("{0}-{1:x8}-{2}", "GS1", Crc32(compressed), ToBase64Url(compressed)); } internal static bool TryUnframe(string code, out byte[] compressed, out string error) { compressed = null; error = null; if (string.IsNullOrEmpty(code)) { error = "NO CODE"; return false; } string[] array = StripWhitespace(code).Split(new char[1] { '-' }, 3); if (array.Length != 3 || array[0] != "GS1") { error = "NOT A GAMESAVER CODE"; return false; } if (!uint.TryParse(array[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result)) { error = "NOT A GAMESAVER CODE"; return false; } byte[] array2; try { array2 = FromBase64Url(array[2]); } catch (Exception) { error = "CODE CORRUPTED"; return false; } if (Crc32(array2) != result) { error = "CODE INCOMPLETE"; return false; } compressed = array2; return true; } internal static byte[] Deflate(byte[] data) { using MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionMode.Compress, leaveOpen: true)) { deflateStream.Write(data, 0, data.Length); } return memoryStream.ToArray(); } internal static byte[] Inflate(byte[] data) { using MemoryStream stream = new MemoryStream(data); using DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress); using MemoryStream memoryStream = new MemoryStream(); byte[] array = new byte[8192]; int count; while ((count = deflateStream.Read(array, 0, array.Length)) > 0) { memoryStream.Write(array, 0, count); } return memoryStream.ToArray(); } internal static string ToBase64Url(byte[] data) { return Convert.ToBase64String(data).Replace('+', '-').Replace('/', '_') .TrimEnd(new char[1] { '=' }); } internal static byte[] FromBase64Url(string text) { string text2 = text.Replace('-', '+').Replace('_', '/'); switch (text2.Length % 4) { case 2: text2 += "=="; break; case 3: text2 += "="; break; case 1: throw new FormatException("bad base64url length"); } return Convert.FromBase64String(text2); } internal static string StripWhitespace(string text) { StringBuilder stringBuilder = new StringBuilder(text.Length); foreach (char c in text) { if (!char.IsWhiteSpace(c)) { stringBuilder.Append(c); } } return stringBuilder.ToString(); } internal static uint Crc32(byte[] data) { if (_crcTable == null) { uint[] array = new uint[256]; for (uint num = 0u; num < 256; num++) { uint num2 = num; for (int i = 0; i < 8; i++) { num2 = (((num2 & 1) == 1) ? (0xEDB88320u ^ (num2 >> 1)) : (num2 >> 1)); } array[num] = num2; } _crcTable = array; } uint num3 = uint.MaxValue; foreach (byte b in data) { num3 = _crcTable[(num3 ^ b) & 0xFF] ^ (num3 >> 8); } return num3 ^ 0xFFFFFFFFu; } } } namespace GameSaver.Patches { internal static class OpeningPickPatch { internal static void Apply(Harmony harmony) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method("RWF.PlayerManagerExtensions:GetPickOrder", (Type[])null, (Type[])null); if (methodInfo == null) { GameSaver.LogWarning("could not find RWF.PlayerManagerExtensions.GetPickOrder - loading a save will still run the opening card pick."); } else { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(OpeningPickPatch), "Postfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } } private static void Postfix(int[] winningTeamIDs, ref List __result) { if (SaveManager.SuppressOpeningPick && winningTeamIDs == null) { GameSaver.LogInfo($"skipping the opening card pick for {__result?.Count ?? 0} player(s) - a save is being restored."); __result = new List(); } } } } namespace GameSaver.Network { public class LobbyMonitor : MonoBehaviourPunCallbacks { private static GameObject _loadSaveRow; private static SaveLoadMenu _menu; private static TMP_FontAsset _menuFont; public static LobbyMonitor Instance { get; private set; } public static TMP_FontAsset MenuFont { get { if (Object.op_Implicit((Object)(object)_menuFont) || !Object.op_Implicit((Object)(object)MainMenuHandler.instance)) { return _menuFont; } Transform val = ((Component)MainMenuHandler.instance).transform.Find("Canvas/ListSelector/Main/Group/Local"); if ((Object)(object)val == (Object)null) { return _menuFont; } TextMeshProUGUI componentInChildren = ((Component)val).GetComponentInChildren(); _menuFont = ((componentInChildren != null) ? ((TMP_Text)componentInChildren).font : null); return _menuFont; } } private void Awake() { Instance = this; } public override void OnCreatedRoom() { } public override void OnJoinedRoom() { if (PhotonNetwork.OfflineMode) { return; } SteamManager.steamIds.Clear(); ExtensionMethods.ExecuteAfterFrames((MonoBehaviour)(object)Unbound.Instance, 1, (Action)LoadSaveButton); ExtensionMethods.ExecuteAfterSeconds((MonoBehaviour)(object)Unbound.Instance, 2f, (Action)delegate { //IL_000a: Unknown result type (might be due to invalid IL or missing references) try { SendSteamId(PhotonNetwork.LocalPlayer.ActorNumber, SteamUser.GetSteamID().m_SteamID); } catch (Exception ex) { GameSaver.LogWarning("could not broadcast steam id: " + ex.Message); } }); } public override void OnPlayerEnteredRoom(Player newPlayer) { if (!PhotonNetwork.IsMasterClient) { return; } ExtensionMethods.ExecuteAfterSeconds((MonoBehaviour)(object)Unbound.Instance, 2.5f, (Action)delegate { foreach (KeyValuePair steamId in SteamManager.steamIds) { SendSteamId(steamId.Key, steamId.Value); } }); } public override void OnLeftRoom() { SaveManagerResetSelection(); if (!((Object)(object)SaveLoadMenu.instance == (Object)null)) { SaveLoadMenu.instance.Reset(); } } private static void SaveManagerResetSelection() { SaveManager._selectedSave = null; } public static void SendSteamId(int player, ulong steamId) { NetworkingManager.RPC(typeof(LobbyMonitor), "SyncSteamId", new object[2] { player, steamId.ToString() }); } [UnboundRPC] private static void SyncSteamId(int player, string serializedSteamId) { try { SteamManager.steamIds[player] = ulong.Parse(serializedSteamId); } catch (Exception ex) { GameSaver.LogWarning($"could not record steam id for actor {player}: {ex.Message}"); } } public void LoadSaveButton() { //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Expected O, but got Unknown if (!PhotonNetwork.IsMasterClient || GameSaver.AssetsFailed) { return; } GameObject val = GameObject.Find("PrivateRoom/Main/Group"); if ((Object)(object)val == (Object)null) { GameSaver.LogWarning("private room UI not found - the LOAD button was not added."); return; } if ((Object)(object)_loadSaveRow != (Object)null) { Object.Destroy((Object)(object)_loadSaveRow); } if ((Object)(object)_menu != (Object)null) { Object.Destroy((Object)(object)((Component)_menu).gameObject); } Transform val2 = val.transform.Find("LOAD_SAVE"); if ((Object)(object)val2 != (Object)null) { Object.Destroy((Object)(object)((Component)val2).gameObject); } GameObject val3 = new GameObject("LOAD_SAVE"); val3.transform.SetParent(val.transform); val3.transform.localScale = Vector3.one; val3.transform.SetSiblingIndex(val3.transform.GetSiblingIndex() - 1); GameObject text = GetText("LOAD"); text.transform.SetParent(val3.transform); text.transform.localScale = Vector3.one; val3.AddComponent(); val3.AddComponent(); val3.AddComponent().minHeight = 92f; ListMenuButton val4 = val3.AddComponent(); val4.setBarHeight = 92f; GameObject val5 = Object.Instantiate(AssetManager.ElementSection); SaveLoadMenu saveLoadMenu = val5.AddComponent(); saveLoadMenu.lobbyUi = val; saveLoadMenu.listMenuButton = val4; ((UnityEvent)val3.AddComponent