Skip to content

Add IUnrealClasses for modifying and extending UClass functionality#3

Merged
rirurin merged 12 commits intoRyoTune:mainfrom
rirurin:main
Dec 9, 2025
Merged

Add IUnrealClasses for modifying and extending UClass functionality#3
rirurin merged 12 commits intoRyoTune:mainfrom
rirurin:main

Conversation

@rirurin
Copy link
Collaborator

@rirurin rirurin commented Nov 10, 2025

My goal with this PR is to port some of the features from p3rpc.classconstructor into UE.Toolkit and add some other features that I've wanted to add.

The current features I've either implemented or planned are:

  • Fixing CreateFString in IUnrealStrings, which doesn't add a null terminator. CreateFString in IUnrealObjects and IUnrealStrings are both now thunks to call UnrealStringsStatic.CreateFString (done)
  • Ability to mark/unmark an object in GUObjectArray as part of the root set to prevent garbage collection if it's not referenced elsewhere. (done)
  • IUnrealClasses.AddExtension to extend the allocation size of each instance of a particular object. An extra constructor which runs after the original one can be used to initialize this extra space. (done)
  • Methods for adding new blueprint/Object XML exposable fields (done)
  • Methods for invoking blueprint exposable functions (done)

@rirurin
Copy link
Collaborator Author

rirurin commented Nov 26, 2025

Here's what I ended up implementing. There's a lot of stuff here since I ended up porting most of the features from p3rpc.classconstructor so I could migrate to using only UE Toolkit for some frameworks I'm developing for Femc Reloaded.
Note that all the testing done has only been with Persona 3 Reload. There's one feature that I know is unimplemented for UE5 which I make note of below.

Edit/Add TMap entries in Object XML

Previously this only worked with data tables, which were effectively TMap<FName, ...>. Currently, TMap<FName, ...> and TMap<int, ...> are supported. This is handled using TMapDynamicDictionary which only has a compile-time defined key type.

Here's an example with BustupExistDataAsset:

<BustupExistDataAsset>
    <Data>
        <Item id="151"> <!-- The map doesn't contain a key for 151, so a new entry is created -->
            <Faces> <!-- FBustupFace -->
                <Item id="0">
                    <Clothes> <!-- FBustupCloth -->
                        <Item id="2">
                            <Pose value="PoseA"/> <!-- FBustupParts -->
                            <EyePartsID value="900"/>
                            <MouthPartsID value="900"/>
                            <bEyeAnim value="true"/>
                            <bMouthAnim value="true"/>
                            <InBetween value="0"/>
                            <EyeX value="668"/>
                            <EyeY value="1072"/>
                            <MouthX value="668"/>
                            <MouthY value="1310"/>
                            <BlushX value="716"/>
                            <BlushY value="1158"/>
                            <SweatX value="1047"/>
                            <SweatY value="1238"/>
                            <OffsetX value="0"/>
                            <OffsetY value="0"/>
                        </Item>
                    </Clothes>
                </Item>
            </Faces>
        </Item>
    </Data>
</BustupExistDataAsset>

UBustupExistDataAsset, FBustupFace and FBustupCloth all contain TMap<int, ...> data entries. I plan on supporting other key types as needed.

Add TArray entries in Object XML

Added the ability to push a new array entry into a TArray<...> by setting the id to -1.

An example from DlcBgmAsset:

<DatDlcBgmTable>
    <Data>
        <Item id="-1">
            <BandleID value="99"/>
            <SerialNumber value="26"/>
            <Title value="Example Song"/>
            <Offset value="1000"/>
            <ControlNumber value="26"/>
            <Sort value="26"/>
            <Result value="9999"/>
        </Item>
    </Data>
</DatDlcBgmTable>

Add new fields into an existing type

IUnrealClasses contains a set of functions (Add[Type]Property) that allow for registering a field of a certain type and offset into Unreal's type reflection system. This allows that field to be readable from Object XML and blueprints (you can see the new fields in the object dump or in UE4SS' debugger).

// UGlobalWork is P3R's game instance class
_context._toolkitClasses.AddConstructor<UGlobalWork>(obj => 
{
    if (!CreatedFields) 
    {
        _context._toolkitClasses.AddCBoolProperty<UAgePanel>("bIsDarkHour", 0xb0, out _);
        _context._toolkitClasses.AddU32Property<UAgePanel>("ActiveDrawTypeId", 0x2f8, out _);
        _context._toolkitClasses.AddStructProperty<UAgePanel, FLinearColor>("BottomColorNormal",   0x318, out _);
        _context._toolkitClasses.AddStructProperty<UAgePanel, FLinearColor>("BottomColorDarkHour", 0x328, out _);
        _context._toolkitClasses.AddStructProperty<UAgePanel, FLinearColor>("TopColorNormal",   0x338, out _);
        _context._toolkitClasses.AddStructProperty<UAgePanel, FLinearColor>("TopColorDarkHour", 0x348, out _);
        _context._toolkitClasses.AddStructProperty<UAgePanel, FLinearColor>("WaterColorNormal",   0x358, out _);
        _context._toolkitClasses.AddStructProperty<UAgePanel, FLinearColor>("WaterColorDarkHour", 0x368, out _);

        _context._toolkitClasses.AddU32Property<UFldManagerSubsystem>("CurrFieldMajor", 0x54, out _);
        _context._toolkitClasses.AddU32Property<UFldManagerSubsystem>("CurrFieldMinor", 0x58, out _);
        _context._toolkitClasses.AddU32Property<UFldManagerSubsystem>("CurrFieldSub", 0x5c, out _);
        CreatedFields = true;
    }
});

