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.
 Entire forum ➜ MUSHclient ➜ Lua ➜ Building pauses into scripts

Building pauses into scripts

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,102 posts)  Bio   Forum Administrator
Date Wed 01 Dec 2004 08:30 PM (UTC)

Amended on Tue 26 Jun 2007 10:18 PM (UTC) by Nick Gammon

Message
We have been asked many times in this forum:

"How do I pause in a script?"

For example, someone wants to do this in an alias:


Send ("prepare heal")

-- wait 2 seconds

Send ("cast heal")

-- wait 3 seconds

Send ("eat bread"


Up till now this hasn't been possible, and the occasional attempt to achieve this by making a loop like this, doesn't work very well:


Send ("prepare heal")

-- loop for a second
for i = 1 to 100000
next

Send ("cast heal")


It doesn't work very well because all that achieves is to "hang" the entire PC for a while. Apart from the problems that causes, the 1-second delay will still probably come in the wrong place, because while it is hanging the original send ("prepare heal") probably has not even been sent over the network yet.

What you need is some way of causing the script to pause for a second, and then resume where you left off.

Finally that is possible, using Lua. The script below demonstrates how to do that. The first part can be put in your script file and shared between every trigger and alias that needs to have pauses in it.

The basic technique is to use the Lua "thread" model. This lets you define "co-routines" or "threads" that can yield execution back to MUSHclient when they have finished for the moment.

Then we will make a timer to resume them at the desired time.

The first part, which is shared between all scripts that need it is:


  • A table of outstanding threads, keyed by timer name

  • A routine called when the timer fires, which resumes the thread

  • A "wait" script which is called when we want to pause.

    The "wait" script:


    • Generates a unique timer name to be used in our table of threads

    • Adds a timer with this unique timer name for the specified time

    • Adds the timer name and the thread address to the table of threads

    • Yields execution (pauses)






-- table of outstanding threads that are waiting
wait_table = {}

-- called by a timer to resume a thread
function wait_timer_resume (name)
  thread = wait_table [name]
  if thread then
    assert (coroutine.resume (thread))
  end -- if
end -- function wait_timer_resume 

-- we call this to wait in a script
function wait (thread, seconds)

  id = "wait_timer_" .. GetUniqueNumber ()

  hours = math.floor (seconds / 3600)
  seconds = seconds - (hours * 3600)
  minutes = math.floor (seconds / 60)
  seconds = seconds - (minutes * 60)

  status = AddTimer (id, hours, minutes, seconds, "",
            timer_flag.Enabled + timer_flag.OneShot + 
            timer_flag.Temporary + timer_flag.Replace, 
            "wait_timer_resume")
  assert (status == error_code.eOK, error_desc [status])

  wait_table [id] = thread
  coroutine.yield ()
end -- function wait




Now with those sitting in our script file, we are ready to make an alias with pauses in it. This is really a two-step process. In order to get a thread to yield, we first need a thread to execute in the first place. So, the real work is going to be done in my_alias_thread, which is started by the alias, however it has a fourth argument, which is the thread address. This looks pretty simple, and you can see in it the pauses (calls to "wait"). We need to pass down the address of our current thread to the wait routine, so it knows which thread to eventually resume.

In this example I am doing a "Note" however you can do "Send" or whatever you need.


function my_alias_thread (thread, name, line, wildcards)

  Send ("prepare heal")
  wait (thread, 1)

  Send ("cast heal")
  wait (thread, 2)

  Send ("eat bread")
  wait (thread, 3)

  Note ("Done")

end -- function my_alias_thread 



Finally a small "stub" which is called by the actual alias - this simply creates a coroutine (a thread) which is the script just above, and then resumes it. The word "thread" appears twice when we resume it. The first time we are telling coroutine.resume which thread to resume, the second one is passed down to the script itself, so it can be used for pausing purposes. The assert function is there to trap errors, as errors in a resumed function are not automatically reported.


function my_alias (name, line, wildcards)

  thread = coroutine.create (my_alias_thread)
  assert (coroutine.resume (thread, thread, name, line, wildcards))

end -- function my_alias



Finally the alias which I will use to test it:


<aliases>
  <alias
   name="test_alias"
   script="my_alias"
   match="test"
   enabled="y"
   sequence="100"
  >
  </alias>
</aliases>



Doing the alias as "send to script"

It is possible to have the alias (or trigger) as a "send to script" and still use the inbuilt pauses. You still need the wait_table, wait_timer_resume and wait functions in your script file. However the rest can be done in "send to script" by using an anonymous function created on-the-fly.

Below is an example. You bracket what you are trying to do with an extra line at the start and end, in bold below. I think it still looks pretty easy to read ...


<aliases>
  <alias
   match="test2"
   enabled="y"
   send_to="12"
   sequence="100"
  >
  <send>

do local t = coroutine.create (function (t)

  Note ("prepare heal")
  wait (t, 1)

  Note ("cast heal")
  wait (t, 2)

  Note ("eat bread")
  wait (t, 3)

  Note ("Done")

end) assert (coroutine.resume (t, t)) end

</send>
  </alias>
</aliases>

- Nick Gammon

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

Posted by Nick Gammon   Australia  (23,102 posts)  Bio   Forum Administrator
Date Reply #1 on Thu 08 Mar 2007 07:57 PM (UTC)

Amended on Tue 26 Jun 2007 10:18 PM (UTC) by Nick Gammon

Message
Under Lua 5.1 (MUSHclient version 3.80 onwards) the whole thing is simpler, because a coroutine can obtain its own thread.


-- table of outstanding threads that are waiting
wait_table = {}

-- called by a timer to resume a thread
function wait_timer_resume (name)
  thread = wait_table [name]
  if thread then
    wait_table [name] = nil  -- remove from table
    assert (coroutine.resume (thread))
  end -- if
end -- function wait_timer_resume 

-- we call this to wait in a script
function wait (seconds)

  local id = "wait_timer_" .. GetUniqueNumber ()
  wait_table [id] = assert (coroutine.running (), "Must be in coroutine")

  local hours = math.floor (seconds / 3600)
  local seconds = seconds - (hours * 3600)
  local minutes = math.floor (seconds / 60)
  local seconds = seconds - (minutes * 60)

  status = AddTimer (id, hours, minutes, seconds, "",
            timer_flag.Enabled + timer_flag.OneShot + 
            timer_flag.Temporary + timer_flag.Replace, 
            "wait_timer_resume")
  assert (status == error_code.eOK, error_desc [status])

  coroutine.yield ()
end -- function wait


The "wait" routine has been changed to find its own coroutine address (line in bold), so we don't need to pass it as an argument.

That simplifies creating the thread, as we can omit the "thread" argument, so it looks like this:


function my_alias_thread (name, line, wildcards)

  Send ("prepare heal")
  wait (1)

  Send ("cast heal")
  wait (2)

  Send ("eat bread")
  wait (3)

  Note ("Done")

end -- function my_alias_thread 


function my_alias (name, line, wildcards)

  assert (coroutine.resume (coroutine.create (my_alias_thread), 
          name, line, wildcards))

end -- function my_alias



And now the "inline scripting" version is simpler, too:


<aliases>
  <alias
   match="test2"
   enabled="y"
   send_to="12"
   sequence="100"
  >
  <send>
coroutine.wrap (function ()

  Send ("prepare heal")
  wait (1)

  Send ("cast heal")
  wait (2)

  Send ("eat bread")
  wait (3)

  Note ("Done")

end) ()
</send>
  </alias>
</aliases>


We don't need to find the thread variable, so we can simplify creating the thread, using coroutine.wrap.

- Nick Gammon

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

Posted by Nick Gammon   Australia  (23,102 posts)  Bio   Forum Administrator
Date Reply #2 on Thu 08 Mar 2007 08:07 PM (UTC)

Amended on Tue 26 Jun 2007 10:17 PM (UTC) by Nick Gammon

Message
This functionality is pre-supplied now in the "wait.lua" file that ships with MUSHclient. Thus, without needing a script file at all, you can "require" the wait file, and make an inline waiting alias, like this:


<aliases>
  <alias
   match="test"
   enabled="y"
   send_to="12"
   sequence="100"
  >
  <send>
require "wait"
wait.make (function ()

  Send ("prepare heal")
  wait.time (1)

  Send ("cast heal")
  wait.time (2)

  Send ("eat bread")
  wait.time (3)

  Note ("Done")

end) 
</send>
  </alias>
</aliases>



The lines in bold are what you need to copy into your own scripts. Now waiting for a few seconds is recoded was "wait.time" which is the "time delay" function built into the wait.lua file.

- Nick Gammon

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

Posted by Magee101   (1 post)  Bio
Date Reply #3 on Tue 25 Oct 2011 05:50 PM (UTC)
Message
Hello, I've been using MUSHclient since 2006, and after reading through this topic.. I have to admit I have no clue what to do to create a wait timer. I'm also not very good at creating scripts (This is actually my first attempt in all the time of using the client) So if anyone could perhaps but this into more layman terms it would be appreciated!
Top

Posted by Nick Gammon   Australia  (23,102 posts)  Bio   Forum Administrator
Date Reply #4 on Tue 25 Oct 2011 08:26 PM (UTC)
Message
Try the code in the post above yours:

Template:pasting For advice on how to copy the above, and paste it into MUSHclient, please see Pasting XML.


Then type "test".

Once you confirm that works, modify to suit your needs.

- Nick Gammon

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

Posted by Darcon   (26 posts)  Bio
Date Reply #5 on Thu 15 Nov 2012 09:36 PM (UTC)

Amended on Thu 15 Nov 2012 09:40 PM (UTC) by Darcon

Message
I am trying to make a smoke pipe alias for achaea. I don't know if I know enough to make this work. In fact I know it doesn't because I am getting an error when trying to use it.

Here is what I have so far.


<aliases>
  <alias
   match="smokecure"
   enabled="y"
   group="Smoke"
   send_to="12"
   sequence="100"
  >
  <send>require "wait"

wait.make (function () -- coroutine starts here

local Balace = tonumber (GetVariable ("Balance"))
local Pipe1 = GetVariable ("PipeValerian")
local Pipe2 = GetVariable ("PipeElm")
local FullPipe1 = tonumber (GetVariable ("FullPipeValerian"))
local FullPipe2 = tonumber (GetVariable ("FullPipeElm"))
local LitPipe1 = tonumber (GetVariable ("LightPipe1"))
local LitPipe2 = tonumber (GetVariable ("LightPipe2"))

-- Affliction Variables

    -- Smoke Elm Cures

local Aeon = tonumber (GetVariable ("Aeon")) -- Elm
local Deadening = tonumber (GetVariable ("Deadening")) -- Elm
local Hellsight = tonumber (GetVariable ("Hellsight")) -- Elm

   -- Smoke Valerian Cures

local Disfigurement = tonumber (GetVariable ("Disfigurement")) -- Valerian
local ManaLeech = tonumber (GetVariable ("ManaLeech")) -- Valerian
local Slickness = tonumber (GetVariable ("Slickness")) -- Valerian

   -- Pick Correct Pipe

if Aeon == 1 then
   Pipe = Pipe1
elseif Deadening == 1 then
   Pipe = Pipe1
elseif Hellsight == 1 then
   Pipe = Pipe1
elseif Disfigurement == 1 then
   Pipe = Pipe2
elseif ManaLeech == 1 then
   Pipe = Pipe2
elseif Slickness == 1 then
   Pipe = Pipe2
else ColourNote ("white", "red", "No Smoke Cures Needed!!!")
end

  -- Try Smoke Pipe

if Balance == 1 then
   if Pipe then
      SmokePipe ()
   else ColourNote ("white", "red", "Pipe Not Picked!!!")
   end
end

function SmokePipe () -- Smoke Pipe Function

Send ("Smoke " .. Pipe)

local x = wait.match ("^You take a long drag off your pipe\.$", 1)
local y = wait.match ("^The pipe has nothing smokeable in it\.$", 1)
local z = wait.match ("^That pipe isn\'t lit\.$", 1)

   if x then
      if Pipe == Pipe1 then
         SetVariable("FullPipeValerian", FullPipe1 - 1)
      elseif Pipe == Pipe2 then
         SetVariable("FullPipeElm", FullPipe2 - 1)
      end
   elseif y then
      EmptyPipe ()
   elseif z then
      LightPipe ()
   end

end -- Smoke Pipe Function

function EmptyPipe () -- Empty Pipe Function

local w = wait.match ("^You remove 1 valerian\, bringing the total in the Rift (.*?)$", 1)
local x = wait.match ("^You have no valerian stored in the Rift\.$", 1)
local y = wait match ("^You remove 1 slippery elm\, bringing the total in the Rift (.*?)$", 1)
local z = wait.match ("^You have no elm stored in the Rift\.$", 1)

   if Pipe == Pipe1 then
      Send ("outr valerian")
   elseif Pipe == Pipe2 then
      Send ("outr elm")
   end

   if w then
      Send ("put valerian in " .. Pipe)
      SetVariable ("FullPipeValerian", 10)
      LightPipe ()
   elseif x then
      ColourNote ("white", "red", "You are out of Valerian!!!")
   elseif y then
      Send ("put elm in " .. Pipe)
      SetVariable ("FullPipeElm", 10)
      LightPipe ()
   elseif z then
      ColourNote ("white", "red", "You are out of elm!!!")
   end

end -- Empty Pipe Function

function LightPipe ()

local x = wait.match ("^You carefully light your treasured pipe until it is smoking nicely\.$", 1)
local y = wait.match ("^That pipe is already lit and burning nicely\.$", 1)
local z = wait.match ("^There is nothing in the pipe to light\.$", 1)

Send ("light " .. Pipe)
   
   if x then
      SmokePipe ()
   if y then
      SmokePipe ()
   if z then
      EmptyPipe ()
   end

end -- light Pipe Function

end)</send>
  </alias>
</aliases>


Here is the error message


Compile error
World: Achaea
Immediate execution
[string "Alias: "]:120: unexpected symbol near ')'


This is only the beginning of what I would like to do with this alias, but if I cannot get the basic down I may as well scrap it.
Top

Posted by Nick Gammon   Australia  (23,102 posts)  Bio   Forum Administrator
Date Reply #6 on Fri 16 Nov 2012 03:40 AM (UTC)
Message

  if x then
      SmokePipe ()
   if y then
      SmokePipe ()
   if z then
      EmptyPipe ()
   end


You are missing some "ends" there.

Change the second two "if" to "elseif" and it compiles.

- Nick Gammon

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

Posted by Darcon   (26 posts)  Bio
Date Reply #7 on Fri 16 Nov 2012 05:02 AM (UTC)
Message
Thank you Nick, it's always the little things that trip you up.

My second question on this is could I wrap this all into a function and place it in my script file? say...

SmokeCure ()

or is the wait.make only reserved for aliases?
Top

Posted by Darcon   (26 posts)  Bio
Date Reply #8 on Fri 16 Nov 2012 05:25 AM (UTC)
Message
Oh, never mind, I guess I got some more bugs to work out. If I set the Disfigurement variable to 1 just to test it and it does nothing. It should go into the SmokeCure () function. Maybe I need to define a few more things in the SmokeCure () function in the original alias.
Top

Posted by Nick Gammon   Australia  (23,102 posts)  Bio   Forum Administrator
Date Reply #9 on Fri 16 Nov 2012 08:13 PM (UTC)
Message
Darcon said:

My second question on this is could I wrap this all into a function and place it in my script file?


The wait concept can be used elsewhere than aliases.

You don't really need to wrap all the functions in wait.make, just the "main" stuff done by the alias.

- Nick Gammon

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

Posted by Darcon   (26 posts)  Bio
Date Reply #10 on Fri 16 Nov 2012 10:05 PM (UTC)
Message
So, should I move the SmokeCure, EmptyPipe, and LightPipe functions to my script file and add the require "wait" to those functions? Or could I move those functions as is, and leave the require "wait" in the alias?

Thank you for all your help so far Nick. I probably couldn't do all this stuff without it.
Top

Posted by Nick Gammon   Australia  (23,102 posts)  Bio   Forum Administrator
Date Reply #11 on Fri 16 Nov 2012 10:29 PM (UTC)
Message
Yes, you can do that as far as I know. Just one "require 'wait' " is all that is needed for the script file.

Try it and see.

- Nick Gammon

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

Posted by Darcon   (26 posts)  Bio
Date Reply #12 on Sat 17 Nov 2012 12:36 AM (UTC)
Message
Ok, so I made some modifications to my smokecure alias. It compiles fine. The problem I am having is that it is not matching like it should.

As a test I set the Disfigurement variable to 1 and I did not light the pipe before hand.

In theory it should attempt to smoke the pipe. Pick up that it is not lit. Light the pipe. Smoke the pipe.

It attempted to smoke the pipe (like it should) returned

That pipe isn't lit.

Which should have been caught by the "local z" in the "SmokeCure ()" and sent to the "LightPipe ()". It wasn't.

Here is the revised alias.


<aliases>
  <alias
   match="smokecure"
   enabled="y"
   group="Smoke"
   send_to="12"
   sequence="100"
  >
  <send>require "wait"

wait.make (function () -- coroutine starts here

Balance = tonumber (GetVariable ("Balance"))
PipeSelect = GetVariable ("PipeSelect")
Pipe1 = GetVariable ("PipeValerian")
Pipe2 = GetVariable ("PipeElm")

-- Affliction Variables

    -- Smoke Elm Cures

Aeon = tonumber (GetVariable ("Aeon")) -- Elm
Deadening = tonumber (GetVariable ("Deadening")) -- Elm
Hellsight = tonumber (GetVariable ("Hellsight")) -- Elm

   -- Smoke Valerian Cures

Disfigurement = tonumber (GetVariable ("Disfigurement")) -- Valerian
ManaLeech = tonumber (GetVariable ("ManaLeech")) -- Valerian
Slickness = tonumber (GetVariable ("Slickness")) -- Valerian

   -- Pick Correct Pipe

if Disfigurement == 1 then
   Pipe = Pipe1
   SetVariable("PipeSelect", GetVariable ("PipeValerian"))
elseif ManaLeech == 1 then
   Pipe = Pipe1
   SetVariable("PipeSelect", GetVariable ("PipeValerian"))
elseif Slickness == 1 then
   Pipe = Pipe1
   SetVariable("PipeSelect", GetVariable ("PipeValerian"))
elseif Aeon == 1 then
   Pipe = Pipe2
   SetVariable("PipeSelect", GetVariable ("PipeElm"))
elseif Deadening == 1 then
   Pipe = Pipe2
   SetVariable("PipeSelect", GetVariable ("PipeElm"))
elseif Hellsight == 1 then
   Pipe = Pipe2
   SetVariable("PipeSelect", GetVariable ("PipeElm"))
else ColourNote ("white", "red", "No Smoke Cures Needed!!!")
     SetVariable("PipeSelect", "")
     Pipe = nil
end

  -- Try Smoke Pipe

if Balance == 1 then
   if Pipe then
      SmokePipe ()
   else ColourNote ("white", "red", "Pipe Not Picked!!!")
   end
end

-- Smoke Pipe Function

function SmokePipe ()

local Pipe = GetVariable ("PipeSelect")

if Pipe then  -- What did I do with that thing?

   Send ("Smoke " .. Pipe)

   local x = wait.match ("^(.*?) a long drag off your pipe\.$", 1)
   local y = wait.match ("^(.*?) pipe has nothing smokeable in it\.$", 1)
   local z = wait.match ("^(.*?) pipe isn\'t lit\.$", 1)

   if x then  -- Smooth? Isn't it?

      if Pipe == GetVariable ("PipeValerian") then  -- Is it Valerian I taste?

         SetVariable("FullPipeValerian", GetVariable ("FullPipeValerian") - 1)

      elseif Pipe == GetVariable ("PipeElm") then  -- Nah, this is Elm!

         SetVariable("FullPipeElm", GetVariable ("FullPipeElm") - 1)

      end  -- Better then turkish blend?

   elseif y then  -- Hey! I thought I packed this already!

      EmptyPipe ()

   elseif z then  -- Where is my lighter?

      LightPipe ()

   end  -- Are you (not) satisfied?

end  -- Always around when I need it!

end -- Smoke Pipe Function

-- Empty Pipe Function

function EmptyPipe ()

local Pipe = GetVariable ("PipeSelect")

if Pipe == GetVariable ("PipeValerian") then
      
   Send ("outr valerian")  -- Get Valerian from Rift
      
   local w = wait.match ("^(.*?) remove 1 valerian\, bringing the total in the Rift (.*?)$", 1)
   local x = wait.match ("^(.*?) have no valerian stored in the Rift\.$", 1)

   if w then  -- Got Valerian from Rift

      Send ("put valerian in " .. Pipe)
      SetVariable ("FullPipeValerian", 10)
      LightPipe ()
   
   elseif x then  -- No Valerian in Rift

      ColourNote ("white", "red", "You are out of Valerian!!!")

   end  -- Have Valerian?

elseif Pipe == GetVariable ("PipeElm") then

   Send ("outr elm")  -- Get Elm from Rift
   
   local y = wait.match ("^(.*?) remove 1 slippery elm\, bringing the total in the Rift (.*?)$", 1)
   local z = wait.match ("^(.*?) have no elm stored in the Rift\.$", 1)
   
   if y then  -- Got Elm from Rift

      Send ("put elm in " .. Pipe)
      SetVariable ("FullPipeElm", 10)
      LightPipe ()

   elseif z then  -- No Elm in Rift

      ColourNote ("white", "red", "You are out of elm!!!")

   end  -- Have Elm?

end  -- Is it the Valerian or Elm Pipe?

end -- Empty Pipe Function

-- Light Pipe Function

function LightPipe ()

local Pipe = GetVariable ("PipeSelect")

if Pipe then  -- Have Pipe

   Send ("light " .. Pipe)  -- Click, click, click, Pshh!

   local x = wait.match ("^(.*?) light your treasured pipe until it is smoking nicely\.$", 1)
   local y = wait.match ("^(.*?) pipe is already lit and burning nicely\.$", 1)
   local z = wait.match ("^(.*?) is nothing in the pipe to light\.$", 1)

   if x then  -- Don't let it go out man!

      SmokePipe ()

   elseif y then  -- I must be stupid!

      SmokePipe ()

   elseif z then  -- Wasn't this packed yesterday!

      EmptyPipe ()

   end  -- Whats the verdict?

end  -- Always around when I need it!

end -- light Pipe Function

end) -- Coroutine</send>
  </alias>
</aliases>


As usual, any feedback is appreciated.
Top

Posted by Darcon   (26 posts)  Bio
Date Reply #13 on Sun 25 Nov 2012 04:05 AM (UTC)
Message
Alright, I ended up giving up on that script because I just could't get it to work right. I went with triggers for all the wait strings. Now it works the right way. I still am a little confused as to why it didn't work the way that I had it. If someone could take a look at it and give me some pointers I would appreciate it.
Top

Posted by Nick Gammon   Australia  (23,102 posts)  Bio   Forum Administrator
Date Reply #14 on Sun 25 Nov 2012 05:55 AM (UTC)
Message

   local x = wait.match ("^(.*?) light your treasured pipe until it is smoking nicely\.$", 1)
   local y = wait.match ("^(.*?) pipe is already lit and burning nicely\.$", 1)
   local z = wait.match ("^(.*?) is nothing in the pipe to light\.$", 1)



I don't think that will work the way you want. It will wait for three lines to arrive, so if the one about "nothing in the pipe" arrives first it will be missed.

You probably want to use the "or" feature of regular expressions to test for all three lines in the one wait.match.

Template:regexp Regular expressions
  • Regular expressions (as used in triggers and aliases) are documented on the Regular expression tips forum page.
  • Also see how Lua string matching patterns work, as documented on the Lua string.find page.

- Nick Gammon

www.gammon.com.au, www.mushclient.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.


115,436 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.