Compare commits

...

14 commits

Author SHA1 Message Date
github-actions[bot]
2af27cc81d [NAK_CVR_Mods] Update mod list in README
Some checks are pending
Update Mod List / update-modlist (push) Waiting to run
2025-04-11 12:52:38 +00:00
NotAKidoS
784249a08b Merge branch 'main' of https://github.com/NotAKidoS/NAK_CVR_Mods 2025-04-11 07:52:21 -05:00
NotAKidoS
1fbfcd80cd [NAK_CVR_Mods] 2 2025-04-11 07:52:15 -05:00
github-actions[bot]
d5eb9ae1a0 [NAK_CVR_Mods] Update mod list in README 2025-04-11 12:47:11 +00:00
NotAKidoS
88faf93b3b Merge branch 'main' of https://github.com/NotAKidoS/NAK_CVR_Mods 2025-04-11 07:46:57 -05:00
NotAKidoS
a2e29149e2 [NAK_CVR_Mods] test 2025-04-11 07:46:51 -05:00
NotAKidoS
8f8f2ad1fb [NAK_CVR_Mods] fixed generated download link in readme 2025-04-11 07:45:31 -05:00
github-actions[bot]
a2aa3b9871 [NAK_CVR_Mods] Update mod list in README 2025-04-11 12:41:14 +00:00
NotAKidoS
febd9f2741 Merge branch 'main' of https://github.com/NotAKidoS/NAK_CVR_Mods 2025-04-11 07:40:59 -05:00
NotAKidoS
b246f71e6e [CustomRichPresence] fix readme 2025-04-11 07:40:53 -05:00
github-actions[bot]
0cde9ecb21 [NAK_CVR_Mods] Update mod list in README 2025-04-11 12:39:51 +00:00
NotAKidoS
964d6009b7 Merge branch 'main' of https://github.com/NotAKidoS/NAK_CVR_Mods 2025-04-11 07:39:35 -05:00
NotAKidoS
392390cde7 [YouAreMyPropNowWeAreHavingSoftTacosLater] Initial push 2025-04-11 07:39:32 -05:00
NotAKidoS
ba26a1faae [CustomRichPresence] Move to .Experimental folder 2025-04-11 07:39:16 -05:00
13 changed files with 714 additions and 52 deletions

View file

@ -1,6 +1,6 @@
# DropPropTweak
# Custom Rich Presence
Gives the Drop Prop button more utility by allowing you to drop props in the air.
Lets you customize the Steam & Discord rich presence messages & values.
---

View file

