Friday, March 26, 2010

Undo ... and replacement effects!

I finally got time to work on Laterna Magica again. It wasn't that exciting, just adding undo support to all the classes that already existed. While I browsed the classes and looked for actions that I hadn't revised yet, I found the LifeTotal class. Its job is easy: store the number of life points for a player, and whenever it changes, inform listeners.

...except that there are replacement effects. Only because I want to change life points, that doesn't mean it happens! what could be done?

Surprisingly, the solution looks very similar to my undo engine: Every modification is encapsulated into an edit and stored in the engine. If you want to undo something, the undo engine knows what happened, and every single edit knows how to revert itself. For the caller, it's only addListener(myListener), but inside there's new AddListenerEdit(myListener).execute().

Similarly, the replacement engine is the authority for performing replaceable actions, like gaining life. Calling gainLife(amount) really means new LifeEvent(amount).execute().
Execute uses the replacement engine to apply all replacement effects. Those may replace the event with another event, until all are through. The resulting event is then really executed (the implementation method is called execute0).

Edit and ReplaceableEvent are not connected. Both have an execute method, but that's more or less coincidence. Logically, there is a connection. Look at execute0 from LifeEvent:
public void execute0() {
    ((LifeTotalImpl) getPlayer().getLifeTotal()).fireLifeEvent(this);
}


//...and from LifeTotalImpl


void fireLifeEvent(LifeEvent e) {
    CompoundEdit ed = new CompoundEdit(getGame(), true, "Adjust " + getPlayer() + "'s life by "
            + (e.isGained()? "+":"-") + e.getAmount());
   
    new AdjustLifeEdit(e.getChange()).execute();
   
    // Guaranteed to return a non-null array
    Object[] l = listeners.getListenerList();
    // Process the listeners last to first, notifying
    // those that are interested in this event
    boolean gain = e.isGained();
    for(int i = l.length - 2; i >= 0; i -= 2) {
        if(l[i] == LifeListener.class) {
            if(gain) ((LifeListener) l[i + 1]).lifeGained(e);
            else ((LifeListener) l[i + 1]).lifeLost(e);
        }
    }
    ed.end();
}

Two things are of importance here: First, changing the life is done through an edit, so it can be undone. Second, informing the listeners is encapsulated ina compound edit, so that everything related to this event is grouped together.

I'm quite proud about this piece of code; I expected replacement effects to be very hard, but as it turns out, if there are no flaws in this concept, they should be quite easy.

No comments: