Compare commits

...

13 commits

Author SHA1 Message Date
NotAKidoS
6d28f734da [YouAreMyPropNowWeAreHavingSoftTacosLater] Updated format.json 2025-08-20 00:04:03 -05:00
NotAKidoS
969bd00df3 [YouAreMyPropNowWeAreHavingSoftTacosLater] Updated format.json 2025-08-19 23:57:04 -05:00
NotAKidoS
7deddd88ea [CustomSpawnPoint] Fixes for 2025r180 2025-08-19 23:47:06 -05:00
NotAKidoS
1a591749c6 [YouAreMyPropNowWeAreHavingSoftTacosLater] Update format.json 2025-08-19 23:42:54 -05:00
NotAKidoS
218344a121 [ShareBubbles] Updated format.json 2025-08-19 23:41:03 -05:00
NotAKidoS
07daceea44 [ScrollFlight] Fixes for 2025r180 2025-08-19 23:35:04 -05:00
NotAKidoS
aaeb187b9e [PropUndoButton] Fixes for 2025r180 2025-08-19 23:34:19 -05:00
NotAKidoS
e378a717d3 [RCCVirtualSteeringWheel] Fixes for 2025r180 2025-08-19 23:34:09 -05:00
NotAKidoS
a0a859aa86 [ShareBubbles] Fixes for 2025r180 2025-08-19 23:33:53 -05:00
NotAKidoS
13e206cd58 [ThirdPerson] Updated format.json 2025-08-19 23:33:42 -05:00
NotAKidoS
548fcf72bc [ThirdPerson] Updated format.json 2025-08-19 23:31:07 -05:00
NotAKidoS
ee4df06d2e [ThirdPerson] Fixes for r180 2025-08-19 23:30:16 -05:00
NotAKidoS
faf9d48fb6 [YouAreMyPropNowWeAreHavingSoftTacosLater] initial release 2025-08-19 23:30:02 -05:00
33 changed files with 193 additions and 2053 deletions

View file

@ -17,7 +17,7 @@ using System.Reflection;
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/CustomSpawnPoint"
)]
[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 System.Reflection;
namespace NAK.CustomSpawnPoint.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.2";
public const string Version = "1.0.3";
public const string Author = "NotAKidoS";
}

View file

