Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to verify your details, confirm your email, resolve issues, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.

Due to spam on this forum, all posts now need moderator approval.

 Entire forum ➜ Programming ➜ General ➜ Events handler

Events handler

It is now over 60 days since the last post. This thread is closed.     Refresh page


Pages: 1 2  

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Sun 20 Jul 2003 10:46 PM (UTC)

Amended on Tue 22 Jul 2003 06:47 AM (UTC) by Nick Gammon

Message
As discussed in another thread, here is an example of an event handler.

It is really designed to be called from a "main loop" somewhere (eg. idle loop, after doing a select).

To use it you create an event queue (see below for example), and then use AddEvent (or EventQueue.push) to put events on the queue.


  • The queue is a priority queue, so events with the lower "fire" time will be extracted first (as the sense of the priority is reversed, so the lowest time is the highest priority).

  • The granularity is 1 millisecond (1/1000 of a second) so you can have fine control over sequence of events.

  • If events have the same fire time they are processed in an undefined order, however you can use a sequence argument to force processing in a certain order (the lower sequence number will be processed first).

  • To do something useful you simply derive a class from the CEvent class - there are two examples below.

  • Events can auto-repeat, so you can have things that happen, say, every 5 seconds. Auto-repeated events are the same event requeued, so any "state" in the derived class is retained.

  • Events are automatically counted by the base class, so you can easily see when x events (of a particular instance) have fired.

  • There should not be "event creep" - even if processing an event takes a few milliseconds, if an event is repeated it is rescheduled based on when it should have fired, not when it finished processing.





Amended to insert sequence number, to guarantee that events with the same fire time are processed in a designated sequence.


#include <string>
#include <functional>
#include <queue>
#include <vector>
#include <iostream>


// events.h #ifdef WIN32 #include <windows.h> // for timeGetTime #else #include <sys/time.h> // for gettimeofday #include <unistd.h> // for sleep #endif using namespace std; // thanks to Ksilyan for this routine :) long GetMillisecondsTime (void) { #ifdef WIN32 #pragma comment( lib, "winmm.lib" ) // this is the library it is in return timeGetTime(); #else struct timeval resultTimeval; gettimeofday( &resultTimeval, NULL ); resultTimeval.tv_sec -= 946080000; // 30 years return resultTimeval.tv_sec * 1000 + resultTimeval.tv_usec / 1000; #endif } // end of GetMillisecondsTime // event object class CEvent { public: CEvent (const long iMilliseconds = 1000, // default, 1 second const bool bRepeat = false, // default, no repeat const long iSequence = 0) : // default, unsequenced m_iInterval (iMilliseconds), // time interval m_bRepeat (bRepeat), // whether to keep doing it m_iWhen (GetMillisecondsTime ()), // set last fired time to now m_iInstance (0), m_iSequence (iSequence) { Reschedule (); // calculate when it first fires }; virtual ~CEvent () {}; // important - see Scott Meyers // operator< (for sorting) inline bool operator< (const CEvent & rhs) const { // we compare > because the sooner events have the // higher priority if (m_iWhen == rhs.m_iWhen) return m_iSequence > rhs.m_iSequence; // sequence order if times same else return m_iWhen > rhs.m_iWhen; // time order }; // call to reschedule it - add interval to last fired time // so as to avoid event creep inline void Reschedule (void) { m_iWhen += m_iInterval; }; inline void Fired (void) { m_iInstance++; }; // get values inline long GetTime (void) const { return m_iWhen; }; inline long GetInterval (void) const { return m_iInterval; }; inline bool Repeat (void) const { return m_bRepeat; }; inline long GetInstance (void) const { return m_iInstance; }; // derive a class and override this to actually do something virtual void OnEvent (void) = 0; protected: long m_iInterval; // seconds until next one bool m_bRepeat; // true if we are to re-enter in queue private: long m_iWhen; // when event fires long m_iInstance; // how many times it fired long m_iSequence; // what order to sequence events with the same fire time }; // end of class CEvent // for adding events to a priority_queue struct event_less : binary_function<CEvent*, CEvent*, bool> { inline bool operator() (const CEvent* X, const CEvent* Y) const { return (*X < *Y); } }; typedef priority_queue<CEvent*, vector<CEvent*>, event_less > tEvents; // event handler void ProcessEvents (tEvents & EventQueue);
// events.cpp // my own iterator can be used for inserting into the event queue class event_queue_back_inserter : public iterator <output_iterator_tag, CEvent*> { public: event_queue_back_inserter (tEvents & events) : m_events (events) {}; // assignment is used to insert into the queue event_queue_back_inserter & operator= (CEvent* & e) { m_events.push (e); return *this; }; // dereference and increments are no-ops that return // the iterator itself event_queue_back_inserter & operator* () { return *this; }; event_queue_back_inserter & operator++ () { return *this; }; event_queue_back_inserter & operator++ (int) { return *this; }; private: tEvents & m_events; }; // end of class event_queue_back_inserter // 1. pull out events from the event queue // 2. keep going until one is due in the future // 3, if rescheduling wanted keep in temporary vector until all are done void ProcessEvents (tEvents & EventQueue) { long now = GetMillisecondsTime (); vector<CEvent*> repeated_events; // pull out event that need doing while (!EventQueue.empty ()) { CEvent * e = EventQueue.top (); if (e->GetTime () > now) break; // not yet EventQueue.pop (); // remove from queue e->Fired (); // note it fired e->OnEvent (); // do the event (derived class) // if repeating it wanted, recalculate and keep in separate list if (e->Repeat ()) { e->Reschedule (); repeated_events.push_back (e); } else delete e; // otherwise pointer not needed any more } // end of event loop // put any event we rescheduled back into the queue // we do it now so we don'now get into a loop if the event is due to // fire immediately copy (repeated_events.begin (), repeated_events.end (), event_queue_back_inserter (EventQueue)); } // end of ProcessEvents
// main.cpp // an instance of an event queue tEvents EventQueue; // helper routine to add an event void AddEvent (CEvent * e) { if (e) // being cautious here :) EventQueue.push (e); } // end of AddEvent int main (void) { class myevent_A : public CEvent { public: myevent_A (const int iTime, const string sMsg, const bool bRepeat = false, const long iSequence = 0) : CEvent (iTime, bRepeat, iSequence), m_sMsg (sMsg) { }; virtual void OnEvent (void) { cout << m_sMsg << " instance " << GetInstance () << endl; }; private: const string m_sMsg; }; // end of class myevent_A class myevent_B : public CEvent { public: myevent_B () : CEvent (0, true) { }; virtual void OnEvent (void) { cout << "myevent_B, instance " << GetInstance () << endl; }; }; // end of class myevent_B AddEvent (new myevent_A (10000, "after 10 secs")); AddEvent (new myevent_A (5000, "after 5 secs", false, 1)); AddEvent (new myevent_A (5000, "another after 5 secs", false, 2)); AddEvent (new myevent_A (7000, "after 7 secs")); AddEvent (new myevent_A (2000, "You hear rustling sounds", true)); AddEvent (new myevent_B ()); // main program loop while (true) { ProcessEvents (EventQueue); // sleep for 1 second - simulate waiting on comms or something #ifdef WIN32 Sleep (1000); #else sleep (1); #endif } return 0; } // end of main



Example output

myevent_B, instance 1
myevent_B, instance 2
myevent_B, instance 3
You hear rustling sounds instance 1
myevent_B, instance 4
myevent_B, instance 5
You hear rustling sounds instance 2
myevent_B, instance 6
after 5 secs instance 1
another after 5 secs instance 1
myevent_B, instance 7
You hear rustling sounds instance 3
myevent_B, instance 8
after 7 secs instance 1
myevent_B, instance 9
You hear rustling sounds instance 4
myevent_B, instance 10
myevent_B, instance 11
You hear rustling sounds instance 5
after 10 secs instance 1
myevent_B, instance 12
myevent_B, instance 13
You hear rustling sounds instance 6
myevent_B, instance 14
myevent_B, instance 15
You hear rustling sounds instance 7


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #1 on Mon 21 Jul 2003 07:59 AM (UTC)
Message
Looking at the results, they don't quite do what was advertised, namely retain the initial order. Note these two lines:

another after 5 secs instance 1
after 5 secs instance 1

I think the problem is that priority_queue is not in fact implemented as a map of queues, but a vector (or deque) that is sorted. Unfortunately, apart from the overhead of doing a sort for every insertion, sorts do not (always) guarantee that items which have an equal sorting sequence retain their initial order.

I will look into redoing the priority_queue tomorrow as a map of queues, which I think will be faster, and do what I expect.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #2 on Mon 21 Jul 2003 12:45 PM (UTC)
Message
That's really neat stuff, Nick. Do you mind if I use that and implement it in my MUD?

Quote:
Looking at the results, they don't quite do what was advertised, namely retain the initial order.


