ManagedBlam.dll is a wrapper library that allows for direct in-code access to engine functions and tag data, examples being - reading tag data, manipulating tag fields, creating tag entries or tags from scratch and much more. It is available for all engines post-H2 (it was backported to H3 and ODST with the July 2023 update).
Of course this is an extremely powerful tool with practically endless possibilities and greatly increases the potential and speed of external helper programs. On this page, we will provide some basic information on getting ManagedBlam up and running in your dev environment, and some simple(ish!) code examples for doing things such as reading and writing tag field data.
Most information on this page applies across all engine versions - where there are known differences, such as the initialization code, it will be pointed out.
For more details on writing foundation plugins see the API page for Reach.
Development environment
ManagedBlam is primarily used with the C# programming language. ManagedBlam also specifically requires .NET 4.8
- if you do not have the developer pack, you can get it here. Older versions of .NET Framework will not work, it is however possible to use the assembly with .NET 5+ but this requires additional setup detailed below.
The IDE you use is of course your personal choice, but it is highly recommended to use Visual Studio. At the most basic, you can start with a blank console application, as ManagedBlam has no special requirements beyond .NET 4.8
. If you are unfamiliar with using external libraries (.dll) files, you can add the H3 ManagedBlam.dll to your VS project like so:
- Create or load your solution in Visual Studio
- In the
Solution Explorer
panel on the right, right-click your solution (should have a symbol like a small green C# inside a box). - Hover over
Add
, then clickProject Reference
in the sub-menu to open theReference Manager
. - Click the
Browse
button in the bottom right. - Navigate to
H3EK\bin
, and open theManagedBlam.dll
file. - Back in the
Reference Manager
window, make sure the checkbox next toManagedBlam.dll
is ticked, the clickOK
. - ManagedBlam is now usable within your project! Add
using Bungie;
to the top of your C# program to import the library and get started. - (optional) If you intend to use automatic builds (e.g. Github Actions) you may want to generate a reference assembly for
managedblam
. This allows you to compile code that uses the assembly without needing a copy of the full assembly. JetBrains has an open source tool for doing this on GitHub.
Settings in the .csproj file
There is a property we need to set in the .csproj file to avoid issues at runtime:
- Open your .csproj file. You can do this either by opening the file in a text editor, or by double clicking the solution name in Visual Studio's
Solution Explorer
to open it there. - Inside the first
<PropertyGroup>
section, add this line:<HighEntropyVA>false</HighEntropyVA>
This section should now read similarly to this:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net4.8</TargetFramework>
<HighEntropyVA>false</HighEntropyVA>
</PropertyGroup>
If you accidentally created the project using the wrong .NET version, or otherwise need to make sure that it is running .NET 4.8
, this is also where you set that. After updating and saving the .csproj file, you will need to unload and reload the project in Visual Studio, or close and reopen Visual Studio for the changes to take effect.
High Entropy ASLR (HighEntropyVA
) needs to be disabled as H3+ uses a pointer packing scheme to encode some 64-bit pointers using just 32-bits, 4-byte alignment is assumed giving an effective address space of just 34-bits or 16 gigabytes. The code subtracts a base address (which appears to be the base address of the main executable) from pointers before packing them.
With High Entropy ASLR (Address Space Layout Randomisation) disabled memory tends to be allocated within that first 16 gigabytes that can be encoded correctly, however with it enabled Windows will attempt to make memory addresses less predictable and will almost certainly allocate memory well outside the 16-gigabyte range as each procress has ~8-128 TB of address space - much more than the 16 gigabytes the pointer packing scheme can address.
If High Entropy ASLR is enabled, you will mostly likely get a fatal assertion in cseries\port_64.h
when you first open a tag, as blam
will attempt to pack a tag-related pointer and fail catastrophically. If you get a crash in anything relating to port_64
make sure HighEntropyVA
is disabled before debugging it further.
Using ManagedBlam with .NET 5+
.NET 5 is not directly based on .NET Framework 4.8 and uses a new runtime and packages applications in a different way - programs are now by default compiled to a DLL file with a small loader executable (apphost
) being used to launch the DLL, while .NET 4.8 compiles your program to an executable file that contains both a (much smaller) loader and all of your code.
It is possible to package the program into a single executable afterwards but this is a post-build step (publishing)
This new setup does cause issues when used with managedblam
as settings such as HighEntropyVA
or the base address are applied to the DLL that actually contains your code and not the loader. However Windows uses the configuration of the main executable file to configure ASLR including the entropy setting.
As a security measure HighEntropyVA
is enabled by default for the loader executable which means in effect it is also enabled for all programs in dotnet 5+. We can use a custom build step manually disable it by editing the loader executable configuration. Add the following XML code to your .csproj
file:
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="call "$(DevEnvDir)..\Tools\VsDevCmd.bat" && editbin /HIGHENTROPYVA:NO /DYNAMICBASE:NO /REBASE:base=0x10000 "$(TargetDir)$(TargetName).exe"" />
</Target>
<Target Name="PostPublish" AfterTargets="Publish">
<Exec Command="call "$(DevEnvDir)..\Tools\VsDevCmd.bat" && editbin /HIGHENTROPYVA:NO /DYNAMICBASE:NO /REBASE:base=0x10000 "$(TargetDir)publish\$(TargetName).exe"" />
</Target>
You will now be able to use managedblam without the crashes relating to ASLR. If you use the dotnet
command line tool to build your project you will now need to run it from a Visual Studio Developer Command Line
or Visual Studio Developer Powershell
instance. Otherwise, the DevEnvDir
variable will not be set correctly. Alternative you can figure out a different way of adding editbin
to path.
If you use GitHub actions you can switch to the developer command prompt using ilammy/msvc-dev-cmd@v1
, see the Osoyoos build script for an example.
Breakdown of editbin
parameters
/HIGHENTROPYVA:NO
- Disables High Entropy ASLR, this makes the address space more predictable and keeps the memory addresses closer together./DYNAMICBASE:NO
- Requests ASLR is completely disabled and that the loader executable should not be randomly rebased./REBASE:base=0x10000
- Changes the base address of the executable to be 0x10000 - this cannot be lowered further and the default base address of some builds ofapphost
is too high for managedblam to work correctly.
editbin
is invoked twice, once after building a executable and once after publishing. The second event is required for the edit to be applied to the published binary.
Initialising ManagedBlam for Halo 3
This process differs slightly from the Reach+ ManagedBlam version exemplified on this page https://c20.reclaimers.net/hr/API/. ManagedBlam must be running in order for code to be able to open and edit tag files. The line required to start ManagedBlam is ManagedBlamSystem.InitializeProject()
- this must be run before any tag reading/manipulation code, with a reference to your H3EK root folder as the second argument:
string h3ek = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\H3EK"
ManagedBlamSystem.InitializeProject(InitializationType.TagsOnly, h3ek);
Unlike in the Reach version where ManagedBlam.Stop()
can be used to gracefully stop the ManagedBlam process, this function does not exist in the H3 version, and so you should not worry about tryint to close it properly.
Loading a tag file into memory
This part is fairly straightforward - all manipulation of a tag file in memory is done after opening the tag by setting it to a variable. One common method is to use a using
block that opens the relevant tag file, but getting the filepath in the right format can be a little tricky at first. Bungie.Tags.Tagfile()
doesn't take a simple filepath as its parameter, but rather a Bungie.Tags.TagPath
variable. You need two variables to generate this, the relative tag path and the short file extension. For example, the following will get you a Bungie.Tags.TagPath
variable for the this scenario tag "H3EK\tags\levels\solo\010_jungle\010_jungle.scenario"
:
var tag_path = Bungie.Tags.TagPath.FromPathAndType(@"levels\solo\010_jungle\010_jungle", "scnr*");
If you wish to convert a full filepath (e.g. from the drive letter to the file and extension) to a relative path that this function will accept, you can use the following:
scen_path = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\H3EK\\tags\\levels\\solo\\010_jungle\\010_jungle.scenario";
var tag_path = Bungie.Tags.TagPath.FromPathAndType(Path.ChangeExtension(scen_path.Split(new[] { "\\tags\\" }, StringSplitOptions.None).Last(), null).Replace('\\', Path.DirectorySeparatorChar), "scnr*");
The short file extension is the 3 or 4 letter version of the file extension. If you aren't sure what this might be for a specific tag type, you can use the cache file reader/editor Assembly to check. Examples include:
scnr
- .scenariohsc
- .scenario_hs_ source_ file hlmt
- .modelmode
- .render_model phmo
- .physics_model sbsp
- .scenario_structure_ bsp
Now you can use the tag_path
variable to get the tag into memory:
using (var tagFile = new Bungie.Tags.TagFile(tag_path))
{
// Tag manipulation code goes here
}
Saving changes to a tag
Only one function required here - .Save()
Example:
using (var tagFile = new Bungie.Tags.TagFile(tag_path))
{
// Tag manipulation code goes here
//...
//...
tagFile.Save();
}
The tagFile.Save()
line will save the current tag data in memory back to the original tag. This includes any and all changes you have made since opening the tag - this means that it is not necessary to save after every tag data change, only when you have finished making your changes. If you save repeatedly for no good reason, this will significantly slow down your program as each save takes about a second or so.
Reading tag data, and adding new block entries
Want to read tag data into code? Need to add new entries to blocks with code? Accessing specific blocks/elements/fields within a tag can be done in at least two different ways:
- Using the block, element and field indices (if they are known).
- Using the
.SelectField()
function to access elements based on their names
It doesn't particularly matter which method you use, but option 2 will usually result in code that is easier to read.
See this example, which will add an extra zone to the Zones
block of a scenario tag, and then adds a new area entry to the Areas
block. As this requires knowing the amount of pre-existing zones, the first line is an example of reading data:
using (var tagFile = new Bungie.Tags.TagFile(tag_path))
{
// Get the amount of zones already present in the scenario
int zones_max_index = ((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).Elements.Count() - 1; // 83 is the zones block in a scenario
// Add a new element to the zones block
((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).AddElement();
// Add a new area, using the zones_max_index to know which zone to add it to
((Bungie.Tags.TagFieldBlock)((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).Elements[zones_max_index + 1].Fields[4]).AddElement();
// The zones are the 84th field, we use the max index + 1 to locate the newly added zone, and the `Areas` block is the 5th field inside the zones block (indexes are 0-based)
// Save the changes to the tag file
tagFile.Save();
}
Examples of using method 2 - the .SelectField()
function:
// Access the curve mode value of a bitmap tag
var curve = (TagFieldEnum)tagFile.SelectField("CharEnum:curve mode");
// Access the fade factor value of a bitmap tag
var fade = (TagFieldElementSingle)tagFile.SelectField("RealFraction:fade factor");
// Access the type of an animated parameter element in a given parameter in a shader tag
var type = (TagFieldEnum)tagFile.SelectField($"Struct:render_method[0]/Block:parameters[{index}]/Block:animated parameters[{anim_index}]/LongEnum:type");
// Add an element to the animated parameters block of a given parameter in a shader tag
((TagFieldBlock)tagFile.SelectField($"Struct:render_method[0]/Block:parameters[{index}]/Block:animated parameters")).AddElement();
Field data types
When using .SelectField()
, you may need to know what data type a given field uses, so that you can set it correctly. For example, if you read the tag XML, or otherwise view the tag block data, you may see data types such as long enum
, short integer
, string id
or tag reference
, to name just a few examples. When accessing these fields with .SelectField()
, you will need to provide the data type. For the previous examples, they would be LongEnum
, ShortInteger
, StringID
, and Reference
respectively. Some of these are not obvious conversions, so see the list below of all possible field data types for use in your code:
# 0 String
# 1 LongString
# 2 StringId
# 3 OldStringId
# 4 CharInteger
# 5 ShortInteger
# 6 LongInteger
# 7 Int64Integer
# 8 Angle
# 9 Tag
# 10 CharEnum
# 11 ShortEnum
# 12 LongEnum
# 13 Flags
# 14 WordFlags
# 15 ByteFlags
# 16 Point2d
# 17 Rectangle2d
# 18 RgbPixel32
# 19 ArgbPixel32
# 20 Real
# 21 RealSlider
# 22 RealFraction
# 23 RealPoint2d
# 24 RealPoint3d
# 25 RealVector2d
# 26 RealVector3d
# 27 RealQuaternion
# 28 RealEulerAngles2d
# 29 RealEulerAngles3d
# 30 RealPlane2d
# 31 RealPlane3d
# 32 RealRgbColor
# 33 RealArgbColor
# 34 RealHsvColor
# 35 RealAhsvColor
# 36 ShortIntegerBounds
# 37 AngleBounds
# 38 RealBounds
# 39 RealFractionBounds
# 40 Reference
# 41 Block
# 42 BlockFlags
# 43 WordBlockFlags
# 44 ByteBlockFlags
# 45 CharBlockIndex
# 46 CharBlockIndexCustomSearch
# 47 ShortBlockIndex
# 48 ShortBlockIndexCustomSearch
# 49 LongBlockIndex
# 50 LongBlockIndexCustomSearch
# 51 Data
# 52 VertexBuffer
# 53 Pad
# 54 UselessPad
# 55 Skip
# 56 Explanation
# 57 Custom
# 58 Struct
# 59 Array
# 60 Resource
# 61 Interop
# 62 Terminator
Changing tag data
Let's continue with the previous example, but add some extra lines to set the name and flags of the new zone and area:
using (var tagFile = new Bungie.Tags.TagFile(tag_path))
{
// Get the amount of zones already present in the scenario
int zones_max_index = ((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).Elements.Count() - 1; // 83 is the zones block in a scenario
// Add a new element to the zones block
((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).AddElement();
// Add a new area, using the zones_max_index to know which zone to add it to
((Bungie.Tags.TagFieldBlock)((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).Elements[zones_max_index + 1].Fields[4]).AddElement();
// The zones are the 84th field, we use the max index + 1 to locate the newly added zone, and the `Areas` block is the 5th field inside the zones block (indexes are 0-based)
// Set the zone name, the name is the first field in the zones block
var zone_name = (Bungie.Tags.TagFieldElementString)((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).Elements[zones_max_indes + 1].Fields[0];
zone_name.Data = "our_new_zone_name";
// Set the name of the new area
var area_name = (Bungie.Tags.TagFieldElementString)((Bungie.Tags.TagFieldBlock)((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).Elements[zones_max_index + 1].Fields[4]).Elements[0].Fields[0];
area_name.Data = "area_name_69420"
// Set the flags of the new area
var area_flags = (Bungie.Tags.TagFieldFlags)((Bungie.Tags.TagFieldBlock)((Bungie.Tags.TagFieldBlock)tagFile.Fields[83]).Elements[zones_max_index + 1].Fields[4]).Elements[0].Fields[1];
area_flags.RawValue = uint.Parse("1");
// Save the changes to the tag file
tagFile.Save();
}
Determining field indices
Should you be using the previously demonstrated method 1 to access tag data, you will need to know the indices of the blocks/elements/fields that are relevant to what you want to edit. This can be a little tricky, as tag blocks often have hidden data (that doesn't even show up in Expert Mode). Let's take the Scenery
block within a .scenario
tag as an example: Looking at the block, the first two fields we can see are type
and name
. However if you try to access these with field indices of 0
and 1
respectively, you will get type casting errors. This is because there are hidden data fields padding the block. So how can we figure out the correct field indices without using a lengthy trial and error process? The Visual Studio debugger! By hitting a breakpoint on a line where field data is accessed within the block we want to introspect, we can press View
on the Value
section in the variables window to open the IEnumerable Visualizer
, which shows us a table with all the block data!
Using this example line, where 20
is the block index of the Scenery
block, 0
is to simple access the first scenery element, and 0 is to access the first field (but you could pick any):
var x = ((Bungie.Tags.TagFieldBlock)tagFile.Fields[20]).Elements[0].Fields[0].FieldName;
Setting a breakpoint on this line, and then stepping over it, allows us to view the returns in the variables window. Look for the last return that returns a Bungie.Tags.TagField[]
type (Bungie.Tags.TagElement.Fields.get
). Click View
next to the Value
section of this line to open the IEnumerable Visualizer
window. Looking at this window, we can see that there are hidden fields with a custom type - we do not really need to worry about what they are, but rather that they exist and therefore offset the expected index of other fields. From here, it is a simple job of counting the fields (remembering to start at zero) to determine the correct index for a given field. For the example we started with, we can count up and determine that the type
field is actually index 1
, not 0
, and that the name
field is actually index 3
, not 1
.
Miscellaneous examples
Setting a function value in a Reach shader tag
var tag = new Bungie.Tags.TagFile();
var tag_path = Bungie.Tags.TagPath.FromPathAndExtension(@"levels\solo\m10\shaders\040_garage_wall", "shader");
try
{
tag.Load(tag_path);
TagFieldCustomFunctionEditor func = (TagFieldCustomFunctionEditor)tag.SelectField("Struct:render_method[0]/Block:parameters[0]/Block:animated parameters[2]/Custom:animation function");
func.Value.ClampRangeMin = 25;
tag.Save();
}
finally
{
tag.Dispose();
}
Running ManagedBlam with Python
# Requires the pythonnet module to be installed -> pip install pythonnet
import clr
import os
ek_path = r"path-to-ek"
mb_path = os.path.join(ek_path, 'bin', 'managedblam')
clr.AddReference(mb_path)
import Bungie
def main():
startup_parameters = Bungie.ManagedBlamStartupParameters()
Bungie.ManagedBlamSystem.Start(ek_path, callback(), startup_parameters)
load_tag_and_print_some_fields(r"globals\ai_globals.ai_globals")
def load_tag_and_print_some_fields(relative_tag_path):
tag, tag_path = get_tag_and_path(relative_tag_path)
tag.Load(tag_path)
block_data = tag.SelectField("Block:data")
first_element = block_data.Elements[0]
for field in first_element.Fields:
print(field)
def get_tag_and_path(user_path):
"""Return the tag and bungie tag path for tag creation"""
relative_path, tag_ext = get_path_and_ext(user_path)
tag = Bungie.Tags.TagFile()
tag_path = Bungie.Tags.TagPath.FromPathAndExtension(relative_path, tag_ext)
return tag, tag_path
def get_path_and_ext(user_path):
"""Splits a file path into path and extension"""
return user_path.rpartition(".")[0], user_path.rpartition(".")[2]
def callback():
pass
if __name__ == '__main__':
main()
Using helper functions
def Element_set_field_value(self, element, field_name: str, value):
"""Sets the value of the given field by name. Requires the tag element to be specified as the first arg. Returns the field"""
field = element.SelectField(field_name)
field_type_str = str(field.FieldType)
match field_type_str:
case "StringId":
field.SetStringData(value)
case "ShortInteger":
field.SetStringData(value)
case "LongEnum":
field.SetValue(value)
case "Reference":
field.Path = self.TagPath_from_string(value)
case "WordInteger":
field.SetStringData(value)
return field
Another Python example provided by Crisp
, this is a helper function that makes setting field values much easier to read in large sections of code. This is only confirmed to work with Reach+ ManagedBlam. See the following code for the use case:
# Set up shader parameters
block_parameters = struct_render_method.SelectField("parameters")
for m in maps:
element = self.Element_from_field_value(
block_parameters, "parameter name", m['parameter name'])
if element is None:
element = block_parameters.AddElement()
self.Element_set_field_value(
element, "bitmap flags", "1") # sets override
# sets anisotropic (4) EXPENSIVE
self.Element_set_field_value(element, "bitmap filter mode", "6")
self.Element_set_field_value(element, "parameter type", "bitmap")
self.Element_set_field_values(element, m)
Tool for transferring H2 zones/areas/firingpositions to a H3 scenario
Additional examples
If you have experience with ManagedBlam and wish to provide more examples for this page, either make a pull request with the code, or contact pepperman
on Discord
!
Acknowledgements
Thanks to the following individuals for their research or contributions to this topic:
- Crisp (Research and help, example python code, .SelectField() usage, and for development of Foundry, which has many examples of ManagedBlam functionality for Reach+)
- Krevil (Significant early research and help, and for making PresentationODST, which exemplifies use of ManagedBlam for H3/ODST)
- num0005 (Figured out how to load managedblam in dotnet 6+ because it was less hassel than figuring out how to ship two executables together. Worked out the probably cause of the ASLR linked assertions.)
- PepperMan (Writing this page, compiling information together, and the example C# programs for ManagedBlam in Halo 3)