diff --git a/NewHorizons/Builder/Props/Audio/SignalBuilder.cs b/NewHorizons/Builder/Props/Audio/SignalBuilder.cs index b5fbfaf0..9fd8617c 100644 --- a/NewHorizons/Builder/Props/Audio/SignalBuilder.cs +++ b/NewHorizons/Builder/Props/Audio/SignalBuilder.cs @@ -121,6 +121,7 @@ namespace NewHorizons.Builder.Props.Audio public static string GetCustomFrequencyName(SignalFrequency frequencyName) { + if (_customFrequencyNames == null) return string.Empty; _customFrequencyNames.TryGetValue(frequencyName, out string name); return name; } @@ -140,6 +141,7 @@ namespace NewHorizons.Builder.Props.Audio public static string GetCustomSignalName(SignalName signalName) { + if (_customSignalNames == null) return string.Empty; _customSignalNames.TryGetValue(signalName, out string name); return name; } diff --git a/NewHorizons/Builder/Props/ProjectionBuilder.cs b/NewHorizons/Builder/Props/ProjectionBuilder.cs index ed9d136a..2562768c 100644 --- a/NewHorizons/Builder/Props/ProjectionBuilder.cs +++ b/NewHorizons/Builder/Props/ProjectionBuilder.cs @@ -1,3 +1,4 @@ +using HarmonyLib; using NewHorizons.External.Modules.Props; using NewHorizons.External.Modules.Props.EchoesOfTheEye; using NewHorizons.Handlers; @@ -8,14 +9,18 @@ using OWML.Common; using System; using System.Collections.Generic; using System.IO; -using System.Threading; +using System.Linq; using UnityEngine; +using UnityEngine.InputSystem; using static NewHorizons.Main; namespace NewHorizons.Builder.Props { public static class ProjectionBuilder { + public const string INVERTED_SLIDE_CACHE_FOLDER = "SlideReelCache/Inverted"; + public const string ATLAS_SLIDE_CACHE_FOLDER = "SlideReelCache/Atlas"; + public static GameObject SlideReelWholePrefab { get; private set; } public static GameObject SlideReelWholePristinePrefab { get; private set; } public static GameObject SlideReelWholeRustedPrefab { get; private set; } @@ -113,6 +118,8 @@ namespace NewHorizons.Builder.Props } } + 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(); @@ -139,33 +146,67 @@ namespace NewHorizons.Builder.Props var slideCollection = new SlideCollection(slidesCount); slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null - // The base game ones only have 15 slides max - var textures = new Texture2D[slidesCount >= 15 ? 15 : slidesCount]; + // We can fit 16 slides max into an atlas + var textures = new Texture2D[slidesCount > 16 ? 16 : slidesCount]; - var imageLoader = AddAsyncLoader(slideReelObj, mod, info.slides, ref slideCollection); + var (invImageLoader, atlasImageLoader, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, true, true); - // 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) => + var key = GetUniqueSlideReelID(mod, info.slides); + + if (invImageLoader != null && atlasImageLoader != null) + { + // Loading directly from cache + invImageLoader.imageLoadedEvent.AddListener( + (Texture2D tex, int index, string originalPath) => + { + slideCollection.slides[index]._image = tex; + } + ); + atlasImageLoader.imageLoadedEvent.AddListener( + (Texture2D tex, int _, string originalPath) => + { + // all textures required to build the reel's textures have been loaded + var slidesBack = slideReelObj.GetComponentInChildren().transform.Find("Slides_Back").GetComponent(); + var slidesFront = slideReelObj.GetComponentInChildren().transform.Find("Slides_Front").GetComponent(); + + // 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) => { - slideCollection.slides[index]._image = ImageUtilities.Invert(tex); + var time = DateTime.Now; - // Track the first 15 to put on the slide reel object - if (index < textures.Length) + 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; - if (Interlocked.Increment(ref displaySlidesLoaded) >= textures.Length) + displaySlidesLoaded++; + if (displaySlidesLoaded == textures.Length) { // all textures required to build the reel's textures have been loaded var slidesBack = slideReelObj.GetComponentInChildren().transform.Find("Slides_Back").GetComponent(); var slidesFront = slideReelObj.GetComponentInChildren().transform.Find("Slides_Front").GetComponent(); // Now put together the textures into a 4x4 thing for the materials - var reelTexture = ImageUtilities.MakeReelTexture(textures); + var reelTexture = ImageUtilities.MakeReelTexture(mod, textures, key); slidesBack.material.mainTexture = reelTexture; slidesBack.material.SetTexture(EmissionMap, reelTexture); slidesBack.material.name = reelTexture.name; @@ -174,8 +215,11 @@ namespace NewHorizons.Builder.Props 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()._physicsRemoved = false; @@ -196,58 +240,58 @@ namespace NewHorizons.Builder.Props 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; + 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; + 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; + 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; + switch (condition) + { + case ProjectionInfo.SlideReelCondition.Antique: + default: + return SlideReelWholePrefab; + case ProjectionInfo.SlideReelCondition.Pristine: + return SlideReelWholePristinePrefab; + case ProjectionInfo.SlideReelCondition.Rusted: + return SlideReelWholeRustedPrefab; + } } - } } } @@ -300,8 +344,25 @@ namespace NewHorizons.Builder.Props var slideCollection = new SlideCollection(slidesCount); slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null - var imageLoader = AddAsyncLoader(projectorObj, mod, info.slides, ref slideCollection); - imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index]._image = ImageUtilities.Invert(tex); }); + var (invImageLoader, _, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, true, false); + 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; @@ -339,8 +400,13 @@ namespace NewHorizons.Builder.Props 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 = AddAsyncLoader(g, mod, info.slides, ref slideCollection); - imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index]._image = tex; }); + var (_, _, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, false, false); + 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 = g.AddComponent(); @@ -374,20 +440,20 @@ namespace NewHorizons.Builder.Props // Set some required properties on the torch var mindSlideProjector = standingTorch.GetComponent(); mindSlideProjector._mindProjectorImageEffect = SearchUtilities.Find("Player_Body/PlayerCamera").GetComponent(); - + // Setup for visually supporting async texture loading - mindSlideProjector.enabled = false; + mindSlideProjector.enabled = false; var visionBeamEffect = standingTorch.FindChild("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 + // 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 = AddAsyncLoader(standingTorch, mod, slides, ref slideCollection); + var (_, _, imageLoader) = StartAsyncLoader(mod, slides, ref slideCollection, false, false); // 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 @@ -395,15 +461,18 @@ namespace NewHorizons.Builder.Props // 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) => - { + (Texture2D tex, int index, string originalPath) => + { + var time = DateTime.Now; slideCollection.slides[index]._image = tex; - if (Interlocked.Increment(ref displaySlidesLoaded) == slides.Length) + displaySlidesLoaded++; + if (displaySlidesLoaded == slides.Length) { mindSlideProjector.enabled = true; visionBeamEffect.SetActive(true); } + NHLogger.LogVerbose($"Slide reel another set time {(DateTime.Now - time).TotalMilliseconds}ms"); } ); @@ -427,9 +496,25 @@ namespace NewHorizons.Builder.Props return standingTorch; } - private static ImageUtilities.AsyncImageLoader AddAsyncLoader(GameObject gameObject, IModBehaviour mod, SlideInfo[] slides, ref SlideCollection slideCollection) + private static (SlideReelAsyncImageLoader inverted, SlideReelAsyncImageLoader atlas, SlideReelAsyncImageLoader slides) + StartAsyncLoader(IModBehaviour mod, SlideInfo[] slides, ref SlideCollection slideCollection, bool useInvertedCache, bool useAtlasCache) { - var imageLoader = gameObject.AddComponent(); + var invertedImageLoader = new SlideReelAsyncImageLoader(); + var atlasImageLoader = new SlideReelAsyncImageLoader(); + var imageLoader = new SlideReelAsyncImageLoader(); + + var atlasKey = GetUniqueSlideReelID(mod, slides); + + var cacheExists = Directory.Exists(Path.Combine(mod.ModHelper.Manifest.ModFolderPath, ATLAS_SLIDE_CACHE_FOLDER)); + + NHLogger.Log($"Does cache exist for slide reels? {cacheExists}"); + + if (useAtlasCache && cacheExists) + { + // Load the atlas texture used to draw onto the physical slide reel object + atlasImageLoader.PathsToLoad.Add((0, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, ATLAS_SLIDE_CACHE_FOLDER, $"{atlasKey}.png"))); + } + for (int i = 0; i < slides.Length; i++) { var slide = new Slide(); @@ -438,10 +523,15 @@ namespace NewHorizons.Builder.Props if (string.IsNullOrEmpty(slideInfo.imagePath)) { - imageLoader.imageLoadedEvent?.Invoke(Texture2D.blackTexture, i); + imageLoader.imageLoadedEvent?.Invoke(Texture2D.blackTexture, i, null); } else { + if (useInvertedCache && cacheExists) + { + // Load the inverted images used when displaying slide reels to a screen + invertedImageLoader.PathsToLoad.Add((i, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, INVERTED_SLIDE_CACHE_FOLDER, slideInfo.imagePath))); + } imageLoader.PathsToLoad.Add((i, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, slideInfo.imagePath))); } @@ -450,7 +540,30 @@ namespace NewHorizons.Builder.Props slideCollection.slides[i] = slide; } - return imageLoader; + if (cacheExists) + { + // This code will execute in order to create the cache + // Loaders go sequentually - Load the inverted textures to the cache so that ImageUtilities will reuse them later + if (useInvertedCache) + { + invertedImageLoader.Start(true); + } + // Atlas texture next so that the normal iamgeLoader knows not to regenerate them unless they were missing + if (useAtlasCache) + { + atlasImageLoader.Start(false); + } + imageLoader.Start(true); + + return (invertedImageLoader, atlasImageLoader, imageLoader); + } + else + { + // Will be slow and create the cache if needed + imageLoader.Start(true); + + return (null, null, imageLoader); + } } private static void AddModules(SlideInfo slideInfo, ref Slide slide, IModBehaviour mod) @@ -516,7 +629,7 @@ namespace NewHorizons.Builder.Props Slide.WriteModules(modules, ref slide._modulesList, ref slide._modulesData, ref slide.lengths); } - + private static void LinkShipLogFacts(ProjectionInfo info, SlideCollectionContainer slideCollectionContainer) { // Idk why but it wants reveals to be comma delimited not a list diff --git a/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs b/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs index 89b4249b..9c1a4eba 100644 --- a/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs +++ b/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs @@ -471,7 +471,7 @@ namespace NewHorizons.Builder.Props.TranslatorText if (info.arcInfo != null && info.arcInfo.Count() != dict.Values.Count()) { - NHLogger.LogError($"Can't make NomaiWallText, arcInfo length [{info.arcInfo.Count()}] doesn't equal text entries [{dict.Values.Count()}]"); + NHLogger.LogError($"Can't make NomaiWallText, arcInfo length [{info.arcInfo.Count()}] doesn't equal number of TextBlocks [{dict.Values.Count()}] in the xml"); return; } diff --git a/NewHorizons/Components/Quantum/QuantumPlanet.cs b/NewHorizons/Components/Quantum/QuantumPlanet.cs index 5e5a4ecf..d4da2a7e 100644 --- a/NewHorizons/Components/Quantum/QuantumPlanet.cs +++ b/NewHorizons/Components/Quantum/QuantumPlanet.cs @@ -22,6 +22,16 @@ namespace NewHorizons.Components.Quantum private OWRigidbody _rb; private OrbitLine _orbitLine; + public NHAstroObject astroObject + { + get + { + if (_astroObject == null) + _astroObject = GetComponent(); + return _astroObject; + } + } + public int CurrentIndex { get; private set; } public override void Awake() @@ -97,7 +107,7 @@ namespace NewHorizons.Components.Quantum primaryBody = AstroObjectLocator.GetAstroObject(newOrbit.primaryBody); var primaryGravity = new Gravity(primaryBody.GetGravityVolume()); - var secondaryGravity = new Gravity(_astroObject.GetGravityVolume()); + var secondaryGravity = new Gravity(astroObject.GetGravityVolume()); orbitalParams = newOrbit.GetOrbitalParameters(primaryGravity, secondaryGravity); var pos = primaryBody.transform.position + orbitalParams.InitialPosition; @@ -139,15 +149,16 @@ namespace NewHorizons.Components.Quantum private void SetNewOrbit(AstroObject primaryBody, OrbitalParameters orbitalParameters) { - _astroObject._primaryBody = primaryBody; - DetectorBuilder.SetDetector(primaryBody, _astroObject, _detector); + astroObject._primaryBody = primaryBody; + DetectorBuilder.SetDetector(primaryBody, astroObject, _detector); _detector._activeInheritedDetector = primaryBody.GetComponentInChildren(); _detector._activeVolumes = new List() { primaryBody.GetGravityVolume() }; if (_alignment != null) _alignment.SetTargetBody(primaryBody.GetComponent()); - _astroObject.SetOrbitalParametersFromTrueAnomaly(orbitalParameters.eccentricity, orbitalParameters.semiMajorAxis, orbitalParameters.inclination, orbitalParameters.argumentOfPeriapsis, orbitalParameters.longitudeOfAscendingNode, orbitalParameters.trueAnomaly); + astroObject.SetOrbitalParametersFromTrueAnomaly(orbitalParameters.eccentricity, orbitalParameters.semiMajorAxis, orbitalParameters.inclination, orbitalParameters.argumentOfPeriapsis, orbitalParameters.longitudeOfAscendingNode, orbitalParameters.trueAnomaly); - PlanetCreationHandler.UpdatePosition(gameObject, orbitalParameters, primaryBody, _astroObject); + PlanetCreationHandler.UpdatePosition(gameObject, orbitalParameters, primaryBody, astroObject); + gameObject.transform.parent = null; if (!Physics.autoSyncTransforms) { diff --git a/NewHorizons/Handlers/PlayerSpawnHandler.cs b/NewHorizons/Handlers/PlayerSpawnHandler.cs index 40670ad8..dd4f4f27 100644 --- a/NewHorizons/Handlers/PlayerSpawnHandler.cs +++ b/NewHorizons/Handlers/PlayerSpawnHandler.cs @@ -137,30 +137,39 @@ namespace NewHorizons.Handlers private static IEnumerator SpawnCoroutine(int length) { + FixPlayerVelocity(); for(int i = 0; i < length; i++) { - FixPlayerVelocity(); + FixPlayerVelocity(false); // dont recenter universe here or else it spams and lags game yield return new WaitForEndOfFrame(); } + FixPlayerVelocity(); InvulnerabilityHandler.MakeInvulnerable(false); } - private static void FixPlayerVelocity() + private static void FixPlayerVelocity(bool recenter = true) { var playerBody = SearchUtilities.Find("Player_Body").GetAttachedOWRigidbody(); var resources = playerBody.GetComponent(); - SpawnBody(playerBody, GetDefaultSpawn()); + SpawnBody(playerBody, GetDefaultSpawn(), recenter: recenter); resources._currentHealth = 100f; } - public static void SpawnBody(OWRigidbody body, SpawnPoint spawn, Vector3? positionOverride = null) + public static void SpawnBody(OWRigidbody body, SpawnPoint spawn, Vector3? positionOverride = null, bool recenter = true) { var pos = positionOverride ?? spawn.transform.position; - body.WarpToPositionRotation(pos, spawn.transform.rotation); + if (recenter) + { + body.WarpToPositionRotation(pos, spawn.transform.rotation); + } + else + { + body.transform.SetPositionAndRotation(pos, spawn.transform.rotation); + } var spawnVelocity = spawn._attachedBody.GetVelocity(); var spawnAngularVelocity = spawn._attachedBody.GetPointTangentialVelocity(pos); diff --git a/NewHorizons/Utility/Files/ImageUtilities.cs b/NewHorizons/Utility/Files/ImageUtilities.cs index ea0ceaf6..3c01bd16 100644 --- a/NewHorizons/Utility/Files/ImageUtilities.cs +++ b/NewHorizons/Utility/Files/ImageUtilities.cs @@ -1,14 +1,10 @@ -using HarmonyLib; +using NewHorizons.Builder.Props; using NewHorizons.Utility.OWML; using OWML.Common; using System; -using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; using UnityEngine; -using UnityEngine.Events; -using UnityEngine.Networking; namespace NewHorizons.Utility.Files { @@ -17,9 +13,10 @@ namespace NewHorizons.Utility.Files // key is path + applied effects private static readonly Dictionary _textureCache = new(); public static bool CheckCachedTexture(string key, out Texture existingTexture) => _textureCache.TryGetValue(key, out existingTexture); - public static void TrackCachedTexture(string key, Texture texture) => _textureCache.Add(key, texture); + public static void TrackCachedTexture(string key, Texture texture) => _textureCache[key] = texture; - private static string GetKey(string path) => path.Substring(Main.Instance.ModHelper.OwmlConfig.ModsPath.Length); + public static string GetKey(string path) => + path.Substring(Main.Instance.ModHelper.OwmlConfig.ModsPath.Length + 1).Replace('\\', '/'); public static bool IsTextureLoaded(IModBehaviour mod, string filename) { @@ -99,7 +96,7 @@ namespace NewHorizons.Utility.Files /// used specifically for projected slides. /// also adds a border (to prevent weird visual bug) and makes the texture linear (otherwise the projected image is too bright). /// - public static Texture2D Invert(Texture2D texture) + public static Texture2D InvertSlideReel(IModBehaviour mod, Texture2D texture, string originalPath) { var key = $"{texture.name} > invert"; if (_textureCache.TryGetValue(key, out var existingTexture)) return (Texture2D)existingTexture; @@ -135,18 +132,27 @@ namespace NewHorizons.Utility.Files _textureCache.Add(key, newTexture); + // Since doing this is expensive we cache the results to the disk + // Preloading cached values is done in ProjectionBuilder + if (!string.IsNullOrEmpty(originalPath)) + { + var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, ProjectionBuilder.INVERTED_SLIDE_CACHE_FOLDER, originalPath.Replace(mod.ModHelper.Manifest.ModFolderPath, "")); + NHLogger.LogVerbose($"Caching inverted image to {path}"); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, newTexture.EncodeToPNG()); + } + return newTexture; } - public static Texture2D MakeReelTexture(Texture2D[] textures) + public static Texture2D MakeReelTexture(IModBehaviour mod, Texture2D[] textures, string uniqueSlideReelID) { - var key = $"SlideReelAtlas of {textures.Join(x => x.name)}"; - if (_textureCache.TryGetValue(key, out var existingTexture)) return (Texture2D)existingTexture; + if (_textureCache.TryGetValue(uniqueSlideReelID, out var existingTexture)) return (Texture2D)existingTexture; var size = 256; var texture = new Texture2D(size * 4, size * 4, TextureFormat.ARGB32, false); - texture.name = key; + texture.name = uniqueSlideReelID; var fillPixels = new Color[size * size * 4 * 4]; for (int xIndex = 0; xIndex < 4; xIndex++) @@ -188,7 +194,14 @@ namespace NewHorizons.Utility.Files texture.SetPixels(fillPixels); texture.Apply(); - _textureCache.Add(key, texture); + _textureCache.Add(uniqueSlideReelID, texture); + + // Since doing this is expensive we cache the results to the disk + // Preloading cached values is done in ProjectionBuilder + var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, ProjectionBuilder.ATLAS_SLIDE_CACHE_FOLDER, $"{uniqueSlideReelID}.png"); + NHLogger.LogVerbose($"Caching atlas image to {path}"); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, texture.EncodeToPNG()); return texture; } @@ -429,96 +442,5 @@ namespace NewHorizons.Utility.Files sprite.name = texture.name; return sprite; } - - // Modified from https://stackoverflow.com/a/69141085/9643841 - public class AsyncImageLoader : MonoBehaviour - { - public List<(int index, string path)> PathsToLoad { get; private set; } = new(); - - public class ImageLoadedEvent : UnityEvent { } - public ImageLoadedEvent imageLoadedEvent = new(); - - private readonly object _lockObj = new(); - - public bool FinishedLoading { get; private set; } - private int _loadedCount = 0; - - // 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() - { - imageLoadedEvent.AddListener(OnImageLoaded); - foreach (var (index, path) in PathsToLoad) - { - StartCoroutine(DownloadTexture(path, index)); - } - } - - private void OnImageLoaded(Texture texture, int index) - { - lock (_lockObj) - { - _loadedCount++; - - if (_loadedCount >= PathsToLoad.Count) - { - NHLogger.LogVerbose($"Finished loading all textures for {gameObject.name} (one was {PathsToLoad.FirstOrDefault()}"); - FinishedLoading = true; - } - } - } - - IEnumerator DownloadTexture(string url, int index) - { - var key = GetKey(url); - lock (_textureCache) - { - if (_textureCache.TryGetValue(key, out var existingTexture)) - { - NHLogger.LogVerbose($"Already loaded image {index}:{url}"); - imageLoadedEvent?.Invoke((Texture2D)existingTexture, index); - yield break; - } - } - - using UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(url); - - yield return uwr.SendWebRequest(); - - var hasError = uwr.error != null && uwr.error != ""; - - if (hasError) - { - NHLogger.LogError($"Failed to load {index}:{url} - {uwr.error}"); - } - else - { - var texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); - texture.name = key; - texture.wrapMode = TextureWrapMode.Clamp; - - var handler = (DownloadHandlerTexture)uwr.downloadHandler; - texture.LoadImage(handler.data); - - lock (_textureCache) - { - if (_textureCache.TryGetValue(key, out var existingTexture)) - { - NHLogger.LogVerbose($"Already loaded image {index}:{url}"); - Destroy(texture); - texture = (Texture2D)existingTexture; - } - else - { - _textureCache.Add(key, texture); - } - - imageLoadedEvent?.Invoke(texture, index); - } - } - } - } } } diff --git a/NewHorizons/Utility/Files/SlideReelAsyncImageLoader.cs b/NewHorizons/Utility/Files/SlideReelAsyncImageLoader.cs new file mode 100644 index 00000000..61b86d14 --- /dev/null +++ b/NewHorizons/Utility/Files/SlideReelAsyncImageLoader.cs @@ -0,0 +1,139 @@ +using NewHorizons.Utility.OWML; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.Networking; +using UnityEngine.SceneManagement; + +namespace NewHorizons.Utility.Files; + +/// +/// Modified from https://stackoverflow.com/a/69141085/9643841 +/// +public class SlideReelAsyncImageLoader +{ + public List<(int index, string path)> PathsToLoad { get; private set; } = new(); + + public class ImageLoadedEvent : UnityEvent { } + public ImageLoadedEvent imageLoadedEvent = new(); + + public bool FinishedLoading { get; private set; } + private int _loadedCount = 0; + + // 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) + // also remember this for ship logs!!! lol + + private bool _started; + private bool _clamp; + + public void Start(bool clamp) + { + if (_started) return; + + _clamp = clamp; + + _started = true; + + if (SingletonSlideReelAsyncImageLoader.Instance == null) + { + Main.Instance.gameObject.AddComponent(); + } + + NHLogger.LogVerbose("Loading new slide reel"); + imageLoadedEvent.AddListener(OnImageLoaded); + SingletonSlideReelAsyncImageLoader.Instance.Load(this); + } + + private void OnImageLoaded(Texture texture, int index, string originalPath) + { + _loadedCount++; + + if (_loadedCount >= PathsToLoad.Count) + { + NHLogger.LogVerbose($"Finished loading all textures for a slide reel (one was {PathsToLoad.FirstOrDefault()}"); + FinishedLoading = true; + } + } + + private IEnumerator DownloadTexture(string url, int index) + { + var key = ImageUtilities.GetKey(url); + if (ImageUtilities.CheckCachedTexture(key, out var existingTexture)) + { + NHLogger.LogVerbose($"Already loaded image {index}:{url} with key {key}"); + imageLoadedEvent?.Invoke((Texture2D)existingTexture, index, url); + yield break; + } + + using UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(url); + + yield return uwr.SendWebRequest(); + + var hasError = uwr.error != null && uwr.error != ""; + + if (hasError) + { + NHLogger.LogError($"Failed to load {index}:{url} - {uwr.error}"); + } + else + { + var texture = DownloadHandlerTexture.GetContent(uwr); + texture.name = key; + if (_clamp) + { + texture.wrapMode = TextureWrapMode.Clamp; + } + + if (ImageUtilities.CheckCachedTexture(key, out existingTexture)) + { + // the image could be loaded by something else by the time we're done doing async stuff + NHLogger.LogVerbose($"Already loaded image {index}:{url}"); + GameObject.Destroy(texture); + texture = (Texture2D)existingTexture; + } + else + { + ImageUtilities.TrackCachedTexture(key, texture); + } + + var time = DateTime.Now; + imageLoadedEvent?.Invoke(texture, index, url); + NHLogger.LogVerbose($"Slide reel event took: {(DateTime.Now - time).TotalMilliseconds}ms"); + } + } + + private class SingletonSlideReelAsyncImageLoader : MonoBehaviour + { + public static SingletonSlideReelAsyncImageLoader Instance { get; private set; } + + public void Awake() + { + Instance = this; + SceneManager.sceneUnloaded += OnSceneUnloaded; + } + + private void OnSceneUnloaded(Scene _) + { + StopAllCoroutines(); + } + + public void Load(SlideReelAsyncImageLoader loader) + { + // Delay at least one frame to let things subscribe to the event before it fires + Delay.FireOnNextUpdate(() => + { + foreach (var (index, path) in loader.PathsToLoad) + { + NHLogger.LogVerbose($"Loaded slide reel {index} of {loader.PathsToLoad.Count}"); + + StartCoroutine(loader.DownloadTexture(path, index)); + } + }); + } + } +} \ No newline at end of file diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 1a7cdab3..7f1a3787 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,7 +4,7 @@ "author": "xen, Bwc9876, JohnCorby, MegaPiggy, Clay, Trifid, and friends", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.21.2", + "version": "1.21.3", "owmlVersion": "2.12.1", "dependencies": [ "JohnCorby.VanillaFix", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ], "conflicts": [ "PacificEngine.OW_CommonResources" ],