Mod & Runtime Builders – BB – Update 15
Posted: May 26, 2025
Last Updated: May 26, 2025
Mod & Runtime Builders – BB – Update 15
Timeframe: March 7, 2025 – March 16, 2025
Hello. Been a while. I know I said the last update and this update would come out in quick succession, but I got sidetracked by a completely different project that’s basically taken the past 2 months. This update brings InGameBuilders that can be added through mods and a ton of other fixes.
To be completely honest, I had to look back through all my notes to remember what I was even doing before. They helped a lot though, and I would basically be restating what was in them, so this week’s update will be a little different. I’m going to be really lazy and basically copy-past my notes from this time period in chronological order. A lot of it is shorthand or lingo or other arbitrary stuff that won’t really make sense if you’re not me, but I thought it would be an interesting look, nonetheless. I’ll even include some of my current thoughts/input on the notes here (in red). You probably won’t, but please enjoy my unfiltered (except for some formatting) notes:
March 7, 2025:
“
Tested relative and original sprite settings and animations for size and position.
-Size Setting:
-20%v,100%r
-50%r,50%v
-10%o,10%o
-Position Setting:
-50%h, 50%r
-50%r, 50%v
Implemented IAssetFileMap in AudioAssetManager.
“
This was the last of my work from the previous update adding animations to OVNS.
“
Then I did a dumb thing and started to make a way to create InGameBuilders at runtime using config files.
This will be useful for modders, but also useful for me so I have to do even less scene setup.
I created the concept of a RuntimeBuilder, an abstract class which inherits from InGameBuilder and is mostly just responsible for instantiating a UI and figuring out where the config file is and using the config to set up the BuilderInspector.
So, in summary, I developed:
-RuntimeBuilder: above.
-RuntimeBuilderDataBehaviour: A connector that holds data to assign in the instantiated RuntimeBuilder upon building.
-RuntimeBuilderHelper: (NEEDS TESTING) Contains helper methods for setting up a RuntimeBuilder’s subject.
-RuntimeBuilderSubjectAuthoring: Authoring for a data object to help RuntimeBuilderHelper.
-RuntimeBuilderAttribute: Attribute to be placed on your RuntimeBuilder classes to automatically mark them for building.
-InGameBuilderManager_Runtime: an InGameBuilderManager subclass that first instantiates the RuntimeBuilders of the necessary types before moving on the normal functionality.
-ModBuilder: For later…
I still need to do a bunch of stuff to connect things up that I can’t really set in the inspector anymore:
Connect (widgetManager) to a child WidgetManager (you may AddComponent<WidgetManager> if none exist)
Connect (binder) to a Binder (you may add one if none exist). To add one, make sure to set the settings:
-findChildBindingGroups = true
-includeInactiveChildren = true
-noSetup = false
Connect (display) to a Display
Set your registry file name (registryFile)
Set your registry ID (registryID)
Connect (panelControl) to a PanelSwitchButtons (though this should be found during builder initialization anyways).
Set registry compression level (compressionLevel)
“
Here’s my initial brainstorm/outline for RuntimeBuilders which are just InGameBuilders that are put together at runtime instead of made beforehand.
March 8, 2025:
“
I’ve done a bunch of stuff already that I haven’t written down yet because I’m trying to fix something stupid.
Modified some code to fix some of the connectivity issues in the runtime builder.
One thing that was happening was that I was getting null reference exceptions when adding SingleWidgetInfo’s to the detailsWidgetInfo with AddWidgetInfo BuilderInspectorField’s DoSliderDetailsWidget call.
Basically, creator.detailsWidgetInfo was null when trying this, which in retrospect makes sense because it should have been set to a null value (SettingAttribute) in BuilderInspectorDataField (constructor), but it was only happening in the runtime builder version of events.
I thought the problem was that, in the editor, we end up serializing the creator’s detailsWidgetInfo (which is null), so maybe it doesn’t write it out or defaults it to some non-null default value, so that when we read it in in the scene, detailsWidgetInfo is not null, but some default version of DetailsWidgetInfo. This would not happen if we were building the creators at runtime and immediately using them.
Changing BuilderInspectorDataField (constructor) to return a default DetailsWidgetInfo, however, created an infinite loop that would use up all the editor resources, and I don’t know why. I then added a bunch of default values to the BuilderInspectorElementCreator fields (like Lists/SettingAttributes and stuff), and this seemed to work.
However, I’m getting a really strange argument out of bounds error when trying to DisableBuilder AFTER the prefabification process. It was already disabled… at the BEGINNING of the prefabify process. Idk what the fucking shit it’s doing to fuck it up that bad.
Undoing all the shit I changed, and I still get this error, but just resuming gameplay afterwards seems to still work.
I recall turning off Error Pause at some point yesterday. Maybe it was showing up and I didn’t notice.
“
My first attempts at the RuntimeBuilder were… not good. There were a lot of blemishes that I hadn’t had to deal with in InGameBuilder before because they were kinda covered up by having them statically built in the Scene. Trying to create builders on-demand turned out to be significantly more annoying.
“
Before getting back to the prior problem, I also ran into the thing where clicking a slider widget button in a dynamic buffer list pulls up about 100 sliders… I don’t remember if I ever fixed/tested it, but it obviously doesn’t work.
The problem, I think, is that the BuilderInspectorDynamicBufferField instantiates a BuilderInspectorDynamicBufferDisplay and passes a copy of the BuilderInspectorElementCreator prefabs to it. The Display then creates 100 BuilderInspectors, each of which are passed these prefabs as their elements field. When each BuilderInspector calls BuildInspector, it adds another SingleWidgetInfo to the creator’s detailsWidgetInfo, which refers back to the same ones.
I’m trying to fix this by providing some explicit Creator-copying logic to hopefully separate them into new Creators.
After some tweaking, this did fix the 100-slider problem, but the slider widgets for fields in dynamic buffer elements still were not working.
They all had the wrong indices (-1), well actually they had 2 indices (-1, correct_index).
This is because we call BuilderInspectorDynamicBufferDisplay.Initialize, we call SetIndex on the individual inspectors AFTER we call BuildInspector, and we need the creator indices to be correct before BuildInspector to accurately set the widget indices during BuildInspector > CreateWidgets.
“
This is me being all over the place. I’ve had this problem where clicking a widget button in a dynamic buffer display created from the BuilderInspector process had 100 widgets show up at once. The gist of this problem was that I was passing a reference to a List when I should have made a new List.
“
Looking back at argument out of range exception, the problem seems to be that there are bindings from the BuilderInspectorDynamicBufferDisplay leftover after the prefabification process, and this is causing a problem when calling remake widgets.
I’m not super sure, but a patchwork solution was to edit EnemyBuilder.ClearPathNodes to call UpdateBufferDisplays after pathNodeBuffer.Clear so that the BuilderInspectorDynamicBufferDisplay responsible for path movement can update to reflect the empty buffer.
While I was at it, I modified Creator’s AddTranslationWidget and AddRotationWidget calls to use creator.indices, which also required me to amemd the relevant widget InGameBuilder methods.
Also changed the tags that the widgets in spawn with to include indices, that way the same widgets are not overwritten when referencing a different index but the same field.
“
Some Widget fixes.
March 9, 2025:
“
Now I went back to the BuilderScene to try and get it working at runtime (for real, though).
Running into some problems when setting up the EnemyBuilder subject.
When calling my RuntimeBuilderHelpers to apply various components, I am not getting past the TryGet for the RuntimeBuilderTag singleton.
I think it’s because my MonoBehaviour (InGameBuilderManager_Runtime), runs this stuff on Awake, so the subscenes haven’t really initialized yet? Maybe like that…
Did some stuff ot InGameBuilderManager_Runtime to just add the runtime builders to ‘builders’ on awake.
I have to wait until the wait for builders coroutine to do the other setup because that happens after the RuntimeBuilderTag entity has been setup.
Now, before the normal WaitForBuilderSetupCoroutine coroutine, I iterate all the created runtimebuilders and setup the builders UI and subject.
This caused a problem because the InGameBuilderManager was incorrectly saying that all the builders were done waiting for subject and mapper before all of them actually were (the runtime builders take an extra frame to find the subject because they make it directly after the call to OpenBuilderFromProject because WaitForBuilderSetupCoroutine is actually invoked after OpenBuilderFromProject, OpenBuilderFromProject being the method that starts WaitForSubject and WaitForMapper.
In other words: For normal builders, WaitForSubject and WaitForMapper basically finish on the same frame they were invoked because all that stuff already exists, but for the runtime builders, the mapper exists, but the subject is created at the beginning of WaitForBuilderSetupCoroutine in the InGameBuilderManager_Runtime, which is started after all the calls to OpenBuilderFromProject.
The ACTUAL problem was in my waiting logic in WaitForBuilderSetupCoroutine, and I was ending the wait instantly every time. This didn’t cause problems before because everything was always available that first frame.
“
Here were my first real bugfixes for RuntimeBuilder. There was an issue in my WaitForX… logic that I had never caught before because it wasn’t causing issues. It caused issues for the RuntimeBuilder, though.
“
Right now, it seems to be working. Except for the ‘Add Button’ button in the buffer inspectors.
I need to figure out if it’s just the button that’s not hooking up, or a binding problem.
Also the registry buttons don’t work anymore. Probably need to connect them back up to the new InGameBuilderManager_Runtime.
Update: Nope. The problem is again in OpenBuilderFromProject. Here, we call TryFirstTimeSetup, which initializes ‘inspectorBufferDisplays’ with all the current/existing children. Upon UpdateBufferDisplays, our inspectorBufferDisplays is empty, so it does not update.
Added a coroutine setup for SetupBuilders and an override for it which does this setup first and waits for it to be done before calling OpenBuilderFromProject.
This gets the buffer display to update, but now there is a problem with the Enemy not moving when changing the nodes.
“
Things seemed to work until I checked the buffer displays created by the RuntimeBuilders. These were mostly unresponsive, and the problem was that certain initialization logic was happening before everything to be initialized was in place. I basically had to wait for RuntimeBuilders to do some setup before the typical InGameBuilder setup.
“
It seems like the enemy does not move because the PathMovement component on it is not ‘Enabled’.
I’m going to fix this by adding more prototypes to RuntimeBuilderHelper to aid in getting these things to be more like they are post-baking.
Added more prototypes. These all relate to a corresponding Authoring:
EnemyPrototype
HealthPrototype
PathMoverPrototype
IncrementalMoverPrototype
Made the incremental movement components serializable.
Added a IncrementalMoverAuthoring to place speed, accelerate, and jerk components to an entity.
Next I ran into an error copying component ‘DoTrigger’.
I’m thinking this is because it’s a buffer element, so probably should be copied differently.
This was the case. I decided to pick my battles here. Just knowing that the buffer is on the entity is good enough, so I just add the buffer and don’t copy any elements over.
Next started turning LevelBuilder into a RuntimeBuilder.
Problem where there are 2 LevelComponents, but this is because the prefabs in the RuntimeBuilderSubject are not disabled.
Disabled the entities in the RuntimeBuilderSubject.
InGameBuilderManager_Runtime now also checks if the RuntimeBuilder type is already present in the scene and does not add it if so.
RuntimeBuilderSubjectAuthoring now disables all prototype entities.
“
Finally, I fixed some stuff with my RuntimeBuilderHelper methods. These methods only really exist to be a terrible version of Unity’s baking process, where I call these helper methods to add presets of components to created Entities. For normal InGameBuilders, I manually create a SubScene, create a “Subject” entity, and place MonoBehaviours on the subject to act as the target for InGameBuilder modifications. However, these subjects don’t exist beforehand for RuntimeBuilder. Since my previous subjects were heavily reliant on the Baking process (and I can’t Bake at runtime), I had to make RuntimeBuilderHelper, which I populate with a bunch of pre-Baked entities with specific components. I call RuntimeBuilderHelper to copy components from the pre-baked entities to my new subjects. Each pre-baked entity typically represents some base functionality, like having an entity with incremental movement components, another entity with physics/hitbox components, another entity with sprite rendering components, etc.
March 10, 2025:
“
Starting on ModBuilder.
ModLoader- Big modifications which fit IModLoaderModules into the mod loading pipeline. After loading the sorted mod assemblies, it then checks each mod’s assemblies for any types that implement IModLoaderModule. Then, it creates an instance of each of those types (in same order as sorted mods) and caches them. Then, for each (ordered) IModLoaderModule, it does the following:
-Calls module.LoadInternal(…) to allow the mod loader module to act only on the assemblies on the mod where the loader was defined.
-Calls module.Load(…) on ALL the mods in the order that they were sorted. This gives it a chance to operate on all modded assemblies.
-Note: We add the game’s initial assemblies to the front of the lists so that the game’s assemblies are included for any IModLoaderModule loading stuff.
ModBuilder- A new type of InGameBuilder that extends RuntimeBuilder. ModBuilder is responsible for setting up it’s RuntimeBuilder methods by querying statically-loaded ModBuilderLoader for the corresponding ModBuilderManifest for that type and transferring that information to the RuntimeBuilder methods.
IModLoaderModule- Defines LoadInternal(IModLoaderData data), which acts on only the mod that defines the IModLoaderModule. Defines Load(IModLoaderData data), which acts on all loaded mod assemblies (not on base assmblies).
IModLoaderData- Defines an argument to be passed into IModLoaderModule methods. Includes information like ModManifest, project manifest, mod folder and subfolder, and assemblies included by the mod.
ModBuilderLoader- A new IModLoaderModule that is responsible for setting up ModBuilders during mod loading. It is responsible for taking and syncing data defined in the loader config and mapping that information to each type in each mod that implements ModBuilder.
ModBuilderManifest-A poorly named data class that just holds stuff similar to RuntimeBuilderDataBehaviour
ModBuilderAttribute-A backup method for defining the availability/configuration of a modded-in builder.
After brief test, it seems to work, except for the builder display name not carrying through…
Should probably experiment by making a ForceField mod and corresponding ForceFieldBuilder:ModBuilder.
“
This one’s pretty self-contained. I subclassed RuntimeBuilder into ModBuilder, and created a new process for loading mods. IModLoaderModule is basically a custom process for loading mods. In your mods, you can implement IModLoaderModule, and it will run on all the assemblies in the game (including other mods). I use this to create a special ModBuilderLoader which helps me create custom InGameBuilders at runtime, even those that are not built-in to the game.
March 11, 2025:
“
Fixed problem with display name not carrying through. Just added to RuntimeBuilderDataBehaviour SyncMetadata.
For some reason, the BulletLimitControl was acting up. Finally adjusted it so the EntityQuery is built on-demand if it hasn’t been setup yet. Avoids the nullref I get sometimes.
Also, the WidgetSnapControl was acting up, and always defaulted to show origin. Added a delayed coroutine to Start to just reset it after 1 frame. IDK why it was needed or why it worked, but that seems to be a common solution to things.
Fixed a bug where the inspector buffer lists were not being placed on the correct inspector page. Fixed this by editing BuilderInspectorHelpers.GetBufferFromData to add the display name, current page, and field name to the new buffer Creator.
Finished up some of the other builder inspector configs and got them working in the RiskyTestScene, finally.
Starting process of modding in ForceField.
“
Easygoing day. Fixed up a couple of miscellaneous things before diving into making an example InGameBuilder through a mod: ForceFieldBuilder.
March 14, 2025:
“
More work on modded ForceField today.
The main problem continues to be that the ITriggerEventJob does not play nicely in the host.
Let’s look at this output:
“””
0x00007FF87F65DB50 (UnityPlayer) (function-name not available)
ERROR: SymGetSymFromAddr64, GetLastError: ‘Attempt to access invalid address.’ (Address: 00007FF87ED32C15)
0x00007FF87ED32C15 (UnityPlayer) (function-name not available)
0x0000019CEF07BEFC (Mono JIT Code) (wrapper managed-to-native) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule_Injected (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&,Unity.Jobs.JobHandle&)
0x0000019CEF07BD8B (Mono JIT Code) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&)
“””
I notice that we go from Mono JIT Code to UnityPlayer. We also know that Schedule_Injected references some native/pre-built code that I *really* don’t have access to. I’m thinking that maybe the modded version goes through its calls and eventually makes a reference to this native code in its OWN UnityPlayer.dll file (the one generated from building the SDK), but that does not match up with the reference/address of the Schedule_Injected used in the host project’s .dll file. Maybe, we need a hook in the host project, that the mod can call which eventually resolves in the TRUE Schedule_Injected native call being made.
1. In host/Core package, create a new method/hook/utility/static helper whose ONLY JOB is to do exactly the same thing that we normally do to get the call stack down to Schedule_Injected. Call stack starting from Schedule here:
“””
0x00007FF87F65DB50 (UnityPlayer) (function-name not available)
ERROR: SymGetSymFromAddr64, GetLastError: ‘Attempt to access invalid address.’ (Address: 00007FF87ED32C15)
0x00007FF87ED32C15 (UnityPlayer) (function-name not available)
0x0000019CEF07BEFC (Mono JIT Code) (wrapper managed-to-native) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule_Injected (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&,Unity.Jobs.JobHandle&)
0x0000019CEF07BD8B (Mono JIT Code) Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule (Unity.Jobs.LowLevel.Unsafe.JobsUtility/JobScheduleParameters&)
0x0000019D38DA1E1B (Mono JIT Code) [.\Library\PackageCache\com.unity.physics@1.2.3\Unity.Physics\Dynamics\Simulation\ITriggerEventsJob.cs:108] Unity.Physics.ITriggerEventJobExtensions:ScheduleUnityPhysicsTriggerEventsJob<BH.Modding.ForceField.ForceFieldJob> (BH.Modding.ForceField.ForceFieldJob,Unity.Physics.Simulation,Unity.Jobs.JobHandle)
0x0000019D38DA06BB (Mono JIT Code) [.\Library\PackageCache\com.unity.physics@1.2.3\Unity.Physics\Dynamics\Simulation\ITriggerEventsJob.cs:56] Unity.Physics.ITriggerEventJobExtensions:Schedule<BH.Modding.ForceField.ForceFieldJob> (BH.Modding.ForceField.ForceFieldJob,Unity.Physics.SimulationSingleton,Unity.Jobs.JobHandle)
“””
This means we have the following chain:
1. ScheduleHelper.Schedule<T>(
Trying first in Core package.
So that didn’t really work.
The problems still is that it redirects to some native code that I don’t understand.
I took a different approach, which was to ask the question: why is ITriggerEventJob so weird compared to other jobs?
Basically the only constraint with it is passing in this SimulationSingleton. What’s so hard about the rest?
I dug into SimulationSingleton > Simulation > TriggerEvents > TriggerEvents.Enumerator and found that I could basically pass a TriggerEvents.Enumerator into a normal IJob and just perform the iteration directly there.
This… might be working?
I’m not sure yet. Need to modify the host a bit more to see.
Next up, probably go for adding in PlayerBuilder to the Builder scene so I can test it with bullets. Also add back debug logging that just prints all the stuff with force fields.
“
The most significant thing about today was learning how to handle the Unity Physics stuff in modded content. ITriggerEventJob seemed not to be very usable in mods (which ForceField relies upon). I think that eventually, scheduling an ITriggerEventJob eventually calls some special secret UnityPlayer.dll code. Unfortunately, this reference would be to the UnityPlayer.dll of the mod project, not the main game, causing it to break. After a lot of bad attempts, I finally bypassed the whole thing by changing the ITriggerEventJob responsible for ForceField functionality into a normal IJob where I just pass in the physics simulation trigger events and manually iterate them. It seems like this will have to be the approach for other physics-based mods for the foreseeable future.
March 15, 2025:
“
Maybe it was NaNing itself? I was placing an incremental movement thing on the field, so maybe it was interacting with itself? But how would it trigger itself? I don’t think that’s it… Checking the location anyways.
Also trying to remove incremental movement components from the field in TrySetupSubject.
So, removing the incremental mover from the force field did (kinda) fix the problem.
New problem, though…
During testing, the actual effect on the bullets seems to be really erratic. Some bullets are affected for a moment and other bullets are not, and the ones that are affected seem the change around.
During testing in the SDK, I do not have these kinds of problems, so I’m thinking maybe it has something to do with the trigger events being “consumed” by the BulletCollision check or something?
NO DONT DO THIS-> I can probably test this by making an identical force field component/system that does the same thing and add another force field to the scene with this alternative field. If events are consumed, then only 1 should activate … no.
I could make another system that does something with these collisions.
After some testing, i’m not sure it’s my logic yet. I think it might be a test object in the scene that is causing the weird stuff.
I had a test field that I don’t think does anything anymore, but when bullets pass through it, it messes with the bullets currently in the functional force field, like it makes the field not affect them.
I’m going to remove the test object from the scene and see what happens.
Okay, I removed the thing and it works.
And yes, I found out what it is and I’m stupid.
I’m still mostly using code copied from my old ITriggerEventJob implementation which takes in a single TriggerEvent into Execute.
With this old method, I call ‘return’ when I know we shouldn’t process it and I kept that in the new code.
However, in the new code, I have a single job that iterates all the TriggerEvents, so the first one that should not be processed calls return and skips the rest…
I just need to call ‘continue’ instead.
Works now. Even fixed the inconsistencies that were happening because of player collision triggers messing things up before.
“
I kind of jump right in here, but the problem I was having was that while testing the ForceField mod, my bullets seemed to only be allowed to interact with one thing at a time. It took a while to notice this because there was another ForceField in the scene that I had forgotten about causing seemingly erratic bullet behavior. I finally figured out that the logic I used while iterating TriggerEvents in the ForceField processing job was calling “return” instead of “continue”. This was bad because “return” was the correct logic when it was implemented with an ITriggerEventJob that represents a single trigger, but now that it’s just an IJob that iterates all triggers, it quits too early and “consumes” the trigger the first time it touches the force field.