diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index 154a6f4b..1fe65895 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -1,4 +1,4 @@ -using NewHorizons.External.Configs; +using NewHorizons.External.Configs; using NewHorizons.External.Modules; using NewHorizons.Handlers; using NewHorizons.Utility; @@ -153,6 +153,14 @@ namespace NewHorizons.Builder.Props { 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(); + } } else { @@ -219,4 +227,4 @@ namespace NewHorizons.Builder.Props return prop; } } -} +} \ No newline at end of file diff --git a/NewHorizons/Builder/Props/ProjectionBuilder.cs b/NewHorizons/Builder/Props/ProjectionBuilder.cs index 4b111c86..de3a9a3e 100644 --- a/NewHorizons/Builder/Props/ProjectionBuilder.cs +++ b/NewHorizons/Builder/Props/ProjectionBuilder.cs @@ -1,10 +1,11 @@ -using NewHorizons.External.Modules; +using NewHorizons.External.Modules; using NewHorizons.Handlers; using NewHorizons.Utility; using OWML.Common; using System; using System.Collections.Generic; using UnityEngine; +using static NewHorizons.External.Modules.PropModule; using Logger = NewHorizons.Utility.Logger; namespace NewHorizons.Builder.Props { @@ -24,6 +25,12 @@ namespace NewHorizons.Builder.Props case PropModule.ProjectionInfo.SlideShowType.SlideReel: MakeSlideReel(go, sector, info, mod); break; + case PropModule.ProjectionInfo.SlideShowType.VisionTorchTarget: + MakeMindSlidesTarget(go, sector, info, mod); + break; + case PropModule.ProjectionInfo.SlideShowType.StandingVisionTorch: + MakeStandingVisionTorch(go, sector, info, mod); + break; default: Logger.LogError($"Invalid projection type {info.type}"); break; @@ -68,21 +75,51 @@ namespace NewHorizons.Builder.Props // The base game ones only have 15 slides max var textures = new Texture2D[slidesCount >= 15 ? 15 : slidesCount]; + var imageLoader = slideReelObj.AddComponent(); for (int i = 0; i < slidesCount; i++) { var slide = new Slide(); var slideInfo = info.slides[i]; - var texture = ImageUtilities.GetTexture(mod, slideInfo.imagePath); - slide.textureOverride = ImageUtilities.Invert(texture); - - // Track the first 15 to put on the slide reel object - if (i < 15) textures[i] = texture; + 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 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(); + var slidesFront = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Front").GetComponent(); + + // 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 slideReelObj.GetComponent()._physicsRemoved = false; @@ -95,16 +132,6 @@ namespace NewHorizons.Builder.Props OWAssetHandler.LoadObject(slideReelObj); sector.OnOccupantEnterSector.AddListener((x) => OWAssetHandler.LoadObject(slideReelObj)); - var slidesBack = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Back").GetComponent(); - var slidesFront = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Front").GetComponent(); - - // 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); } @@ -136,19 +163,20 @@ namespace NewHorizons.Builder.Props // Now we replace the slides int slidesCount = info.slides.Length; var slideCollection = new SlideCollection(slidesCount); - + + var imageLoader = projectorObj.AddComponent(); for (int i = 0; i < slidesCount; i++) { var slide = new Slide(); var slideInfo = info.slides[i]; - var texture = ImageUtilities.GetTexture(mod, slideInfo.imagePath); - slide.textureOverride = ImageUtilities.Invert(texture); + 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 = ImageUtilities.Invert(tex); }); slideCollectionContainer.slideCollection = slideCollection; @@ -163,6 +191,143 @@ namespace NewHorizons.Builder.Props 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(); + 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(); + var slideCollectionContainer = g.AddComponent(); + slideCollectionContainer.slideCollection = slideCollection; + target.slideCollection = g.AddComponent(); + 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._mindProjectorImageEffect = GameObject.Find("Player_Body/PlayerCamera").GetComponent(); + + // 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(); + 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.slideCollection = slideCollection; + var mindSlideCollection = standingTorch.AddComponent(); + 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) { var modules = new List(); @@ -211,4 +376,10 @@ namespace NewHorizons.Builder.Props Slide.WriteModules(modules, ref slide._modulesList, ref slide._modulesData, ref slide.lengths); } } + + public class VisionTorchTarget : MonoBehaviour + { + public MindSlideCollection slideCollection; + public SlideCollectionContainer slideCollectionContainer; + } } diff --git a/NewHorizons/External/Modules/PropModule.cs b/NewHorizons/External/Modules/PropModule.cs index ac06228e..0c658138 100644 --- a/NewHorizons/External/Modules/PropModule.cs +++ b/NewHorizons/External/Modules/PropModule.cs @@ -1,7 +1,8 @@ -using System.ComponentModel; + +using NewHorizons.Utility; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; -using NewHorizons.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -487,8 +488,13 @@ namespace NewHorizons.External.Modules public enum SlideShowType { [EnumMember(Value = @"slideReel")] SlideReel = 0, + + [EnumMember(Value = @"autoProjector")] AutoProjector = 1, + + [EnumMember(Value = @"visionTorchTarget")] VisionTorchTarget = 2, + + [EnumMember(Value = @"standingVisionTorch")] StandingVisionTorch = 3, - [EnumMember(Value = @"autoProjector")] AutoProjector = 1 } /// diff --git a/NewHorizons/Handlers/TitleSceneHandler.cs b/NewHorizons/Handlers/TitleSceneHandler.cs index 2776c0f4..6baf33ea 100644 --- a/NewHorizons/Handlers/TitleSceneHandler.cs +++ b/NewHorizons/Handlers/TitleSceneHandler.cs @@ -1,4 +1,4 @@ -using NewHorizons.Builder.Body; +using NewHorizons.Builder.Body; using NewHorizons.External.Modules; using NewHorizons.Utility; using System.Collections.Generic; diff --git a/NewHorizons/Patches/ToolModeSwapperPatches.cs b/NewHorizons/Patches/ToolModeSwapperPatches.cs new file mode 100644 index 00000000..987ae1dc --- /dev/null +++ b/NewHorizons/Patches/ToolModeSwapperPatches.cs @@ -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; + } + } +} diff --git a/NewHorizons/Patches/VisionTorchPatches.cs b/NewHorizons/Patches/VisionTorchPatches.cs new file mode 100644 index 00000000..e6df091c --- /dev/null +++ b/NewHorizons/Patches/VisionTorchPatches.cs @@ -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(); + 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().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; + } + } +} diff --git a/NewHorizons/Utility/ImageUtilities.cs b/NewHorizons/Utility/ImageUtilities.cs index 3a89b7ba..7eb0e869 100644 --- a/NewHorizons/Utility/ImageUtilities.cs +++ b/NewHorizons/Utility/ImageUtilities.cs @@ -1,8 +1,12 @@ using OWML.Common; using System; +using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; +using UnityEngine.Events; +using UnityEngine.Networking; + namespace NewHorizons.Utility { public static class ImageUtilities @@ -314,4 +318,46 @@ namespace NewHorizons.Utility return newTexture; } } + + // Modified from https://stackoverflow.com/a/69141085/9643841 + public class AsyncImageLoader : MonoBehaviour + { + public List pathsToLoad = new List(); + + public class ImageLoadedEvent : UnityEvent { } + 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); + } + } + } + } } diff --git a/NewHorizons/Utility/SearchUtilities.cs b/NewHorizons/Utility/SearchUtilities.cs index 821e68ea..6697ebdc 100644 --- a/NewHorizons/Utility/SearchUtilities.cs +++ b/NewHorizons/Utility/SearchUtilities.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; 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) { if (CachedGameObjects.ContainsKey(path))