Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0292a4d
Add new GenerateSoftRefManifest method
jneb802 Mar 19, 2025
1ce254d
Add harmonyPatch to add softRef manifests to vanilla SoftReferences
jneb802 Mar 19, 2025
bd13895
Add new GenerateAssetID overload
jneb802 Mar 19, 2025
3721764
Add new CustomLocation SoftRef<GameObject> constructor
jneb802 Mar 19, 2025
7a5d8be
Add new DestroyLocationContainer to remove location from container if…
jneb802 Apr 9, 2025
d4b97f9
Use new InjectLoadedAsset in customLocation initializer
jneb802 Apr 9, 2025
3abd936
Remove logic to add customLocation to locationContainer
jneb802 Apr 9, 2025
2632971
Clean up previous changes
jneb802 Apr 9, 2025
8389154
Add new InjectLoadedAsset method to add asset to SoftRef system, used…
jneb802 Apr 9, 2025
24da505
Clean ups for PR
jneb802 Apr 9, 2025
383fb90
Skip mock prefabs in PrefabManager RegisterToZNetScece
jneb802 May 4, 2025
c920f0c
Add new SpawnLocation() HarmonyPatches to resolve location prefab mocks
jneb802 May 4, 2025
080e7dc
Skip existing fixReference if customLocation use softReference
jneb802 May 4, 2025
eb3dd39
Load softReference prefab and ignore non-mocked prefabs in PrefabMana…
jneb802 May 4, 2025
3b8b6f5
Remove InjectLoadedAsset method and new GenerateAssetID method from P…
jneb802 May 4, 2025
f704737
Add new SoftReference field for use in checks when mocking and spawni…
jneb802 May 4, 2025
1c2f307
Revert to existing Jotunn state
jneb802 May 4, 2025
efb0cdd
Add new customLocation constructor for softReference locations
jneb802 May 4, 2025
13068b2
Remove extra line breaks
jneb802 May 4, 2025
6cee10e
Skip activate customLocation prefab if using SoftRefernce
jneb802 May 4, 2025
a793313
Remove DestroyCustomLocation method because no longer needed in PR
jneb802 May 4, 2025
9c0ce8b
Remove DestroyLocationContainer and add back DestroyCustomLocation.
jneb802 May 4, 2025
f14d2ce
chore: use MockManager.JVLMockPrefix instead of magic string
MSchmoecker May 8, 2025
1729d0b
chore: formatting
MSchmoecker May 8, 2025
0afaa12
Change customLocation softReference constructor param to softReferenc…
jneb802 May 14, 2025
425a86b
fix: handle spawn and destroy of SoftRef location prefabs to prevent …
MSchmoecker May 29, 2025
5f746b0
fix: load time of SoftRef locations and prepare them after instanciating
MSchmoecker Jun 15, 2025
cdf6b15
fix: prevent errors while rendering broken prefabs
MSchmoecker Jun 15, 2025
96dcb3c
feat: allow multiple callbacks for ResolveMocksOnLoad
MSchmoecker Jun 15, 2025
56713c2
chore: better AssetLoader entrypoint to resolve mocks
MSchmoecker Jun 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions JotunnLib/Entities/CustomLocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Jotunn.Managers;
using SoftReferenceableAssets;
using UnityEngine;
using Object = UnityEngine.Object;

namespace Jotunn.Entities
{
Expand Down Expand Up @@ -38,6 +39,12 @@ public class CustomLocation : CustomEntity
/// Indicator if references from <see cref="Entities.Mock{T}"/>s will be replaced at runtime.
/// </summary>
public bool FixReference { get; set; }

/// <summary>
/// Indicator if location is added from SoftReferenceableAssets.<br />
/// Used to delay mocking prefabs until ZoneSystem.SpawnLocation()
/// </summary>
public bool SoftReference { get; set; }

/// <summary>
/// Custom location from a prefab with a <see cref="LocationConfig"/> attached.<br />
Expand Down Expand Up @@ -103,6 +110,38 @@ public CustomLocation(GameObject exteriorPrefab, GameObject interiorPrefab, bool
FixReference = fixReference;
}

/// <summary>
/// Custom location from a prefab with a <see cref="LocationConfig"/> attached. Using SoftReference system.
/// </summary>
/// <param name="softReferencePrefab">The exterior prefab for this custom location.</param>
/// <param name="fixReference">If true references for <see cref="Entities.Mock{T}"/> objects get resolved at runtime by Jötunn.</param>
/// <param name="locationConfig">The <see cref="LocationConfig"/> for this custom location.</param>
public CustomLocation(SoftReference<GameObject> softReferencePrefab, bool fixReference, LocationConfig locationConfig) : base(Assembly.GetCallingAssembly())
{
if (!softReferencePrefab.IsValid)
{
Logger.LogError($"SoftReference invalid for prefab: {softReferencePrefab.Name}");
return;
}

var parent = ZoneManager.Instance.LocationContainer.transform;
AssetManager.Instance.ResolveMocksOnLoad(softReferencePrefab, parent, OnLocationResolve);
Name = softReferencePrefab.Name;
ZoneLocation = locationConfig.GetZoneLocation();
ZoneLocation.m_prefab = softReferencePrefab;
ZoneLocation.m_prefabName = softReferencePrefab.Name;
FixReference = fixReference;
SoftReference = true;
}

private void OnLocationResolve(GameObject gameObject)
{
if (gameObject.TryGetComponent<ZoneSystem.ZoneLocation>(out var zoneLocation))
{
ZoneManager.Instance.PrepareLocation(zoneLocation, SourceMod);
}
}

/// <summary>
/// Helper method to determine if a location prefab with a given name is a custom location created with Jötunn.
/// </summary>
Expand Down
135 changes: 134 additions & 1 deletion JotunnLib/Managers/AssetManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class AssetManager : IManager
private Dictionary<Type, Dictionary<string, AssetID>> mapNameToAssetID;
internal Dictionary<Type, Dictionary<string, AssetID>> MapNameToAssetID => mapNameToAssetID ??= CreateNameToAssetID();

private GameObject ResolvedAssetsContainer;
private Dictionary<AssetID, MockResolutionContext> assetsToResolve = new Dictionary<AssetID, MockResolutionContext>();

/// <summary>
/// Hide .ctor
/// </summary>
Expand All @@ -45,6 +48,11 @@ static AssetManager()
void IManager.Init()
{
Main.LogInit(nameof(AssetManager));

ResolvedAssetsContainer = new GameObject("Resolved Assets");
ResolvedAssetsContainer.transform.parent = Main.RootObject.transform;
ResolvedAssetsContainer.SetActive(false);

Main.Harmony.PatchAll(typeof(Patches));
}

Expand Down Expand Up @@ -84,6 +92,31 @@ private static IEnumerable<CodeInstruction> AssetBundleLoader_GetAllAssetPathsMa
.SetInstruction(new CodeInstruction(OpCodes.Call, addSafeMethod))
.InstructionEnumeration();
}

[HarmonyPatch(typeof(AssetLoader), nameof(AssetLoader.InvokeCallbacks)), HarmonyPrefix]
private static void SwapResolvedAsset(ref AssetLoader __instance, LoadResult result)
{
if (result == LoadResult.Succeeded)
{
if (Instance.assetsToResolve.TryGetValue(__instance.m_assetID, out var mockedAsset))
{
mockedAsset.InstantiateAndResolveAsset(__instance.m_asset);
__instance.m_asset = mockedAsset.Asset;
}
}
}

[HarmonyPatch(typeof(AssetLoader), nameof(AssetLoader.Release)), HarmonyPostfix]
private static void AssetLoader_Release(ref AssetLoader __instance)
{
if (__instance.ReferenceCount == 0)
{
if (Instance.assetsToResolve.TryGetValue(__instance.m_assetID, out var mockedAsset))
{
mockedAsset.DestroyAsset();
}
}
}
}

/// <summary>
Expand Down Expand Up @@ -135,6 +168,59 @@ public AssetID AddAsset(Object asset)
return AddAsset(asset, null);
}

/// <summary>
/// Registers an asset to be instantiated under the given parent and have its mock references resolved on load.<b/>
/// Must be called before the asset is loaded the first time.
/// </summary>
/// <param name="assetID">The <see cref="AssetID"/> of the asset to instantiate and resolve mocks for on load</param>
public void ResolveMocksOnLoad(AssetID assetID)
{
ResolveMocksOnLoad(assetID, null, null);
}

/// <summary>
/// Registers an asset to be instantiated under the given parent and have its mock references resolved on load.<b/>
/// Must be called before the asset is loaded the first time.
/// </summary>
/// <param name="assetID">The <see cref="AssetID"/> of the asset to instantiate and resolve mocks for on load</param>
/// <param name="parent">Optional transform under which the asset will be instantiated, otherwise a default container is used</param>
public void ResolveMocksOnLoad(AssetID assetID, Transform parent)
{
ResolveMocksOnLoad(assetID, parent, null);
}

/// <summary>
/// Registers an asset to be instantiated under the given parent and have its mock references resolved on load.<b/>
/// Must be called before the asset is loaded the first time.
/// </summary>
/// <param name="softReference">The <see cref="SoftReference{T}"/> to instantiate and resolve mocks for on load</param>
/// <param name="parent">Optional transform under which the asset will be instantiated, otherwise a default container is used</param>
/// <param name="resolveCallback">Adds a callback when the asset was resolved and instantiated</param>
public void ResolveMocksOnLoad<T>(SoftReference<T> softReference, Transform parent, Action<T> resolveCallback) where T : Object
{
ResolveMocksOnLoad(softReference.m_assetID, parent, (asset) => resolveCallback?.Invoke(asset as T));
}

/// <summary>
/// Registers an asset to be instantiated under the given parent and have its mock references resolved on load.<b/>
/// Must be called before the asset is loaded the first time.
/// </summary>
/// <param name="assetID">The <see cref="AssetID"/> of the asset to instantiate and resolve mocks for on load</param>
/// <param name="parent">Optional transform under which the asset will be instantiated, otherwise a default container is used</param>
/// <param name="resolveCallback">Adds a callback when the asset was resolved and instantiated</param>
public void ResolveMocksOnLoad(AssetID assetID, Transform parent, Action<Object> resolveCallback)
{
if (assetsToResolve.TryGetValue(assetID, out var context))
{
context.Parent = parent ?? context.Parent ?? ResolvedAssetsContainer.transform;
context.ResolveCallback += resolveCallback;
}
else
{
assetsToResolve.Add(assetID, new MockResolutionContext(parent ?? ResolvedAssetsContainer.transform, resolveCallback));
}
}

private static void AddAssetToBundleLoader(AssetBundleLoader assetBundleLoader, AssetID assetID, AssetRef assetRef)
{
// create fake bundle, since an AssetBundle can't be created at runtime
Expand Down Expand Up @@ -276,7 +362,7 @@ public SoftReference<Object> GetSoftReference(Type type, string name)
/// <param name="name">Asset name to search for</param>
/// <typeparam name="T">Asset type to search for</typeparam>
/// <returns></returns>
public SoftReference<T> GetSoftReference<T>(string name) where T: Object
public SoftReference<T> GetSoftReference<T>(string name) where T : Object
{
AssetID assetID = GetAssetID<T>(name);
return assetID.IsValid ? new SoftReference<T>(assetID) : default;
Expand Down Expand Up @@ -409,5 +495,52 @@ public AssetRef(BepInPlugin sourceMod, Object asset, Object original)
this.originalID = original && Instance.IsReady() ? Instance.GetAssetID(original.GetType(), original.name) : default;
}
}

internal class MockResolutionContext
{
public Object Asset { get; private set; }
public Transform Parent { get; set; }
public Action<Object> ResolveCallback { get; set; }

public MockResolutionContext(Transform parent, Action<Object> resolveCallback)
{
this.Parent = parent;
this.ResolveCallback += resolveCallback;
}

public bool IsResolved => (bool)Asset;

public void InstantiateAndResolveAsset(Object realAsset)
{
if (IsResolved)
{
return;
}

Asset = Object.Instantiate(realAsset, Parent);
Asset.name = realAsset.name;

if (Asset is GameObject gameObject)
{
gameObject.FixReferences(true);
}
else
{
Asset.FixReferences();
}

ResolveCallback?.Invoke(Asset);
}

public void DestroyAsset()
{
if (Asset)
{
Object.Destroy(Asset);
}

Asset = null;
}
}
}
}
6 changes: 6 additions & 0 deletions JotunnLib/Managers/PrefabManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ public void RegisterToZNetScene(GameObject gameObject)
if (znet)
{
string name = gameObject.name;

if (gameObject.name.StartsWith(MockManager.JVLMockPrefix))
{
return;
}

int hash = name.GetStableHashCode();

if (znet.m_namedPrefabs.ContainsKey(hash))
Expand Down
10 changes: 10 additions & 0 deletions JotunnLib/Managers/RenderManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -451,8 +451,18 @@ private static List<Type> GetTopolocialSort(Transform tranform)
HashSet<Type> visitedNodes = new HashSet<Type>();
HashSet<Type> recursionStack = new HashSet<Type>();

if (!tranform || !tranform.gameObject)
{
return result;
}

foreach (Component component in tranform.gameObject.GetComponents<Component>())
{
if (!component)
{
continue;
}

if (!TopologicalSortUtil(component.GetType(), visitedNodes, recursionStack, result))
{
Logger.LogWarning($"Cycles detected in component dependencies for type {component.GetType()}. Unable to determine deletion order for {tranform}.");
Expand Down
84 changes: 45 additions & 39 deletions JotunnLib/Managers/ZoneManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,14 @@ public bool AddCustomLocation(CustomLocation customLocation)
return false;
}

customLocation.Prefab.transform.SetParent(LocationContainer.transform);

// The root prefab needs to be active, otherwise ZNetViews are not prepared correctly
customLocation.Prefab.SetActive(true);

// Skip if location uses softReference system.
// If using softReference prefab is in assetbundle and therefore not immutable
if (!customLocation.SoftReference)
{
// The root prefab needs to be active, otherwise ZNetViews are not prepared correctly
customLocation.Prefab.SetActive(true);
}

Locations.Add(customLocation.Name, customLocation);
return true;
}
Expand Down Expand Up @@ -504,24 +507,25 @@ private void RegisterLocations(ZoneSystem self)
{
try
{
Logger.LogDebug(
$"Adding custom location {customLocation} in {string.Join(", ", GetMatchingBiomes(customLocation.ZoneLocation.m_biome))}");
Logger.LogDebug($"Adding custom location {customLocation} in {string.Join(", ", GetMatchingBiomes(customLocation.ZoneLocation.m_biome))}");

// Fix references if needed
if (customLocation.FixReference)
if (customLocation.FixReference && !customLocation.SoftReference)
{
customLocation.Prefab.FixReferences(true);
customLocation.FixReference = false;
}

var zoneLocation = customLocation.ZoneLocation;
if (!customLocation.SoftReference)
{
PrepareLocation(customLocation.ZoneLocation, customLocation.SourceMod);
}

RegisterLocationInZoneSystem(self, zoneLocation, customLocation.SourceMod);
RegisterLocationInZoneSystem(self, customLocation.ZoneLocation);
}
catch (Exception ex)
{
Logger.LogWarning(customLocation?.SourceMod, $"Exception caught while adding location: {ex}");
toDelete.Add(customLocation.Name);
toDelete.Add(customLocation?.Name);
}
}

Expand Down Expand Up @@ -581,30 +585,19 @@ private void RegisterVegetation(ZoneSystem self)
/// No mock references are fixed.
/// </summary>
/// <param name="zoneLocation"><see cref="ZoneLocation"/> to add to the <see cref="ZoneSystem"/></param>
public void RegisterLocationInZoneSystem(ZoneLocation zoneLocation) =>
RegisterLocationInZoneSystem(ZoneSystem.instance, zoneLocation, BepInExUtils.GetSourceModMetadata());
public void RegisterLocationInZoneSystem(ZoneLocation zoneLocation)
{
PrepareLocation(zoneLocation, BepInExUtils.GetSourceModMetadata());
RegisterLocationInZoneSystem(ZoneSystem.instance, zoneLocation);
}

/// <summary>
/// Internal method for adding a ZoneLocation to a specific ZoneSystem.
/// </summary>
/// <param name="zoneSystem"><see cref="ZoneSystem"/> the location should be added to</param>
/// <param name="zoneLocation"><see cref="ZoneLocation"/> to add</param>
/// <param name="sourceMod"><see cref="BepInPlugin"/> which created the location</param>
private void RegisterLocationInZoneSystem(ZoneSystem zoneSystem, ZoneLocation zoneLocation, BepInPlugin sourceMod)
internal void PrepareLocation(ZoneLocation zoneLocation, BepInPlugin sourceMod)
{
zoneLocation.m_prefab.Load();

foreach (var znet in global::Utils.GetEnabledComponentsInChildren<ZNetView>(zoneLocation.m_prefab.Asset))
{
string prefabName = znet.GetPrefabName();
if (!ZNetScene.instance.m_namedPrefabs.ContainsKey(prefabName.GetStableHashCode()))
{
var prefab = Object.Instantiate(znet.gameObject, PrefabManager.Instance.PrefabContainer.transform);
prefab.name = prefabName;
CustomPrefab customPrefab = new CustomPrefab(prefab, sourceMod);
PrefabManager.Instance.AddPrefab(customPrefab);
PrefabManager.Instance.RegisterToZNetScene(customPrefab.Prefab);
}
RegisterUnknownPrefab(sourceMod, znet);
}

RandomSpawn[] randomSpawns = global::Utils.GetEnabledComponentsInChildren<RandomSpawn>(zoneLocation.m_prefab.Asset);
Expand All @@ -615,17 +608,30 @@ private void RegisterLocationInZoneSystem(ZoneSystem zoneSystem, ZoneLocation zo

foreach (var znet in randomSpawns.SelectMany(x => x.m_childNetViews))
{
string prefabName = znet.GetPrefabName();
if (!ZNetScene.instance.m_namedPrefabs.ContainsKey(prefabName.GetStableHashCode()))
{
var prefab = Object.Instantiate(znet.gameObject, PrefabManager.Instance.PrefabContainer.transform);
prefab.name = prefabName;
CustomPrefab customPrefab = new CustomPrefab(prefab, sourceMod);
PrefabManager.Instance.AddPrefab(customPrefab);
PrefabManager.Instance.RegisterToZNetScene(customPrefab.Prefab);
}
RegisterUnknownPrefab(sourceMod, znet);
}
}

private static void RegisterUnknownPrefab(BepInPlugin sourceMod, ZNetView znet) {
string prefabName = znet.GetPrefabName();

if (prefabName.StartsWith(MockManager.JVLMockPrefix))
{
return;
}

if (!ZNetScene.instance.m_namedPrefabs.ContainsKey(prefabName.GetStableHashCode()))
{
var prefab = Object.Instantiate(znet.gameObject, PrefabManager.Instance.PrefabContainer.transform);
prefab.name = prefabName;
CustomPrefab customPrefab = new CustomPrefab(prefab, sourceMod);
PrefabManager.Instance.AddPrefab(customPrefab);
PrefabManager.Instance.RegisterToZNetScene(customPrefab.Prefab);
}
}

private void RegisterLocationInZoneSystem(ZoneSystem zoneSystem, ZoneLocation zoneLocation)
{
if (!zoneSystem.m_locationsByHash.ContainsKey(zoneLocation.Hash))
{
zoneSystem.m_locationsByHash.Add(zoneLocation.Hash, zoneLocation);
Expand Down
Loading