using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using BlockSpace.Account; using Photon.Pun; using TMPro; using UnityEngine; using UnityEngine.Networking; namespace BlockSpace.Voxels { [DisallowMultipleComponent] public sealed class RandomLoginCodeButton : MonoBehaviour { [Serializable] private sealed class LinkStatusRequest { public string code; } [Serializable] private sealed class LinkStatusResponse { public bool ok; public bool exists; public bool claimed; public bool expired; public string code; public string playerId; public DiscordInfo discord; } [Serializable] private sealed class DiscordInfo { public string id; public string username; public string globalName; public string displayName; } private const float ButtonPressCooldown = 0.2f; private const float ButtonShrinkDuration = 0.08f; private const float ButtonSettleDuration = 0.12f; private const float ButtonPressedScale = 0.84f; private const float ButtonOvershootScale = 1.04f; private const string ButtonThockResourcePath = "Thock"; private const string DefaultCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; [Header("Button")] [SerializeField] private Collider buttonCollider; [SerializeField] private VRVoxelBrush voxelBrush; [Header("Code Output")] [SerializeField] private TMP_Text codeText; [SerializeField] private int codeLength = 6; [SerializeField] private string allowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; [SerializeField] private bool generateOnStart; [Header("Discord Link (optional)")] [SerializeField] private bool pollForDiscordLink = true; [SerializeField] private string backendBaseUrl = "https://blockspacebackend-production.up.railway.app"; [SerializeField] private float pollIntervalSeconds = 2f; [SerializeField] private float pollTimeoutSeconds = 180f; [Header("Audio")] [SerializeField] private AudioClip buttonThockClip; private readonly Dictionary buttonBaseScales = new Dictionary(); private readonly Dictionary buttonAnimationCoroutines = new Dictionary(); private bool insidePrevious; private float nextButtonPressTime; private string currentCode = ""; private Coroutine pollCoroutine; public string CurrentCode => currentCode; private void Awake() { ResolveReferences(); EnsureButtonAudioAssigned(); if (generateOnStart) { GenerateAndShowCode(); } } private void OnDisable() { StopAllCoroutines(); ResetAllButtonScales(); } private void Update() { ResolveReferences(); HandleButton(); } private void ResolveReferences() { if (buttonCollider == null) { buttonCollider = GetComponent(); } if (voxelBrush == null) { voxelBrush = ((VRVoxelBrush.Instance != null) ? VRVoxelBrush.Instance : UnityEngine.Object.FindObjectOfType()); } if (codeText == null) { codeText = GetComponentInChildren(includeInactive: true); } } private void HandleButton() { bool flag = IsHandInside(buttonCollider, (voxelBrush != null) ? voxelBrush.LeftHandAnchor : null) || IsHandInside(buttonCollider, (voxelBrush != null) ? voxelBrush.RightHandAnchor : null); if (flag && !insidePrevious) { PlayButtonEnterAnimation(buttonCollider); if (Time.unscaledTime >= nextButtonPressTime) { nextButtonPressTime = Time.unscaledTime + 0.2f; PlayButtonClickFeedback(buttonCollider); GenerateAndShowCode(); } } insidePrevious = flag; } public void GenerateAndShowCode() { currentCode = GenerateCode(); if (codeText != null) { codeText.text = currentCode; } else { Debug.LogWarning("[RandomLoginCodeButton] No TMP_Text assigned."); } Debug.Log("[RandomLoginCodeButton] Generated code: " + currentCode); if (pollForDiscordLink) { if (pollCoroutine != null) { StopCoroutine(pollCoroutine); pollCoroutine = null; } pollCoroutine = StartCoroutine(PollDiscordLink(currentCode)); } } private string GenerateCode() { int num = Mathf.Max(6, codeLength); string text = (string.IsNullOrEmpty(allowedCharacters) ? "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" : allowedCharacters.ToUpperInvariant()); char[] array = new char[num]; for (int i = 0; i < array.Length; i++) { int index = UnityEngine.Random.Range(0, text.Length); array[i] = text[index]; } return new string(array); } private IEnumerator PollDiscordLink(string code) { if (string.IsNullOrWhiteSpace(code)) { yield break; } string baseUrl = (backendBaseUrl ?? string.Empty).Trim(); if (string.IsNullOrEmpty(baseUrl)) { Debug.LogWarning("[RandomLoginCodeButton] No backendBaseUrl set; cannot poll link status."); yield break; } float start = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - start <= Mathf.Max(1f, pollTimeoutSeconds)) { string url = baseUrl.TrimEnd('/') + "/api/link-codes/status"; string s = JsonUtility.ToJson(new LinkStatusRequest { code = code }); using (UnityWebRequest request = new UnityWebRequest(url, "POST")) { byte[] bytes = Encoding.UTF8.GetBytes(s); request.uploadHandler = new UploadHandlerRaw(bytes); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { LinkStatusResponse linkStatusResponse = null; try { linkStatusResponse = JsonUtility.FromJson(request.downloadHandler.text); } catch { } if (linkStatusResponse != null && linkStatusResponse.ok && linkStatusResponse.exists && linkStatusResponse.claimed) { string text = linkStatusResponse.playerId ?? string.Empty; string text2 = ((linkStatusResponse.discord != null) ? linkStatusResponse.discord.id : string.Empty); string text3 = ((linkStatusResponse.discord != null) ? linkStatusResponse.discord.displayName : string.Empty); if (string.IsNullOrEmpty(text3)) { text3 = ((linkStatusResponse.discord != null) ? linkStatusResponse.discord.globalName : string.Empty); } if (string.IsNullOrEmpty(text3)) { text3 = ((linkStatusResponse.discord != null) ? linkStatusResponse.discord.username : string.Empty); } string userFolderId = BlockSpaceAccountPrefs.GetUserFolderId(); BlockSpaceAccountPrefs.SetLinkedAccount(text, text2, text3); TryMigrateAvatarPayload(userFolderId, text); if (!string.IsNullOrEmpty(text3)) { PlayerPrefs.SetString("Username", text3); PlayerPrefs.Save(); if (PhotonNetwork.LocalPlayer != null) { PhotonNetwork.LocalPlayer.NickName = text3; } } Debug.Log("[RandomLoginCodeButton] Linked to Discord user '" + text3 + "' (" + text2 + ")."); pollCoroutine = null; yield break; } } else if (request.result != UnityWebRequest.Result.ProtocolError) { Debug.LogWarning($"[RandomLoginCodeButton] Link poll failed ({request.result}): {request.error}"); } } yield return new WaitForSecondsRealtime(Mathf.Clamp(pollIntervalSeconds, 0.25f, 30f)); } Debug.LogWarning("[RandomLoginCodeButton] Link poll timed out; generate a new code if needed."); pollCoroutine = null; } private static void TryMigrateAvatarPayload(string fromUserId, string toUserId) { if (string.IsNullOrEmpty(fromUserId) || string.IsNullOrEmpty(toUserId) || fromUserId == toUserId) { return; } try { string text = "avatar_payload.json"; string text2 = Path.Combine(Application.persistentDataPath, "BlockSpace", "Users", fromUserId, text); string text3 = Path.Combine(Application.persistentDataPath, "BlockSpace", "Users", toUserId, text); if (File.Exists(text2) && !File.Exists(text3)) { string directoryName = Path.GetDirectoryName(text3); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } File.Copy(text2, text3, overwrite: false); } } catch (Exception ex) { Debug.LogWarning("[RandomLoginCodeButton] Avatar migration failed: " + ex.Message); } } private void PlayButtonEnterAnimation(Collider button) { if (!(button == null)) { Transform transform = button.transform; if (!buttonBaseScales.TryGetValue(transform, out var value)) { value = transform.localScale; buttonBaseScales.Add(transform, value); } if (buttonAnimationCoroutines.TryGetValue(transform, out var value2) && value2 != null) { StopCoroutine(value2); } transform.localScale = value; buttonAnimationCoroutines[transform] = StartCoroutine(AnimateButtonScale(transform, value)); } } private void PlayButtonClickFeedback(Collider button) { if (!(buttonThockClip == null) && !(button == null)) { AudioSource.PlayClipAtPoint(buttonThockClip, button.bounds.center, 1f); } } private IEnumerator AnimateButtonScale(Transform buttonTransform, Vector3 baseScale) { Vector3 pressedScale = baseScale * 0.84f; Vector3 overshootScale = baseScale * 1.04f; float elapsed = 0f; while (elapsed < 0.08f) { if (buttonTransform == null) { yield break; } elapsed += Time.unscaledDeltaTime; float t = Mathf.Clamp01(elapsed / 0.08f); buttonTransform.localScale = Vector3.LerpUnclamped(baseScale, pressedScale, t); yield return null; } elapsed = 0f; while (elapsed < 0.12f) { if (buttonTransform == null) { yield break; } elapsed += Time.unscaledDeltaTime; float num = Mathf.Clamp01(elapsed / 0.12f); float num2 = num * num * (3f - 2f * num); Vector3 localScale = ((num2 < 0.5f) ? Vector3.LerpUnclamped(pressedScale, overshootScale, num2 / 0.5f) : Vector3.LerpUnclamped(overshootScale, baseScale, (num2 - 0.5f) / 0.5f)); buttonTransform.localScale = localScale; yield return null; } if (buttonTransform != null) { buttonTransform.localScale = baseScale; buttonAnimationCoroutines[buttonTransform] = null; } } private void ResetAllButtonScales() { foreach (KeyValuePair buttonBaseScale in buttonBaseScales) { if (buttonBaseScale.Key != null) { buttonBaseScale.Key.localScale = buttonBaseScale.Value; } } buttonAnimationCoroutines.Clear(); } private void EnsureButtonAudioAssigned() { if (buttonThockClip == null) { buttonThockClip = Resources.Load("Thock"); } } private static bool IsHandInside(Collider button, Transform handAnchor) { if (button == null || handAnchor == null) { return false; } Vector3 position = handAnchor.position; return (button.ClosestPoint(position) - position).sqrMagnitude <= 1E-06f; } } }