Header-only library to help create RED4ext plugins.
add_compile_definitions(NOMINMAX)
add_subdirectory(vendor/RedLib)
target_link_libraries(Project PRIVATE RedLib)
#include <RedLib.hpp>
The namespace of the library Red
is also an alias for RED4ext
.
You can use whichever one you prefer (for example, RED4ext::IScriptable
vs Red::IScriptable
).
Red::DynArray<int32_t> MakeArray(int32_t n)
{
Red::DynArray<int32_t> array;
array.Reserve(n);
while (--n >= 0)
{
array.PushBack(std::rand());
}
return array;
}
void SortArray(Red::ScriptRef<Red::DynArray<int32_t>>& array)
{
std::sort(array.ref->begin(), array.ref->end());
}
void Swap(int32_t* a, int32_t* b)
{
std::swap(*a, *b);
}
RTTI_DEFINE_GLOBALS({
RTTI_FUNCTION(MakeArray);
RTTI_FUNCTION(SortArray);
RTTI_FUNCTION(Swap);
});
public static native func MakeArray(n: Int32) -> array<Int32>
public static native func SortArray(array: script_ref<array<Int32>>)
public static native func Swap(a: Int32, b: Int32)
public static func TestGlobals() {
let array = MakeArray(5);
SortArray(array);
for item in array {
LogChannel(n"DEBUG", ToString(item));
}
let a = 3;
let b = 7;
Swap(a, b);
LogChannel(n"DEBUG", s"a=\(a) b=\(b)");
}
enum class MyEnum
{
OptionA,
OptionB,
OptionC,
};
enum class MyFlags
{
FlagA = 1 << 0,
FlagB = 1 << 1,
FlagC = 1 << 2,
};
RTTI_DEFINE_ENUM(MyEnum);
RTTI_DEFINE_FLAGS(MyFlags);
enum MyEnum {
OptionA = 0,
OptionB = 1,
OptionC = 2,
}
enum MyFlags {
FlagA = 1,
FlagB = 2,
FlagC = 4,
}
struct MyStruct
{
int32_t Inc(Red::Optional<int32_t, 1> step)
{
value += step;
return value;
}
int32_t value;
};
RTTI_DEFINE_CLASS(MyStruct, {
RTTI_METHOD(Inc);
RTTI_PROPERTY(value);
});
Note that redscript structs can't have instance methods. All non-static methods are converted to static methods in redscript by the lib, so you can conveniently use instance methods in C++ despite the limitation.
public native struct MyStruct {
native let value: Int32;
public static native func Inc(self: script_ref<MyStruct>, opt step: Int32) -> Int32
}
public static func TestStruct() {
let x = new MyStruct(10);
MyStruct.Inc(x);
MyStruct.Inc(x, 5);
LogChannel(n"DEBUG", ToString(x.value)); // 16
}
struct MyClass : Red::IScriptable
{
void AddItem(MyEnum item)
{
items.EmplaceBack(item);
}
void AddFrom(const Red::Handle<MyClass>& other)
{
for (const auto& item : other->items)
{
items.EmplaceBack(item);
}
}
inline static Red::Handle<MyClass> Create()
{
return Red::MakeHandle<MyClass>();
}
Red::DynArray<MyEnum> items;
RTTI_IMPL_TYPEINFO(MyClass);
RTTI_IMPL_ALLOCATOR();
};
RTTI_DEFINE_CLASS(MyClass, {
RTTI_METHOD(AddItem);
RTTI_METHOD(AddFrom);
RTTI_METHOD(Create);
RTTI_GETTER(items);
});
public native class MyClass {
public native func AddItem(item: MyEnum)
public native func AddFrom(other: ref<MyClass>)
public native func GetItems() -> array<MyEnum>
public static native func Create() -> ref<MyClass>
}
public static func TestClass() {
let a = new MyClass();
a.AddItem(MyEnum.OptionA);
a.AddItem(MyEnum.OptionC);
let b = MyClass.Create();
b.AddItem(MyEnum.OptionB);
b.AddFrom(a);
for item in b.GetItems() {
LogChannel(n"DEBUG", ToString(item));
}
}
struct ClassA : Red::IScriptable
{
RTTI_IMPL_TYPEINFO(ClassA);
RTTI_IMPL_ALLOCATOR();
};
struct ClassB : ClassA
{
RTTI_IMPL_TYPEINFO(ClassB);
RTTI_IMPL_ALLOCATOR();
};
struct ClassC : ClassB
{
RTTI_IMPL_TYPEINFO(ClassC);
RTTI_IMPL_ALLOCATOR();
};
RTTI_DEFINE_CLASS(ClassA, "A", {
RTTI_ABSTRACT();
});
RTTI_DEFINE_CLASS(ClassB, "B", {
RTTI_PARENT(ClassA);
});
RTTI_DEFINE_CLASS(ClassC, "C", {
RTTI_PARENT(ClassB);
});
public abstract native class A {}
public native class B extends A {}
public native class C extends B {}
struct MyData
{
int32_t first;
int32_t second;
};
RTTI_DEFINE_CLASS(MyData, {
RTTI_PERSISTENT(first);
RTTI_PROPERTY(second);
});
public native struct MyData {
native persistent let first: Int32;
native let second: Int32;
}
public class MySystem extends ScriptableSystem {
private persistent let data: MyData;
private func OnAttach() {
this.data.first += 1; // Will be added to a save file and restored on load
this.data.second += 1; // Will reset on every load
LogChannel(n"DEBUG", s"MyData: \(this.data.first) / \(this.data.second)");
}
}
When you define IGameSystem
class, it will be automatically registered in game instance.
class MyGameSystem : public Red::IGameSystem
{
public:
bool IsAttached() const
{
return attached;
}
private:
void OnWorldAttached(Red::world::RuntimeScene* scene) override
{
attached = true;
}
void OnWorldDetached(Red::world::RuntimeScene* scene) override
{
attached = false;
}
bool attached{};
RTTI_IMPL_TYPEINFO(MyGameSystem);
RTTI_IMPL_ALLOCATOR();
};
RTTI_DEFINE_CLASS(MyGameSystem, {
RTTI_METHOD(IsAttached);
});
public native class MyGameSystem extends IGameSystem {
public native func IsAttached() -> Bool
}
@addMethod(GameInstance)
public static native func GetMyGameSystem() -> ref<MyGameSystem>
public static func TestGameSystem() {
let system = GameInstance.GetMyGameSystem();
LogChannel(n"DEBUG", s"Attached = \(system.IsAttached())");
}
In some cases game expects scripted classes and/or functions, and rejects native members. You can create scripted members backed by native code. In particular, it allows you to create scriptable systems.
struct MyScriptableSystem : Red::ScriptableSystem
{
void OnAttach()
{
Red::Log::Debug("Attached");
}
void OnDetach()
{
Red::Log::Debug("Detached");
}
void OnRestored(int32_t saveVersion, int32_t gameVersion)
{
Red::Log::Debug("Restored save={} game={}", saveVersion, gameVersion);
}
RTTI_IMPL_TYPEINFO(MyScriptableSystem);
RTTI_FWD_CONSTRUCTOR();
};
RTTI_DEFINE_CLASS(MyScriptableSystem, {
RTTI_SCRIPT_METHOD(OnAttach);
RTTI_SCRIPT_METHOD(OnDetach);
RTTI_SCRIPT_METHOD(OnRestored);
});
Using RTTI_FWD_CONSTRUCTOR()
as in the previous example,
you can inherit partially dedcoded classes and delegate construction and destruction to RTTI system.
You can use other names for RTTI definitions instead of the original C++ identifiers:
RTTI_DEFINE_ENUM(MyEnum, "Xyzzy");
RTTI_DEFINE_CLASS(MyStruct, "Foo", {
RTTI_PROPERTY(value, "bar");
RTTI_METHOD(Inc, "Baz");
});
enum Xyzzy {
OptionA = 0,
OptionB = 1,
OptionC = 2,
}
public native struct Foo {
native let bar: Int32;
public static native func Baz(self: script_ref<Foo>, opt step: Int32) -> Int32
}
You can add methods to already defined classes.
struct MyExtension : Red::GameObject
{
void AddTag(Red::CName tag)
{
tags.Add(tag);
}
};
RTTI_EXPAND_CLASS(Red::GameObject, {
RTTI_METHOD_FQN(MyExtension::AddTag);
});
@addMethod(GameObject)
public native func AddTag(tag: CName)
public static func TestExtension(game: GameInstance) {
let player = GetPlayer(game);
LogChannel(n"DEBUG", s"HasTag = \(player.HasTag(n"Test"))");
player.AddTag(n"Test");
LogChannel(n"DEBUG", s"HasTag = \(player.HasTag(n"Test"))");
}
Properties cannot be added to existing classes.
struct RawExample : RED4ext::IScriptable
{
inline static void Add(RawExample* self, RED4ext::CStackFrame* frame,
int32_t* out, RED4ext::CBaseRTTIType*)
{
int32_t a;
int32_t b;
RED4ext::GetParameter(frame, &a);
RED4ext::GetParameter(frame, &b);
++frame->code;
if (out)
{
*out = a + b;
}
// If this function was called from scripts,
// then stak frame should contain the caller
if (frame->func)
{
self->caller = frame->func->shortName;
}
}
RED4ext::CName caller;
RTTI_IMPL_TYPEINFO(RawExample);
};
RTTI_DEFINE_CLASS(RawExample, {
RTTI_METHOD(Add);
RTTI_GETTER(caller);
});
public native class RawExample {
public native func Add(a: Int32, b: Int32) -> Int32
public native func GetCaller() -> CName
}
public static func TestRaw() {
let obj = new RawExample();
let sum = obj.Add(2, 5);
LogChannel(n"DEBUG", s"Sum = \(sum)");
LogChannel(n"DEBUG", s"Called from \(obj.GetCaller())");
}
If you need access to stack frame, alternatively you can just add it as a param to a regular method:
struct RawExample : Red::IScriptable
{
int32_t Add(int32_t a, int32_t b, Red::CStackFrame* frame)
{
if (frame->func)
{
caller = frame->func->shortName;
}
return a + b;
}
Red::CName caller;
RTTI_IMPL_TYPEINFO(RawExample);
};
To register your definitions you have to call TypeInfoRegistrar::RegisterDiscovered()
.
RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::PluginHandle aHandle, RED4ext::EMainReason aReason,
const RED4ext::Sdk* aSdk)
{
if (aReason == RED4ext::EMainReason::Load)
{
Red::TypeInfoRegistrar::RegisterDiscovered();
}
return true;
}
At compile time you can convert any C++ type to a corresponding RTTI type name:
// CName("Uint64")
constexpr auto name = Red::GetTypeName<uint64_t>();
// CName("String")
constexpr auto name = Red::GetTypeName<Red::CString>();
// CName("array:handle:MyClass")
constexpr auto name = Red::GetTypeName<Red::DynArray<Red::Handle<MyClass>>>();
// std::array<char, 7> = "String\0"
constexpr auto name = Red::GetTypeNameStr<RED4ext::CString>();
At runtime you can get CBaseRTTIType
and CClass
based on C++ types:
auto stringType = Red::GetType<Red::CString>();
auto enumArrayType = Red::GetType<Red::DynArray<MyEnum>>();
auto entityClass = Red::GetClass<Red::Entity>();
float a = 13, b = 78, max;
Red::CallGlobal("MaxF", max, a, b); // max = MaxF(a, b)
Red::Vector4 vec{};
Red::CallStatic("Vector4", "Rand", vec); // vec = Vector4.Rand()
Red::ScriptGameInstance game;
Red::Handle<Red::PlayerSystem> system;
Red::Handle<Red::GameObject> player;
// system = GameInstance.GetPlayerSystem(game)
Red::CallStatic("ScriptGameInstance", "GetPlayerSystem", system, game);
// player = system.GetLocalPlayerControlledGameObject()
Red::CallVirtual(system, "GetLocalPlayerControlledGameObject", player);
// player.Revive(100.0)
Red::CallVirtual(player, "Revive", 100.0f);
auto system = Red::GetGameSystem<Red::IPersistencySystem>();
auto status = system->GetEntityStatus(1ULL);
const auto projectName = "MyMod";
Red::Log::Debug("Hello from {}", projectName);