@ -41,20 +41,20 @@ internal static class SpawnPointManager
{
while (ViewManager.Instance == null)
yield return null;
while (ViewManager.Instance.gameMenuView == null)
while (ViewManager.Instance.cohtmlView == null)
yield return null;
while (ViewManager.Instance.gameMenuView.Listener == null)
while (ViewManager.Instance.cohtmlView.Listener == null)
yield return null;
ViewManager.Instance.OnUiConfirm.AddListener(OnClearSpawnpointConfirm);
ViewManager.Instance.gameMenuView.Listener.FinishLoad += (_) =>
ViewManager.Instance.cohtmlView.Listener.FinishLoad += (_) =>
{
ViewManager.Instance.gameMenuView.View._view.ExecuteScript(spawnpointJs);
ViewManager.Instance.cohtmlView.View._view.ExecuteScript(spawnpointJs);
};
ViewManager.Instance.gameMenuView.Listener.ReadyForBindings += () =>
ViewManager.Instance.cohtmlView.Listener.ReadyForBindings += () =>
{
// listen for setting the spawn point on our custom button
ViewManager.Instance.gameMenuView.View.BindCall("NAKCallSetSpawnpoint", SetSpawnPoint);
ViewManager.Instance.cohtmlView.View.BindCall("NAKCallSetSpawnpoint", SetSpawnPoint);
};
// create our custom spawn point object
@ -179,7 +179,7 @@ internal static class SpawnPointManager
private static void UpdateMenuButtonState(bool hasSpawnpoint, bool isInWorld)
{
ViewManager.Instance.gameMenuView.View.TriggerEvent("NAKUpdateSpawnpointStatus", hasSpawnpoint.ToString(), isInWorld.ToString());
ViewManager.Instance.cohtmlView.View.TriggerEvent("NAKUpdateSpawnpointStatus", hasSpawnpoint.ToString(), isInWorld.ToString());
}
private static void ClearCurrentWorldState()

View file

@ -1,9 +1,9 @@
{
"_id": 228,
"name": "CustomSpawnPoint",
"modversion": "1.0.2",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"modversion": "1.0.3",
"gameversion": "2025r180",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Replaces the unused Images button in the World Details page with a button to set a custom spawn point.",
@ -17,8 +17,8 @@
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/CustomSpawnPoint.dll",
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/CustomSpawnPoint.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/CustomSpawnPoint/",
"changelog": "- Recompiled for 2025r179",
"changelog": "- Fixes for 2025r180",
"embedcolor": "#f61963"
}

View file

@ -16,17 +16,17 @@ namespace NAK.PropUndoButton;
public class PropUndoButton : MelonMod
{
public static readonly MelonPreferences_Category Category =
private static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(nameof(PropUndoButton));
public static readonly MelonPreferences_Entry<bool> EntryEnabled =
private static readonly MelonPreferences_Entry<bool> EntryEnabled =
Category.CreateEntry("Enabled", true, description: "Toggle Undo Prop Button.");
public static readonly MelonPreferences_Entry<bool> EntryUseSFX =
private static readonly MelonPreferences_Entry<bool> EntryUseSFX =
Category.CreateEntry("Use SFX", true,
description: "Toggle audio queues for prop spawn, undo, redo, and warning.");
internal static List<DeletedProp> deletedProps = new();
private static readonly List<DeletedProp> deletedProps = [];
// audio clip names, InterfaceAudio adds "PropUndo_" prefix
private const string sfx_spawn = "PropUndo_sfx_spawn";
@ -92,11 +92,11 @@ public class PropUndoButton : MelonMod
if (!File.Exists(clipPath))
{
// read the clip data from embedded resources
byte[] clipData = null;
byte[] clipData;
var resourceName = "PropUndoButton.SFX." + clipName;
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
{
clipData = new byte[stream.Length];
clipData = new byte[stream!.Length];
stream.Read(clipData, 0, clipData.Length);
}

View file

@ -17,7 +17,7 @@ using System.Reflection;
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/UndoPropButton"
)]
[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 System.Reflection;
namespace NAK.PropUndoButton.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.3";
public const string Version = "1.0.4";
public const string Author = "NotAKidoS";
}

View file

@ -1,9 +1,9 @@
{
"_id": 147,
"name": "PropUndoButton",
"modversion": "1.0.3",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"modversion": "1.0.4",
"gameversion": "2025r180",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "**CTRL+Z** to undo latest spawned prop. **CTRL+SHIFT+Z** to redo deleted prop.\nIncludes optional SFX for prop spawn, undo, redo, warn, and deny, which can be disabled in settings.\n\nYou can replace the sfx in 'ChilloutVR\\ChilloutVR_Data\\StreamingAssets\\Cohtml\\UIResources\\GameUI\\mods\\PropUndo\\audio'.",
@ -16,8 +16,8 @@
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/PropUndoButton.dll",
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/PropUndoButton.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/PropUndoButton/",
"changelog": "- Recompiled for 2025r179",
"changelog": "- Fixes for 2025r180",
"embedcolor": "#00FFFF"
}

View file

@ -18,7 +18,7 @@ using NAK.RCCVirtualSteeringWheel.Properties;
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/RCCVirtualSteeringWheel"
)]
[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
@ -28,6 +28,6 @@ using NAK.RCCVirtualSteeringWheel.Properties;
namespace NAK.RCCVirtualSteeringWheel.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.4";
public const string Version = "1.0.6";
public const string Author = "NotAKidoS";
}

View file

