[ShareBubbles] Fixes for 2025r180

This commit is contained in:
NotAKidoS 2025-08-19 23:33:53 -05:00
parent 13e206cd58
commit a0a859aa86
14 changed files with 60 additions and 1953 deletions

View file

@ -28,7 +28,7 @@ public class ShareBubblesMod : MelonMod
LoadAssetBundle();
}
public override void OnApplicationQuit()
{
ModNetwork.Unsubscribe();

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,7 @@ using NAK.ShareBubbles.Properties;
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ShareBubbles"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonGame("ChilloutVR", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 246, 25, 99)] // red-pink
@ -27,6 +27,6 @@ using NAK.ShareBubbles.Properties;
namespace NAK.ShareBubbles.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.5";
public const string Version = "1.1.6";
public const string Author = "NotAKidoS, Exterrata, Noachi, RaidShadowLily, Tejler";
}

View file

@ -1,66 +0,0 @@
using System.Net;
namespace NAK.ShareBubbles.API.Exceptions;
public class ShareApiException : Exception
{
public HttpStatusCode StatusCode { get; } // TODO: network back status code to claiming client, to show why the request failed
public string UserFriendlyMessage { get; }
public ShareApiException(HttpStatusCode statusCode, string message, string userFriendlyMessage)
: base(message)
{
StatusCode = statusCode;
UserFriendlyMessage = userFriendlyMessage;
}
}
public class ContentNotSharedException : ShareApiException
{
public ContentNotSharedException(string contentId)
: base(HttpStatusCode.BadRequest,
$"Content {contentId} is not currently shared",
"This content is not currently shared with anyone")
{
}
}
public class ContentNotFoundException : ShareApiException
{
public ContentNotFoundException(string contentId)
: base(HttpStatusCode.NotFound,
$"Content {contentId} not found",
"The specified content could not be found")
{
}
}
public class UserOnlyAllowsSharesFromFriendsException : ShareApiException
{
public UserOnlyAllowsSharesFromFriendsException(string userId)
: base(HttpStatusCode.Forbidden,
$"User {userId} only accepts shares from friends",
"This user only accepts shares from friends")
{
}
}
public class UserNotFoundException : ShareApiException
{
public UserNotFoundException(string userId)
: base(HttpStatusCode.NotFound,
$"User {userId} not found",
"The specified user could not be found")
{
}
}
public class ContentAlreadySharedException : ShareApiException
{
public ContentAlreadySharedException(string contentId, string userId)
: base(HttpStatusCode.Conflict,
$"Content {contentId} is already shared with user {userId}",
"This content is already shared with this user")
{
}
}

View file

@ -1,6 +1,5 @@
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.API.Responses;
using NAK.ShareBubbles.API.Responses;
namespace NAK.ShareBubbles.API;
@ -34,6 +33,8 @@ public static class PedestalInfoBatchProcessor
{ PedestalType.Prop, false }
};
// This breaks compile accepting this change.
// ReSharper disable once ChangeFieldTypeToSystemThreadingLock
private static readonly object _lock = new();
private const float BATCH_DELAY = 2f;

View file

