CustomSpawnPoint: initial release

This commit is contained in:
NotAKidoS 2024-08-05 19:22:16 -05:00
parent 46b84107c3
commit e35cfa4bcd
7 changed files with 378 additions and 0 deletions

View file

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

29
CustomSpawnPoint/Main.cs Normal file
View 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);
}
}

View 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";
}

View 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.

View 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
}

View 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"
}

View file

@ -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