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;
}
}