408 lines
11 KiB
C#
408 lines
11 KiB
C#
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<Transform, Vector3> buttonBaseScales = new Dictionary<Transform, Vector3>();
|
|
|
|
private readonly Dictionary<Transform, Coroutine> buttonAnimationCoroutines = new Dictionary<Transform, Coroutine>();
|
|
|
|
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<Collider>();
|
|
}
|
|
if (voxelBrush == null)
|
|
{
|
|
voxelBrush = ((VRVoxelBrush.Instance != null) ? VRVoxelBrush.Instance : UnityEngine.Object.FindObjectOfType<VRVoxelBrush>());
|
|
}
|
|
if (codeText == null)
|
|
{
|
|
codeText = GetComponentInChildren<TMP_Text>(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<LinkStatusResponse>(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<Transform, Vector3> buttonBaseScale in buttonBaseScales)
|
|
{
|
|
if (buttonBaseScale.Key != null)
|
|
{
|
|
buttonBaseScale.Key.localScale = buttonBaseScale.Value;
|
|
}
|
|
}
|
|
buttonAnimationCoroutines.Clear();
|
|
}
|
|
|
|
private void EnsureButtonAudioAssigned()
|
|
{
|
|
if (buttonThockClip == null)
|
|
{
|
|
buttonThockClip = Resources.Load<AudioClip>("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;
|
|
}
|
|
}
|
|
}
|