====== Adding Havok Variables (Script) ======
Authors: Vawser
This tutorial is for Dark Souls III. It is also applicable to Elden Ring, however you will need to replace HKX2 usage with HKLib.
Note: you will need two C# libraries to use this script:
- [[https://github.com/vawser/Smithbox/tree/main/src/Andre/SoulsFormats|SoulsFormats]]
- [[https://github.com/vawser/Smithbox/tree/main/src/Havok|HKX2]]
===== Overview =====
The following C# script will allow you to easily inject new havok variables into a Havok Behavior Graph.
The file path should be the binder file that contains the target behavior havok file.
The internal path should be the file path within the binder (you can find this by unpacking the binder with WitchyBND and looking in the xml it produces.
===== Main Handler =====
public HavokPropertyCache HavokPropertyCache = new();
public void BehaviorFileHandler()
{
var filePath = @"C:\Example\c0000.behbnd.dcx";
var writefilePath = @"C:\Example\new_c0000.behbnd.dcx";
var internalPath = @"N:\FDP\data\INTERROOT_win64\Action\c0000\Export\Behaviors\c0000.hkx";
// Load
var fileData = File.ReadAllBytes(filePath);
HKX2.hkRootLevelContainer workingHkx = null;
HKX2.PackFileDeserializer deserializer = new HKX2.PackFileDeserializer();
HKX2.PackFileSerializer serializer = new HKX2.PackFileSerializer();
var readBinder = new BND4Reader(fileData);
foreach (var file in readBinder.Files)
{
if (file.Name != internalPath)
continue;
var fileBytes = readBinder.ReadFile(file).ToArray();
using (MemoryStream memoryStream = new MemoryStream(fileBytes))
{
try
{
var br = new BinaryReaderEx(false, memoryStream.ToArray());
workingHkx = (HKX2.hkRootLevelContainer)deserializer.Deserialize(br);
}
catch (InvalidDataException) { }
}
}
readBinder.Dispose();
InjectVariable(workingHkx);
// Save
var writeBinder = BND4.Read(fileData);
foreach (var file in writeBinder.Files)
{
if (file.Name != internalPath)
continue;
using (MemoryStream memoryStream = new MemoryStream(file.Bytes.ToArray()))
{
if (workingHkx != null)
{
var bw = new BinaryWriterEx(false);
serializer.Serialize((HKX2.IHavokObject)workingHkx, bw);
file.Bytes = bw.FinishBytes();
}
}
}
File.WriteAllBytes(writefilePath, writeBinder.Write());
}
===== Inject Variable =====
public void InjectVariable(HKX2.hkRootLevelContainer root)
{
var varIndex = -1;
// Add the variable setup
var behaviorGraphs = HavokTreeSearch.FindAll(
root, HavokPropertyCache.GetCachedHavokFields);
var top = behaviorGraphs.FirstOrDefault();
if (top != null)
{
var newVarInfo = new HKX2.hkbVariableInfo()
{
m_role = new HKX2.hkbRoleAttribute()
{
m_role = HKX2.Role.ROLE_DEFAULT,
m_flags = 0
},
m_type = HKX2.VariableType.VARIABLE_TYPE_REAL
};
top.m_variableInfos.Add(newVarInfo);
var newVarBounds = new HKX2.hkbVariableBounds()
{
m_min = new HKX2.hkbVariableValue()
{
m_value = 0
},
m_max = new HKX2.hkbVariableValue()
{
m_value = 10
},
};
top.m_variableBounds.Add(newVarBounds);
var newWordVar = new HKX2.hkbVariableValue()
{
m_value = 1
};
top.m_variableInitialValues.m_wordVariableValues.Add(newWordVar);
top.m_stringData.m_variableNames.Add("WeaponAnimSpeed");
// This is the index to use for the clips
varIndex = top.m_variableInitialValues.m_wordVariableValues.Count - 1;
}
if (varIndex != -1)
{
// Add variable binding to clips
var clips = HavokTreeSearch.FindAll(
root, HavokPropertyCache.GetCachedHavokFields);
var newBinding = new HKX2.hkbVariableBindingSetBinding()
{
m_variableIndex = varIndex,
m_memberPath = "playbackSpeed",
m_bitIndex = -1,
m_bindingType = HKX2.BindingType.BINDING_TYPE_VARIABLE
};
var set = new HKX2.hkbVariableBindingSet()
{
m_bindings = new()
{
newBinding
},
m_indexOfBindingToEnable = -1
};
foreach (var clip in clips)
{
//x000_000000
var animIDstr = clip.m_name.Substring(5, 6);
int.TryParse(animIDstr, out int animID);
if (animID >= 30000 && animID <= 39999)
{
if (clip.m_variableBindingSet == null)
{
clip.m_variableBindingSet = set;
}
else
{
clip.m_variableBindingSet.m_bindings.Add(newBinding);
}
}
}
}
}
===== Utilities =====
public static class HavokTreeSearch
{
private const BindingFlags DefaultFieldFlags =
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
private static readonly Dictionary DefaultFieldCache = new();
public static List FindAll(
object root,
Func fieldProvider = null,
bool includeDerivedTypes = true) where T : class
{
var results = new List();
var visited = new HashSet