mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 06:19:22 +00:00
CustomSpawnPoint: initial release
This commit is contained in:
parent
46b84107c3
commit
e35cfa4bcd
7 changed files with 378 additions and 0 deletions
2
CustomSpawnPoint/CustomSpawnPoint.csproj
Normal file
2
CustomSpawnPoint/CustomSpawnPoint.csproj
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk" />
|
29
CustomSpawnPoint/Main.cs
Normal file
29
CustomSpawnPoint/Main.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Reflection;
|
||||
using ABI_RC.Core.InteractionSystem;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
|
||||
namespace NAK.CustomSpawnPoint;
|
||||
|
||||
public class CustomSpawnPointMod : MelonMod
|
||||
{
|
||||
internal static MelonLogger.Instance Logger;
|
||||
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
Logger = LoggerInstance;
|
||||
|
||||
SpawnPointManager.Init();
|
||||
|
||||
HarmonyInstance.Patch( // listen for world details page request
|
||||
typeof(ViewManager).GetMethod(nameof(ViewManager.RequestWorldDetailsPage)),
|
||||
new HarmonyMethod(typeof(CustomSpawnPointMod).GetMethod(nameof(OnRequestWorldDetailsPage),
|
||||
BindingFlags.NonPublic | BindingFlags.Static))
|
||||
);
|
||||
}
|
||||
|
||||
private static void OnRequestWorldDetailsPage(string worldId)
|
||||
{
|
||||
SpawnPointManager.OnRequestWorldDetailsPage(worldId);
|
||||
}
|
||||
}
|
32
CustomSpawnPoint/Properties/AssemblyInfo.cs
Normal file
32
CustomSpawnPoint/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using MelonLoader;
|
||||
using NAK.CustomSpawnPoint.Properties;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyTitle(nameof(NAK.CustomSpawnPoint))]
|
||||
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||
[assembly: AssemblyProduct(nameof(NAK.CustomSpawnPoint))]
|
||||
|
||||
[assembly: MelonInfo(
|
||||
typeof(NAK.CustomSpawnPoint.CustomSpawnPointMod),
|
||||
nameof(NAK.CustomSpawnPoint),
|
||||
AssemblyInfoParams.Version,
|
||||
AssemblyInfoParams.Author,
|
||||
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/CustomSpawnPoint"
|
||||
)]
|
||||
|
||||
[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.CustomSpawnPoint.Properties;
|
||||
internal static class AssemblyInfoParams
|
||||
{
|
||||
public const string Version = "1.0.0";
|
||||
public const string Author = "NotAKidoS";
|
||||
}
|
14
CustomSpawnPoint/README.md
Normal file
14
CustomSpawnPoint/README.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# CustomSpawnPoint
|
||||
|
||||
Replaces the unused Images button in the World Details page with a button to set a custom spawn point.
|
||||
|
||||
---
|
||||
|
||||
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.
|
265
CustomSpawnPoint/SpawnPointManager.cs
Normal file
265
CustomSpawnPoint/SpawnPointManager.cs
Normal file
|
@ -0,0 +1,265 @@
|
|||
using ABI_RC.Core.InteractionSystem;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Systems.GameEventSystem;
|
||||
using ABI.CCK.Components;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
using ABI_RC.Core;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NAK.CustomSpawnPoint
|
||||
{
|
||||
internal static class SpawnPointManager
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private static string currentWorldId = string.Empty;
|
||||
private static SpawnPointData? currentSpawnPoint;
|
||||
|
||||
private static string requestedWorldId = string.Empty;
|
||||
private static SpawnPointData? requestedSpawnPoint;
|
||||
|
||||
private static Dictionary<string, SpawnPointData> spawnPoints = new();
|
||||
private static readonly string jsonFilePath = Path.Combine("UserData", "customspawnpoints.json");
|
||||
|
||||
private static GameObject[] customSpawnPointsArray;
|
||||
private static GameObject[] originalSpawnPointsArray;
|
||||
|
||||
#endregion Fields
|
||||
|
||||
#region Initialization
|
||||
|
||||
internal static void Init()
|
||||
{
|
||||
LoadSpawnpoints();
|
||||
CVRGameEventSystem.World.OnLoad.AddListener(OnWorldLoaded);
|
||||
CVRGameEventSystem.World.OnUnload.AddListener(OnWorldUnloaded);
|
||||
MelonLoader.MelonCoroutines.Start(WaitMainMenuUi());
|
||||
}
|
||||
|
||||
private static System.Collections.IEnumerator WaitMainMenuUi()
|
||||
{
|
||||
while (ViewManager.Instance == null)
|
||||
yield return null;
|
||||
while (ViewManager.Instance.gameMenuView == null)
|
||||
yield return null;
|
||||
while (ViewManager.Instance.gameMenuView.Listener == null)
|
||||
yield return null;
|
||||
|
||||
ViewManager.Instance.OnUiConfirm.AddListener(OnClearSpawnpointConfirm);
|
||||
ViewManager.Instance.gameMenuView.Listener.FinishLoad += (_) =>
|
||||
{
|
||||
ViewManager.Instance.gameMenuView.View._view.ExecuteScript(spawnpointJs);
|
||||
};
|
||||
ViewManager.Instance.gameMenuView.Listener.ReadyForBindings += () =>
|
||||
{
|
||||
// listen for setting the spawn point on our custom button
|
||||
ViewManager.Instance.gameMenuView.View.BindCall("NAKCallSetSpawnpoint", SetSpawnPoint);
|
||||
};
|
||||
|
||||
// create our custom spawn point object
|
||||
GameObject customSpawnPointObject = new("[CustomSpawnPoint]");
|
||||
Object.DontDestroyOnLoad(customSpawnPointObject);
|
||||
|
||||
// add to array so we can easily replace worlds spawn points
|
||||
customSpawnPointsArray = new[] { customSpawnPointObject };
|
||||
}
|
||||
|
||||
#endregion Initialization
|
||||
|
||||
#region Game Events
|
||||
|
||||
private static void OnWorldLoaded(string worldId)
|
||||
{
|
||||
CVRWorld world = CVRWorld.Instance;
|
||||
if (world == null) return;
|
||||
|
||||
CustomSpawnPointMod.Logger.Msg("World loaded: " + worldId);
|
||||
|
||||
currentWorldId = worldId;
|
||||
currentSpawnPoint = spawnPoints.GetValueOrDefault(currentWorldId);
|
||||
originalSpawnPointsArray ??= world.spawns; // cache the original spawn points array, if null its fine
|
||||
|
||||
if (currentSpawnPoint.HasValue)
|
||||
{
|
||||
UpdateCustomSpawnPointTransform(currentSpawnPoint.Value);
|
||||
world.spawns = customSpawnPointsArray; // set the custom spawn points array
|
||||
|
||||
// CVRWorld.Awake already moved player, but OnWorldLoaded is invoked in OnEnable
|
||||
RootLogic.Instance.SpawnOnWorldInstance();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnWorldUnloaded(string worldId)
|
||||
{
|
||||
ClearCurrentWorldState();
|
||||
}
|
||||
|
||||
internal static void OnRequestWorldDetailsPage(string worldId)
|
||||
{
|
||||
CustomSpawnPointMod.Logger.Msg("Requesting world details page for world: " + worldId);
|
||||
|
||||
requestedWorldId = worldId;
|
||||
requestedSpawnPoint = spawnPoints.TryGetValue(requestedWorldId, out SpawnPointData spawnPoint) ? spawnPoint : null;
|
||||
|
||||
bool hasSpawnpoint = requestedSpawnPoint.HasValue;
|
||||
UpdateMenuButtonState(hasSpawnpoint, worldId == currentWorldId && CVRWorld.Instance != null && CVRWorld.Instance.allowFlying);
|
||||
}
|
||||
|
||||
private static void OnClearSpawnpointConfirm(string id, string value, string data)
|
||||
{
|
||||
if (id != "nak_clear_spawnpoint") return;
|
||||
if (value == "true") ClearSpawnPoint();
|
||||
}
|
||||
|
||||
#endregion Game Events
|
||||
|
||||
#region Spawnpoint Management
|
||||
|
||||
public static void SetSpawnPoint()
|
||||
=> SetSpawnPointForWorld(currentWorldId);
|
||||
|
||||
public static void ClearSpawnPoint()
|
||||
=> ClearSpawnPointForWorld(currentWorldId);
|
||||
|
||||
private static void SetSpawnPointForWorld(string worldId)
|
||||
{
|
||||
CustomSpawnPointMod.Logger.Msg("Setting spawn point for world: " + worldId);
|
||||
|
||||
Vector3 playerPosition = PlayerSetup.Instance.GetPlayerPosition();
|
||||
Quaternion playerRotation = PlayerSetup.Instance.GetPlayerRotation();
|
||||
|
||||
// Update or create the spawn point data
|
||||
SpawnPointData spawnPoint = new()
|
||||
{
|
||||
Position = playerPosition,
|
||||
Rotation = playerRotation.eulerAngles
|
||||
};
|
||||
|
||||
spawnPoints[worldId] = spawnPoint;
|
||||
|
||||
// update the current world state if applicable
|
||||
if (worldId == currentWorldId)
|
||||
{
|
||||
currentSpawnPoint = spawnPoint;
|
||||
UpdateCustomSpawnPointTransform(spawnPoint);
|
||||
// update the custom spawn points array
|
||||
if (CVRWorld.Instance != null) CVRWorld.Instance.spawns = customSpawnPointsArray;
|
||||
ViewManager.Instance.NotifyUser("(Local) Client", "Set custom spawnpoint", 2f);
|
||||
}
|
||||
|
||||
SaveSpawnpoints();
|
||||
UpdateMenuButtonState(true, worldId == currentWorldId);
|
||||
}
|
||||
|
||||
private static void ClearSpawnPointForWorld(string worldId)
|
||||
{
|
||||
CustomSpawnPointMod.Logger.Msg("Clearing spawn point for world: " + worldId);
|
||||
|
||||
if (spawnPoints.ContainsKey(worldId))
|
||||
spawnPoints.Remove(worldId);
|
||||
|
||||
if (worldId == currentWorldId)
|
||||
{
|
||||
currentSpawnPoint = null;
|
||||
|
||||
// restore the original spawn points array
|
||||
if (CVRWorld.Instance != null) CVRWorld.Instance.spawns = originalSpawnPointsArray;
|
||||
ViewManager.Instance.NotifyUser("(Local) Client", "Cleared custom spawnpoint", 2f);
|
||||
}
|
||||
|
||||
SaveSpawnpoints();
|
||||
UpdateMenuButtonState(false, worldId == currentWorldId);
|
||||
}
|
||||
|
||||
private static void UpdateCustomSpawnPointTransform(SpawnPointData spawnPoint)
|
||||
{
|
||||
customSpawnPointsArray[0].transform.SetPositionAndRotation(spawnPoint.Position, Quaternion.Euler(spawnPoint.Rotation));
|
||||
}
|
||||
|
||||
private static void UpdateMenuButtonState(bool hasSpawnpoint, bool isInWorld)
|
||||
{
|
||||
ViewManager.Instance.gameMenuView.View.TriggerEvent("NAKUpdateSpawnpointStatus", hasSpawnpoint.ToString(), isInWorld.ToString());
|
||||
}
|
||||
|
||||
private static void ClearCurrentWorldState()
|
||||
{
|
||||
currentWorldId = string.Empty;
|
||||
currentSpawnPoint = null;
|
||||
originalSpawnPointsArray = null;
|
||||
}
|
||||
|
||||
#endregion Spawnpoint Management
|
||||
|
||||
#region JSON Management
|
||||
|
||||
private static void LoadSpawnpoints()
|
||||
{
|
||||
if (File.Exists(jsonFilePath))
|
||||
{
|
||||
string json = File.ReadAllText(jsonFilePath);
|
||||
spawnPoints = JsonConvert.DeserializeObject<Dictionary<string, SpawnPointData>>(json);
|
||||
}
|
||||
else
|
||||
{
|
||||
SaveSpawnpoints(); // create the file if it doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveSpawnpoints()
|
||||
{
|
||||
File.WriteAllText(jsonFilePath, JsonConvert.SerializeObject(spawnPoints, Formatting.Indented,
|
||||
new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore } // death
|
||||
));
|
||||
}
|
||||
|
||||
#endregion JSON Management
|
||||
|
||||
#region Spawnpoint JS
|
||||
|
||||
private const string spawnpointJs = @"
|
||||
let hasSpawnpointForThisWorld = false;
|
||||
let spawnpointButton = null;
|
||||
|
||||
// replace the screenshot button with ours
|
||||
spawnpointButton = document.querySelector('.action-btn.button.data-worldPreload.row2.col2.disabled');
|
||||
if (spawnpointButton) {
|
||||
spawnpointButton.classList.remove('disabled');
|
||||
spawnpointButton.innerHTML = '<img src=""gfx/respawn.svg""><span>Spawnpoint</span>';
|
||||
spawnpointButton.setAttribute('onclick', 'onClickSpawnpointButton();');
|
||||
}
|
||||
|
||||
function onClickSpawnpointButton(){
|
||||
if (spawnpointButton.classList.contains('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasSpawnpointForThisWorld) {
|
||||
uiConfirmShow(""Custom Spawnpoint"", ""Are you sure you want to clear your spawnpoint?"", ""nak_clear_spawnpoint"", """");
|
||||
} else {
|
||||
engine.call('NAKCallSetSpawnpoint');
|
||||
}
|
||||
}
|
||||
|
||||
engine.on('NAKUpdateSpawnpointStatus', function (hasSpawnpoint, isInWorld) {
|
||||
hasSpawnpointForThisWorld = hasSpawnpoint === 'True';
|
||||
if (spawnpointButton) {
|
||||
spawnpointButton.classList.toggle('disabled', isInWorld !== 'True');
|
||||
}
|
||||
});
|
||||
";
|
||||
|
||||
#endregion Spawnpoint JS
|
||||
}
|
||||
|
||||
#region Serializable
|
||||
|
||||
[Serializable]
|
||||
public struct SpawnPointData
|
||||
{
|
||||
public Vector3 Position;
|
||||
public Vector3 Rotation;
|
||||
}
|
||||
|
||||
#endregion Serializable
|
||||
}
|
24
CustomSpawnPoint/format.json
Normal file
24
CustomSpawnPoint/format.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"_id": -1,
|
||||
"name": "CustomSpawnPoint",
|
||||
"modversion": "1.0.0",
|
||||
"gameversion": "2024r175",
|
||||
"loaderversion": "0.6.1",
|
||||
"modtype": "Mod",
|
||||
"author": "NotAKidoS",
|
||||
"description": "Replaces the unused Images button in the World Details page with a button to set a custom spawn point.",
|
||||
"searchtags": [
|
||||
"spawn",
|
||||
"point",
|
||||
"custom",
|
||||
"world",
|
||||
"load"
|
||||
],
|
||||
"requirements": [
|
||||
"None"
|
||||
],
|
||||
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r36/CustomSpawnPoint.dll",
|
||||
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/CustomSpawnPoint/",
|
||||
"changelog": "- Initial Release",
|
||||
"embedcolor": "#f61963"
|
||||
}
|
|
@ -81,6 +81,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ASTExtension", "ASTExtensio
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HeadLookLockingInputFix", "HeadLookLockingInputFix\HeadLookLockingInputFix.csproj", "{AAE8A952-2764-43CF-A5D7-515924CA2D9F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvatarQueueSystemTweaks", "AvatarQueueSystemTweaks\AvatarQueueSystemTweaks.csproj", "{D178E422-283B-4FB3-89A6-AA4FB9F87E2F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomSpawnPoint", "CustomSpawnPoint\CustomSpawnPoint.csproj", "{51CA34CA-7684-4819-AC9E-89DFAD63E9AB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -243,6 +247,14 @@ Global
|
|||
{AAE8A952-2764-43CF-A5D7-515924CA2D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AAE8A952-2764-43CF-A5D7-515924CA2D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AAE8A952-2764-43CF-A5D7-515924CA2D9F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D178E422-283B-4FB3-89A6-AA4FB9F87E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D178E422-283B-4FB3-89A6-AA4FB9F87E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D178E422-283B-4FB3-89A6-AA4FB9F87E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D178E422-283B-4FB3-89A6-AA4FB9F87E2F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{51CA34CA-7684-4819-AC9E-89DFAD63E9AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{51CA34CA-7684-4819-AC9E-89DFAD63E9AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{51CA34CA-7684-4819-AC9E-89DFAD63E9AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{51CA34CA-7684-4819-AC9E-89DFAD63E9AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue