scripting.md 30 KB

Scripting {#scripting}

[TOC]

In the previous chapter we talked about how to expose a C++ type to the scripting API by using the script binding generator tool. This tool ensures you can generate script bindings easily, but in some cases it is not robust enough and you must interact with the scripting API manually. This manual will explain how to interact with the scripting API and expose C++ code to it manually (without the script binding generator tool).

You can use manual generation to achieve everything as with the script binding generator tool (in fact this tool uses the same API as described in this manual), plus a lot more. It is preferred you use automatic generation whenever possible, but when working with lower level systems that closely interact with the scripting system (like serialization, script compilation, etc.) you need more direct access.

Mono {#scripting_a}

All C# script code is executed using the Mono runtime. Mono runtime allows you to communicate with C# code and vice-versa by invoking methods and querying class/method/field information. In this section we'll focus on how to interact with Mono (and therefore the C# runtime).

BansheeMono is a plugin that wraps the functionality of the Mono runtime. The main entry point of the scripting system is the @ref bs::MonoManager "MonoManager" class which allows you to start the runtime and load managed (script) assemblies. The most important method here is @ref bs::MonoManager::loadAssembly "MonoManager::loadAssembly()". It loads all the script code from the managed assembly (.dll) at the provided path, and provides meta-data for the entire assembly through the returned @ref bs::MonoAssembly "MonoAssembly" object.

MonoAssembly {#scripting_a_a}

MonoAssembly gives you access to all the script classes in an assembly. You can retrieve all clases using @ref bs::MonoAssembly::getAllClasses "MonoAssembly::getAllClasses()", or retrieve a specific one by calling @ref bs::MonoAssembly::getClass(const String&, const String&) const "MonoAssembly::getClass(const String& namespace, const String& typename)". Both of these methods return a @ref bs::MonoClass "MonoClass" object.

MonoClass {#scripting_a_b}

MonoClass gives you access to all methods, fields, properties and attributes of a specific class. It also allows you to register "internal" methods. These methods allow the managed code to call C++ code, and we'll go into them later.

Classes also allow you to create object instances of their type. Use @ref bs::MonoClass::createInstance "MonoClass::createInstance()" to create a new object instance. This method returns a MonoObject instance, which is a C++ representation of the C# object, but more on them later. When creating an instance you may choose whether to construct it or not, and to provide constructor signature if you need a specific one.

To retrieve a method from a class call @ref bs::MonoClass::getMethod() "MonoClass::getMethod()", accepting a name (without parameter types) and a number of parameters. If your method is overloaded you can use @ref bs::MonoClass::getMethodExact "MonoClass:getMethodExact()" which accepts a method name, and a comma separated list of parameter types. You may also use @ref bs::MonoClass::getAllMethods "MonoClass::getAllMethods()" to retrieve all methods in a class. All three of these methods return a @ref bs::MonoMethod "MonoMethod" object.

TODO - Example getting a method

MonoMethod {#scripting_a_c}

MonoMethod class provides information about about a managed method, as well as giving you multiple ways of invoking it (i.e. calling C# methods from C++).

To invoke a method you may use multiple approaches:

  • @ref bs::MonoMethod::invoke "MonoMethod::invoke()" - Accepts a MonoObject to invoke the method on (null if a static method), as well as an optional list of parameters. Invokes the exact method to exact type it was provided on.
  • @ref bs::MonoMethod::invokeVirtual "MonoMethod::invokeVirtual()" - Accepts a MonoObject to invoke the method on (null if a static method), as well as an optional list of parameters. Unlike MonoMethod::invoke() it calls the method polymorphically, meaning it determines the actual type of the provided managed object instance and calls an overriden method if available.
  • @ref bs::MonoMethod::getThunk "MonoMethod::getThunk()" - Returns a C++ function pointer accepting a MonoObject, zero or more parameters and an exception object. You can then call the function pointer like you would a C++ function. This is equivalent to MonoMethod::invoke() but is significantly faster. A helper method @ref bs::MonoUtil::invokeThunk "MonoUtil::invokeThunk()" is provided - it is suggested you use it instead of calling thunks manually because it handles exceptions internally.
  • TODO - Example invoking a method TODO - Thunk example

    We'll talk more about how to pass parameters to methods later.

    MonoField {#scripting_a_d}

    Similar to methods, field information can be retrieved from a MonoClass object by calling @ref bs::MonoClass::getField "MonoClass::getField()" or @ref bs::MonoClass::getAllFields "MonoClass::getAllFields()". The returned value is a @ref bs::MonoField "MonoField" which provides information about the field and allows you to retrieve and set values in the field using @ref bs::MonoField::getValue "MonoField::getValue()" / @ref bs::MonoField::setValue "MonoField::setValue()".

    TODO - Example getting/setting a field

    MonoProperty {#scripting_a_e}

    Properties are very similar to fields, retrieved from a MonoClass object by calling @ref bs::MonoClass::getProperty "MonoClass::getProperty()". The returned value is a @ref bs::MonoProperty "MonoProperty" which provides information about the property and allows you to retrieve and set values on it. The main difference is that properties in C# can be indexed (like arrays) and therefore a two set of set/get methods are provided, one accepting an index and other one not. It's up to the user to know which one to call. The methods are @ref bs::MonoProperty::get "MonoProperty::get()" / @ref bs::MonoProperty::set "MonoProperty::set()" and @ref bs::MonoProperty::getIndexed "MonoProperty::getIndexed()" / @ref bs::MonoProperty::setIndexed "MonoProperty::setIndexed()".

    TODO - Example getting/setting a property

    Attributes {#scripting_a_f}

    Attributes provide data about a class, method or field provided at runtime, which usually allows such objects to be specialized in some regard. Attributes don't have their own wrapper, because they are esentially normal managed objects and you can work with them as such.

    To retrieve a list of attributes from a class use @ref bs::MonoClass::getAllAttributes() "MonoClass::getAllAttributes()", which returns a list of MonoClass objects that identify the attribute types. To get the actual object instance of the attribute you may call @ref bs::MonoClass::getAttribute "MonoClass::getAttribute()" with the wanted attribute's MonoClass. After that you can call methods, work with field values and similar, same as you would with a normal managed object.

    Attributes can also be retrieved from a MonoMethod by using @ref bs::MonoMethod::getAttribute "MonoMethod::getAttribute()", or from MonoField by using @ref bs::MonoField::getAttribute "MonoField::getAttribute()".

    TODO - Stopped here

    Managed objects {#scripting_a_g}

    So far we have talked about invoking methods and retrieving field values, but we haven't yet explained how to access managed object instances in C++ code. All managed objects in Mono are represented by a MonoObject.

    For example, when calling a non-static method the first parameter provided to @ref bs::MonoMethod::invoke "MonoMethod::invoke" is a MonoObject pointer. Same goes for retrieving or setting values on fields, properties. Attributes are also a MonoObject.

    Mono also provides two more specialized types of managed objects: MonoArray for managed arrays, and MonoString for managed strings, but they are both still a MonoObject.

    Be aware that all managed objects are garbage collected. This means you should not keep a reference to them unless you are sure they are alive. Just having a pointer to a MonoObject will not keep the object alive and it may go out of scope as soon as the control returns to managed code. A good way to deal with this issue is:

    • Call a native method in the object's finalizer (~MyObject()) which will notify you when the object is no longer valid. Be aware that finalizer may be called after the object is unusable.
    • Require the user to manually destroy the object by calling a custom Destroy method or similar.
    • Force the garbage collector to keep the object alive by calling @ref bs::MonoUtil::newGCHandle "MonoUtil::newGCHandle" which will return a handle to the object. The handle will keep the object alive until you release it by calling @ref bs::MonoUtil::freeGCHandle "MonoUtil::freeGCHandle". Be aware if an assembly the object belongs to is unloaded all objects will be destroyed regardless of kept handles.

    Marshalling data {#scripting_a_h}

    Mono does not perform automatic marshalling of data when calling managed code from C++ (or vice versa). This is important when calling methods, retrieving/setting field/property values, and responding to calls from managed code, because you need to know in what format to expect the data.

    The rules are:

    • All primitive types are passed as is. e.g. an int in C# will be a 4 byte integer in C++, a float will be a float, a bool will be a bool.
    • All reference types (class in C#) are passed as a MonoObject*. Strings and arrays are handled specially, where strings are passed as MonoString*, and arrays as MonoArray*.
      • If a reference type parameter in a method in managed code is prefixed with an out modifier, then the received parameters are MonoObject**, MonoString**, MonoArray** and your method is expected to populate those values.
    • Structs (non-primitive value types, struct in C#) are provided as raw memory. Make sure that all structs in C# that require marshalling have a [StructLayout(LayoutKind.Sequential)] attribute, which ensures they have the same memory layout as C++ structs. This way you can just accept the raw C++ structure and read it with no additional conversion.
      • It is suggested you never pass structures by value, it is known to cause problems in Mono. Instead pass all structures by prefixing them with ref which will give you a pointer to the structure (e.g. MyStruct*). If you need to output a struct use the out modifier which you will give you a double pointer (e.g. MyStruct**).
      • In cases where it is not possible to avoid passing structures by value (e.g. when retrieving them from a field, use the @ref bs::MonoField::getValueBoxed "MonoField::getValueBoxed" method instead @ref bs::MonoField::getValue "MonoField::getValue", which will return a struct in the form of a MonoObject. You can then retrieve the raw struct value by calling @ref bs::MonoUtil::unbox "MonoUtil::unbox".
      • Everything above applies only when managed code is calling C++. When calling into managed code from C++, all structs need to be boxed (i.e. converted to MonoObject). Use @ref bs::MonoUtil::box "MonoUtil::box" to convert a C++ struct into a MonoObject*. See @ref bs::ScriptVector3 "ScriptVector3" for an example implementation.

    Banshee provides a helper code to assist with marshalling strings:

    • @ref bs::MonoUtil::monoToWString "MonoUtil::monoToWString" / @ref bs::MonoUtil::monoToString "MonoUtil::monoToString" - Converts a MonoString* to a native string
    • @ref bs::MonoUtil::wstringToMono "MonoUtil::wstringToMono" / @ref bs::MonoUtil::stringToMono "MonoUtil::stringToMono" - Converts a native string into a MonoString*

    @ref bs::ScriptArray "ScriptArray" is a helper class that allows you to construct new arrays and read managed arrays, easily.

    To create a new arrays call @ref bs::ScriptArray::create "ScriptArray::create". Type can be a primitive type like int, float, a native string or a Script* object (more about Script* objects later). You can then fill the array by calling @ref bs::ScriptArray::set "ScriptArray::set" and retrieve the managed MonoArray* by calling @ref bs::ScriptArray::getInternal "ScriptArray::getInternal".

    To more easily read existing arrays create a new @ref bs::ScriptArray "ScriptArray" by providing it with a MonoArray* in the constructor. Then you can easily retrieve the size of the array using @ref bs::ScriptArray::size() "ScriptArray::size()", and the value of its elements by calling @ref bs::ScriptArray::get "ScriptArray::get"".

    Internal methods {#scripting_a_i}

    So far we have talked about calling managed code, and retrieving information about managed types, but we have yet to show how managed code calls C++ code. This is accomplished using native methods.

    The first step is to define a stub method in managed code, like so:

    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern float Internal_GetSomeValue(MyObject obj);
    

    You then hook up this method with managed code by calling @ref bs::MonoClass::addInternalCall "MonoClass::addInternalCall". In this specific case it would be:

    myClass->addInternalCall("Internal_GetSomeValue", &myNativeFunction);
    

    Assuming myClass is a @ref bs::MonoClass "MonoClass" of the type that contains the stub method. After this call any call to the managed stub method will call the provided native function myNativeFunction. You should take care to properly handle parameter passing as described above.

    Take a look at @ref bs::ScriptGUISkin "ScriptGUISkin" implementation for a simple example of how exactly does this work.

    Script objects {#scripting_b}

    As you can see interaction between the two languages can get a bit cumbersome. For that reason Banshee implements a higher level system built on the functionality shown so far. It provides an universal interface all script objects must implement. It primarily ensures that native and managed code is always linked by keeping a pointer to each other's objects, as well as gracefully handling managed object destruction and handling assembly refresh (due to script hot-swap).

    When exposing a class to the scripting interface you need to add two things:

    • A native interop object (C++)
    • Managed wrapper for the class (C#)

    Native interop object ## {#scripting_b_a}

    All native interop objects implement the @ref bs::ScriptObject "ScriptObject" interface. A basic implementation of such an interface can be:

    class ScriptMyObject : public ScriptObject <ScriptMyObject>
    {
    public:
    	SCRIPT_OBJ(ENGINE_ASSEMBLY, "BansheeEngine", "MyObject")
    
    private:
    	ScriptMyObject(MonoObject* instance);
    	
    	static void internal_CreateInstance(MonoObject* obj);
    	static float internal_GetSomeValue(MonoObject* obj);
    };
    

    All @ref bs::ScriptObject "ScriptObjects" must begin with a @ref SCRIPT_OBJ macro. The macro accepts (in order):

    • the name of the assembly (.dll) the managed script object is in, this is either ENGINE_ASSEMBLY or EDITOR_ASSEMBLY
    • the namespace the type is in
    • the name of the managed type

    @ref SCRIPT_OBJ macro also defines a static initRuntimeData() method you need to implement (more on that later).

    When constructing a @ref bs::ScriptObject "ScriptObject" you must also provide a pointer to the managed object that wraps it (note the constructor). If the @ref bs::ScriptObject "ScriptObject" is used for a class that is static then the constructor is of no consequence as the @ref bs::ScriptObject "ScriptObject" itself never needs to be instantiated (all of its methods will be static).

    The two last method definitions in the example are called from C# (via an internal call, see the section about internal methods earlier).

    initRuntimeData ### {#scripting_b_a_a}

    initRuntimeData is a static method that every @ref bs::ScriptObject "ScriptObject" needs to implement. It takes care of hooking up managed internal methods to C++ functions. It gets called automatically whenever the assembly containing the related managed class is loaded.

    Every @ref bs::ScriptObject "ScriptObject" provides a static @ref bs::ScriptObject::getMetaData "metaData" structure you can use for retrieving the @ref bs::MonoClass "MonoClass" of the related managed class. You can use that @ref bs::MonoClass "MonoClass" to register internal methods to it (as described earlier). For example a basic initRuntimeData() might look like so:

    void ScriptMyObject::initRuntimeData()
    {
    	metaData.scriptClass->addInternalCall("Internal_CreateInstance", &ScriptFont::internal_CreateInstance);
    	metaData.scriptClass->addInternalCall("Internal_GetSomeValue", &ScriptFont::internal_GetSomeValue);
    }
    

    initRuntimeData is also a good spot to retrieve @ref bs::MonoMethod "MonoMethods" (or thunks) for managed methods that needed to be called by the script interop object, if any.

    Creating script object instances ### {#scripting_b_a_b}

    If your class is not static you will need to eventually create an instance of the script object. This can be done either from C# or C++, depending on what is needed. For example script interop objects for GUI will be created from managed code because user can add GUI elements himself, but a resource like @ref bs::Font "Font" will have its script interop object (and managed instance) created purely from C++ because such an object cannot be created directly in managed code.

    For the first case you should set up an internal method that accepts the managed object instance, and is called in the managed constructor (internal_CreateInstance in the above example). This way the method gets called whenever the managed object gets created and you can create the related script interop object. A simple implementation would look like so:

    void ScriptMyObject::internal_createInstance(MonoObject* obj)
    {
    	bs_new<ScriptMyObject>(obj);
    }
    

    Note that you don't actually need to store the created object anywhere. The @ref bs::ScriptObject "ScriptObject" constructor ensures that the pointer to the script interop object is stored in the managed object.

    For the second case where you want to create the interop object from C++ you can create a static create() method like so:

    MonoObject* ScriptMyObject::create()
    {
    	MonoObject* managedObj = metaData.scriptClass->createInstance();
    	bs_new<ScriptMyObject>(managedObj);
    	
    	return managedObj;
    }
    

    In this case the method calls a parameterless constructor but you may specify parameters as needed.

    If you have a MonoObject* but need to retrieve its @ref bs::ScriptObject "ScriptObject" use @ref bs::ScriptObject::toNative(MonoObject) "toNative(MonoObject)" static method. Within the interop object instance you can use @ref bs::ScriptObjectBase::getManagedInstance() "ScriptObject::getManagedInstance()" to retrieve the managed object.

    Destroying script object instances ### {#scripting_b_a_c}

    When the managed object is destroyed (e.g. goes out of scope and gets garbage collected) the system will automatically take care of freeing the related ScriptObject. If you need to add onto or replace that functionality you can override @ref bs::ScriptObjectBase::_onManagedInstanceDeleted "ScriptObject::_onManagedInstanceDeleted" method.

    Managed wrapper object ## {#scripting_b_b}

    Creating the script interop object is one half of the job done. You also need to create the managed counterpart, however that is significantly simpler.

    Every managed script object must implement the ScriptObject interface. For example a C# version of the class we're using in this example would look like:

    namespace BansheeEngine
    {
    	public class MyObject : ScriptObject
    	{
    		public MyObject()
    		{
    			Internal_CreateInstance(this)
    		}
    		
    		public float SomeValue
    		{
    			get { return Internal_GetSomeValue(this); }
    		}
    		
    		[MethodImpl(MethodImplOptions.InternalCall)]
    		private static extern void Internal_CreateInstance(MyObject obj);
    		
    		[MethodImpl(MethodImplOptions.InternalCall)]
    		private static extern float Internal_GetSomeValue(MyObject obj);
    	}
    }
    

    That's all that needs to be done. You can now create the object in C# and use its property to retrieve the value from C++ code. All managed ScriptObjects provide a GetCachedPtr method which returns an IntPtr which points to the script interop object described in previous sections.

    Assembly refresh

    What has been shown so far is enough to create a class exposed to script, however object of such a class will not survive assembly refresh. Assembly refresh happens when scripts are recompiled and managed assemblies are unloaded and reloaded. This is something that generally happens only in the editor.

    If you don't care about your object surviving the refresh, you do not need to implement what is described here. For example GUI elements don't persist refresh, because they're just rebuilt from the managed code every time the refresh happens. However objects like resources, scene objects and components are persistent - we don't wish to reload the entire scene and all resources every time assembly refresh happens.

    A persistent script object need to inherit a variation of @ref bs::ScriptObject "ScriptObject" like so:

    class MyScriptObject : public ScriptObject<MyScriptObject, PersistentScriptObjectBase>
    

    This ensures that your object is treated properly during assembly refresh. Persistent object then needs to handle four different actions, represented by overrideable methods. These methods are called in order specified, during assembly refresh.

    • @ref bs::ScriptObjectBase::beginRefresh() "ScriptObject::beginRefresh()" - Called just before the refresh starts. The object is still alive here and you can perform needed actions (e.g. saving managed object's contents).
    • @ref bs::ScriptObjectBase::_onManagedInstanceDeleted "ScriptObject::_onManagedInstanceDeleted()" - Called after assembly unload happened and the managed object was destroyed. You should override this to prevent the @ref bs::ScriptObject "ScriptObject" itself from being deleted if the assembly refresh is in progress. If assembly refresh is not in progress this method should delete the @ref bs::ScriptObject "ScriptObject" as normal because it likely got called due to standard reasons (managed object went out of scope).
    • @ref bs::ScriptObject::_createManagedInstance "ScriptObject::_createManagedInstance()" - Creates the managed instance after new assemblies are loaded. You should override this if your managed class is constructed using a constructor with parameters. By default this will call @ref bs::MonoClass::createInstance "MonoClass::createInstance()" using the parameterless constructor.
    • @ref bs::ScriptObjectBase::endRefresh() "ScriptObject::endRefresh()" - Called after all assemblies are loaded, and after all script interop objects were either destroyed (non-persistent) or had their managed instances created (persistent). If you stored any data during @ref bs::ScriptObjectBase::beginRefresh() "ScriptObject::beginRefresh()", you should restore it here.

    See @ref bs::ScriptSceneObject "ScriptSceneObject" and its base class @ref bs::ScriptGameObjectBase "ScriptGameObjectBase" for example implementations of these methods.

    Deriving from ScriptObject

    Sometimes script objects are polymorphic. For example a GUIElement is derived from ScriptObject in managed code, and GUIButton is derived from GUIElement, however they both have script interop objects of their own.

    Due to the nature of how our script interop objects are defined we cannot follow the same simple chain of inheritance in C++ code. For example class definition script interop object for GUIElement would be:

    class ScriptGUIElement : public ScriptObject<ScriptGUIElement>
    {
    public:
    	SCRIPT_OBJ(ENGINE_ASSEMBLY, "BansheeEngine", "GUIElement")
    ...
    }
    

    But what would it be for GUIButton? It also needs to implement @ref bs::ScriptObject "ScriptObject" with its own @ref SCRIPT_OBJ macro so we cannot just inherit from ScriptGUIElement directly as it would clash.

    The solution is to create a third class that will serve as a base for both. This third class will be a base class for @ref bs::ScriptObject "ScriptObject" (its second template parameter allows us to override its default ScriptObjectBase base class). The third class will need to inherit @ref bs::ScriptObjectBase "ScriptObjectBase" and can implement any functionality common to all GUI elements (e.g. it might store a pointer to a native GUIElement*).

    Then we can define script interop object for GUI element as:

    class ScriptGUIElement : public ScriptObject<ScriptGUIElement, ScriptGUIElementBase>
    

    Where ScriptGUIElementBase is our third (common) class. Interop object for GUIButton would then be:

    class ScriptGUIButton : public ScriptObject<ScriptGUIButton, ScriptGUIElementBase>
    

    This ensures that all GUI elements can now be accessed through the common ScriptGUIElementBase interface. Which is important if GUIElement provides some internal method calls shared between all GUI element types, otherwise we wouldn't know what to cast the interop object held by its managed object to.

    See @ref bs::ScriptGUIElement "ScriptGUIElement" and @ref bs::ScriptGUIButton "ScriptGUIButton" for an example.

    Exposing resources to scripting {#resources_c_a}

    Once you have a custom resource you might want to expose it to the scripting API. Take a look at the scripting interop guide to learn how to create scripting API objects.

    Every resource scripting interop object must implement @ref bs::TScriptResource "TScriptResource", but other than that creating GUI interop objects is the same as the general case described in the guide above. See @ref bs::ScriptFont "ScriptFont" in BsScriptFont.cpp for an example.

    Creating managed GUI objects is again the same as the general case. Take a look at the managed Font implementation in Font.cs for an example.

    Resources are unlike most other scripting objects because they can be created in the native code and then used in the managed code, or vice versa. Resources are also serialized by reference, which requires some specialized code for managed serialization to properly recognize the resource.

    Native <-> managed resource conversion {#resources_c_a_a}

    When a resource is created in native code, but needs to be passed to scripting code, the system creates a script interop object and a managed object, which wrap the native resource. The system also needs to be aware if such managed representation of an object already exists, and return an existing one if so. This is the job of the @ref bs::ScriptResourceManager "ScriptResourceManager".

    You will need to update this manager with your custom resource type. Relevant methods are:

    • @ref bs::ScriptResourceManager::createScriptResource "ScriptResourceManager::createScriptResource" - Creates a new managed object instance from a native resource (only valid if one doesn't already exist). This method has two overloads, and is specialized for specific resource types. Make sure to add a specialization for both overloads, with your custom resource type. And also update the specialization that accepts a generic @ref bs::ResourceHandleBase "HResource" with an entry of your own resource. You can use existing resources as an example of how it should be done, only the type names need changing.
    • @ref bs::ScriptResourceManager::getScriptResource "ScriptResourceManager::getScriptResource" - Retrieves an existing managed object instance from a native resource handle. Optionally creates the managed object if one doesn't already exist. Add a specialization for your own resource type similar to methods above.
    • Managed resource serialization {#resources_c_a_b}

      In order for your new resource type to be properly referenced by serialized managed objects you need to make a few more additions:

      • First of append a new resource type to the @ref bs::ScriptReferenceType "ScriptReferenceType" enum.
      • Append a new entry to @ref bs::ManagedSerializableTypeInfoRef::getMonoClass "ManagedSerializableTypeInfoRef::getMonoClass" which maps the enum entry above to your resource script interop object type
      • Append a new entry to @ref bs::ManagedSerializableTypeInfoRef::isTypeLoaded "ManagedSerializableTypeInfoRef::isTypeLoaded" so it always returns true if your resource is encountered (since it's built-in, it's always loaded)
      • Append a new entry in the @ref bs::ScriptAssemblyManager::getTypeInfo "ScriptAssemblyManager::getTypeInfo" which maps the managed resource object type to the enum above
      • Append a new entry to the getResourceFieldLookup method in BsManagedSerializableField.cpp which uses the native <-> managed conversion methods to map between a native and a managed instance of a resource during serialization/deserialization

      For all of these you can use examples of existing resources which already exist within the mentioned methods, it is just a matter of changing the type/enum names for your own resource.

      Other {#resources_c_a_c}

      In order for your resources to be properly recognized by GUI drop fields, as well as the project library append new entries to @ref bs::ScriptResource::getClassFromTypeId "ScriptResource::getClassFromTypeId" and @ref bs::ScriptResource::getTypeFromTypeId "ScriptResource::getTypeFromTypeId" which map between the enum above and the RTTI type ID of the resource.