@ -1,11 +1,56 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
// Configuration
const ROOT = '.';
const EXPERIMENTAL = '.Experimental';
const README_PATH = 'README.md';
const MARKER_START = '<!-- BEGIN MOD LIST -->';
const MARKER_END = '<!-- END MOD LIST -->';
const REPO_OWNER = process.env.REPO_OWNER || 'NotAKidoS';
const REPO_NAME = process.env.REPO_NAME || 'NAK_CVR_Mods';
// Function to get latest release info from GitHub API
async function getLatestRelease() {
return new Promise((resolve, reject) => {
const options = {
hostname: 'api.github.com',
path: `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
method: 'GET',
headers: {
'User-Agent': 'Node.js GitHub Release Checker',
'Accept': 'application/vnd.github.v3+json'
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`Failed to parse GitHub API response: ${e.message}`));
}
} else {
reject(new Error(`GitHub API request failed with status code: ${res.statusCode}`));
}
});
});
req.on('error', (e) => {
reject(new Error(`GitHub API request error: ${e.message}`));
});
req.end();
});
}
function getModFolders(baseDir) {
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
@ -19,7 +64,7 @@ function extractDescription(readmePath) {
try {
const content = fs.readFileSync(readmePath, 'utf8');
const lines = content.split('\n');
// Find the first header (# something)
let headerIndex = -1;
for (let i = 0; i < lines.length; i++) {
@ -28,7 +73,7 @@ function extractDescription(readmePath) {
break;
}
}
// If we found a header, look for the first non-empty line after it
if (headerIndex !== -1) {
for (let i = headerIndex + 1; i < lines.length; i++) {
@ -38,7 +83,7 @@ function extractDescription(readmePath) {
}
}
}
return 'No description available';
} catch (error) {
console.error(`Error reading ${readmePath}:`, error);
@ -46,44 +91,95 @@ function extractDescription(readmePath) {
}
}
function formatTable(mods, baseDir) {
async function formatTable(mods, baseDir) {
if (mods.length === 0) return '';
let rows = mods.map(modPath => {
const modName = path.basename(modPath);
const readmeLink = path.join(modPath, 'README.md');
const zipLink = path.join(modPath, `${modName}.zip`);
const readmePath = path.join(modPath, 'README.md');
const description = extractDescription(readmePath);
try {
// Get the latest release info from GitHub
const latestRelease = await getLatestRelease();
const releaseAssets = latestRelease.assets || [];
return `| [${modName}](${readmeLink}) | ${description} | [Download](${zipLink}) |`;
});
return [
`### ${baseDir === EXPERIMENTAL ? 'Experimental Mods' : 'Released Mods'}`,
'',
'| Name | Description | Download |',
'|------|-------------|----------|',
...rows,
''
].join('\n');
// Create a map of available files in the release
const availableFiles = {};
releaseAssets.forEach(asset => {
availableFiles[asset.name] = asset.browser_download_url;
});
let rows = mods.map(modPath => {
const modName = path.basename(modPath);
const readmeLink = path.join(modPath, 'README.md');
const readmePath = path.join(modPath, 'README.md');
const description = extractDescription(readmePath);
// Check if the DLL exists in the latest release
const dllFilename = `${modName}.dll`;
let downloadSection;
if (availableFiles[dllFilename]) {
downloadSection = `[Download](${availableFiles[dllFilename]})`;
} else {
downloadSection = 'No Download';
}
return `| [${modName}](${readmeLink}) | ${description} | ${downloadSection} |`;
});
return [
`### ${baseDir === EXPERIMENTAL ? 'Experimental Mods' : 'Released Mods'}`,
'',
'| Name | Description | Download |',
'|------|-------------|----------|',
...rows,
''
].join('\n');
} catch (error) {
console.error('Error fetching release information:', error);
// Fallback to showing "No Download" for all mods if we can't fetch release info
let rows = mods.map(modPath => {
const modName = path.basename(modPath);
const readmeLink = path.join(modPath, 'README.md');
const readmePath = path.join(modPath, 'README.md');
const description = extractDescription(readmePath);
return `| [${modName}](${readmeLink}) | ${description} | No Download |`;
});
return [
`### ${baseDir === EXPERIMENTAL ? 'Experimental Mods' : 'Released Mods'}`,
'',
'| Name | Description | Download |',
'|------|-------------|----------|',
...rows,
''
].join('\n');
}
}
function updateReadme(modListSection) {
const readme = fs.readFileSync(README_PATH, 'utf8');
const before = readme.split(MARKER_START)[0];
const after = readme.split(MARKER_END)[1];
const newReadme = `${before}${MARKER_START}\n\n${modListSection}\n${MARKER_END}${after}`;
fs.writeFileSync(README_PATH, newReadme);
}
const mainMods = getModFolders(ROOT).filter(dir => !dir.startsWith(EXPERIMENTAL));
const experimentalMods = getModFolders(EXPERIMENTAL);
async function main() {
try {
const mainMods = getModFolders(ROOT).filter(dir => !dir.startsWith(EXPERIMENTAL));
const experimentalMods = getModFolders(EXPERIMENTAL);
const mainModsTable = await formatTable(mainMods, ROOT);
const experimentalModsTable = await formatTable(experimentalMods, EXPERIMENTAL);
const tableContent = [mainModsTable, experimentalModsTable].join('\n');
updateReadme(tableContent);
console.log('README.md updated successfully!');
} catch (error) {
console.error('Error updating README:', error);
process.exit(1);
}
}
const tableContent = [
formatTable(mainMods, ROOT),
formatTable(experimentalMods, EXPERIMENTAL)
].join('\n');
updateReadme(tableContent);
main();

View file

@ -54,6 +54,8 @@ EndProject
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RCCVirtualSteeringWheel", "RCCVirtualSteeringWheel\RCCVirtualSteeringWheel.csproj", "{4A378F81-3805-41E8-9565-A8A89A8C00D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YouAreMyPropNowWeAreHavingSoftTacosLater", "YouAreMyPropNowWeAreHavingSoftTacosLater\YouAreMyPropNowWeAreHavingSoftTacosLater.csproj", "{8DA821CC-F911-4FCB-8C29-5EF3D76A5F76}"
EndProject
EndProject
EndProject
EndProject
@ -289,6 +291,10 @@ Global
{ED2CAA2D-4E49-4636-86C4-367D0CDC3572}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED2CAA2D-4E49-4636-86C4-367D0CDC3572}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED2CAA2D-4E49-4636-86C4-367D0CDC3572}.Release|Any CPU.Build.0 = Release|Any CPU
{8DA821CC-F911-4FCB-8C29-5EF3D76A5F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DA821CC-F911-4FCB-8C29-5EF3D76A5F76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DA821CC-F911-4FCB-8C29-5EF3D76A5F76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DA821CC-F911-4FCB-8C29-5EF3D76A5F76}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View file

