Site Tools


tutorial:adding-havok-variables-script

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:

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<HKX2.hkbBehaviorGraphData>(
            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<HKX2.hkbClipGenerator>(
                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<Type, FieldInfo[]> DefaultFieldCache = new();
 
    public static List<T> FindAll<T>(
        object root,
        Func<Type, FieldInfo[]> fieldProvider = null,
        bool includeDerivedTypes = true) where T : class
    {
        var results = new List<T>();
        var visited = new HashSet<object>(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<Type, List<object>> BuildTypeIndex(
        object root,
        Func<Type, FieldInfo[]> fieldProvider = null)
    {
        var index = new Dictionary<Type, List<object>>();
        var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
 
        Walk(root, fieldProvider ?? GetDefaultFields, visited, obj =>
        {
            var type = obj.GetType();
            if (!index.TryGetValue(type, out var list))
            {
                list = new List<object>();
                index[type] = list;
            }
            list.Add(obj);
        });
 
        return index;
    }
 
    private static void Walk(
        object obj,
        Func<Type, FieldInfo[]> fieldProvider,
        HashSet<object> visited,
        Action<object> 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<object> FindValueList<T>(
        object root,
        Func<Type, FieldInfo[]> fieldProvider,
        string fieldName,
        Type expectedFieldType = null,
        bool includeDerivedTypes = true) where T : class
    {
        fieldProvider ??= GetDefaultFields;
 
        var instances = FindAll<T>(root, fieldProvider, includeDerivedTypes);
        var results = new List<object>(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<TValue> FindValueList<T, TValue>(
    object root,
    Func<Type, FieldInfo[]> fieldProvider,
    string fieldName,
    bool includeDerivedTypes = true) where T : class
    {
        fieldProvider ??= GetDefaultFields;
 
        var instances = FindAll<T>(root, fieldProvider, includeDerivedTypes);
        var results = new List<TValue>(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<Type, FieldInfo[]> 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<string, FieldInfo[]> 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;
    }
}
tutorial/adding-havok-variables-script.txt · Last modified: by admin