RUMBLE does not support other mod managers. If you want to use a manager, you must use the RUMBLE Mod Manager, a manager specifically designed for this game.
Decompiled source of RumbleStats v1.0.4
Mods/RumbleStats.dll
Decompiled 7 months agousing System; using System.Collections; 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.4", "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 += delegate { MelonCoroutines.Start(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.4"; 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 IEnumerator OnRoundEnded() { int localHealth = Players.GetLocalPlayer().Data.HealthPoints; int remoteHealth = Players.GetAllPlayers()[1].Data.HealthPoints; mod.AddStatRow(new object[4] { Players.GetAllPlayers()[1].Data.GeneralData.PublicUsername, localHealth, remoteHealth, round + 1 }, "RoundHealthData"); if (localHealth == remoteHealth) { rounds[round] = RoundResult.T; } else if (localHealth > remoteHealth) { rounds[round] = RoundResult.W; } else { rounds[round] = RoundResult.L; } yield return (object)new WaitForSeconds(1f); 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; } }