Merge pull request #155 from FreezeDriedMangos/feat/vision-torches

vision torches
This commit is contained in:
Nick 2022-05-25 18:33:17 -04:00 committed by GitHub
commit 89169d7560
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 368 additions and 26 deletions

View File

@ -1,4 +1,4 @@
using NewHorizons.External.Configs; using NewHorizons.External.Configs;
using NewHorizons.External.Modules; using NewHorizons.External.Modules;
using NewHorizons.Handlers; using NewHorizons.Handlers;
using NewHorizons.Utility; using NewHorizons.Utility;
@ -153,6 +153,14 @@ namespace NewHorizons.Builder.Props
{ {
socket._sector = sector; socket._sector = sector;
} }
// Fix vision torch
if (component is VisionTorchItem torchItem)
{
torchItem.enabled = true;
torchItem.mindProjectorTrigger.enabled = true;
torchItem.mindSlideProjector._mindProjectorImageEffect = GameObject.Find("Player_Body/PlayerCamera").GetComponent<MindProjectorImageEffect>();
}
} }
else else
{ {

View File

@ -1,10 +1,11 @@
using NewHorizons.External.Modules; using NewHorizons.External.Modules;
using NewHorizons.Handlers; using NewHorizons.Handlers;
using NewHorizons.Utility; using NewHorizons.Utility;
using OWML.Common; using OWML.Common;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using static NewHorizons.External.Modules.PropModule;
using Logger = NewHorizons.Utility.Logger; using Logger = NewHorizons.Utility.Logger;
namespace NewHorizons.Builder.Props namespace NewHorizons.Builder.Props
{ {
@ -24,6 +25,12 @@ namespace NewHorizons.Builder.Props
case PropModule.ProjectionInfo.SlideShowType.SlideReel: case PropModule.ProjectionInfo.SlideShowType.SlideReel:
MakeSlideReel(go, sector, info, mod); MakeSlideReel(go, sector, info, mod);
break; break;
case PropModule.ProjectionInfo.SlideShowType.VisionTorchTarget:
MakeMindSlidesTarget(go, sector, info, mod);
break;
case PropModule.ProjectionInfo.SlideShowType.StandingVisionTorch:
MakeStandingVisionTorch(go, sector, info, mod);
break;
default: default:
Logger.LogError($"Invalid projection type {info.type}"); Logger.LogError($"Invalid projection type {info.type}");
break; break;
@ -68,22 +75,52 @@ namespace NewHorizons.Builder.Props
// The base game ones only have 15 slides max // The base game ones only have 15 slides max
var textures = new Texture2D[slidesCount >= 15 ? 15 : slidesCount]; var textures = new Texture2D[slidesCount >= 15 ? 15 : slidesCount];
var imageLoader = slideReelObj.AddComponent<AsyncImageLoader>();
for (int i = 0; i < slidesCount; i++) for (int i = 0; i < slidesCount; i++)
{ {
var slide = new Slide(); var slide = new Slide();
var slideInfo = info.slides[i]; var slideInfo = info.slides[i];
var texture = ImageUtilities.GetTexture(mod, slideInfo.imagePath); imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath);
slide.textureOverride = ImageUtilities.Invert(texture);
// Track the first 15 to put on the slide reel object
if (i < 15) textures[i] = texture;
AddModules(slideInfo, ref slide); AddModules(slideInfo, ref slide);
slideCollection.slides[i] = slide; slideCollection.slides[i] = slide;
} }
// this variable just lets us track how many of the first 15 slides have been loaded.
// this way as soon as the last one is loaded (due to async loading, this may be
// slide 7, or slide 3, or whatever), we can build the slide reel texture. This allows us
// to avoid doing a "is every element in the array `textures` not null" check every time a texture finishes loading
int displaySlidesLoaded = 0;
imageLoader.imageLoadedEvent.AddListener(
(Texture2D tex, int index) =>
{
slideCollection.slides[index].textureOverride = ImageUtilities.Invert(tex);
// Track the first 15 to put on the slide reel object
if (index < 15)
{
textures[index] = tex;
displaySlidesLoaded++; // threading moment
}
if (displaySlidesLoaded >= textures.Length)
{
// all textures required to build the reel's textures have been loaded
var slidesBack = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Back").GetComponent<MeshRenderer>();
var slidesFront = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Front").GetComponent<MeshRenderer>();
// Now put together the textures into a 4x4 thing for the materials
var reelTexture = ImageUtilities.MakeReelTexture(textures);
slidesBack.material.mainTexture = reelTexture;
slidesBack.material.SetTexture(EmissionMap, reelTexture);
slidesFront.material.mainTexture = reelTexture;
slidesFront.material.SetTexture(EmissionMap, reelTexture);
}
}
);
// Else when you put them down you can't pick them back up // Else when you put them down you can't pick them back up
slideReelObj.GetComponent<OWCollider>()._physicsRemoved = false; slideReelObj.GetComponent<OWCollider>()._physicsRemoved = false;
@ -95,16 +132,6 @@ namespace NewHorizons.Builder.Props
OWAssetHandler.LoadObject(slideReelObj); OWAssetHandler.LoadObject(slideReelObj);
sector.OnOccupantEnterSector.AddListener((x) => OWAssetHandler.LoadObject(slideReelObj)); sector.OnOccupantEnterSector.AddListener((x) => OWAssetHandler.LoadObject(slideReelObj));
var slidesBack = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Back").GetComponent<MeshRenderer>();
var slidesFront = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Front").GetComponent<MeshRenderer>();
// Now put together the textures into a 4x4 thing for the materials
var reelTexture = ImageUtilities.MakeReelTexture(textures);
slidesBack.material.mainTexture = reelTexture;
slidesBack.material.SetTexture(EmissionMap, reelTexture);
slidesFront.material.mainTexture = reelTexture;
slidesFront.material.SetTexture(EmissionMap, reelTexture);
slideReelObj.SetActive(true); slideReelObj.SetActive(true);
} }
@ -137,18 +164,19 @@ namespace NewHorizons.Builder.Props
int slidesCount = info.slides.Length; int slidesCount = info.slides.Length;
var slideCollection = new SlideCollection(slidesCount); var slideCollection = new SlideCollection(slidesCount);
var imageLoader = projectorObj.AddComponent<AsyncImageLoader>();
for (int i = 0; i < slidesCount; i++) for (int i = 0; i < slidesCount; i++)
{ {
var slide = new Slide(); var slide = new Slide();
var slideInfo = info.slides[i]; var slideInfo = info.slides[i];
var texture = ImageUtilities.GetTexture(mod, slideInfo.imagePath); imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath);
slide.textureOverride = ImageUtilities.Invert(texture);
AddModules(slideInfo, ref slide); AddModules(slideInfo, ref slide);
slideCollection.slides[i] = slide; slideCollection.slides[i] = slide;
} }
imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index].textureOverride = ImageUtilities.Invert(tex); });
slideCollectionContainer.slideCollection = slideCollection; slideCollectionContainer.slideCollection = slideCollection;
@ -163,6 +191,143 @@ namespace NewHorizons.Builder.Props
projectorObj.SetActive(true); projectorObj.SetActive(true);
} }
// Makes a target for a vision torch to scan
public static GameObject MakeMindSlidesTarget(GameObject planetGO, Sector sector, PropModule.ProjectionInfo info, IModBehaviour mod)
{
// spawn a trigger for the vision torch
var path = "DreamWorld_Body/Sector_DreamWorld/Sector_Underground/Sector_PrisonCell/Ghosts_PrisonCell/GhostNodeMap_PrisonCell_Lower/Prefab_IP_GhostBird_Prisoner/Ghostbird_IP_ANIM/Ghostbird_Skin_01:Ghostbird_Rig_V01:Base/Ghostbird_Skin_01:Ghostbird_Rig_V01:Root/Ghostbird_Skin_01:Ghostbird_Rig_V01:Spine01/Ghostbird_Skin_01:Ghostbird_Rig_V01:Spine02/Ghostbird_Skin_01:Ghostbird_Rig_V01:Spine03/Ghostbird_Skin_01:Ghostbird_Rig_V01:Spine04/Ghostbird_Skin_01:Ghostbird_Rig_V01:Neck01/Ghostbird_Skin_01:Ghostbird_Rig_V01:Neck02/Ghostbird_Skin_01:Ghostbird_Rig_V01:Head/PrisonerHeadDetector";
var g = DetailBuilder.MakeDetail(planetGO, sector, path, info.position, Vector3.zero, 2, false);
if (g == null)
{
Logger.LogWarning($"Tried to make a vision torch target but couldn't. Do you have the DLC installed?");
return null;
}
g.name = "VisionStaffDetector";
// The number of slides is unlimited, 15 is only for texturing the actual slide reel item. This is not a slide reel item
var slides = info.slides;
var slidesCount = slides.Length;
var slideCollection = new SlideCollection(slidesCount);
var imageLoader = g.AddComponent<AsyncImageLoader>();
for (int i = 0; i < slidesCount; i++)
{
var slide = new Slide();
var slideInfo = slides[i];
imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath);
AddModules(slideInfo, ref slide);
slideCollection.slides[i] = slide;
}
imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index].textureOverride = tex; });
// attatch a component to store all the data for the slides that play when a vision torch scans this target
var target = g.AddComponent<VisionTorchTarget>();
var slideCollectionContainer = g.AddComponent<SlideCollectionContainer>();
slideCollectionContainer.slideCollection = slideCollection;
target.slideCollection = g.AddComponent<MindSlideCollection>();
target.slideCollection._slideCollectionContainer = slideCollectionContainer;
target.slideCollectionContainer = slideCollectionContainer;
// Idk why but it wants reveals to be comma delimited not a list
if (info.reveals != null) slideCollectionContainer._shipLogOnComplete = string.Join(",", info.reveals);
return g;
}
public static GameObject MakeStandingVisionTorch(GameObject planetGO, Sector sector, PropModule.ProjectionInfo info, IModBehaviour mod)
{
//
// spawn the torch itself
//
var path = "RingWorld_Body/Sector_RingWorld/Sector_SecretEntrance/Interactibles_SecretEntrance/Experiment_1/VisionTorchApparatus/VisionTorchRoot/Prefab_IP_VisionTorchProjector";
var standingTorch = DetailBuilder.MakeDetail(planetGO, sector, path, info.position, info.rotation, 1, false);
if (standingTorch == null)
{
Logger.LogWarning($"Tried to make a vision torch target but couldn't. Do you have the DLC installed?");
return null;
}
//
// set some required properties on the torch
//
var mindSlideProjector = standingTorch.GetComponent<MindSlideProjector>();
mindSlideProjector._mindProjectorImageEffect = GameObject.Find("Player_Body/PlayerCamera").GetComponent<MindProjectorImageEffect>();
// setup for visually supporting async texture loading
mindSlideProjector.enabled = false;
var visionBeamEffect = SearchUtilities.FindChild(standingTorch, "VisionBeam");
visionBeamEffect.SetActive(false);
//
// set up slides
//
// The number of slides is unlimited, 15 is only for texturing the actual slide reel item. This is not a slide reel item
var slides = info.slides;
var slidesCount = slides.Length;
var slideCollection = new SlideCollection(slidesCount);
var imageLoader = standingTorch.AddComponent<AsyncImageLoader>();
for (int i = 0; i < slidesCount; i++)
{
var slide = new Slide();
var slideInfo = slides[i];
imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath);
AddModules(slideInfo, ref slide);
slideCollection.slides[i] = slide;
}
// this variable just lets us track how many of the slides have been loaded.
// this way as soon as the last one is loaded (due to async loading, this may be
// slide 7, or slide 3, or whatever), we can enable the vision torch. This allows us
// to avoid doing a "is every element in the array `slideCollection.slides` not null" check every time a texture finishes loading
int displaySlidesLoaded = 0;
imageLoader.imageLoadedEvent.AddListener(
(Texture2D tex, int index) =>
{
slideCollection.slides[index].textureOverride = tex;
displaySlidesLoaded++; // threading moment
if (displaySlidesLoaded >= slides.Length)
{
mindSlideProjector.enabled = true;
visionBeamEffect.SetActive(true);
}
}
);
// set up the containers for the slides
var slideCollectionContainer = standingTorch.AddComponent<SlideCollectionContainer>();
slideCollectionContainer.slideCollection = slideCollection;
var mindSlideCollection = standingTorch.AddComponent<MindSlideCollection>();
mindSlideCollection._slideCollectionContainer = slideCollectionContainer;
// make sure that these slides play when the player wanders into the beam
// _slideCollectionItem is actually a reference to a SlideCollectionContainer. Not a slide reel item
mindSlideProjector._mindSlideCollection = mindSlideCollection;
mindSlideProjector._slideCollectionItem = slideCollectionContainer;
mindSlideProjector.SetMindSlideCollection(mindSlideCollection);
// Idk why but it wants reveals to be comma delimited not a list
if (info.reveals != null) slideCollectionContainer._shipLogOnComplete = string.Join(",", info.reveals);
return standingTorch;
}
private static void AddModules(PropModule.SlideInfo slideInfo, ref Slide slide) private static void AddModules(PropModule.SlideInfo slideInfo, ref Slide slide)
{ {
var modules = new List<SlideFunctionModule>(); var modules = new List<SlideFunctionModule>();
@ -211,4 +376,10 @@ namespace NewHorizons.Builder.Props
Slide.WriteModules(modules, ref slide._modulesList, ref slide._modulesData, ref slide.lengths); Slide.WriteModules(modules, ref slide._modulesList, ref slide._modulesData, ref slide.lengths);
} }
} }
public class VisionTorchTarget : MonoBehaviour
{
public MindSlideCollection slideCollection;
public SlideCollectionContainer slideCollectionContainer;
}
} }

