Repository navigation
Expand file tree
/
Copy pathVCManager.cs
More file actions
213 lines (191 loc) · 8.44 KB
/
Copy pathVCManager.cs
File metadata and controls
213 lines (191 loc) · 8.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
using Il2CppInterop.Runtime.Injection;
using UnityEngine;
using UnityEngine.SceneManagement;
using VoiceChatPlugin.VoiceChat;
namespace VoiceChatPlugin;
internal static class VoiceSceneInputPolicy
{
private static bool IsContinuousVoiceScene(string sceneName)
=> sceneName is "OnlineGame" or "EndGame";
internal static bool ShouldPreserveHeldTransmitInputs(
string previousSceneName,
string nextSceneName)
=> IsContinuousVoiceScene(previousSceneName) && IsContinuousVoiceScene(nextSceneName);
internal static string ResolvePreviousSceneName(
string eventPreviousSceneName,
string nextSceneName,
string cachedActiveSceneName,
string sceneNameBeforeLoad)
{
if (!string.IsNullOrEmpty(eventPreviousSceneName))
return eventPreviousSceneName;
if (!string.IsNullOrEmpty(cachedActiveSceneName) &&
cachedActiveSceneName != nextSceneName)
return cachedActiveSceneName;
if (!string.IsNullOrEmpty(sceneNameBeforeLoad) &&
sceneNameBeforeLoad != nextSceneName)
return sceneNameBeforeLoad;
return "";
}
internal static bool ShouldReleaseHeldTransmitInputs(
bool preserveHeldTransmitInputs,
bool panelWasOpen)
=> !preserveHeldTransmitInputs || panelWasOpen;
}
internal class VCManager : MonoBehaviour
{
private static bool _sceneHookRegistered;
private static GameObject? _managerObject;
// Cached active scene name. Reading SceneManager.GetActiveScene().name marshals a fresh managed
// string across the IL2CPP boundary on every access; doing that per-frame in Update() was a steady
// GC contributor. We update this only on scene load / active-scene change instead.
private static string _activeSceneName = "";
// sceneLoaded can precede activeSceneChanged. Preserve the old cache until the latter decides
// whether the transition is a privacy boundary.
private static string _sceneNameBeforeLoad = "";
static VCManager()
{
ClassInjector.RegisterTypeInIl2Cpp<VCManager>();
}
internal static void RegisterSceneHook()
{
if (_sceneHookRegistered) return;
_sceneHookRegistered = true;
_activeSceneName = SceneManager.GetActiveScene().name;
VoiceSceneState.SetEndGameSceneHint(_activeSceneName == "EndGame");
VoiceChatRoom.NotifyScenePhaseBoundary();
SceneManager.sceneLoaded +=
(UnityEngine.Events.UnityAction<Scene, LoadSceneMode>)OnSceneLoaded;
SceneManager.activeSceneChanged +=
(UnityEngine.Events.UnityAction<Scene, Scene>)OnActiveSceneChanged;
}
private static void OnActiveSceneChanged(Scene previous, Scene next)
{
string previousSceneName = VoiceSceneInputPolicy.ResolvePreviousSceneName(
previous.name,
next.name,
_activeSceneName,
_sceneNameBeforeLoad);
_sceneNameBeforeLoad = "";
_activeSceneName = next.name;
VoiceSceneState.SetEndGameSceneHint(_activeSceneName == "EndGame");
VoiceChatRoom.NotifyScenePhaseBoundary();
VoiceUiKit.ClosePersistentPanels(
$"active-scene:{previousSceneName}->{next.name}",
preserveHeldTransmitInputs:
VoiceSceneInputPolicy.ShouldPreserveHeldTransmitInputs(
previousSceneName,
next.name));
}
private static void OnSceneLoaded(Scene scene, LoadSceneMode _)
{
// Among Us transitions are single-mode loads, so the loaded scene becomes the active one.
// Refresh the cache here too in case activeSceneChanged ordering differs across the boundary.
if (_activeSceneName != scene.name)
_sceneNameBeforeLoad = _activeSceneName;
_activeSceneName = scene.name;
VoiceSceneState.SetEndGameSceneHint(_activeSceneName == "EndGame");
VoiceChatRoom.NotifyScenePhaseBoundary();
// activeSceneChanged owns the privacy-boundary decision. This callback may run before or
// after it, so it closes persistent UI without turning the second notification into a
// spurious held-key release. Session creation/closure independently releases every hold.
VoiceUiKit.ClosePersistentPanels(
$"scene-loaded:{scene.name}",
preserveHeldTransmitInputs: true);
VoiceFirstRunSetup.CloseForSceneChange();
EnsureManagerObject();
switch (scene.name)
{
case "MainMenu":
case "MatchMaking":
VoiceJoinGuard.Reset();
VoiceLobbyRegistryPublisher.ClearLocalListing();
VoiceChatRoom.CloseCurrentRoom($"scene-loaded:{scene.name}");
VoiceLobbyBrowserUi.Clear();
break;
}
}
private static void EnsureManagerObject()
{
if (_managerObject != null) return;
_managerObject = new GameObject("VC_Manager");
GameObject.DontDestroyOnLoad(_managerObject);
_managerObject.hideFlags |= HideFlags.DontUnloadUnusedAsset | HideFlags.HideAndDontSave;
_managerObject.AddComponent<VCManager>();
}
private static float _lastUpdateErrorLogTime = -999f;
private static string _lastUpdateErrorStep = "";
// Unity calls this on its main thread before native/managed teardown. Release the active
// session and synchronously terminate the process-lifetime helper here; AppDomain hooks are
// the last-resort fallback.
void OnApplicationQuit()
{
VoiceChatPluginMain.ShutdownVoiceRuntime("application-quit");
}
void OnApplicationPause(bool paused)
{
if (paused)
{
try { VoiceChatPatches.ReleaseHeldTransmitInputs(); } catch { }
}
try { VoiceChatRoom.Current?.SetApplicationPaused(paused); } catch { }
}
void OnApplicationFocus(bool hasFocus)
{
if (hasFocus) return;
// KeyboardJoystick.Update is not guaranteed to run after Windows/Android focus is lost.
// Release transmit holds from Unity's authoritative focus callback as well.
try { VoiceChatPatches.ReleaseHeldTransmitInputs(); } catch { }
}
void Update()
{
switch (_activeSceneName)
{
case "OnlineGame":
case "EndGame":
VoiceJoinGuard.Tick();
VoiceFrameProfiler.Tick();
long vcTicks = VoiceFrameProfiler.Begin();
VoiceChatPatches.UpdateKeybindsFromFrameDriver();
VoiceSettingsPanelTriggers.UpdatePanelHotkeysFromFrameDriver();
long hudTicks = VoiceFrameProfiler.Begin();
SafeUpdateHud();
VoiceFrameProfiler.End("hud", hudTicks);
VoiceChatRoomDriver.Update();
PingTrackerPatch.RenderEndGameOverlay();
long pubTicks = VoiceFrameProfiler.Begin();
VoiceLobbyRegistryPublisher.Update();
VoiceFrameProfiler.End("publisher", pubTicks);
VoiceFrameProfiler.End("vc.tick", vcTicks);
break;
default:
// Left the profiled scenes: flush the final frame + open window so they aren't stranded.
// No-ops when profiling is disabled or nothing is pending.
VoiceFrameProfiler.Flush();
break;
}
}
// IL2CPP scene transitions can invalidate AllPlayerControls mid-walk; swallow the throw so
// Update isn't aborted, which would strand the player in their last (possibly wrong) mute state.
private static void SafeUpdateHud()
{
try
{
VoiceChatHudState.UpdateHud();
}
catch (System.Exception ex)
{
string step = VoiceChatHudState.LastUpdateStep;
string context = VoiceChatHudState.DescribeUpdateContext();
VoiceChatHudState.RecoverAfterUpdateFailure();
// Keep repeated failures quiet, but do not hide a second failure in a different
// initialization step (for example mic clone then speaker clone) behind the timer.
if (step == _lastUpdateErrorStep && Time.time - _lastUpdateErrorLogTime < 5f) return;
_lastUpdateErrorLogTime = Time.time;
_lastUpdateErrorStep = step;
VoiceDiagnostics.DebugError(
$"[VC] HUD update failed scene={_activeSceneName} frame={Time.frameCount} " +
$"{context}: {ex.GetType().Name}: {ex.Message}");
}
}
}