mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-07 16:49:17 +00:00
429 lines
No EOL
16 KiB
C#
429 lines
No EOL
16 KiB
C#
using ABI_RC.Core.IO;
|
|
using ABI_RC.Core.Networking.IO.Instancing;
|
|
using ABI_RC.Core.Player;
|
|
using ABI_RC.Core.Savior;
|
|
using ABI_RC.Core.UI;
|
|
using ABI_RC.Systems.GameEventSystem;
|
|
using ABI_RC.Systems.Gravity;
|
|
using NAK.ShareBubbles.Impl;
|
|
using NAK.ShareBubbles.Networking;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace NAK.ShareBubbles;
|
|
|
|
public class ShareBubbleManager
|
|
{
|
|
#region Constants
|
|
|
|
public const int MaxBubblesPerUser = 3;
|
|
private const float BubbleCreationCooldown = 1f;
|
|
|
|
#endregion Constants
|
|
|
|
#region Singleton
|
|
|
|
public static ShareBubbleManager Instance { get; private set; }
|
|
|
|
public static void Initialize()
|
|
{
|
|
if (Instance != null)
|
|
return;
|
|
|
|
Instance = new ShareBubbleManager();
|
|
RegisterDefaultBubbleTypes();
|
|
|
|
CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(Instance.OnPlayerSetupStart);
|
|
}
|
|
|
|
private static void RegisterDefaultBubbleTypes()
|
|
{
|
|
ShareBubbleRegistry.RegisterBubbleType(GetMaskedHash("Avatar"), () => new AvatarBubbleImpl());
|
|
ShareBubbleRegistry.RegisterBubbleType(GetMaskedHash("Spawnable"), () => new SpawnableBubbleImpl());
|
|
}
|
|
|
|
public static uint GetMaskedHash(string typeName) => StringToHash(typeName) & 0xFFFF;
|
|
|
|
public static uint GenerateBubbleId(string content, uint implTypeHash)
|
|
{
|
|
// Allows us to extract the implementation type hash from the bubble ID
|
|
return (implTypeHash << 16) | GetMaskedHash(content);
|
|
}
|
|
|
|
private static uint StringToHash(string str)
|
|
{
|
|
unchecked
|
|
{
|
|
uint hash = 5381;
|
|
foreach (var ch in str) hash = ((hash << 5) + hash) + ch;
|
|
return hash;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Fields
|
|
|
|
private class PlayerBubbleData
|
|
{
|
|
// Can only ever be true when the batched bubble message is what created the player data
|
|
public bool IgnoreBubbleCreationCooldown;
|
|
public float LastBubbleCreationTime;
|
|
public readonly Dictionary<uint, ShareBubble> BubblesByLocalId = new();
|
|
}
|
|
|
|
private readonly Dictionary<string, PlayerBubbleData> playerBubbles = new();
|
|
|
|
public bool IsPlacingBubbleMode { get; set; }
|
|
private ShareBubbleData selectedBubbleData;
|
|
|
|
#endregion Fields
|
|
|
|
#region Game Events
|
|
|
|
private void OnPlayerSetupStart()
|
|
{
|
|
CVRGameEventSystem.Instance.OnConnected.AddListener(OnConnected);
|
|
CVRGameEventSystem.Player.OnLeaveEntity.AddListener(OnPlayerLeft);
|
|
}
|
|
|
|
private void OnConnected(string _)
|
|
{
|
|
if (Instances.IsReconnecting)
|
|
return;
|
|
|
|
// Clear all bubbles on disconnect (most should have died during world unload)
|
|
foreach (PlayerBubbleData playerData in playerBubbles.Values)
|
|
{
|
|
foreach (ShareBubble bubble in playerData.BubblesByLocalId.Values.ToList())
|
|
DestroyBubble(bubble);
|
|
}
|
|
playerBubbles.Clear();
|
|
|
|
// This also acts to signal to other clients we rejoined and need our bubbles cleared
|
|
ModNetwork.SendActiveBubblesRequest();
|
|
}
|
|
|
|
private void OnPlayerLeft(CVRPlayerEntity player)
|
|
{
|
|
if (!playerBubbles.TryGetValue(player.Uuid, out PlayerBubbleData playerData))
|
|
return;
|
|
|
|
foreach (ShareBubble bubble in playerData.BubblesByLocalId.Values.ToList())
|
|
DestroyBubble(bubble);
|
|
|
|
playerBubbles.Remove(player.Uuid);
|
|
}
|
|
|
|
#endregion Game Events
|
|
|
|
#region Local Operations
|
|
|
|
public void DropBubbleInFront(ShareBubbleData data)
|
|
{
|
|
PlayerSetup localPlayer = PlayerSetup.Instance;
|
|
string localPlayerId = MetaPort.Instance.ownerId;
|
|
|
|
if (!CanPlayerCreateBubble(localPlayerId))
|
|
{
|
|
ShareBubblesMod.Logger.Msg("Bubble creation on cooldown!");
|
|
return;
|
|
}
|
|
|
|
float playSpaceScale = localPlayer.GetPlaySpaceScale();
|
|
Vector3 playerForward = localPlayer.GetPlayerForward();
|
|
Vector3 position = localPlayer.activeCam.transform.position + playerForward * 0.5f * playSpaceScale;
|
|
|
|
if (Physics.Raycast(position,
|
|
localPlayer.CharacterController.GetGravityDirection(),
|
|
out RaycastHit raycastHit, 4f, localPlayer.dropPlacementMask))
|
|
{
|
|
CreateBubbleForPlayer(localPlayerId, raycastHit.point,
|
|
Quaternion.LookRotation(playerForward, raycastHit.normal), data);
|
|
return;
|
|
}
|
|
|
|
CreateBubbleForPlayer(localPlayerId, position,
|
|
Quaternion.LookRotation(playerForward, -localPlayer.transform.up), data);
|
|
}
|
|
|
|
public void SelectBubbleForPlace(string contentCouiPath, string contentName, ShareBubbleData data)
|
|
{
|
|
selectedBubbleData = data;
|
|
IsPlacingBubbleMode = true;
|
|
CohtmlHud.Instance.SelectPropToSpawn(
|
|
contentCouiPath,
|
|
contentName,
|
|
"Selected content to bubble:");
|
|
}
|
|
|
|
public void PlaceSelectedBubbleFromControllerRay(Transform transform)
|
|
{
|
|
Vector3 position = transform.position;
|
|
Vector3 forward = transform.forward;
|
|
|
|
// Every layer other than IgnoreRaycast, PlayerLocal, PlayerClone, PlayerNetwork, and UI Internal
|
|
const int LayerMask = ~((1 << 2) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 15));
|
|
if (!Physics.Raycast(position, forward, out RaycastHit hit,
|
|
10f, LayerMask, QueryTriggerInteraction.Ignore))
|
|
return; // No hit
|
|
|
|
if (selectedBubbleData.ImplTypeHash == 0)
|
|
return;
|
|
|
|
Vector3 bubblePos = hit.point;
|
|
|
|
PlayerSetup localPlayer = PlayerSetup.Instance;
|
|
Vector3 playerPos = localPlayer.GetPlayerPosition();
|
|
|
|
// Sample gravity at bubble position to get bubble orientation
|
|
Vector3 bubbleUp = -GravitySystem.TryGetResultingGravity(bubblePos, false).AppliedGravity.normalized;
|
|
Vector3 bubbleForward = Vector3.ProjectOnPlane((playerPos - bubblePos).normalized, bubbleUp);
|
|
|
|
// Bubble faces player aligned with gravity
|
|
Quaternion bubbleRot = Quaternion.LookRotation(bubbleForward, bubbleUp);
|
|
|
|
CreateBubbleForPlayer(MetaPort.Instance.ownerId, bubblePos, bubbleRot, selectedBubbleData);
|
|
}
|
|
|
|
#endregion Local Operations
|
|
|
|
#region Player Callbacks
|
|
|
|
private void CreateBubbleForPlayer(
|
|
string playerId,
|
|
Vector3 position,
|
|
Quaternion rotation,
|
|
ShareBubbleData data)
|
|
{
|
|
GameObject bubbleRootObject = null;
|
|
try
|
|
{
|
|
if (!CanPlayerCreateBubble(playerId))
|
|
return;
|
|
|
|
if (!ShareBubbleRegistry.TryCreateImplementation(data.ImplTypeHash, out IShareBubbleImpl impl))
|
|
{
|
|
Debug.LogError($"Failed to create bubble: Unknown bubble type hash: {data.ImplTypeHash}");
|
|
return;
|
|
}
|
|
|
|
// Get or create player data
|
|
if (!playerBubbles.TryGetValue(playerId, out PlayerBubbleData playerData))
|
|
{
|
|
playerData = new PlayerBubbleData();
|
|
playerBubbles[playerId] = playerData;
|
|
}
|
|
|
|
// If a bubble with this ID already exists for this player, destroy it
|
|
if (playerData.BubblesByLocalId.TryGetValue(data.BubbleId, out ShareBubble existingBubble))
|
|
{
|
|
ShareBubblesMod.Logger.Msg($"Replacing existing bubble: {data.BubbleId}");
|
|
DestroyBubble(existingBubble);
|
|
}
|
|
// Check bubble limit only if we're not replacing an existing bubble
|
|
else if (playerData.BubblesByLocalId.Count >= MaxBubblesPerUser)
|
|
{
|
|
ShareBubble oldestBubble = playerData.BubblesByLocalId.Values
|
|
.OrderBy(b => b.Data.CreatedAt)
|
|
.First();
|
|
ShareBubblesMod.Logger.Msg($"Bubble limit reached, destroying oldest bubble: {oldestBubble.Data.BubbleId}");
|
|
DestroyBubble(oldestBubble);
|
|
}
|
|
|
|
bubbleRootObject = Object.Instantiate(ShareBubblesMod.SharingBubblePrefab);
|
|
bubbleRootObject.transform.SetLocalPositionAndRotation(position, rotation);
|
|
bubbleRootObject.SetActive(true);
|
|
|
|
// Move to Additive scene to prevent being hit by shader replacement on VR switch
|
|
SceneManager.MoveGameObjectToScene(bubbleRootObject, SceneManager.GetSceneByName(CVRObjectLoader.AdditiveContentSceneName));
|
|
|
|
Transform bubbleTransform = bubbleRootObject.transform.GetChild(0);
|
|
if (bubbleTransform == null)
|
|
throw new InvalidOperationException("Bubble prefab is missing expected child transform");
|
|
|
|
(float targetHeight, float scaleModifier) = GetBubbleHeightAndScale();
|
|
bubbleTransform.localPosition = new Vector3(0f, targetHeight, 0f);
|
|
bubbleTransform.localScale = new Vector3(scaleModifier, scaleModifier, scaleModifier);
|
|
|
|
ShareBubble bubble = bubbleRootObject.GetComponent<ShareBubble>();
|
|
if (bubble == null)
|
|
throw new InvalidOperationException("Bubble prefab is missing ShareBubble component");
|
|
|
|
bubble.OwnerId = playerId;
|
|
bubble.Initialize(data, impl);
|
|
playerData.BubblesByLocalId[data.BubbleId] = bubble;
|
|
playerData.LastBubbleCreationTime = Time.time;
|
|
|
|
if (playerId == MetaPort.Instance.ownerId)
|
|
ModNetwork.SendBubbleCreated(position, rotation, data);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogError($"Failed to create bubble (ID: {data.BubbleId}, Owner: {playerId}): {ex}");
|
|
|
|
// Clean up the partially created bubble if it exists
|
|
if (bubbleRootObject != null)
|
|
{
|
|
Object.DestroyImmediate(bubbleRootObject);
|
|
|
|
// Remove from player data if it was added
|
|
if (playerBubbles.TryGetValue(playerId, out PlayerBubbleData playerData))
|
|
playerData.BubblesByLocalId.Remove(data.BubbleId);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void DestroyBubble(ShareBubble bubble)
|
|
{
|
|
if (bubble == null) return;
|
|
bubble.IsDestroyed = true; // Prevents ShareBubble.OnDestroy invoking OnBubbleDestroyed
|
|
Object.DestroyImmediate(bubble.gameObject);
|
|
OnBubbleDestroyed(bubble.OwnerId, bubble.Data.BubbleId);
|
|
}
|
|
|
|
public void OnBubbleDestroyed(string ownerId, uint bubbleId)
|
|
{
|
|
if (playerBubbles.TryGetValue(ownerId, out PlayerBubbleData playerData))
|
|
playerData.BubblesByLocalId.Remove(bubbleId);
|
|
|
|
if (ownerId == MetaPort.Instance.ownerId) ModNetwork.SendBubbleDestroyed(bubbleId);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Remote Events
|
|
|
|
public void SetRemoteBatchCreateBubbleState(string ownerId, bool batchCreateBubbleState)
|
|
{
|
|
// Get or create player data
|
|
if (!playerBubbles.TryGetValue(ownerId, out PlayerBubbleData playerData))
|
|
{
|
|
playerData = new PlayerBubbleData();
|
|
playerBubbles[ownerId] = playerData;
|
|
|
|
// If player data didn't exist, ignore bubble creation cooldown for the time we create active bubbles on join
|
|
playerData.IgnoreBubbleCreationCooldown = batchCreateBubbleState;
|
|
}
|
|
else
|
|
{
|
|
// If player data already exists, ignore batched bubble creation
|
|
// Probably makes a race condition here, but it's not critical
|
|
// This will prevent users from using the batched bubble creation when they've already created bubbles
|
|
playerData.IgnoreBubbleCreationCooldown = false;
|
|
}
|
|
}
|
|
|
|
public void OnRemoteBubbleCreated(string ownerId, Vector3 position, Vector3 rotation, ShareBubbleData data)
|
|
{
|
|
CreateBubbleForPlayer(ownerId, position, Quaternion.Euler(rotation), data);
|
|
}
|
|
|
|
public void OnRemoteBubbleDestroyed(string ownerId, uint bubbleId)
|
|
{
|
|
if (!playerBubbles.TryGetValue(ownerId, out PlayerBubbleData playerData) ||
|
|
!playerData.BubblesByLocalId.TryGetValue(bubbleId, out ShareBubble bubble))
|
|
return;
|
|
|
|
DestroyBubble(bubble);
|
|
}
|
|
|
|
public void OnRemoteBubbleMoved(string ownerId, uint bubbleId, Vector3 position, Vector3 rotation)
|
|
{
|
|
if (!playerBubbles.TryGetValue(ownerId, out PlayerBubbleData playerData) ||
|
|
!playerData.BubblesByLocalId.TryGetValue(bubbleId, out ShareBubble bubble))
|
|
return;
|
|
|
|
// TODO: fix
|
|
bubble.transform.parent.SetPositionAndRotation(position, Quaternion.Euler(rotation));
|
|
}
|
|
|
|
public void OnRemoteBubbleClaimRequest(string requesterUserId, uint bubbleId)
|
|
{
|
|
// Get our bubble data if exists, respond to requester
|
|
string ownerId = MetaPort.Instance.ownerId;
|
|
if (!playerBubbles.TryGetValue(ownerId, out PlayerBubbleData playerData) ||
|
|
!playerData.BubblesByLocalId.TryGetValue(bubbleId, out ShareBubble bubble))
|
|
return;
|
|
|
|
bubble.OnRemoteWantsClaim(requesterUserId);
|
|
}
|
|
|
|
public void OnRemoteBubbleClaimResponse(string ownerId, uint bubbleId, bool wasAccepted)
|
|
{
|
|
// Get senders bubble data if exists, receive response
|
|
if (!playerBubbles.TryGetValue(ownerId, out PlayerBubbleData playerData) ||
|
|
!playerData.BubblesByLocalId.TryGetValue(bubbleId, out ShareBubble bubble))
|
|
return;
|
|
|
|
bubble.OnClaimResponseReceived(wasAccepted);
|
|
}
|
|
|
|
public void OnRemoteActiveBubbleRequest(string requesterUserId)
|
|
{
|
|
// Clear all bubbles for requester (as this msg is sent by them on initial join)
|
|
// This catches the case where they rejoin faster than we heard their leave event
|
|
if (playerBubbles.TryGetValue(requesterUserId, out PlayerBubbleData playerData))
|
|
{
|
|
foreach (ShareBubble bubble in playerData.BubblesByLocalId.Values.ToList())
|
|
DestroyBubble(bubble);
|
|
}
|
|
|
|
var myBubbles = GetOwnActiveBubbles();
|
|
if (myBubbles.Count == 0)
|
|
return; // No bubbles to send
|
|
|
|
// Send all active bubbles to requester
|
|
ModNetwork.SendActiveBubblesResponse(requesterUserId, myBubbles);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Utility Methods
|
|
|
|
private bool CanPlayerCreateBubble(string playerId)
|
|
{
|
|
if (!playerBubbles.TryGetValue(playerId, out PlayerBubbleData playerData))
|
|
return true;
|
|
|
|
if (playerData.IgnoreBubbleCreationCooldown)
|
|
return true; // Only ignore for the time we create active bubbles on join
|
|
|
|
return Time.time - playerData.LastBubbleCreationTime >= BubbleCreationCooldown;
|
|
}
|
|
|
|
private static (float, float) GetBubbleHeightAndScale()
|
|
{
|
|
float targetHeight = 0.5f * PlayerSetup.Instance.GetAvatarHeight();
|
|
float scaleModifier = PlayerSetup.Instance.GetPlaySpaceScale() * 1.8f;
|
|
return (targetHeight, scaleModifier);
|
|
}
|
|
|
|
public void OnPlayerScaleChanged()
|
|
{
|
|
(float targetHeight, float scaleModifier) = GetBubbleHeightAndScale();
|
|
|
|
foreach (PlayerBubbleData playerData in playerBubbles.Values)
|
|
{
|
|
foreach (ShareBubble bubble in playerData.BubblesByLocalId.Values)
|
|
{
|
|
if (bubble == null) continue;
|
|
Transform bubbleOffset = bubble.transform.GetChild(0);
|
|
Vector3 localPos = bubbleOffset.localPosition;
|
|
bubbleOffset.localPosition = new Vector3(localPos.x, targetHeight, localPos.z);
|
|
bubbleOffset.localScale = new Vector3(scaleModifier, scaleModifier, scaleModifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<ShareBubble> GetOwnActiveBubbles()
|
|
{
|
|
string ownerId = MetaPort.Instance.ownerId;
|
|
return playerBubbles.TryGetValue(ownerId, out PlayerBubbleData playerData)
|
|
? playerData.BubblesByLocalId.Values.ToList()
|
|
: new List<ShareBubble>();
|
|
}
|
|
|
|
#endregion
|
|
} |