diff --git a/.utmp/RelWithDebInfo/6b10225s/arm64-v8a/configure_fingerprint.bin b/.utmp/RelWithDebInfo/6b10225s/arm64-v8a/configure_fingerprint.bin
index fc5ff59..88df226 100644
--- a/.utmp/RelWithDebInfo/6b10225s/arm64-v8a/configure_fingerprint.bin
+++ b/.utmp/RelWithDebInfo/6b10225s/arm64-v8a/configure_fingerprint.bin
@@ -2,29 +2,29 @@ C/C++ Structured Logb
 `
 ^E:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\additional_project_files.txtC
 A
-?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint	Øÿ‚ØÜ3› ýêôÓÜ3_
+?com.android.build.gradle.internal.cxx.io.EncodedFileFingerPrint	ËÀ°…Ý3› ýêôÓÜ3_
 ]
-[E:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\android_gradle_build.json	Øÿ‚ØÜ3Þ
 …ëôÓÜ3d
+[E:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\android_gradle_build.json	ËÀ°…Ý3Þ
 …ëôÓÜ3d
 b
-`E:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\android_gradle_build_mini.json	Øÿ‚ØÜ3È	 ÃëôÓÜ3Q
+`E:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\android_gradle_build_mini.json	ËÀ°…Ý3È	 ÃëôÓÜ3Q
 O
-ME:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\build.ninja	Øÿ‚ØÜ3Ü¡ ÜéôÓÜ3U
+ME:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\build.ninja	ËÀ°…Ý3Ü¡ ÜéôÓÜ3U
 S
-QE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\build.ninja.txt	Øÿ‚ØÜ3Z
+QE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\build.ninja.txt	ËÀ°…Ý3Z
 X
-VE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\build_file_index.txt	Øÿ‚ØÜ3ß ËëôÓÜ3[
+VE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\build_file_index.txt	ËÀ°…Ý3ß ËëôÓÜ3[
 Y
-WE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\compile_commands.json	Ùÿ‚ØÜ3£U ÚéôÓÜ3_
+WE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\compile_commands.json	ËÀ°…Ý3£U ÚéôÓÜ3_
 ]
-[E:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\compile_commands.json.bin	Ùÿ‚ØÜ3	Ä ÚéôÓÜ3e
+[E:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\compile_commands.json.bin	ËÀ°…Ý3	Ä ÚéôÓÜ3e
 c
-aE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\metadata_generation_command.txt	Ùÿ‚ØÜ3
+aE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\metadata_generation_command.txt	ËÀ°…Ý3
 ß ÊëôÓÜ3X
 V
-TE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\prefab_config.json	Ùÿ‚ØÜ3ã ÊëôÓÜ3]
+TE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\prefab_config.json	ËÀ°…Ý3ã ÊëôÓÜ3]
 [
-YE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\symbol_folder_index.txt	Ùÿ‚ØÜ3Š ËëôÓÜ3l
+YE:\Code\GeoSus\GeoSusGame\.utmp\RelWithDebInfo\6b10225s\arm64-v8a\symbol_folder_index.txt	ËÀ°…Ý3Š ËëôÓÜ3l
 j
-hE:\Code\GeoSus\GeoSusGame\Library\Bee\Android\Prj\IL2CPP\Gradle\unityLibrary\src\main\cpp\CMakeLists.txt	Ùÿ‚ØÜ3

W û”ìÓÜ3y
+hE:\Code\GeoSus\GeoSusGame\Library\Bee\Android\Prj\IL2CPP\Gradle\unityLibrary\src\main\cpp\CMakeLists.txt	ËÀ°…Ý3

W û”ìÓÜ3y
 w
-uE:\Code\GeoSus\GeoSusGame\Library\Bee\Android\Prj\IL2CPP\Gradle\unityLibrary\src\main\cpp\GameActivity\CMakeLists.txt	Ùÿ‚ØÜ3û ó”ìÓÜ3
\ No newline at end of file
+uE:\Code\GeoSus\GeoSusGame\Library\Bee\Android\Prj\IL2CPP\Gradle\unityLibrary\src\main\cpp\GameActivity\CMakeLists.txt	ËÀ°…Ý3û ó”ìÓÜ3
\ No newline at end of file
diff --git a/Assets/ClientSDK/GameClient.cs b/Assets/ClientSDK/GameClient.cs
index 00d9331..4f63cc6 100644
--- a/Assets/ClientSDK/GameClient.cs
+++ b/Assets/ClientSDK/GameClient.cs
@@ -533,6 +533,11 @@ public class GameClient : IDisposable
     
     public void CreateLobby(Position? center = null, int impostorCount = 1, int taskCount = 5, string? password = null, double playAreaRadius = 500, GameSettingsOverrides? settings = null)
     {
+        // DisplayName is sent on every CreateLobby/JoinLobby so the server
+        // picks up the live nickname (typed into the input field after the
+        // ClientHello handshake fired). Without this the server uses the
+        // ClientHello-time name, which is the GameManager prefab default
+        // for any user who immediately created/joined a lobby.
         Send(new CreateLobby
         {
             PlayAreaCenter = center,
@@ -540,16 +545,18 @@ public class GameClient : IDisposable
             ImpostorCount = impostorCount,
             TaskCount = taskCount,
             Password = password,
-            Settings = settings
+            Settings = settings,
+            DisplayName = DisplayName
         });
     }
-    
+
     public void JoinLobby(string joinCode, string? password = null)
     {
         Send(new JoinLobby
         {
             JoinCode = joinCode.ToUpperInvariant(),
-            Password = password
+            Password = password,
+            DisplayName = DisplayName
         });
     }
     
diff --git a/Assets/ClientSDK/Protocol.cs b/Assets/ClientSDK/Protocol.cs
index 1f3b302..ae6478f 100644
--- a/Assets/ClientSDK/Protocol.cs
+++ b/Assets/ClientSDK/Protocol.cs
@@ -197,6 +197,16 @@ public class CreateLobby : Message
     /// </summary>
     [JsonProperty("settings")]
     public GameSettingsOverrides? Settings { get; set; }
+
+    /// <summary>
+    /// Optional. Live host display name from the nickname input field at
+    /// the moment of CreateLobby. ClientHello-time name is stale because
+    /// the handshake fires before the user has typed anything; this lets
+    /// the server pick up the freshly-typed name without a separate
+    /// rename round-trip.
+    /// </summary>
+    [JsonProperty("displayName")]
+    public string? DisplayName { get; set; }
 }
 
 public class CreateLobbyResponse : Message
@@ -222,12 +232,19 @@ public class CreateLobbyResponse : Message
 public class JoinLobby : Message
 {
     public override string Type => "JoinLobby";
-    
+
     [JsonProperty("joinCode")]
     public string JoinCode { get; set; } = "";
-    
+
     [JsonProperty("password")]
     public string? Password { get; set; }
+
+    /// <summary>
+    /// Optional. Live joiner display name from the nickname input field
+    /// at the moment of Join. See CreateLobby.DisplayName for rationale.
+    /// </summary>
+    [JsonProperty("displayName")]
+    public string? DisplayName { get; set; }
 }
 
 public class JoinLobbyResponse : Message
diff --git a/Assets/GameManager/GameManager.cs b/Assets/GameManager/GameManager.cs
index d42e23c..a4540ff 100644
--- a/Assets/GameManager/GameManager.cs
+++ b/Assets/GameManager/GameManager.cs
@@ -66,10 +66,39 @@ public class GameManager : MonoBehaviour
 
     [Header("Debug")]
     public bool testMode = false;
-    private GameClient _secondClient;
-    private GameClient _thirdClient;
-    private GameManager_Network _secondNetwork;
-    private GameManager_Network _thirdNetwork;
+    /// <summary>
+    /// When true, draw a small GPS status banner across the top of every
+    /// screen. Useful for diagnosing why CreateLobby is blocked or why a
+    /// joiner's position isn't updating - failures otherwise only show up
+    /// in logcat which most users can't reach. Toggle off for release.
+    /// </summary>
+    public bool showGPSDebugOverlay = true;
+
+    /// <summary>
+    /// Number of in-process test client bots to spawn alongside the host
+    /// when testMode is on. Each gets its own GameClient + Network and
+    /// joins the host's lobby automatically. Bots are switchable via
+    /// number keys 1..N (host = 0). Default 3 keeps memory reasonable;
+    /// bump for stress-testing voting / sabotage flows.
+    /// </summary>
+    public int testClientCount = 3;
+
+    /// <summary>
+    /// Per-bot network + display-name + sim-position state. The active slot
+    /// (host = 0, bots = 1..N) gets WASD on the next tick.
+    /// </summary>
+    private class TestBot
+    {
+        public GameClient Client;
+        public GameManager_Network Network;
+        public string DisplayName;
+        public GeoSus.Client.Position SimPosition;
+        public bool Joined;
+        public float LastSendTime;
+    }
+    private System.Collections.Generic.List<TestBot> _testBots = new System.Collections.Generic.List<TestBot>();
+    /// <summary>Slot 0 = host (real player), 1..N = test bot index.</summary>
+    private int _activeClientSlot = 0;
 
     void Awake()
     {
@@ -80,11 +109,71 @@ public class GameManager : MonoBehaviour
         }
         Instance = this;
         DontDestroyOnLoad(gameObject);
+
+        // Keep the screen on while the player is in the app. A geographic
+        // social-deduction game asks the user to walk around for 5-15 minutes
+        // staring at the map; default Android sleep timeout (15-60s) blacks
+        // the screen out mid-round, drops GPS updates, and requires the
+        // player to re-unlock the phone. Two layers of belt-and-suspenders:
+        // (1) Unity's Screen.sleepTimeout, which works on most devices and
+        // is one line, but is overridden by some MIUI/EMUI ROMs.
+        // (2) Android FLAG_KEEP_SCREEN_ON on the activity window, harder for
+        // OEM ROMs to override and the standard pattern for navigation/maps
+        // apps. Wrapped in #if UNITY_ANDROID so editor/iOS skip it.
+        Screen.sleepTimeout = SleepTimeout.NeverSleep;
+        AcquireAndroidWakelock();
+    }
+
+    /// <summary>
+    /// Set FLAG_KEEP_SCREEN_ON on the Unity activity's window. This is the
+    /// standard navigation/maps-app pattern and survives ROM-level overrides
+    /// of Unity's Screen.sleepTimeout. No-op on non-Android platforms.
+    /// </summary>
+    private static void AcquireAndroidWakelock()
+    {
+#if UNITY_ANDROID && !UNITY_EDITOR
+        try
+        {
+            using (var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
+            using (var activity = player.GetStatic<AndroidJavaObject>("currentActivity"))
+            {
+                // addFlags must run on the UI thread. Capture activity into a
+                // local for the closure - AndroidJavaObject can be reused.
+                var act = activity;
+                act.Call("runOnUiThread", new AndroidJavaRunnable(() =>
+                {
+                    try
+                    {
+                        using (var window = act.Call<AndroidJavaObject>("getWindow"))
+                        {
+                            // WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                            const int FLAG_KEEP_SCREEN_ON = 0x00000080;
+                            window.Call("addFlags", FLAG_KEEP_SCREEN_ON);
+                        }
+                    }
+                    catch (System.Exception ex)
+                    {
+                        Debug.LogWarning("[Wakelock] addFlags failed: " + ex.Message);
+                    }
+                }));
+            }
+        }
+        catch (System.Exception ex)
+        {
+            Debug.LogWarning("[Wakelock] Android JNI bridge failed: " + ex.Message);
+        }
+#endif
     }
 
     void Start()
     {
-        if (string.IsNullOrEmpty(displayName))
+        // The prefab default in SampleScene.unity is "Hrac" (Czech for
+        // "player"). Treat it as equivalent to "no name set" so users who
+        // never customize their name don't all show up identically. This
+        // override only fires at startup; users who explicitly type "Hrac"
+        // into the nickname field will still send "Hrac" via the live
+        // DisplayName payload field.
+        if (string.IsNullOrEmpty(displayName) || displayName == "Hrac")
             displayName = PlayerPrefs.GetString("PlayerName", GenerateUsername());
 
         gameClient = new GameClient(GenerateUUID(), displayName);
@@ -96,12 +185,18 @@ public class GameManager : MonoBehaviour
 
         if (testMode)
         {
-            _secondClient = new GameClient(GenerateUUID(), GenerateUsername());
-            _secondNetwork = new GameManager_Network(_secondClient, null);
-            _thirdClient = new GameClient(GenerateUUID(), GenerateUsername());
-            _thirdNetwork = new GameManager_Network(_thirdClient, null);
-            _secondNetwork.OpenConnection();
-            _thirdNetwork.OpenConnection();
+            int n = Mathf.Max(0, testClientCount);
+            for (int i = 0; i < n; i++)
+            {
+                var bot = new TestBot
+                {
+                    DisplayName = "TestBot" + (i + 1),
+                };
+                bot.Client = new GameClient(GenerateUUID(), bot.DisplayName);
+                bot.Network = new GameManager_Network(bot.Client, null);
+                bot.Network.OpenConnection();
+                _testBots.Add(bot);
+            }
         }
 
         networkSubsystem.OpenConnection();
@@ -118,14 +213,103 @@ public class GameManager : MonoBehaviour
             SceneManager.LoadScene(firstMenuScene, LoadSceneMode.Single);
     }
 
+    /// <summary>
+    /// Draws a GPS status banner across the top of every screen. We use OnGUI
+    /// rather than a uGUI Canvas element because OnGUI works without any
+    /// scene wiring - we want this visible from the very first frame, on
+    /// every screen, even if the lobby canvas hasn't been bound yet. This is
+    /// a debug overlay; toggle showGPSDebugOverlay off for release builds.
+    /// </summary>
+    private void OnGUI()
+    {
+        if (!showGPSDebugOverlay) return;
+        if (inputSubsystem == null) return;
+
+        var diag = inputSubsystem.GpsDiagnostic;
+        var label = "GPS: " + diag;
+
+        // Scale font size to screen so it's legible on phones (HDPI) and
+        // editor (lower DPI) alike. Phones tend to have ~400dpi; the
+        // editor game view runs at ~100dpi.
+        int fontSize = Mathf.Max(14, Screen.width / 50);
+
+        var style = new GUIStyle(GUI.skin.label)
+        {
+            fontSize = fontSize,
+            fontStyle = FontStyle.Bold,
+            alignment = TextAnchor.MiddleLeft,
+            wordWrap = false,
+            normal = { textColor = Color.white }
+        };
+
+        // Width covers most of the screen so longer error strings don't get
+        // clipped. Height auto-fits the chosen font size.
+        float pad = fontSize * 0.5f;
+        float bannerH = fontSize * 2f;
+        var rect = new Rect(pad, pad, Screen.width - pad * 2, bannerH);
+
+        // Translucent black background for legibility against the map.
+        var prevColor = GUI.color;
+        GUI.color = new Color(0f, 0f, 0f, 0.65f);
+        GUI.Box(rect, GUIContent.none);
+        GUI.color = prevColor;
+
+        // Indent the label inside the box.
+        var textRect = new Rect(rect.x + pad, rect.y, rect.width - pad * 2, rect.height);
+        GUI.Label(textRect, label, style);
+
+        // Second row: position-source picker (tap to cycle) + active client
+        // indicator (testMode only). Both are diagnostic; the source picker
+        // is the recovery path when one backend silently fails on a phone.
+        float row2Y = rect.y + bannerH + pad * 0.5f;
+        var btnStyle = new GUIStyle(GUI.skin.button)
+        {
+            fontSize = Mathf.Max(12, fontSize - 2),
+            fontStyle = FontStyle.Bold,
+            alignment = TextAnchor.MiddleCenter,
+        };
+
+        // Source button: shows current source name + invites tap.
+        string sourceLabel = "Source: " + inputSubsystem.CurrentSourceName + "  [tap to cycle]";
+        // Width sized to the text so the touch area matches the label.
+        Vector2 sourceSize = btnStyle.CalcSize(new GUIContent(sourceLabel));
+        float sourceW = Mathf.Min(Screen.width - pad * 2, sourceSize.x + pad * 2);
+        var sourceRect = new Rect(pad, row2Y, sourceW, bannerH);
+        if (GUI.Button(sourceRect, sourceLabel, btnStyle))
+        {
+            inputSubsystem.CycleNextPositionSource();
+        }
+
+        // Active-client indicator (only when we have test bots).
+        if (testMode && _testBots.Count > 0)
+        {
+            string slot = _activeClientSlot == 0 ? "Host" : ("Bot " + _activeClientSlot);
+            string indicator = $"WASD: {slot} (0..{_testBots.Count} to switch)";
+            var indStyle = new GUIStyle(GUI.skin.label)
+            {
+                fontSize = Mathf.Max(12, fontSize - 2),
+                fontStyle = FontStyle.Bold,
+                alignment = TextAnchor.MiddleLeft,
+                normal = { textColor = new Color(0.9f, 1f, 0.4f) },
+            };
+            Vector2 indSize = indStyle.CalcSize(new GUIContent(indicator));
+            var indRect = new Rect(sourceRect.xMax + pad, row2Y, indSize.x + pad * 2, bannerH);
+            GUI.color = new Color(0f, 0f, 0f, 0.65f);
+            GUI.Box(indRect, GUIContent.none);
+            GUI.color = prevColor;
+            GUI.Label(new Rect(indRect.x + pad, indRect.y, indRect.width, indRect.height), indicator, indStyle);
+        }
+    }
+
     private void Update()
     {
         // Tick the SDK dispatcher so callbacks fire on main thread
         gameClient?.Update();
         if (testMode)
         {
-            _secondClient?.Update();
-            _thirdClient?.Update();
+            for (int i = 0; i < _testBots.Count; i++)
+                _testBots[i].Client?.Update();
+            HandleTestBotInput();
         }
 
         if (gameClient?.CurrentLobbyState != null)
@@ -137,6 +321,102 @@ public class GameManager : MonoBehaviour
             UpdateKillCooldown();
 
         inputSubsystem?.positionCheck();
+
+        if (testMode) StepActiveTestBot();
+    }
+
+    /// <summary>
+    /// Number-key handling for slot switching. 0 = host, 1..N = test bot N.
+    /// Suppress host WASD when a non-host bot is active so the host capsule
+    /// doesn't drift while the user is moving a bot. Only fires when
+    /// testMode is on; release builds never see this path.
+    /// </summary>
+    private void HandleTestBotInput()
+    {
+        // 0 = host. 1..9 = bots (capped by Unity KeyCode.Alpha9).
+        if (Input.GetKeyDown(KeyCode.Alpha0)) _activeClientSlot = 0;
+        for (int i = 1; i <= 9 && i <= _testBots.Count; i++)
+        {
+            if (Input.GetKeyDown(KeyCode.Alpha0 + i)) _activeClientSlot = i;
+        }
+
+        // Tell the host's input subsystem to ignore WASD when a bot is active.
+        if (inputSubsystem != null)
+            inputSubsystem.SuppressWasd = (_activeClientSlot != 0);
+    }
+
+    /// <summary>
+    /// If the active slot is a bot, step its sim position from WASD axes
+    /// and send to the server. Idle bots get a periodic keep-alive so their
+    /// avatars don't time out.
+    /// </summary>
+    private void StepActiveTestBot()
+    {
+        if (_testBots.Count == 0) return;
+        var state = gameClient?.CurrentLobbyState;
+        if (state == null || state.MapData == null) return;
+
+        // Lazy-init each bot's sim position to the lobby's map center on
+        // first lobby state. Until the bot has joined a lobby it can't
+        // send position updates.
+        for (int i = 0; i < _testBots.Count; i++)
+        {
+            var bot = _testBots[i];
+            if (!bot.Joined) continue;
+            if (bot.SimPosition.Lat == 0 && bot.SimPosition.Lon == 0)
+            {
+                // Spawn each bot in a small ring around the map center so
+                // they don't all stack on top of each other on frame one.
+                double offsetLat = 0.00003 * Mathf.Cos(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
+                double offsetLon = 0.00003 * Mathf.Sin(i * Mathf.PI * 2f / Mathf.Max(1, _testBots.Count));
+                bot.SimPosition = new GeoSus.Client.Position(
+                    state.MapData.Center.Lat + offsetLat,
+                    state.MapData.Center.Lon + offsetLon);
+                bot.Client.UpdatePosition(bot.SimPosition);
+                bot.LastSendTime = Time.time;
+            }
+        }
+
+        // WASD only drives the active bot.
+        if (_activeClientSlot >= 1 && _activeClientSlot <= _testBots.Count)
+        {
+            var bot = _testBots[_activeClientSlot - 1];
+            if (bot.Joined)
+            {
+                float dx = Input.GetAxis("Horizontal");
+                float dy = Input.GetAxis("Vertical");
+                const double speed = 0.00001;
+                bool moved = Mathf.Abs(dx) > 0.001f || Mathf.Abs(dy) > 0.001f;
+                if (moved)
+                {
+                    bot.SimPosition = new GeoSus.Client.Position(
+                        bot.SimPosition.Lat + dy * speed,
+                        bot.SimPosition.Lon + dx * speed);
+                }
+                // Send on movement OR on keep-alive cadence so the server
+                // doesn't drop our presence.
+                bool dueKeepAlive = (Time.time - bot.LastSendTime) >= 1.0f;
+                if (moved || dueKeepAlive)
+                {
+                    bot.Client.UpdatePosition(bot.SimPosition);
+                    bot.LastSendTime = Time.time;
+                }
+            }
+        }
+        else
+        {
+            // No bot is active. All bots get keep-alive only.
+            for (int i = 0; i < _testBots.Count; i++)
+            {
+                var bot = _testBots[i];
+                if (!bot.Joined) continue;
+                if ((Time.time - bot.LastSendTime) >= 1.0f)
+                {
+                    bot.Client.UpdatePosition(bot.SimPosition);
+                    bot.LastSendTime = Time.time;
+                }
+            }
+        }
     }
 
 
@@ -326,9 +606,38 @@ public class GameManager : MonoBehaviour
         return "Player" + UnityEngine.Random.Range(1000, 9999).ToString();
     }
 
+    /// <summary>
+    /// Pull the nickname input field's current text into displayName +
+    /// gameClient.DisplayName + PlayerPrefs before sending a network
+    /// action. Defensive against any TMP_InputField / soft-keyboard race
+    /// where the user types and immediately taps a button: onValueChanged
+    /// normally fires before the click handler in the same frame, but
+    /// some Android keyboards batch text events oddly. Call this at the
+    /// top of any Create/Join/Rename flow. No-op if the input field
+    /// doesn't exist in the current scene.
+    /// </summary>
+    private void CommitNicknameFromInput()
+    {
+        var nameGO = GameObject.Find("name");
+        if (nameGO == null) return;
+        var field = nameGO.GetComponent<TMPro.TMP_InputField>();
+        if (field == null) return;
+        // Force the InputField to flush any pending soft-keyboard text.
+        // ForceLabelUpdate() is harmless if there's nothing pending.
+        field.ForceLabelUpdate();
+        string typed = (field.text ?? "").Trim();
+        if (string.IsNullOrEmpty(typed)) return;
+        if (typed == displayName) return; // already in sync, skip the writes
+        displayName = typed;
+        if (gameClient != null) gameClient.DisplayName = typed;
+        PlayerPrefs.SetString("PlayerName", typed);
+        PlayerPrefs.Save();
+    }
+
     // Called by HostLobbyUI
     public void CreateLobbyButton()
     {
+        CommitNicknameFromInput();
         // Refuse to create a lobby without a real GPS fix. The previous
         // behavior of silently using a hardcoded Czechia fallback meant the
         // game always started at the same place no matter where the host was,
@@ -340,9 +649,12 @@ public class GameManager : MonoBehaviour
             // testMode bypasses the GPS gate entirely so debug runs still work.
             if (!testMode)
             {
-                Debug.LogWarning("[GameManager] CreateLobby blocked: no GPS fix yet. " +
-                                 "Make sure location permission is granted and you have signal.");
-                uiSubsystem?.ShowToast("Waiting for GPS fix... grant location permission and try again.");
+                // Surface the actual GPS state in both logs and the toast
+                // instead of the generic "Waiting for GPS fix..." that hides
+                // permission/timeout/device-disabled distinctions.
+                string diag = inputSubsystem?.GpsDiagnostic ?? "no input subsystem";
+                Debug.LogWarning("[GameManager] CreateLobby blocked. " + diag);
+                uiSubsystem?.ShowToast("Cannot create lobby. " + diag);
                 inputSubsystem?.EnsureGPSStarted();
                 return;
             }
@@ -358,6 +670,7 @@ public class GameManager : MonoBehaviour
     // Called by JoinLobbyUI with the code from the input field
     public void JoinLobbyButton(string code)
     {
+        CommitNicknameFromInput();
         if (!string.IsNullOrEmpty(code))
             networkSubsystem.JoinLobby(code);
         else
@@ -377,12 +690,14 @@ public class GameManager : MonoBehaviour
     void OnApplicationQuit()
     {
         gameClient?.Disconnect();
-        _secondClient?.Disconnect();
-        _thirdClient?.Disconnect(); 
+        for (int i = 0; i < _testBots.Count; i++)
+            _testBots[i].Client?.Disconnect();
     }
 
-    IEnumerator ConnectTestClients() 
-    { 
+    IEnumerator ConnectTestClients()
+    {
+        if (_testBots.Count == 0) yield break;
+
         // Wait until host lobby code exists
         float wait = 0f;
         while ((gameClient?.CurrentLobbyState == null || string.IsNullOrEmpty(gameClient.CurrentLobbyState.JoinCode)) && wait < 20f)
@@ -394,26 +709,46 @@ public class GameManager : MonoBehaviour
         var joinCode = gameClient?.CurrentLobbyState?.JoinCode;
         if (string.IsNullOrEmpty(joinCode))
         {
-            Debug.LogWarning("[TestMode] Could not join test clients: join code not available.");
+            Debug.LogWarning("[TestMode] Could not join test bots: join code not available.");
             yield break;
         }
 
-        // Wait until helper clients are connected and handshake-complete
+        // Wait until every bot's client has finished its TCP handshake.
+        // IsReady flips once ClientHello + ClientHelloAck round-trip.
         wait = 0f;
-        while (((_secondClient == null || !_secondClient.IsReady) || (_thirdClient == null || !_thirdClient.IsReady)) && wait < 20f)
+        bool allReady;
+        do
         {
-            wait += 0.25f;
-            yield return new WaitForSeconds(0.25f);
+            allReady = true;
+            for (int i = 0; i < _testBots.Count; i++)
+            {
+                if (_testBots[i].Client == null || !_testBots[i].Client.IsReady)
+                {
+                    allReady = false;
+                    break;
+                }
+            }
+            if (!allReady)
+            {
+                wait += 0.25f;
+                yield return new WaitForSeconds(0.25f);
+            }
+        } while (!allReady && wait < 20f);
+
+        if (!allReady)
+        {
+            Debug.LogWarning("[TestMode] Some test bots not ready, joining the ready ones only.");
         }
 
-        if (_secondClient == null || _thirdClient == null || !_secondClient.IsReady || !_thirdClient.IsReady)
+        for (int i = 0; i < _testBots.Count; i++)
         {
-            Debug.LogWarning("[TestMode] Helper clients are not ready, skipping auto-join.");
-            yield break;
+            var bot = _testBots[i];
+            if (bot.Client != null && bot.Client.IsReady)
+            {
+                bot.Network?.JoinLobby(joinCode);
+                bot.Joined = true;
+            }
         }
-
-        _secondNetwork?.JoinLobby(joinCode);
-        _thirdNetwork?.JoinLobby(joinCode);
-        Debug.Log($"[TestMode] Helper clients joined lobby with code {joinCode}.");
+        Debug.Log($"[TestMode] {_testBots.Count} bot(s) joined lobby with code {joinCode}.");
     }
 }
diff --git a/Assets/GameManager/GameManager_Input.cs b/Assets/GameManager/GameManager_Input.cs
index c845931..533198d 100644
--- a/Assets/GameManager/GameManager_Input.cs
+++ b/Assets/GameManager/GameManager_Input.cs
@@ -1,4 +1,4 @@
-ï»¿using UnityEngine;
+using UnityEngine;
 using GeoSus.Client;
 using System;
 using System.Collections;
@@ -16,6 +16,292 @@ namespace Subsystems
         Running,
         Failed
     }
+
+    /// <summary>
+    /// Position source backend. Selectable at runtime via the GPS overlay
+    /// "Source" button so the user can recover when one path misbehaves on
+    /// their phone:
+    ///   Auto         - JNI: subscribe to gps + network, pick most recent fix.
+    ///   GpsOnly      - JNI: subscribe to gps only (network's frequent indoor
+    ///                  fixes don't drown out the slower-but-precise gps fix).
+    ///   NetworkOnly  - JNI: subscribe to network only (cell tower / WiFi).
+    ///                  Useful indoors when no satellite lock is possible.
+    ///   UnityInput   - Unity's Input.location wrapper. Verified to hang on
+    ///                  Mi 9T / A20e (which is why JNI exists), but works on
+    ///                  newer Android where the JNI streaming-callbacks path
+    ///                  silently doesn't fire (MIUI/HyperOS battery saver,
+    ///                  approximate-vs-precise permission split, minDistance
+    ///                  gating on stationary phones).
+    ///   EditorWasd   - WASD-driven simulated position. Available regardless
+    ///                  of testMode flag so desktop builds and editor sessions
+    ///                  can navigate the map without real GPS.
+    /// </summary>
+    public enum PositionSource
+    {
+        Auto,
+        GpsOnly,
+        NetworkOnly,
+        UnityInput,
+        EditorWasd,
+    }
+
+#if UNITY_ANDROID && !UNITY_EDITOR
+    /// <summary>
+    /// Bridges android.location.LocationListener to managed code. The method
+    /// names here must match Java's LocationListener interface exactly so
+    /// AndroidJavaProxy's reflection dispatcher can find them.
+    /// </summary>
+    internal class AndroidLocationProxy : AndroidJavaProxy
+    {
+        public AndroidLocationProvider Owner { get; set; }
+        public AndroidLocationProxy() : base("android.location.LocationListener") { }
+
+        // Called by Android each time a new fix arrives from the registered provider.
+        public void onLocationChanged(AndroidJavaObject location)
+        {
+            try
+            {
+                if (location == null) return;
+                double lat = location.Call<double>("getLatitude");
+                double lon = location.Call<double>("getLongitude");
+                long t = location.Call<long>("getTime");
+                string provider = "";
+                try { provider = location.Call<string>("getProvider"); } catch { }
+                // Streaming callbacks are LIVE (never cached). The cached path
+                // calls UpdateLocation directly with isCached=true.
+                Owner?.UpdateLocation(lat, lon, t, provider, isCached: false);
+            }
+            catch (Exception ex)
+            {
+                Debug.LogWarning("[GPS-JNI] onLocationChanged failed: " + ex.Message);
+            }
+        }
+
+        // Required by the LocationListener interface even if we don't use them.
+        // Missing methods cause java.lang.AbstractMethodError at runtime.
+        public void onStatusChanged(string provider, int status, AndroidJavaObject extras) { }
+        public void onProviderEnabled(string provider) { }
+        public void onProviderDisabled(string provider) { }
+    }
+
+    /// <summary>
+    /// Direct wrapper around android.location.LocationManager via JNI, used as
+    /// a replacement for Unity's Input.location on Android when the user picks
+    /// Auto/GpsOnly/NetworkOnly. Subscribed providers are configurable so the
+    /// position-source picker can rewire live without restart.
+    /// </summary>
+    internal class AndroidLocationProvider
+    {
+        private AndroidJavaObject _activity;
+        private AndroidJavaObject _locationManager;
+        private AndroidLocationProxy _gpsListener;
+        private AndroidLocationProxy _networkListener;
+        private double _lat, _lon;
+        private long _lastTimeMillis;
+        private long _lastLiveTimeMillis;   // Time of most recent NON-cached fix.
+        private bool _hasFix;
+        private bool _hasLiveFix;           // True once any streaming callback fired.
+        private string _activeProvider = "";
+
+        // Captured at Initialize() so the diagnostic can report
+        // "GPS provider DISABLED, only network enabled" etc.
+        private bool _gpsProviderEnabled;
+        private bool _networkProviderEnabled;
+        private bool _gpsLastKnownExists;
+        private bool _networkLastKnownExists;
+        private string _enabledProvidersList = "";
+
+        // Subscription scope - set in Initialize, used in Shutdown to know
+        // which listeners we registered.
+        private bool _subscribedGps;
+        private bool _subscribedNetwork;
+
+        public bool HasFix => _hasFix;
+        public bool HasLiveFix => _hasLiveFix;
+        public long LastLiveTimeMillis => _lastLiveTimeMillis;
+        public long LastTimeMillis => _lastTimeMillis;
+        public double Lat => _lat;
+        public double Lon => _lon;
+        public string ActiveProvider => _activeProvider;
+        public bool GpsProviderEnabled => _gpsProviderEnabled;
+        public bool NetworkProviderEnabled => _networkProviderEnabled;
+        public bool GpsLastKnownExists => _gpsLastKnownExists;
+        public bool NetworkLastKnownExists => _networkLastKnownExists;
+        public string EnabledProvidersList => _enabledProvidersList;
+        public bool SubscribedGps => _subscribedGps;
+        public bool SubscribedNetwork => _subscribedNetwork;
+
+        public bool Initialize(out string error, bool useGps, bool useNetwork)
+        {
+            error = "";
+            try
+            {
+                using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
+                {
+                    _activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
+                }
+                if (_activity == null) { error = "no current activity"; return false; }
+
+                _locationManager = _activity.Call<AndroidJavaObject>("getSystemService", "location");
+                if (_locationManager == null) { error = "getSystemService(\"location\") returned null"; return false; }
+
+                // Capture provider enable state up front so the diagnostic
+                // can distinguish "provider disabled at OS level" from
+                // "provider enabled but produced no fix yet".
+                _gpsProviderEnabled = SafeIsProviderEnabled("gps");
+                _networkProviderEnabled = SafeIsProviderEnabled("network");
+                _enabledProvidersList = SafeGetEnabledProviders();
+
+                Debug.Log($"[GPS-JNI] init useGps={useGps} useNetwork={useNetwork} gps enabled={_gpsProviderEnabled} network enabled={_networkProviderEnabled} all enabled=[{_enabledProvidersList}]");
+
+                // Try cached last-known fixes from the providers we're about
+                // to subscribe to. If the OS already knows where we are
+                // (e.g. from another app that recently used GPS), we get a
+                // fix at zero cost and zero wait time. Tagged isCached so
+                // the diagnostic can mark them and we know we still need
+                // to wait for a streaming callback.
+                if (useNetwork) TryLastKnown("network", out _networkLastKnownExists);
+                if (useGps)     TryLastKnown("gps",     out _gpsLastKnownExists);
+
+                _subscribedGps = useGps;
+                _subscribedNetwork = useNetwork;
+
+                if (useGps)     _gpsListener     = new AndroidLocationProxy { Owner = this };
+                if (useNetwork) _networkListener = new AndroidLocationProxy { Owner = this };
+
+                // requestLocationUpdates must be called on a thread with a
+                // Looper. Use the Activity's UI thread, which always has one.
+                // minTime=1000ms, minDistance=0f - we want updates on every
+                // fix the OS produces. Previously this was 1f which gated
+                // out updates from a stationary phone (MIUI/newer Android
+                // are stricter about this and that's the suspected cause of
+                // "via gps (cached)" sticking forever).
+                _activity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
+                {
+                    if (useGps)
+                    {
+                        try { _locationManager.Call("requestLocationUpdates", "gps", 1000L, 0f, _gpsListener); }
+                        catch (Exception ex) { Debug.LogWarning("[GPS-JNI] gps subscribe failed: " + ex.Message); }
+                    }
+                    if (useNetwork)
+                    {
+                        try { _locationManager.Call("requestLocationUpdates", "network", 1000L, 0f, _networkListener); }
+                        catch (Exception ex) { Debug.LogWarning("[GPS-JNI] network subscribe failed: " + ex.Message); }
+                    }
+                }));
+
+                return true;
+            }
+            catch (Exception ex)
+            {
+                error = "JNI init exception: " + ex.Message;
+                return false;
+            }
+        }
+
+        void TryLastKnown(string provider, out bool nonNullReturned)
+        {
+            nonNullReturned = false;
+            try
+            {
+                var loc = _locationManager.Call<AndroidJavaObject>("getLastKnownLocation", provider);
+                if (loc != null)
+                {
+                    nonNullReturned = true;
+                    double lat = loc.Call<double>("getLatitude");
+                    double lon = loc.Call<double>("getLongitude");
+                    long t = loc.Call<long>("getTime");
+                    UpdateLocation(lat, lon, t, provider, isCached: true);
+                }
+            }
+            catch (Exception ex)
+            {
+                Debug.LogWarning($"[GPS-JNI] getLastKnownLocation({provider}) failed: " + ex.Message);
+            }
+        }
+
+        bool SafeIsProviderEnabled(string provider)
+        {
+            try
+            {
+                return _locationManager.Call<bool>("isProviderEnabled", provider);
+            }
+            catch (Exception ex)
+            {
+                Debug.LogWarning($"[GPS-JNI] isProviderEnabled({provider}) failed: " + ex.Message);
+                return false;
+            }
+        }
+
+        // Build a comma-separated list of currently-enabled providers via
+        // LocationManager.getProviders(true). We iterate the returned
+        // java.util.List by index because AndroidJavaObject does not
+        // implement IEnumerable.
+        string SafeGetEnabledProviders()
+        {
+            try
+            {
+                var list = _locationManager.Call<AndroidJavaObject>("getProviders", true);
+                if (list == null) return "";
+                int size = list.Call<int>("size");
+                var parts = new System.Text.StringBuilder();
+                for (int i = 0; i < size; i++)
+                {
+                    var name = list.Call<string>("get", i);
+                    if (i > 0) parts.Append(",");
+                    parts.Append(name);
+                }
+                return parts.ToString();
+            }
+            catch (Exception ex)
+            {
+                Debug.LogWarning("[GPS-JNI] getProviders failed: " + ex.Message);
+                return "";
+            }
+        }
+
+        public void UpdateLocation(double lat, double lon, long timeMillis, string provider, bool isCached)
+        {
+            // Ignore older fixes if a newer one is already in hand. This lets
+            // both gps + network listeners feed us without ping-ponging
+            // between stale and fresh data.
+            if (timeMillis < _lastTimeMillis) return;
+            _lat = lat;
+            _lon = lon;
+            _lastTimeMillis = timeMillis;
+            // Active-provider name carries cached/live state in the diagnostic
+            // banner so the user can see at a glance whether streaming has
+            // kicked in or we're still on the initial cached snapshot.
+            _activeProvider = (provider ?? "") + (isCached ? " (cached)" : "");
+            _hasFix = true;
+            if (!isCached)
+            {
+                _hasLiveFix = true;
+                _lastLiveTimeMillis = timeMillis;
+            }
+        }
+
+        public void Shutdown()
+        {
+            try
+            {
+                if (_locationManager != null)
+                {
+                    if (_gpsListener != null) _locationManager.Call("removeUpdates", _gpsListener);
+                    if (_networkListener != null) _locationManager.Call("removeUpdates", _networkListener);
+                }
+            }
+            catch (Exception ex)
+            {
+                Debug.LogWarning("[GPS-JNI] Shutdown failed: " + ex.Message);
+            }
+            _gpsListener = null;
+            _networkListener = null;
+            _locationManager = null;
+            _activity = null;
+        }
+    }
+#endif
     public static class PositonExtensions
     {
         public static Position ToLocal(this Position position, Position center)
@@ -26,7 +312,7 @@ namespace Subsystems
             double metersPerDegreeLon = 111320.0 * Math.Cos(center.Lat * Math.PI / 180.0);
             float x = (float)(lonDiff * metersPerDegreeLon);
             float z = (float)(latDiff * metersPerDegreeLat);
-            return new Position(z, x); 
+            return new Position(z, x);
         }
         public static Vector3 ToLocalVector3(this Position position, Position center)
         {
@@ -51,6 +337,18 @@ namespace Subsystems
         private GameObject _player;
         private bool _testMode;
 
+        // PlayerPrefs key for the user's chosen position source. Persists
+        // across app restarts so a user who flipped to UnityInput because
+        // their phone hated the JNI path doesn't have to flip again every
+        // launch.
+        private const string PrefsSourceKey = "PositionSource_v1";
+        private PositionSource _currentSource = PositionSource.Auto;
+
+        // When the multi-client editor test mode picks a non-host bot as
+        // active, we need the host's WASD path to NOT also move. Set true
+        // by GameManager when active slot != 0.
+        public bool SuppressWasd = false;
+
         private GPSState _GPSState = GPSState.Uninitialized;
         private float _speed = 0.00001f;
         private Position _mapCenter;
@@ -61,9 +359,125 @@ namespace Subsystems
         private float _lastPositionSendTime;
         private const float _positionKeepAliveSeconds = 1.0f;
 
+        // Diagnostic state. We capture *why* GPS init failed so the UI can
+        // surface it to the user without requiring logcat. Older Android
+        // phones (Mi 9T, A20e) hit silent failure modes that are impossible
+        // to distinguish from "still warming up" without this.
+        private string _lastGpsError = "";
+        private float _gpsInitStartTime = -1f;
+        // Bump from the original 20s. Cold-start GPS on older Android can
+        // easily exceed 20s indoors or under cloud cover - by the time the
+        // user notices nothing is happening, we've already given up.
+        private const int _gpsInitTimeoutSeconds = 60;
+
+#if UNITY_ANDROID && !UNITY_EDITOR
+        // JNI-backed location provider, used for Auto/GpsOnly/NetworkOnly.
+        // UnityInput uses Input.location instead and leaves this null.
+        private AndroidLocationProvider _androidProvider;
+#endif
+
         /// <summary>Last known GPS position (for CreateLobby centre point)</summary>
         public Position? LastKnownPosition => _currentPosition.Lat != 0 || _currentPosition.Lon != 0 ? _currentPosition : (Position?)null;
 
+        /// <summary>Current GPS state machine value (debug/diagnostic).</summary>
+        public string GpsStateName => _GPSState.ToString();
+
+        /// <summary>Last GPS error reason captured during init (empty if none).</summary>
+        public string LastGpsError => _lastGpsError ?? "";
+
+        /// <summary>Retry count out of max (debug/diagnostic).</summary>
+        public string GpsRetryProgress => $"{_gpsRetryCount}/{_maxGpsRetries}";
+
+        /// <summary>Currently selected position source (for UI cycle button).</summary>
+        public PositionSource CurrentSource => _currentSource;
+
+        /// <summary>Display name for the current source (for UI label).</summary>
+        public string CurrentSourceName
+        {
+            get
+            {
+                switch (_currentSource)
+                {
+                    case PositionSource.Auto:        return "Auto (GPS+Net)";
+                    case PositionSource.GpsOnly:     return "GPS only";
+                    case PositionSource.NetworkOnly: return "Network only";
+                    case PositionSource.UnityInput:  return "Unity Input";
+                    case PositionSource.EditorWasd:  return "WASD";
+                    default: return _currentSource.ToString();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Human-readable one-line GPS status for on-screen overlay. Designed
+        /// to be visible without ADB so users can self-diagnose permission
+        /// vs. timeout vs. device-disabled vs. running-but-no-fix-yet.
+        /// </summary>
+        public string GpsDiagnostic
+        {
+            get
+            {
+                if (_currentSource == PositionSource.EditorWasd)
+                {
+                    if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
+                        return "WASD: waiting for map center";
+                    return $"WASD lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}";
+                }
+
+                switch (_GPSState)
+                {
+                    case GPSState.Uninitialized:
+                        return "Uninitialized (will start on first lobby action)";
+                    case GPSState.Initializing:
+                    {
+                        float elapsed = _gpsInitStartTime >= 0 ? Time.time - _gpsInitStartTime : 0;
+                        string providers = "";
+#if UNITY_ANDROID && !UNITY_EDITOR
+                        if (_androidProvider != null && !string.IsNullOrEmpty(_androidProvider.EnabledProvidersList))
+                            providers = $" providers=[{_androidProvider.EnabledProvidersList}]";
+#endif
+                        return $"Initializing ({elapsed:F1}s / max {_gpsInitTimeoutSeconds}s){providers}";
+                    }
+                    case GPSState.Running:
+                    {
+                        string suffix = "";
+#if UNITY_ANDROID && !UNITY_EDITOR
+                        if (_androidProvider != null)
+                        {
+                            string p = _androidProvider.ActiveProvider;
+                            if (!string.IsNullOrEmpty(p)) suffix = " via " + p;
+                            // Show how stale the most recent fix is (ms-level
+                            // resolution) so "stuck on cached" is obvious at
+                            // a glance: "via gps (cached) [no live, 47s old]".
+                            if (!_androidProvider.HasLiveFix)
+                            {
+                                long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
+                                long ageMs = now - _androidProvider.LastTimeMillis;
+                                if (_androidProvider.LastTimeMillis > 0 && ageMs > 0)
+                                    suffix += $" [no live, {ageMs / 1000}s old]";
+                                else
+                                    suffix += " [no live]";
+                            }
+                            else
+                            {
+                                long now = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
+                                long ageMs = now - _androidProvider.LastLiveTimeMillis;
+                                if (ageMs > 5000) suffix += $" [live {ageMs / 1000}s old]";
+                            }
+                        }
+#endif
+                        if (_currentPosition.Lat == 0 && _currentPosition.Lon == 0)
+                            return "Running but no fix yet (waiting for satellites)" + suffix;
+                        return $"Running lat={_currentPosition.Lat:F5} lon={_currentPosition.Lon:F5}" + suffix;
+                    }
+                    case GPSState.Failed:
+                        return $"Failed: {(_lastGpsError ?? "unknown")} (retries {GpsRetryProgress})";
+                    default:
+                        return "?";
+                }
+            }
+        }
+
         public GameManager_Input(GameClient gameClient, GameObject player, bool testMode)
         {
             _gameClient = gameClient;
@@ -73,12 +487,85 @@ namespace Subsystems
             var hostGO = new UnityEngine.GameObject("_CoroutineHost");
             UnityEngine.Object.DontDestroyOnLoad(hostGO);
             _coroutineHost = hostGO.AddComponent<CoroutineHost>();
+
+            // Restore the user's last picked source. Default depends on
+            // platform: editor defaults to EditorWasd (no GPS hardware in
+            // editor anyway); device defaults to Auto.
+            string saved = PlayerPrefs.GetString(PrefsSourceKey, "");
+            if (!string.IsNullOrEmpty(saved) && Enum.TryParse(saved, out PositionSource parsed))
+            {
+                _currentSource = parsed;
+            }
+            else
+            {
+#if UNITY_EDITOR
+                _currentSource = PositionSource.EditorWasd;
+#else
+                _currentSource = PositionSource.Auto;
+#endif
+            }
+
+            // Legacy testMode flag forces EditorWasd. New code paths should
+            // use SwitchPositionSource(EditorWasd) instead, but we keep the
+            // old behavior for backward compatibility with the inspector flag.
+            if (_testMode) _currentSource = PositionSource.EditorWasd;
         }
 
         /// <summary>Called from OnSceneLoaded when Client.unity loads so the
         /// Player capsule (which lives in Client.unity) can be wired at runtime.</summary>
         public void SetPlayerObject(GameObject player) { _player = player; }
 
+        /// <summary>
+        /// Switch the active position source backend live. Tears down the
+        /// current backend's listeners (JNI proxies, Input.location), resets
+        /// the state machine, and kicks off init for the new source. Persists
+        /// the choice to PlayerPrefs.
+        /// </summary>
+        public void SwitchPositionSource(PositionSource newSource)
+        {
+            if (_currentSource == newSource) return;
+            Debug.Log($"[GPS] SwitchPositionSource {_currentSource} -> {newSource}");
+
+            // Tear down whatever's running.
+            ShutdownCurrentBackend();
+
+            _currentSource = newSource;
+            PlayerPrefs.SetString(PrefsSourceKey, newSource.ToString());
+            PlayerPrefs.Save();
+
+            _GPSState = GPSState.Uninitialized;
+            _gpsRetryCount = 0;
+            _lastGpsError = "";
+            _gpsInitStartTime = -1f;
+            // Don't clear _currentPosition - the user has presumably been
+            // playing somewhere. Map markers/avatar position can stay until
+            // the next fix arrives from the new source.
+
+            EnsureGPSStarted();
+        }
+
+        /// <summary>Cycle through the available sources for tap-to-cycle UI.</summary>
+        public void CycleNextPositionSource()
+        {
+            var values = (PositionSource[])Enum.GetValues(typeof(PositionSource));
+            int idx = Array.IndexOf(values, _currentSource);
+            var next = values[(idx + 1) % values.Length];
+            SwitchPositionSource(next);
+        }
+
+        private void ShutdownCurrentBackend()
+        {
+#if UNITY_ANDROID && !UNITY_EDITOR
+            if (_androidProvider != null)
+            {
+                _androidProvider.Shutdown();
+                _androidProvider = null;
+            }
+#endif
+            // Stop Unity Input.location too, in case it was running.
+            try { Input.location.Stop(); } catch { }
+        }
+
         /// <summary>
         /// Kick off GPS initialization if it hasn't started yet. Safe to call
         /// repeatedly. Hosts must call this from the lobby setup screen so
@@ -88,10 +575,19 @@ namespace Subsystems
         /// </summary>
         public void EnsureGPSStarted()
         {
-            if (_testMode) return;
+            if (_currentSource == PositionSource.EditorWasd) return;
             if (_coroutineHost == null) return;
+            // Allow tapping "Create Lobby" again (or any caller of this
+            // method) to retry from Failed up to _maxGpsRetries times.
             if (_GPSState == GPSState.Uninitialized)
+            {
                 _coroutineHost.StartCoroutine(InitiallizeGPS());
+            }
+            else if (_GPSState == GPSState.Failed && _gpsRetryCount < _maxGpsRetries)
+            {
+                _gpsRetryCount++;
+                _coroutineHost.StartCoroutine(InitiallizeGPS());
+            }
         }
         public void positionCheck()
         {
@@ -101,7 +597,7 @@ namespace Subsystems
 
             try
             {
-                if (_testMode)
+                if (_currentSource == PositionSource.EditorWasd)
                 {
                     if (_currentPosition == new Position(0, 0))
                     {
@@ -114,7 +610,10 @@ namespace Subsystems
                         _lastSentPosition = _currentPosition;
                     }
 
-                    TestPlayerPosition();
+                    if (!SuppressWasd)
+                        TestPlayerPosition();
+                    else
+                        TrySendCurrentPosition(); // keep-alive only
                 }
                 else
                 {
@@ -194,18 +693,16 @@ namespace Subsystems
         {
             double x = Input.GetAxis("Horizontal");
             double y = Input.GetAxis("Vertical");
-            Debug.Log($"Input: {x}, {y}");
             _currentPosition = new Position( _lastSentPosition.Lat + y * _speed, _lastSentPosition.Lon + x * _speed);
-            Debug.Log($"Current Position: {_currentPosition.Lat}, {_currentPosition.Lon}");
             var localCurrent = _currentPosition.ToLocalVector3(_mapCenter);
-            Debug.Log($"Local Current Position: {localCurrent}");
             var heading = CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), localCurrent);
             if (heading != null)
             {
-                Debug.Log($"Heading: {heading}");
-                _player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
+                if (_player != null)
+                    _player.transform.rotation = Quaternion.Euler(0, (float)heading, 0);
             }
-            _player.transform.position = localCurrent;
+            if (_player != null)
+                _player.transform.position = localCurrent;
             try
             {
                 TrySendCurrentPosition();
@@ -228,9 +725,13 @@ namespace Subsystems
         IEnumerator InitiallizeGPS()
         {
             _GPSState = GPSState.Initializing;
+            _gpsInitStartTime = Time.time;
+            _lastGpsError = "";
 
 #if UNITY_ANDROID
-            // Request fine location permission if not already granted
+            // Request fine location permission if not already granted.
+            // On Android 12+ a "precise" toggle exists separately from coarse,
+            // but Unity's FineLocation request covers both for our purposes.
             if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
             {
                 UnityEngine.Android.Permission.RequestUserPermission(UnityEngine.Android.Permission.FineLocation);
@@ -241,42 +742,133 @@ namespace Subsystems
                     yield return new WaitForSeconds(0.5f);
                     waited += 0.5f;
                 }
+                if (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation))
+                {
+                    _lastGpsError = "Permission denied (fine location)";
+                    Debug.LogError("[GPS] " + _lastGpsError);
+                    _GPSState = GPSState.Failed;
+                    yield break;
+                }
             }
 #endif
 
+#if UNITY_ANDROID && !UNITY_EDITOR
+            // Choose subscription scope based on selected source. UnityInput
+            // skips JNI entirely and falls through to the Input.location path
+            // below (the same path iOS / editor use).
+            if (_currentSource == PositionSource.Auto ||
+                _currentSource == PositionSource.GpsOnly ||
+                _currentSource == PositionSource.NetworkOnly)
+            {
+                bool useGps     = (_currentSource != PositionSource.NetworkOnly);
+                bool useNetwork = (_currentSource != PositionSource.GpsOnly);
+
+                if (_androidProvider != null)
+                {
+                    _androidProvider.Shutdown();
+                    _androidProvider = null;
+                }
+                _androidProvider = new AndroidLocationProvider();
+                if (!_androidProvider.Initialize(out var initError, useGps, useNetwork))
+                {
+                    _lastGpsError = "Native LocationManager failed: " + initError;
+                    Debug.LogError("[GPS] " + _lastGpsError);
+                    _androidProvider = null;
+                    _GPSState = GPSState.Failed;
+                    yield break;
+                }
+
+                // Fast-fail if neither subscribed provider is enabled at OS
+                // level. Waiting 60s for fixes from disabled providers is
+                // pointless - tell the user immediately what's wrong.
+                bool anyUsableEnabled =
+                    (useGps && _androidProvider.GpsProviderEnabled) ||
+                    (useNetwork && _androidProvider.NetworkProviderEnabled);
+                if (!anyUsableEnabled)
+                {
+                    string which = useGps && useNetwork ? "gps + network"
+                                 : useGps ? "gps"
+                                 : "network";
+                    _lastGpsError = $"{which} provider DISABLED at OS level. Open Settings > Location and switch it ON. Or tap [Source] to try a different backend.";
+                    Debug.LogError("[GPS] " + _lastGpsError);
+                    _androidProvider.Shutdown();
+                    _androidProvider = null;
+                    _GPSState = GPSState.Failed;
+                    yield break;
+                }
+
+                // Wait for the first fix (cached or live).
+                int maxWaitJni = _gpsInitTimeoutSeconds;
+                while (!_androidProvider.HasFix && maxWaitJni > 0)
+                {
+                    yield return new WaitForSeconds(1);
+                    maxWaitJni--;
+                }
+
+                if (!_androidProvider.HasFix)
+                {
+                    string enabled = _androidProvider.EnabledProvidersList ?? "";
+                    string gpsState = _androidProvider.GpsProviderEnabled ? "ON" : "OFF";
+                    string netState = _androidProvider.NetworkProviderEnabled ? "ON" : "OFF";
+                    string lastKnown = $"lastKnown[gps={(_androidProvider.GpsLastKnownExists ? "yes" : "no")}, net={(_androidProvider.NetworkLastKnownExists ? "yes" : "no")}]";
+
+                    _lastGpsError = $"Timeout {_gpsInitTimeoutSeconds}s on {_currentSource}. enabled=[{enabled}] gps={gpsState} net={netState} {lastKnown}. Try [Source] cycle to switch backends.";
+                    Debug.LogError("[GPS] " + _lastGpsError);
+                    _androidProvider.Shutdown();
+                    _androidProvider = null;
+                    _GPSState = GPSState.Failed;
+                    yield break;
+                }
+
+                _currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
+                _GPSState = GPSState.Running;
+                _gpsRetryCount = 0;
+                _coroutineHost.StartCoroutine(AndroidGPSService());
+                yield break;
+            }
+
+            // _currentSource == UnityInput on Android: fall through to the
+            // Input.location path below. This is the recovery path for
+            // newer Android phones where JNI's streaming-callbacks don't
+            // fire (MIUI/HyperOS background restrictions, approximate-vs-
+            // precise permission, minDistance gating on stationary phones).
+#endif
+
+            // iOS / editor / non-Android / Android-with-UnityInput-source:
+            // use Unity's Input.location.
             if (!Input.location.isEnabledByUser)
             {
-                Debug.LogError("Location not enabled on device or app does not have permission to access location");
+                _lastGpsError = "Location services not enabled by user";
+                Debug.LogError("[GPS] " + _lastGpsError);
                 _GPSState = GPSState.Failed;
                 yield break;
             }
-            // Starts the location service.
 
             float desiredAccuracyInMeters = 5f;
             float updateDistanceInMeters = 1f;
 
             Input.location.Start(desiredAccuracyInMeters, updateDistanceInMeters);
 
-            // Waits until the location service initializes
-            int maxWait = 20;
+            int maxWait = _gpsInitTimeoutSeconds;
             while (Input.location.status == LocationServiceStatus.Initializing && maxWait > 0)
             {
                 yield return new WaitForSeconds(1);
                 maxWait--;
             }
 
-            // If the service didn't initialize in 20 seconds this cancels location service use.
             if (maxWait < 1)
             {
+                _lastGpsError = $"Timed out after {_gpsInitTimeoutSeconds}s waiting for first fix (try moving outdoors, or tap [Source] to try a different backend)";
+                Debug.LogError("[GPS] " + _lastGpsError);
                 _GPSState = GPSState.Failed;
-                Debug.LogError("Timed out");
                 yield break;
             }
 
             if (Input.location.status == LocationServiceStatus.Failed)
             {
+                _lastGpsError = "Unity Input.location reported Failed status";
+                Debug.LogError("[GPS] " + _lastGpsError);
                 _GPSState = GPSState.Failed;
-                Debug.LogError("Unable to determine device location");
                 yield break;
             }
 
@@ -284,14 +876,43 @@ namespace Subsystems
             _gpsRetryCount = 0;
             _coroutineHost.StartCoroutine(GPSService());
         }
+
+#if UNITY_ANDROID && !UNITY_EDITOR
+        /// <summary>
+        /// Mirrors the JNI provider's most recent fix into _currentPosition
+        /// every 0.5s so the rest of the game (which polls _currentPosition
+        /// indirectly via LastKnownPosition / TrySendCurrentPosition) keeps
+        /// working unchanged. Replaces GPSService on Android.
+        /// </summary>
+        IEnumerator AndroidGPSService()
+        {
+            while (_GPSState == GPSState.Running && _androidProvider != null)
+            {
+                if (_androidProvider.HasFix)
+                {
+                    _currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon);
+                }
+                yield return new WaitForSeconds(0.5f);
+            }
+
+            // Loop ended (state != Running or provider disposed). Clean up
+            // listeners so we don't leak across retries.
+            if (_androidProvider != null)
+            {
+                _androidProvider.Shutdown();
+                _androidProvider = null;
+            }
+        }
+#endif
         IEnumerator GPSService()
         {
             while (_GPSState == GPSState.Running)
             {
                 if (Input.location.status == LocationServiceStatus.Failed)
                 {
+                    _lastGpsError = "Location service died after init (provider stopped)";
+                    Debug.LogError("[GPS] " + _lastGpsError);
                     _GPSState = GPSState.Failed;
-                    Debug.LogError("Unable to determine device location");
                     yield break;
                 }
 
@@ -302,4 +923,4 @@ namespace Subsystems
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/Assets/GameManager/GameManager_Map.cs b/Assets/GameManager/GameManager_Map.cs
index 1099cdf..c9c1acc 100644
--- a/Assets/GameManager/GameManager_Map.cs
+++ b/Assets/GameManager/GameManager_Map.cs
@@ -702,6 +702,20 @@ namespace Subsystems{
             _bodyMarkers.Clear();
         }
 
+        // â”€â”€ Player avatar sizing â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
+        // The default Unity capsule primitive is 2m tall in local space. The
+        // map camera defaults to 150m orthographic-ish height (see
+        // MapCameraController), so anything smaller than ~3m world-size is a
+        // pixel on screen. Original code used scale=0.4 (~0.8m capsule) which
+        // was invisible. Markers (POIs/tasks/bodies) are 8m pillars; players
+        // need to be visibly distinct from those AND from each other. The
+        // local player gets a halo light + larger scale so the user can find
+        // themselves on the map at a glance.
+        private const float kLocalPlayerScale  = 4f;   // ~8m capsule (matches marker height)
+        private const float kRemotePlayerScale = 2f;   // ~4m capsule (smaller than markers)
+        private const float kLocalPlayerHaloRange = 18f;
+        private const float kLocalPlayerHaloIntensity = 2.5f;
+
         public void UpdatePlayerAvatars(Dictionary<string, PlayerPositionInfo> positions, string myUuid)
         {
             if (_mapCenterPoint == null) return;
@@ -715,20 +729,47 @@ namespace Subsystems{
             {
                 string uuid = kvp.Key;
                 var info = kvp.Value;
+                bool isLocal = uuid == myUuid;
                 if (!_playerAvatars.TryGetValue(uuid, out var go) || go == null)
                 {
                     go = GameObject.CreatePrimitive(PrimitiveType.Capsule);
                     go.name = $"Player_{uuid.Substring(0, Mathf.Min(8, uuid.Length))}";
                     go.transform.parent = _mapCenterPoint?.transform;
-                    go.transform.localScale = Vector3.one * 0.4f;
+                    // Strip the auto-collider - avatars are visual only and the
+                    // collider would interact with the map's MeshColliders.
+                    var col = go.GetComponent<Collider>();
+                    if (col != null) UnityEngine.Object.Destroy(col);
+
+                    float scale = isLocal ? kLocalPlayerScale : kRemotePlayerScale;
+                    go.transform.localScale = Vector3.one * scale;
+
+                    if (isLocal)
+                    {
+                        // Halo light around the local player so the user can
+                        // find themselves at a glance even at the widest zoom.
+                        // Range/intensity tuned so it reads as "this is me"
+                        // without bleeding far enough to drown POI markers.
+                        var halo = go.AddComponent<Light>();
+                        halo.color = new Color(0.30f, 1.00f, 0.55f); // matches green capsule color
+                        halo.intensity = kLocalPlayerHaloIntensity;
+                        halo.range = kLocalPlayerHaloRange;
+                    }
+
                     _playerAvatars[uuid] = go;
                 }
-                go.transform.position = info.Position.ToLocalVector3(_centerPosition) + Vector3.up * 1f;
+
+                // Lift the avatar so the bottom of the capsule sits roughly at
+                // ground level despite the larger scale. Capsule's local pivot
+                // is at center, height = 2 * localScale.y world units, so we
+                // raise by half the local height.
+                float halfHeight = (isLocal ? kLocalPlayerScale : kRemotePlayerScale);
+                go.transform.position = info.Position.ToLocalVector3(_centerPosition)
+                                      + Vector3.up * halfHeight;
 
                 var mr = go.GetComponent<MeshRenderer>();
                 if (mr)
                 {
-                    if (uuid == myUuid) mr.material.color = Color.green;
+                    if (isLocal) mr.material.color = new Color(0.30f, 1.00f, 0.55f);
                     else if (info.State == GeoSus.Client.PlayerState.Dead) mr.material.color = Color.grey;
                     else mr.material.color = Color.white;
                 }
diff --git a/Assets/GameManager/GameManager_Network.cs b/Assets/GameManager/GameManager_Network.cs
index ab85b1a..53bedac 100644
--- a/Assets/GameManager/GameManager_Network.cs
+++ b/Assets/GameManager/GameManager_Network.cs
@@ -487,6 +487,15 @@ namespace Subsystems
         {
             State.Phase = GamePhase.Lobby;
             _manager?.uiSubsystem?.HideMeetingPanel();
+            // Bodies survive the scene reload because the marker GameObjects are
+            // parented under MapCenterPoint (which lives in the persistent
+            // Client.unity scene). Without this clear, returning to lobby and
+            // starting a new round leaves stale corpses on the map of the new
+            // round. Server already cleared its `_bodies` set in
+            // ProcessReturnToLobby; this is the client-side mirror that was
+            // missing in HandleVotingClosed's symmetry.
+            _manager?.mapSubsystem?.ClearBodyMarkers();
+            _manager?.mapSubsystem?.ClearSabotageMarkers();
             // Unified lobby: regardless of role, return to create.unity.
             SceneManager.LoadScene("create", LoadSceneMode.Single);
         }
diff --git a/Assets/GameManager/GameManager_Tasks.cs b/Assets/GameManager/GameManager_Tasks.cs
index 0b2845a..d646f6e 100644
--- a/Assets/GameManager/GameManager_Tasks.cs
+++ b/Assets/GameManager/GameManager_Tasks.cs
@@ -25,6 +25,8 @@ namespace Subsystems
         private List<TaskEntry> _tasks = new List<TaskEntry>();
         private bool _minigameOpen;
         private string _loadedMinigameScene;
+        private Camera _hostCameraSuspended;
+        private GameObject _hostInGameHudHidden;
 
         // Proximity state (checked every frame in UpdateProximity)
         public GeoSus.Client.GameTask NearbyTask { get; private set; }
@@ -190,12 +192,31 @@ namespace Subsystems
             // Inform server that task started
             _gameClient.Send(new TaskStart { TaskId = entry.ServerTask.TaskId });
 
+            // Disable the host scene's main camera while the minigame is up.
+            // With both cameras enabled the minigame's UI/3D content would
+            // fight the host's map camera for screen space, and what gets
+            // drawn depends on Camera.depth which isn't guaranteed across
+            // scenes. Restored in FinishMinigame.
+            _hostCameraSuspended = Camera.main;
+            if (_hostCameraSuspended != null) _hostCameraSuspended.enabled = false;
+
+            // Hide the persistent InGame HUD canvas (if present). It lives
+            // in Client.unity and renders Screen Space - Overlay so it would
+            // otherwise stack on top of the minigame's UI regardless of
+            // which scene is active. SetActive(false) is reversible.
+            _hostInGameHudHidden = GameObject.Find("InGame");
+            if (_hostInGameHudHidden != null && _hostInGameHudHidden.activeSelf)
+                _hostInGameHudHidden.SetActive(false);
+            else
+                _hostInGameHudHidden = null; // nothing to restore
+
             var op = SceneManager.LoadSceneAsync(entry.MinigameScene, LoadSceneMode.Additive);
             if (op == null)
             {
                 Debug.LogError($"[Tasks] LoadSceneAsync returned null for '{entry.MinigameScene}'.");
                 GameManager.Instance?.uiSubsystem?.ShowToast(
                     $"Task scene failed to load: {entry.MinigameScene}");
+                if (_hostCameraSuspended != null) { _hostCameraSuspended.enabled = true; _hostCameraSuspended = null; }
                 _minigameOpen = false;
                 yield break;
             }
@@ -203,8 +224,34 @@ namespace Subsystems
 
             _loadedMinigameScene = entry.MinigameScene;
 
-            // Find the ITask component in the newly loaded scene
+            // CRITICAL: switch the active scene to the loaded minigame.
+            // LoadSceneMode.Additive stacks scenes without changing which one
+            // is "active" - and an inactive scene's RenderSettings, ambient
+            // light, and skybox don't drive rendering. The host (Client.unity)
+            // remains active and its lighting context still applies, which
+            // is the root cause of "task opens to white screen": the
+            // minigame's content loads but its visuals don't take over.
+            // Without SetActiveScene, even minigames that ARE wired up
+            // correctly render against the host's lighting and look broken.
             Scene scene = SceneManager.GetSceneByName(entry.MinigameScene);
+            if (scene.IsValid()) SceneManager.SetActiveScene(scene);
+
+            // Diagnostic: count cameras / canvases / lights in the loaded
+            // scene. If the white screen persists after this fix, the
+            // numbers tell us whether the scene is missing rendering bits
+            // (camera=0, canvas=0) or if the issue is elsewhere.
+            int camCount = 0, canvasCount = 0, lightCount = 0;
+            foreach (var root in scene.GetRootGameObjects())
+            {
+                camCount    += root.GetComponentsInChildren<Camera>(true).Length;
+                canvasCount += root.GetComponentsInChildren<Canvas>(true).Length;
+                lightCount  += root.GetComponentsInChildren<Light>(true).Length;
+            }
+            Debug.Log($"[Tasks] Loaded '{entry.MinigameScene}': cameras={camCount}, " +
+                      $"canvases={canvasCount}, lights={lightCount}, " +
+                      $"activeScene={SceneManager.GetActiveScene().name}");
+
+            // Find the ITask component in the newly loaded scene
             ITask taskComponent = null;
             foreach (var root in scene.GetRootGameObjects())
             {
@@ -214,7 +261,9 @@ namespace Subsystems
 
             if (taskComponent == null)
             {
-                Debug.LogWarning($"[Tasks] No ITask found in '{entry.MinigameScene}'. Auto-completing.");
+                Debug.LogWarning($"[Tasks] No ITask found in '{entry.MinigameScene}'. " +
+                                 $"Either the minigame's controller script isn't attached to a GameObject in the scene, " +
+                                 $"or the script doesn't implement ITask. Auto-completing.");
                 yield return FinishMinigame(entry, true);
                 yield break;
             }
@@ -246,14 +295,33 @@ namespace Subsystems
                 Debug.Log($"[Tasks] Task '{entry.ServerTask.Name}' exited without completion.");
             }
 
-            // Unload minigame scene
+            // Unload minigame scene. Switch the active scene back to the
+            // host BEFORE the unload so we don't end up with no active
+            // scene mid-frame (Unity will complain and lighting flickers).
             if (!string.IsNullOrEmpty(_loadedMinigameScene))
             {
+                var hostScene = SceneManager.GetSceneByName("Client");
+                if (hostScene.IsValid()) SceneManager.SetActiveScene(hostScene);
+
                 var unload = SceneManager.UnloadSceneAsync(_loadedMinigameScene);
                 yield return unload;
                 _loadedMinigameScene = null;
             }
 
+            // Re-enable the host camera that was suspended during the minigame.
+            if (_hostCameraSuspended != null)
+            {
+                _hostCameraSuspended.enabled = true;
+                _hostCameraSuspended = null;
+            }
+
+            // Re-show the InGame HUD canvas hidden at minigame entry.
+            if (_hostInGameHudHidden != null)
+            {
+                _hostInGameHudHidden.SetActive(true);
+                _hostInGameHudHidden = null;
+            }
+
             _minigameOpen = false;
         }
     }