View File

@ -1,7 +1,8 @@
using System.ComponentModel;
using NewHorizons.Utility;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using NewHorizons.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
@ -488,7 +489,12 @@ namespace NewHorizons.External.Modules
{ {
[EnumMember(Value = @"slideReel")] SlideReel = 0, [EnumMember(Value = @"slideReel")] SlideReel = 0,
[EnumMember(Value = @"autoProjector")] AutoProjector = 1 [EnumMember(Value = @"autoProjector")] AutoProjector = 1,
[EnumMember(Value = @"visionTorchTarget")] VisionTorchTarget = 2,
[EnumMember(Value = @"standingVisionTorch")] StandingVisionTorch = 3,
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,4 @@
using NewHorizons.Builder.Body; using NewHorizons.Builder.Body;
using NewHorizons.External.Modules; using NewHorizons.External.Modules;
using NewHorizons.Utility; using NewHorizons.Utility;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HarmonyLib;
namespace NewHorizons.Patches
{
[HarmonyPatch]
public static class ToolModeSwapperPatches
{
// Patches ToolModeSwapper.EquipToolMode(ToolMode mode) to deny swaps if you're holding a vision torch.
// This is critical for preventing swapping to the scout launcher (causes memory slides to fail) but it
// just doesn't look right when you switch to other stuff (eg the signalscope), so I'm disabling swapping tools entirely
// the correct way to do this is to patch ToolModeSwapper.Update to be exactly the same as it is now, but change the below line
// to include a check for "is holding vision torch", but I'm not copy/pasting an entire function, no sir
// if (((_currentToolMode == ToolMode.None || _currentToolMode == ToolMode.Item) && Locator.GetPlayerSuit().IsWearingSuit(includeTrainingSuit: false)) || ((_currentToolMode == ToolMode.None || _currentToolMode == ToolMode.SignalScope) && OWInput.IsInputMode(InputMode.ShipCockpit)))
[HarmonyPrefix]
[HarmonyPatch(typeof(ToolModeSwapper), nameof(ToolModeSwapper.EquipToolMode))]
public static bool ToolModeSwapper_EquipToolMode(ToolModeSwapper __instance, ToolMode mode)
{
var isHoldingVisionTorch = __instance.GetItemCarryTool()?.GetHeldItemType() == ItemType.VisionTorch;
var swappingToRestrictedTool =
mode == ToolMode.Probe ||
mode == ToolMode.SignalScope ||
mode == ToolMode.Translator;
if (isHoldingVisionTorch && swappingToRestrictedTool) return false;
return true;
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HarmonyLib;
using NewHorizons.Builder.Props;
using UnityEngine;
namespace NewHorizons.Patches
{
[HarmonyPatch]
public static class MindProjectorTriggerPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(MindProjectorTrigger), nameof(MindProjectorTrigger.OnTriggerVolumeEntry))]
public static bool MindProjectorTrigger_OnTriggerVolumeEntry(MindProjectorTrigger __instance, GameObject hitObj)
{
var t = hitObj.GetComponent<VisionTorchTarget>();
if (t != null) //(hitObj.CompareTag("PrisonerDetector"))
{
// _slideCollectionItem is actually a reference to a SlideCollectionContainer. Not a slide reel item
__instance._mindProjector._slideCollectionItem = t.slideCollectionContainer;
__instance._mindProjector._mindSlideCollection = t.slideCollection;
__instance._mindProjector.SetMindSlideCollection(t.slideCollection);
__instance.OnBeamStartHitPrisoner.Invoke();
__instance._mindProjector.Play(reset: true);
__instance._mindProjector.OnProjectionStart += new OWEvent.OWCallback(__instance.OnProjectionStart);
__instance._mindProjector.OnProjectionComplete += new OWEvent.OWCallback(__instance.OnProjectionComplete);
Locator.GetPlayerTransform().GetComponent<PlayerLockOnTargeting>().LockOn(hitObj.transform, Vector3.zero);
__instance._playerLockedOn = true;
return false;
}
return true;
}
}
[HarmonyPatch]
public static class VisionTorchItemPatches
{
// This is some dark magic
// this creates a method called base_DropItem that basically just calls OWItem.PickUpItem whenever it (VisionTorchItemPatches.base_PickUpItem) is called
[HarmonyReversePatch]
[HarmonyPatch(typeof(OWItem), nameof(OWItem.DropItem))]
private static void base_DropItem(OWItem instance, Vector3 position, Vector3 normal, Transform parent, Sector sector, IItemDropTarget customDropTarget) { }
// Make the vision torch droppable. In the base game you can only drop it if you're in the dream world.
[HarmonyPrefix]
[HarmonyPatch(typeof(VisionTorchItem), nameof(VisionTorchItem.DropItem))]
public static bool VisionTorchItem_DropItem(VisionTorchItem __instance, Vector3 position, Vector3 normal, Transform parent, Sector sector, IItemDropTarget customDropTarget)
{
if (!Locator.GetDreamWorldController().IsInDream())
{
base_DropItem(__instance, position, normal, parent, sector, customDropTarget);
}
return true;
}
}
}

View File

@ -1,8 +1,12 @@
using OWML.Common; using OWML.Common;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using UnityEngine; using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
namespace NewHorizons.Utility namespace NewHorizons.Utility
{ {
public static class ImageUtilities public static class ImageUtilities
@ -314,4 +318,46 @@ namespace NewHorizons.Utility
return newTexture; return newTexture;
} }
} }
// Modified from https://stackoverflow.com/a/69141085/9643841
public class AsyncImageLoader : MonoBehaviour
{
public List<string> pathsToLoad = new List<string>();
public class ImageLoadedEvent : UnityEvent<Texture2D, int> { }
public ImageLoadedEvent imageLoadedEvent = new ImageLoadedEvent();
// TODO: set up an optional “StartLoading” and “StartUnloading” condition on AsyncTextureLoader,
// and make use of that for at least for projector stuff (require player to be in the same sector as the slides
// for them to start loading, and unload when the player leaves)
void Start()
{
for (int i = 0; i < pathsToLoad.Count; i++)
{
StartCoroutine(DownloadTexture(pathsToLoad[i], i));
}
}
IEnumerator DownloadTexture(string url, int index)
{
using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(url))
{
yield return uwr.SendWebRequest();
var hasError = uwr.error != null && uwr.error != "";
if (hasError) // (uwr.result != UnityWebRequest.Result.Success)
{
Debug.Log(uwr.error);
}
else
{
// Get downloaded asset bundle
var texture = DownloadHandlerTexture.GetContent(uwr);
imageLoadedEvent.Invoke(texture, index);
}
}
}
}
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using UnityEngine; using UnityEngine;
@ -119,6 +119,16 @@ namespace NewHorizons.Utility
} }
*/ */
public static GameObject FindChild(GameObject g, string childName)
{
foreach(Transform child in g.transform)
{
if (child.gameObject.name == childName) return child.gameObject;
}
return null;
}
public static GameObject Find(string path) public static GameObject Find(string path)
{ {
if (CachedGameObjects.ContainsKey(path)) if (CachedGameObjects.ContainsKey(path))