@ -1,22 +0,0 @@
using Newtonsoft.Json;
namespace NAK.ShareBubbles.API.Responses;
public class ActiveSharesResponse
{
[JsonProperty("value")]
public List<ShareUser> Value { get; set; }
}
// Idk why not just reuse UserDetails
public class ShareUser
{
[JsonProperty("image")]
public string Image { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}

View file

@ -1,237 +0,0 @@
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web;
using ABI_RC.Core.Networking;
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.API.Responses;
using ABI_RC.Core.Savior;
using NAK.ShareBubbles.API.Exceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace NAK.ShareBubbles.API;
/// <summary>
/// API for content sharing management.
/// </summary>
public static class ShareApiHelper
{
#region Enums
public enum ShareContentType
{
Avatar,
Spawnable
}
private enum ShareApiOperation
{
ShareAvatar,
ReleaseAvatar,
ShareSpawnable,
ReleaseSpawnable,
GetAvatarShares,
GetSpawnableShares
}
#endregion Enums
#region Public API
/// <summary>
/// Shares content with a specified user.
/// </summary>
/// <param name="type">Type of content to share</param>
/// <param name="contentId">ID of the content</param>
/// <param name="userId">Target user ID</param>
/// <returns>Response containing share information</returns>
/// <exception cref="ShareApiException">Thrown when API request fails</exception>
/// <exception cref="UserNotFoundException">Thrown when target user is not found</exception>
/// <exception cref="ContentNotFoundException">Thrown when content is not found</exception>
/// <exception cref="UserOnlyAllowsSharesFromFriendsException">Thrown when user only accepts shares from friends</exception>
/// <exception cref="ContentAlreadySharedException">Thrown when content is already shared with user</exception>
public static Task<BaseResponse<T>> ShareContentAsync<T>(ShareContentType type, string contentId, string userId)
{
ShareApiOperation operation = type == ShareContentType.Avatar
? ShareApiOperation.ShareAvatar
: ShareApiOperation.ShareSpawnable;
ShareRequest data = new()
{
ContentId = contentId,
UserId = userId
};
return MakeApiRequestAsync<T>(operation, data);
}
/// <summary>
/// Releases shared content from a specified user.
/// </summary>
/// <param name="type">Type of content to release</param>
/// <param name="contentId">ID of the content</param>
/// <param name="userId">Optional user ID. If null, releases share from self</param>
/// <returns>Response indicating success</returns>
/// <exception cref="ShareApiException">Thrown when API request fails</exception>
/// <exception cref="ContentNotSharedException">Thrown when content is not shared</exception>
/// <exception cref="ContentNotFoundException">Thrown when content is not found</exception>
/// <exception cref="UserNotFoundException">Thrown when specified user is not found</exception>
public static Task<BaseResponse<T>> ReleaseShareAsync<T>(ShareContentType type, string contentId, string userId = null)
{
ShareApiOperation operation = type == ShareContentType.Avatar
? ShareApiOperation.ReleaseAvatar
: ShareApiOperation.ReleaseSpawnable;
// If no user ID is provided, release share from self
userId ??= MetaPort.Instance.ownerId;
ShareRequest data = new()
{
ContentId = contentId,
UserId = userId
};
return MakeApiRequestAsync<T>(operation, data);
}
/// <summary>
/// Gets all shares for specified content.
/// </summary>
/// <param name="type">Type of content</param>
/// <param name="contentId">ID of the content</param>
/// <returns>Response containing share information</returns>
/// <exception cref="ShareApiException">Thrown when API request fails</exception>
/// <exception cref="ContentNotFoundException">Thrown when content is not found</exception>
public static Task<BaseResponse<T>> GetSharesAsync<T>(ShareContentType type, string contentId)
{
ShareApiOperation operation = type == ShareContentType.Avatar
? ShareApiOperation.GetAvatarShares
: ShareApiOperation.GetSpawnableShares;
ShareRequest data = new() { ContentId = contentId };
return MakeApiRequestAsync<T>(operation, data);
}
#endregion Public API
#region Private Implementation
[Serializable]
private record ShareRequest
{
public string ContentId { get; set; }
public string UserId { get; set; }
}
private static async Task<BaseResponse<T>> MakeApiRequestAsync<T>(ShareApiOperation operation, ShareRequest data)
{
ValidateAuthenticationState();
(string endpoint, HttpMethod method) = GetApiEndpointAndMethod(operation, data);
using HttpRequestMessage request = CreateHttpRequest(endpoint, method, data);
try
{
using HttpResponseMessage response = await ApiConnection._client.SendAsync(request);
string content = await response.Content.ReadAsStringAsync();
return HandleApiResponse<T>(response, content, data.ContentId, data.UserId);
}
catch (HttpRequestException ex)
{
throw new ShareApiException(
HttpStatusCode.ServiceUnavailable,
$"Failed to communicate with the server: {ex.Message}",
"Unable to connect to the server. Please check your internet connection.");
}
catch (JsonException ex)
{
throw new ShareApiException(
HttpStatusCode.UnprocessableEntity,
$"Failed to process response data: {ex.Message}",
"Server returned invalid data. Please try again later.");
}
}
private static void ValidateAuthenticationState()
{
if (!AuthManager.IsAuthenticated)
{
throw new ShareApiException(
HttpStatusCode.Unauthorized,
"User is not authenticated",
"Please log in to perform this action");
}
}
private static HttpRequestMessage CreateHttpRequest(string endpoint, HttpMethod method, ShareRequest data)
{
HttpRequestMessage request = new(method, endpoint);
if (method == HttpMethod.Post)
{
JObject json = JObject.FromObject(data);
request.Content = new StringContent(json.ToString(), Encoding.UTF8, "application/json");
}
return request;
}
private static BaseResponse<T> HandleApiResponse<T>(HttpResponseMessage response, string content, string contentId, string userId)
{
if (response.IsSuccessStatusCode)
return CreateSuccessResponse<T>(content);
// Let specific exceptions propagate up to the caller
throw response.StatusCode switch
{
HttpStatusCode.BadRequest => new ContentNotSharedException(contentId),
HttpStatusCode.NotFound when userId != null => new UserNotFoundException(userId),
HttpStatusCode.NotFound => new ContentNotFoundException(contentId),
HttpStatusCode.Forbidden => new UserOnlyAllowsSharesFromFriendsException(userId),
HttpStatusCode.Conflict => new ContentAlreadySharedException(contentId, userId),
_ => new ShareApiException(
response.StatusCode,
$"API request failed with status {response.StatusCode}: {content}",
"An unexpected error occurred. Please try again later.")
};
}
private static BaseResponse<T> CreateSuccessResponse<T>(string content)
{
var response = new BaseResponse<T>("")
{
IsSuccessStatusCode = true,
HttpStatusCode = HttpStatusCode.OK
};
if (!string.IsNullOrEmpty(content))
{
response.Data = JsonConvert.DeserializeObject<T>(content);
}
return response;
}
private static (string endpoint, HttpMethod method) GetApiEndpointAndMethod(ShareApiOperation operation, ShareRequest data)
{
string baseUrl = $"{ApiConnection.APIAddress}/{ApiConnection.APIVersion}";
string encodedContentId = HttpUtility.UrlEncode(data.ContentId);
return operation switch
{
ShareApiOperation.GetAvatarShares => ($"{baseUrl}/avatars/{encodedContentId}/shares", HttpMethod.Get),
ShareApiOperation.ShareAvatar => ($"{baseUrl}/avatars/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Post),
ShareApiOperation.ReleaseAvatar => ($"{baseUrl}/avatars/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Delete),
ShareApiOperation.GetSpawnableShares => ($"{baseUrl}/spawnables/{encodedContentId}/shares", HttpMethod.Get),
ShareApiOperation.ShareSpawnable => ($"{baseUrl}/spawnables/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Post),
ShareApiOperation.ReleaseSpawnable => ($"{baseUrl}/spawnables/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Delete),
_ => throw new ArgumentException($"Unknown operation: {operation}")
};
}
#endregion Private Implementation
}

View file

@ -1,10 +1,10 @@
using ABI_RC.Core.EventSystem;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.IO;
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.API.Exceptions;
using ABI_RC.Core.Networking.API.UserWebsocket;
using ABI_RC.Core.Savior;
using NAK.ShareBubbles.API;
using NAK.ShareBubbles.API.Exceptions;
using ShareBubbles.ShareBubbles.Implementation;
using UnityEngine;
using Object = UnityEngine.Object;

View file

@ -1,10 +1,10 @@
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.IO;
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.API.Exceptions;
using ABI_RC.Core.Networking.API.UserWebsocket;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using NAK.ShareBubbles.API;
using NAK.ShareBubbles.API.Exceptions;
using ShareBubbles.ShareBubbles.Implementation;
using UnityEngine;
using Object = UnityEngine.Object;

View file

@ -1,8 +1,8 @@
using ABI_RC.Core.Networking.API.UserWebsocket;
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.API.UserWebsocket;
using ABI_RC.Core.Networking.IO.Instancing;
using ABI_RC.Core.Player;
using ABI_RC.Systems.GameEventSystem;
using NAK.ShareBubbles.API;
using Newtonsoft.Json;
using UnityEngine;

View file

@ -1,7 +1,7 @@
using ABI_RC.Core.Networking.IO.Social;
using ABI_RC.Core.Networking.API;
using ABI_RC.Core.Networking.IO.Social;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.ModNetwork;
using NAK.ShareBubbles.API;
using UnityEngine;
namespace NAK.ShareBubbles.Networking;

View file

@ -1,5 +1,5 @@
using ABI_RC.Systems.ModNetwork;
using NAK.ShareBubbles.API;
using ABI_RC.Core.Networking.API;
using ABI_RC.Systems.ModNetwork;
using UnityEngine;
namespace NAK.ShareBubbles.Networking;

View file

@ -1,6 +1,8 @@
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.InteractionSystem.Base;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.InputManagement;
using UnityEngine;
namespace NAK.ShareBubbles.UI;
@ -9,11 +11,23 @@ namespace NAK.ShareBubbles.UI;
// Must be added manually by ShareBubble creation...
public class BubbleInteract : Interactable
{
public override bool IsInteractableWithinRange(Vector3 sourcePos)
public override bool IsInteractable
{
return Vector3.Distance(transform.position, sourcePos) < 1.5f;
get
{
if (ViewManager.Instance.IsAnyMenuOpen)
return true;
if (!MetaPort.Instance.isUsingVr
&& CVRInputManager.Instance.unlockMouse)
return true;
return false;
}
}
public override bool IsInteractableWithinRange(Vector3 sourcePos) => true;
public override void OnInteractDown(InteractionContext context, ControllerRay controllerRay)
{
// Not used

View file

@ -1,9 +1,9 @@
{
"_id": 244,
"name": "ShareBubbles",
"modversion": "1.0.5",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"modversion": "1.1.6",
"gameversion": "2025r80",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "NotAKidoS, Exterrata, Noachi, RaidShadowLily, Tejler, Luc",
"description": "Share Bubbles! Allows you to drop down bubbles containing Avatars & Props. Requires both users to have the mod installed. Synced over Mod Network.\n### Features:\n- Can drop Share Bubbles linking to **any** Avatar or Prop page.\n - Share Bubbles can grant Permanent or Temporary shares to your **Private** Content if configured.\n- Adds basic in-game Share Management:\n - Directly share content to users within the current instance.\n - Revoke granted or received content shares.\n### Important:\nThe claiming of content shares through Share Bubbles will likely fail if you do not allow content shares from non-friends in your ABI Account settings!\n\n-# More information can be found on the [README](https://github.com/NotAKidoS/NAK_CVR_Mods/blob/main/ShareBubbles/README.md).",
@ -17,8 +17,8 @@
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/ShareBubbles.dll",
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/ShareBubbles.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ShareBubbles/",
"changelog": "- Fixes for 2025r179\n- Fixed Public/Private text on bubble not being correct\n- Fixed bubble displaying as locked or not claimed despite being shared the content if it was private and not owned by you",
"changelog": "- Fixes for 2025r180",
"embedcolor": "#f61963"
}