Adventures In Scripting 1
#1 posted by Preach on 2010/04/12 23:59:16
Hey Preach, what's up with the thread?
Well, if you click the link in the top post, you can find a link to download fitzsc.zip. The zip file contains a map and an engine, so put the bsp in id1/maps and the engine in your root quake folder. Then run the engine and load the map from it. The map contains a number of features hazardous to unattended toddlers, and a healing pool which restores you to 50 hp when you're badly hurt (but won't heal you more than 5 times). Have a quick playaround to check it does what it says on the tin.
So it's a...healing pool. The original quake maps had one of them!
Yeah, but at least the mechanics changed a bit with this one... I'm sure that quite a few of you are unimpressed, and have already worked out how you'd code this simple healing trigger in your head. You probably came up with something like:
if(other.health <50 && self.heal_count < 5)
{
��other.health = 50;
��self.heal_count = self.heal_count + 1;
}
That's fair enough, and if you extract the entities from the map, you'll find that the trigger for the pool has essentially that block of code in it's "touch" field. If you're thinking that Preach has integrated a JIT qc compiler into his new engine, then you're not far wrong. But it isn't QuakeC. It's Javascript.
Javascript? splutters But why?
Well, I was actually quite impressed by Javascript's more advanced features like closures and anonymous functions. It's also very similar in syntax and organisation to QuakeC - not just that some code work the same in both languages. Javascript has a small set of basic types like Number and String, along with one "extensible" type called the Object. It even defines "classes" of object in terms of an initialiser function, roughly speaking. Lastly, Google have written a wonderful Javascript engine called V8 which is very easy to plug into other projects, which was a bit less work than rolling my own.
Okay, I wanna have a play with it, what can I do?
Well, the first thing to say is that this is in no way complete! It's at this stage about reached the bare minimum level where it's worth sharing. There are likely bugs in there, and please share them if you find them.
To get a script into the engine, simply type it into any field of an entity with "function" type, for example "think", "touch", "blocked", "th_die", "th_pain". Treat it like entity hacking, except you can define what the function does too. The way this works in the engine is based on the entity parsing system. Previously when you set these fields, the engine searches the list for a function with a matching name, and reports an error if a match is not found. Now if a match is not found, the engine assumes you were trying to define a script, and attempts to compile it instead.
What can you do with scripts?
Well, you can access and modify a limited subset of the quakec fields. All floats and entities, both global variables and fields of entities can be read and set from JS. In addition, string globals and fields can be read, but not written to. This is for technical reasons I'm hoping to get around in the future, especially since JS has much better string manipulation than QuakeC does. For now, read only allows you to do things like
if(other.classname == "player")
This doesn't mean that strings taken from QC are always immutable JS objects, you should be able to do
var s = other.classname;
var t = s.slice(0,6);
if(t == "monster")
��//do stuff
Vectors are something I intend to give more attention to later. At the moment, the vectors themselves are not available in the JS sandbox, but the individual float components like origin_x are imported. So you can do vector maths the long way if you need to
Functions are not available in JS yet, which is actually one of the most pressing omissions, because it means you can't directly trigger "use" events from your scripts. I'm sure clever hackers could come up with a workaround though. There are a few more behind the scenes things that need to happen before functions are available. This includes calling functions are well as modifying function pointers like "think" or "use".
Hey, Preach, I think I see where you're going with this...
Well, don't spoil the surprise for everyone else then!
Note:
#2 posted by metlslime on 2010/04/13 00:04:16
I think the maximum key length is a quake map is 1024 chars, so your scripts can't become giant.
Also: wow, this is pretty cool. Not sure if it makes sense to add another scripting language to quake (since quakec is effectively a scripting language) but short of adding a quakec interpreter or something, this is a good alternative.
Max Key Lengths..
#3 posted by rj on 2010/04/13 00:16:52
i actually thought it was 128.. i sometimes break that with the 'wad' field in worldspawn (as it lists all the wads i have in worldcraft including paths) and have to take some unused wads out or merge a couple of them. i even moved them all to c:\t\ and gave them all really short names to help prevent it.
maybe it was only like that with an old engine/compiler? can't remember if it was a quake or qbsp error now..
this sounds interesting anyway, not sure what the 'surprise' is but look forward to it :)
Rj:
#4 posted by metlslime on 2010/04/13 00:25:11
some compilers had smaller limits i think, but the official max is 1024 if i recall correctly.
maybe i should investigate this...
Size Limits And Access
#5 posted by Preach on 2010/04/13 00:37:01
Yeah, the limit from the engine is 1024 although I'm pretty sure that's only limited by the buffer size in the tokeniser. I did mean to mention that but it slipped my mind.
I have been thinking about whether this is the best way to produce scripts. There would be advantages to reading them from an external file too - it's easier to edit them on the fly than recompiling a map, and easier to read them there too. It seems a bit crazy to be including a new file every time you need a one-line script, so maybe both are needed.
#6 posted by metlslime on 2010/04/13 00:47:33
It seems a bit crazy to be including a new file every time you need a one-line script, so maybe both are needed.
could just have one file per bsp, and define all the functions you want in that file. Then, the entity can be set up with "touch" "myfunction"
Or, you could just search all files in a given directory for the named function, leaving the developer to decide how many script files he wants to maintain. (one huge vs. many small)
Per Bsp File
#7 posted by Preach on 2010/04/13 01:03:19
I think I'd prefer to not search all files, to avoid the risk of collisions. Once you have this kind of scripting mappers don't need to create whole new mods for small tweaks. You lose a bit of the benefit of that if people need to keep their maps in separate folders to avoid interference.
Code reuse isn't high priority for scripting anyway, so it's fair to make people cut and paste for new maps. It would be easiest if the on-script-file's scripts were all just made named functions in the JS "context". That way you'd set them with "touch" "myfunction()" - defining an anonymous function which calls "myfunction". That sounds a bit convoluted, but it leaves the name resolution up to the JS engine, rather than having to hack it into the QC recognition. It would also leave the option for short scripts to be embedded into the map, which I think is cute at least.
Also, for the foolhardy who want longer scripts now: use the eval() function on unused string fields on your entity, concatenated together! What could go wrong?
#8 posted by necros on 2010/04/13 01:07:11
this is wild! o.o
but definitely put the scripts in an external file (preferably like idtech4 with a mapname.script file or something simple like that), then simply putting the script function's name into whatever function slot you want on your entity. no way would i type a complex script into an entity field.
Code Reuse Isn't High Priority For Scripting Anyway
#9 posted by megaman on 2010/04/13 13:01:03
Ha !
#10 posted by roblot on 2010/04/13 18:21:14
Take a look at preach's wad line in his js01.bsp
#11 posted by necros on 2010/04/13 19:48:58
looks like hammer has it's own limit on max key field characters?
The Wadfather
#12 posted by Preach on 2010/04/13 19:51:20
Yeah, it's just a pain to keep removing and adding them in hammer - especially since you need to restart before they're actually removed.
To add some useful content, I thought I'd explain a bit more about JS Objects, how they relate to entities in this engine, and type-safety stuff.
One of the key differences between Javascript and QuakeC is that Javascript is dynamically typed. This means that if you declare a variable like heal_count, you don't declare what kind of data it can hold. If you assign a Number to it then you can use it in calculations. Later on you could assign a String to heal_count, and this would not cause an error. If a function is expecting a particular kind of data and receives another one, then it may try to coerce the data, for example automatically converting a Number into a String.
There is a potential problem here. What if we start assigning the wrong types in the JS context to variables that QC (or worse the engine itself) will need to read? This would be bad, so we prevent it. Whenever you attempt to get or set a variable in JS which was imported from QC, a special c++(*) function handles the conversion, and does not modify the underlying variable if the conversion fails. Similarly, all world fields are read only, in the same way that they are in QC - although here the failure is silent rather than game-ending. I am hoping to add JS exceptions for these conditions in the future.
The same kind of protection is afforded to entities themselves. Although every QC entity is represented by a corresponding JS Object, this doesn't mean that every JS Object has a matching entity created. In fact, you would need to use the standard QC builtin to create a new entity (except that I haven't coded functions yet). This does mean that you are free to create new Objects in JS if it will help your script - did I mention that JS supports arrays and maps?
Since the entities are Objects, you can also add new fields to them. The example map actually demonstrates this - heal_count is not an unused QC value but an entirely new field. JS does not add these properties globally to all Objects or even all entities. Instead heal_count is only found on this one entity. Of course, you cannot access these new fields from QC.
*Yes, the interface to the V8 engine is written in c++, adding a 4th language to the equation. Not for the faint-hearted...
|