Code Monkey home page Code Monkey logo

red4ext-rs's Introduction

red4ext-rs CI Update RED4ext.SDK

Automagical Rust binding to RED4ext.

quickstart

Modify Cargo.toml to make your crate a cdylib so that it compiles into a DLL:

[lib]
crate-type = ["cdylib"]

Define your plugin in src/lib.rs:

use red4ext_rs::prelude::*;

// this macro generates boilerplate that allows red4ext to boostrap the plugin
define_plugin! {
    name: "example",
    author: "author",
    version: 0:1:0,
    on_register: {
        // functions registered here become accessible in redscript and CET under the name provided as the first parameter
        register_function!("SumInts", sum_ints);
    }
}

fn sum_ints(ints: Vec<i32>) -> i32 {
    ints.iter().sum()
}

If you want the function to be available in redscript you need to provide a binding in redscript too:

native func SumInts(ints: array<Int32>) -> Int32;

Now, when you run cargo build --release, a DLL file will be generated in target/release. This DLL is a plugin that is ready to be deployed to Cyberpunk 2077/red4ext/plugins/.

A complete example project is available here.

calling functions

The main crate exposes small macro that allows you to call game functions directly from Rust:

let result = call!("OperatorAdd;Uint32Uint32;Uint32" (2u32, 2u32) -> u32);

It can also be used to invoke methods on objects:

fn is_player(scriptable: Ref<IScriptable>) -> bool {
    call!(scriptable, "IsPlayer;" () -> bool)
}

It works OK if you don't need to invoke game functions frequently, but for larger projects a more convenient, proc macro approach is described in the next section.

proc macros

The macros crate feature enables a few proc macros that make interop even easier.

Available macros:

  • redscript_global

    Imports a global and exposes it as plain a Rust function, taking care of name mangling automatically.

    Parameters:

    • name - the in-game function name (it defaults to a PascalCase version of the Rust name)
    • native - whether the function is native (affects mangling)
    • operator - whether the function is an operator (affects mangling)

    Example:

    #[redscript_global(name = "OperatorAdd", operator)]
    fn add_u32(l: u32, r: u32) -> u32;
  • redscript_import

    Imports a set of methods for a class type.

    Parameters (optionally specified for each method with the #[redscript(...)] attribute):

    • name - the in-game function name (it defaults to a PascalCase version of the Rust name)
    • native - whether the function is native (affects mangling)
    • cb - whether the function is a callback (affects mangling)

    functions without a self receiver generate calls to static methods

    functions with a self receiver require a nightly Rust compiler with the arbitrary_self_types feature enabled for now

    Example:

    #![feature(arbitrary_self_types)]
    
    struct PlayerPuppet;
    
    impl ClassType for PlayerPuppet {
        // should be ScriptedPuppet if we were re-creating the entire class hierarchy,
        // but IScriptable can be used instead because every scripted class inherits from it
        type BaseClass = IScriptable;
    
        const NAME: &'static str = "PlayerPuppet";
    }
    
    #[redscript_import]
    impl PlayerPuppet {
        /// imports 'public native func GetDisplayName() -> String'
        #[redscript(native)]
        fn get_display_name(self: &Ref<Self>) -> String;
    
        /// imports 'private func DisableCameraBobbing(b: Bool) -> Void'
        #[redscript(name = "DisableCameraBobbing")]
        fn disable_cam_bobbing(self: &Ref<Self>, toggle: bool);
    
        /// imports 'public static func GetCriticalHealthThreshold() -> Float'
        fn get_critical_health_threshold() -> f32;
    }

custom types

By default this project only provides support for standard types like integers, floats and some collections.

As a convenience, it already provides most common literal types:

  • CName
  • TweakDBID
  • ResRef

and native structs:

If you want to use other types, you have to write your own binding which is relatively easy to do, but it's on you to guarantee that it matches the layout of the underlying type.

  • if you have types that directly map into one of the known primitives like i32, String etc. you should implement the FromRepr and IntoRepr traits for them; this is the only option that doesn't involve unsafe code

  • structs should be represented as Rust structs with #[repr(C)]

    #[repr(C)]
    struct Vector2 {
        x: f32,
        y: f32,
    }
    
    unsafe impl NativeRepr for Vector2 {
        // this needs to refer to an actual in-game type name
        const NAME: &'static str = "Vector2";
    }
  • classes should be represented as empty structs and implement ClassType with the native class name

    class types cannot be passed by value, they should always remain behind an indirection like Ref or WRef

    struct PlayerPuppet;
    
    impl ClassType for PlayerPuppet {
        // should be ScriptedPuppet if we were re-creating the entire class hierarchy,
        // but IScriptable can be used instead because every scripted class inherits from it
        type BaseClass = IScriptable;
    
        const NAME: &'static str = "PlayerPuppet";
    }
  • enums should be represented as Rust enums with #[repr(i64)]

    #[repr(i64)]
    enum ShapeVariant {
        Fill = 0,
        Border = 1,
        FillAndBorder = 2,
    }
    
    unsafe impl NativeRepr for ShapeVariant {
        const NAME: &'static str = "inkEShapeVariant";
    }

debugging

When compiled in debug mode, a panic handler is installed for each function. It helps with debugging common issues like function invokation errors:

[2023-04-24 23:37:11.396] [example] [error] CallDemo function panicked: failed to invoke OperatorAdd;Uint32Uint32;Uint32: expected Uint32 argument at index 0

contributing

When testing or contributing to this repo locally, here's a couple of commands to make your life easier:

  1. make sure Just command runner is installed
  2. run any of these commands:
    1. overwrite example mod folders to game directory
      just dev

      by default it will install them in "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077" but you can configure it with an .env file like so:

      GAME_DIR="C:\\path\\to\\my\\game\\folder"
    2. overwrite only redscript example mod folder in-game:
      just hot-reload
    3. display RED4ext and example mod logs:
      just logs
    4. hard delete all example mod folders from your game directory:
      just uninstall
    5. list all available recipes and their alias:
      just

credits

red4ext-rs's People

Contributors

drjackiebright avatar github-actions[bot] avatar jac3km4 avatar roms1383 avatar sahewat avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

red4ext-rs's Issues

crash from crate metadata

Hi jekky !

I stumbled upon a corner-case bug where, if I use metadata crate in my plugin (I would like to use especially MediaFileMetadata's _duration property to get the duration of audio files), the game crashes on load stating missing native functions ... with the native functions I defined on Redscript side, most likely during compilation phase.

As soon as I remove the import and the code calling metadata, then it just launches fine.
I don't really see why this bug happens, hence why I'm reporting here.

Here's the code that triggers the error on launch:

use metadata::MediaFileMetadata;
let duration = MediaFileMetadata::new(&filepath).unwrap()._duration.unwrap();

I checked: the path is valid, the metadata exists and it doesn't panic.

Here are the logs (plugin's name is audioware):

Click to see red4ext.log (module error stated)

[2023-08-15 10:52:31.705] [RED4ext] [info] RED4ext (v1.15.0) is initializing...
[2023-08-15 10:52:31.705] [RED4ext] [info] Game patch: 1.63 Hotfix 1
[2023-08-15 10:52:31.705] [RED4ext] [info] Product version: 1.63
[2023-08-15 10:52:31.705] [RED4ext] [info] File version: 3.0.72.54038
[2023-08-15 10:52:31.739] [RED4ext] [info] RED4ext has been successfully initialized
[2023-08-15 10:52:31.792] [RED4ext] [info] RED4ext is starting up...
[2023-08-15 10:52:31.792] [RED4ext] [info] Loading plugins...
[2023-08-15 10:52:31.792] [RED4ext] [info] Loading plugin from 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\ArchiveXL\ArchiveXL.dll'...
[2023-08-15 10:52:31.921] [RED4ext] [info] ArchiveXL (version: 1.5.1, author(s): psiberx) has been loaded
[2023-08-15 10:52:31.928] [RED4ext] [info] Loading plugin from 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\audioware\audioware.dll'...
[2023-08-15 10:52:32.033] [RED4ext] [warning] Could not load plugin 'audioware'. Error code: 126, msg: 'The specified module could not be found.', path: 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\audioware\audioware.dll'
[2023-08-15 10:52:32.033] [RED4ext] [info] Loading plugin from 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\Codeware\Codeware.dll'...
[2023-08-15 10:52:32.490] [RED4ext] [info] Codeware (version: 1.1.8, author(s): psiberx) has been loaded
[2023-08-15 10:52:32.490] [RED4ext] [info] Loading plugin from 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\mod_settings\mod_settings.dll'...
[2023-08-15 10:52:32.531] [RED4ext] [info] Mod Settings (version: 0.2.0, author(s): Jack Humbert) has been loaded
[2023-08-15 10:52:32.532] [RED4ext] [info] Loading plugin from 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\RedHotTools\RedHotTools.dll'...
[2023-08-15 10:52:32.668] [RED4ext] [info] RedHotTools (version: 0.4.12, author(s): psiberx) has been loaded
[2023-08-15 10:52:32.668] [RED4ext] [info] Loading plugin from 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\TweakXL\TweakXL.dll'...
[2023-08-15 10:52:32.746] [RED4ext] [info] TweakXL (version: 1.2.1, author(s): psiberx) has been loaded
[2023-08-15 10:52:32.746] [RED4ext] [info] 5 plugin(s) loaded
[2023-08-15 10:52:32.746] [RED4ext] [info] RED4ext has been started
[2023-08-15 10:52:35.577] [RED4ext] [info] Scripts BLOB is set to 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\cache\modded\final.redscripts'
[2023-08-15 10:52:35.578] [RED4ext] [info] Using scriptsBlobPath
[2023-08-15 10:52:35.578] [RED4ext] [info] Adding paths to redscript compilation:
[2023-08-15 10:52:35.579] [RED4ext] [info] Mod Settings: 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\mod_settings\packed.reds'
[2023-08-15 10:52:35.579] [RED4ext] [info] Mod Settings: 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\mod_settings\module.reds'
[2023-08-15 10:52:35.579] [RED4ext] [info] Paths written to: 'C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\redscript_paths.txt'
[2023-08-15 10:52:35.579] [RED4ext] [info] Final redscript compilation arg string: '-compile "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\scripts" "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\cache\modded\final.redscripts" -compilePathsFile "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\redscript_paths.txt"'
[2023-08-15 10:52:47.103] [RED4ext] [info] RED4ext is terminating...
[2023-08-15 10:52:47.135] [RED4ext] [info] RED4ext has been terminated

Click to see redscript_rCURRENT.log

[INFO - Tue, 15 Aug 2023 10:52:35 +0700] Using defaults for the script manifest (manifest not present)
[INFO - Tue, 15 Aug 2023 10:52:35 +0700] Bundle path provided: C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\cache\modded\final.redscripts
[INFO - Tue, 15 Aug 2023 10:52:35 +0700] Redscript cache file is not ours, copying it to C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\cache\modded\final.redscripts.bk
[INFO - Tue, 15 Aug 2023 10:52:35 +0700] Compiling files in C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\scripts:
Addicted\Config.reds
Addicted\Crossover.reds
Addicted\Debug.reds
Addicted\Definitions.reds
Addicted\Helper.reds
Addicted\helpers\Bits.reds
Addicted\helpers\Effect.reds
Addicted\helpers\Feeling.reds
Addicted\helpers\Generic.reds
Addicted\helpers\Items.reds
Addicted\helpers\Translations.reds
Addicted\i18n\English.reds
Addicted\i18n\French.reds
Addicted\Localization.reds
Addicted\managers\AudioManager.reds
Addicted\managers\BlackLaceManager.reds
Addicted\managers\HealerManager.reds
Addicted\managers\StimulantManager.reds
Addicted\managers\WithdrawalSymptomsManager.reds
Addicted\System.reds
Addicted\Tweaks.reds
Addicted\ui\BiomonitorController.reds
Addicted\Utils.reds
ArchiveXL\ArchiveXL.reds
audioware\Natives.reds
Codeware\Codeware.Global.reds
Codeware\Codeware.Localization.reds
Codeware\Codeware.reds
Codeware\Codeware.UI.reds
Codeware\Codeware.UI.TextInput.reds
Toxicity.reds
TweakXL\TweakXL.reds
C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\mod_settings\packed.reds
C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\mod_settings\module.reds
[INFO - Tue, 15 Aug 2023 10:52:36 +0700] Compilation complete
[INFO - Tue, 15 Aug 2023 10:52:36 +0700] Output successfully saved to C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\cache\modded\final.redscripts

Import function with array of ref<SomeClass> as parameter

Hi jekky, hope you're doing fine :)
Here's the report for the issue I mentioned to you on Discord.
Sorry for the weird naming, it's directly copied from my current mod dev.

✅ works with array of primitives

Redscript
module Addicted

public class Consumption extends IScriptable {
   public persistent let doses: array<Float>;
   public func SetDoses(value: array<Float>) -> Void { this.doses = value; }
}
Rust
#[derive(Default, Clone)]
#[repr(transparent)]
pub struct Consumption(pub Ref<IScriptable>);
unsafe impl RefRepr for Consumption {
    type Type = Strong;
    const CLASS_NAME: &'static str = "Addicted.Consumption";
}

#[redscript_import]
impl Consumption {
    fn set_doses(&mut self, values: Vec<f32>) -> ();
}

❌fails with array of e.g. Consumption(Ref<IScriptable>)

Redscript
module Addicted

public class Consumption extends IScriptable {
   public persistent let current: Int32;
}

public class Consumptions extends IScriptable {
   private persistent let values: array<ref<Consumption>>;
   public func SetValues(values: array<ref<Consumption>>) -> Void { this.values = values; }
}
Rust
#[derive(Default, Clone)]
#[repr(transparent)]
pub struct Consumption(Ref<IScriptable>);
unsafe impl RefRepr for Consumption {
    type Type = Strong;
    const CLASS_NAME: &'static str = "Addicted.Consumption";
}

#[derive(Default, Clone)]
#[repr(transparent)]
pub struct Consumptions(Ref<IScriptable>);
unsafe impl RefRepr for Consumptions {
    type Type = Strong;
    const CLASS_NAME: &'static str = "Addicted.Consumptions";
}

#[redscript_import]
impl Consumptions {
    fn set_values(&mut self, values: Vec<Consumption>) -> (); // or RedArray<Consumption>, same outcome
}

Error: [2023-09-30 13:11:02.785] [addicted] [error] Function 'Addicted.OnIngestedItem' has panicked: failed to invoke SetValues;array<Addicted.Consumption>: function not found

✅ works with array of Ref<IScriptable>

Redscript
public class Consumptions extends IScriptable {
    private persistent let values: array<ref<Consumption>>;
    // sadly the array has to be reconstructed
    public func SetValues(values: array<ref<IScriptable>>) -> Void {
        ArrayClear(this.values);
        this.values = [];
        for value in values {
            ArrayPush(this.values, value as Consumption);
        }
    }
}
Rust
#[derive(Default, Clone)]
#[repr(transparent)]
pub struct Consumption(pub Ref<IScriptable>);
unsafe impl RefRepr for Consumption {
    type Type = Strong;
    const CLASS_NAME: &'static str = "Addicted.Consumption";
}

#[derive(Default, Clone)]
#[repr(transparent)]
pub struct Consumptions(Ref<IScriptable>);
unsafe impl RefRepr for Consumptions {
    type Type = Strong;
    const CLASS_NAME: &'static str = "Addicted.Consumptions";
}

#[redscript_import]
impl Consumptions {
    fn set_values(&mut self, values: Vec<Ref<IScriptable>>) -> ();
}

The solution is a bit cumbersome, since it involves on Rust side:

let values = values.into_iter().map(|x| x.0).collect();
self.set_values(values);

Followed on Redscript side by:

ArrayClear(this.values);
this.values = [];
for value in values {
    ArrayPush(this.values, value as Consumption);
}

Call static function

Trying to call a static function is a known issue already, but I'm gathering informations here to make it easier to fix it in the future.

How to reproduce

reds/Native.reds

native func CompareEngineTime(sim: EngineTime, float: Float) -> Void;

// call like: Game.GetPlayer():Test();
@addMethod(PlayerPuppet)
public func Test() -> Void {
    let system: ref<TimeSystem> = GameInstance.GetTimeSystem(this.GetGame());
    let sim: EngineTime = system.GetSimTime();
    let float: Float = EngineTime.ToFloat(sim);
    CompareEngineTime(sim, float);
}

src/lib.rs

use red4ext_rs::prelude::*;

define_plugin! {
    name: "example",
    author: "author",
    version: 0:1:0,
    on_register: {
        register_function!("CompareEngineTime", compare_engine_time);
    }
}

/// see [RED4ext.SDK](https://github.com/WopsS/RED4ext.SDK/blob/3a41c61f6d6f050545ab62681fb72f1efd276536/include/RED4ext/Scripting/Natives/Generated/EngineTime.hpp#L12)
#[derive(Debug, Default, Clone)]
#[repr(C)]
struct EngineTime {
    pub unk00: [u8; 8],
}

unsafe impl NativeRepr for EngineTime {
    const NAME: &'static str = "EngineTime";
}

/// see [cyberdoc](https://jac3km4.github.io/cyberdoc/#28637)
#[redscript_import]
impl EngineTime {
    #[redscript(native)]
    fn to_float(time: Self) -> f32;

    #[redscript(native)]
    fn from_float(time: f32) -> Self;
}

fn compare_engine_time(sim: EngineTime, float: f32) {
    let roundtrip: f32 = EngineTime::ToFloat(sim); // error: "failed to invoke EngineTime::ToFloat: function not found"
    assert_eq!(float, roundtrip);
}

Comparing with C++

Using Codeware Reflection API works as expected, like so:

import Codeware.*

// call like: Game.GetPlayer():TestRoundTrip()
@addMethod(PlayerPuppet)
public func TestRoundTrip() -> Void {
    let system: ref<TimeSystem> = GameInstance.GetTimeSystem(this.GetGame());
    let sim: EngineTime = system.GetSimTime();
    let float: Float = EngineTime.ToFloat(sim);
    LogChannel(n"DEBUG", ToString(float));
    let cls = Reflection.GetClass(n"EngineTime");
    let fromfloat = cls.GetStaticFunction(n"FromFloat");
    let outcome: Bool;
    let back = fromfloat.Call([float], outcome);
    LogChannel(n"DEBUG", ToString(outcome));
    let casted: EngineTime = FromVariant<EngineTime>(back);
    let roundtrip: Float = EngineTime.ToFloat(casted);
    LogChannel(n"DEBUG", ToString(roundtrip));
}

I'm going to keep investigating and comparing between C++ and Rust adding more comments below, and maybe resulting in a PR if I can come up with a fix.

Call imported static function without instance parameter: not found or instant crash

I'm currently trying and noticed limitations with corner-case usages of static func:

static without instance on vanilla class

When calling a static func which expects its first parameter to be an instance of itself
e.g. public final static func CanApplyBreathingEffect(player: wref<PlayerPuppet>) -> Bool from example it works ✅

However, static func which does not take any parameter
e.g. public static func GetCriticalHealthThreshold() -> Float (from PlayerPuppet too) causes instant crash with the following error :

// from REDEngine/ReportQueue
Error reason: Unhandled exception
Expression: EXCEPTION_ACCESS_VIOLATION (0xC0000005)
Message: The thread attempted to read inaccessible data at 0x0.
File: <Unknown>(0)

I also tried specifying a fake player: PlayerPuppet as a parameter out of curiosity
e.g. pub fn get_critical_health_threshold(player: PlayerPuppet) -> f32; and then it doesn't crash but instead ends up as expected like:

[2023-09-28 12:20:41.578] [example] [error] Function 'My.TestSystemThruPlayer' has panicked: failed to invoke PlayerPuppet::GetCriticalHealthThreshold;PlayerPuppet: function not found

static without instance on custom class

Another attempt with a custom class of mine like:

module My

public class System extends ScriptableSystem {
    public static func HelloStatic() -> Int32 {
        return 42;
    }
}
#[derive(Default, Clone)]
#[repr(transparent)]
pub struct System(Ref<IScriptable>);

unsafe impl RefRepr for System {
    type Type = Strong;

    const CLASS_NAME: &'static str = "My.System";
}

#[redscript_import]
impl System {
    pub fn hello_static() -> i32;
}

ends up like:

// from mod's RED4ext log
[2023-09-28 11:44:12.381] [example] [error] Function 'My.TestSystem' has panicked: failed to invoke System::HelloStatic;: function not found

Other cases work just fine 👌

It's not blocking for me now since there's workarounds, but I report for completeness.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.