@ -6,30 +6,31 @@
| Name | Description | Download |
|------|-------------|----------|
| [ASTExtension](ASTExtension/README.md) | Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool): | [Download](ASTExtension/ASTExtension.zip) |
| [AvatarQueueSystemTweaks](AvatarQueueSystemTweaks/README.md) | Small tweaks to the Avatar Queue System. | [Download](AvatarQueueSystemTweaks/AvatarQueueSystemTweaks.zip) |
| [CustomRichPresence](CustomRichPresence/README.md) | Gives the Drop Prop button more utility by allowing you to drop props in the air. | [Download](CustomRichPresence/CustomRichPresence.zip) |
| [CustomSpawnPoint](CustomSpawnPoint/README.md) | Replaces the unused Images button in the World Details page with a button to set a custom spawn point. | [Download](CustomSpawnPoint/CustomSpawnPoint.zip) |
| [FuckToes](FuckToes/README.md) | Prevents VRIK from autodetecting toes in HalfbodyIK. | [Download](FuckToes/FuckToes.zip) |
| [KeepVelocityOnExitFlight](KeepVelocityOnExitFlight/README.md) | Keeps the player's velocity when exiting flight mode. Makes it possible to fling yourself like in Garry's Mod. | [Download](KeepVelocityOnExitFlight/KeepVelocityOnExitFlight.zip) |
| [LazyPrune](LazyPrune/README.md) | Prevents loaded objects from immediately unloading on destruction. Should prevent needlessly unloading & reloading all avatars/props on world rejoin or GS reconnection. | [Download](LazyPrune/LazyPrune.zip) |
| [PropLoadingHexagon](PropLoadingHexagon/README.md) | https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/a892c765-71c1-47f3-a781-bdb9b60ba117 | [Download](PropLoadingHexagon/PropLoadingHexagon.zip) |
| [RCCVirtualSteeringWheel](RCCVirtualSteeringWheel/README.md) | 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. | [Download](RCCVirtualSteeringWheel/RCCVirtualSteeringWheel.zip) |
| [RelativeSync](RelativeSync/README.md) | Relative sync for Movement Parent & Chairs. Requires both users to have the mod installed. Synced over Mod Network. | [Download](RelativeSync/RelativeSync.zip) |
| [ShareBubbles](ShareBubbles/README.md) | Share Bubbles! Allows you to drop down bubbles containing Avatars & Props. Requires both users to have the mod installed. Synced over Mod Network. | [Download](ShareBubbles/ShareBubbles.zip) |
| [SmootherRay](SmootherRay/README.md) | Smoothes your controller while the raycast lines are visible. | [Download](SmootherRay/SmootherRay.zip) |
| [Stickers](Stickers/README.md) | Stickers! Allows you to place small images on any surface. Requires both users to have the mod installed. Synced over Mod Network. | [Download](Stickers/Stickers.zip) |
| [ThirdPerson](ThirdPerson/README.md) | Original repo: https://github.com/oestradiol/CVR-Mods | [Download](ThirdPerson/ThirdPerson.zip) |
| [ASTExtension](ASTExtension/README.md) | Extension mod for [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool): | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/ASTExtension.dll) |
| [AvatarQueueSystemTweaks](AvatarQueueSystemTweaks/README.md) | Small tweaks to the Avatar Queue System. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/AvatarQueueSystemTweaks.dll) |
| [CustomSpawnPoint](CustomSpawnPoint/README.md) | Replaces the unused Images button in the World Details page with a button to set a custom spawn point. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/CustomSpawnPoint.dll) |
| [FuckToes](FuckToes/README.md) | Prevents VRIK from autodetecting toes in HalfbodyIK. | No Download |
| [KeepVelocityOnExitFlight](KeepVelocityOnExitFlight/README.md) | Keeps the player's velocity when exiting flight mode. Makes it possible to fling yourself like in Garry's Mod. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/KeepVelocityOnExitFlight.dll) |
| [LazyPrune](LazyPrune/README.md) | Prevents loaded objects from immediately unloading on destruction. Should prevent needlessly unloading & reloading all avatars/props on world rejoin or GS reconnection. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/LazyPrune.dll) |
| [PropLoadingHexagon](PropLoadingHexagon/README.md) | https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/a892c765-71c1-47f3-a781-bdb9b60ba117 | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/PropLoadingHexagon.dll) |
| [RCCVirtualSteeringWheel](RCCVirtualSteeringWheel/README.md) | 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. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/RCCVirtualSteeringWheel.dll) |
| [RelativeSync](RelativeSync/README.md) | Relative sync for Movement Parent & Chairs. Requires both users to have the mod installed. Synced over Mod Network. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/RelativeSync.dll) |
| [ShareBubbles](ShareBubbles/README.md) | Share Bubbles! Allows you to drop down bubbles containing Avatars & Props. Requires both users to have the mod installed. Synced over Mod Network. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/ShareBubbles.dll) |
| [SmootherRay](SmootherRay/README.md) | Smoothes your controller while the raycast lines are visible. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/SmootherRay.dll) |
| [Stickers](Stickers/README.md) | Stickers! Allows you to place small images on any surface. Requires both users to have the mod installed. Synced over Mod Network. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/Stickers.dll) |
| [ThirdPerson](ThirdPerson/README.md) | Original repo: https://github.com/oestradiol/CVR-Mods | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/ThirdPerson.dll) |
| [YouAreMyPropNowWeAreHavingSoftTacosLater](YouAreMyPropNowWeAreHavingSoftTacosLater/README.md) | Lets you bring held & attached props through world loads. | No Download |
### Experimental Mods
| Name | Description | Download |
|------|-------------|----------|
| [CVRLuaToolsExtension](.Experimental/CVRLuaToolsExtension/README.md) | Extension mod for [CVRLuaTools](https://github.com/NotAKidoS/CVRLuaTools) Hot Reload functionality. | [Download](.Experimental/CVRLuaToolsExtension/CVRLuaToolsExtension.zip) |
| [LuaNetworkVariables](.Experimental/LuaNetworkVariables/README.md) | Simple mod that makes your controller rays always visible when the menus are open. Useful for when you're trying to aim at something in the distance. Also visualizes which ray is being used for menu interaction. | [Download](.Experimental/LuaNetworkVariables/LuaNetworkVariables.zip) |
| [LuaTTS](.Experimental/LuaTTS/README.md) | Provides access to the built-in text-to-speech (TTS) functionality to lua scripts. Allows you to make the local player speak. | [Download](.Experimental/LuaTTS/LuaTTS.zip) |
| [OriginShift](.Experimental/OriginShift/README.md) | Experimental mod that allows world origin to be shifted to prevent floating point precision issues. | [Download](.Experimental/OriginShift/OriginShift.zip) |
| [ScriptingSpoofer](.Experimental/ScriptingSpoofer/README.md) | Prevents **local** scripts from accessing your Username or UserID by spoofing them with random values each session. | [Download](.Experimental/ScriptingSpoofer/ScriptingSpoofer.zip) |
| [CVRLuaToolsExtension](.Experimental/CVRLuaToolsExtension/README.md) | Extension mod for [CVRLuaTools](https://github.com/NotAKidoS/CVRLuaTools) Hot Reload functionality. | No Download |
| [CustomRichPresence](.Experimental/CustomRichPresence/README.md) | Lets you customize the Steam & Discord rich presence messages & values. | No Download |
| [LuaNetworkVariables](.Experimental/LuaNetworkVariables/README.md) | Simple mod that makes your controller rays always visible when the menus are open. Useful for when you're trying to aim at something in the distance. Also visualizes which ray is being used for menu interaction. | No Download |
| [LuaTTS](.Experimental/LuaTTS/README.md) | Provides access to the built-in text-to-speech (TTS) functionality to lua scripts. Allows you to make the local player speak. | No Download |
| [OriginShift](.Experimental/OriginShift/README.md) | Experimental mod that allows world origin to be shifted to prevent floating point precision issues. | No Download |
| [ScriptingSpoofer](.Experimental/ScriptingSpoofer/README.md) | Prevents **local** scripts from accessing your Username or UserID by spoofing them with random values each session. | No Download |
<!-- END MOD LIST -->

View file

@ -0,0 +1,479 @@
using System.Collections;
using System.Reflection;
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.Systems.GameEventSystem;
using ABI_RC.Systems.Movement;
using ABI.CCK.Components;
using DarkRift;
using HarmonyLib;
using MelonLoader;
using UnityEngine;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
namespace NAK.YouAreMyPropNowWeAreHavingSoftTacosLater;
public class YouAreMyPropNowWeAreHavingSoftTacosLaterMod : MelonMod
{
#region Melon Events
public override void OnInitializeMelon()
{
#region CVRPickupObject Patches
HarmonyInstance.Patch(
typeof(CVRPickupObject).GetMethod(nameof(CVRPickupObject.OnGrab),
BindingFlags.NonPublic | BindingFlags.Instance),
postfix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnCVRPickupObjectOnGrab),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch(
typeof(CVRPickupObject).GetMethod(nameof(CVRPickupObject.OnDrop), BindingFlags.NonPublic | BindingFlags.Instance),
postfix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnCVRPickupObjectOnDrop),
BindingFlags.NonPublic | BindingFlags.Static))
);
#endregion CVRPickupObject Patches
#region CVRAttachment Patches
HarmonyInstance.Patch( // Cannot compile when using nameof
typeof(CVRAttachment).GetMethod("\u003CAttachInternal\u003Eg__DoAttachmentSetup\u007C43_0",
BindingFlags.NonPublic | BindingFlags.Instance),
postfix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnCVRAttachmentAttachInternal),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch(
typeof(CVRAttachment).GetMethod(nameof(CVRAttachment.DeAttach)),
prefix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnCVRAttachmentDeAttach),
BindingFlags.NonPublic | BindingFlags.Static))
);
#endregion CVRAttachment Patches
#region CVRSyncHelper Patches
HarmonyInstance.Patch( // Replaces method, original needlessly ToArray???
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.ClearProps),
BindingFlags.Public | BindingFlags.Static),
prefix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnCVRSyncHelperClearProps),
BindingFlags.NonPublic | BindingFlags.Static))
);
#endregion CVRSyncHelper Patches
#region CVRDownloadManager Patches
HarmonyInstance.Patch(
typeof(CVRDownloadManager).GetMethod(nameof(CVRDownloadManager.QueueTask),
BindingFlags.Public | BindingFlags.Instance),
prefix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnCVRDownloadManagerQueueTask),
BindingFlags.NonPublic | BindingFlags.Static))
);
#endregion CVRDownloadManager Patches
#region BetterBetterCharacterController Patches
HarmonyInstance.Patch(
typeof(BetterBetterCharacterController).GetMethod(nameof(BetterBetterCharacterController.SetSitting),
BindingFlags.Public | BindingFlags.Instance),
prefix: new HarmonyMethod(typeof(YouAreMyPropNowWeAreHavingSoftTacosLaterMod).GetMethod(nameof(OnBetterBetterCharacterControllerSetSitting),
BindingFlags.NonPublic | BindingFlags.Static))
);
#endregion BetterBetterCharacterController Patches
#region CVRWorld Game Events
CVRGameEventSystem.World.OnLoad.AddListener(OnWorldLoad);
CVRGameEventSystem.World.OnUnload.AddListener(OnWorldUnload);
#endregion CVRWorld Game Events
#region Instances Game Events
CVRGameEventSystem.Instance.OnConnected.AddListener(OnInstanceConnected);
#endregion Instances Game Events
}
#endregion Melon Events
#region Harmony Patches
private static readonly List<CVRSyncHelper.PropData> _heldPropData = new();
private static GameObject _persistantPropsContainer;
private static GameObject GetOrCreatePropsContainer()
{
if (_persistantPropsContainer != null) return _persistantPropsContainer;
_persistantPropsContainer = new("[NAK] PersistantProps");
_persistantPropsContainer.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
_persistantPropsContainer.transform.localScale = Vector3.one;
Object.DontDestroyOnLoad(_persistantPropsContainer);
return _persistantPropsContainer;
}
private static readonly Dictionary<Vector3, CVRSyncHelper.PropData> _keyToPropData = new();
private static readonly Stack<SpawnablePositionContainer> _spawnablePositionStack = new();
private static bool _ignoreNextSeatExit;
private static float _heightOffset;
private static void OnCVRPickupObjectOnGrab(CVRPickupObject __instance)
{
if (!GetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (!_heldPropData.Contains(propData)) _heldPropData.Add(propData);
}
private static void OnCVRPickupObjectOnDrop(CVRPickupObject __instance)
{
if (!GetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (_heldPropData.Contains(propData)) _heldPropData.Remove(propData);
}
private static void OnCVRAttachmentAttachInternal(CVRAttachment __instance)
{
if (!GetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (!_heldPropData.Contains(propData)) _heldPropData.Add(propData);
}
private static void OnCVRAttachmentDeAttach(CVRAttachment __instance)
{
if (!__instance._isAttached) return; // Can invoke DeAttach without being attached
if (!GetPropData(__instance.GetComponentInParent<CVRSpawnable>(true), out CVRSyncHelper.PropData propData)) return;
if (_heldPropData.Contains(propData)) _heldPropData.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)
{
if (type != DownloadTask.ObjectType.Prop) return true; // Only care about props
// toAttach is our instanceId, lets find the propData
if (!GetPropDataById(toAttach, out CVRSyncHelper.PropData newPropData)) return true;
// Check if this is a prop we requested to spawn
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);
// If original prop data is null spawn a new prop i guess :(
if (originalPropData.Spawnable == null) return true;
// Add the new prop data to our held props in place of the old one
if (!_heldPropData.Contains(newPropData)) _heldPropData.Add(newPropData);
// Apply new prop data to the spawnable
newPropData.Spawnable = originalPropData.Spawnable;
newPropData.Wrapper = originalPropData.Wrapper;
newPropData.Wrapper.name = $"p+{newPropData.ObjectId}~{newPropData.InstanceId}";
// Copy sync values
Array.Copy(newPropData.CustomFloats, originalPropData.CustomFloats, newPropData.CustomFloatsAmount);
CVRSyncHelper.ApplyPropValuesSpawn(newPropData);
// Place the prop in the additive content scene
PlacePropInAdditiveContentScene(newPropData.Spawnable);
// Clear old data so Recycle() doesn't delete our prop
originalPropData.Spawnable = null;
originalPropData.Wrapper = null;
originalPropData.Recycle();
return false;
}
private static bool OnCVRSyncHelperClearProps() // Prevent deleting of our held props on scene load
{
for (var index = CVRSyncHelper.Props.Count - 1; index >= 0; index--)
{
CVRSyncHelper.PropData prop = CVRSyncHelper.Props[index];
if (prop.Spawnable != null && _heldPropData.Contains(prop))
continue; // Do not recycle props that are valid & held
DeleteOrRecycleProp(prop);
}
CVRSyncHelper.MySpawnedPropInstanceIds.Clear();
return false;
}
private static bool OnBetterBetterCharacterControllerSetSitting(bool isSitting, CVRSeat cvrSeat = null, bool callExitSeat = true)
{
if (!_ignoreNextSeatExit) return true;
_ignoreNextSeatExit = false;
if (BetterBetterCharacterController.Instance._lastCvrSeat == null) return true; // run original
return false; // dont run if there is a chair & we skipped it
}
#endregion Harmony Patches
#region Game Events
private object _worldLoadTimer;
private void OnWorldLoad(string _)
{
CVRWorld worldInstance = CVRWorld.Instance;
if (worldInstance != null && !worldInstance.allowSpawnables)
{
foreach (CVRSyncHelper.PropData prop in _heldPropData) DeleteOrRecycleProp(prop); // Delete all props we kept
return;
}
for (var index = _heldPropData.Count - 1; index >= 0; index--)
{
CVRSyncHelper.PropData prop = _heldPropData[index];
if (prop.Spawnable == null)
{
DeleteOrRecycleProp(prop);
return;
}
// apply positions
int stackCount = _spawnablePositionStack.Count;
for (int i = stackCount - 1; i >= 0; i--) _spawnablePositionStack.Pop().ReapplyOffsets();
}
// Start a timer, and anything that did not load within 3 seconds will be destroyed
if (_worldLoadTimer != null)
{
MelonCoroutines.Stop(_worldLoadTimer);
_worldLoadTimer = null;
}
_worldLoadTimer = MelonCoroutines.Start(DestroyPersistantPropContainerInFive());
_ignoreNextSeatExit = true; // just in case we are in a car / vehicle
}
private IEnumerator DestroyPersistantPropContainerInFive()
{
yield return new WaitForSeconds(3f);
_worldLoadTimer = null;
Object.Destroy(_persistantPropsContainer);
_persistantPropsContainer = null;
_keyToPropData.Clear(); // no more chances
}
private static void OnWorldUnload(string _)
{
// Prevent deleting of our held props on scene destruction
foreach (CVRSyncHelper.PropData prop in _heldPropData)
{
if (prop.Spawnable == null) continue;
PlacePropInPersistantPropsContainer(prop.Spawnable);
_spawnablePositionStack.Push(new SpawnablePositionContainer(prop.Spawnable));
}
// Likely in a vehicle
_heightOffset = BetterBetterCharacterController.Instance._lastCvrSeat != null
? GetHeightOffsetFromPlayer()
: 0f;
}
private static void OnInstanceConnected(string _)
{
// 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)
{
if (prop.Spawnable == null) 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));
_keyToPropData.Add(identityKey, prop);
SpawnPropFromGuid(prop.ObjectId,
new Vector3(prop.PositionX, prop.PositionY, prop.PositionZ),
new Vector3(prop.RotationX, prop.RotationY, prop.RotationZ),
identityKey);
}
}
#endregion Game Events
#region Util
private static bool GetPropData(CVRSpawnable spawnable, out CVRSyncHelper.PropData propData)
{
if (spawnable == null)
{
propData = null;
return false;
}
foreach (CVRSyncHelper.PropData data in CVRSyncHelper.Props)
{
if (data.InstanceId != spawnable.instanceId) continue;
propData = data;
return true;
}
propData = null;
return false;
}
private static bool GetPropDataById(string instanceId, out CVRSyncHelper.PropData propData)
{
foreach (CVRSyncHelper.PropData data in CVRSyncHelper.Props)
{
if (data.InstanceId != instanceId) continue;
propData = data;
return true;
}
propData = null;
return false;
}
private static void PlacePropInAdditiveContentScene(CVRSpawnable spawnable)
{
spawnable.transform.parent.SetParent(null); // Unparent from the prop container
SceneManager.MoveGameObjectToScene(spawnable.transform.parent.gameObject,
SceneManager.GetSceneByName(CVRObjectLoader.AdditiveContentSceneName));
}
private static void PlacePropInPersistantPropsContainer(CVRSpawnable spawnable)
{
spawnable.transform.parent.SetParent(GetOrCreatePropsContainer().transform);
}
private static void DeleteOrRecycleProp(CVRSyncHelper.PropData prop)
{
if (prop.Spawnable == null) prop.Recycle();
else prop.Spawnable.Delete();
if (_heldPropData.Contains(prop)) _heldPropData.Remove(prop);
}
private static void SpawnPropFromGuid(string propGuid, Vector3 position, Vector3 rotation, Vector3 identityKey)
{
using DarkRiftWriter darkRiftWriter = DarkRiftWriter.Create();
darkRiftWriter.Write(propGuid);
darkRiftWriter.Write(position.x);
darkRiftWriter.Write(position.y);
darkRiftWriter.Write(position.z);
darkRiftWriter.Write(rotation.x);
darkRiftWriter.Write(rotation.y);
darkRiftWriter.Write(rotation.z);
darkRiftWriter.Write(identityKey.x); // for scale, but unused by CVR
darkRiftWriter.Write(identityKey.y); // we will use this to identify our prop
darkRiftWriter.Write(identityKey.z); // and recycle existing instance if it exists
darkRiftWriter.Write(0f); // if not zero, prop spawn will be rejected by gs
using Message message = Message.Create(10050, darkRiftWriter);
NetworkManager.Instance.GameNetwork.SendMessage(message, SendMode.Reliable);
}
private static Vector3 GetIdentityKeyFromPropData(CVRSyncHelper.PropData propData)
=> new(propData.ScaleX, propData.ScaleY, propData.ScaleZ);
private const int WORLD_RAYCAST_LAYER_MASK =
(1 << 0) | // Default
(1 << 16) | (1 << 17) | (1 << 18) | (1 << 19) |
(1 << 20) | (1 << 21) | (1 << 22) | (1 << 23) |
(1 << 24) | (1 << 25) | (1 << 26) | (1 << 27) |
(1 << 28) | (1 << 29) | (1 << 30) | (1 << 31);
private static float GetHeightOffsetFromPlayer()
{
Vector3 playerPos = PlayerSetup.Instance.GetPlayerPosition();
Ray ray = new(playerPos, Vector3.down);
// ReSharper disable once Unity.PreferNonAllocApi
RaycastHit[] hits = Physics.RaycastAll(ray, 1000f, WORLD_RAYCAST_LAYER_MASK, QueryTriggerInteraction.Ignore);
Scene baseScene = SceneManager.GetActiveScene();
float closestDist = float.MaxValue;
Vector3 closestPoint = Vector3.zero;
bool foundValidHit = false;
foreach (RaycastHit hit in hits)
{
if (hit.collider.gameObject.scene != baseScene) continue; // Ignore objects not in the world scene
if (!(hit.distance < closestDist)) continue;
closestDist = hit.distance;
closestPoint = hit.point;
foundValidHit = true;
}
if (!foundValidHit) return 0f; // TODO: idk if i should do this
float offset = playerPos.y - closestPoint.y;
return Mathf.Clamp(offset, 0f, 20f);
}
#endregion Util
#region Helper Classes
private readonly struct SpawnablePositionContainer
{
private readonly CVRSpawnable _spawnable;
private readonly Vector3[] _posOffsets;
private readonly Quaternion[] _rotOffsets;
public SpawnablePositionContainer(CVRSpawnable spawnable)
{
_spawnable = spawnable;
int syncedTransforms = 1 + _spawnable.subSyncs.Count; // root + subSyncs
_posOffsets = new Vector3[syncedTransforms];
_rotOffsets = new Quaternion[syncedTransforms];
Transform playerTransform = PlayerSetup.Instance.transform;
// Save root offset relative to player
Transform _spawnableTransform = _spawnable.transform;
_posOffsets[0] = playerTransform.InverseTransformPoint(_spawnableTransform.position);
_rotOffsets[0] = Quaternion.Inverse(playerTransform.rotation) * _spawnableTransform.rotation;
// Save subSync offsets relative to player
for (int i = 0; i < _spawnable.subSyncs.Count; i++)
{
Transform subSyncTransform = _spawnable.subSyncs[i].transform;
if (subSyncTransform == null) continue;
_posOffsets[i + 1] = playerTransform.InverseTransformPoint(subSyncTransform.position);
_rotOffsets[i + 1] = Quaternion.Inverse(playerTransform.rotation) * subSyncTransform.rotation;
}
}
public void ReapplyOffsets()
{
Transform playerTransform = PlayerSetup.Instance.transform;
// Reapply to root
Vector3 rootWorldPos = playerTransform.TransformPoint(_posOffsets[0]);
rootWorldPos.y += _heightOffset;
_spawnable.transform.position = rootWorldPos;
_spawnable.transform.rotation = playerTransform.rotation * _rotOffsets[0];
// Reapply to subSyncs
for (int i = 0; i < _spawnable.subSyncs.Count; i++)
{
Transform subSyncTransform = _spawnable.subSyncs[i].transform;
if (subSyncTransform == null) continue;
Vector3 subWorldPos = playerTransform.TransformPoint(_posOffsets[i + 1]);
subWorldPos.y += _heightOffset;
subSyncTransform.position = subWorldPos;
subSyncTransform.rotation = playerTransform.rotation * _rotOffsets[i + 1];
}
// hack
_spawnable.needsUpdate = true;
_spawnable.UpdateSubSyncValues();
_spawnable.sendUpdate();
}
}
#endregion Helper Classes
}

