Decompiled source of RumbleStats v1.0.3

Mods/RumbleStats.dll

Decompiled 3 days ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using HarmonyLib;
using Il2CppRUMBLE.Players.Subsystems;
using Il2CppRUMBLE.Poses;
using MelonLoader;
using MelonLoader.Utils;
using RumbleModUI;
using RumbleModdingAPI;
using RumbleStats;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: MelonInfo(typeof(Main), "Rumble Stats", "1.0.3", "ERROR", null)]
[assembly: MelonGame("Buckethead Entertainment", "RUMBLE")]
[assembly: MelonColor(255, 255, 0, 0)]
[assembly: MelonAuthorColor(255, 255, 0, 0)]
[assembly: AssemblyTitle("RumbleStats")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Rumble Stats")]
[assembly: AssemblyCopyright("Copyright ©  2024")]
[assembly: VerifyLoaderVersion(0, 6, 2, true)]
[assembly: AssemblyTrademark("")]
[assembly: ComVisible(false)]
[assembly: Guid("b10c94a1-8a40-4701-bc5b-98eabb44dfea")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace RumbleStats;

public class StatsMod
{
	private string baseFilePath;

	public Dictionary<string, Dictionary<string, Type>> columnTypes = new Dictionary<string, Dictionary<string, Type>>();

	public string ModName { get; set; }

	public bool storeEntryTime { get; set; } = false;


	public bool DebugMode { get; set; } = false;


	public event Action StatAdded;

	private void Log(string message)
	{
		if (DebugMode)
		{
			MelonLogger.Msg(message);
		}
	}

	public void InitializeStatFile(string fileName, Dictionary<string, Type> columns)
	{
		MelonLogger.Msg("Initializing stat file: " + fileName);
		if (string.IsNullOrEmpty(fileName))
		{
			MelonLogger.Error("File name cannot be null or empty.", new object[1] { "fileName" });
		}
		if (columns == null || columns.Count == 0)
		{
			MelonLogger.Error("Columns cannot be null or empty.", new object[1] { "columns" });
		}
		baseFilePath = Path.Combine(MelonEnvironment.UserDataDirectory, "RUMBLEStats", ModName);
		string text = Path.Combine(baseFilePath, fileName + ".csv");
		Log("Base file path: " + baseFilePath);
		Log("CSV file path: " + text);
		Dictionary<string, Type> dictionary = new Dictionary<string, Type>(columns);
		if (storeEntryTime)
		{
			dictionary["EntryTime"] = typeof(DateTime);
			Log("Added 'EntryTime' column.");
		}
		columnTypes[fileName + ".csv"] = dictionary;
		string text2 = string.Join(",", dictionary.Keys);
		Log("Columns: " + text2);
		try
		{
			if (!Directory.Exists(baseFilePath))
			{
				Directory.CreateDirectory(baseFilePath);
				MelonLogger.Msg("Created directory: " + baseFilePath);
			}
			if (!File.Exists(text))
			{
				using (StreamWriter streamWriter = new StreamWriter(text, append: false))
				{
					streamWriter.WriteLine(text2);
					MelonLogger.Msg("Initialized stat file: " + text);
					return;
				}
			}
		}
		catch (Exception ex)
		{
			MelonLogger.Error("Failed to initialize stat file: " + ex.Message);
		}
	}

	public bool StatFileExists(string csvFileName)
	{
		string text = Path.Combine(baseFilePath, csvFileName + ".csv");
		bool flag = File.Exists(text);
		Log($"Checking if stat file exists: {text} - Exists: {flag}");
		return flag;
	}

	public List<string[]> GetAllRows(string csvFileName, bool skipHeader = false)
	{
		string text = Path.Combine(baseFilePath, csvFileName + ".csv");
		Log("Reading all rows from file: " + text);
		if (!StatFileExists(csvFileName))
		{
			MelonLogger.Error("Stat file '" + text + "' is not initialized or does not exist. Ensure you have called InitializeStatFile before reading rows.");
		}
		List<string[]> list = new List<string[]>();
		using (StreamReader streamReader = new StreamReader(text))
		{
			if (skipHeader)
			{
				string text2 = streamReader.ReadLine();
				Log("Skipped header: " + text2);
			}
			string text3;
			while ((text3 = streamReader.ReadLine()) != null)
			{
				list.Add(text3.Split(new char[1] { ',' }));
				Log("Read row: " + text3);
			}
		}
		Log($"Total rows read: {list.Count}");
		return list;
	}

	public List<string[]> GetRows(Func<string[], bool> predicate, string csvFileName)
	{
		Log("Getting rows with predicate from file: " + csvFileName);
		List<string[]> allRows = GetAllRows(csvFileName);
		List<string[]> list = allRows.FindAll(predicate.Invoke);
		Log($"Total rows matching predicate: {list.Count}");
		return list;
	}

	public string[] GetRow(int index, string csvFileName)
	{
		Log($"Getting row at index {index} from file: {csvFileName}");
		List<string[]> allRows = GetAllRows(csvFileName);
		if (index < 0 || index >= allRows.Count)
		{
			MelonLogger.Error("index", new object[1] { $"Index {index} is out of range. Total rows: {allRows.Count}" });
			return null;
		}
		string[] array = allRows[index];
		Log(string.Format("Found row at index {0}: {1}", index, string.Join(",", array)));
		return array;
	}

	public void RemoveRows(Func<string[], bool> predicate, string csvFileName)
	{
		Log("Removing rows with predicate from file: " + csvFileName);
		List<string[]> allRows = GetAllRows(csvFileName);
		List<string[]> source = allRows.Where((string[] row) => !predicate(row)).ToList();
		string path = Path.Combine(baseFilePath, csvFileName + ".csv");
		File.WriteAllLines(path, source.Select((string[] row) => string.Join(",", row)));
	}

	public void RemoveRow(int index, string csvFileName)
	{
		Log($"Removing row at index {index} from file: {csvFileName}");
		List<string[]> allRows = GetAllRows(csvFileName);
		if (index < 0 || index >= allRows.Count)
		{
			MelonLogger.Error("index", new object[1] { $"Index {index} is out of range. Total rows: {allRows.Count}" });
		}
		else
		{
			allRows.RemoveAt(index);
			WriteAllRows(csvFileName, allRows);
			Log($"Row at index {index} removed successfully.");
		}
	}

	public void WriteAllRows(string csvFileName, List<string[]> rows)
	{
		string text = Path.Combine(baseFilePath, csvFileName + ".csv");
		Log("Writing all rows to file: " + text);
		if (!columnTypes.ContainsKey(csvFileName + ".csv"))
		{
			MelonLogger.Error("The stat file '" + csvFileName + ".csv' has not been initialized.");
			return;
		}
		Dictionary<string, Type> dictionary = columnTypes[csvFileName + ".csv"];
		string text2 = string.Join(",", dictionary.Keys);
		try
		{
			using StreamWriter streamWriter = new StreamWriter(text, append: false);
			streamWriter.WriteLine(text2);
			Log("Wrote header: " + text2);
			foreach (string[] row in rows)
			{
				streamWriter.WriteLine(string.Join(",", row));
				Log("Wrote row: " + string.Join(",", row));
			}
		}
		catch (Exception ex)
		{
			MelonLogger.Error("Failed to write rows to file: " + csvFileName + ". Error: " + ex.Message);
		}
	}

	public void UpdateRow(int index, string[] updatedValues, string csvFileName)
	{
		Log($"Updating row at index {index} in file: {csvFileName}");
		string text = Path.Combine(baseFilePath, csvFileName + ".csv");
		List<string[]> allRows = GetAllRows(csvFileName);
		if (index < 0 || index >= allRows.Count)
		{
			MelonLogger.Error($"Index {index} is out of range. Total rows: {allRows.Count}");
			return;
		}
		allRows[index] = updatedValues;
		try
		{
			WriteAllRows(csvFileName, allRows);
			Log($"Row updated successfully at index {index} in file: {csvFileName}");
		}
		catch (Exception ex)
		{
			MelonLogger.Error($"Failed to update row at index {index}. Error: {ex.Message}");
		}
	}

	public void AddStatRow(object[] values, string csvFileName)
	{
		Log("Adding row to file: " + csvFileName);
		if (!columnTypes.ContainsKey(csvFileName + ".csv"))
		{
			MelonLogger.Error("The stat file '" + csvFileName + ".csv' has not been initialized. Ensure you have called InitializeStatFile before adding rows.");
		}
		Dictionary<string, Type> dictionary = columnTypes[csvFileName + ".csv"];
		int num = (storeEntryTime ? (dictionary.Count - 1) : dictionary.Count);
		Log($"Expected column count: {num}, Values provided: {values.Length}");
		if (values.Length != num)
		{
			MelonLogger.Error($"The number of values ({values.Length}) does not match the number of columns ({num}).");
		}
		string[] array = new string[dictionary.Count];
		int num2 = 0;
		foreach (KeyValuePair<string, Type> item in dictionary)
		{
			if (storeEntryTime && item.Key == "EntryTime")
			{
				array[num2++] = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture);
				Log("Added 'EntryTime': " + array[num2 - 1]);
			}
			else if (num2 < values.Length)
			{
				object obj = values[num2];
				if (obj != null && obj.GetType() != item.Value)
				{
					MelonLogger.Error("Value at column '" + item.Key + "' is not of type " + item.Value.Name + ". Provided type: " + obj.GetType().Name);
				}
				array[num2++] = FormatValue(obj);
				Log("Formatted value for column '" + item.Key + "': " + array[num2 - 1]);
			}
		}
		WriteStatRow(array, csvFileName);
		Log("Row added successfully: " + string.Join(",", array));
		this.StatAdded?.Invoke();
	}

	private void WriteStatRow(string[] row, string csvFileName)
	{
		string text = Path.Combine(baseFilePath, csvFileName + ".csv");
		Log("Writing row to file: " + text);
		Dictionary<string, Type> dictionary = columnTypes[csvFileName + ".csv"];
		if (row.Length != dictionary.Count)
		{
			MelonLogger.Error($"The number of values ({row.Length}) does not match the number of columns ({dictionary.Count}).");
		}
		try
		{
			using StreamWriter streamWriter = new StreamWriter(text, append: true);
			string text2 = string.Join(",", row);
			streamWriter.WriteLine(text2);
			Log("Row written to file: " + text2);
		}
		catch (Exception ex)
		{
			MelonLogger.Error("Failed to write row to stat file: " + ex.Message);
		}
	}

	public string FormatValue(object value)
	{
		if (value == null)
		{
			return "null";
		}
		if (value is int num)
		{
			return num.ToString(CultureInfo.InvariantCulture);
		}
		if (value is float num2)
		{
			return num2.ToString(CultureInfo.InvariantCulture);
		}
		if (value is double num3)
		{
			return num3.ToString(CultureInfo.InvariantCulture);
		}
		if (value is bool flag)
		{
			return flag ? "true" : "false";
		}
		if (value is DateTime dateTime)
		{
			return dateTime.ToString("o", CultureInfo.InvariantCulture);
		}
		if (value is TimeSpan timeSpan)
		{
			return timeSpan.ToString("c", CultureInfo.InvariantCulture);
		}
		if (value is string result)
		{
			return result;
		}
		try
		{
			return value.ToString();
		}
		catch
		{
			MelonLogger.Error("Unsupported value type: " + (value?.GetType()?.FullName ?? "null"));
			return "unsupported";
		}
	}
}
public class Main : MelonMod
{
	public enum RoundResult
	{
		T,
		W,
		L,
		u
	}

	[HarmonyPatch(typeof(PlayerPoseSystem), "OnPoseSetCompleted")]
	public class PosePatch
	{
		private static void Prefix(ref PoseSet set)
		{
			Main instance = Main.instance;
			if (instance.currentScene == "Map0" || instance.currentScene == "Map1")
			{
				instance.roundPoseSets.Add(((Object)set).name);
			}
		}
	}

	public static Main instance;

	private StatsMod mod = new StatsMod();

	private List<StatsMod> statsMods = new List<StatsMod>();

	private Mod UImod = new Mod();

	private UI UI = UI.instance;

	private bool init = false;

	public string currentScene = "Loader";

	private int round = 0;

	private RoundResult[] rounds = new RoundResult[3]
	{
		RoundResult.u,
		RoundResult.u,
		RoundResult.u
	};

	public List<string> roundPoseSets = new List<string>();

	public Main()
	{
		//IL_0017: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Expected O, but got Unknown
		instance = this;
	}

	public override void OnLateInitializeMelon()
	{
		//IL_0232: Unknown result type (might be due to invalid IL or missing references)
		//IL_0237: Unknown result type (might be due to invalid IL or missing references)
		//IL_0244: Expected O, but got Unknown
		Calls.onMatchEnded += onMatchEnded;
		Calls.onRoundEnded += onRoundEnded;
		Calls.onMatchStarted += onMatchStarted;
		mod.ModName = "RUMBLEStats";
		mod.storeEntryTime = true;
		Dictionary<string, Type> dictionary = new Dictionary<string, Type>();
		dictionary.Add("Structure Name", typeof(string));
		dictionary.Add("Modifier Name", typeof(string));
		Dictionary<string, Type> columns = dictionary;
		mod.InitializeStatFile("MoveData", columns);
		instance.RegisterCSVFile("MoveData", "Records each move or modifier you do, even if it does not activate on a structure.");
		dictionary = new Dictionary<string, Type>();
		dictionary.Add("Opponent Name", typeof(string));
		dictionary.Add("Your Health", typeof(int));
		dictionary.Add("Opponent Health", typeof(int));
		dictionary.Add("Round Count", typeof(int));
		Dictionary<string, Type> columns2 = dictionary;
		mod.InitializeStatFile("RoundHealthData", columns2);
		instance.RegisterCSVFile("RoundHealthData", "Records the data of your health and the opponents health at the end of each round.");
		dictionary = new Dictionary<string, Type>();
		dictionary.Add("Opponent Name", typeof(string));
		dictionary.Add("Match Result", typeof(string));
		dictionary.Add("Rounds", typeof(string));
		dictionary.Add("BP Gained", typeof(int));
		dictionary.Add("Total BP", typeof(int));
		dictionary.Add("Map", typeof(string));
		Dictionary<string, Type> columns3 = dictionary;
		mod.InitializeStatFile("MatchData", columns3);
		instance.RegisterCSVFile("MatchData", "Records the data of each map such as the map, bp gained, the match result (win or loss), rounds (WLW, WLL, LL-, etc).");
		UImod.ModName = "RumbleStats";
		UImod.ModVersion = "1.0.3";
		UImod.SetFolder("RUMBLEStats");
		UImod.AddDescription("Description", "", "A mod that allows other mods to track statistics in the form of CSV files. ModUI toggles currently do not work.", new Tags
		{
			IsSummary = true
		});
		UImod.GetFromFile();
		UI.instance.UI_Initialized += OnUIInit;
		MelonLogger.Msg("RumbleStats initiated");
	}

	private void OnUIInit()
	{
		UI.AddMod(UImod);
	}

	public void RegisterCSVFile(string fileName, string description)
	{
		//IL_000b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0015: Expected O, but got Unknown
		ModSetting<bool> val = UImod.AddToList(fileName, true, 0, description, new Tags());
	}

	private void onMatchEnded()
	{
		string text = string.Concat(rounds.Select((RoundResult result) => (result == RoundResult.u) ? "-" : result.ToString()));
		string empty = string.Empty;
		string text2 = ((currentScene == "Map0") ? "Ring" : "Pit");
		int num = 2;
		int battlePoints = Players.GetLocalPlayer().Data.GeneralData.BattlePoints;
		int num2 = 0;
		int num3 = 0;
		RoundResult[] array = rounds;
		for (int i = 0; i < array.Length; i++)
		{
			switch (array[i])
			{
			case RoundResult.W:
				num2++;
				break;
			case RoundResult.L:
				num3++;
				break;
			}
		}
		if (num2 >= 2)
		{
			empty = "Win";
			num = 5;
		}
		else
		{
			empty = "Lose";
		}
		mod.AddStatRow(new object[6]
		{
			Players.GetAllPlayers()[1].Data.GeneralData.PublicUsername,
			empty,
			text,
			num,
			battlePoints,
			text2
		}, "MatchData");
	}

	private void onMatchStarted()
	{
		round = 0;
	}

	private void onRoundEnded()
	{
		int healthPoints = Players.GetLocalPlayer().Data.HealthPoints;
		int healthPoints2 = Players.GetAllPlayers()[1].Data.HealthPoints;
		mod.AddStatRow(new object[4]
		{
			Players.GetAllPlayers()[1].Data.GeneralData.PublicUsername,
			healthPoints,
			healthPoints2,
			round + 1
		}, "RoundHealthData");
		if (healthPoints == healthPoints2)
		{
			rounds[round] = RoundResult.T;
		}
		else if (healthPoints > healthPoints2)
		{
			rounds[round] = RoundResult.W;
		}
		else
		{
			rounds[round] = RoundResult.L;
		}
		AddToFile(roundPoseSets);
		roundPoseSets.Clear();
		round++;
	}

	private void AddToFile(List<string> poseNames)
	{
		List<string> list = new List<string> { "PoseSetDisc", "PoseSetSpawnPillar", "PoseSetBall", "PoseSetWall_Grounded", "PoseSetSpawnCube" };
		foreach (string poseName in poseNames)
		{
			if (list.Contains(poseName))
			{
				mod.AddStatRow(new object[2]
				{
					poseName,
					string.Empty
				}, "MoveData");
			}
			else
			{
				mod.AddStatRow(new object[2]
				{
					string.Empty,
					poseName
				}, "MoveData");
			}
		}
	}

	public override void OnSceneWasLoaded(int buildIndex, string sceneName)
	{
		currentScene = sceneName;
		init = false;
	}
}