1495 lines
49 KiB
C#
1495 lines
49 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Text.RegularExpressions;
|
|
using BlockSpace.Account;
|
|
using BlockSpace.Nodes;
|
|
using BlockSpace.Worlds;
|
|
using Photon.Pun;
|
|
using Photon.Realtime;
|
|
using Photon.VR;
|
|
using Photon.VR.Player;
|
|
using UnityEngine;
|
|
|
|
namespace BlockSpace.Voxels
|
|
{
|
|
[DisallowMultipleComponent]
|
|
[RequireComponent(typeof(PhotonView))]
|
|
public sealed class VoxelGroupPhotonSync : MonoBehaviourPunCallbacks
|
|
{
|
|
private sealed class StrokePreviewState
|
|
{
|
|
public VoxelGroup group;
|
|
|
|
public byte voxelValue;
|
|
|
|
public float radius;
|
|
|
|
public float forwardOffset;
|
|
|
|
public Vector3 lastPoint;
|
|
|
|
public readonly HashSet<Vector3Int> cells = new HashSet<Vector3Int>();
|
|
}
|
|
|
|
private sealed class ErasePreviewState
|
|
{
|
|
public float radius;
|
|
|
|
public float forwardOffset;
|
|
|
|
public Vector3 lastPoint;
|
|
|
|
public readonly Dictionary<ushort, VoxelGroupManager.SavedGroupData> beforeByGroupId = new Dictionary<ushort, VoxelGroupManager.SavedGroupData>();
|
|
}
|
|
|
|
private sealed class MovePreviewState
|
|
{
|
|
public VoxelGroup group;
|
|
|
|
public Vector3 targetPosition;
|
|
|
|
public Quaternion targetRotation;
|
|
|
|
public Vector3 velocity;
|
|
|
|
public float lastPacketTime;
|
|
}
|
|
|
|
[SerializeField]
|
|
private VoxelGroupManager groupManager;
|
|
|
|
[SerializeField]
|
|
private NodeGraphManager nodeGraphManager;
|
|
|
|
[SerializeField]
|
|
private float lateJoinReplayDelay = 0.5f;
|
|
|
|
[Header("World Persistence")]
|
|
[SerializeField]
|
|
private bool enableWorldPersistence = true;
|
|
|
|
[SerializeField]
|
|
private string backendBaseUrl = "https://blockspacebackend-production.up.railway.app";
|
|
|
|
[SerializeField]
|
|
private float autoSaveIntervalSeconds = 30f;
|
|
|
|
[SerializeField]
|
|
private float permissionRefreshIntervalSeconds = 5f;
|
|
|
|
[Header("World Sync")]
|
|
[SerializeField]
|
|
private float authoritySnapshotIntervalSeconds = 5f;
|
|
|
|
[SerializeField]
|
|
private float localEditSnapshotGraceSeconds = 3f;
|
|
|
|
private readonly List<VoxelGroup> syncBuffer = new List<VoxelGroup>();
|
|
|
|
private readonly List<VoxelGroup> persistBuffer = new List<VoxelGroup>();
|
|
|
|
private readonly Dictionary<int, StrokePreviewState> strokePreviews = new Dictionary<int, StrokePreviewState>();
|
|
|
|
private readonly Dictionary<int, ErasePreviewState> erasePreviews = new Dictionary<int, ErasePreviewState>();
|
|
|
|
private readonly Dictionary<int, Transform> rightHandCache = new Dictionary<int, Transform>();
|
|
|
|
private readonly Dictionary<ushort, MovePreviewState> movePreviews = new Dictionary<ushort, MovePreviewState>();
|
|
|
|
private readonly List<ushort> movePreviewCleanup = new List<ushort>();
|
|
|
|
private PhotonView cachedPhotonView;
|
|
|
|
private Coroutine worldJoinCoroutine;
|
|
|
|
private Coroutine worldAutoSaveCoroutine;
|
|
|
|
private Coroutine worldPermissionRefreshCoroutine;
|
|
|
|
private Coroutine worldTeleportCoroutine;
|
|
|
|
private bool pendingWorldTeleportRestart;
|
|
|
|
private bool currentTeleportShouldCompleteLoading;
|
|
|
|
private bool pendingTeleportShouldCompleteLoading;
|
|
|
|
private Coroutine authoritySnapshotCoroutine;
|
|
|
|
private WorldBackendClient worldBackendClient;
|
|
|
|
private string currentWorldId = string.Empty;
|
|
|
|
private bool currentWorldExists;
|
|
|
|
private bool isCreatorForCurrentWorld;
|
|
|
|
private bool worldDirty;
|
|
|
|
private bool suppressWorldDirty;
|
|
|
|
private string cachedPlayerId = string.Empty;
|
|
|
|
private string cachedBaseWorldName = string.Empty;
|
|
|
|
private float lastLocalNetworkEditSentAt = float.NegativeInfinity;
|
|
|
|
private void Reset()
|
|
{
|
|
groupManager = GetComponent<VoxelGroupManager>();
|
|
nodeGraphManager = GetComponent<NodeGraphManager>();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
cachedPhotonView = GetComponent<PhotonView>();
|
|
if (groupManager == null)
|
|
{
|
|
groupManager = GetComponent<VoxelGroupManager>();
|
|
if (groupManager == null)
|
|
{
|
|
groupManager = UnityEngine.Object.FindAnyObjectByType<VoxelGroupManager>();
|
|
}
|
|
}
|
|
if (nodeGraphManager == null)
|
|
{
|
|
nodeGraphManager = GetComponent<NodeGraphManager>();
|
|
if (nodeGraphManager == null)
|
|
{
|
|
nodeGraphManager = UnityEngine.Object.FindAnyObjectByType<NodeGraphManager>();
|
|
}
|
|
}
|
|
groupManager?.RegisterNetworkSync(this);
|
|
}
|
|
|
|
public override void OnEnable()
|
|
{
|
|
base.OnEnable();
|
|
groupManager?.RegisterNetworkSync(this);
|
|
}
|
|
|
|
public override void OnDisable()
|
|
{
|
|
ClearStrokePreviews();
|
|
ClearMovePreviews(restoreColliders: true);
|
|
rightHandCache.Clear();
|
|
StopWorldCoroutines();
|
|
base.OnDisable();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
UpdateMovePreviews();
|
|
}
|
|
|
|
public override void OnJoinedRoom()
|
|
{
|
|
Debug.Log($"[VoxelGroupPhotonSync] OnJoinedRoom: joined room '{PhotonNetwork.CurrentRoom?.Name}', isMaster={PhotonNetwork.IsMasterClient}.");
|
|
SetLoadingCheckpoint(0.68f);
|
|
if (!(groupManager == null))
|
|
{
|
|
ClearStrokePreviews();
|
|
ClearMovePreviews(restoreColliders: false);
|
|
rightHandCache.Clear();
|
|
lastLocalNetworkEditSentAt = float.NegativeInfinity;
|
|
if (!PhotonNetwork.IsMasterClient)
|
|
{
|
|
groupManager.ClearAllGroupsForNetworkSync();
|
|
}
|
|
if (enableWorldPersistence)
|
|
{
|
|
StopWorldCoroutines();
|
|
worldJoinCoroutine = StartCoroutine(WorldResolveAndMaybeLoad());
|
|
}
|
|
StartAuthoritySnapshotLoopIfNeeded();
|
|
}
|
|
}
|
|
|
|
public override void OnLeftRoom()
|
|
{
|
|
StopWorldCoroutines();
|
|
StopAuthoritySnapshotLoop();
|
|
ClearMovePreviews(restoreColliders: false);
|
|
lastLocalNetworkEditSentAt = float.NegativeInfinity;
|
|
if (groupManager != null)
|
|
{
|
|
groupManager.ClearAllGroupsForNetworkSync();
|
|
}
|
|
base.OnLeftRoom();
|
|
}
|
|
|
|
public override void OnMasterClientSwitched(Player newMasterClient)
|
|
{
|
|
base.OnMasterClientSwitched(newMasterClient);
|
|
if (enableWorldPersistence && newMasterClient != null && newMasterClient == PhotonNetwork.LocalPlayer)
|
|
{
|
|
StartWorldAutoSaveIfNeeded();
|
|
}
|
|
}
|
|
|
|
public override void OnPlayerEnteredRoom(Player newPlayer)
|
|
{
|
|
if (PhotonNetwork.IsMasterClient && !(groupManager == null) && newPlayer != null)
|
|
{
|
|
StartCoroutine(SendExistingGroupsToPlayerAfterDelay(newPlayer));
|
|
}
|
|
}
|
|
|
|
public override void OnPlayerLeftRoom(Player otherPlayer)
|
|
{
|
|
if (otherPlayer != null)
|
|
{
|
|
EndStrokePreview(otherPlayer.ActorNumber);
|
|
EndErasePreview(otherPlayer.ActorNumber);
|
|
rightHandCache.Remove(otherPlayer.ActorNumber);
|
|
}
|
|
}
|
|
|
|
private IEnumerator SendExistingGroupsToPlayerAfterDelay(Player newPlayer)
|
|
{
|
|
float num = Mathf.Max(0f, lateJoinReplayDelay);
|
|
if (num > 0f)
|
|
{
|
|
yield return new WaitForSecondsRealtime(num);
|
|
}
|
|
else
|
|
{
|
|
yield return null;
|
|
}
|
|
if (!PhotonNetwork.InRoom || !PhotonNetwork.IsMasterClient || groupManager == null || newPlayer == null)
|
|
{
|
|
yield break;
|
|
}
|
|
syncBuffer.Clear();
|
|
syncBuffer.AddRange(groupManager.GetAllGroups());
|
|
for (int i = 0; i < syncBuffer.Count; i++)
|
|
{
|
|
if (!IsStrokePreviewGroup(syncBuffer[i]))
|
|
{
|
|
BroadcastCreate(syncBuffer[i], newPlayer, playSpawnAnimation: false, playSpawnParticles: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void BroadcastCreate(VoxelGroup group, bool playSpawnAnimation, bool playSpawnParticles = true)
|
|
{
|
|
BroadcastCreate(group, null, playSpawnAnimation, playSpawnParticles);
|
|
}
|
|
|
|
internal void BroadcastModify(VoxelGroup group)
|
|
{
|
|
if (CanSendRpc() && !(group == null))
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
MarkWorldDirty();
|
|
byte[] array = ((group.voxels != null) ? ((byte[])group.voxels.Clone()) : new byte[0]);
|
|
cachedPhotonView.RPC("RPC_ModifyGroup", RpcTarget.Others, (int)group.id, group.position.x, group.position.y, group.position.z, group.zoneID, (int)group.packedRotation, group.size.x, group.size.y, group.size.z, array);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastMovePreview(VoxelGroup group)
|
|
{
|
|
if (CanSendRpc() && !(group == null) && PhotonNetwork.LocalPlayer != null)
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
cachedPhotonView.RPC("RPC_MovePreview", RpcTarget.Others, PhotonNetwork.LocalPlayer.ActorNumber, (int)group.id, group.position.x, group.position.y, group.position.z, (int)group.packedRotation);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastMoveFinal(VoxelGroup group)
|
|
{
|
|
BroadcastModify(group);
|
|
}
|
|
|
|
internal void BroadcastSetColliderEnabled(VoxelGroup group, bool enabled)
|
|
{
|
|
if (CanSendRpc() && !(group == null))
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
MarkWorldDirty();
|
|
cachedPhotonView.RPC("RPC_SetGroupColliderEnabled", RpcTarget.Others, (int)group.id, enabled);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastDestroy(ushort groupId, bool playDestroyAnimation = true, bool playDestroyParticles = true)
|
|
{
|
|
if (CanSendRpc() && groupId != 0)
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
MarkWorldDirty();
|
|
cachedPhotonView.RPC("RPC_DestroyGroup", RpcTarget.Others, (int)groupId, playDestroyAnimation, playDestroyParticles);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastStrokePreviewBegin(byte voxelValue, float radius, float forwardOffset, Vector3 startPoint)
|
|
{
|
|
if (CanSendStrokePreviewRpc(voxelValue, radius))
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
cachedPhotonView.RPC("RPC_BeginStrokePreview", RpcTarget.Others, PhotonNetwork.LocalPlayer.ActorNumber, (int)voxelValue, radius, forwardOffset, startPoint.x, startPoint.y, startPoint.z);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastStrokePreviewUpdate(byte voxelValue, float radius, float forwardOffset, Vector3 point)
|
|
{
|
|
if (CanSendStrokePreviewRpc(voxelValue, radius))
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
cachedPhotonView.RPC("RPC_UpdateStrokePreview", RpcTarget.Others, PhotonNetwork.LocalPlayer.ActorNumber, (int)voxelValue, radius, forwardOffset, point.x, point.y, point.z);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastStrokePreviewEnd()
|
|
{
|
|
if (CanSendRpc() && PhotonNetwork.LocalPlayer != null)
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
cachedPhotonView.RPC("RPC_EndStrokePreview", RpcTarget.Others, PhotonNetwork.LocalPlayer.ActorNumber);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastErasePreviewBegin(float radius, float forwardOffset, Vector3 startPoint)
|
|
{
|
|
if (CanSendRpc() && PhotonNetwork.LocalPlayer != null && !(radius <= 0f))
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
cachedPhotonView.RPC("RPC_BeginErasePreview", RpcTarget.Others, PhotonNetwork.LocalPlayer.ActorNumber, radius, forwardOffset, startPoint.x, startPoint.y, startPoint.z);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastErasePreviewUpdate(float radius, float forwardOffset, Vector3 point)
|
|
{
|
|
if (CanSendRpc() && PhotonNetwork.LocalPlayer != null && !(radius <= 0f))
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
cachedPhotonView.RPC("RPC_UpdateErasePreview", RpcTarget.Others, PhotonNetwork.LocalPlayer.ActorNumber, radius, forwardOffset, point.x, point.y, point.z);
|
|
}
|
|
}
|
|
|
|
internal void BroadcastErasePreviewEnd()
|
|
{
|
|
if (CanSendRpc() && PhotonNetwork.LocalPlayer != null)
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
cachedPhotonView.RPC("RPC_EndErasePreview", RpcTarget.Others, PhotonNetwork.LocalPlayer.ActorNumber);
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_CreateGroup(int groupId, int posX, int posY, int posZ, int zoneId, int packedRotation, int sizeX, int sizeY, int sizeZ, byte[] voxelData, bool playSpawnAnimation, bool playSpawnParticles, PhotonMessageInfo info)
|
|
{
|
|
if (!(groupManager == null))
|
|
{
|
|
EndPreviewForIncomingGroupEdit(groupId, info, includeStrokePreview: true);
|
|
groupManager.ApplyNetworkCreateOrReplace((ushort)groupId, new Vector3Int(posX, posY, posZ), (byte)packedRotation, new Vector3Int(sizeX, sizeY, sizeZ), voxelData, zoneId, playSpawnAnimation, playSpawnParticles);
|
|
EndMovePreview((ushort)groupId, restoreCollider: true);
|
|
if (PhotonNetwork.IsMasterClient)
|
|
{
|
|
MarkWorldDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_ModifyGroup(int groupId, int posX, int posY, int posZ, int zoneId, int packedRotation, int sizeX, int sizeY, int sizeZ, byte[] voxelData, PhotonMessageInfo info)
|
|
{
|
|
if (!(groupManager == null))
|
|
{
|
|
EndPreviewForIncomingGroupEdit(groupId, info, includeStrokePreview: false);
|
|
groupManager.ApplyNetworkCreateOrReplace((ushort)groupId, new Vector3Int(posX, posY, posZ), (byte)packedRotation, new Vector3Int(sizeX, sizeY, sizeZ), voxelData, zoneId, playSpawnAnimation: false, playSpawnParticles: false);
|
|
EndMovePreview((ushort)groupId, restoreCollider: true);
|
|
if (PhotonNetwork.IsMasterClient)
|
|
{
|
|
MarkWorldDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_MovePreview(int actorNumber, int groupId, int posX, int posY, int posZ, int packedRotation, PhotonMessageInfo info)
|
|
{
|
|
if (!(groupManager == null) && groupId > 0 && (PhotonNetwork.LocalPlayer == null || actorNumber != PhotonNetwork.LocalPlayer.ActorNumber) && groupManager.TryGetGroup((ushort)groupId, out var group) && !(group == null))
|
|
{
|
|
ushort key = (ushort)groupId;
|
|
Vector3 vector = new Vector3(posX, posY, posZ) * groupManager.VoxelSize;
|
|
Quaternion targetRotation = GridRotationUtility.ToQuaternion((byte)packedRotation);
|
|
float realtimeSinceStartup = Time.realtimeSinceStartup;
|
|
if (!movePreviews.TryGetValue(key, out var value) || value.group == null)
|
|
{
|
|
value = new MovePreviewState
|
|
{
|
|
group = group,
|
|
targetPosition = group.transform.position,
|
|
targetRotation = group.transform.rotation,
|
|
lastPacketTime = realtimeSinceStartup
|
|
};
|
|
movePreviews[key] = value;
|
|
}
|
|
float num = Mathf.Max(0.001f, realtimeSinceStartup - value.lastPacketTime);
|
|
value.velocity = (vector - value.targetPosition) / num;
|
|
value.targetPosition = vector;
|
|
value.targetRotation = targetRotation;
|
|
value.lastPacketTime = realtimeSinceStartup;
|
|
groupManager.SetGroupColliderEnabled(group, enabled: false);
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_DestroyGroup(int groupId, bool playDestroyAnimation, bool playDestroyParticles, PhotonMessageInfo info)
|
|
{
|
|
if (!(groupManager == null))
|
|
{
|
|
EndPreviewForIncomingGroupEdit(groupId, info, includeStrokePreview: false);
|
|
groupManager.ApplyNetworkDestroy((ushort)groupId, playDestroyAnimation, playDestroyParticles);
|
|
EndMovePreview((ushort)groupId, restoreCollider: false);
|
|
if (PhotonNetwork.IsMasterClient)
|
|
{
|
|
MarkWorldDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_BeginStrokePreview(int actorNumber, int voxelValue, float radius, float forwardOffset, float pointX, float pointY, float pointZ)
|
|
{
|
|
if (!ShouldIgnoreStrokePreview(actorNumber, voxelValue, radius))
|
|
{
|
|
EndStrokePreview(actorNumber);
|
|
byte voxelValue2 = (byte)voxelValue;
|
|
Vector3 fallbackPoint = new Vector3(pointX, pointY, pointZ);
|
|
Vector3 vector = ResolveRightHandStrokePoint(actorNumber, fallbackPoint, forwardOffset);
|
|
VoxelGroup voxelGroup = groupManager.BeginStrokeGroup(0);
|
|
StrokePreviewState strokePreviewState = new StrokePreviewState
|
|
{
|
|
group = voxelGroup,
|
|
voxelValue = voxelValue2,
|
|
radius = radius,
|
|
forwardOffset = forwardOffset,
|
|
lastPoint = vector
|
|
};
|
|
strokePreviews[actorNumber] = strokePreviewState;
|
|
groupManager.RasterizeStrokeSegment(voxelGroup, vector, vector, radius, voxelValue2, strokePreviewState.cells);
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_UpdateStrokePreview(int actorNumber, int voxelValue, float radius, float forwardOffset, float pointX, float pointY, float pointZ)
|
|
{
|
|
if (!ShouldIgnoreStrokePreview(actorNumber, voxelValue, radius))
|
|
{
|
|
byte voxelValue2 = (byte)voxelValue;
|
|
Vector3 fallbackPoint = new Vector3(pointX, pointY, pointZ);
|
|
Vector3 vector = ResolveRightHandStrokePoint(actorNumber, fallbackPoint, forwardOffset);
|
|
if (!strokePreviews.TryGetValue(actorNumber, out var value) || value.group == null)
|
|
{
|
|
RPC_BeginStrokePreview(actorNumber, voxelValue, radius, forwardOffset, pointX, pointY, pointZ);
|
|
return;
|
|
}
|
|
value.voxelValue = voxelValue2;
|
|
value.radius = radius;
|
|
value.forwardOffset = forwardOffset;
|
|
groupManager.RasterizeStrokeSegment(value.group, value.lastPoint, vector, radius, voxelValue2, value.cells);
|
|
value.lastPoint = vector;
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_EndStrokePreview(int actorNumber)
|
|
{
|
|
EndStrokePreview(actorNumber);
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_BeginErasePreview(int actorNumber, float radius, float forwardOffset, float pointX, float pointY, float pointZ)
|
|
{
|
|
if (!ShouldIgnoreErasePreview(actorNumber, radius))
|
|
{
|
|
EndErasePreview(actorNumber);
|
|
Vector3 fallbackPoint = new Vector3(pointX, pointY, pointZ);
|
|
Vector3 vector = ResolveRightHandStrokePoint(actorNumber, fallbackPoint, forwardOffset);
|
|
ErasePreviewState erasePreviewState = new ErasePreviewState
|
|
{
|
|
radius = radius,
|
|
forwardOffset = forwardOffset,
|
|
lastPoint = vector
|
|
};
|
|
erasePreviews[actorNumber] = erasePreviewState;
|
|
ApplyErasePreviewSegment(erasePreviewState, vector, vector);
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_UpdateErasePreview(int actorNumber, float radius, float forwardOffset, float pointX, float pointY, float pointZ)
|
|
{
|
|
if (!ShouldIgnoreErasePreview(actorNumber, radius))
|
|
{
|
|
Vector3 fallbackPoint = new Vector3(pointX, pointY, pointZ);
|
|
Vector3 vector = ResolveRightHandStrokePoint(actorNumber, fallbackPoint, forwardOffset);
|
|
if (!erasePreviews.TryGetValue(actorNumber, out var value))
|
|
{
|
|
RPC_BeginErasePreview(actorNumber, radius, forwardOffset, pointX, pointY, pointZ);
|
|
return;
|
|
}
|
|
value.radius = radius;
|
|
value.forwardOffset = forwardOffset;
|
|
ApplyErasePreviewSegment(value, value.lastPoint, vector);
|
|
value.lastPoint = vector;
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_EndErasePreview(int actorNumber)
|
|
{
|
|
EndErasePreview(actorNumber);
|
|
}
|
|
|
|
private void BroadcastCreate(VoxelGroup group, Player targetPlayer, bool playSpawnAnimation, bool playSpawnParticles)
|
|
{
|
|
if (CanSendRpc() && !(group == null))
|
|
{
|
|
byte[] array = ((group.voxels != null) ? ((byte[])group.voxels.Clone()) : new byte[0]);
|
|
if (targetPlayer == null)
|
|
{
|
|
MarkLocalNetworkEditSent();
|
|
MarkWorldDirty();
|
|
cachedPhotonView.RPC("RPC_CreateGroup", RpcTarget.Others, (int)group.id, group.position.x, group.position.y, group.position.z, group.zoneID, (int)group.packedRotation, group.size.x, group.size.y, group.size.z, array, playSpawnAnimation, playSpawnParticles);
|
|
}
|
|
else
|
|
{
|
|
cachedPhotonView.RPC("RPC_CreateGroup", targetPlayer, (int)group.id, group.position.x, group.position.y, group.position.z, group.zoneID, (int)group.packedRotation, group.size.x, group.size.y, group.size.z, array, playSpawnAnimation, playSpawnParticles);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StopWorldCoroutines()
|
|
{
|
|
if (worldJoinCoroutine != null)
|
|
{
|
|
StopCoroutine(worldJoinCoroutine);
|
|
worldJoinCoroutine = null;
|
|
}
|
|
if (worldAutoSaveCoroutine != null)
|
|
{
|
|
StopCoroutine(worldAutoSaveCoroutine);
|
|
worldAutoSaveCoroutine = null;
|
|
}
|
|
if (worldPermissionRefreshCoroutine != null)
|
|
{
|
|
StopCoroutine(worldPermissionRefreshCoroutine);
|
|
worldPermissionRefreshCoroutine = null;
|
|
}
|
|
if (worldTeleportCoroutine != null)
|
|
{
|
|
StopCoroutine(worldTeleportCoroutine);
|
|
worldTeleportCoroutine = null;
|
|
}
|
|
pendingWorldTeleportRestart = false;
|
|
currentTeleportShouldCompleteLoading = false;
|
|
pendingTeleportShouldCompleteLoading = false;
|
|
StopAuthoritySnapshotLoop();
|
|
currentWorldId = string.Empty;
|
|
currentWorldExists = false;
|
|
isCreatorForCurrentWorld = false;
|
|
worldDirty = false;
|
|
suppressWorldDirty = false;
|
|
cachedBaseWorldName = string.Empty;
|
|
}
|
|
|
|
private void StartAuthoritySnapshotLoopIfNeeded()
|
|
{
|
|
if (authoritySnapshotCoroutine == null && PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && !(groupManager == null))
|
|
{
|
|
authoritySnapshotCoroutine = StartCoroutine(AuthoritySnapshotLoop());
|
|
}
|
|
}
|
|
|
|
private void StopAuthoritySnapshotLoop()
|
|
{
|
|
if (authoritySnapshotCoroutine != null)
|
|
{
|
|
StopCoroutine(authoritySnapshotCoroutine);
|
|
authoritySnapshotCoroutine = null;
|
|
}
|
|
}
|
|
|
|
private IEnumerator AuthoritySnapshotLoop()
|
|
{
|
|
while (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && cachedPhotonView != null && groupManager != null)
|
|
{
|
|
float time = Mathf.Clamp(authoritySnapshotIntervalSeconds, 1.5f, 30f);
|
|
yield return new WaitForSecondsRealtime(time);
|
|
if (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && !(groupManager == null))
|
|
{
|
|
List<VoxelGroup> persistableWorldGroups = GetPersistableWorldGroups();
|
|
int[] array = new int[persistableWorldGroups.Count];
|
|
int[] array2 = new int[persistableWorldGroups.Count];
|
|
for (int i = 0; i < persistableWorldGroups.Count; i++)
|
|
{
|
|
VoxelGroup voxelGroup = persistableWorldGroups[i];
|
|
array[i] = ((voxelGroup != null) ? voxelGroup.id : 0);
|
|
array2[i] = ((voxelGroup != null) ? ComputeFastGroupHash(voxelGroup) : 0);
|
|
}
|
|
cachedPhotonView.RPC("RPC_AuthoritySnapshot", RpcTarget.Others, array, array2);
|
|
}
|
|
}
|
|
authoritySnapshotCoroutine = null;
|
|
}
|
|
|
|
private List<VoxelGroup> GetPersistableWorldGroups()
|
|
{
|
|
persistBuffer.Clear();
|
|
if (groupManager == null)
|
|
{
|
|
return persistBuffer;
|
|
}
|
|
syncBuffer.Clear();
|
|
syncBuffer.AddRange(groupManager.GetAllGroups());
|
|
for (int i = 0; i < syncBuffer.Count; i++)
|
|
{
|
|
VoxelGroup voxelGroup = syncBuffer[i];
|
|
if (!(voxelGroup == null) && !IsStrokePreviewGroup(voxelGroup) && voxelGroup.id < 60000)
|
|
{
|
|
persistBuffer.Add(voxelGroup);
|
|
}
|
|
}
|
|
return persistBuffer;
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_SetGroupColliderEnabled(int groupId, bool enabled, PhotonMessageInfo info)
|
|
{
|
|
if (!(groupManager == null) && groupId > 0)
|
|
{
|
|
if (groupManager.TryGetGroup((ushort)groupId, out var group) && group != null)
|
|
{
|
|
groupManager.SetGroupColliderEnabled(group, enabled);
|
|
}
|
|
if (PhotonNetwork.IsMasterClient)
|
|
{
|
|
MarkWorldDirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Dictionary<int, int> BuildGroupSaveIndexMap(IList<VoxelGroup> groups)
|
|
{
|
|
Dictionary<int, int> dictionary = new Dictionary<int, int>();
|
|
if (groups == null)
|
|
{
|
|
return dictionary;
|
|
}
|
|
for (int i = 0; i < groups.Count; i++)
|
|
{
|
|
VoxelGroup voxelGroup = groups[i];
|
|
if (voxelGroup != null)
|
|
{
|
|
dictionary[voxelGroup.id] = i;
|
|
}
|
|
}
|
|
return dictionary;
|
|
}
|
|
|
|
private static int ComputeFastGroupHash(VoxelGroup group)
|
|
{
|
|
uint num = 2166136261u;
|
|
num = (num ^ (uint)group.position.x) * 16777619;
|
|
num = (num ^ (uint)group.position.y) * 16777619;
|
|
num = (num ^ (uint)group.position.z) * 16777619;
|
|
num = (num ^ group.packedRotation) * 16777619;
|
|
num = (num ^ (uint)group.size.x) * 16777619;
|
|
num = (num ^ (uint)group.size.y) * 16777619;
|
|
num = (num ^ (uint)group.size.z) * 16777619;
|
|
byte[] voxels = group.voxels;
|
|
int num2 = ((voxels != null) ? voxels.Length : 0);
|
|
num = (num ^ (uint)num2) * 16777619;
|
|
if (num2 <= 0)
|
|
{
|
|
return (int)num;
|
|
}
|
|
int num3 = Mathf.Max(1, num2 / 256);
|
|
for (int i = 0; i < num2; i += num3)
|
|
{
|
|
num = (num ^ voxels[i]) * 16777619;
|
|
}
|
|
num = (num ^ voxels[0]) * 16777619;
|
|
return (int)((num ^ voxels[num2 - 1]) * 16777619);
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_AuthoritySnapshot(int[] groupIds, int[] hashes, PhotonMessageInfo info)
|
|
{
|
|
if (groupManager == null || PhotonNetwork.IsMasterClient || groupIds == null || hashes == null || groupIds.Length != hashes.Length || ShouldDeferAuthoritySnapshot())
|
|
{
|
|
return;
|
|
}
|
|
int num = 0;
|
|
for (int i = 0; i < groupIds.Length; i++)
|
|
{
|
|
int num2 = groupIds[i];
|
|
if (num2 > 0 && num2 < 60000)
|
|
{
|
|
if (!groupManager.TryGetGroup((ushort)num2, out var group) || group == null)
|
|
{
|
|
num++;
|
|
}
|
|
else if (ComputeFastGroupHash(group) != hashes[i])
|
|
{
|
|
num++;
|
|
}
|
|
}
|
|
}
|
|
if (num <= 0 || cachedPhotonView == null)
|
|
{
|
|
return;
|
|
}
|
|
if (num > 6 || groupIds.Length > 64)
|
|
{
|
|
groupManager.ClearAllGroupsForNetworkSync();
|
|
cachedPhotonView.RPC("RPC_RequestFullSnapshot", RpcTarget.MasterClient, PhotonNetwork.LocalPlayer.ActorNumber);
|
|
return;
|
|
}
|
|
for (int j = 0; j < groupIds.Length; j++)
|
|
{
|
|
int num3 = groupIds[j];
|
|
if (num3 > 0 && num3 < 60000 && (!groupManager.TryGetGroup((ushort)num3, out var group2) || group2 == null || ComputeFastGroupHash(group2) != hashes[j]))
|
|
{
|
|
cachedPhotonView.RPC("RPC_RequestGroupSnapshot", RpcTarget.MasterClient, num3, PhotonNetwork.LocalPlayer.ActorNumber);
|
|
}
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_RequestFullSnapshot(int actorNumber, PhotonMessageInfo info)
|
|
{
|
|
if (PhotonNetwork.IsMasterClient && !(groupManager == null))
|
|
{
|
|
Player player = ((PhotonNetwork.CurrentRoom != null) ? PhotonNetwork.CurrentRoom.GetPlayer(actorNumber) : null);
|
|
if (player != null)
|
|
{
|
|
StartCoroutine(SendExistingGroupsToPlayerAfterDelay(player));
|
|
}
|
|
}
|
|
}
|
|
|
|
[PunRPC]
|
|
private void RPC_RequestGroupSnapshot(int groupId, int actorNumber, PhotonMessageInfo info)
|
|
{
|
|
if (PhotonNetwork.IsMasterClient && !(groupManager == null) && groupId > 0)
|
|
{
|
|
Player player = ((PhotonNetwork.CurrentRoom != null) ? PhotonNetwork.CurrentRoom.GetPlayer(actorNumber) : null);
|
|
if (player != null && groupManager.TryGetGroup((ushort)groupId, out var group) && !(group == null))
|
|
{
|
|
BroadcastCreate(group, player, playSpawnAnimation: false, playSpawnParticles: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MarkLocalNetworkEditSent()
|
|
{
|
|
lastLocalNetworkEditSentAt = Time.realtimeSinceStartup;
|
|
}
|
|
|
|
private void EndPreviewForIncomingGroupEdit(int groupId, PhotonMessageInfo info, bool includeStrokePreview)
|
|
{
|
|
int num = ((info.Sender != null) ? info.Sender.ActorNumber : 0);
|
|
if (num <= 0)
|
|
{
|
|
num = GetActorNumberFromGroupId(groupId);
|
|
}
|
|
if (num > 0)
|
|
{
|
|
EndErasePreview(num);
|
|
if (includeStrokePreview)
|
|
{
|
|
EndStrokePreview(num);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool ShouldDeferAuthoritySnapshot()
|
|
{
|
|
if (strokePreviews.Count > 0 || erasePreviews.Count > 0)
|
|
{
|
|
return true;
|
|
}
|
|
float num = Mathf.Clamp(localEditSnapshotGraceSeconds, 0f, 10f);
|
|
if (num <= 0f)
|
|
{
|
|
return false;
|
|
}
|
|
return Time.realtimeSinceStartup - lastLocalNetworkEditSentAt < num;
|
|
}
|
|
|
|
private void MarkWorldDirty()
|
|
{
|
|
if (!suppressWorldDirty && enableWorldPersistence && currentWorldExists && !string.IsNullOrEmpty(currentWorldId))
|
|
{
|
|
worldDirty = true;
|
|
}
|
|
}
|
|
|
|
private void EnsureWorldBackendClient()
|
|
{
|
|
if (worldBackendClient == null)
|
|
{
|
|
worldBackendClient = new WorldBackendClient(backendBaseUrl);
|
|
}
|
|
}
|
|
|
|
private void ScheduleTeleportAfterWorldLoad(bool isMaster, bool forceRestart = false, bool completeLoading = false)
|
|
{
|
|
Debug.Log($"[VoxelGroupPhotonSync] ScheduleTeleportAfterWorldLoad: isMaster={isMaster}, forceRestart={forceRestart}, completeLoading={completeLoading}, alreadyScheduled={worldTeleportCoroutine != null}.");
|
|
SetLoadingCheckpoint(forceRestart ? 0.95f : 0.82f);
|
|
if (forceRestart && worldTeleportCoroutine != null)
|
|
{
|
|
pendingWorldTeleportRestart = true;
|
|
pendingTeleportShouldCompleteLoading = completeLoading;
|
|
return;
|
|
}
|
|
if (worldTeleportCoroutine != null)
|
|
{
|
|
currentTeleportShouldCompleteLoading |= completeLoading;
|
|
return;
|
|
}
|
|
pendingWorldTeleportRestart = false;
|
|
pendingTeleportShouldCompleteLoading = false;
|
|
currentTeleportShouldCompleteLoading = completeLoading;
|
|
float delaySeconds = (isMaster ? 0.5f : Mathf.Clamp(lateJoinReplayDelay + 0.5f, 0.5f, 5f));
|
|
worldTeleportCoroutine = StartCoroutine(TeleportLocalPlayerAfterDelay(delaySeconds));
|
|
}
|
|
|
|
private IEnumerator TeleportLocalPlayerAfterDelay(float delaySeconds)
|
|
{
|
|
Debug.Log($"[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: waiting for Photon lobby/room connectivity. delay={delaySeconds}.");
|
|
SetLoadingCheckpoint(0.84f);
|
|
float connectionWaitStart = Time.realtimeSinceStartup;
|
|
while (!PhotonNetwork.IsConnectedAndReady || (!PhotonNetwork.InLobby && !PhotonNetwork.InRoom))
|
|
{
|
|
if (Time.realtimeSinceStartup - connectionWaitStart > 10f)
|
|
{
|
|
Debug.LogWarning($"[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: timed out waiting for Photon connection. state={PhotonNetwork.NetworkClientState}, inRoom={PhotonNetwork.InRoom}, inLobby={PhotonNetwork.InLobby}.");
|
|
if (currentTeleportShouldCompleteLoading)
|
|
{
|
|
CompleteLoadingScreen();
|
|
}
|
|
FinishTeleportCoroutine();
|
|
yield break;
|
|
}
|
|
yield return null;
|
|
}
|
|
Debug.Log($"[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: Photon connected. InLobby={PhotonNetwork.InLobby}, InRoom={PhotonNetwork.InRoom}.");
|
|
SetLoadingCheckpoint(0.86f);
|
|
yield return new WaitForSecondsRealtime(Mathf.Max(0f, delaySeconds));
|
|
if (!PhotonNetwork.InRoom || PhotonNetwork.LocalPlayer == null)
|
|
{
|
|
Debug.LogWarning("[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: aborting because not in room or local player is missing after delay.");
|
|
if (currentTeleportShouldCompleteLoading && PhotonNetwork.InRoom)
|
|
{
|
|
CompleteLoadingScreen();
|
|
}
|
|
FinishTeleportCoroutine();
|
|
yield break;
|
|
}
|
|
PhotonVRPlayer localPlayer = null;
|
|
float playerWaitStart = Time.realtimeSinceStartup;
|
|
while (localPlayer == null && Time.realtimeSinceStartup - playerWaitStart < 3f)
|
|
{
|
|
PhotonVRPlayer[] array = UnityEngine.Object.FindObjectsByType<PhotonVRPlayer>(FindObjectsInactive.Include);
|
|
foreach (PhotonVRPlayer photonVRPlayer in array)
|
|
{
|
|
if (!(photonVRPlayer == null))
|
|
{
|
|
PhotonView component = photonVRPlayer.GetComponent<PhotonView>();
|
|
if (!(component == null) && component.OwnerActorNr == PhotonNetwork.LocalPlayer.ActorNumber)
|
|
{
|
|
localPlayer = photonVRPlayer;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (localPlayer != null)
|
|
{
|
|
break;
|
|
}
|
|
yield return null;
|
|
}
|
|
if (localPlayer == null)
|
|
{
|
|
Debug.LogWarning($"[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: no local PhotonVRPlayer found after {Time.realtimeSinceStartup - playerWaitStart:F2}s.");
|
|
if (currentTeleportShouldCompleteLoading)
|
|
{
|
|
CompleteLoadingScreen();
|
|
}
|
|
FinishTeleportCoroutine();
|
|
yield break;
|
|
}
|
|
SetLoadingCheckpoint(0.9f);
|
|
Vector3 vector = new Vector3(0f, 5f, 0f);
|
|
Transform localVrTeleportRoot = GetLocalVrTeleportRoot();
|
|
if (localVrTeleportRoot != null)
|
|
{
|
|
MoveTransformTo(localVrTeleportRoot, vector);
|
|
Debug.Log($"[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: teleported VR root '{localVrTeleportRoot.name}' to {vector}.");
|
|
NotifyLoadingPlayerTeleported(currentTeleportShouldCompleteLoading);
|
|
FinishTeleportCoroutine();
|
|
yield break;
|
|
}
|
|
Debug.LogWarning("[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: no VR root found; teleporting local PhotonVRPlayer only.");
|
|
if (localPlayer != null)
|
|
{
|
|
MoveTransformTo(localPlayer.transform, vector);
|
|
Debug.Log($"[VoxelGroupPhotonSync] TeleportLocalPlayerAfterDelay: teleported local PhotonVRPlayer '{localPlayer.name}' to {vector}.");
|
|
NotifyLoadingPlayerTeleported(currentTeleportShouldCompleteLoading);
|
|
}
|
|
FinishTeleportCoroutine();
|
|
}
|
|
|
|
private void FinishTeleportCoroutine()
|
|
{
|
|
worldTeleportCoroutine = null;
|
|
currentTeleportShouldCompleteLoading = false;
|
|
if (pendingWorldTeleportRestart)
|
|
{
|
|
bool completeLoading = pendingTeleportShouldCompleteLoading;
|
|
pendingWorldTeleportRestart = false;
|
|
pendingTeleportShouldCompleteLoading = false;
|
|
ScheduleTeleportAfterWorldLoad(isMaster: true, forceRestart: false, completeLoading);
|
|
}
|
|
}
|
|
|
|
private static void MoveTransformTo(Transform target, Vector3 position)
|
|
{
|
|
if (target == null)
|
|
{
|
|
return;
|
|
}
|
|
CharacterController component = target.GetComponent<CharacterController>();
|
|
if (component != null)
|
|
{
|
|
bool flag = component.enabled;
|
|
component.enabled = false;
|
|
target.position = position;
|
|
component.enabled = flag;
|
|
Rigidbody component2 = target.GetComponent<Rigidbody>();
|
|
if (component2 != null)
|
|
{
|
|
component2.linearVelocity = Vector3.zero;
|
|
component2.angularVelocity = Vector3.zero;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target.position = position;
|
|
Rigidbody component3 = target.GetComponent<Rigidbody>();
|
|
if (component3 != null)
|
|
{
|
|
component3.linearVelocity = Vector3.zero;
|
|
component3.angularVelocity = Vector3.zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Transform GetLocalVrTeleportRoot()
|
|
{
|
|
if (PhotonVRManager.Manager == null || PhotonVRManager.Manager.Head == null)
|
|
{
|
|
return null;
|
|
}
|
|
Transform head = PhotonVRManager.Manager.Head;
|
|
if (head.parent == null)
|
|
{
|
|
return head;
|
|
}
|
|
Transform parent = head.parent;
|
|
while (parent != null)
|
|
{
|
|
if (parent.GetComponent<CharacterController>() != null)
|
|
{
|
|
return parent;
|
|
}
|
|
if (parent.parent == null)
|
|
{
|
|
return parent;
|
|
}
|
|
parent = parent.parent;
|
|
}
|
|
return head;
|
|
}
|
|
|
|
private static string GetBaseWorldName(string roomName)
|
|
{
|
|
string text = (roomName ?? string.Empty).Trim();
|
|
if (string.IsNullOrEmpty(text))
|
|
{
|
|
return text;
|
|
}
|
|
int num = text.LastIndexOf('|');
|
|
if (num <= 0)
|
|
{
|
|
return text;
|
|
}
|
|
string text2 = text.Substring(num + 1);
|
|
if (text2.Length == 3 && Regex.IsMatch(text2, "^[A-Za-z0-9]{3}$"))
|
|
{
|
|
return text.Substring(0, num);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
private IEnumerator WorldResolveAndMaybeLoad()
|
|
{
|
|
Debug.Log("[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: starting world resolve and load.");
|
|
SetLoadingCheckpoint(0.7f);
|
|
if (groupManager == null || !PhotonNetwork.InRoom)
|
|
{
|
|
Debug.LogWarning("[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: aborted because groupManager is null or not in room.");
|
|
yield break;
|
|
}
|
|
string text = ((PhotonNetwork.CurrentRoom != null) ? PhotonNetwork.CurrentRoom.Name : string.Empty);
|
|
Debug.Log("[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: current room name='" + text + "'.");
|
|
SetLoadingCheckpoint(0.72f);
|
|
string baseWorldName = GetBaseWorldName(text);
|
|
if (string.IsNullOrEmpty(baseWorldName))
|
|
{
|
|
Debug.LogWarning("[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: could not determine base world name from room.");
|
|
yield break;
|
|
}
|
|
cachedPlayerId = BlockSpaceAccountPrefs.GetUserFolderId();
|
|
cachedBaseWorldName = baseWorldName;
|
|
EnsureWorldBackendClient();
|
|
WorldBackendClient.ResolveWorldResponse resolve = null;
|
|
SetLoadingCheckpoint(0.74f);
|
|
yield return worldBackendClient.ResolveWorld(baseWorldName, cachedPlayerId, delegate(WorldBackendClient.ResolveWorldResponse r)
|
|
{
|
|
resolve = r;
|
|
});
|
|
if (resolve == null || !resolve.ok)
|
|
{
|
|
Debug.LogWarning("[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: resolve failed. resolve=" + ((resolve == null) ? "null" : resolve.ok.ToString()) + ", worldId=" + ((resolve != null) ? resolve.worldId : "<null>") + ", error=" + ((resolve != null) ? resolve.error : "<null>"));
|
|
groupManager.SetEditingLocked(locked: true);
|
|
ScheduleTeleportAfterWorldLoad(PhotonNetwork.IsMasterClient, forceRestart: false, completeLoading: true);
|
|
yield break;
|
|
}
|
|
currentWorldExists = resolve.exists;
|
|
currentWorldId = resolve.worldId ?? string.Empty;
|
|
isCreatorForCurrentWorld = resolve.isCreator;
|
|
bool flag = string.Equals(cachedBaseWorldName, "Lobby", StringComparison.OrdinalIgnoreCase);
|
|
groupManager.SetEditingLocked(!isCreatorForCurrentWorld && !flag);
|
|
Debug.Log($"[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: resolved baseWorldName='{cachedBaseWorldName}', worldExists={currentWorldExists}, worldId='{currentWorldId}', isCreator={isCreatorForCurrentWorld}.");
|
|
SetLoadingCheckpoint(0.78f);
|
|
StartWorldAutoSaveIfNeeded();
|
|
StartWorldPermissionRefreshIfNeeded();
|
|
Debug.Log("[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: scheduling teleport after world load.");
|
|
ScheduleTeleportAfterWorldLoad(PhotonNetwork.IsMasterClient, forceRestart: false, !PhotonNetwork.IsMasterClient);
|
|
if (!currentWorldExists || string.IsNullOrEmpty(currentWorldId) || !PhotonNetwork.IsMasterClient)
|
|
{
|
|
ScheduleTeleportAfterWorldLoad(PhotonNetwork.IsMasterClient, forceRestart: false, completeLoading: true);
|
|
yield break;
|
|
}
|
|
groupManager.ClearAllGroupsForNetworkSync();
|
|
byte[] gzipBytes = null;
|
|
SetLoadingCheckpoint(0.88f);
|
|
yield return worldBackendClient.DownloadGroups(currentWorldId, delegate(byte[] bytes)
|
|
{
|
|
gzipBytes = bytes;
|
|
});
|
|
if (gzipBytes == null)
|
|
{
|
|
gzipBytes = Array.Empty<byte>();
|
|
}
|
|
if (!WorldGroupsCodec.TryDecodeGzip(gzipBytes, out WorldGroupsCodec.WorldSaveData saveData))
|
|
{
|
|
Debug.LogWarning("[VoxelGroupPhotonSync] Failed to decode world groups; starting with an empty world.");
|
|
ScheduleTeleportAfterWorldLoad(isMaster: true, forceRestart: false, completeLoading: true);
|
|
yield break;
|
|
}
|
|
List<WorldGroupsCodec.WorldGroupRecord> list = saveData.groupRecords ?? new List<WorldGroupsCodec.WorldGroupRecord>();
|
|
Debug.Log($"[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: decoded {list.Count} world group records.");
|
|
SetLoadingCheckpoint(0.91f);
|
|
if (list.Count == 0)
|
|
{
|
|
Debug.Log("[VoxelGroupPhotonSync] WorldResolveAndMaybeLoad: world contains no saved groups.");
|
|
}
|
|
suppressWorldDirty = true;
|
|
try
|
|
{
|
|
Dictionary<int, int> dictionary = new Dictionary<int, int>();
|
|
for (int num = 0; num < list.Count; num++)
|
|
{
|
|
WorldGroupsCodec.WorldGroupRecord worldGroupRecord = list[num];
|
|
VoxelGroupManager.SavedGroupData data = new VoxelGroupManager.SavedGroupData
|
|
{
|
|
position = worldGroupRecord.position,
|
|
packedRotation = worldGroupRecord.packedRotation,
|
|
size = worldGroupRecord.size,
|
|
voxels = worldGroupRecord.voxels
|
|
};
|
|
VoxelGroup voxelGroup = groupManager.SpawnSavedGroup(data, worldGroupRecord.position);
|
|
if (voxelGroup != null)
|
|
{
|
|
dictionary[num] = voxelGroup.id;
|
|
}
|
|
}
|
|
if (nodeGraphManager != null)
|
|
{
|
|
nodeGraphManager.ApplySavedGraph(saveData.nodeRecords, saveData.wireRecords, clearExisting: true, broadcast: false, Vector3.zero, dictionary);
|
|
(nodeGraphManager.GetComponent<NodeWorldDefaultSpawner>() ?? nodeGraphManager.gameObject.AddComponent<NodeWorldDefaultSpawner>()).EnsureDefaultNodesSpawnedIfEmpty(saveData.nodeRecords, PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient);
|
|
}
|
|
worldDirty = false;
|
|
SetLoadingCheckpoint(0.93f);
|
|
}
|
|
finally
|
|
{
|
|
suppressWorldDirty = false;
|
|
}
|
|
WorldBackendClient.VisitResponse visit = null;
|
|
yield return worldBackendClient.IncrementVisit(currentWorldId, delegate(WorldBackendClient.VisitResponse r)
|
|
{
|
|
visit = r;
|
|
});
|
|
SetLoadingCheckpoint(0.94f);
|
|
ScheduleTeleportAfterWorldLoad(isMaster: true, forceRestart: true, completeLoading: true);
|
|
}
|
|
|
|
private static void SetLoadingCheckpoint(float progress)
|
|
{
|
|
((WorldLoadingScreenController.Instance != null) ? WorldLoadingScreenController.Instance : UnityEngine.Object.FindFirstObjectByType<WorldLoadingScreenController>())?.SetCheckpoint(progress);
|
|
}
|
|
|
|
private static void NotifyLoadingPlayerTeleported(bool completeLoading)
|
|
{
|
|
((WorldLoadingScreenController.Instance != null) ? WorldLoadingScreenController.Instance : UnityEngine.Object.FindFirstObjectByType<WorldLoadingScreenController>())?.NotifyLocalPlayerTeleported(completeLoading);
|
|
}
|
|
|
|
private static void CompleteLoadingScreen()
|
|
{
|
|
((WorldLoadingScreenController.Instance != null) ? WorldLoadingScreenController.Instance : UnityEngine.Object.FindFirstObjectByType<WorldLoadingScreenController>())?.CompleteLoading();
|
|
}
|
|
|
|
private void StartWorldPermissionRefreshIfNeeded()
|
|
{
|
|
if (worldPermissionRefreshCoroutine == null && enableWorldPersistence && !string.IsNullOrEmpty(cachedBaseWorldName) && !string.IsNullOrEmpty(cachedPlayerId))
|
|
{
|
|
worldPermissionRefreshCoroutine = StartCoroutine(WorldPermissionRefreshLoop());
|
|
}
|
|
}
|
|
|
|
private IEnumerator WorldPermissionRefreshLoop()
|
|
{
|
|
while (PhotonNetwork.InRoom && enableWorldPersistence && !string.IsNullOrEmpty(cachedBaseWorldName))
|
|
{
|
|
yield return new WaitForSecondsRealtime(Mathf.Clamp(permissionRefreshIntervalSeconds, 2f, 30f));
|
|
if (groupManager == null)
|
|
{
|
|
continue;
|
|
}
|
|
EnsureWorldBackendClient();
|
|
WorldBackendClient.ResolveWorldResponse resolve = null;
|
|
yield return worldBackendClient.ResolveWorld(cachedBaseWorldName, cachedPlayerId, delegate(WorldBackendClient.ResolveWorldResponse r)
|
|
{
|
|
resolve = r;
|
|
});
|
|
if (resolve != null && resolve.ok)
|
|
{
|
|
bool isCreator = resolve.isCreator;
|
|
if (isCreator != isCreatorForCurrentWorld)
|
|
{
|
|
isCreatorForCurrentWorld = isCreator;
|
|
bool flag = string.Equals(cachedBaseWorldName, "Lobby", StringComparison.OrdinalIgnoreCase);
|
|
groupManager.SetEditingLocked(!isCreatorForCurrentWorld && !flag);
|
|
}
|
|
}
|
|
}
|
|
worldPermissionRefreshCoroutine = null;
|
|
}
|
|
|
|
private void StartWorldAutoSaveIfNeeded()
|
|
{
|
|
if (worldAutoSaveCoroutine == null && PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && enableWorldPersistence && currentWorldExists && !string.IsNullOrEmpty(currentWorldId))
|
|
{
|
|
worldAutoSaveCoroutine = StartCoroutine(WorldAutoSaveLoop());
|
|
}
|
|
}
|
|
|
|
private IEnumerator WorldAutoSaveLoop()
|
|
{
|
|
while (PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient && enableWorldPersistence && currentWorldExists && !string.IsNullOrEmpty(currentWorldId))
|
|
{
|
|
yield return new WaitForSecondsRealtime(Mathf.Clamp(autoSaveIntervalSeconds, 5f, 300f));
|
|
if (worldDirty && !suppressWorldDirty && isCreatorForCurrentWorld && !(groupManager == null))
|
|
{
|
|
EnsureWorldBackendClient();
|
|
List<VoxelGroup> persistableWorldGroups = GetPersistableWorldGroups();
|
|
byte[] gzipBytes = WorldGroupsCodec.EncodeGzip(persistableWorldGroups, (nodeGraphManager != null) ? nodeGraphManager.BuildWorldNodeRecords() : null, (nodeGraphManager != null) ? nodeGraphManager.BuildWorldWireRecords(null, BuildGroupSaveIndexMap(persistableWorldGroups)) : null);
|
|
WorldBackendClient.UploadGroupsResponse upload = null;
|
|
yield return worldBackendClient.UploadGroups(currentWorldId, gzipBytes, cachedPlayerId, delegate(WorldBackendClient.UploadGroupsResponse r)
|
|
{
|
|
upload = r;
|
|
});
|
|
if (upload != null && upload.ok && upload.saved)
|
|
{
|
|
worldDirty = false;
|
|
}
|
|
}
|
|
}
|
|
worldAutoSaveCoroutine = null;
|
|
}
|
|
|
|
public IEnumerator ForceSaveNow()
|
|
{
|
|
if (!enableWorldPersistence || !currentWorldExists || string.IsNullOrEmpty(currentWorldId) || !PhotonNetwork.InRoom || !isCreatorForCurrentWorld || groupManager == null)
|
|
{
|
|
yield break;
|
|
}
|
|
EnsureWorldBackendClient();
|
|
List<VoxelGroup> persistableWorldGroups = GetPersistableWorldGroups();
|
|
byte[] gzipBytes = WorldGroupsCodec.EncodeGzip(persistableWorldGroups, (nodeGraphManager != null) ? nodeGraphManager.BuildWorldNodeRecords() : null, (nodeGraphManager != null) ? nodeGraphManager.BuildWorldWireRecords(null, BuildGroupSaveIndexMap(persistableWorldGroups)) : null);
|
|
WorldBackendClient.UploadGroupsResponse upload = null;
|
|
yield return worldBackendClient.UploadGroups(currentWorldId, gzipBytes, cachedPlayerId, delegate(WorldBackendClient.UploadGroupsResponse r)
|
|
{
|
|
upload = r;
|
|
});
|
|
if (upload != null && upload.ok && upload.saved)
|
|
{
|
|
worldDirty = false;
|
|
WorldBackendClient.BroadcastReloadResponse broadcast = null;
|
|
yield return worldBackendClient.BroadcastReload(cachedPlayerId, cachedBaseWorldName, delegate(WorldBackendClient.BroadcastReloadResponse r)
|
|
{
|
|
broadcast = r;
|
|
});
|
|
}
|
|
}
|
|
|
|
private bool CanSendRpc()
|
|
{
|
|
if (!PhotonNetwork.InRoom || cachedPhotonView == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (cachedPhotonView.ViewID == 0)
|
|
{
|
|
Debug.LogWarning("VoxelGroupPhotonSync requires a valid PhotonView ViewID to send RPCs.", this);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool CanSendStrokePreviewRpc(byte voxelValue, float radius)
|
|
{
|
|
if (CanSendRpc() && PhotonNetwork.LocalPlayer != null && radius > 0f)
|
|
{
|
|
return !PackedVoxel.IsAir(voxelValue);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool ShouldIgnoreStrokePreview(int actorNumber, int voxelValue, float radius)
|
|
{
|
|
if (!(groupManager == null) && actorNumber > 0 && (PhotonNetwork.LocalPlayer == null || actorNumber != PhotonNetwork.LocalPlayer.ActorNumber) && voxelValue >= 0 && voxelValue <= 255 && !(radius <= 0f))
|
|
{
|
|
return PackedVoxel.IsAir((byte)voxelValue);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool ShouldIgnoreErasePreview(int actorNumber, float radius)
|
|
{
|
|
if (!(groupManager == null) && actorNumber > 0 && (PhotonNetwork.LocalPlayer == null || actorNumber != PhotonNetwork.LocalPlayer.ActorNumber))
|
|
{
|
|
return radius <= 0f;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private Vector3 ResolveRightHandStrokePoint(int actorNumber, Vector3 fallbackPoint, float forwardOffset)
|
|
{
|
|
if (TryFindRightHand(actorNumber, out var rightHand))
|
|
{
|
|
return rightHand.position + rightHand.rotation * Vector3.forward * forwardOffset;
|
|
}
|
|
return fallbackPoint;
|
|
}
|
|
|
|
private bool TryFindRightHand(int actorNumber, out Transform rightHand)
|
|
{
|
|
if (rightHandCache.TryGetValue(actorNumber, out rightHand) && rightHand != null)
|
|
{
|
|
return true;
|
|
}
|
|
PhotonVRPlayer[] array = UnityEngine.Object.FindObjectsByType<PhotonVRPlayer>(FindObjectsInactive.Include);
|
|
foreach (PhotonVRPlayer photonVRPlayer in array)
|
|
{
|
|
if (photonVRPlayer == null)
|
|
{
|
|
continue;
|
|
}
|
|
PhotonView component = photonVRPlayer.GetComponent<PhotonView>();
|
|
if (!(component == null) && component.OwnerActorNr == actorNumber)
|
|
{
|
|
rightHand = ((photonVRPlayer.RightHand != null) ? photonVRPlayer.RightHand : photonVRPlayer.transform.Find("RightHand"));
|
|
if (!(rightHand != null))
|
|
{
|
|
break;
|
|
}
|
|
rightHandCache[actorNumber] = rightHand;
|
|
return true;
|
|
}
|
|
}
|
|
rightHandCache.Remove(actorNumber);
|
|
rightHand = null;
|
|
return false;
|
|
}
|
|
|
|
private void EndStrokePreview(int actorNumber)
|
|
{
|
|
if (actorNumber > 0 && strokePreviews.TryGetValue(actorNumber, out var value))
|
|
{
|
|
strokePreviews.Remove(actorNumber);
|
|
if (groupManager != null && value.group != null)
|
|
{
|
|
groupManager.DiscardTransientStrokeGroup(value.group);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyErasePreviewSegment(ErasePreviewState state, Vector3 start, Vector3 end)
|
|
{
|
|
if (!(groupManager == null) && state != null)
|
|
{
|
|
float num = Mathf.Max(groupManager.VoxelSize * 0.5f, state.radius * 0.5f);
|
|
float num2 = Vector3.Distance(start, end);
|
|
int num3 = Mathf.Max(1, Mathf.CeilToInt(num2 / num));
|
|
HashSet<VoxelGroup> editedGroups = new HashSet<VoxelGroup>();
|
|
for (int i = 0; i <= num3; i++)
|
|
{
|
|
float t = ((num3 == 0) ? 0f : ((float)i / (float)num3));
|
|
Vector3 vector = Vector3.Lerp(start, end, t);
|
|
SnapshotErasePreviewGroups(state, vector, state.radius);
|
|
groupManager.EraseSpherePreview(vector, state.radius, editedGroups);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void SnapshotErasePreviewGroups(ErasePreviewState state, Vector3 point, float radius)
|
|
{
|
|
List<VoxelGroup> groupsInRadius = groupManager.GetGroupsInRadius(point, radius);
|
|
for (int i = 0; i < groupsInRadius.Count; i++)
|
|
{
|
|
VoxelGroup voxelGroup = groupsInRadius[i];
|
|
if (!(voxelGroup == null) && !state.beforeByGroupId.ContainsKey(voxelGroup.id) && groupManager.TryCaptureGroupData(voxelGroup, out var data))
|
|
{
|
|
state.beforeByGroupId[voxelGroup.id] = data;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EndErasePreview(int actorNumber)
|
|
{
|
|
if (actorNumber <= 0 || !erasePreviews.TryGetValue(actorNumber, out var value))
|
|
{
|
|
return;
|
|
}
|
|
erasePreviews.Remove(actorNumber);
|
|
foreach (KeyValuePair<ushort, VoxelGroupManager.SavedGroupData> item in value.beforeByGroupId)
|
|
{
|
|
groupManager?.TryRestoreTransientGroup(item.Key, item.Value);
|
|
}
|
|
}
|
|
|
|
private void UpdateMovePreviews()
|
|
{
|
|
if (movePreviews.Count == 0 || groupManager == null)
|
|
{
|
|
return;
|
|
}
|
|
float realtimeSinceStartup = Time.realtimeSinceStartup;
|
|
float t = 1f - Mathf.Exp(-18f * Time.deltaTime);
|
|
movePreviewCleanup.Clear();
|
|
foreach (KeyValuePair<ushort, MovePreviewState> movePreview in movePreviews)
|
|
{
|
|
MovePreviewState value = movePreview.Value;
|
|
if (value == null || value.group == null)
|
|
{
|
|
movePreviewCleanup.Add(movePreview.Key);
|
|
continue;
|
|
}
|
|
float num = Mathf.Clamp(realtimeSinceStartup - value.lastPacketTime, 0f, 0.15f);
|
|
Vector3 b = value.targetPosition + value.velocity * num;
|
|
Quaternion rotation = value.group.transform.rotation;
|
|
value.group.transform.SetPositionAndRotation(Vector3.Lerp(value.group.transform.position, b, t), Quaternion.Slerp(rotation, value.targetRotation, t));
|
|
if (realtimeSinceStartup - value.lastPacketTime > 2f)
|
|
{
|
|
movePreviewCleanup.Add(movePreview.Key);
|
|
}
|
|
}
|
|
for (int i = 0; i < movePreviewCleanup.Count; i++)
|
|
{
|
|
EndMovePreview(movePreviewCleanup[i], restoreCollider: true);
|
|
}
|
|
movePreviewCleanup.Clear();
|
|
}
|
|
|
|
private void EndMovePreview(ushort groupId, bool restoreCollider)
|
|
{
|
|
if (movePreviews.Remove(groupId) && restoreCollider && groupManager != null && groupManager.TryGetGroup(groupId, out var group))
|
|
{
|
|
groupManager.SetGroupColliderEnabled(group, enabled: true);
|
|
}
|
|
}
|
|
|
|
private void ClearMovePreviews(bool restoreColliders)
|
|
{
|
|
if (movePreviews.Count != 0)
|
|
{
|
|
movePreviewCleanup.Clear();
|
|
movePreviewCleanup.AddRange(movePreviews.Keys);
|
|
for (int i = 0; i < movePreviewCleanup.Count; i++)
|
|
{
|
|
EndMovePreview(movePreviewCleanup[i], restoreColliders);
|
|
}
|
|
movePreviewCleanup.Clear();
|
|
}
|
|
}
|
|
|
|
private void ClearStrokePreviews()
|
|
{
|
|
if (strokePreviews.Count != 0 || erasePreviews.Count != 0)
|
|
{
|
|
List<int> list = new List<int>(strokePreviews.Keys);
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
EndStrokePreview(list[i]);
|
|
}
|
|
list.Clear();
|
|
list.AddRange(erasePreviews.Keys);
|
|
for (int j = 0; j < list.Count; j++)
|
|
{
|
|
EndErasePreview(list[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsStrokePreviewGroup(VoxelGroup group)
|
|
{
|
|
if (group == null)
|
|
{
|
|
return false;
|
|
}
|
|
foreach (StrokePreviewState value in strokePreviews.Values)
|
|
{
|
|
if (value.group == group)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static int GetActorNumberFromGroupId(int groupId)
|
|
{
|
|
return (groupId >> 8) & 0xFF;
|
|
}
|
|
}
|
|
}
|