REFramework wrapper in Zig

REFramework is a modding framework, scripting platform, and modding tool for RE Engine games(copy pasted from their description). Some famous titles made using RE Engine: Resident Evil Requiem, Resident Evil 7, Resident Evil Village, Pragmata, Devil May Cry 5, Street Fighter 6 and so on.

But wanted to write in-game “modding” scripts in Zig :slight_smile: . So here it is zig_reframework_plugin.

It was a fun project, but I made a weird API convention for this, which I would like to discuss a little.

Since we’re dealing with a lot of C pointers, and REFramework’s C-API exposes a lot of them.
So my idea was how about instead of checking every single possible C struct fields(which are pointers, almost 90% of them are I think) if it’s null or not, you define some sort of “spec” of what you want to use. And check those fields automatically during “init”. And be use without checking anymore nulls.

The Verified is the main type generator here.

So an example:

pub const minimal = .{
    .functions = .all,
    .sdk = .{
        .functions = .{
            .get_managed_singleton,
            .get_tdb,
            .add_hook,
            .remove_hook,
            .create_managed_string_normal,
            .create_managed_array,
        },
    },
};
const Minimal = Verified(CTYPE, minimal);

// later
const minimal = try Minimal.init(c_pointer);
minimal.safe().functions.safe().<all>;
minimal.safe().sdk.safe().functions.safe().get_managed_singleton()

Okay first thing you might be thinking is wow a lot of .safe() there, yes its one of the drawbacks of this approach since using namespace is removed and I don’t know how to deal with it in a better way.

And one more drawback is say a function is

fn foo(sdk: Verified(SomeCType, .{ .sdk = .{ .functions = .get_managed_singleton } }))

now to pass a “superset” of the speced Varified type eg. the above Minimal type is a superset for this foo’s parameter type, since Minimal already checks .sdk.functions.get_managed_singleton we’ve to use .fromOther(minimal) or short-hand .fo(minimal).

Andd another drawback is:

const Foo = Verified(CType, .{ .sdk = .{ .functions = .get_managed_singleton } });
const Bar = Verified(CType, .{ .sdk = .{ .functions = .get_managed_singleton } });

here Foo != Bar that’s kind of makes sense from language prospective, you’re creating two different types by calling Verified there’s kind of no way to tell if they’re same, or you’d just have to deal with a lot of edge cases to support it in the language, which is not worth. So, .fo call is a way to do it again, they’re “superset” of each other.

If we don’t know what to use we can just pass Verified(CType, .all_recursive) of course to verify everything during init.

I know I didn’t need to go this route for this project, since most of the API exposed structs contains only function pointers and from the REFRamework plugin’s implementation point of view its garunteed that those function pointers will be set. But think about other use cases, where there are not only function pointers but mix of optional, required C-pointers, we need to access from zig-land.

And there’s this interop implementation to bridge between RE Engine Il2Cpp data and some Zig data.

If you’ve time you can check this example how some “real-world” implementation would look like, the equivilent implementation in REFramework’s C# plugin is here.

2 Likes