Slide reel streaming (#1040)

## Minor features
- Can set `displaySlides` on a slide reel now to define which slide
indices should be displayed on the physical reel model. Fixes #888.

## Improvements

- Slide reels are now streamed (Fixes #898). Other projectors (auto,
torch) are not streamed yet.
- Empty slide reel slots are now transparent on the slide reel model.
Requires existing slide reel caches to be cleared.

## Bug fixes

- Fixed a 3 frame hitch when changing tools


So the strategy is:
If the cache does not exist, do nothing different. It will take like 5
minutes and all your memory but that doesn't matter that's on the dev to
make sure that they pre-gen the caches (the sequential pre-caching
option should stop you running out of memory when making the cache
probably). Users won't experience any of that

Then we just do not ever load the inverted cached images when loading
slides from the cache. Only do it right as the player is about to see a
slide, by patching any base game method that tries to get a streamed
slide. We currently do not change how auto-projectors and vision torches
work.

TODO:
- [x] Track who is requesting to load what image so that an unsocketed
slide reel doesnt unload all slides
- [x] Investigate why load times are longer
- [x] Make loading the images async (on an SSD doing it sync is
unnoticeable but might be on older hardware)
- [x] I need somebody to test this on an HDD and see that the slide
reels are actually loading async without hitching
- [x] When slotting slide reels in on EOTP you get a ~3 frame drop. Does
not affect NH Examples (smaller images). Need to figure out why (since
this is meant to be async it shouldnt matter the image size)

In EOTP I save 6 seconds of load time and 3.5gb of memory (6.5gb vs
10gb)
This commit is contained in:
xen-42 2025-02-14 13:22:19 -05:00 committed by GitHub
commit d12ef2ac97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 441 additions and 36 deletions

View File

@ -150,28 +150,22 @@ namespace NewHorizons.Builder.Props
// Now we replace the slides
int slidesCount = info.slides.Length;
var slideCollection = new SlideCollection(slidesCount);
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];
var (invImageLoader, atlasImageLoader, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, true, true);
// 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 = !CacheExists(mod);
imageLoader.deleteTexturesWhenDone = true;
var key = GetUniqueSlideReelID(mod, info.slides);
if (invImageLoader != null && atlasImageLoader != null)
if (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) =>
{
@ -202,6 +196,7 @@ namespace NewHorizons.Builder.Props
{
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
@ -215,8 +210,14 @@ namespace NewHorizons.Builder.Props
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 = ImageUtilities.MakeReelTexture(mod, textures, key);
// 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;
@ -348,15 +349,16 @@ namespace NewHorizons.Builder.Props
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;
var slideCollection = new SlideCollection(slidesCount);
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);
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;
@ -381,6 +383,7 @@ namespace NewHorizons.Builder.Props
}
slideCollectionContainer.slideCollection = slideCollection;
slideCollectionContainer._playWithShipLogFacts = Array.Empty<string>(); // else it NREs in container initialize
StreamingHandler.SetUpStreaming(projectorObj, sector);
@ -402,9 +405,9 @@ namespace NewHorizons.Builder.Props
if (_visionTorchDetectorPrefab == null) return null;
// spawn a trigger for the vision torch
var g = DetailBuilder.Make(planetGO, sector, mod, _visionTorchDetectorPrefab, new DetailInfo(info) { scale = 2, rename = !string.IsNullOrEmpty(info.rename) ? info.rename : "VisionStaffDetector" });
var visionTorchTargetGO = DetailBuilder.Make(planetGO, sector, mod, _visionTorchDetectorPrefab, new DetailInfo(info) { scale = 2, rename = !string.IsNullOrEmpty(info.rename) ? info.rename : "VisionStaffDetector" });
if (g == null)
if (visionTorchTargetGO == null)
{
NHLogger.LogWarning($"Tried to make a vision torch target but couldn't. Do you have the DLC installed?");
return null;
@ -416,7 +419,7 @@ 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) = StartAsyncLoader(mod, info.slides, ref slideCollection, false, false);
var (_, _, imageLoader) = StartAsyncLoader(mod, info.slides, ref slideCollection, false, false, true);
imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index, string originalPath) =>
{
var time = DateTime.Now;
@ -425,17 +428,18 @@ namespace NewHorizons.Builder.Props
});
// attach 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<NHSlideCollectionContainer>();
var target = visionTorchTargetGO.AddComponent<VisionTorchTarget>();
var slideCollectionContainer = visionTorchTargetGO.AddComponent<NHSlideCollectionContainer>();
slideCollectionContainer.doAsyncLoading = false;
slideCollectionContainer.slideCollection = slideCollection;
target.slideCollection = g.AddComponent<MindSlideCollection>();
target.slideCollection = visionTorchTargetGO.AddComponent<MindSlideCollection>();
target.slideCollection._slideCollectionContainer = slideCollectionContainer;
LinkShipLogFacts(info, slideCollectionContainer);
g.SetActive(true);
visionTorchTargetGO.SetActive(true);
return g;
return visionTorchTargetGO;
}
public static GameObject MakeStandingVisionTorch(GameObject planetGO, Sector sector, ProjectionInfo info, IModBehaviour mod)
@ -469,7 +473,7 @@ namespace NewHorizons.Builder.Props
var slideCollection = new SlideCollection(slidesCount);
slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null
var (_, _, imageLoader) = StartAsyncLoader(mod, slides, ref slideCollection, false, false);
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
@ -494,6 +498,7 @@ namespace NewHorizons.Builder.Props
// Set up the containers for the slides
var slideCollectionContainer = standingTorch.AddComponent<NHSlideCollectionContainer>();
slideCollectionContainer.doAsyncLoading = false;
slideCollectionContainer.slideCollection = slideCollection;
var mindSlideCollection = standingTorch.AddComponent<MindSlideCollection>();
@ -512,8 +517,18 @@ namespace NewHorizons.Builder.Props
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)
StartAsyncLoader(IModBehaviour mod, SlideInfo[] slides, ref SlideCollection slideCollection, bool useInvertedCache, bool useAtlasCache, bool loadRawImages)
{
var invertedImageLoader = new SlideReelAsyncImageLoader();
var atlasImageLoader = new SlideReelAsyncImageLoader();
@ -545,7 +560,7 @@ namespace NewHorizons.Builder.Props
// 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
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")));
@ -553,12 +568,12 @@ namespace NewHorizons.Builder.Props
}
else
{
if (useInvertedCache && cacheExists)
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)));
}
else
if (!cacheExists || loadRawImages)
{
imageLoader.PathsToLoad.Add((i, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, slideInfo.imagePath)));
}
@ -577,12 +592,11 @@ namespace NewHorizons.Builder.Props
{
atlasImageLoader.Start(false, false);
}
// When using the inverted cache we never need the regular images
if (useInvertedCache)
{
invertedImageLoader.Start(true, false);
}
else
if (loadRawImages)
{
imageLoader.Start(true, false);
}