@ -26,8 +26,8 @@ public class SteeringWheelPickup : Pickupable
public override void OnUseDown(InteractionContext context) { }
public override void OnUseUp(InteractionContext context) { }
public override void OnFlingTowardsTarget(Vector3 target) { }
public override void FlingTowardsTarget(ControllerRay controllerRay) { }
public override void OnGrab(InteractionContext context, Vector3 grabPoint) {
if (ControllerRay?.pivotPoint != null)
root.StartTrackingTransform(ControllerRay.transform);

View file

@ -1,9 +1,9 @@
{
"_id": 248,
"name": "RCCVirtualSteeringWheel",
"modversion": "1.0.4",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"modversion": "1.0.6",
"gameversion": "2025r180",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Allows you to physically grab rigged RCC steering wheels in VR to provide steering input. No explicit setup required other than defining the Steering Wheel transform within the RCC component.\n",
@ -16,8 +16,8 @@
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/RCCVirtualSteeringWheel.dll",
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/RCCVirtualSteeringWheel.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/RCCVirtualSteeringWheel/",
"changelog": "- Recompiled for 2025r179\n- Fixed generated steering wheel pickup collider not being marked isTrigger\n- Fixed error spam when sitting in a CVRSeat with lockControls, but no RCC component in parent",
"changelog": "- Fixes for 2025r180",
"embedcolor": "#f61963"
}

View file

@ -1,10 +1,7 @@
using System.Globalization;
using System.Reflection;
using ABI_RC.Core.UI;
using ABI_RC.Systems.IK.VRIKHandlers;
using ABI_RC.Systems.Movement;
using ABI.CCK.Components;
using HarmonyLib;
using MelonLoader;
using UnityEngine;

View file

@ -17,7 +17,7 @@ using System.Reflection;
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ScrollFlight"
)]
[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 System.Reflection;
namespace NAK.ScrollFlight.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.3";
public const string Version = "1.0.4";
public const string Author = "NotAKidoS";
}

View file

@ -1,9 +1,9 @@
{
"_id": 219,
"name": "ScrollFlight",
"modversion": "1.0.3",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"modversion": "1.0.4",
"gameversion": "2025r180",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Scroll-wheel to adjust flight speed in Desktop. Stole idea from Luc.",
@ -16,8 +16,8 @@
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/ScrollFlight.dll",
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/ScrollFlight.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ScrollFlight/",
"changelog": "- Recompiled for 2025r179\n- Added an option to reset flight speed to default on exit flight",
"changelog": "- Fixes for 2025r180",
"embedcolor": "#f61963"
}

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": "2025r180",
"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"
}

View file

@ -1,7 +1,6 @@
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Core.Util.Object_Behaviour;
using System.Collections;
using ABI_RC.Core;
using ABI.CCK.Components;
using UnityEngine;
@ -42,10 +41,8 @@ internal static class CameraLogic
}
}
internal static IEnumerator SetupCamera()
internal static void SetupCamera()
{
yield return new WaitUntil(() => PlayerSetup.Instance);
_thirdPersonCam = new GameObject("ThirdPersonCameraObj", typeof(Camera)).GetComponent<Camera>();
_cameraFovClone = _thirdPersonCam.gameObject.AddComponent<CameraFovClone>();

View file

@ -17,7 +17,7 @@ using System.Reflection;
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ThirdPerson"
)]
[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, 97)] // do not change color, originally chosen by Davi
@ -27,6 +27,6 @@ using System.Reflection;
namespace NAK.ThirdPerson.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.1.2";
public const string Version = "1.1.3";
public const string Author = "Davi & NotAKidoS";
}

View file

@ -1,4 +1,5 @@
using MelonLoader;
using ABI_RC.Systems.GameEventSystem;
using MelonLoader;
using UnityEngine;
using static NAK.ThirdPerson.CameraLogic;
@ -13,7 +14,7 @@ public class ThirdPerson : MelonMod
Logger = LoggerInstance;
Patches.Apply(HarmonyInstance);
MelonCoroutines.Start(SetupCamera());
CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(SetupCamera);
}
public override void OnUpdate()

View file

@ -2,9 +2,9 @@
{
"_id": 16,
"name": "ThirdPerson",
"modversion": "1.1.2",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"modversion": "1.1.3",
"gameversion": "2025r180",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "Davi & NotAKidoS",
"description": "Allows you to go into third person view by pressing Ctrl + T to toggle and Ctrl + Y to cycle modes.",
@ -14,9 +14,9 @@
"third person"
],
"requirements": [],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/ThirdPerson.dll",
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/ThirdPerson.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ThirdPerson",
"changelog": "- Adjusted the local player scaled event patch to work both on Stable and Nightly (r179 & r180)",
"changelog": "- Fixes for 2025r180",
"embedcolor": "#F61961"
}
]

View file

