|
| 1 | +using HarmonyLib; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Linq; |
| 4 | +using System.Reflection; |
| 5 | +using System.Reflection.Emit; |
| 6 | +using UnityEngine; |
| 7 | + |
| 8 | +namespace Distance.ReplayIntensifies.Harmony |
| 9 | +{ |
| 10 | + /// <summary> |
| 11 | + /// Patch to handle filling unused replay slots where there aren't enough online replays to load. |
| 12 | + /// <para/> |
| 13 | + /// This function is only called when auto-loading replays (AKA not when picking individual replays from a menu). |
| 14 | + /// </summary> |
| 15 | + /// <remarks> |
| 16 | + /// Required For: Fill With Local Replays. |
| 17 | + /// </remarks> |
| 18 | + [HarmonyPatch(typeof(ReplayManager), nameof(ReplayManager.OnLoadedOnlineReplaysDownloadFinished))] |
| 19 | + internal static class ReplayManager__OnLoadedOnlineReplaysDownloadFinished |
| 20 | + { |
| 21 | + [HarmonyTranspiler] |
| 22 | + internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) |
| 23 | + { |
| 24 | + var codes = new List<CodeInstruction>(instructions); |
| 25 | + |
| 26 | + Mod.Instance.Logger.Info("Transpiling..."); |
| 27 | + // VISUAL: |
| 28 | + // After online replays are populated, handle filling unused slots with local replays. |
| 29 | + //this.AddOnlineReplaysToGroup(replays, this.loadedReplays_); |
| 30 | + // -to- |
| 31 | + //this.AddOnlineReplaysToGroup(replays, this.loadedReplays_); |
| 32 | + //FillRemainingWithLocalReplays_(this, this.loadedReplays_, userSkipped); |
| 33 | + |
| 34 | + for (int i = 1; i < codes.Count; i++) |
| 35 | + { |
| 36 | + if ((codes[i - 1].opcode == OpCodes.Ldfld) && // 'group' (should be loadedReplays_) |
| 37 | + (codes[i ].opcode == OpCodes.Call && ((MethodInfo)codes[i ].operand).Name == "AddOnlineReplaysToGroup")) |
| 38 | + { |
| 39 | + Mod.Instance.Logger.Info($"call AddOnlineReplaysToGroup @ {i}"); |
| 40 | + |
| 41 | + // After: call AddOnlineReplaysToGroup |
| 42 | + // Insert: ldarg.0 |
| 43 | + // Insert: ldarg.0 |
| 44 | + // Insert: ldfld 'group' (should be loadedReplays_) |
| 45 | + // Insert: ldarg.2 |
| 46 | + // Insert: call FillRemainingWithLocalReplays_ |
| 47 | + codes.InsertRange(i + 1, new CodeInstruction[] |
| 48 | + { |
| 49 | + new CodeInstruction(OpCodes.Ldarg_0, null), |
| 50 | + new CodeInstruction(OpCodes.Ldarg_0, null), |
| 51 | + new CodeInstruction(OpCodes.Ldfld, codes[i - 1].operand), // 'group' (should be loadedReplays_) |
| 52 | + new CodeInstruction(OpCodes.Ldarg_2, null), // userSkipped |
| 53 | + new CodeInstruction(OpCodes.Call, typeof(ReplayManager__OnLoadedOnlineReplaysDownloadFinished).GetMethod(nameof(FillRemainingWithLocalReplays_))), |
| 54 | + }); |
| 55 | + |
| 56 | + break; |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + return codes.AsEnumerable(); |
| 61 | + } |
| 62 | + |
| 63 | + #region Helpers |
| 64 | + |
| 65 | + public static void FillRemainingWithLocalReplays_(ReplayManager replayManager, GameObject group, bool userSkipped) |
| 66 | + { |
| 67 | + if (!Mod.Instance.Config.FillWithLocalReplays) |
| 68 | + { |
| 69 | + return; // Stop now if the setting is disabled. |
| 70 | + } |
| 71 | + // TODO: Should we stop when the user skips loading the remaining online ghosts? |
| 72 | + // This is assuming skip is generally only used when unintenionally started a level. |
| 73 | + /*if (userSkipped) |
| 74 | + { |
| 75 | + return; |
| 76 | + }*/ |
| 77 | + |
| 78 | + CarReplayData[] onlineReplays = group.GetComponentsInChildren<CarReplayData>(); |
| 79 | + int missingReplayCount = replayManager.settings_.GhostsInArcadeCount_ - onlineReplays.Length; |
| 80 | + if (missingReplayCount <= 0) |
| 81 | + { |
| 82 | + return; // We already have the maximum number of replays. |
| 83 | + } |
| 84 | + |
| 85 | + string levelPath = replayManager.gm_.NextLevelPath_; |
| 86 | + GameModeID gameModeID = replayManager.gm_.NextGameModeID_; |
| 87 | + LocalLeaderboard localLeaderboard = LocalLeaderboard.Load(levelPath, gameModeID); |
| 88 | + if (localLeaderboard != null) |
| 89 | + { |
| 90 | + // Depending on the player's ghost download type (Friends/Global Near Me/Global Best), it's possible that |
| 91 | + // we've also downloaded one of our own ghosts. Because that ghost normally would be the top local replay |
| 92 | + // on our leaderboard, we need to take time to filter that out to avoid duplicate overlapping ghosts. |
| 93 | + |
| 94 | + // NOTE: It's not guaranteed the top local replay will be our online ghost, since it's possible it was created |
| 95 | + // while there was no online connection. So compare all local replays for `missingReplayCount + 1`. |
| 96 | + List<CarReplayData> localReplays = CarReplayData.LoadReplaysFromLeaderboard(localLeaderboard, missingReplayCount + 1).ToList(); |
| 97 | + |
| 98 | + bool duplicateFound = false; |
| 99 | + for (int i = 0; i < onlineReplays.Length && !duplicateFound; i++) |
| 100 | + { |
| 101 | + var onlineReplay = onlineReplays[i]; |
| 102 | + |
| 103 | + for (int j = 0; j < localReplays.Count; j++) |
| 104 | + { |
| 105 | + var localReplay = localReplays[j]; |
| 106 | + // Compare the data that's most likely to differ first. |
| 107 | + if ((localReplay.steamID_ == 0 || localReplay.steamID_ == onlineReplay.steamID_) && |
| 108 | + localReplay.FinishValue_ == onlineReplay.FinishValue_ && |
| 109 | + localReplay.ReplayLengthMS_ == onlineReplay.ReplayLengthMS_ && |
| 110 | + localReplay.StateBuffer_.Length == onlineReplay.StateBuffer_.Length && |
| 111 | + localReplay.EventBuffer_.Length == onlineReplay.EventBuffer_.Length && |
| 112 | + localReplay.carData_.name_ == onlineReplay.carData_.name_ && |
| 113 | + localReplay.carData_.colors_.primary_ == onlineReplay.carData_.colors_.primary_ && |
| 114 | + localReplay.carData_.colors_.secondary_ == onlineReplay.carData_.colors_.secondary_ && |
| 115 | + localReplay.carData_.colors_.glow_ == onlineReplay.carData_.colors_.glow_ && |
| 116 | + localReplay.carData_.colors_.sparkle_ == onlineReplay.carData_.colors_.sparkle_ && |
| 117 | + localReplay.StateBuffer_.SequenceEqual(onlineReplay.StateBuffer_) && |
| 118 | + localReplay.EventBuffer_.SequenceEqual(onlineReplay.EventBuffer_)) |
| 119 | + { |
| 120 | + // This replay is the same as the player's online replay, avoid populating with duplicates. |
| 121 | + localReplays.RemoveAt(j); |
| 122 | + duplicateFound = true; |
| 123 | + break; |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + if (!duplicateFound && localReplays.Count > missingReplayCount) |
| 128 | + { |
| 129 | + // We obtained more replays than we wanted (to check for dups), so remove the last in the list. |
| 130 | + localReplays.RemoveAt(localReplays.Count - 1); |
| 131 | + } |
| 132 | + |
| 133 | + if (localReplays.Count > 0) |
| 134 | + { |
| 135 | + replayManager.AddReplaysToGroup(localReplays.ToArray(), replayManager.loadedReplays_); |
| 136 | + } |
| 137 | + UnityEngine.Object.Destroy(localLeaderboard.gameObject); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + #endregion |
| 142 | + } |
| 143 | +} |
0 commit comments