====== 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(ReferenceEqualityComparer.Instance); Walk(root, fieldProvider ?? GetDefaultFields, visited, obj => { if (includeDerivedTypes ? obj is T match : obj.GetType() == typeof(T)) { results.Add((T)obj); } }); return results; } public static Dictionary> BuildTypeIndex( object root, Func fieldProvider = null) { var index = new Dictionary>(); var visited = new HashSet(ReferenceEqualityComparer.Instance); Walk(root, fieldProvider ?? GetDefaultFields, visited, obj => { var type = obj.GetType(); if (!index.TryGetValue(type, out var list)) { list = new List(); index[type] = list; } list.Add(obj); }); return index; } private static void Walk( object obj, Func fieldProvider, HashSet visited, Action onVisit) { if (obj == null) { return; } Type type = obj.GetType(); if (!type.IsClass || type == typeof(string)) { return; } if (!visited.Add(obj)) { return; } if (type.IsArray) { var elementType = type.GetElementType(); if (elementType != null && elementType.IsClass && elementType != typeof(string) && !elementType.IsArray) { foreach (var item in (Array)obj) { Walk(item, fieldProvider, visited, onVisit); } } return; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) { var elementType = type.GetGenericArguments()[0]; if (elementType.IsClass && elementType != typeof(string) && !elementType.IsArray) { foreach (var item in (IList)obj) { Walk(item, fieldProvider, visited, onVisit); } } return; } onVisit(obj); foreach (var field in fieldProvider(type)) { var value = field.GetValue(obj); if (value != null) { Walk(value, fieldProvider, visited, onVisit); } } } private static FieldInfo[] GetDefaultFields(Type type) { if (!DefaultFieldCache.TryGetValue(type, out var fields)) { fields = type.GetFields(DefaultFieldFlags); DefaultFieldCache[type] = fields; } return fields; } public static List FindValueList( object root, Func fieldProvider, string fieldName, Type expectedFieldType = null, bool includeDerivedTypes = true) where T : class { fieldProvider ??= GetDefaultFields; var instances = FindAll(root, fieldProvider, includeDerivedTypes); var results = new List(instances.Count); foreach (var instance in instances) { var field = ResolveField(instance.GetType(), fieldProvider, fieldName, expectedFieldType); results.Add(field.GetValue(instance)); } return results; } public static List FindValueList( object root, Func fieldProvider, string fieldName, bool includeDerivedTypes = true) where T : class { fieldProvider ??= GetDefaultFields; var instances = FindAll(root, fieldProvider, includeDerivedTypes); var results = new List(instances.Count); foreach (var instance in instances) { var field = ResolveField(instance.GetType(), fieldProvider, fieldName, expectedFieldType: null); if (!typeof(TValue).IsAssignableFrom(field.FieldType)) { throw new InvalidOperationException( $"Field '{fieldName}' on type '{instance.GetType().FullName}' has type " + $"'{field.FieldType}', which is not assignable to requested value type '{typeof(TValue)}'."); } results.Add((TValue)field.GetValue(instance)); } return results; } private static FieldInfo ResolveField( Type instanceType, Func fieldProvider, string fieldName, Type expectedFieldType) { FieldInfo field = null; foreach (var candidate in fieldProvider(instanceType)) { if (candidate.Name == fieldName) { field = candidate; break; } } if (field == null) { throw new MissingFieldException(instanceType.FullName, fieldName); } if (expectedFieldType != null && field.FieldType != expectedFieldType) { throw new InvalidOperationException( $"Field '{fieldName}' on type '{instanceType.FullName}' has type '{field.FieldType}', " + $"but expected '{expectedFieldType}'."); } return field; } } public class HavokPropertyCache { public HavokPropertyCache() { } public readonly Dictionary FieldCache = new(); public FieldInfo[] GetCachedHavokFields(Type type) { if (!FieldCache.TryGetValue(type.FullName, out FieldInfo[] fields)) { fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); fields = fields.OrderBy(f => f.MetadataToken).ToArray(); FieldCache.Add(type.FullName, fields); } return fields; } }