@ -1,11 +1,13 @@
using System.Collections;
using System.Reflection;
using ABI_RC.Core.EventSystem;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.IO;
using ABI_RC.Core.Networking;
using ABI_RC.Core.Networking.API.Responses;
using ABI_RC.Core.Player;
using ABI_RC.Core.Util;
using ABI_RC.Core.Util.Encryption;
using ABI_RC.Systems.GameEventSystem;
using ABI_RC.Systems.Movement;
using ABI.CCK.Components;
@ -64,7 +66,7 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
#region CVRAttachment Patches
HarmonyInstance.Patch( // Cannot compile when using nameof
typeof(CVRAttachment).GetMethod("\u003CAttachInternal\u003Eg__DoAttachmentSetup\u007C43_0",
typeof(CVRAttachment).GetMethod(nameof(CVRAttachment.DoAttachmentSetup),
BindingFlags.NonPublic | BindingFlags.Instance),
postfix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnCVRAttachmentAttachInternal),
BindingFlags.NonPublic | BindingFlags.Static))
@ -96,6 +98,17 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
#endregion CVRSeat Patches
#region CVRSpawnable Patches
HarmonyInstance.Patch(
typeof(CVRSpawnable).GetMethod(nameof(CVRSpawnable.OnDestroy),
BindingFlags.Public | BindingFlags.Instance),
prefix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnSpawnableOnDestroy),
BindingFlags.NonPublic | BindingFlags.Static))
);
#endregion CVRSpawnable Patches
#region CVRSyncHelper Patches
HarmonyInstance.Patch( // Replaces method, original needlessly ToArray???
@ -147,12 +160,28 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
#region Harmony Patches
private static readonly List<CVRSyncHelper.PropData> _heldPropData = new();
[Flags] private enum HeldPropState { None = 0, Pickup = 1, Attachment = 2, Seat = 3 }
private static readonly Dictionary<CVRSyncHelper.PropData, HeldPropState> _heldPropStates = new();
private static void AddHeldPropState(CVRSyncHelper.PropData propData, HeldPropState state)
{
if (!_heldPropStates.TryAdd(propData, state)) _heldPropStates[propData] |= state;
}
private static void RemoveHeldPropState(CVRSyncHelper.PropData propData, HeldPropState state)
{
if (!_heldPropStates.TryGetValue(propData, out HeldPropState currentState)) return;
currentState &= ~state;
if (currentState == HeldPropState.None) _heldPropStates.Remove(propData);
else _heldPropStates[propData] = currentState;
}
private static GameObject _persistantPropsContainer;
private static GameObject GetOrCreatePropsContainer()
{
if (_persistantPropsContainer != null) return _persistantPropsContainer;
_persistantPropsContainer = new("[NAK] PersistantProps");
if (_persistantPropsContainer) return _persistantPropsContainer;
_persistantPropsContainer = new("YouAreMyPropNowWeAreHavingSoftTacosLater");
_persistantPropsContainer.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
_persistantPropsContainer.transform.localScale = Vector3.one;
Object.DontDestroyOnLoad(_persistantPropsContainer);
@ -168,47 +197,50 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
{
if (!EntryTrackPickups.Value) return;
if (!TryGetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (!_heldPropData.Contains(propData)) _heldPropData.Add(propData);
AddHeldPropState(propData, HeldPropState.Pickup);
}
private static void OnCVRPickupObjectOnDrop(CVRPickupObject __instance)
{
if (!TryGetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (_heldPropData.Contains(propData)) _heldPropData.Remove(propData);
RemoveHeldPropState(propData, HeldPropState.Pickup);
}
private static void OnCVRAttachmentAttachInternal(CVRAttachment __instance)
{
if (!EntryTrackAttachments.Value) return;
if (!TryGetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (!_heldPropData.Contains(propData)) _heldPropData.Add(propData);
AddHeldPropState(propData, HeldPropState.Attachment);
}
private static void OnCVRAttachmentDeAttach(CVRAttachment __instance)
{
if (!__instance._isAttached) return; // Can invoke DeAttach without being attached
if (!TryGetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (_heldPropData.Contains(propData)) _heldPropData.Remove(propData);
RemoveHeldPropState(propData, HeldPropState.Attachment);
}
private static void OnCVRSeatSitDown(CVRSeat __instance)
{
if (!EntryTrackSeats.Value) return;
if (!TryGetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (!_heldPropData.Contains(propData)) _heldPropData.Add(propData);
AddHeldPropState(propData, HeldPropState.Seat);
}
private static void OnCVRSeatExitSeat(CVRSeat __instance)
{
if (!TryGetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (_heldPropData.Contains(propData)) _heldPropData.Remove(propData);
RemoveHeldPropState(propData, HeldPropState.Seat);
}
private static void OnSpawnableOnDestroy(CVRSpawnable __instance)
{
if (!TryGetPropData(__instance, out CVRSyncHelper.PropData propData)) return;
_heldPropStates.Remove(propData);
}
// ReSharper disable UnusedParameter.Local
private static bool OnCVRDownloadManagerQueueTask(string assetId, DownloadTask.ObjectType type, string assetUrl, string fileId, long fileSize, string fileKey, string toAttach,
string fileHash = null, UgcTagsData tagsData = null, CVRLoadingAvatarController loadingAvatarController = null,
bool joinOnComplete = false, bool isHomeRequested = false, int compatibilityVersion = 0, int encryptionAlgorithm = 0,
string spawnerId = null)
private static bool OnCVRDownloadManagerQueueTask(AssetManagement.UgcMetadata metadata, DownloadTask.ObjectType type, string assetUrl, string fileId, string toAttach, CVRLoadingAvatarController loadingAvatarController = null, bool joinOnComplete = false, bool isHomeRequested = false, CompatibilityVersions compatibilityVersion = CompatibilityVersions.NotSpi, CVREncryptionRouter.EncryptionAlgorithm encryptionAlgorithm = CVREncryptionRouter.EncryptionAlgorithm.Gen1, string spawnerId = null)
{
if (type != DownloadTask.ObjectType.Prop) return true; // Only care about props
@ -219,14 +251,20 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
Vector3 identity = GetIdentityKeyFromPropData(newPropData);
if (!_keyToPropData.Remove(identity, out CVRSyncHelper.PropData originalPropData)) return true;
// Remove original prop data from held
if (_heldPropData.Contains(originalPropData)) _heldPropData.Remove(originalPropData);
// Remove original prop data from held, cache states
HeldPropState heldState = HeldPropState.None;
if (_heldPropStates.ContainsKey(originalPropData))
{
heldState = _heldPropStates[originalPropData];
_heldPropStates.Remove(originalPropData);
}
// If original prop data is null spawn a new prop i guess :(
if (originalPropData.Spawnable == null) return true;
if (!originalPropData.Spawnable) return true;
// Add the new prop data to our held props in place of the old one
if (!_heldPropData.Contains(newPropData)) _heldPropData.Add(newPropData);
// if (!_heldPropData.Contains(newPropData)) _heldPropData.Add(newPropData);
_heldPropStates.TryAdd(newPropData, heldState);
// Apply new prop data to the spawnable
newPropData.Spawnable = originalPropData.Spawnable;
@ -255,7 +293,7 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
for (var index = CVRSyncHelper.Props.Count - 1; index >= 0; index--)
{
CVRSyncHelper.PropData prop = CVRSyncHelper.Props[index];
if (prop.Spawnable != null && _heldPropData.Contains(prop))
if (prop.Spawnable && _heldPropStates.ContainsKey(prop))
continue; // Do not recycle props that are valid & held
DeleteOrRecycleProp(prop);
@ -269,7 +307,7 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
{
if (!_ignoreNextSeatExit) return true;
_ignoreNextSeatExit = false;
if (BetterBetterCharacterController.Instance._lastCvrSeat == null) return true; // run original
if (!BetterBetterCharacterController.Instance._lastCvrSeat) return true; // run original
return false; // dont run if there is a chair & we skipped it
}
@ -282,16 +320,16 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
private void OnWorldLoad(string _)
{
CVRWorld worldInstance = CVRWorld.Instance;
if (worldInstance != null && !worldInstance.allowSpawnables)
if (worldInstance && !worldInstance.allowSpawnables)
{
foreach (CVRSyncHelper.PropData prop in _heldPropData) DeleteOrRecycleProp(prop); // Delete all props we kept
foreach (CVRSyncHelper.PropData prop in _heldPropStates.Keys) DeleteOrRecycleProp(prop); // Delete all props we kept
return;
}
for (var index = _heldPropData.Count - 1; index >= 0; index--)
for (var index = _heldPropStates.Count - 1; index >= 0; index--)
{
CVRSyncHelper.PropData prop = _heldPropData[index];
if (prop.Spawnable == null)
CVRSyncHelper.PropData prop = _heldPropStates.ElementAt(index).Key;
if (!prop.Spawnable)
{
DeleteOrRecycleProp(prop);
return;
@ -324,9 +362,9 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
private static void OnWorldUnload(string _)
{
// Prevent deleting of our held props on scene destruction
foreach (CVRSyncHelper.PropData prop in _heldPropData)
foreach (CVRSyncHelper.PropData prop in _heldPropStates.Keys)
{
if (prop.Spawnable == null) continue;
if (!prop.Spawnable) continue;
PlacePropInPersistantPropsContainer(prop.Spawnable);
_spawnablePositionStack.Push(new SpawnablePositionContainer(prop.Spawnable));
}
@ -341,9 +379,9 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
{
// Request the server to respawn our props by GUID, and add a secret key to the propData to identify it
foreach (CVRSyncHelper.PropData prop in _heldPropData)
foreach (CVRSyncHelper.PropData prop in _heldPropStates.Keys)
{
if (prop.Spawnable == null) continue;
if (!prop.Spawnable) continue;
// Generate a new identity key for the prop (this is used to identify the prop when we respawn it)
Vector3 identityKey = new(Random.Range(0, 1000), Random.Range(0, 1000), Random.Range(0, 1000));
@ -362,7 +400,7 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
private static bool TryGetPropData(CVRSpawnable spawnable, out CVRSyncHelper.PropData propData)
{
if (spawnable == null)
if (!spawnable)
{
propData = null;
return false;
@ -408,9 +446,9 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
private static void DeleteOrRecycleProp(CVRSyncHelper.PropData prop)
{
if (prop.Spawnable == null) prop.Recycle();
if (!prop.Spawnable) prop.Recycle();
else prop.Spawnable.Delete();
if (_heldPropData.Contains(prop)) _heldPropData.Remove(prop);
_heldPropStates.Remove(prop);
}
private static void SpawnPropFromGuid(string propGuid, Vector3 position, Vector3 rotation, Vector3 identityKey)
@ -518,7 +556,7 @@ public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
for (int i = 0; i < _spawnable.subSyncs.Count; i++)
{
Transform subSyncTransform = _spawnable.subSyncs[i].transform;
if (subSyncTransform == null) continue;
if (!subSyncTransform) continue;
Vector3 subWorldPos = playerTransform.TransformPoint(_posOffsets[i + 1]);
subWorldPos.y += _heightOffset;

View file

@ -17,7 +17,7 @@ using System.Reflection;
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/YouAreMyPropNowWeAreHavingSoftTacosLater"
)]
[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

View file

@ -1,12 +1,12 @@
{
"_id": -1,
"_id": 262,
"name": "YouAreMyPropNowWeAreHavingSoftTacosLater",
"modversion": "1.0.0",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"gameversion": "2025r180",
"loaderversion": "0.7.2",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Lets you bring held & attached props through world loads.\nhttps://youtu.be/9P6Jeh-VN58?si=eXTPGyKB_0wq1gZO",
"description": "Lets you bring held, attached, and occupied props through world loads. This is configurable in the mod settings.\nhttps://youtu.be/9P6Jeh-VN58?si=eXTPGyKB_0wq1gZO\n\nThere is special logic in place for bringing air vehicles through world loads. If above the ground you will be placed up to 20m above the spawnpoint of the next world.",
"searchtags": [
"prop",
"spawn",
@ -16,8 +16,8 @@
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/YouAreMyPropNowWeAreHavingSoftTacosLater.dll",
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r47/YouAreMyPropNowWeAreHavingSoftTacosLater.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/YouAreMyPropNowWeAreHavingSoftTacosLater/",
"changelog": "- Initial Release",
"embedcolor": "#00FFFF"
"changelog": "- Initial release",
"embedcolor": "#f61963"
}