View File

@ -0,0 +1,215 @@
using HarmonyLib;
using NewHorizons.Builder.Props;
using NewHorizons.Utility;
using NewHorizons.Utility.Files;
using NewHorizons.Utility.OWML;
using OWML.Common;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace NewHorizons.Components.EOTE;
[HarmonyPatch]
public class NHSlideCollection : SlideCollection
{
public string[] slidePaths;
public IModBehaviour mod;
private HashSet<string> _pathsBeingLoaded = new();
/// <summary>
/// map of slide path to collections that have this path loaded. used to only unload slide when nothing else is using it
/// </summary>
public static Dictionary<string, HashSet<NHSlideCollection>> _slidesRequiringPath = new();
private static ShipLogSlideProjector _shipLogSlideProjector;
static NHSlideCollection()
{
SceneManager.sceneUnloaded += (_) =>
{
foreach (var (slide, collections) in _slidesRequiringPath)
{
// If it has null, that means some other permanent thing loaded this texture and it will get cleared elsewhere
// Otherwise it was loaded by an NHSlideCollection and should be deleted
if (collections.Any() && !collections.Contains(null))
{
ImageUtilities.DeleteTexture(slide);
}
}
_slidesRequiringPath.Clear();
};
}
public NHSlideCollection(int startArrSize, IModBehaviour mod, string[] slidePaths) : base(startArrSize)
{
this.mod = mod;
this.slidePaths = slidePaths;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollection), nameof(SlideCollection.RequestStreamSlides))]
public static bool SlideCollection_RequestStreamSlides(SlideCollection __instance, int[] slideIndices)
{
if (__instance is NHSlideCollection collection)
{
foreach (var id in slideIndices)
{
collection.LoadSlide(id);
}
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollection), nameof(SlideCollection.RequestRelease))]
public static bool SlideCollection_RequestRelease(SlideCollection __instance, int[] slideIndices)
{
if (__instance is NHSlideCollection collection)
{
foreach (var id in slideIndices)
{
collection.UnloadSlide(id);
}
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollection), nameof(SlideCollection.IsStreamedTextureIndexLoaded))]
public static bool SlideCollection_IsStreamedTextureIndexLoaded(SlideCollection __instance, int streamIdx, ref bool __result)
{
if (__instance is NHSlideCollection collection)
{
__result = collection.IsSlideLoaded(streamIdx);
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollection), nameof(SlideCollection.GetStreamingTexture))]
public static bool SlideCollection_GetStreamingTexture(SlideCollection __instance, int id, ref Texture __result)
{
if (__instance is NHSlideCollection collection)
{
__result = collection.LoadSlide(id);
return false;
}
else
{
return true;
}
}
public Texture LoadSlide(int index)
{
Texture LoadSlideInt(int index)
{
var wrappedIndex = (index + slides.Length) % slides.Length;
var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, ProjectionBuilder.InvertedSlideReelCacheFolder, slidePaths[wrappedIndex]);
// We are the first slide collection container to try and load this image
var key = ImageUtilities.GetKey(mod, path);
if (!_slidesRequiringPath.ContainsKey(key))
{
// Something else has loaded this image i.e., AutoProjector or Vision torch. We want to ensure we do not delete it
if (ImageUtilities.IsTextureLoaded(mod, path))
{
// null is dummy value to ensure its never empty (so its not deleted)
_slidesRequiringPath[key] = new() { null };
}
else
{
_slidesRequiringPath[key] = new();
}
_slidesRequiringPath[key].Add(this);
}
if (ImageUtilities.IsTextureLoaded(mod, path))
{
// already loaded
var texture = ImageUtilities.GetTexture(mod, path);
slides[wrappedIndex]._image = texture;
return texture;
}
else if (!_pathsBeingLoaded.Contains(path))
{
// not loaded yet, we need to load it
var loader = new SlideReelAsyncImageLoader();
loader.PathsToLoad.Add((wrappedIndex, path));
loader.Start(true, false);
loader.imageLoadedEvent.AddListener((Texture2D tex, int index, string originalPath) =>
{
// weird: sometimes we set image, sometimes we return from GetStreamingTexture. oh well
slides[wrappedIndex]._image = tex;
_pathsBeingLoaded.Remove(path);
if (_shipLogSlideProjector == null)
{
// Object.FindObjectOfType doesnt work with inactive
_shipLogSlideProjector = Resources.FindObjectsOfTypeAll<ShipLogSlideProjector>().FirstOrDefault();
}
if (_shipLogSlideProjector != null)
{
// gotta tell ship log we updated the image
_shipLogSlideProjector._slideDirty = true;
}
else
{
NHLogger.LogVerbose("No ship log slide reel projector exists");
}
});
_pathsBeingLoaded.Add(path);
return null;
}
else
{
// It is being loaded so we just wait
return null;
}
}
var texture = LoadSlideInt(index);
LoadSlideInt(index - 1);
LoadSlideInt(index + 1);
return texture;
}
public bool IsSlideLoaded(int index)
{
var wrappedIndex = (index + slides.Length) % slides.Length;
var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, ProjectionBuilder.InvertedSlideReelCacheFolder, slidePaths[wrappedIndex]);
return ImageUtilities.IsTextureLoaded(mod, path);
}
public void UnloadSlide(int index)
{
var wrappedIndex = (index + slides.Length) % slides.Length;
var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, ProjectionBuilder.InvertedSlideReelCacheFolder, slidePaths[wrappedIndex]);
// Only unload textures that we were the ones to load in
if (ImageUtilities.IsTextureLoaded(mod, path))
{
var key = ImageUtilities.GetKey(mod, path);
_slidesRequiringPath[key].Remove(this);
if (!_slidesRequiringPath[key].Any())
{
NHLogger.LogVerbose($"Slide reel deleting {key} since nobody is using it anymore");
ImageUtilities.DeleteTexture(mod, path, ImageUtilities.GetTexture(mod, path));
slides[wrappedIndex]._image = null;
}
}
}
}

