Fallout 4

Platform: Fallout 4 (PC, XBOX)
ModFile:  SKKWorkshopUtilities.ESP
ModName:  SKK Workshop Ownership Utilities
Author:   [email protected]
PC:       https://www.nexusmods.com/fallout4/mods/31625
Xbox:     https://bethesda.net/en/mods/fallout4/mod-detail/4065233
Date:     June 2020
Version:  020 onwards

Whilst working on Workshop Ownership Utilities "manage workshop population to the number of beds" and "find unassigned settlers and resources" functions I ran into the common issue of workshop resource counters not being correct for WorkshopParentScript registered resource assigned/producing settlements.

You have probably seen this bad info in Pipboy / Data / Workshops list where population is wrong, happiness is wrong, food is wrong, water is wrong, beds are wrong ... everything can be wrong. Even when your actually at the workshop itself while settlers do not consistently assign to resources. This is why:

(1) ResetWorkshop calculations

Workshop resources are calculated/updated by WorkshopParentScript.ResetWorkshop function which is triggered by player movement/teleport event OnLocationChange into a workshop location tagged LocTypeWorkshopSettlement. This is why it is super important for folks publishing registered settlement workshops to tag the locations correctly (common n00b error).

ResetWorkshop can take up to 120 seconds to process a large workshop at its build limit with 20 settlers and 120 resource producing objects on a 60 fps system. Even longer if your running low FPS as script execution can be tied to frame rate, and/or have so many scripts running the papyrus system is queued/lagging.

Imagine what happens to the calc time when extending build triangle (object) limits, vomiting up yet more scripted objects, slowing frame rate and all those scripts competing for the same limited slices of script time.

(2) OnLocationChange trigger

Most of the commonwealth is configured as a single (unmarked) "Commonwealth:Wilderness" location. Properly constructed workshops introduce their own unique locations which give the workshop its name, spanning one or more game cells (4096x4096 game unit boxes). It is these named location boundaries which trigger OnLocationChange, not each game cell.

Game Unit Distances: A standard player is 128 game units high. 10,000 game units is a direct line from V111 elevator to Sanctuary workshop, or Sanctuary workshop direct line to the Minutemen statue. That's 2.5 Cells.

This means that workshop location boundaries can be variable distances from the actual workshop depending on workshop location tagged cells and the direction/speed of travel. Examples of directional OnLocationChange distances (rounded): Abernathy 2500/2900, RedRocket 3000/3900, Starlight 4500/5100, Spectacle 7500, Sanctuary 9800/5200/3800. Note the Workshop Location tagged cells are not the same as build area (but they could be).

Imagine the resource update confusions extending the build boundary past the workshop location tagged cells.

(3) Finding WorkshopItemKeyword Resource Objects

Keywords and LinkedReferences are one of the fundamental bonding agents that holds the game together to make game objects findable. Just like you Google for Keywords and then click on the LinkedReference pages.

Any resources built by a workshop are linked back to the workshop with a linked reference keyword WorkshopItemKeyword. So the workshop can be queried: "give me all the food producing objects connected to you with WorkshopItemKeyword" and the built objects use this link to find their workshop boss.

Most base game placed, spawned or built objects are "non persistent" meaning they do not have permanent in-game object reference IDs and are unloaded from memory when 3d unloads so they can't be found unless they are in the 3d loaded active area around the player.

(4) uGridsToLoad 3d loading

The area for objects 3d to be loaded with guaranteed findable Object IDs is defined (mostly) by the uGridsToLoad INI setting, default 5 for a math radius of 10,240 units around the player. In actual fact logging OnLoad events and Is3DLoaded tests on Workshops shows them loading and unloading between 9,000 and 12,000 game units from the player, probably a factor of where in its 4096 cell the workshop is placed. Conversely OnUnload happens around 12,000 to 18,000 game units from the player which is probably a cell cache/buffer factored by uExteriorCellBuffer (36) and the fLODFadeOutMultObjects (Ultra:30) quality settings.

Imagine the impact of extending a workshop build area outside the uGridsTLoad active area from the workshop so resources can never be found to link and update.

Object loading is also affected by player movement speed and the backlog on the presentation engine which is visible through FPS drops and movement/rendering spikes. This is for movable objects, static and precombined objects are probably handled differently.

(5) ResetWorkshop & Unloaded WorkshopItemKeyword Objects

So the player can trigger the ResetWorkshop update from a location boundary which can be up to 9K units from a Workshop, and that build area can span another 9K units in the opposite direction. Therefore workshop built objects can be 18K units from the player when ResetWorkshop triggers. 

Any objects that are 18K units from the player will not be loaded, and as they are mostly non persistent the workshop will not find them through its WorkshopItemKeyword linked references. So they wont be counted in the resource arrays.

This can be demonstrated with a picture of the player heading to Sanctuary with a Mutfruit plant in each corner of the build area


(6)Player Movement & speed

What about if the player crosses an invisible workshop location boundary, triggers ResetWorkshop and then throws a U turn and runs away ? Less resources loaded, less found, more errors. Because the script doesn't test if the workshop 3d is still loaded during the (remember up to 120 seconds) calculation. 

There are some poorly constructed base game locations that can trigger a workshop OnLocationChange when the player is actually running AWAY from the workshop, because the workshop location tagged cells are noncontiguous (just over Sanctuary bridge by the statue is one). In 120 seconds a high END  player can sprint another 60,000 units away from the workshop. Or fast travel, or teleport though a load door in Concord ... zero resources found. Meanwhile ol' Red Rocket is also disappearing in the player's rear view mirror and hasn't even started its ResetWorkshop which is now sitting in a queue to run on no loaded objects at all.

I shit you not, here is a picture of Sanctuary > Red Rocket outbound ResetWorkshop trigger events

(7) Some workarounds

(a) Stand in front of a workSHOP and use Workshop Ownership Utilities [ Force Reset Workshop ] or [ Detect issues with local workshop resource assignment and production ] functions. They wait for any ResetWorkshop function calls that may be queued on other workshops to finish and then force triggers ResetWorkshop on the local workshop.

(b) Stand in front of a workSHOP and use the PC console (replace Sanctuary 000250fe with the actual workshop reference);

[ cqf WorkshopParent "WorkshopParentScript.ResetWorkshop" 000250fe ] 
... wait a minute ...
[ 000250fe.cf "WorkshopScript.DailyUpdate" 1 ] 

(c) When heading towards a workshop location/buildarea, walk and don't turn around and run the other way.

(d) Configure a higher uGridsToLoad. At 11 (22,000 active radius) the issue mostly doesn't happen, but comes at the cost of more stuff loaded (area is squared/pied) and active everywhere so downtown Boston FPS becomes total dog sh1t. I mostly run 7 uGridsToLoad for performance rather than heaping on high poly/texture RPG immersion fluff for looks and tonnes of actors. You do you.

(e) Whilst I don't use other peoples mods, you may find that Kinggath's WorkshopFramework addresses the ResetWorkshop trigger issue.

(f) Hack the base game WorkshopParentScript to not use the OnLocationChange event to trigger ResetWorkshop. Whilst I abohr hacking base game scripts for the compatibility and conflict cascade that causes, I have tested replacing OnLocationChnage with OnLoad and OnDistanceLessThan events with Is3DLoaded tests and they work 100% consistently. If only someone ... 

(*) Disclaimer for internet neck beards: it is not just "3d render loaded" as textureless, marker only, alpha 0 and disabled objects can fire Onload script events, but the average reader doesnt need a fully qualified list to understand the concept "for example, but not limited to."

Article information

Added on

Edited on

Written by