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 cells = new HashSet(); } private sealed class ErasePreviewState { public float radius; public float forwardOffset; public Vector3 lastPoint; public readonly Dictionary beforeByGroupId = new Dictionary(); } 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 syncBuffer = new List(); private readonly List persistBuffer = new List(); private readonly Dictionary strokePreviews = new Dictionary(); private readonly Dictionary erasePreviews = new Dictionary(); private readonly Dictionary rightHandCache = new Dictionary(); private readonly Dictionary movePreviews = new Dictionary(); private readonly List movePreviewCleanup = new List(); 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(); nodeGraphManager = GetComponent(); } private void Awake() { cachedPhotonView = GetComponent(); if (groupManager == null) { groupManager = GetComponent(); if (groupManager == null) { groupManager = UnityEngine.Object.FindAnyObjectByType(); } } if (nodeGraphManager == null) { nodeGraphManager = GetComponent(); if (nodeGraphManager == null) { nodeGraphManager = UnityEngine.Object.FindAnyObjectByType(); } } 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 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 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 BuildGroupSaveIndexMap(IList groups) { Dictionary dictionary = new Dictionary(); 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(FindObjectsInactive.Include); foreach (PhotonVRPlayer photonVRPlayer in array) { if (!(photonVRPlayer == null)) { PhotonView component = photonVRPlayer.GetComponent(); 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(); if (component != null) { bool flag = component.enabled; component.enabled = false; target.position = position; component.enabled = flag; Rigidbody component2 = target.GetComponent(); if (component2 != null) { component2.linearVelocity = Vector3.zero; component2.angularVelocity = Vector3.zero; } } else { target.position = position; Rigidbody component3 = target.GetComponent(); 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() != 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 : "") + ", error=" + ((resolve != null) ? resolve.error : "")); 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(); } 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 list = saveData.groupRecords ?? new List(); 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 dictionary = new Dictionary(); 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() ?? nodeGraphManager.gameObject.AddComponent()).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())?.SetCheckpoint(progress); } private static void NotifyLoadingPlayerTeleported(bool completeLoading) { ((WorldLoadingScreenController.Instance != null) ? WorldLoadingScreenController.Instance : UnityEngine.Object.FindFirstObjectByType())?.NotifyLocalPlayerTeleported(completeLoading); } private static void CompleteLoadingScreen() { ((WorldLoadingScreenController.Instance != null) ? WorldLoadingScreenController.Instance : UnityEngine.Object.FindFirstObjectByType())?.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 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 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(FindObjectsInactive.Include); foreach (PhotonVRPlayer photonVRPlayer in array) { if (photonVRPlayer == null) { continue; } PhotonView component = photonVRPlayer.GetComponent(); 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 editedGroups = new HashSet(); 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 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 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 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 list = new List(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; } } }