View File

@ -1,4 +1,7 @@
using HarmonyLib;
using System;
using System.Linq;
using UnityEngine;
namespace NewHorizons.Components.EOTE;
@ -7,12 +10,14 @@ public class NHSlideCollectionContainer : SlideCollectionContainer
{
public string[] conditionsToSet;
public string[] persistentConditionsToSet;
// at some point we'll do streaming on all slides. until then just have an off switch
public bool doAsyncLoading = true;
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollectionContainer), nameof(SlideCollectionContainer.Initialize))]
public static bool SlideCollectionContainer_Initialize(SlideCollectionContainer __instance)
{
if (__instance is NHSlideCollectionContainer)
if (__instance is NHSlideCollectionContainer container)
{
if (__instance._initialized)
return false;
@ -28,6 +33,7 @@ public class NHSlideCollectionContainer : SlideCollectionContainer
{
var fact = Locator.GetShipLogManager().GetFact(factID);
fact?.RegisterSlideCollection(__instance._slideCollection);
// in original it logs. we dont want that here ig
}
return false;
}
@ -59,4 +65,98 @@ public class NHSlideCollectionContainer : SlideCollectionContainer
}
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollectionContainer), nameof(SlideCollectionContainer.NextSlideAvailable))]
public static bool SlideCollectionContainer_NextSlideAvailable(SlideCollectionContainer __instance, ref bool __result)
{
if (__instance is NHSlideCollectionContainer container && container.doAsyncLoading)
{
__result = ((NHSlideCollection)container.slideCollection).IsSlideLoaded(container.slideIndex + 1);
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollectionContainer), nameof(SlideCollectionContainer.PrevSlideAvailable))]
public static bool SlideCollectionContainer_PrevSlideAvailable(SlideCollectionContainer __instance, ref bool __result)
{
if (__instance is NHSlideCollectionContainer container && container.doAsyncLoading)
{
__result = ((NHSlideCollection)container.slideCollection).IsSlideLoaded(container.slideIndex - 1);
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollectionContainer), nameof(SlideCollectionContainer.UnloadStreamingTextures))]
public static bool SlideCollectionContainer_UnloadStreamingTextures(SlideCollectionContainer __instance)
{
if (__instance is NHSlideCollectionContainer container && container.doAsyncLoading)
{
for (int i = 0; i < ((NHSlideCollection)container.slideCollection).slidePaths.Length; i++)
{
((NHSlideCollection)container.slideCollection).UnloadSlide(i);
}
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollectionContainer), nameof(SlideCollectionContainer.GetStreamingTexture))]
public static bool SlideCollectionContainer_GetStreamingTexture(SlideCollectionContainer __instance, int id, ref Texture __result)
{
if (__instance is NHSlideCollectionContainer container && container.doAsyncLoading)
{
__result = ((NHSlideCollection)container.slideCollection).LoadSlide(id);
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollectionContainer), nameof(SlideCollectionContainer.RequestManualStreamSlides))]
public static bool SlideCollectionContainer_RequestManualStreamSlides(SlideCollectionContainer __instance)
{
if (__instance is NHSlideCollectionContainer container && container.doAsyncLoading)
{
((NHSlideCollection)container.slideCollection).LoadSlide(__instance._currentSlideIndex);
return false;
}
else
{
return true;
}
}
[HarmonyPrefix]
[HarmonyPatch(typeof(SlideCollectionContainer), nameof(SlideCollectionContainer.streamingTexturesAvailable), MethodType.Getter)]
public static bool SlideCollectionContainer_streamingTexturesAvailable(SlideCollectionContainer __instance, ref bool __result)
{
if (__instance is NHSlideCollectionContainer container && container.doAsyncLoading)
{
__result = ((NHSlideCollection)container.slideCollection).slidePaths != null && ((NHSlideCollection)container.slideCollection).slidePaths.Any();
return false;
}
else
{
return true;
}
}
}

