Wiki

Clone wiki

karhu / Scripting

Karhu uses AngelScript for scripting. See the official documentation for a general overview. These files are considered resources and go in the res/ subdirectory of the project folder rather than the src/ subdirectory where C++ files go. Editor scripts go in the editor/ subdirectory of the project folder.

Syntax highlighting and code completion

The Karhu editor has no interface for working with scripts as dedicated apps are better suited for this. Direct AS support is hard to come by, but thanks to the similarities a C++ setup can be bent to work. Unfortunately a bit ugly in all cases.

Sublime Text

Syntax highlighting

Install the Sublime package AngelScript for syntax highlighting or use this custom setup. Open an .as file, choose View > Syntax > (User >) AngelScript from Sublime's menu, or View > Syntax > Open all with current extension as… > (User >) AngelScript to make the association permanent (User > if you put the custom file there).

Code completion

Only tested on Mac; uses a Bash script that may or may not work on Linux; will not work on Windows without being translated into Batch or maybe through WSL.

Install the Sublime package EasyClangComplete. Make sure Clang and CMake are installed for it to work.

Our magic script needs for the command line utilities perl, pcregrep as well as clang++ itself to be globally installed.

Karhu comes with a sublime project setup. If you don't have one, choose Project > Save Project As… from the menu and save it as scripts.sublime-project, then replace its contents with the following (note that it also includes GLSL shader files):

https://bitbucket.org/avaskoog/karhu/src/master/templates/project/game.sublime-project

This sets the package up to work with a bit of magic that deals with discrepancies between AS and C++. Make sure your $KARHU_HOME environment variable is set.

Karhu projects also contain the necessary CMakeLists.txt in the root project folder, with the following contents:

https://bitbucket.org/avaskoog/karhu/src/master/templates/project/CMakeLists.txt

Make sure generated/scriptsymbols.inc exists in your Karhu project folder. This is generated by the Karhu editor:

karhu_genscr-kopi.png

For the generation code, see https://bitbucket.org/avaskoog/karhu/src/master/src/karhu/app/scriptutil.cpp

Pretty cool!

Skjermbilde 2021-06-07 kl. 01.52.14-kopi.png

Skjermbilde 2021-06-07 kl. 01.51.01-kopi.png

To bear in mind

Our magic script and include file deal with most of the differences between C++ and AS but Other than that the syntax and grammar needs to be valid C++ and AS at the same time; you're editing using a C++ parser, and the code still needs to run as AS in the engine.

  • Does not currently handle AS properties at all; would have to be done in the magic script by finding declarations and turning them into valid C++ ones.

  • The keywords in, out and inout are removed altogether so the autocomplete cannot display these qualifiers in signatures; unfortunately Clang has nothing like MSVC's SAL annotations which actually mirror these keywords, or it might have been possible to redefine these AS keywords as such. Hope to figure something out in the future!

  • You must end class and enum declarations with a semicolon after the closing bracket, e.g. class A {}; which is required in C++ and optional but luckily possible in AS.

  • Default initialised class members cannot use the constructor syntax like private A a(1); due to "the most vexing parse" and braces {} not being an option in AS, so the syntax must be private A a = A(1);. The compact syntax does still work in function bodies.

  • List initialisation always requires the = in AS and is possible in C++ so std::vector<int> v = {1, 2, 3}; is the only valid syntax also within a function.

  • Attributes need double braces, e.g. [[serialise]]; see below.

  • AS only supports the const keyword on the left (a.k.a. "west const").

  • Scoped enums do not support implicit conversion to int in C++, so you must cast even though AS supports it if you want this setup to work, since Karhu enforces scoped enums in AS.

  • When you do cast non-handle types you must also use the T(value) rather than the (T)value syntax as this is the only one AS supports.

  • While not necessary in AS, C++ requires this be used to access dependent names (e.g. members and methods of the class itself) in templates (AS mixins get turned into templates to work with the C++ compiler), and AS still allows it, so it must be done when using this setup.

  • AS allows use of symbols before their declaration as long as they do eventually get declared in the module, but C++ requires predeclaration. The magic script is able to find and predeclare classes, enums, typedefs and funcdefs but not free functions (this is much trickier; see below), so you can use any of the former kinds before their declaration but not free functions.

Magic include

If you're reading this to set AS up with a different engine, the basic setup without the API specific to Karhu may still be of use to you:

template<template<typename> class T, typename U> U &_this_(T<U> *);
template<typename T> T &_this_(T *);
template<template<typename> class T, typename U> U const &_this_(T<U> const *);
template<typename T> T const &_this_(T const *);

template<typename T, typename U> T cast(U);

struct null
{
    template<typename T> operator T() const;
    template<typename T> friend bool operator==(T, null);
    template<typename T> friend bool operator==(null, T);
    template<typename T> friend bool operator!=(T, null);
    template<typename T> friend bool operator!=(null, T);
} null;

using int8 = signed char;
using int16 = signed short;
using int32 = int;
using int64 = long;
using uint8 = unsigned char;
using uint16 = unsigned short;
using uint32 = unsigned int;
using uint = unsigned int;
using uint64 = unsigned long;

