mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-03 06:49:22 +00:00
[ShareBubbles] Fixes for 2025r180
This commit is contained in:
parent
13e206cd58
commit
a0a859aa86
14 changed files with 60 additions and 1953 deletions
|
@ -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
|
@ -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";
|
||||
}
|
|
@ -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")
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue