A lot of what other toolkits have going for them is the capability of reflection. RTTI is useful; ENIGMA already has it to some extent by virtue of having object_index. While further reflection is, to some extent, planned, I do not want it to be a heavy emphasis in the engine.
I notice that a lot of what people try to do with reflection should have been done by their environment at compile time. Like listing classes that have <x>, or do <y>. The compiler does shitloads of this while compiling games; without proper compile-time reflection, there would be no way to translate EDL to C++. The issue is, the compiler's behavior is really static, and the compiler is the only entity with this capability.
Thus, I'm considering a mechanism I'm calling compile scripts. In this context, they are scripts of EDL run by the compiler which offer full reflection of all game entities. Scripts, objects, rooms, paths, timelines... If it's in the game, you can iterate it with the compiler. Having this mechanism would allow users to define functions which would otherwise require runtime reflection, but generally should not. Game Maker, for the part, provides a lot of functions to offer reflection in a somewhat efficient manner. For example, sprite_get_name(). What it lacks is a sprite_get_first() and sprite_get_next(). I am
not proposing we add those directly. While they might be convenient for some games, they just waste space in others. I'm proposing a system that would allow users to define them.
global map item_sprites = [];
foreach (sprites as sprite) {
if (sprite.name.starts_with("spr_item_")) {
item_sprites.define(sprite.name, sprite.id);
}
}
Coupled with JDI, EDL's AST incorporates everything that is required to interpret that code, except the string and map object representations, which should be easy. Meanwhile, behind the scenes,
jdi::value cs_map_define(cs_map* cs_this, jdi::AST_Node_Parameters *anp, jdi::error_handler *herr) {
if (anp.params.length() != 2)
return (herr->error("Expected two parameters to map::define"), value());
cs_this->definitions.push_back(value_pair(anp->params[0].eval(), anp->params[1].eval()));
}
compile_scripts::objects["map"].functions["define"] = cs_map_define;
The above code is approximate, as it doesn't bail on the first error, and assumes that cs_this can be cs_map rather than a generic cs_object.In other words, the define() operation only adds a key to be defined to some vector in the map object; the actual work is done elsewhere.
- At syntax check time, it is up to ENIGMA to define that item_sprites is a global variable, if it is intended to be used in user code. If it is only intended to be used in library code, eg, in our definitions resource, we leave it out of our syntax checking definitions. I think this would be the preferable behavior.
- At compile time, the cs_map object is asked to generate code to ensure that the item_sprites object is what the library implementer thinks it is.
So, in the C++ module, the map object does this:
string obj_map_cpp::get_code() {
string res;
for (value_pair_vector::iterator it = definitions.begin(); it != definitions.end(); ++it)
res += " item_sprites[" + it->second->first.toString() + "] = " + it->second->second.toString() + ";\n";
return res;
}
In the javascript model, it does this:
string obj_map_cpp::get_code() {
string res = " item_sprites = {\n";
for (value_pair_vector::iterator it = definitions.begin(); it != definitions.end(); ++it)
res += " \"" + it->second->first.toString() + "\" : " + it->second->second.toString() + ",\n";
return res + " }\n";
}
Optimizations will be made in the C++ version to ensure that all keys have the same type (all VT_INTEGER or VT_DOUBLE, or all VT_STRING), and the correct jdi::value cast will be used. This is because all keys are string in JavaScript, and the benefit of ensured compatibility outweighs the benefit of letting users mix types in a map. Use two maps.
That said, to implement such a compiler extension, the user will give something similar to the pseudo-EDL above, coupled with a regular Definitions resource which can use its results. The pseudo-EDL is the compile script, and is interpreted by the compiler. The Definitions resource is a script which defines functions run during the game.
Speaking of the Definitions resource: I want Definitions to allow scripting in each native language, as well as in EDL. It is looking as though EDL could easily be made to compile to any of the even vaguely planned backend languages, some of which might lead to it being a popular prototyping language outside the game design space. EDL could be a "write-once, compile in any of the languages that claim to be write-once, compile anywhere" language, which would make it even more useful to the sort of people who use GML for quick prototyping.
So, an example Definitions resource to go with the above psuedo-code would be this:
int item_get_sprite(string itemname) {
return item_sprite[itemname];
}
This allows the other EDL code in the game to be oblivious to the existence of that map. Equivalently, this should be able to be accomplished using the new EDL spec, still in progress, simply by adding the word "function" to the beginning.
The purpose of this topic is not for more stargazing, but rather for everyone else to present capabilities they think will be needed by these scripts. If you have an idea, shoot.