PlayerCodePatcher.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
  2. using System;
  3. using System.Threading;
  4. using System.Threading.Tasks;
  5. using SingularityGroup.HotReload.DTO;
  6. namespace SingularityGroup.HotReload {
  7. static class PlayerCodePatcher {
  8. static Timer timer;
  9. static PlayerCodePatcher() {
  10. if (PlayerEntrypoint.IsPlayerWithHotReload()) {
  11. timer = new Timer(OnIntervalThreaded, (Action) OnIntervalMainThread, 500, 500);
  12. serverHealthyAt = DateTime.MinValue;
  13. }
  14. }
  15. private static DateTime serverHealthyAt;
  16. private static TimeSpan TimeSinceServerHealthy() => DateTime.UtcNow - serverHealthyAt;
  17. /// <summary>
  18. /// Set server that you want to try connect to.
  19. /// </summary>
  20. /// <remarks>
  21. /// <para>
  22. /// This allows repetitions of:
  23. /// - try handshake
  24. /// - success -> try healthcheck
  25. /// - success -> poll method patches
  26. /// -
  27. /// </para>
  28. /// <para>
  29. /// Only do this after confirming (with /handshake) that server is compatible with this build.<br/>
  30. /// The user will be prompted if handshake needs confirmation.
  31. /// </para>
  32. /// </remarks>
  33. internal static Task<ServerHandshake.Result> UpdateHost(PatchServerInfo serverInfo) {
  34. Log.Debug($"UpdateHost to {(serverInfo == null ? "null" : serverInfo.hostName)}");
  35. // In player builds, server is remote, se we don't load assemblies from any paths
  36. RequestHelper.ChangeAssemblySearchPaths(Array.Empty<string>());
  37. ServerHealthCheck.I.SetServerInfo(null); // stop doing health check on old server
  38. RequestHelper.SetServerInfo(serverInfo);
  39. // Show feedback about connection progress (handshake can take ~5 seconds for our big game)
  40. if (serverInfo == null) {
  41. Prompts.SetConnectionState(ConnectionSummary.Disconnected);
  42. } else {
  43. Prompts.SetConnectionState(ConnectionSummary.Connected);
  44. Prompts.ShowConnectionDialog();
  45. }
  46. return ServerHandshake.I.SetServerInfo(serverInfo);
  47. }
  48. public static Task Disconnect() => UpdateHost(null);
  49. static void OnIntervalThreaded(object o) {
  50. ServerHandshake.I.CheckHandshake();
  51. ServerHealthCheck.I.CheckHealthAsync().Forget();
  52. ThreadUtility.RunOnMainThread((Action)o);
  53. }
  54. static string lastPatchId = string.Empty;
  55. static void OnIntervalMainThread() {
  56. PatchServerInfo verifiedServer;
  57. if(ServerHandshake.I.TryGetVerifiedServer(out verifiedServer)) {
  58. // now that handshake verified, we are connected.
  59. // Note: If there is delay between handshake done and chosing to connect, then it may be outdated.
  60. Prompts.SetConnectionState(ConnectionSummary.Connecting);
  61. // Note: verified does not imply that server is running, sometimes we verify the host just from the deeplink data
  62. ServerHealthCheck.I.SetServerInfo(verifiedServer);
  63. }
  64. if(ServerHealthCheck.I.IsServerHealthy) {
  65. // we may have reconnected to the same host, after losing connection for several seconds
  66. Prompts.SetConnectionState(ConnectionSummary.Connected, false);
  67. serverHealthyAt = DateTime.UtcNow;
  68. RequestHelper.PollMethodPatches(lastPatchId, resp => HandleResponseReceived(resp));
  69. } else if (ServerHealthCheck.I.WasServerResponding) { // only update prompt state if disconnected server
  70. var secondsSinceHealthy = TimeSinceServerHealthy().TotalSeconds;
  71. var reconnectTimeout = 30; // seconds
  72. if (secondsSinceHealthy > 2) {
  73. Log.Info("Hot Reload was unreachable for 5 seconds, trying to reconnect...");
  74. // feedback for the user so they know why patches are not applying
  75. Prompts.SetConnectionState($"{ConnectionSummary.TryingToReconnect} {reconnectTimeout - secondsSinceHealthy:F0}s", false);
  76. Prompts.ShowConnectionDialog();
  77. }
  78. if (secondsSinceHealthy > reconnectTimeout) {
  79. // give up on the server, give user a way to connect to another
  80. Log.Info($"Hot Reload was unreachable for {reconnectTimeout} seconds, disconnecting");
  81. var disconnectedServer = RequestHelper.ServerInfo;
  82. Disconnect().Forget();
  83. // Let user tap button to retry connecting to the same server (maybe just need to run Hot Reload again)
  84. // Assumption: prompt also has a way to connect to a different server
  85. Prompts.ShowRetryDialog(disconnectedServer);
  86. }
  87. }
  88. }
  89. static void HandleResponseReceived(MethodPatchResponse response) {
  90. Log.Debug("PollMethodPatches handling MethodPatchResponse id:{0} response.patches.Length:{1} response.failures.Length:{2}",
  91. response.id, response.patches.Length, response.failures.Length);
  92. if(response.patches.Length > 0) {
  93. CodePatcher.I.RegisterPatches(response, persist: true);
  94. }
  95. if(response.failures.Length > 0) {
  96. foreach (var failure in response.failures) {
  97. // feedback to user so they know why their patch wasn't applied
  98. Log.Warning(failure);
  99. }
  100. }
  101. lastPatchId = response.id;
  102. }
  103. }
  104. }
  105. #endif