While I've yet to test this myself, this should allow for people who do like using Unreal's editor to edit UFldManagerSubsystem in their UProject to include major/minor/sub fields and refer to those in a blueprint to determine what field ID they're currently in.

Registering new struct types

It's possible to register a completely new FStruct into UE's type reflection. IUnrealClasses also contains methods (Create[Type]Param) to build a list of property params to pass into CreateScriptStruct:

TryCreateScriptStruct("SprColor", 4, new List<IFPropertyParams>
{
    _context._toolkitClasses.CreateI8Param("A", 0),
    _context._toolkitClasses.CreateI8Param("B", 1),
    _context._toolkitClasses.CreateI8Param("G", 2),
    _context._toolkitClasses.CreateI8Param("R", 3),
});

TryCreateScriptStruct("AgePanelSection", 0x30, new List<IFPropertyParams>
{
    _context._toolkitClasses.CreateF32Param("X1", 0),
    _context._toolkitClasses.CreateF32Param("X2", 4),
    _context._toolkitClasses.CreateF32Param("Y1", 8),
    _context._toolkitClasses.CreateF32Param("Y2", 0xc),
    _context._toolkitClasses.CreateF32Param("Field28", 0x28),
});

Much like adding new fields, this is viewable in Object XML and blueprints.

This is currently only implemented for UE4!. CreateScriptStruct calls CreateStructParam in the target version's type factory, What UE4 does (in UE4_27_2\TypeFactory.cs) is build the required compile-time reflection information (FPropertyParams and FStructParams, which UE processes into the runtime reflection you've used before), including building a custom vtable for the type. UE5 seems to handle struct initialization differently.

Type extension/creation is currently only possible through code, although I'm thinking of adding "type XML" in a future release to do all this codeless.

Extending object sizes and custom constructors

Inside IUnrealClasses, AddExtension(uint extraSize, callback) allows for increasing the size of a UE type by extraSize and define a custom constructor in callback to run after the original. The example above uses AddConstructor, which is simply AddExtension(0, callback) to register custom fields.

Invoking blueprint exposed methods

IUnrealMethods contains methods for creating parameters which can be input as a list into ProcessEvent to invoke a function that's exposable from blueprints. An example is:

var Params = new List<IInvocationParameter>();
var Value = _context._toolkitMethods.ProcessEvent<UBtlCoreComponent, float>(
    new ToolkitUObject<UBtlCoreComponent>(CurrentBtlCore),
    "GetElapsedTime", ref Params);
_context._utils.Log($"(UBtlCoreComponent @ 0x{(nint)CurrentBtlCore:x})->GetElapsedTime: {Value}");

Given that UE's definition for GetElapsedTime is

UFUNCTION(BlueprintCallable, BlueprintPure)
float GetElapsedTime() const;

Other Stuff

  • IUnrealState contains methods for getting the currently active world. This is based on UE's implementation of GetCurrentPlayWorld which checks for a Game world, but will fallback to a None world (usually the base world containing a list of streamed sublevels) if no game world is found (I have the fallback set to only happen with P3R atm but this might be a universal thing).
  • IUnrealState also has methods for getting a subsystem from a IUGameInstance.
  • It's possible to spawn new UObjects in code using the SpawnObject methods in IUnrealSpawning.
  • UObjects can avoid being garbage collected by being added to the root set AddToRootSet(int InternalIndex) in IUObjectArray. RemoveFromRootSet(int InternalIndex) allows for GC.
  • The object dumper no longer dumps fields from super types. Object XML can still set values in a super type since StructFieldNode traverses through the inheritance chain to obtain all types.
  • Allow for Object XML to properly set values for bit-sized booleans. StructFieldNode contains a map of FieldData values to store the field's byte offset and bit offset, which is calculated if multiple bool fields use the same offset.

@rirurin rirurin marked this pull request as ready for review November 26, 2025 03:09
@rirurin rirurin marked this pull request as draft November 26, 2025 05:41
@rirurin rirurin marked this pull request as ready for review November 26, 2025 06:34
@rirurin rirurin merged commit 887feb9 into RyoTune:main Dec 9, 2025
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant