diff --git a/GameMod/CMTracker/Menu.cs b/GameMod/CMTracker/Menu.cs new file mode 100644 index 00000000..92076c10 --- /dev/null +++ b/GameMod/CMTracker/Menu.cs @@ -0,0 +1,74 @@ +using HarmonyLib; +using Overload; +using System.Collections.Generic; +using System.Reflection.Emit; +using UnityEngine; + +namespace GameMod.CMTracker +{ + class CMTracker + { + public static bool mms_cm_runs_visible_in_tracker = false; + } + + [HarmonyPatch(typeof(UIElement), "DrawChallengeLevelSelectMenu")] + class CMTracker_Menu_UIElement_DrawChallengeLevelSelectMenu + { + + private static void PatchMenu(UIElement uie, Vector2 position) + { + uie.SelectAndDrawCheckboxItem("SUBMIT RESULTS TO PUBLIC TRACKER", position, 7, CMTracker.mms_cm_runs_visible_in_tracker, false, 1f, -1); + } + + private static IEnumerable Transpiler(IEnumerable codes) + { + // Skip from float num = 137f; to end of method, not applicable to olmod + bool skip = false; + foreach (var code in codes) + { + if (code.opcode == OpCodes.Ldc_R4 && (float)code.operand == 137f) + skip = true; + + if (skip) + continue; + + yield return code; + } + + yield return new CodeInstruction(OpCodes.Ldarg_0); + yield return new CodeInstruction(OpCodes.Ldloc_0); + yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(CMTracker_Menu_UIElement_DrawChallengeLevelSelectMenu), "PatchMenu")); + yield return new CodeInstruction(OpCodes.Ret); + } + } + + [HarmonyPatch(typeof(MenuManager), "ChallengeLevelSelectUpdate")] + internal class CMTracker_Menu_MenuManager_ChallengeLevelSelectUpdate + { + private static int PatchMenu() + { + if (UIManager.m_menu_selection == 7) + { + CMTracker.mms_cm_runs_visible_in_tracker = !CMTracker.mms_cm_runs_visible_in_tracker; + MenuManager.PlaySelectSound(); + return UIManager.m_menu_selection; + } + + // ilcode represented differently in Harmony than dnSpy, we need to return something other than UIManager.m_menu_selection to hit CreateNewGame branch + return 99; + } + + private static IEnumerable Transpiler(IEnumerable codes) + { + foreach (var code in codes) + { + if (code.opcode == OpCodes.Ldc_I4_S && (sbyte)code.operand == 97) + { + yield return new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(CMTracker_Menu_MenuManager_ChallengeLevelSelectUpdate), "PatchMenu")); + continue; + } + yield return code; + } + } + } +} diff --git a/GameMod/CMTracker/Models/LeaderboardEntry.cs b/GameMod/CMTracker/Models/LeaderboardEntry.cs new file mode 100644 index 00000000..ebe63d60 --- /dev/null +++ b/GameMod/CMTracker/Models/LeaderboardEntry.cs @@ -0,0 +1,11 @@ +namespace GameMod.CMTracker.Models +{ + public class LeaderboardEntry + { + public int FavoriteWeaponId { get; set; } + public float AliveTime { get; set; } + public int RobotsDestroyed { get; set; } + public string PilotName { get; set; } + public int Score { get; set; } + } +} \ No newline at end of file diff --git a/GameMod/CMTracker/Models/Run.cs b/GameMod/CMTracker/Models/Run.cs new file mode 100644 index 00000000..ca367fad --- /dev/null +++ b/GameMod/CMTracker/Models/Run.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace GameMod.CMTracker.Models +{ + public class Run + { + public string PlayerId { get; set; } + public string PilotName { get; set; } + public bool DisplayOnTracker { get; set; } + public string LevelName { get; set; } + public string LevelHash { get; set; } + public int KillerId { get; set; } + public int FavoriteWeaponId { get; set; } + public int DifficultyLevelId { get; set; } + public int ModeId { get; set; } + public int RobotsDestroyed { get; set; } + public float AliveTime { get; set; } + public int Score { get; set; } + public float SmashDamage { get; set; } + public int SmashKills { get; set; } + public float AutoOpDamage { get; set; } + public int AutoOpKills { get; set; } + public float SelfDamage { get; set; } + public List StatsRobot { get; set; } + public List StatsPlayer { get; set; } + } +} \ No newline at end of file diff --git a/GameMod/CMTracker/Models/StatPlayer.cs b/GameMod/CMTracker/Models/StatPlayer.cs new file mode 100644 index 00000000..739638e4 --- /dev/null +++ b/GameMod/CMTracker/Models/StatPlayer.cs @@ -0,0 +1,10 @@ +namespace GameMod.CMTracker.Models +{ + public class StatPlayer + { + public int WeaponTypeId { get; set; } + public bool IsPrimary { get; set; } + public float DamageDealt { get; set; } + public int NumKilled { get; set; } + } +} \ No newline at end of file diff --git a/GameMod/CMTracker/Models/StatRobot.cs b/GameMod/CMTracker/Models/StatRobot.cs new file mode 100644 index 00000000..8c238bc7 --- /dev/null +++ b/GameMod/CMTracker/Models/StatRobot.cs @@ -0,0 +1,11 @@ +namespace GameMod.CMTracker.Models +{ + public class StatRobot + { + public int EnemyTypeId { get; set; } + public bool IsSuper { get; set; } + public float DamageReceived { get; set; } + public float DamageDealt { get; set; } + public int NumKilled { get; set; } + } +} \ No newline at end of file diff --git a/GameMod/CMTracker/Platform.cs b/GameMod/CMTracker/Platform.cs new file mode 100644 index 00000000..f2744e1a --- /dev/null +++ b/GameMod/CMTracker/Platform.cs @@ -0,0 +1,174 @@ +using HarmonyLib; +using Newtonsoft.Json; +using Overload; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Networking; + +namespace GameMod.CMTracker +{ + [HarmonyPatch(typeof(Platform), "Init")] + internal class CMTracker_Platform_Init + { + static void Postfix() + { + AccessTools.Field(typeof(Platform), "CloudProvider").SetValue(null, 4); + } + } + + [HarmonyPatch(typeof(Platform), "UserName", MethodType.Getter)] + internal class CMTracker_Platform_UserName + { + static void Postfix(ref string __result) + { + __result = PilotManager.PilotName; + } + } + + [HarmonyPatch(typeof(Platform), "PlatformName", MethodType.Getter)] + internal class CMTracker_Platform_PlatformName + { + public static void Postfix(ref string __result) + { + __result = "OLMOD"; + } + } + + [HarmonyPatch(typeof(Platform), "StatsAvailable", MethodType.Getter)] + internal class CMTracker_Platform_PlatformStatsAvailable + { + public static void Postfix(ref bool __result) + { + __result = true; + } + } + + [HarmonyPatch(typeof(Platform), "OnlineErrorMessage", MethodType.Getter)] + internal class CMTracker_Platform_OnlineErrorMessage + { + public static void Postfix(ref string __result) + { + __result = null; + } + } + + [HarmonyPatch(typeof(Platform), "GetLeaderboardData")] + internal class CMTracker_Platform_GetLeaderboardData + { + public static void Postfix(ref LeaderboardEntry[] __result, out int leaderboard_length, out int user_index, out Platform.LeaderboardDataState result) + { + user_index = -1; + leaderboard_length = 0; + + if (m_download_state == DownloadState.NoLeaderboard) + { + result = Platform.LeaderboardDataState.NoLeaderboard; + __result = null; + return; + } + if (m_download_state == DownloadState.RetryFromStart) + { + m_request_start = 1; + try + { + Platform.RequestChallengeLeaderboardData(MenuManager.ChallengeMission.DisplayName, MenuManager.m_leaderboard_challenge_countdown, MenuManager.m_leaderboard_difficulty, 1, m_request_num_entries - 1, false); + m_download_state = DownloadState.WaitingForData; + result = Platform.LeaderboardDataState.Waiting; + } + catch (Exception ex) + { + Debug.Log($"Error requesting olmod leaderboard entries: {ex.Message}"); + m_download_state = DownloadState.NoLeaderboard; + result = Platform.LeaderboardDataState.NoLeaderboard; + } + __result = null; + return; + } + if (m_download_state != DownloadState.HaveData) + { + result = Platform.LeaderboardDataState.Waiting; + __result = null; + return; + } + + LeaderboardEntry[] array = new LeaderboardEntry[m_entries.Length]; + for (int i = 0; i < m_entries.Length; i++) + { + if (m_entries[i].m_name == null) + { + m_entries[i].m_name = "m_name here"; + m_entries[i].m_rank = i + 1; + } + array[i] = m_entries[i]; + } + leaderboard_length = m_entries.Length; + result = Platform.LeaderboardDataState.HaveData; + __result = array; + } + + public static DownloadState m_download_state = DownloadState.RetryFromStart; + public static int m_request_start; + public static int m_request_num_entries = 0; + public static int m_leaderboard_length = 0; + public static LeaderboardEntry[] m_entries; + + public enum DownloadState + { + WaitingForData, + NoLeaderboard, + RetryFromStart, + HaveData + } + } + + [HarmonyPatch(typeof(Platform), "RequestChallengeLeaderboardData")] + internal static class CMRequestChallengeLeaderboardData + { + public static CloudDataYield Postfix(CloudDataYield __result, string level_name, bool submode, int difficulty_level, int range_start, int num_entries, bool friends) + { + var levelHash = MenuManager.ChallengeMission.IsLevelAnAddon(MenuManager.m_leaderboard_level_num) + ? MenuManager.ChallengeMission.GetAddOnLevelIdStringHash(MenuManager.m_leaderboard_level_num) + : MenuManager.ChallengeMission.GetLevelFileName(MenuManager.m_leaderboard_level_num) + ":STOCK"; + CMTracker_Platform_GetLeaderboardData.m_download_state = CMTracker_Platform_GetLeaderboardData.DownloadState.WaitingForData; + GameManager.m_gm.StartCoroutine(DownloadLeaderboard(level_name, levelHash, Convert.ToInt32(submode), difficulty_level)); + CloudDataYield cdy = new CloudDataYield(() => CMTracker_Platform_GetLeaderboardData.m_download_state != CMTracker_Platform_GetLeaderboardData.DownloadState.HaveData && + CMTracker_Platform_GetLeaderboardData.m_download_state != CMTracker_Platform_GetLeaderboardData.DownloadState.NoLeaderboard); + + __result = cdy; + return null; + } + + static IEnumerator DownloadLeaderboard(string levelName, string levelHash, int modeId, int difficultyLevelId) + { + var url = $"{Config.Settings.Value("trackerBaseUrl")}/api/challengemodeleaderboard?levelHash={levelHash}&difficultyLevelId={difficultyLevelId}&modeId={modeId}"; + + UnityWebRequest www = UnityWebRequest.Get(url); + yield return www.SendWebRequest(); + + if (www.isNetworkError || www.isHttpError) + { + Debug.Log($"DownloadLeaderboard Error: {www.error}"); + } + else + { + List results = JsonConvert.DeserializeObject>(www.downloadHandler.text); + + CMTracker_Platform_GetLeaderboardData.m_entries = results.Select(x => new LeaderboardEntry + { + m_data_is_valid = true, + m_favorite_weapon = x.FavoriteWeaponId, + m_game_time = (int)Math.Round(x.AliveTime), + m_kills = x.RobotsDestroyed, + m_name = x.PilotName, + m_rank = 1, + m_score = x.Score, + m_time_stamp = DateTime.Now + }).ToArray(); + CMTracker_Platform_GetLeaderboardData.m_download_state = CMTracker_Platform_GetLeaderboardData.DownloadState.HaveData; + } + } + } +} \ No newline at end of file diff --git a/GameMod/CMTracker/PostRun.cs b/GameMod/CMTracker/PostRun.cs new file mode 100644 index 00000000..5f763c56 --- /dev/null +++ b/GameMod/CMTracker/PostRun.cs @@ -0,0 +1,133 @@ +using GameMod.CMTracker.Models; +using HarmonyLib; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Overload; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace GameMod.CMTracker +{ + [HarmonyPatch(typeof(Overload.GameplayManager), "DoneLevel")] + internal class CMTracker_PostRun_GameplayManager_DoneLevel + { + static void Postfix(GameplayManager.DoneReason reason) + { + if (GameplayManager.IsChallengeMode && GameplayManager.m_level_info.Mission.FileName != "_EDITOR" && (int)ChallengeManager.ChallengeRobotsDestroyed > 0) + { + string url = $"{Config.Settings.Value("trackerBaseUrl")}/api/challengemoderun"; + Post(url, GetTrackerPost()); + } + } + + private static void Post(string url, Models.Run cmRun) + { + var request = new UnityWebRequest(url) + { + uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject(cmRun, new JsonSerializerSettings() + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }))), + downloadHandler = new DownloadHandlerBuffer(), + method = UnityWebRequest.kHttpVerbPOST + }; + request.SetRequestHeader("Content-Type", "application/json"); + GameManager.m_gm.StartCoroutine(RequestCoroutine(request.SendWebRequest())); + } + + private static IEnumerator RequestCoroutine(UnityWebRequestAsyncOperation op) + { + yield return op; + } + + private static Models.Run GetTrackerPost() + { + var request = new Models.Run + { + PlayerId = PlayerPrefs.GetString("UserID"), + PilotName = PilotManager.PilotName, + DisplayOnTracker = CMTracker.mms_cm_runs_visible_in_tracker, + RobotsDestroyed = (int)ChallengeManager.ChallengeRobotsDestroyed, + AliveTime = GameplayManager.AliveTime, + Score = (int)ChallengeManager.ChallengeScore, + LevelName = GameplayManager.Level.DisplayName, + LevelHash = GameplayManager.Level.IsAddOn ? GameplayManager.Level.GetAddOnLevelIdStringHash() : GameplayManager.Level.FileName + ":STOCK", + ModeId = Convert.ToInt32(ChallengeManager.CountdownMode), + FavoriteWeaponId = GameplayManager.MostDamagingWeapon(), + DifficultyLevelId = (int)GameplayManager.DifficultyLevel, + KillerId = GameplayManager.m_stats_player_killer, + SmashDamage = GameplayManager.m_robot_cumulative_damage_by_other_type[0], + SmashKills = GameplayManager.m_other_killer[0], + AutoOpDamage = GameplayManager.m_robot_cumulative_damage_by_other_type[1], + AutoOpKills = GameplayManager.m_other_killer[1], + SelfDamage = GameplayManager.m_stats_player_self_damage, + StatsRobot = new List(), + StatsPlayer = new List() + }; + + var enemyTypes = Enum.GetNames(typeof(EnemyType)); + for (int i = 0; i < GameplayManager.m_player_cumulative_damage_by_robot_type.Length; i++) + { + if (GameplayManager.m_player_cumulative_damage_by_robot_type[i] != 0) + { + request.StatsRobot.Add(new StatRobot + { + EnemyTypeId = i, + IsSuper = false, + DamageReceived = GameplayManager.m_player_cumulative_damage_by_robot_type[i], + DamageDealt = 0, + NumKilled = GameplayManager.m_robots_killed.Count(x => x.robot_type == (EnemyType)i) + }); + } + + if (GameplayManager.m_player_cumulative_damage_by_super_robot_type[i] != 0) + { + request.StatsRobot.Add(new StatRobot + { + EnemyTypeId = i, + IsSuper = true, + DamageReceived = GameplayManager.m_player_cumulative_damage_by_super_robot_type[i], + DamageDealt = 0, + NumKilled = GameplayManager.m_super_robots_killed.Count(x => x.robot_type == (EnemyType)i) + }); + } + } + + for (int i = 0; i < 8; i++) + { + if (GameplayManager.m_robot_cumulative_damage_by_weapon_type[i] != 0 || GameplayManager.m_weapon_killer[i] != 0) + { + request.StatsPlayer.Add(new StatPlayer + { + IsPrimary = true, + WeaponTypeId = i, + DamageDealt = GameplayManager.m_robot_cumulative_damage_by_weapon_type[i], + NumKilled = GameplayManager.m_weapon_killer[i] + }); + } + } + + for (int i = 0; i < 8; i++) + { + if (GameplayManager.m_robot_cumulative_damage_by_missile_type[i] != 0 || GameplayManager.m_missile_killer[i] != 0) + { + request.StatsPlayer.Add(new StatPlayer + { + IsPrimary = false, + WeaponTypeId = i, + DamageDealt = GameplayManager.m_robot_cumulative_damage_by_missile_type[i], + NumKilled = GameplayManager.m_missile_killer[i] + }); + } + } + + return request; + } + } +} \ No newline at end of file diff --git a/GameMod/GameMod.csproj b/GameMod/GameMod.csproj index ae2fafa4..e86977c3 100644 --- a/GameMod/GameMod.csproj +++ b/GameMod/GameMod.csproj @@ -105,6 +105,12 @@ + + + + + + diff --git a/GameMod/MPSetup.cs b/GameMod/MPSetup.cs index eddb2877..affcf025 100644 --- a/GameMod/MPSetup.cs +++ b/GameMod/MPSetup.cs @@ -292,6 +292,7 @@ static void Postfix(string filename) FramerateLimiter.target_framerate = ModPrefs.GetInt("TARGET_FRAMERATE", 0); Menus.mms_collision_mesh = ModPrefs.GetInt("MP_COLLIDER_MESH", 0); + CMTracker.CMTracker.mms_cm_runs_visible_in_tracker = ModPrefs.GetBool("CM_RUNS_VISIBLE_IN_TRACKER", true); } else // for compatibility with old olmod, no need to add new settings { @@ -382,6 +383,7 @@ private static void Prefix(string filename) ModPrefs.SetBool("MP_AUDIOTAUNT_SHOW_FREQUENCYBAND", MPAudioTaunts.AClient.display_audio_spectrum); ModPrefs.SetInt("TARGET_FRAMERATE", FramerateLimiter.target_framerate); ModPrefs.SetInt("MP_COLLIDER_MESH", Menus.mms_collision_mesh); + ModPrefs.SetBool("CM_RUNS_VISIBLE_IN_TRACKER", CMTracker.CMTracker.mms_cm_runs_visible_in_tracker); ModPrefs.Flush(filename + "mod"); }