Red Lib
Header-only library to help create RED4ext plugins.
Integration
CMake
add_compile_definitions(NOMINMAX)
add_subdirectory(vendor/RedLib)
target_link_libraries(Project PRIVATE RedLib)
#include <RedLib.hpp>
Namespaces
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
).
Building type info
Global functions
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 definitions
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,
}
Class definitions
Structs
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
}
IScriptables
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));
}
}
Inheritance
struct ClassA : Red::IScriptable
{
RTTI_IMPL_TYPEINFO(ClassA);
};
struct ClassB : ClassA
{
RTTI_IMPL_TYPEINFO(ClassB);
};
struct ClassC : ClassB
{
RTTI_IMPL_TYPEINFO(ClassC);
};
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 {}
Persistence
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)");
}
}
Game systems
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())");
}
Scripted classes
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);
});
Incomplete classes
Using RTTI_FWD_CONSTRUCTOR()
as in the previous example,
you can inherit partially dedcoded classes and delegate construction and destruction to RTTI system.
Alternative naming
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
}
Class extensions
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.
Raw native handlers
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);
};
Registration
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;
}
Accessing type info
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>();
Calling functions
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);
Accessing game systems
auto system = Red::GetGameSystem<Red::IPersistencySystem>();
auto status = system->GetEntityStatus(1ULL);
Printing to game log
const auto projectName = "MyMod";
Red::Log::Debug("Hello from {}", projectName);