mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-03 06:49:22 +00:00
Compare commits
14 commits
c13e48638c
...
2af27cc81d
Author | SHA1 | Date | |
---|---|---|---|
|
2af27cc81d | ||
|
784249a08b | ||
|
1fbfcd80cd | ||
|
d5eb9ae1a0 | ||
|
88faf93b3b | ||
|
a2e29149e2 | ||
|
8f8f2ad1fb | ||
|
a2aa3b9871 | ||
|
febd9f2741 | ||
|
b246f71e6e | ||
|
0cde9ecb21 | ||
|
964d6009b7 | ||
|
392390cde7 | ||
|
ba26a1faae |
13 changed files with 714 additions and 52 deletions
|
@ -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.
|
||||
|
||||
---
|
||||
|
158
.github/scripts/update-modlist.js
vendored
158
.github/scripts/update-modlist.js
vendored
|
@ -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();
|
|
@ -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
|
||||
|
|
39
README.md
39
README.md
|
@ -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 -->
|
||||
|
||||
|
|
479
YouAreMyPropNowWeAreHavingSoftTacosLater/Main.cs
Normal file
479
YouAreMyPropNowWeAreHavingSoftTacosLater/Main.cs
Normal 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
|
||||
}
|
|
@ -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";
|
||||
}
|
19
YouAreMyPropNowWeAreHavingSoftTacosLater/README.md
Normal file
19
YouAreMyPropNowWeAreHavingSoftTacosLater/README.md
Normal 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.
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>YouAreMineNow</RootNamespace>
|
||||
</PropertyGroup>
|
||||
</Project>
|
23
YouAreMyPropNowWeAreHavingSoftTacosLater/format.json
Normal file
23
YouAreMyPropNowWeAreHavingSoftTacosLater/format.json
Normal 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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue