Skip to content
Open
Changes from 5 commits
Commits
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
90 changes: 88 additions & 2 deletions Assets/Scripts/Utility/AssetInjection/WorldDataReplacement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@

namespace DaggerfallWorkshop.Utility.AssetInjection
{
/// <summary>
/// Thrown when WorldDataReplacement can’t load or parse the expected JSON for a block.
/// </summary>
public class WorldDataReplacementException : System.Exception
{
public WorldDataReplacementException(string message) : base(message) { }
public WorldDataReplacementException(string message, System.Exception inner) : base(message, inner) { }
}

public struct BlockRecordKey
{
public int blockIndex;
Expand Down Expand Up @@ -486,7 +495,7 @@ private static bool AssignBlockIndices(ref DFLocation dfLocation)
// RMB blocks
foreach (string blockName in dfLocation.Exterior.ExteriorData.BlockNames)
if (blocksFile.GetBlockIndex(blockName) == -1)
AssignNextIndex(blockName);
AssignNewIndex(blockName);

// RDB blocks
if (dfLocation.Dungeon.Blocks != null)
Expand All @@ -495,7 +504,7 @@ private static bool AssignBlockIndices(ref DFLocation dfLocation)
{
string blockName = dungeonBlock.BlockName;
if (blocksFile.GetBlockIndex(blockName) == -1)
AssignNextIndex(blockName);
AssignNewIndex(blockName);
}
}
return true;
Expand All @@ -504,6 +513,83 @@ private static bool AssignBlockIndices(ref DFLocation dfLocation)
return false;
}

public static void AssignNewIndex(string blockName)
{
string fileName = GetDFBlockReplacementFilename(blockName, WorldDataVariants.NoVariant);

int jsonBlockIndex;
DFBlock? dfBlock = null; // Make blockData nullable
TextAsset blockReplacementJsonAsset;

// Seek from loose files
if (File.Exists(Path.Combine(worldDataPath, fileName)))
{
string blockReplacementJson = File.ReadAllText(Path.Combine(worldDataPath, fileName));
try
{
dfBlock = (DFBlock)SaveLoadManager.Deserialize(typeof(DFBlock), blockReplacementJson);
}
catch (System.InvalidCastException e)
{
Debug.LogError($"Invalid JSON format for block '{blockName}': {e.Message}");
throw new WorldDataReplacementException(
$"Block '{blockName}' JSON could not be cast to DFBlock.", e
);
}
}
// Seek from mods
else if (ModManager.Instance != null && ModManager.Instance.TryGetAsset(fileName, false, out blockReplacementJsonAsset))
{
try
{
dfBlock = (DFBlock)SaveLoadManager.Deserialize(typeof(DFBlock), blockReplacementJsonAsset.text);
}
catch (System.InvalidCastException e)
{
Debug.LogError($"Invalid JSON format for block '{blockName}': {e.Message}");
throw new WorldDataReplacementException(
$"Block '{blockName}' JSON could not be cast to DFBlock.", e
);
}
}

// Ensure blockData was successfully assigned
if (!dfBlock.HasValue)
{
Debug.LogError($"Failed to load block data for blockName: {blockName}");
throw new WorldDataReplacementException(
$"Block '{blockName}' does not have a valid Index in its JSON file."
);
}

// Grab the variant key
string blockKey = blockName + WorldDataVariants.NoVariant;

// Check for the "Index" field and assign its value
jsonBlockIndex = dfBlock.Value.Index;

// If jsonBlockIndex is invalid (less than or equal to nextBlockIndex), use fallback method
if (jsonBlockIndex <= nextBlockIndex)
{
AssignNextIndex(blockName);

// Cache the full DFBlock
if (!blocks.ContainsKey(blockKey))
blocks[blockKey] = dfBlock.Value;
Copy link
Contributor

@TwelveEyes TwelveEyes Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cached DFBlock for the AssignNextIndex fallback also needs to have it's internal Index value updated. What I would recommend:

diff --git a/Assets/Scripts/Utility/AssetInjection/WorldDataReplacement.cs b/Assets/Scripts/Utility/AssetInjection/WorldDataReplacement.cs
index 92ddee749..73875e6e4 100644
--- a/Assets/Scripts/Utility/AssetInjection/WorldDataReplacement.cs
+++ b/Assets/Scripts/Utility/AssetInjection/WorldDataReplacement.cs
@@ -627,11 +627,19 @@ namespace DaggerfallWorkshop.Utility.AssetInjection
             // If jsonBlockIndex is invalid (less than or equal to nextBlockIndex), use fallback method
             if (jsonBlockIndex <= nextBlockIndex)
             {
+                int newIndex = nextBlockIndex;
+
                 AssignNextIndex(blockName);
 
                 // Cache the full DFBlock
                 if (!blocks.ContainsKey(blockKey))
-                    blocks[blockKey] = dfBlock.Value;
+                {
+                    // Update DFBlock index
+                    DFBlock block = dfBlock.Value;
+                    block.Index = newIndex;
+
+                    blocks[blockKey] = block;
+                }
 
                 return;
             }

Otherwise AssignBlockData() will potentially pull the wrong data for RMBs assigned an index through the AssignNextIndex() fallback, causing the player to enter the wrong interior, or throwing an exception because the door.resourceIndex is outside the bounds of the RMB's SubRecords.

This bug is clearly visible here with Cliffworm's release version of Lively Cities.

For more insight I'll use the mod Village of Fishguard by dotWayton to demonstrate the bug step by step.
Here you can see Fishguard's RMBs are both set to index 308 within their JSON files.
2025-06-11_03-13
2025-06-11_03-13_1

And here we can see Fishguard's RMBs have been assigned new indexes by AssignNextIndex():
2025-06-11_03-19

Attempting to enter a house in FISHER03.RMB results in this:
2025-06-11_03-24

We can see AssignBlockData() grabbed two RMBs for our location, FISHER02.RMB and FISHER03.RMB (lines 4531 and 4532). And we can immediately see three things are wrong upon it trying to load the record data (on line 4533): it is pulling from FISHER02.RMB instead of FISHER03.RMB, the Block Index and the Door BlockIndex are both wrong (308 instead of 1296), and the Door RecordIndex far exceeds the Block SubRecord length (which causes the exception on line 4534). This is because the internal index value for the block wasn't updated, so the door is still using the old block index of 308, and since FISHER02.RMB is the first block that matches the door's block index of 308 it gets selected.

After the change in the diff above was applied:
2025-06-11_03-51

It's now pulling from the correct block FISHER03.RMB instead of FISHER02.RMB, with the correct Block Index and Door BlockIndex of 1297 instead of 308, and with the Door RecordIndex correctly within the bounds of the Block SubRecords length.


return;
}

// Add to cache
newBlockNames[jsonBlockIndex] = blockName;
newBlockIndices[blockName] = jsonBlockIndex;
Copy link
Contributor

@TwelveEyes TwelveEyes Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to add a fallback to AssignNextIndex here too in case there are already entries for the keys jsonBlockIndex and blockName. This way two different modded RMBs with the same custom index (by accident) can still get along.

Debug.LogFormat("Found a new DFBlock: {0}, (assigned index: {1})", blockName, jsonBlockIndex);

// Cache the full DFBlock
if (!blocks.ContainsKey(blockKey))
blocks[blockKey] = dfBlock.Value;
}

private static void AssignNextIndex(string blockName)
{
newBlockNames[nextBlockIndex] = blockName;
Expand Down