#define in [[annotate("in")]]
#define out [[annotate("out")]]
#define inout [[annotate("inout")]]
#define private [[annotate("private")]]
#define protected [[annotate("protected")]]
#define external
#define shared
#define abstract
#define override
#define property
#define final
#define is ==
#define interface struct
#define class struct
#define enum enum class
#define funcdef typedef
#define function []
#define mixin template<typename>
#define this (_this_(this))

Note that enum is defined as enum class because Karhu sets AS enums to be scoped; if your engine does not, remove this define.

Attributes

If your engine supports attributes using the AS standard script builder add-on you will have to make a decision for yourself: either treat every attribute as if it were actually named "[attribute]" rather than just "attribute" and just use the double braces that C++ attributes need (e.g. write [[attribute]] in your AS code) or modify the magic script to deal with single-brace attributes.

Unfortunately I don't know of a way to make complex attributes like [a = b] work since this is not, to my knowledge, possible in C++.

If you have -Werror enabled (see comment in Sublime project file) you will get errors for unknown attributes. This is fixed by suppressing the warning:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunknown-attributes"

AS standard library types

If you've registered the AS standard library ref type add this:

struct ref
{
    ref();
    template<typename T> ref(T);
};

If you want to use the AS standard library array<T> type and want to support list initialisation you will need to #include <initializer_list> at the top of the file before any of the destructive #define statements and then you can declare your interface using std::initializer_list like normal. The header brings in a tiny amount of STL symbols that might be annoying to have show up in your AS autocomplete where these aren't actually available, but it's the only option as far as I can tell.

Custom API

Anything specific to your engine or game need only be declared, not defined. Autogeneration based on the types registered in the AS engine recommended. Remember that you're writing C++ and not AS, but if you place declarations after the preprocessor defines they still apply although any changes requiring the magic script (such as removing @) do not (the magic header doesn't pass through it), so you for example can use inout and AS primitives like uint but you cannot use @.

Magic script

The other half of the puzzle is a Bash script EasyClangComplete is set up to call as if it were Clang; this allows us to intercept the build process and modify the (temporary duplicate) file before it gets compiled and then call the real Clang, passing its output and return value right through. Beware regex monstrosities!

You can name it whatever you want (Karhu calls it clangproxy) but remember to set the executable flag (chmod +x clangproxy) and to put the path to it in your Sublime project settings.

Source: https://bitbucket.org/avaskoog/karhu/src/master/tools/scripts/unix/clangproxy

How it works

The following changes are made to the source code before compilation make it work:

  • Keywords that are meaningless in C++ like shared or inout are completely removed.

  • Words like final and override while they exist in C++ differ too much syntactically in AS so are also removed. The same goes for private and protected.

  • Both class and interface are changed with #define to struct because AS classes are public by default (private in C++) and interfaces don't exist at all in C++.

  • AS primitives like uint are added as aliases for the corresponding C++ types.

  • AS is and !is are replaced with == and != and a null type that emulates the necessary AS behaviour is declared in the magic include file.

  • AS handle token @ is removed altogether as handles need to act like value types in C++ to be able to use . rather than -> for member access to match AS, and they cannot be references or will not work in all contexts that handles do in AS as references in C++ are not reassignable but handles in AS are. If declared as handle to const, the const keyword is removed since the variable needs to remain mutable in C++; any const after @ is however left which properly makes it immutable in C++ just like the AS handle should be since the keyword is allowed to occur on the right (a.k.a. "east const") in C++. I.e. const T @ turns into T while T @const turns into T const. Of course this means you won't get an error on trying to use non-const members with . but it also means you don't get an error when you try to reassign a handle that should be mutable even if the pointee is const. Not sure there's an easy fix.

  • AS &out and &inout qualifiers are removed since they allow for default parameters which non-const references in C++ do not.

  • The operator % is replaced with / since AS allows it to be used between floats but C++ doesn't, and ** (AS pow operator) with * since C++ lacks it completely.

  • AS mixin is replaced with template<typename> so that Clang won't error on member access until instantiation; any derived classes are found by the magic script using regex and <T> is added to the mixin class name, where T is the name of the derived class itself, thus making use of C++ CRTP to emulate the same functionality.

  • C++ template classes need the this keyword to access dependent names and AS allows it in mixins, so this setup requires it be used; to make it work and actually refer to the derived class type rather than the mixin itself, while still working in non-mixin contexts, a template hack is set up in the magic include file and a #define is used to replace this with the hack, while also converting the pointer to a reference since AS lacks -> that this (normally a pointer) needs in regular C++.

  • Since AS allows types to be used before they are declared in the same module, but C++ translation units do not, the magic script extracts all non-mixin class declarations as well as typedefs and funcdefs and generates a file with predeclarations that gets included (rather than prepended, as this would confuse Sublime with regards to line numbers). Note that this does not hold true for free functions because they're hard to separate out (and to associate with the correct namespaces) using regex alone, so code will simply have to be written around this for now unless this is fixed one way or another.

  • AS funcdef luckily works simply by being replaced by typedef but needs (*) added around the name to turn it into a typedef of a function pointer, which the above step also handles.

  • AS anonymous function function keyword is replaced with a C++ lambda [] declaration; the syntax otherwise adds up, but AS allows parameter types to be omitted while C++(14) requires the auto keyword, so the magic script adds them as necessary using regex.

  • AS cast is declared in the magic include file as a template function.

Updated