mirror of
https://github.com/Outer-Wilds-New-Horizons/new-horizons.git
synced 2025-12-11 12:05:23 +01:00
724 lines
37 KiB
C#
724 lines
37 KiB
C#
using HarmonyLib;
|
|
using NewHorizons.Components.EOTE;
|
|
using NewHorizons.External.Modules.Props;
|
|
using NewHorizons.External.Modules.Props.EchoesOfTheEye;
|
|
using NewHorizons.Handlers;
|
|
using NewHorizons.Utility;
|
|
using NewHorizons.Utility.Files;
|
|
using NewHorizons.Utility.OWML;
|
|
using OWML.Common;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using static NewHorizons.Utility.Files.AssetBundleUtilities;
|
|
|
|
namespace NewHorizons.Builder.Props
|
|
{
|
|
public static class ProjectionBuilder
|
|
{
|
|
public static string CurrentSlideReelFolder => "SlideReelCache_" + Main.Instance.CurrentStarSystem;
|
|
public static string InvertedSlideReelCacheFolder => CurrentSlideReelFolder + "/Inverted";
|
|
public static string AtlasSlideReelCacheFolder => CurrentSlideReelFolder + "/Atlas";
|
|
|
|
public static GameObject SlideReelWholePrefab { get; private set; }
|
|
public static GameObject SlideReelWholePristinePrefab { get; private set; }
|
|
public static GameObject SlideReelWholeRustedPrefab { get; private set; }
|
|
public static GameObject SlideReelWholeDestroyedPrefab { get; private set; }
|
|
public static GameObject SlideReel8Prefab { get; private set; }
|
|
public static GameObject SlideReel8PristinePrefab { get; private set; }
|
|
public static GameObject SlideReel8RustedPrefab { get; private set; }
|
|
public static GameObject SlideReel8DestroyedPrefab { get; private set; }
|
|
public static GameObject SlideReel7Prefab { get; private set; }
|
|
public static GameObject SlideReel7PristinePrefab { get; private set; }
|
|
public static GameObject SlideReel7RustedPrefab { get; private set; }
|
|
public static GameObject SlideReel7DestroyedPrefab { get; private set; }
|
|
public static GameObject SlideReel6Prefab { get; private set; }
|
|
public static GameObject SlideReel6PristinePrefab { get; private set; }
|
|
public static GameObject SlideReel6RustedPrefab { get; private set; }
|
|
public static GameObject SlideReel6DestroyedPrefab { get; private set; }
|
|
|
|
private static GameObject _autoPrefab;
|
|
private static GameObject _visionTorchDetectorPrefab;
|
|
private static GameObject _standingVisionTorchPrefab;
|
|
private static GameObject _standingVisionTorchCleanPrefab;
|
|
private static readonly int EmissionMap = Shader.PropertyToID("_EmissionMap");
|
|
|
|
private static bool _isInit;
|
|
|
|
public static bool CacheExists(IModBehaviour mod) => Directory.Exists(Path.Combine(mod.ModHelper.Manifest.ModFolderPath, AtlasSlideReelCacheFolder));
|
|
|
|
internal static void InitPrefabs()
|
|
{
|
|
if (_isInit) return;
|
|
|
|
_isInit = true;
|
|
|
|
SlideReelWholePrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Whole");
|
|
SlideReelWholePristinePrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_DW_Reel_Whole");
|
|
SlideReelWholeRustedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Rusted_Whole");
|
|
SlideReelWholeDestroyedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Destroyed_Whole");
|
|
SlideReel8Prefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_8");
|
|
SlideReel8PristinePrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_DW_Reel_8");
|
|
SlideReel8RustedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Rusted_8");
|
|
SlideReel8DestroyedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Destroyed_8");
|
|
SlideReel7Prefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_7");
|
|
SlideReel7PristinePrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_DW_Reel_7");
|
|
SlideReel7RustedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Rusted_7");
|
|
SlideReel7DestroyedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Destroyed_7");
|
|
SlideReel6Prefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_6");
|
|
SlideReel6PristinePrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_DW_Reel_6");
|
|
SlideReel6RustedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Rusted_6");
|
|
SlideReel6DestroyedPrefab = NHPrivateAssetBundle.LoadAsset<GameObject>("Prefab_IP_Reel_Destroyed_6");
|
|
|
|
if (_autoPrefab == null)
|
|
{
|
|
_autoPrefab = SearchUtilities.Find("RingWorld_Body/Sector_RingInterior/Sector_Zone4/Sector_BlightedShore/Sector_JammingControlRoom_Zone4/Interactables_JammingControlRoom_Zone4/AutoProjector_SignalJammer/Prefab_IP_AutoProjector_SignalJammer")?.gameObject?.InstantiateInactive()?.Rename("Prefab_IP_AutoProjector")?.DontDestroyOnLoad();
|
|
if (_autoPrefab == null)
|
|
NHLogger.LogWarning($"Tried to make auto projector prefab but couldn't. Do you have the DLC installed?");
|
|
else
|
|
_autoPrefab.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
|
|
}
|
|
|
|
if (_visionTorchDetectorPrefab == null)
|
|
{
|
|
_visionTorchDetectorPrefab = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Sector_Underground/Sector_PrisonCell/Ghosts_PrisonCell/GhostDirector_Prisoner/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")?.gameObject?.InstantiateInactive()?.Rename("Prefab_IP_VisionTorchDetector")?.DontDestroyOnLoad();
|
|
if (_visionTorchDetectorPrefab == null)
|
|
NHLogger.LogWarning($"Tried to make vision torch detector prefab but couldn't. Do you have the DLC installed?");
|
|
else
|
|
_visionTorchDetectorPrefab.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
|
|
}
|
|
|
|
if (_standingVisionTorchCleanPrefab == null)
|
|
{
|
|
_standingVisionTorchCleanPrefab = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Sector_DreamZone_4/Interactibles_DreamZone_4_Upper/Prefab_IP_VisionTorchProjector")?.gameObject?.InstantiateInactive()?.Rename("Prefab_DW_VisionTorchProjector")?.DontDestroyOnLoad();
|
|
if (_standingVisionTorchCleanPrefab == null)
|
|
NHLogger.LogWarning($"Tried to make standing vision torch prefab but couldn't. Do you have the DLC installed?");
|
|
else
|
|
{
|
|
_standingVisionTorchCleanPrefab.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
|
|
GameObject.DestroyImmediate(_standingVisionTorchCleanPrefab.FindChild("Prefab_IP_Reel_PrisonPeephole_Vision"));
|
|
}
|
|
}
|
|
|
|
if (_standingVisionTorchPrefab == null)
|
|
{
|
|
_standingVisionTorchPrefab = SearchUtilities.Find("RingWorld_Body/Sector_RingWorld/Sector_SecretEntrance/Interactibles_SecretEntrance/Experiment_1/VisionTorchApparatus/VisionTorchRoot/Prefab_IP_VisionTorchProjector")?.gameObject?.InstantiateInactive()?.Rename("Prefab_IP_VisionTorchProjector")?.DontDestroyOnLoad();
|
|
if (_standingVisionTorchPrefab == null)
|
|
NHLogger.LogWarning($"Tried to make standing vision torch prefab but couldn't. Do you have the DLC installed?");
|
|
else
|
|
{
|
|
_standingVisionTorchPrefab.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
|
|
GameObject.Instantiate(_standingVisionTorchCleanPrefab.FindChild("Effects_IP_SIM_VisionTorch"), _standingVisionTorchPrefab.transform, false).Rename("Effects_IP_SIM_VisionTorch");
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void Make(GameObject go, Sector sector, ProjectionInfo info, IModBehaviour mod)
|
|
{
|
|
switch (info.type)
|
|
{
|
|
case ProjectionInfo.SlideShowType.AutoProjector:
|
|
MakeAutoProjector(go, sector, info, mod);
|
|
break;
|
|
case ProjectionInfo.SlideShowType.SlideReel:
|
|
MakeSlideReel(go, sector, info, mod);
|
|
break;
|
|
case ProjectionInfo.SlideShowType.VisionTorchTarget:
|
|
MakeMindSlidesTarget(go, sector, info, mod);
|
|
break;
|
|
case ProjectionInfo.SlideShowType.StandingVisionTorch:
|
|
MakeStandingVisionTorch(go, sector, info, mod);
|
|
break;
|
|
default:
|
|
NHLogger.LogError($"Invalid projection type {info.type}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
public static string GetUniqueSlideReelID(IModBehaviour mod, SlideInfo[] slides) => $"{mod.ModHelper.Manifest.UniqueName}{slides.Join(x => x.imagePath)}".GetHashCode().ToString();
|
|
|
|
private static GameObject MakeSlideReel(GameObject planetGO, Sector sector, ProjectionInfo info, IModBehaviour mod)
|
|
{
|
|
InitPrefabs();
|
|
|
|
GameObject prefab = GetSlideReelPrefab(info.reelModel, info.reelCondition);
|
|
|
|
if (prefab == null) return null;
|
|
|
|
var slideReelObj = GeneralPropBuilder.MakeFromPrefab(prefab, $"Prefab_IP_Reel_{GetSlideReelName(info.reelModel, info.reelCondition)}_{mod.ModHelper.Manifest.Name}", planetGO, ref sector, info);
|
|
|
|
var slideReel = slideReelObj.GetComponent<SlideReelItem>();
|
|
slideReel.SetSector(sector);
|
|
slideReel.SetVisible(true);
|
|
|
|
var toDestroy = slideReelObj.GetComponent<SlideCollectionContainer>();
|
|
var slideCollectionContainer = slideReelObj.AddComponent<NHSlideCollectionContainer>();
|
|
slideReel._slideCollectionContainer = slideCollectionContainer;
|
|
Component.DestroyImmediate(toDestroy);
|
|
|
|
foreach (var renderer in slideReelObj.GetComponentsInChildren<Renderer>())
|
|
{
|
|
renderer.enabled = true;
|
|
}
|
|
|
|
// Now we replace the slides
|
|
int slidesCount = info.slides.Length;
|
|
SlideCollection slideCollection = new NHSlideCollection(slidesCount, mod, info.slides.Select(x => x.imagePath).ToArray());
|
|
slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null
|
|
|
|
// We can fit 16 slides max into an atlas
|
|
var textures = new Texture2D[slidesCount > 16 ? 16 : slidesCount];
|
|
|
|
// Slide reels dynamically load the inverted cached images when needed. We only need to load raw images to generate the cache or atlases
|
|
var (_, atlasImageLoader, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, false, true, false);
|
|
|
|
// If the cache doesn't exist it will be created here, slide reels only use the base image loader for cache creation so delete the images after to free memory
|
|
imageLoader.deleteTexturesWhenDone = true;
|
|
|
|
var key = GetUniqueSlideReelID(mod, info.slides);
|
|
|
|
if (atlasImageLoader != null)
|
|
{
|
|
atlasImageLoader.imageLoadedEvent.AddListener(
|
|
(Texture2D tex, int _, string originalPath) =>
|
|
{
|
|
// all textures required to build the reel's textures have been loaded
|
|
var slidesBack = slideReelObj.GetComponentInChildren<TransformAnimator>(true).transform.Find("Slides_Back").GetComponent<MeshRenderer>();
|
|
var slidesFront = slideReelObj.GetComponentInChildren<TransformAnimator>(true).transform.Find("Slides_Front").GetComponent<MeshRenderer>();
|
|
|
|
// Now put together the textures into a 4x4 thing for the materials
|
|
var reelTexture = tex;
|
|
slidesBack.material.mainTexture = reelTexture;
|
|
slidesBack.material.SetTexture(EmissionMap, reelTexture);
|
|
slidesBack.material.name = reelTexture.name;
|
|
slidesFront.material.mainTexture = reelTexture;
|
|
slidesFront.material.SetTexture(EmissionMap, reelTexture);
|
|
slidesFront.material.name = reelTexture.name;
|
|
}
|
|
);
|
|
}
|
|
else
|
|
{
|
|
// 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, string originalPath) =>
|
|
{
|
|
var time = DateTime.Now;
|
|
|
|
// inverted slides will be loaded for the whole loop but its fine since this is only when generating cache
|
|
slideCollection.slides[index]._image = ImageUtilities.InvertSlideReel(mod, tex, originalPath);
|
|
NHLogger.LogVerbose($"Slide reel make reel invert texture {(DateTime.Now - time).TotalMilliseconds}ms");
|
|
// Track the first 16 to put on the slide reel object
|
|
if (index < textures.Length)
|
|
{
|
|
textures[index] = tex;
|
|
displaySlidesLoaded++;
|
|
if (displaySlidesLoaded == textures.Length)
|
|
{
|
|
// all textures required to build the reel's textures have been loaded
|
|
var slidesBack = slideReelObj.GetComponentInChildren<TransformAnimator>(true).transform.Find("Slides_Back").GetComponent<MeshRenderer>();
|
|
var slidesFront = slideReelObj.GetComponentInChildren<TransformAnimator>(true).transform.Find("Slides_Front").GetComponent<MeshRenderer>();
|
|
|
|
// Now put together the textures into a 4x4 thing for the materials #888
|
|
var displayTextures = textures;
|
|
if (info.displaySlides != null && info.displaySlides.Length > 0)
|
|
{
|
|
displayTextures = info.displaySlides.Select(x => textures[x]).ToArray();
|
|
}
|
|
|
|
var reelTexture = ImageUtilities.MakeReelTexture(mod, displayTextures, key);
|
|
slidesBack.material.mainTexture = reelTexture;
|
|
slidesBack.material.SetTexture(EmissionMap, reelTexture);
|
|
slidesBack.material.name = reelTexture.name;
|
|
slidesFront.material.mainTexture = reelTexture;
|
|
slidesFront.material.SetTexture(EmissionMap, reelTexture);
|
|
slidesFront.material.name = reelTexture.name;
|
|
}
|
|
}
|
|
|
|
NHLogger.LogVerbose($"Slide reel make reel texture {(DateTime.Now - time).TotalMilliseconds}ms");
|
|
});
|
|
}
|
|
|
|
// Else when you put them down you can't pick them back up
|
|
slideReelObj.GetComponent<OWCollider>()._physicsRemoved = false;
|
|
|
|
slideCollectionContainer.slideCollection = slideCollection;
|
|
|
|
LinkShipLogFacts(info, slideCollectionContainer);
|
|
|
|
StreamingHandler.SetUpStreaming(slideReelObj, sector);
|
|
|
|
slideReelObj.SetActive(true);
|
|
|
|
return slideReelObj;
|
|
}
|
|
|
|
private static GameObject GetSlideReelPrefab(ProjectionInfo.SlideReelType model, ProjectionInfo.SlideReelCondition condition)
|
|
{
|
|
switch (model)
|
|
{
|
|
case ProjectionInfo.SlideReelType.SixSlides:
|
|
{
|
|
switch (condition)
|
|
{
|
|
case ProjectionInfo.SlideReelCondition.Antique:
|
|
default:
|
|
return SlideReel6Prefab;
|
|
case ProjectionInfo.SlideReelCondition.Pristine:
|
|
return SlideReel6PristinePrefab;
|
|
case ProjectionInfo.SlideReelCondition.Rusted:
|
|
return SlideReel6RustedPrefab;
|
|
}
|
|
}
|
|
case ProjectionInfo.SlideReelType.SevenSlides:
|
|
default:
|
|
{
|
|
switch (condition)
|
|
{
|
|
case ProjectionInfo.SlideReelCondition.Antique:
|
|
default:
|
|
return SlideReel7Prefab;
|
|
case ProjectionInfo.SlideReelCondition.Pristine:
|
|
return SlideReel7PristinePrefab;
|
|
case ProjectionInfo.SlideReelCondition.Rusted:
|
|
return SlideReel7RustedPrefab;
|
|
}
|
|
}
|
|
case ProjectionInfo.SlideReelType.EightSlides:
|
|
{
|
|
switch (condition)
|
|
{
|
|
case ProjectionInfo.SlideReelCondition.Antique:
|
|
default:
|
|
return SlideReel8Prefab;
|
|
case ProjectionInfo.SlideReelCondition.Pristine:
|
|
return SlideReel8PristinePrefab;
|
|
case ProjectionInfo.SlideReelCondition.Rusted:
|
|
return SlideReel8RustedPrefab;
|
|
}
|
|
}
|
|
case ProjectionInfo.SlideReelType.Whole:
|
|
{
|
|
switch (condition)
|
|
{
|
|
case ProjectionInfo.SlideReelCondition.Antique:
|
|
default:
|
|
return SlideReelWholePrefab;
|
|
case ProjectionInfo.SlideReelCondition.Pristine:
|
|
return SlideReelWholePristinePrefab;
|
|
case ProjectionInfo.SlideReelCondition.Rusted:
|
|
return SlideReelWholeRustedPrefab;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static string GetSlideReelName(ProjectionInfo.SlideReelType model, ProjectionInfo.SlideReelCondition condition)
|
|
{
|
|
switch (model)
|
|
{
|
|
case ProjectionInfo.SlideReelType.SixSlides:
|
|
return $"6_{condition}";
|
|
case ProjectionInfo.SlideReelType.SevenSlides:
|
|
return $"7_{condition}";
|
|
case ProjectionInfo.SlideReelType.EightSlides:
|
|
return $"8_{condition}";
|
|
case ProjectionInfo.SlideReelType.Whole:
|
|
default:
|
|
return $"{model}_{condition}";
|
|
}
|
|
}
|
|
|
|
public static int GetSlideCount(ProjectionInfo.SlideReelType model)
|
|
{
|
|
switch (model)
|
|
{
|
|
case ProjectionInfo.SlideReelType.SixSlides:
|
|
return 6;
|
|
case ProjectionInfo.SlideReelType.SevenSlides:
|
|
case ProjectionInfo.SlideReelType.Whole:
|
|
return 7;
|
|
case ProjectionInfo.SlideReelType.EightSlides:
|
|
default:
|
|
return 8;
|
|
}
|
|
}
|
|
|
|
public static GameObject MakeAutoProjector(GameObject planetGO, Sector sector, ProjectionInfo info, IModBehaviour mod)
|
|
{
|
|
InitPrefabs();
|
|
|
|
if (_autoPrefab == null) return null;
|
|
|
|
var projectorObj = GeneralPropBuilder.MakeFromPrefab(_autoPrefab, $"Prefab_IP_AutoProjector_{mod.ModHelper.Manifest.Name}", planetGO, ref sector, info);
|
|
|
|
var autoProjector = projectorObj.GetComponent<AutoSlideProjector>();
|
|
autoProjector._sector = sector;
|
|
|
|
var toDestroy = autoProjector.GetComponent<SlideCollectionContainer>();
|
|
var slideCollectionContainer = autoProjector.gameObject.AddComponent<NHSlideCollectionContainer>();
|
|
slideCollectionContainer.doAsyncLoading = false;
|
|
autoProjector._slideCollectionItem = slideCollectionContainer;
|
|
Component.DestroyImmediate(toDestroy);
|
|
|
|
// Now we replace the slides
|
|
int slidesCount = info.slides.Length;
|
|
SlideCollection slideCollection = new NHSlideCollection(slidesCount, mod, info.slides.Select(x => x.imagePath).ToArray());
|
|
slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null
|
|
|
|
var (invImageLoader, _, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, true, false, false);
|
|
|
|
// Autoprojector only uses the inverted images so the original can be destroyed if they are loaded (when creating the cached inverted images)
|
|
imageLoader.deleteTexturesWhenDone = true;
|
|
|
|
if (invImageLoader != null)
|
|
{
|
|
// Loaded directly from cache
|
|
invImageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index, string originalPath) =>
|
|
{
|
|
slideCollection.slides[index]._image = tex;
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// Create the inverted cache from existing images
|
|
imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index, string originalPath) =>
|
|
{
|
|
var time = DateTime.Now;
|
|
slideCollection.slides[index]._image = ImageUtilities.InvertSlideReel(mod, tex, originalPath);
|
|
NHLogger.LogVerbose($"Slide reel invert time {(DateTime.Now - time).TotalMilliseconds}ms");
|
|
});
|
|
}
|
|
|
|
slideCollectionContainer.slideCollection = slideCollection;
|
|
slideCollectionContainer._playWithShipLogFacts = Array.Empty<string>(); // else it NREs in container initialize
|
|
|
|
StreamingHandler.SetUpStreaming(projectorObj, sector);
|
|
|
|
// Change the picture on the lens
|
|
var lens = projectorObj.transform.Find("Spotlight/Prop_IP_SingleSlideProjector/Projector_Lens").GetComponent<MeshRenderer>();
|
|
lens.materials[1].mainTexture = slideCollection.slides[0]._image;
|
|
lens.materials[1].SetTexture(EmissionMap, slideCollection.slides[0]._image);
|
|
|
|
projectorObj.SetActive(true);
|
|
|
|
return projectorObj;
|
|
}
|
|
|
|
// Makes a target for a vision torch to scan
|
|
public static GameObject MakeMindSlidesTarget(GameObject planetGO, Sector sector, ProjectionInfo info, IModBehaviour mod)
|
|
{
|
|
InitPrefabs();
|
|
|
|
if (_visionTorchDetectorPrefab == null) return null;
|
|
|
|
// spawn a trigger for the vision torch
|
|
var visionTorchTargetGO = DetailBuilder.Make(planetGO, ref sector, mod, _visionTorchDetectorPrefab, new DetailInfo(info) { scale = 2, rename = !string.IsNullOrEmpty(info.rename) ? info.rename : "VisionStaffDetector" });
|
|
|
|
if (visionTorchTargetGO == null)
|
|
{
|
|
NHLogger.LogWarning($"Tried to make a vision torch target but couldn't. Do you have the DLC installed?");
|
|
return null;
|
|
}
|
|
|
|
// 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); // TODO: uh I think that info.slides[i].playTimeDuration is not being read here... note to self for when I implement support for that: 0.7 is what to default to if playTimeDuration turns out to be 0
|
|
slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null
|
|
|
|
var (_, _, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, false, false, true);
|
|
imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index, string originalPath) =>
|
|
{
|
|
var time = DateTime.Now;
|
|
slideCollection.slides[index]._image = tex;
|
|
NHLogger.LogVerbose($"Slide reel set time {(DateTime.Now - time).TotalMilliseconds}ms");
|
|
});
|
|
|
|
// attach a component to store all the data for the slides that play when a vision torch scans this target
|
|
var target = visionTorchTargetGO.AddComponent<VisionTorchTarget>();
|
|
var slideCollectionContainer = visionTorchTargetGO.AddComponent<NHSlideCollectionContainer>();
|
|
slideCollectionContainer.doAsyncLoading = false;
|
|
slideCollectionContainer.slideCollection = slideCollection;
|
|
target.slideCollection = visionTorchTargetGO.AddComponent<MindSlideCollection>();
|
|
target.slideCollection._slideCollectionContainer = slideCollectionContainer;
|
|
|
|
LinkShipLogFacts(info, slideCollectionContainer);
|
|
|
|
visionTorchTargetGO.SetActive(true);
|
|
|
|
return visionTorchTargetGO;
|
|
}
|
|
|
|
public static GameObject MakeStandingVisionTorch(GameObject planetGO, Sector sector, ProjectionInfo info, IModBehaviour mod)
|
|
{
|
|
InitPrefabs();
|
|
|
|
if (_standingVisionTorchPrefab == null || _standingVisionTorchCleanPrefab == null) return null;
|
|
|
|
// Spawn the torch itself
|
|
var prefab = info.reelCondition == ProjectionInfo.SlideReelCondition.Pristine ? _standingVisionTorchCleanPrefab : _standingVisionTorchPrefab;
|
|
var standingTorch = DetailBuilder.Make(planetGO, ref sector, mod, prefab, new DetailInfo(info));
|
|
|
|
if (standingTorch == null)
|
|
{
|
|
NHLogger.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 = SearchUtilities.Find("Player_Body/PlayerCamera").GetComponent<MindProjectorImageEffect>();
|
|
|
|
// Setup for visually supporting async texture loading
|
|
mindSlideProjector.enabled = false;
|
|
var visionBeamEffect = standingTorch.FindChild("VisionBeam");
|
|
visionBeamEffect.SetActive(false);
|
|
|
|
// Set up slides
|
|
// The number of slides is unlimited, 16 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);
|
|
slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null
|
|
|
|
var (_, _, imageLoader) = StartAsyncLoader(mod, slides, ref slideCollection, false, false, true);
|
|
|
|
// 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, string originalPath) =>
|
|
{
|
|
var time = DateTime.Now;
|
|
slideCollection.slides[index]._image = tex;
|
|
|
|
displaySlidesLoaded++;
|
|
if (displaySlidesLoaded == slides.Length)
|
|
{
|
|
mindSlideProjector.enabled = true;
|
|
visionBeamEffect.SetActive(true);
|
|
}
|
|
NHLogger.LogVerbose($"Slide reel another set time {(DateTime.Now - time).TotalMilliseconds}ms");
|
|
}
|
|
);
|
|
|
|
// Set up the containers for the slides
|
|
var slideCollectionContainer = standingTorch.AddComponent<NHSlideCollectionContainer>();
|
|
slideCollectionContainer.doAsyncLoading = false;
|
|
slideCollectionContainer.slideCollection = slideCollection;
|
|
|
|
var mindSlideCollection = standingTorch.AddComponent<MindSlideCollection>();
|
|
mindSlideCollection._slideCollectionContainer = slideCollectionContainer;
|
|
|
|
// Make sure that these slides play when the player wanders into the beam
|
|
slideCollectionContainer._initialized = true; // Hack to avoid initialization in the following call (it would throw NRE)
|
|
mindSlideProjector.SetMindSlideCollection(mindSlideCollection);
|
|
slideCollectionContainer._initialized = false;
|
|
|
|
|
|
LinkShipLogFacts(info, slideCollectionContainer);
|
|
|
|
standingTorch.SetActive(true);
|
|
|
|
return standingTorch;
|
|
}
|
|
|
|
/// <summary>
|
|
/// start loading all the slide stuff we need async.
|
|
/// </summary>
|
|
/// <param name="mod">the mod to load slides from</param>
|
|
/// <param name="slides">slides to load</param>
|
|
/// <param name="slideCollection">where to assign the slide objects</param>
|
|
/// <param name="useInvertedCache">should we load cached inverted images?</param>
|
|
/// <param name="useAtlasCache">should we load cached atlas images?</param>
|
|
/// <param name="loadRawImages">should we load the original images? happens anyway if cache doesnt exist since atlas or inverted will need it</param>
|
|
/// <returns>the 3 loaders (inverted, atlas, original). inverted and atlas will be null if cache doesnt exist, so check those to find out if cache exists</returns>
|
|
private static (SlideReelAsyncImageLoader inverted, SlideReelAsyncImageLoader atlas, SlideReelAsyncImageLoader slides)
|
|
StartAsyncLoader(IModBehaviour mod, SlideInfo[] slides, ref SlideCollection slideCollection, bool useInvertedCache, bool useAtlasCache, bool loadRawImages)
|
|
{
|
|
var invertedImageLoader = new SlideReelAsyncImageLoader();
|
|
var atlasImageLoader = new SlideReelAsyncImageLoader();
|
|
var imageLoader = new SlideReelAsyncImageLoader();
|
|
|
|
var atlasKey = GetUniqueSlideReelID(mod, slides);
|
|
|
|
var cacheExists = CacheExists(mod);
|
|
|
|
NHLogger.Log($"Does cache exist for slide reels? {cacheExists}");
|
|
|
|
if (useAtlasCache && cacheExists)
|
|
{
|
|
NHLogger.LogVerbose($"The atlas cache for slide reel containing [{slides.FirstOrDefault(x => !string.IsNullOrEmpty(x.imagePath))?.imagePath}] is {atlasKey}");
|
|
// Load the atlas texture used to draw onto the physical slide reel object
|
|
atlasImageLoader.PathsToLoad.Add((0, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, AtlasSlideReelCacheFolder, $"{atlasKey}.png")));
|
|
}
|
|
|
|
for (int i = 0; i < slides.Length; i++)
|
|
{
|
|
var slide = new Slide();
|
|
var slideInfo = slides[i];
|
|
slide._streamingImageID = i; // for SlideRotationModule
|
|
|
|
if (string.IsNullOrEmpty(slideInfo.imagePath))
|
|
{
|
|
if (useInvertedCache && cacheExists)
|
|
{
|
|
// Load the inverted images used when displaying slide reels to a screen
|
|
invertedImageLoader.PathsToLoad.Add((i, Path.Combine(Main.Instance.ModHelper.Manifest.ModFolderPath, "Assets/textures/inverted_blank_slide_reel.png")));
|
|
}
|
|
else if (!cacheExists || loadRawImages)
|
|
{
|
|
// Used to then make cached stuff
|
|
imageLoader.PathsToLoad.Add((i, Path.Combine(Main.Instance.ModHelper.Manifest.ModFolderPath, "Assets/textures/blank_slide_reel.png")));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (cacheExists && useInvertedCache)
|
|
{
|
|
// Load the inverted images used when displaying slide reels to a screen
|
|
invertedImageLoader.PathsToLoad.Add((i, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, InvertedSlideReelCacheFolder, slideInfo.imagePath)));
|
|
}
|
|
if (!cacheExists || loadRawImages)
|
|
{
|
|
imageLoader.PathsToLoad.Add((i, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, slideInfo.imagePath)));
|
|
}
|
|
}
|
|
|
|
AddModules(slideInfo, ref slide, mod);
|
|
|
|
slideCollection.slides[i] = slide;
|
|
}
|
|
|
|
if (cacheExists)
|
|
{
|
|
NHLogger.Log("Loading slide reels from cache");
|
|
|
|
if (useAtlasCache)
|
|
{
|
|
atlasImageLoader.Start(false, false);
|
|
}
|
|
if (useInvertedCache)
|
|
{
|
|
invertedImageLoader.Start(true, false);
|
|
}
|
|
if (loadRawImages)
|
|
{
|
|
imageLoader.Start(true, false);
|
|
}
|
|
|
|
return (invertedImageLoader, atlasImageLoader, imageLoader);
|
|
}
|
|
else
|
|
{
|
|
NHLogger.Log("Generating slide reel cache");
|
|
|
|
// Will be slow and create the cache if needed
|
|
// Will run sequentially to ensure we don't run out of memory
|
|
imageLoader.Start(true, true);
|
|
|
|
return (null, null, imageLoader);
|
|
}
|
|
}
|
|
|
|
private static void AddModules(SlideInfo slideInfo, ref Slide slide, IModBehaviour mod)
|
|
{
|
|
var modules = new List<SlideFunctionModule>();
|
|
if (!string.IsNullOrEmpty(slideInfo.beatAudio))
|
|
{
|
|
var audioBeat = new SlideBeatAudioModule
|
|
{
|
|
_audioType = AudioTypeHandler.GetAudioType(slideInfo.beatAudio, mod),
|
|
_delay = slideInfo.beatDelay
|
|
};
|
|
modules.Add(audioBeat);
|
|
}
|
|
if (!string.IsNullOrEmpty(slideInfo.backdropAudio))
|
|
{
|
|
var audioBackdrop = new SlideBackdropAudioModule
|
|
{
|
|
_audioType = AudioTypeHandler.GetAudioType(slideInfo.backdropAudio, mod),
|
|
_fadeTime = slideInfo.backdropFadeTime
|
|
};
|
|
modules.Add(audioBackdrop);
|
|
}
|
|
if (slideInfo.ambientLightIntensity != 0)
|
|
{
|
|
var ambientLight = new SlideAmbientLightModule
|
|
{
|
|
_intensity = slideInfo.ambientLightIntensity,
|
|
_range = slideInfo.ambientLightRange,
|
|
_color = slideInfo.ambientLightColor?.ToColor() ?? Color.white,
|
|
_spotIntensityMod = slideInfo.spotIntensityMod
|
|
};
|
|
modules.Add(ambientLight);
|
|
}
|
|
if (slideInfo.playTimeDuration != 0)
|
|
{
|
|
var playTime = new SlidePlayTimeModule
|
|
{
|
|
_duration = slideInfo.playTimeDuration
|
|
};
|
|
modules.Add(playTime);
|
|
}
|
|
if (slideInfo.blackFrameDuration != 0)
|
|
{
|
|
var blackFrame = new SlideBlackFrameModule
|
|
{
|
|
_duration = slideInfo.blackFrameDuration
|
|
};
|
|
modules.Add(blackFrame);
|
|
}
|
|
if (!string.IsNullOrEmpty(slideInfo.reveal))
|
|
{
|
|
var shipLogEntry = new SlideShipLogEntryModule
|
|
{
|
|
_entryKey = slideInfo.reveal
|
|
};
|
|
modules.Add(shipLogEntry);
|
|
}
|
|
if (slideInfo.rotate)
|
|
{
|
|
modules.Add(new SlideRotationModule());
|
|
}
|
|
|
|
Slide.WriteModules(modules, ref slide._modulesList, ref slide._modulesData, ref slide.lengths);
|
|
}
|
|
|
|
private static void LinkShipLogFacts(ProjectionInfo info, NHSlideCollectionContainer slideCollectionContainer)
|
|
{
|
|
// Idk why but it wants reveals to be comma delimited not a list
|
|
if (info.reveals != null) slideCollectionContainer._shipLogOnComplete = string.Join(",", info.reveals);
|
|
// Don't use null value, NRE in SlideCollectionContainer.Initialize
|
|
slideCollectionContainer._playWithShipLogFacts = info.playWithShipLogFacts ?? Array.Empty<string>();
|
|
|
|
slideCollectionContainer.conditionsToSet = info.conditionsToSet;
|
|
slideCollectionContainer.persistentConditionsToSet = info.persistentConditionsToSet;
|
|
}
|
|
}
|
|
|
|
public class VisionTorchTarget : MonoBehaviour
|
|
{
|
|
public MindSlideCollection slideCollection;
|
|
|
|
// This Callback is never used in NH itself.
|
|
// It exists for addons that want to trigger events when the mind slide show starts.
|
|
public OWEvent.OWCallback onSlidesStart;
|
|
|
|
// This Callback is never used in NH itself.
|
|
// It exists for addons that want to trigger events after the mind slide show is complete.
|
|
public OWEvent.OWCallback onSlidesComplete;
|
|
}
|
|
}
|