Thursday, July 22, 2010

Event Handling

If you recall what I wrote a long time ago about challenges in programming, event handling would be one of the easy, boring parts. The point of event handling is to inform parts of your program when something happens in another part. This is espacially important for the user interface, as you always want to see what's currently going on, but in the context of Magic, there is another very important reason for event handling: Triggered abilities.

If you now think that the User Interface and Triggered abilities can share the event handling system, therefore effectively reducing the ammount of programming needed for each of them, I fear you'll be disappointed. The reason for this is the undo system. While it enables things like handling illegal actions, the most direct interpretation of what it does prevents me from sharing one event system: Undo means that every action and event can happen in two directions in time.
When you attack with a creature, it becomes tapped. An ability could trigger from that tapping. If you now determine that the creature isn't allowed to attack, everything's undone and the creature is untapped again. You want to see the creature untapped in the user interface, but you don't want abilities trigger from it untapping.

Of course the event handling functions after the same principle, but the events are duplicated - in some sense. There's no real 1:1 mapping between GUI and game events, because the GUI is satisfied by state-changes, while the game needs more high level events: The gui doesn't care if you've drawn a card or just put it into your hand. It just needs to know that your library has become smaller and your hand grew larger.

Fortunately, Java has some built in functionality for handling these low-level events, namely PropertyChangeSupport. It allows you to keep track of properties and notify the listeners whenever a change occurs. Well, you still have to call the listeners on a change, but the major work, managing all the listeners and what properties they are interested in, is done for you.
Along with adding PropertyChangeSupport to Laterna Magica, I have restructured a lot of code. Previously, every class implemented undo support for its properties itself. This is now separated into an extra class, which also allows to use PropertyChangeSupport more easily:

public class EditableProperty extends AbstractGameContent {
    private EditablePropertyChangeSupport s;
    private String                        name;
    private T                             value;
   
    public EditableProperty(Game game, EditablePropertyChangeSupport s, String name) {
        this(game, s, name, null);
    }
   
    public EditableProperty(Game game, EditablePropertyChangeSupport s, String name, T initialValue) {
        super(game);
        this.s = s;
        this.name = name;
        value = initialValue;
    }
   
    public void setValue(T value) {
        new SetValueEdit(value).execute();
    }
   
    public T getValue() {
        return value;
    }
   
    @Override
    public String toString() {
        return valueOf(getValue());
    }
   
    private class SetValueEdit extends Edit {
        private static final long serialVersionUID = 93955529563844615L;
       
        private T                 oldValue, newValue;
       
        public SetValueEdit(T newValue) {
            super(EditableProperty.this.getGame());
            this.newValue = newValue;
        }
       
        @Override
        protected void execute() {
            oldValue = value;
            value = newValue;
            if(s != null) s.firePropertyChange(name, oldValue, newValue);
        }
       
        @Override
        protected void rollback() {
            value = oldValue;
            if(s != null) s.firePropertyChange(name, newValue, oldValue);
        }
       
        @Override
        public String toString() {
            return "Set " + s.getSourceBean() + "'s " + name + " to " + newValue;
        }
    }
}

6 comments:

nantuko84 said...

seems I need to reread your old articles, but may be you could explain it here.
you brought an example:
When you attack with a creature, it becomes tapped. An ability could trigger from that tapping. If you now determine that the creature isn't allowed to attack, everything's undone and the creature is untapped again. You want to see the creature untapped in the user interface, but you don't want abilities trigger from it untapping.
so you won't get abilities triggered from untap whenever you choose "undo", but what's about abilities triggered from tapping? is here the same thing?

I just think about paying mechanism. let's say you need to sacrifice a creature and tap two lands for mana. just wonder what's the plan? should client receive only "gui" events until cost is totally paid. so you sacrifice a creature, it disappears (one "gui" event), then you tap one land (nothing triggers), then you tap second land, after that server generates several "server" events at once - for sacrifice and tapping two lands if any.

Silly Freak said...

The post I was referring to was btw Traps in the Rules System.
I hope I get your question right and apologize if not. There is no time delay between "gui" and "game" events, they both happen as soon as the triggering action, like tapping a land.
However, the game event is part of the game flow, as is everything resulting from it. This means that returning to an early state will negate the event and everything is fine.
For GUI events, I'll put it in the order I thought of it: what the GUI does is not relevant for the game, so the undo system shouldn't record it. The GUI still needs to sync with the game, which makes another layer of events necessary. These events are focused on the state rather than on the actions, just as undoing changes state but doesn't perform actions.

In fact, to have proper behavior, game events must only cause game actions and GUI events must only cause non-game changes. Violation either will make things screw up when undoing

nantuko84 said...

hi,
recently I found time and read about PropertyChangeSupport classes (first I didn't know that it is from java beans, thought that it is yours)
I like the idea... at the moment I use Observer and Observable interfaces, but going to reimplement event mechanism (using some your code if you don't mind) as I need undo for ai

btw, I found Constrained Properties supported by VetoableChangeListener. what do you think can it be used for rule constrains: e.g. for cards like "Players can't gain life". in that case card would throw PropertyVetoException and players won't get life.

Silly Freak said...

hmm... on first thought this is a brilliant idea. really, i was thrilled when I read it on my smartphone and turned on my laptop just to answer you.

however, thinking about it, there are some problems about it. most importantly, the whole property change thing is not "game". Using a vetoable change support would mean that if you undo a life loss, the rules would forbid that.

the idea in general, using exceptions to implement disallowing rules, is a good one; i have to think about the implications, but it's probably a very good idea

nantuko84 said...

could you please explain what you use CompoundEdit in StateImpl for?

in fireStateChanged you create CompountEdit object, then as I understand notify all listeners and then call ed.end() that removes it from gameState. the question is why you wrap stateChanged into CompoundEdit (create and end)?

2. what's Internal interface in PermanentStateChangedListener?

3. I couldn't find any class that implements PermanentStateChangedListener. should I just create anonymous class in such case?

thanks in advance

Silly Freak said...

a PermanentStateChangedListener is a game listener, aka one that could change the game state. as such, it's handy to encapsulate the event notification into a single compound edit, so you see what happened as a result of the change. it's not necessary for undo to work.

the Internal interface is used for rules engine internal listeners. For example, GameInitializer implements GameStartListener.Internal, because it should happen before any other listeners.

the fact that there are no PermanentStateChangeListeners is that there are no triggered abilities yet ;)