Couldn't this be considered a "feature" instead of a problem? We were trying to figure how to process things more fairly, and if the order gets messed around with a bit, then wouldn't that mean that there's no guarantee that if you typed it first, you get processeed first? Now, it might end up being that whoever types it last gets processed first, in which case we're back at square 1... but, if it truly is a little random, then it might not be so bad.

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #3 on Mon 21 Jul 2003 08:21 PM (UTC)
Message
Well, it could, but I think if you want to randomise things you would slightly alter the fire time (by a few milliseconds).

If you queued up, say, commands, and someone entered N, E and they got E, N they might be a bit annoyed.

The other thing that worries me is the performance penalty - if it really is implemented by adding new things to the end of a vector, and then doing a sort, for every insertion, then it isn't as fast as I hoped it would be.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #4 on Mon 21 Jul 2003 09:33 PM (UTC)
Message
I have amended the original post.

After some research into STL, it seems the performance penalty of the inbuilt priority_queue isn't too bad. It doesn't actually sort each time, it uses a "heap" function which is a sort-of ongoing sorted heap, into which you can insert in logarithmic time.

So, I have added a sequence number into the constructor. If you don't care too much, leave at the default (zero), which means that events for the same moment might not fire in the order they were inserted. If you do care, make sure the sequence increments (eg, using a global variable to which you add 1 each time).

I'm also not sure it needs to be a priority_queue based on deque, it might be OK, or faster even to simply make it a queue of vectors. ie. change:

typedef priority_queue<CEvent*, deque<CEvent*>, event_less > tEvents;

to

typedef priority_queue<CEvent*, vector<CEvent*>, event_less > tEvents;

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #5 on Tue 22 Jul 2003 04:39 AM (UTC)
Message
I have been doing some timing tests, it seems using deque as the base class for the priority queue was a bad idea. Adding events is about the same speed, but processing them is about 4 times as slow. Here are my figures.

The test did this...


  • Adding 10,000 events (all set to fire immediately with repeat).

  • Processing the 10,000 events 100 times.


Results on 166 Mhz Pentium, Windows


Start     : Adding events - deque
Time taken: Adding events - deque     = 0.049950 seconds.
Start     : Adding events - vector
Time taken: Adding events - vector    = 0.047447 seconds.
Start     : Processing events - deque
Time taken: Processing events - deque = 14.692457 seconds.
Start     : Processing events - vector
Time taken: Processing events - vector = 5.265106 seconds.
Total events fired = 2000000


Results on 860 Mhz Pentium, Linux


Start     : Adding events - deque
Time taken: Adding events - deque     = 0.011699 seconds.
Start     : Adding events - vector
Time taken: Adding events - vector    = 0.011204 seconds.
Start     : Processing events - deque
Time taken: Processing events - deque = 4.720998 seconds.
Start     : Processing events - vector
Time taken: Processing events - vector = 1.044776 seconds.
Total events fired = 2000000


Thus I have amended the original post to use a vector rather than a deque, and inlined a few things.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #6 on Tue 22 Jul 2003 04:50 AM (UTC)
Message
There is a bit of overhead for event handling, however I think that 1 second for 1 million events isn't too bad. In practice, I doubt you would have anything like that.

Even in a MUD with 100,000 rooms you would not really need frequent events except in rooms where there is actually a fight happening (say, every 1/2 second), but in that case you would need a *lot* of players connected, even to have 500 simultaneous fights. Even then the event handler overhead would be half of a millisecond.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #7 on Tue 22 Jul 2003 06:46 AM (UTC)
Message
Quote:

Do you mind if I use that and implement it in my MUD?


I think I answered your question in the wrong thread.

You are welcome to use it, indeed all the code in this section of the forum is posted for public consumption.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #8 on Tue 29 Jul 2003 07:18 AM (UTC)
Message
Hello again,

I've been quiet for a while, I've been working on putting all this stuff (database & events handler) into the game.

I've run into a few snags with the event handler, mostly practical questions. It seems that anything that needs to be "persistant" (i.e. that would survive between reboots, or even a player quitting and logging on later) can't be stored in the queue.

Take for instance a player casting a spell on him/herself. The event "expire spell" will be pushed onto the queue with for example 2 minutes. Now, if the player quits and doesn't come back for 3 hours, we have two problems:
a) the event is referring to a non-existant player. This is something I've already more or less figured out by using a memory manager class. I'll post this in another forum shortly.

b) when the player comes back, we want the spell to still have duration left.

So when a player object quits, it would need to find all events marked as "persistant" that refer to it, and them them somehow? Is there a better way of doing this? It seems inefficient to loop through the whole queue every time a player quits. Then again, the game currently loops through EVERY character, regardless of whether there's actually something to do...

Perhaps have two queues, one for persistant events, and one for events that don't need to be saved?

Or perhaps even give every character object (perhaps... EVEY game entity (room/obj/mob) object... muhaha) its own queue?

I'm just throwing out thoughts and ideas in the hopes that someone else has thought about this as well. :)

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #9 on Thu 31 Jul 2003 02:01 AM (UTC)
Message
Quote:

a) the event is referring to a non-existant player. This is something I've already more or less figured out by using a memory manager class. I'll post this in another forum shortly.


I was thinking about this. It seems a good idea is to not store in the event a pointer to the player, as you can expect players to disconnect, and if you store pointers you can also expect crashes. :)

One logical thing would be to have a map of player names to players, eg.

typedef map<string, Player *, less<string> > player_map;

As you create player objects when players connect, they could be added to this map (by their name) and as they leave you would delete them. This would be useful in many places, eg. doing a tell.

Now the event in the event queue could simply refer to the player by name (eg. spell "poison" for player "nick" wears off in 10 minutes).

When the event fires it could attempt to locate the player by looking in the map, if not there it could take appropriate action (eg. do nothing, or maybe write an entry to a file somewhere).

Quote:

b) when the player comes back, we want the spell to still have duration left.


Yes, perhaps. It might be fine if the player disconnects for 5 minutes - although in the system I described the spell would still wear off at the same time, which - in a way - is natural behaviour. It would still find the player, even though s/he might have a different pointer by now.

I suppose players could disconnect and wait a day for spells to wear off, but where is the fun in that? Also, if you remembered a spell for "next time" and they went away on holidays, and came back after a month, and reconnected, they might be a bit surprised to be told "you are still affected by the poison spell" - which they had forgotten about.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #10 on Thu 31 Jul 2003 02:03 AM (UTC)
Message
Quote:

Perhaps have two queues, one for persistant events, and one for events that don't need to be saved?


I'm not sure if you save much here. The occasional linear scan of the event queue wouldn't take too long, and you may find anyway that 90% of the events are persistent, so putting them in a separate queue might not gain you much.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by David Haley   USA  (3,881 posts)  Bio
Date Reply #11 on Sat 02 Aug 2003 12:03 AM (UTC)
Message
Hi again! :)

The idea of a map is definitely an extremely good one. For starters it would greatly speed up operations that involve finding a specific player's pointer.

There are a few problems though, also related to our character-quitting-so-event-fizzles issue. Maybe an event refers to a mob... this mob might die before the event fires. The mob wouldn't be in the map (I see technical issues, since mob names aren't unique like players are) and so the event wouldn't know to get rid of it. That's why I figure that the memory manager is a better solution.

Concerning the persistent events, you're right... there won't be soooo many that a linear search would take too long.

About the spells wearing off, on our MUD it takes in-game time for a spell to go away. It's hard to mix in-game and off-game time... why should your spells wear off when you're not playing, but you don't get hungrier? Besides, we don't want a player to log off until the curse wears off, or something like that. So that's why I'd need to store all persistent events pertaining to a character along with the character. :)



P.S. Sorry for the slow replies of late. I've had some RL stuff that's kept me busy. Also, I'll be in the UK for a week starting Sunday, and I'm not sure if I'm going to be able to post during that time.
I'd just like to say thanks, Nick, for all the ideas you've given, the time you've shared. And, of course, for an excellent MUD client. Keep up the excellent work! :)

David Haley aka Ksilyan
Head Programmer,
Legends of the Darkstone

http://david.the-haleys.org
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #12 on Sat 02 Aug 2003 01:05 AM (UTC)
Message
Thanks for the compliments, plus you have given me some good ideas for my next project. :)

As for the mobs, I think the idea could cover them (and objects as well) by having some simple system where when something non-unique is created (eg. another sword, another mob) it is given a unique tag, like a number which increments by one each time. This could easily be done with a static variable in the constructor, for instance. Then internally that number could be used in the map (you can map numbers, after all), and each time an event fires you check to see if the thing the event refers to still exists, in the way I described.

You would have to have an awful lot of them before a long wrapped around, and even then the allocator could do a quick check to see if the number was in use, and if so, choose another one.

What you are describing with reference counting sounds similar to auto pointers - which I have only today been cursing because I used an auto_ptr in a place where it shouldn't have been. The auto_ptr went out of scope and deleted the variable, and then the variable was also in a list which tried to delete it again.

Anyway, I am going to experiment a bit with the event queue and see how well it works for me.

You could probably go mad with an event queue - I will try it and see what happens. ;) For instance, when the player types "north" they don't immediately move north, but a "go north" event goes into the queue, which moves them in (say) 1/2 a second. That limits all players to moving at the same rate, even if their client fires off speed walks at a great rate. However you could adjust the rate - for example some types of players might be able to move faster so their movement event might be queued for 1/4 second, whereas if you were tired it might be in 1 second.

Similarly for a "heal" potion - you might quaff it, but rather than having the full HP restoration at once, a repeating event gradually restores your HP over a number of seconds.

Again, for something like poison, the poison event might remove HP every second until the poison wore off.

All this could happen with a fairly simple core server - things like poison spells could incorporate their own event classes, so they could handle all that in a self-contained way.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,165 posts)  Bio   Forum Administrator
Date Reply #13 on Tue 05 Aug 2003 11:36 PM (UTC)

Amended on Tue 05 Aug 2003 11:37 PM (UTC) by Nick Gammon

Message
Quote:

Quoting myself ...

One logical thing would be to have a map of player names to players, eg.

typedef map<string, Player *, less<string> > player_map;

As you create player objects when players connect, they could be added to this map (by their name) and as they leave you would delete them. This would be useful in many places, eg. doing a tell.


After working on this idea for a while I decided to try to automate the process, with the results now available in:


http://www.gammon.com.au/forum/?bbsubject_id=3079


As I thought about it, I wanted to do the following:


  • Automate the process of adding things to the map - so you didn't forget - this is done now by the object constructor doing it - so for instance, a new player is immediately in the player map from the moment the constructor is called

  • Automate the process of removing things from the map - so you are guaranteed that the map accurately represents which pointers really exist. This is done by having the destructor remove the pointer from the map. To achieve this the object has to remember which map it belongs to.

  • Have some type-safety. Otherwise using simple numbers could result in you accidentally storing a room number where you meant to have a player number. This is done by having a small class to do this. Because the class only stores the one number it takes no more memory than the number would have in the first place. Then you can store (for instance) in a player definition:

    
      tId<tRoom>  room;  // room he is in
      tId<tQuest> quest; // current quest
      tId<tPlayer> following; // who he is following
    


    These identifiers are keyed to the type of pointer they are designed for, so that for example, this would give a compile error: room = quest;

  • Allow quick conversion from an identifier to the pointer it belongs to. This is done by overloading operator[] for the container class. eg.

    
    tPlayer * p = PlayerMap [following];
    
    if (p)  // if not null, it exists in map, thus pointer is valid
      {
      // do something with it
      }
    


    I think this is nice and simple to use. See the posting above for more details.


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Cash   USA  (626 posts)  Bio
Date Reply #14 on Mon 19 Jan 2004 06:58 AM (UTC)

Amended on Mon 19 Jan 2004 07:15 AM (UTC) by Nick Cash

Message
Note: This reply isn't really about STL

Just thought I'd say this, since I'm bored, and I think you would appreciate hearing some praise.

In my mud (Gundam Wing based) I am in the process of writing a mobile suit combat system to replace the old spam-happy fighting system of SWR. I was thinking of ways to do it, states, timers, anything. However, I ran into many problems. First off, I found timers were not what I wanted for various reasons. An example would be a pilot trying to swing his beam axe at a hostile mobile suit, but in the middle of the timer he wants to check his radar, or ship status, or something kinda like that. This would cancel most mud timers, and for that reason I threw that out. States, now, this really seemed like a good idea at first, and, in fact, it really would work. I did this for my ship upgrading system (thanks to Boborak for his idea), and I figured well, why not do it again? Only a couple problems. First off, thats boring. Why do it again? Its like reinventing the wheel. Next, I looked at this post and it made me think, why not use an event handler, or some event queue?

So, even though its still in its bare bones stages, my code is progressing very nicely. I decided that once I start using an event handler, I can apply it to other things in the mud. Another plus side to this is that when I start writing another mud with my friends, I'll have the exact way I wan't combat done in that already written, along with heaps of code that I'm going to donate anyways.

Now, what exactly is the point of this post, besides the fact I was bored? All of the code in this thread (though most needed editing to fit my mud since its not C++ and I know nothing of STL YET), has helpped me either directly or by giving me something to base my event handler off of. Anyways, the point of this post is just to say thank you for writing this code and displaying it here so I could apply the concepts to my code, and making my mud 100 times more interactive and fun.

Thanks, keep up the good work. I'll see if I can go muster up some money to get an STL book so I don't have to bug you guys all the time. :P

~Nick Cash
http://www.nick-cash.com
Top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


55,688 views.

This is page 1, subject is 2 pages long: 1 2  [Next page]

It is now over 60 days since the last post. This thread is closed.     Refresh page

Go to topic:           Search the forum


[Go to top] top

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.