From c8d8b6b802c17143c44175f3e966c105fcfd4523 Mon Sep 17 00:00:00 2001 From: trubkokrtek Date: Sun, 17 May 2026 11:25:21 +0200 Subject: [PATCH] Conflict resolve --- Assets/GameManager/GameManager_Input.cs | 914 ++++++++++++++++++++---- Assets/Materials.meta | 2 +- Assets/New Material.mat.meta | 2 +- Assets/_Recovery.meta | 2 +- Assets/host a join.meta | 2 +- Assets/main menu.meta | 2 +- 6 files changed, 798 insertions(+), 126 deletions(-) diff --git a/Assets/GameManager/GameManager_Input.cs b/Assets/GameManager/GameManager_Input.cs index ef51873..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 } + + /// + /// 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. + /// + public enum PositionSource + { + Auto, + GpsOnly, + NetworkOnly, + UnityInput, + EditorWasd, + } + +#if UNITY_ANDROID && !UNITY_EDITOR + /// + /// 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. + /// + 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("getLatitude"); + double lon = location.Call("getLongitude"); + long t = location.Call("getTime"); + string provider = ""; + try { provider = location.Call("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) { } + } + + /// + /// 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. + /// + 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("currentActivity"); + } + if (_activity == null) { error = "no current activity"; return false; } + + _locationManager = _activity.Call("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("getLastKnownLocation", provider); + if (loc != null) + { + nonNullReturned = true; + double lat = loc.Call("getLatitude"); + double lon = loc.Call("getLongitude"); + long t = loc.Call("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("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("getProviders", true); + if (list == null) return ""; + int size = list.Call("size"); + var parts = new System.Text.StringBuilder(); + for (int i = 0; i < size; i++) + { + var name = list.Call("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) { @@ -50,97 +336,376 @@ namespace Subsystems private Position _lastSentPosition; 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; - private CoroutineHost _coroutineHost = new CoroutineHost(); + private CoroutineHost _coroutineHost; + + private int _gpsRetryCount = 0; + private const int _maxGpsRetries = 5; + 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 + + /// Last known GPS position (for CreateLobby centre point) + public Position? LastKnownPosition => _currentPosition.Lat != 0 || _currentPosition.Lon != 0 ? _currentPosition : (Position?)null; + + /// Current GPS state machine value (debug/diagnostic). + public string GpsStateName => _GPSState.ToString(); + + /// Last GPS error reason captured during init (empty if none). + public string LastGpsError => _lastGpsError ?? ""; + + /// Retry count out of max (debug/diagnostic). + public string GpsRetryProgress => $"{_gpsRetryCount}/{_maxGpsRetries}"; + + /// Currently selected position source (for UI cycle button). + public PositionSource CurrentSource => _currentSource; + + /// Display name for the current source (for UI label). + 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(); + } + } + } + + /// + /// 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. + /// + 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; _player = player; _testMode = testMode; + // CoroutineHost needs a MonoBehaviour on a real GameObject + var hostGO = new UnityEngine.GameObject("_CoroutineHost"); + UnityEngine.Object.DontDestroyOnLoad(hostGO); + _coroutineHost = hostGO.AddComponent(); + + // 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; + } + + /// Called from OnSceneLoaded when Client.unity loads so the + /// Player capsule (which lives in Client.unity) can be wired at runtime. + public void SetPlayerObject(GameObject player) { _player = player; } + + /// + /// 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. + /// + 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(); + } + + /// Cycle through the available sources for tap-to-cycle UI. + 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 { } + } + + /// + /// Kick off GPS initialization if it hasn't started yet. Safe to call + /// repeatedly. Hosts must call this from the lobby setup screen so + /// that by the time they click "Create Lobby" we have a real GPS + /// fix to use as the play-area center, instead of falling back to + /// the hardcoded coordinates. + /// + public void EnsureGPSStarted() + { + 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() { + var state = _gameClient?.CurrentLobbyState; + if (state == null || state.Phase != GamePhase.Playing) + return; + try { - if (_gameClient.CurrentLobbyState.Phase == GamePhase.Playing) + if (_currentSource == PositionSource.EditorWasd) { - if (_testMode) + if (_currentPosition == new Position(0, 0)) { + if (state.MapData == null) + return; - if (_currentPosition == null || _currentPosition == new Position(0, 0)) - { - //Init blok - _currentPosition = _gameClient.CurrentLobbyState.MapData.Center; - _mapCenter = _gameClient.CurrentLobbyState.MapData.Center; - _lastSentPosition = _currentPosition; - } + //Init blok + _currentPosition = state.MapData.Center; + _mapCenter = state.MapData.Center; + _lastSentPosition = _currentPosition; + } + if (!SuppressWasd) TestPlayerPosition(); + else + TrySendCurrentPosition(); // keep-alive only + } + else + { + if (_GPSState == GPSState.Uninitialized) + { + _coroutineHost.StartCoroutine(InitiallizeGPS()); + return; + } + else if (_GPSState == GPSState.Initializing) + { + return; + } + else if (_GPSState == GPSState.Running) + { + EnsureMapCenter(); + TrySendCurrentPosition(); } else { - if (_GPSState == GPSState.Uninitialized) + Debug.Log("GPS failed, trying again..."); + if (_gpsRetryCount < _maxGpsRetries) { - _coroutineHost.StartCoroutine(InitiallizeGPS()); - return; - } - else if (_GPSState == GPSState.Initializing) - { - return; - } - else if (_GPSState == GPSState.Running) - { - try - { - if (_currentPosition != _lastSentPosition) - { - _gameClient.UpdatePosition(_currentPosition); - _lastSentPosition = _currentPosition; - _player.transform.position = _currentPosition.ToLocalVector3(_mapCenter); - _player.transform.rotation = Quaternion.Euler(0, (float)CalculateHeading(_lastSentPosition.ToLocalVector3(_mapCenter), _currentPosition.ToLocalVector3(_mapCenter)), 0); - } - } - catch (Exception ex) - { - Debug.Log(ex); - } + _gpsRetryCount++; + _GPSState = GPSState.Uninitialized; } else { - Debug.Log("GPS failed, trying again..."); - _GPSState = GPSState.Uninitialized; + Debug.LogWarning("GPS unavailable after max retries. Using last known position."); + // Keep _GPSState = Failed so we stop retrying } } } } - catch (NullReferenceException ex) { Debug.Log(ex); } + catch (Exception ex) + { + Debug.LogWarning($"[Input] positionCheck failed: {ex.Message}"); + } } + + private void EnsureMapCenter() + { + if (_mapCenter.Lat != 0 || _mapCenter.Lon != 0) + return; + + var md = _gameClient?.CurrentLobbyState?.MapData; + if (md != null) + _mapCenter = md.Center; + } + + private void TrySendCurrentPosition() + { + bool moved = _currentPosition != _lastSentPosition; + bool keepAliveDue = (Time.time - _lastPositionSendTime) >= _positionKeepAliveSeconds; + if (!moved && !keepAliveDue) + return; + + var previous = _lastSentPosition; + _gameClient.UpdatePosition(_currentPosition); + _lastSentPosition = _currentPosition; + _lastPositionSendTime = Time.time; + + if (_player == null || (_mapCenter.Lat == 0 && _mapCenter.Lon == 0)) + return; + + var localCurrent = _currentPosition.ToLocalVector3(_mapCenter); + _player.transform.position = localCurrent; + + if (previous.Lat == 0 && previous.Lon == 0) + return; + + var heading = CalculateHeading(previous.ToLocalVector3(_mapCenter), localCurrent); + if (heading.HasValue) + _player.transform.rotation = Quaternion.Euler(0, (float)heading.Value, 0); + } + private void TestPlayerPosition() { 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 { - if (_currentPosition != _lastSentPosition) - { - _gameClient.UpdatePosition(_currentPosition); - _lastSentPosition = _currentPosition; - } + TrySendCurrentPosition(); } catch { @@ -150,105 +715,212 @@ namespace Subsystems } private double? CalculateHeading(Vector3 first, Vector3 second) { - double? heading = null; - if ((first - second).magnitude == 0) - { - return null; - } - else if (first.x == second.x && first.z < second.z) - { - return 0; - } - else if (first.x == second.x && first.z > second.z) - { - return 180; - } - else if (first.x > second.x && first.z == second.z) - { - return 270; - } - else if (first.x < second.x && first.z == second.z) - { - return 90; - } - else if (first.x < second.x && first.z < second.z) - { - heading = Math.Asin((second.z - first.z) / first.DistanceTo(second)); - return (heading * 180) / Math.PI; - } - else if (first.x < second.x && first.z > second.z) - { - heading = Math.Asin((second.z - first.z) / first.DistanceTo(second)); - return (heading * 180) / Math.PI + 180; - } - else if (first.x > second.x && first.z < second.z) - { - heading = Math.Asin((second.z - first.z) / first.DistanceTo(second)); - return (heading * 180) / Math.PI - 90; - } - else if (first.x > second.x && first.z > second.z) - { - heading = Math.Asin((second.z - first.z) / first.DistanceTo(second)); - return (heading * 180) / Math.PI - 90; - } - else - { - return heading; - } + if ((first - second).magnitude < 0.0001f) return null; + float dx = second.x - first.x; + float dz = second.z - first.z; + float heading = Mathf.Atan2(dx, dz) * Mathf.Rad2Deg; + if (heading < 0) heading += 360f; + return heading; } IEnumerator InitiallizeGPS() { _GPSState = GPSState.Initializing; + _gpsInitStartTime = Time.time; + _lastGpsError = ""; + +#if UNITY_ANDROID + // 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); + // Wait up to 10 seconds for user to respond to the permission dialog + float waited = 0f; + while (!UnityEngine.Android.Permission.HasUserAuthorizedPermission(UnityEngine.Android.Permission.FineLocation) && waited < 10f) + { + 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 = 10f; - float updateDistanceInMeters = 10f; + 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; } - _GPSState = GPSState.Running; - yield return _coroutineHost.StartCoroutine(GPSService()); - } - IEnumerator GPSService() - { - // Check if the user has location service enabled. - - // If the connection failed this cancels location service use. if (Input.location.status == LocationServiceStatus.Failed) { - Debug.LogError("Unable to determine device location"); + _lastGpsError = "Unity Input.location reported Failed status"; + Debug.LogError("[GPS] " + _lastGpsError); + _GPSState = GPSState.Failed; yield break; } - else + + _GPSState = GPSState.Running; + _gpsRetryCount = 0; + _coroutineHost.StartCoroutine(GPSService()); + } + +#if UNITY_ANDROID && !UNITY_EDITOR + /// + /// 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. + /// + IEnumerator AndroidGPSService() + { + while (_GPSState == GPSState.Running && _androidProvider != null) { - // If the connection succeeded, this retrieves the device's current location and displays it in the Console window. - _currentPosition = new Position(Input.location.lastData.latitude, Input.location.lastData.longitude); - Debug.Log("Location: " + Input.location.lastData.latitude + " " + Input.location.lastData.longitude + " " + Input.location.lastData.altitude + " " + Input.location.lastData.horizontalAccuracy + " " + Input.location.lastData.timestamp); - yield return new WaitForSeconds(5f); + if (_androidProvider.HasFix) + { + _currentPosition = new Position(_androidProvider.Lat, _androidProvider.Lon); + } + yield return new WaitForSeconds(0.5f); } - // Stops the location service if there is no need to query location updates continuously. - yield return _coroutineHost.StartCoroutine(GPSService()); + // 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; + yield break; + } + + // Keep current GPS position fresh; sending is throttled in positionCheck(). + var data = Input.location.lastData; + _currentPosition = new Position(data.latitude, data.longitude); + yield return new WaitForSeconds(0.5f); + } } } -} \ No newline at end of file +} diff --git a/Assets/Materials.meta b/Assets/Materials.meta index c832773..266e4ff 100644 --- a/Assets/Materials.meta +++ b/Assets/Materials.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 579723ff3377eaf4a9e50b2d20bbb490 +guid: 2230bf768ecb84610af77bea6cdd7074 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/New Material.mat.meta b/Assets/New Material.mat.meta index f1a4f93..a14f623 100644 --- a/Assets/New Material.mat.meta +++ b/Assets/New Material.mat.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 14e7660f277f5414ab8b483f12ca6e3a +guid: 7142a58f80866aba3ae4ffeb79d5f67c NativeFormatImporter: externalObjects: {} mainObjectFileID: 0 diff --git a/Assets/_Recovery.meta b/Assets/_Recovery.meta index 62ed08f..ee2850e 100644 --- a/Assets/_Recovery.meta +++ b/Assets/_Recovery.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: d8e0b736547e8cf4f98d5f7832d39867 +guid: b9c8f9e5bbf063b4fb1152966129a495 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/host a join.meta b/Assets/host a join.meta index 041d4a6..157039b 100644 --- a/Assets/host a join.meta +++ b/Assets/host a join.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 6279204888b22964e9ddee5def64f698 +guid: 0f6331bfef5b3a00cb6f84141e60f9c4 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/main menu.meta b/Assets/main menu.meta index 7b5bd5d..2e2d688 100644 --- a/Assets/main menu.meta +++ b/Assets/main menu.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 73f88b8c97995af4eab9d0126a697831 +guid: 43d195e24415843378eb1b53e9e1da18 folderAsset: yes DefaultImporter: externalObjects: {}