(info) This AI is still in early stages of development.

Events API / backports

Backporting the eventGameSaving, eventGameSaved and eventGameLoaded events to WZ 3.1...

 

Notes

This one will hopefully come to fruition quite soon...

Due to the way EggPlant works, it doesn't like storing stuff on global scope - basically it's data structures are too elaborate to try and make them compatible with the savegame format used by WZ.

As such, the new events coming in WZ 3.2 will allow EP to generate temporary variables prior to saving, and clear them up after saving. Then, when a game is loaded, it can import the basic data it provided for the savegame and more quickly get it's internal data rebuilt.

eventGameSaving()

To backport this should be relatively straightforward - just create a global getter/setter property very soon after the JS environment instantiates (like first few lines of code in eggplant.js) and have the getter trigger eventGameSaving() (if global function is defined).

WZ obviously has to get the values of all the global vars and assuming it doesn't bother checking to see if they are data or accessor properties, it should treat my accessor as if it were a normal var. When it tries to get the value, it triggers my getter function.

The getter function first triggers eventGameSaving(), then queues a call to "eventGameSaved" (if defined), then returns now.

Events are disabled during save cycle so I can be sure that eventGameSaved() will only get called after the save is complete.

eventGameSaved()

So, I lied a little in the paragraphs above. Instead of queuing a call to "eventGameSaved", I'd have it trigger some other function instead.

That other function needs to work out if we've just saved the game or if we've just loaded it. The queued call will persist in the savegame and be restored, and thus triggered, after that game is loaded. If the game's just loaded, I don't want eventGameSaved() triggering because that will make eggplant delete all the vars it created before it gets chance to import them!

But, that queued event won't fire until a loaded game has had it's scripts instantiate. So, the script that creates the getter/setter var (used to detect eventGameSaving) will spot a global var already existing, it's value being when the game was saved (expressed in ms since 1st Jan 1970). It either uses the presence of that var, or it's value, to work out that we've just loaded a game (tada! there's eventGameLoaded sorted!).

If it works out that game has been loaded, it does a removeTimer() to kill off the queued function call, thus preventing eventGameSaved() being triggered after loading a game.

eventGameLoaded()

See above for how it will work.

I've already got this event implemented in Process API as part of the 'timeRemaining' global, but want to move it out – needing a big API like Process just to backport some events seems somewhat excessive!

If I'm going to move it out of Process and in to a backport script, I might as well refactor the way timeRemaining works while I'm at it.

timeRemaining and eventGameTick()

The timeRemaining invalidates every game tick. For it to be as accurate as possible, it needs to be invalidated as soon as any event is fired, because that marks the start of the processing slot for the script.

I've already got the standard events covered (and can easily implement in backported events), but custom events such as setTimer()queue() and bind() are a different matter altogether. I can't assume everything will live in a Process, so there's always going to be some code using custom events directly.

To solve that problem, I'm thinking of wrapping the global JS API functions (setTimer, etc) with some of my own code. When they are invoked, I use the function name parameter to grab a copy of the global function, wrap it in a closure that voids timeRemaining and then replaces the old function on global with my new wrapped version of it. Of course, I'll need to check if the function isn't already wrapped before wrapping it, but that's trivial to do.

This approach will mean that no matter what anyone does with the scripts, so long as the key bits of APIs and backports are included right near the start (like immediately after Util.js) all events - standard or custom - will void timeRemaining transparently.

Now, if I've gone to the trouble of making that happen, it's going to be easy enough to create a new eventGameTick() event to indicate when a new processing slot has started. This could be used when I want code to be triggered at the start of every slot (that is expecting to run some code) prior to any other code being run. Useful for things like invalidating caches, or even polling Process() heartbeat to make it churn through a few tasks...

Also, it means I can move timeRemaining out of Process and in to it's own tiny API (prolly merge it with a backport though to keep things concise). That in turn allows me to tardis all my events much sooner and with less cruft. It's likely that I'll end up with an Events API that:

  • Backports as many 3.2+ events to 3.1
  • Adds a bunch of new events
  • Tardis' all events so they can have multiple handlers
  • Make sure custom events void timeRemaining

For other projects, like Enhanced SitRep mod that I hope to eventually become the standard game rules architecture, it means much lower API overheads to get the sort of functionality that project needs. I'm hoping that all I'll need is Util API, Events API, Diag API and Chat API... maybe Cache API too? and I'll be able to pack a punch with moddable, customisable, reliable, efficient goodness.

Wow, 5am... must sleep more!

Then I had another idea...

I'm going to move Object.uTardis() out of Util API and in to Events API...

That way I have all the events stuff in one place, with only one dependency (Util API).

In doing the refactor, I'm going to change the way uTardis works - if it sees it's only got one listener for a standard game event, it will just ignore the event... because that one listener is there to void timeRemaining. No point in doing that unless we're sure other code is going to get processed as a result of the event. This is particularly important for eventAttackedUnthrottled() - it's gonna get hammered so I want it short circuiting ASAP if there's nothing else hooked to that event.

My plan is to have a custom tardis'ing of standard game events that uses an additional internal var in the closure to denote if there is a "do something proper with the event" listener added. The var will be false by default, only getting set to true when the setter gets called. In fact, that might be a good idea for the normal uTardis... it would mean that any event will short circuit if there's nothing worth doing. As a result, more time is freed up for WZ to get on with doing other stuff.

Right, really must sleep!