View file

@ -0,0 +1,32 @@
using MelonLoader;
using NAK.YouAreMyPropNowWeAreHavingSoftTacosLater.Properties;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.YouAreMyPropNowWeAreHavingSoftTacosLater))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.YouAreMyPropNowWeAreHavingSoftTacosLater))]
[assembly: MelonInfo(
typeof(NAK.YouAreMyPropNowWeAreHavingSoftTacosLater.YouAreMyPropNowWeAreHavingSoftTacosLaterMod),
nameof(NAK.YouAreMyPropNowWeAreHavingSoftTacosLater),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/YouAreMyPropNowWeAreHavingSoftTacosLater"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 246, 25, 99)] // red-pink
[assembly: MelonAuthorColor(255, 158, 21, 32)] // red
[assembly: HarmonyDontPatchAll]
namespace NAK.YouAreMyPropNowWeAreHavingSoftTacosLater.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.0";
public const string Author = "NotAKidoS";
}

View file

@ -0,0 +1,19 @@
# YouAreMyPropNowWeAreHavingSoftTacosLater
Lets you bring held & attached props through world loads.
https://youtu.be/9P6Jeh-VN58?si=eXTPGyKB_0wq1gZO
## Examples
https://fixupx.com/NotAKidoS/status/1910545346922422675
---
Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI.
https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games
> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive.
> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use.
> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive.

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>YouAreMineNow</RootNamespace>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,23 @@
{
"_id": -1,
"name": "YouAreMyPropNowWeAreHavingSoftTacosLater",
"modversion": "1.0.0",
"gameversion": "2025r179",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Lets you bring held & attached props through world loads.\nhttps://youtu.be/9P6Jeh-VN58?si=eXTPGyKB_0wq1gZO",
"searchtags": [
"prop",
"spawn",
"friend",
"load"
],
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r46/YouAreMyPropNowWeAreHavingSoftTacosLater.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/YouAreMyPropNowWeAreHavingSoftTacosLater/",
"changelog": "- Initial Release",
"embedcolor": "#00FFFF"
}