View File

@ -85,6 +85,12 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye
/// Exclusive to the slide reel type. Condition/material of the reel. Antique is the Stranger, Pristine is the Dreamworld, Rusted is a burned reel.
/// </summary>
[DefaultValue("antique")] public SlideReelCondition reelCondition = SlideReelCondition.Antique;
}
/// <summary>
/// Set which slides appear on the slide reel model. Leave empty to default to the first few slides.
/// Takes a list of indices, i.e., to show the first 5 slides in reverse you would put [4, 3, 2, 1, 0].
/// Index starts at 0.
/// </summary>
public int[] displaySlides;
}
}

View File

@ -0,0 +1,37 @@
using HarmonyLib;
using NewHorizons.Components.EOTE;
namespace NewHorizons.Patches.ShipLogPatches;
[HarmonyPatch]
public static class ShipLogSlideReelPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(ShipLogSlideProjector), nameof(ShipLogSlideProjector.CheckStreamingTexturesAvailable))]
public static bool ShipLogSlideProjector_CheckStreamingTexturesAvailable(ShipLogSlideProjector __instance, ref bool __result)
{
if (__instance._collectionIndex >= 0 && __instance._collectionIndex < __instance._slideCollections.Count &&
__instance._slideCollections[__instance._collectionIndex] is NHSlideCollection)
{
__result = true;
return false;
}
return true;
}
[HarmonyPrefix]
[HarmonyPatch(typeof(ShipLogSlideProjector), nameof(ShipLogSlideProjector.UnloadCurrentStreamingTextures))]
public static bool ShipLogSlideProjector_UnloadCurrentStreamingTextures(ShipLogSlideProjector __instance)
{
if (__instance._collectionIndex >= 0 && __instance._collectionIndex < __instance._slideCollections.Count &&
__instance._slideCollections[__instance._collectionIndex] is NHSlideCollection collection)
{
for (int i = 0; i < collection.slides.Length; i++)
{
collection.UnloadSlide(i);
}
return false;
}
return true;
}
}

View File

@ -5,6 +5,8 @@ namespace NewHorizons.Patches.ToolPatches
[HarmonyPatch(typeof(ToolModeSwapper))]
public static class ToolModeSwapperPatches
{
private static ShipCockpitController _shipCockpitController;
// 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
@ -21,7 +23,9 @@ namespace NewHorizons.Patches.ToolPatches
mode == ToolMode.Probe ||
mode == ToolMode.SignalScope ||
mode == ToolMode.Translator;
var isInShip = UnityEngine.Object.FindObjectOfType<ShipCockpitController>()?._playerAtFlightConsole ?? false;
if (_shipCockpitController == null)
_shipCockpitController = UnityEngine.Object.FindObjectOfType<ShipCockpitController>();
var isInShip = _shipCockpitController != null ? _shipCockpitController._playerAtFlightConsole : false;
if (!isInShip && isHoldingVisionTorch && swappingToRestrictedTool) return false;

View File

@ -2863,6 +2863,14 @@
"description": "Exclusive to the slide reel type. Condition/material of the reel. Antique is the Stranger, Pristine is the Dreamworld, Rusted is a burned reel.",
"default": "antique",
"$ref": "#/definitions/SlideReelCondition"
},
"displaySlides": {
"type": "array",
"description": "Set which slides appear on the slide reel model. Leave empty to default to the first few slides.\nTakes a list of indices, i.e., to show the first 5 slides in reverse you would put [4, 3, 2, 1, 0].\nIndex starts at 0.",
"items": {
"type": "integer",
"format": "int32"
}
}
}
},

View File

@ -15,6 +15,9 @@ namespace NewHorizons.Utility.Files
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); // dont reinsert cuz that causes memory leak!
public static string GetKey(IModBehaviour mod, string filename)
=> GetKey(Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename));
public static string GetKey(string path) =>
path.Substring(Main.Instance.ModHelper.OwmlConfig.ModsPath.Length + 1).Replace('\\', '/');
@ -44,7 +47,7 @@ namespace NewHorizons.Utility.Files
var key = GetKey(path);
if (_textureCache.TryGetValue(key, out var existingTexture))
{
NHLogger.LogVerbose($"Already loaded image at path: {path}");
//NHLogger.LogVerbose($"Already loaded image at path: {path}");
return (Texture2D)existingTexture;
}
@ -68,6 +71,19 @@ namespace NewHorizons.Utility.Files
}
}
/// <summary>
/// Not sure why the other method takes in the texture as well
/// </summary>
/// <param name="key"></param>
public static void DeleteTexture(string key)
{
if (_textureCache.ContainsKey(key))
{
UnityEngine.Object.Destroy(_textureCache[key]);
_textureCache.Remove(key);
}
}
public static void DeleteTexture(IModBehaviour mod, string filename, Texture2D texture)
{
var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename);
@ -191,9 +207,9 @@ namespace NewHorizons.Utility.Files
{
for (int j = 0; j < size; j++)
{
var colour = Color.black;
var colour = Color.clear;
if (srcTexture)
if (srcTexture != null)
{
var srcX = i * srcTexture.width / (float)size;
var srcY = j * srcTexture.height / (float)size;

View File

@ -42,6 +42,11 @@ public class SlideReelAsyncImageLoader
private bool _started;
private bool _clamp;
/// <summary>
/// start loading the images a frame later
/// </summary>
/// <param name="clamp">sets wrapMode</param>
/// <param name="sequential">load all slides one at a time vs at the same time</param>
public void Start(bool clamp, bool sequential)
{
if (_started) return;