<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<muclient>
<plugin
   name="Aardwolf_Spellups"
   author="Nick Gammon"
   id="b0a9cef2629fae2eacf97603"
   language="Lua"
   purpose="Does Aardwolf spellups"
   date_written="2008-07-05"
   requires="4.30"
   version="1.01"
   save_state="y"
   >
<description trim="y">

spellups            --> display current spellups in output window

spellup all         --> cast all possible spellups
spellup none        --> remove all spellups

spellup a,b,c       --> add spells a, b, c in that order 
                      (eg. spellup shield, blur, detect magic)
                      (or: spellup 72, 171, 35)

(or)

Spellups: a, b, c   --> add spells a, b, c 
                      (you can copy and paste output from current list)


spellup + blur, avoid    --> add more to the current list
spellup - night vision, detect magic   --> remove those from the list

spellup fast    --> cast all spellups immediately, as fast as you can
spellup pause   --> stop casting spellups until a resume
spellup resume  --> resume casting
spellup refresh --> requery server for current spells on us

spellup brief   --> show summary of spellup situation
spellup full    --> show full details

spellup other (name) --> try to spellup another player with all spellups
                          (eg. spellup other johnsmith)

spellup help    --> this message
</description>

</plugin>


<!--  Shared script stuff  -->

<script>
<![CDATA[

affect_world = "Spellups"
folder = "Aardwolf"

MAX_RETRIES = 10  -- max tries we will allow them to fail to cast

require "getworld"
require "tprint"
require "commas"
require "addxml"
require "checkplugin"
require "serialize"  

current_buffs = {}   -- what they currently have on them
cooldowns = {}       -- what is on cooldown
cast_attempt_count = {}  -- what we tried to cast
failed_attempt_count = {}  -- what we failed to cast
skip = {}            -- ones we have decided to ignore for now
stats = { position = "0", mana = "0", moves = "0" }           -- table will be replaced by Stats_Detector plugin

fighting = false
playing = false
last_buff = os.time ()

-- the next 3 items are saved to the plugin state file

-- known spells and recoveroes indexed by number
spells = {}      -- spells that exist
recoveries = {}  -- recoveries that exist
wanted_buffs = {}  -- what spellups they want

-- cross-reference for spells - given a name, returns a number
spells_xref = {}
recoveries_xref = {}


function capitalize (s)
  return string.sub (s, 1, 1):upper () .. string.sub (s, 2):lower ()
end -- capitalize 

function PrefixCheck (t, s)

   for name, item in pairs (t) do
      if string.match (name, "^" .. s) then -- prefix match, so "avoid" matches "avoidance"
        return name, item
      end -- if name matches
    end -- checking table

   return nil  -- not found
end -- PrefixCheck

function remove_spellup (sn)
  for i, buff in ipairs (wanted_buffs) do
     if sn == buff then
        table.remove (wanted_buffs, i)
        return true
     end -- if
  end -- for
end -- remove_spellup

-- make tables that let us convert spell name to spell number
function make_xrefs ()
  spells_xref = {}
  for k, v in pairs (spells) do
    spells_xref [v.name] = k
  end -- for each spell
 
  recoveries_xref = {}
  for k, v in pairs (recoveries) do
    recoveries_xref [v.name] = k
  end -- for each recovery

end -- make_xrefs


--   PLUGIN INSTALL ---

function OnPluginInstall ()
 -- on plugin install, convert variable into Lua table
  assert (loadstring (GetVariable ("wanted_buffs") or "")) ()
  assert (loadstring (GetVariable ("spells") or "")) ()
  assert (loadstring (GetVariable ("recoveries") or "")) ()
  
  make_xrefs ()

  if GetVariable ("enabled") == "false" then
    ColourNote ("yellow", "", "Warning: Plugin " .. GetPluginName ().. " is currently disabled.")
    check (EnablePlugin(GetPluginID (), false))
    return
  end -- they didn't enable us last time
  
  OnPluginEnable ()  -- do initialization stuff
  
end -- OnPluginInstall

function OnPluginDisconnect ()
  disconnected = true
  coroutine.resume (thread, "disconnect")  
end -- OnPluginDisconnect

-- pull in telnet option handling
dofile (GetPluginInfo (GetPluginID (), 20) .. "telnet_options.lua")
  
function OnPluginConnect ()
  TelnetOptionOn (TELOPT_SPELLUP)
  TelnetOptionOn (TELOPT_SKILLGAINS)
  disconnected = false
  
  thread = coroutine.create (buff_loop)
  resume_buff_loop ("connect")
  
end -- function OnPluginConnect

function OnPluginClose ()
  -- if enabled
  if GetPluginInfo (GetPluginID (), 17) then
    TelnetOptionOff (TELOPT_SPELLUP)
    TelnetOptionOff (TELOPT_SKILLGAINS)
  end -- if enabled
end -- OnPluginClose

function OnPluginEnable ()
  
  checkplugin ("0e191dc7829ff2ac2433c2d8", "AFK_Detector.xml")
  checkplugin ("8a710e0783b431c06d61a54c", "Stats_Detector.xml")
  checkplugin ("f5b05e8826711cdb0d141939", "Playing_Detector.xml")
 
  -- ensure world file exists
  local w = get_a_world (affect_world, folder)
  if (w == nil) then
     CallPlugin ("35dfdbf3afc8cbf60c91277c", "CreateWorldFile", folder .. "," .. affect_world)
     local w = get_a_world (affect_world, folder)
  end
  
  if w then
    w:DeleteOutput ()  
    w:Note "Spellups will appear here."

    w:Note ""
    show_wanted (w)
    
    w:SetOption ("do_not_show_outstanding_lines", 1)
    w:SetCommandWindowHeight (0)  -- no command window
    w:SetWorldWindowStatus (3) -- restore it     
  end -- world

  -- if we are connected when the plugin loads, it must have been reloaded whilst playing
  if IsConnected () then
    TelnetOptionOn (TELOPT_REQUEST_STATUS) -- get actual status (eg. afk, playing)
    OnPluginConnect ()
  end -- if already connected
  
  -- see if we are playing at install time
  playing = GetPluginVariable ("f5b05e8826711cdb0d141939", "playing") == "y"
  if playing then
     Send "slist noprompt"
  end

  
end -- OnPluginEnable

function OnPluginDisable ()
  TelnetOptionOff (TELOPT_SPELLUP)
  TelnetOptionOff (TELOPT_SKILLGAINS)
  local w = get_a_world (affect_world, folder)
  if w then
    w:SetWorldWindowStatus (2) -- minimize it on disable
  end -- if 
  
end -- OnPluginDisable


function resume_buff_loop (reason, other_reason)

  if disconnected then
    return
  end -- don't bother

  if coroutine.status (thread) ~= "suspended" then
    ColourNote ("red", "yellow", "Problem with spellup plugin - please reinstall.")
    return
  end -- if problem
  
  local ok, err = coroutine.resume (thread, reason, other_reason)
  if not ok then
     print (debug.traceback (thread))
     assert (ok, err)
  end -- if
end -- resume_buff_loop

--   SAVE STATE ---

function OnPluginSaveState ()
  SetVariable ("wanted_buffs", 
               "wanted_buffs = " .. serialize.save_simple (wanted_buffs))
  SetVariable ("spells", 
               "spells = " .. serialize.save_simple (spells))
  SetVariable ("recoveries", 
               "recoveries = " .. serialize.save_simple (recoveries))
               
  SetVariable ("enabled", tostring (GetPluginInfo (GetPluginID (), 17)))
               
end -- function OnPluginSaveState

--[[
Spell targets are:

   0 : No target
   1 : Combat / Attack  
   2 : Spellup/cure - can cast on others.
   3 : Spellup/cure - self only.   
   4 : Objects.

  Skilltype:
  
   
  1 for spell,
  2 for skill.

 --]]
 
-- parse spellheaders line, break into pieces
function parse_spell_line (line)
  local sn, name, target, duration, percent, recovery, skilltype = 
     string.match (line, "^(%d+)%,([A-Za-z0-9 ]+)%,(%d+)%,(%d+)%,(%d+),(-?%d+),(%d+)$")
  if not sn then
    ColourNote ("white", "re,d", "Invalid spellheaders line: " .. line)
    return nil
  end -- not a valid spell name
  
  return tonumber (sn), 
         trim (name:lower()),
         tonumber (target),
         tonumber (duration),
         tonumber (percent),
         tonumber (recovery),
         tonumber (skilltype)
         
end -- parse_spell_line

-- parse recoveries line, break into pieces
function parse_recoveries_line (line)
  local sn, name, duration = string.match (line, "^(%d+)%,([A-Za-z0-9 ]+)%,(%d+)$")

  if not sn then
    ColourNote ("white", "red", "Invalid recoveries line: " .. line)
    return nil
  end -- not a valid spell name
  
  return tonumber (sn), trim (name:lower ()), tonumber (duration)
 
end -- parse_recoveries_line


function OnPluginBroadcast (msg, id, name, text)
 
  local old_fighting = fighting
  local old_playing = playing
  local old_AFK = AFK
  local old_position = stats.position
  local old_mana = stats.mana
  local old_moves = stats.moves
 
  -- AFK check
  if id == "0e191dc7829ff2ac2433c2d8" then
    AFK = text == "y"
     
  -- stats change
  elseif id == "8a710e0783b431c06d61a54c" then
    stats = GetPluginVariableList("8a710e0783b431c06d61a54c")
    fighting = stats.fighting == "y"
    
  -- playing status
  elseif id == "f5b05e8826711cdb0d141939" then
    playing = text == "y"

  end -- if

  if AFK ~= old_AFK then
    -- if we are too quick, it rejects it
    ResetTimer ("affects_timer")  -- heartbeat not needed for a while yet
  elseif playing ~= old_playing then
     resume_buff_loop ("playing_change", playing)
  elseif fighting ~= old_fighting then
     resume_buff_loop ("fighting_change", fighting)
  elseif stats.position ~= old_position then
     resume_buff_loop ("position_change", stats.position)
  elseif tonumber (stats.mana) > tonumber (old_mana) then
     low_mana = nil
     resume_buff_loop ("more_mana", stats.mana)
  elseif tonumber (stats.moves) > tonumber (old_moves) then
     low_moves = nil
     resume_buff_loop ("more_moves", stats.moves)
  end -- something changed
  
 old_fighting = fighting
 old_playing = playing
 old_AFK = AFK
 old_position = stats.position
 old_mana = stats.mana
 old_moves = stats.moves
  
end  -- OnPluginBroadcast


]]>
</script>


<!--  {spellheaders}  -->

<triggers>

 <trigger
   enabled="y"
   match="{spellheaders noprompt}"
   script="spellheaders_redirect"
   omit_from_output="y"
   name="start_spellheaders"
   sequence="100"
  >
  </trigger>
    
  <trigger
   enabled="n"
   match="*"
   script="spellheaders_redirect"
   name="multi_line_spellheaders"
   omit_from_output="y"
   sequence="10"
  >
  </trigger>  
  
</triggers>


<script>
<![CDATA[

-- spells redirector
function spellheaders_redirect (name, line, wildcards, styles)
  
  -- start of spells list? remove old ones
  if name == "start_spellheaders" then
    spells = {}
    spells_count = 0
    EnableTrigger ("multi_line_spellheaders", true)  -- capture subsequent lines
    return
  end -- if

  if line == "{/spellheaders}" then  
    EnableTrigger ("multi_line_spellheaders", false)  -- no more lines to go
    ColourNote ("green", "", "Loaded information about " .. spells_count .. " spells.")
    make_xrefs ()
    SaveState ()
    return
  end -- if
  
  local sn, name, target, duration, percent, recovery, skilltype = parse_spell_line (line)
  
  if not sn then return end
  
  spells [sn] = { 
      name = name:lower (),   -- name of spell
      target = target,        -- target code
      percent = percent,      -- percent known for this character
      recovery = recovery,    -- depends on which recovery
      skilltype = skilltype,  -- 1 = spell, 2 = skill
      } -- end spells table item
      
  spells_count = spells_count + 1
  
end -- function spellheaders_redirect 
]]>
</script>



<!--  {spellheaders spellups}  -->

<triggers>

 <trigger
   enabled="y"
   match="{spellheaders spellup noprompt}"
   script="spellups_redirect"
   omit_from_output="y"
   name="start_spellups"
   sequence="100"
  >
  </trigger>
    
  <trigger
   enabled="n"
   match="*"
   script="spellups_redirect"
   name="multi_line_spellups"
   omit_from_output="y"
   sequence="10"
  >
  </trigger>  
  
</triggers>


<script>
<![CDATA[

-- spells redirector
function spellups_redirect (name, line, wildcards, styles)
  
  -- start of spells list? remove old ones
  if name == "start_spellups" then
  
    -- mark all as not spellups for now
    for k, v in pairs (spells) do
      v.spellup = false
    end -- for
    
    EnableTrigger ("multi_line_spellups", true)  -- capture subsequent lines
    return
  end -- if

  if line == "{/spellheaders}" then  
    EnableTrigger ("multi_line_spellups", false)  -- no more lines to go
    have_slist = true  -- we now know all we need to
    make_xrefs ()
    SaveState ()
    return
  end -- if
  
  local sn, name, target, duration, percent, recovery, skilltype = parse_spell_line (line)
  
  if not sn then return end
  
  spells [sn] = { 
      name = name:lower (),   -- name of spell
      target = target,        -- target code
      percent = percent,      -- percent known for this character
      recovery = recovery,    -- depends on which recovery
      spellup = true,         -- this is a spellup
      skilltype = skilltype,  -- 1 = spell, 2 = skill
            } -- end spells table item
      

end -- function spellups_redirect 
]]>
</script>

<!--  {spellheaders affected}  -->


<triggers>

  
 <trigger
   enabled="y"
   match="{spellheaders affected noprompt}"
   script="affected_redirect"
   omit_from_output="y"
   name="start_affected"
   sequence="100"
  >
  </trigger>
    
  <trigger
   enabled="n"
   match="*"
   script="affected_redirect"
   name="multi_line_affected"
   omit_from_output="y"
   sequence="10"
  >
  </trigger>  
    
</triggers>


<script>
<![CDATA[

-- current affects redirector
function affected_redirect (name, line, wildcards, styles)
  
  -- start of affected list? remove old ones
  if name == "start_affected" then
  
    -- we must leave the "true" entries, because they won't be on the list possibly
    for k, v in pairs (current_buffs) do
      if v ~= true then
        current_buffs [k] = nil
      end -- if    
    end -- for
    check (EnableTrigger ("multi_line_affected", true))  -- capture subsequent lines
    return
  end -- if

  if line == "{/spellheaders}" then  
    check (EnableTrigger ("multi_line_affected", false))  -- no more lines to go
    return
  end -- if
  
  local sn, name, target, duration, percent, recovery, skilltype = parse_spell_line (line)

  if sn then
    current_buffs [sn] = os.time () + duration
  end  -- if
  
end -- function affected_redirect 

]]>
</script>


<!--  {spellheaders learned}  -->

<triggers>
   
<trigger
   enabled="y"
   match="{spellheaders learned noprompt}"
   script="learned_redirect"
   omit_from_output="y"
   name="start_learned"
   sequence="100"
  >
  </trigger>
    
  <trigger
   enabled="n"
   match="*"
   script="learned_redirect"
   name="multi_line_learned"
   omit_from_output="y"
   sequence="10"
  >
  </trigger>  
  
</triggers>


<script>
<![CDATA[

-- learned spells redirector
function learned_redirect (name, line, wildcards, styles)
  -- start of learned list? 
  if name == "start_learned" then
  
    -- reset learned amount
    for sn, v in pairs (spells) do
      v.percent = 0
    end -- for
    
    check (EnableTrigger ("multi_line_learned", true))  -- capture subsequent lines
    return
  end -- if

  if line == "{/spellheaders}" then  
    check (EnableTrigger ("multi_line_learned", false))  -- no more lines to go
    return
  end -- if
  
  local sn, name, target, duration, percent, recovery, skilltype = parse_spell_line (line)

  -- update our percent learned
  if sn then
    if spells[sn] == nil then
       Note("New spells have been added. Please type 'spellup refresh'")
    else
       spells [sn].percent = percent
    end
  end  -- if
  
end -- function learned_redirect 

]]>
</script>



<!--  {recoveries}  -->

<triggers>

 
<trigger
   enabled="y"
   match="{recoveries noprompt}"
   script="recoveries_redirect"
   omit_from_output="y"
   name="start_recoveries"
   sequence="100"
  >
  </trigger>
  
  <trigger
   enabled="n"
   match="*"
   script="recoveries_redirect"
   name="multi_line_recoveries"
   omit_from_output="y"
   sequence="10"
  >
  </trigger>  
    
  
</triggers>


<script>
<![CDATA[



-- cooldowns redirector
function recoveries_redirect (name, line, wildcards, styles)
  
  -- start of recoveries list? remove old ones
  if name == "start_recoveries" then
    recoveries = {}
    recoveries_count = 0
    EnableTrigger ("multi_line_recoveries", true)  -- capture subsequent lines
    return
  end -- if

  if line == "{/recoveries}" then  
    EnableTrigger ("multi_line_recoveries", false)  -- no more lines to go
    ColourNote ("green", "", "Loaded information about " .. recoveries_count .. " recoveries.")
    make_xrefs ()
    SaveState ()
    return
  end -- if
  
  local sn, name, duration = parse_recoveries_line (line)
  
  if not sn then
    return
  end -- not a valid spell name
 
  recoveries [sn] = { name = name  }
  recoveries_count = recoveries_count + 1
  
end -- function recoveries_redirect 


]]>
</script>




<!--  {recoveries affected}  -->

<triggers>

    
<trigger
   enabled="y"
   match="{recoveries affected noprompt}"
   script="recoveries_affected_redirect"
   omit_from_output="y"
   name="start_affected_recoveries"
   sequence="100"
  >
  </trigger>
  
  <trigger
   enabled="n"
   match="*"
   script="recoveries_affected_redirect"
   name="multi_line_affected_recoveries"
   omit_from_output="y"
   sequence="10"
  >
  </trigger>  
  

</triggers>


<script>
<![CDATA[

-- cooldowns redirector
function recoveries_affected_redirect (name, line, wildcards, styles)
  
  -- start of recoveries list? remove old ones
  if name == "start_affected_recoveries" then
    cooldowns = {}
    check (EnableTrigger ("multi_line_affected_recoveries", true))  -- capture subsequent lines
    return
  end -- if

  if line == "{/recoveries}" then  
    check (EnableTrigger ("multi_line_affected_recoveries", false))  -- no more lines to go
    return
  end -- if
  
  local sn, name, duration = parse_recoveries_line (line)
  
  if not sn then
    return
  end -- not a valid spell name
  
  cooldowns [sn] = os.time () + duration -- add to our table
  
end -- function recoveries_affected_redirect 


]]>
</script>


<!--  show buffs on us -->

<script>
<![CDATA[

-- note maximum spell name length is 24 at present

function show_spells (w, t, colour)
 
  for _, v in ipairs (t) do
     w:Hyperlink ("help " .. v.name, capitalize (v.name), "Help on '" .. capitalize (v.name) .. "'", 
                colour, "", 
                false)  -- not URL
          
     if tonumber (v.duration) then
     
       local time_to_go = v.duration - os.time ()
       local time_colour
       if time_to_go >= 180 then
         time_colour = "lime"
       elseif time_to_go >= 60 then
         time_colour = "yellow"
       else
         time_colour = "deeppink"
       end -- if
       
       w:ColourNote (time_colour, "", 
                    string.format ("%s %4s", string.rep (" ", 25 - #v.name), 
                                   convert_time (time_to_go)))
     else
       w:Note ""
     end      
  end -- for

end -- show_spells

function show_status (w)
  -- show our status
    
  w:ColourTell ("silver", "", "Position: " .. capitalize (stats.position_str or "unknown"))
  if AFK then
    w:ColourTell ("cyan", "", " (AFK)")
  end -- AFK
  if low_mana then
    w:ColourTell ("yellow", "", " (Mana low)")
  end -- low_mana
  if low_moves then
    w:ColourTell ("yellow", "", " (Moves low)")
  end -- low_moves
  
  if not playing then
    w:ColourTell ("cyan", "", " (Not playing)")
  end -- not playing
  
  if paused then
    w:ColourTell ("orangered", "", " (Paused)")
  end -- paused
  
  w:Note ""
end -- show_status

function show_brief_version (w) 
  local count, badcount = 0, 0
  for k in pairs (current_buffs) do
    if spells [k].target == 1 or    -- combat
       spells [k].skilltype ~= 1 then  -- not a spell
      badcount = badcount + 1
    else
      count = count + 1
    end
  end -- for

  w:Tell ("Spellups: " .. count)
  
  if badcount > 0 then
    w:ColourTell ("red", "", " Debuffs: " .. badcount)
  end -- if
  
  count = 0
  for k in pairs (cooldowns) do
    count = count + 1
  end -- for  

  w:Note (" Recoveries: " .. count)

  
  w:ColourTell ("olive", "", "Requested: ".. #wanted_buffs)
  
  local pending, oncooldown = 0, 0
  
  for _, v in ipairs (wanted_buffs) do
    if not current_buffs [v] then
      if cooldowns [spells [v].recovery] then
        oncooldown = oncooldown + 1
      else
        pending = pending + 1
      end -- if
    end -- not cast yet
  end -- for

  w:ColourTell ("gray", "", " Pending: " .. pending)
  w:ColourNote ("darkslateblue", "", " Cooldown: " .. oncooldown)
  
  show_status (w)
  
end -- show_brief_version



-- show our current state
function show_current_buffs (name, line, wildcards, styles)
  
  local w = get_a_world (affect_world, folder)
  if not w then
    return
  end
  
  if next (spells) == nil or 
     next (recoveries) == nil or
     stats.hp == nil then
    return
  end -- no spells known
  
  
  w:DeleteOutput ()

  if not IsConnected () then
    w:Note "Spellups will appear here."
    w:ColourNote ("silver", "", "Not connected to Aardwolf.")
    w:Note ""
    show_wanted (w)
    return
  end -- not connected
  
  if brief then
    show_brief_version (w)
    return
  end -- if brief
  
  if next (current_buffs) == nil then
    w:Note ("You are not affected by any spellups.")
    w:Note ""
  else
  
    local good = {}
    local bad = {}
    -- local ugly = {}  -- joke
  
    local count, badcount = 0, 0
    for k, v in pairs (current_buffs) do
    if spells [k].target == 1 or    -- combat
       spells [k].skilltype ~= 1 then  -- not a spell
        table.insert (bad, { name = spells [k].name, duration = v } )
        badcount = badcount + 1
      else
        table.insert (good, { name = spells [k].name, duration = v } )
        count = count + 1
      end -- if
    end -- for
    w:Tell ("Spellups affecting you: (" .. count .. ")")
    if badcount > 0 then
      w:ColourTell ("red", "", " / (" .. badcount .. ")")
    end -- if
    w:Note ":"
    
    w:Note ""
    
    table.sort (good, function (a, b) 
        if tonumber (a.duration) and tonumber (b.duration) then
          return a.duration < b.duration 
        end  -- if
        return a.name < b.name
        end )
    table.sort (bad, function (a, b) 
        if tonumber (a.duration) and tonumber (b.duration) then
          return a.duration < b.duration 
        end  -- if
        return a.name < b.name
    end )
            
    show_spells (w, good, "green")
    
    if #good > 0 and #bad > 0 then
      w:Note ""
    end
  
    show_spells (w, bad, "red")

  end -- if at least one spell
  
  
  w:Note ""
  
  if next (cooldowns) == nil then
    w:Note ("You have no spells on recovery.")
    w:Note ""
  else
   
    local cool = {}
    
    for k, v in pairs (cooldowns) do
      if (recoveries[k]) then
         table.insert (cool, { name = recoveries [k].name, duration = v } )
      else
         Note ("Warning - recovery " .. k .. " not known - please  type:  spellup refresh'")
        end -- if
    end -- for

    table.sort (cool, function (a, b) 
        if tonumber (a.duration) and tonumber (b.duration) then
          return a.duration < b.duration 
        end  -- if
        return a.name < b.name
    end )

    if #cool == 1 then
      w:Note ("Active recovery (1):")
    else
      w:Note ("Active recoveries (" .. #cool .. "):")
    end -- if    
    w:Note ""
    
    show_spells (w, cool, "yellow")
    
  end -- if at least one cooldown
  
  w:Note ""
    
  show_wanted (w)
  
  -- what is pending I wonder?
  
  pending = {}
  awaiting_cooldown = {}
  
  for _, v in ipairs (wanted_buffs) do
    if not current_buffs [v] then
      if cooldowns [spells [v].recovery] then
        table.insert (awaiting_cooldown, capitalize (spells [v].name))
      else
        table.insert (pending, capitalize (spells [v].name))
      end -- if
    end -- not cast yet
  end -- for
 
  if #pending > 0 then
    w:ColourNote ("gray", "", "Pending (" .. #pending .. 
                  "): " .. table.concat (pending, ", "))
  end -- some pending
  if #awaiting_cooldown > 0 then
    w:ColourNote ("darkslateblue", "", "On cooldown (" .. #awaiting_cooldown .. 
                  "): " .. table.concat (awaiting_cooldown, ", "))
  end -- some pending

  show_status (w)
  
end -- function show_current_buffs 

]]>
</script>




<!--  show wanted buffs -->

<script>
<![CDATA[

function show_wanted (w)

 w = w or GetWorldById (GetWorldID ())
 
 if next (spells) == nil or 
     next (recoveries) == nil then
    return
  end -- no spells known
 
 if #wanted_buffs == 0 then
    w:ColourNote ("olive", "", "You have not requested any spellups.")
  else
    local buf_names = {}
    for _, sn in ipairs (wanted_buffs) do
      table.insert (buf_names, capitalize (spells [sn].name))
    end -- for
    w:ColourNote ("olive", "", "Requested (" .. #buf_names .. 
                  "): " .. table.concat (buf_names, ", "))
  end -- if
end -- show_wanted

]]>
</script>

<!--  configure wanted buffs -->

<aliases>

  <alias
   name="spellup"
   script="wanted_spellups"
   match="^spellups?\:?\s*((?<action>\+|\-)\s*)?(?<list>[A-Za-z0-9, ]+)?$"
   enabled="y"
   regexp="y"
   ignore_case="y"
   sequence="100"
  >
  </alias>
   
</aliases>


<script>
<![CDATA[

--[[

Spell targets are:

   0 : No target
   1 : Combat / Attack  
   2 : Spellup/cure - can cast on others.
   3 : Spellup/cure - self only.   
   4 : Objects.

--]]
   
function spellup_none (name, line, wildcards)
  if wildcards.action ~= "" then
    ColourNote ("red", "", "'" .. line .. "' does not make sense.")
    return true
  end -- if
  
  show_wanted ()
  wanted_buffs = {}
  ColourNote ("yellow", "", "All those above spellups REMOVED from spellups list.")
  return false  -- keep going
  
end -- spellup_none

function spellup_all (name, line, wildcards)

  if wildcards.action ~= "" then
    ColourNote ("red", "", "'" .. line .. "' does not make sense.")
    return
  end -- if
  wanted_buffs = {}
  
  for sn, v in pairs (spells) do
    if spells [sn].percent > 1 and
       spells [sn].spellup and
       spells [sn].skilltype == 1 then
       table.insert (wanted_buffs, sn) 
    end -- if possible to spellup this one
  end -- for
  ColourNote ("lime", "", "Set spellups list to all " .. #wanted_buffs .. " possible spellups.")
  if #wanted_buffs == 0 then
    ColourNote ("teal", "", "If 'spells spellup' show spells, you may need to 'spellup refresh'")
    ColourNote ("teal", "", "to reload your known list of spells.")
  end -- if none

  return false -- keep going
end -- spellup_all

function spellup_fast (name, line, wildcards)

  if paused then
    ColourNote ("green", "", "Spellups casting resumed.")
    paused = false
  end -- was paused

  -- better check what we can do
  local count = 0
  
  for _, v in ipairs (wanted_buffs) do
    if not current_buffs [v] then
      if not cooldowns [spells [v].recovery] then
        count = count + 1
      end -- if
    end -- not cast yet
  end -- for

  if count == 0 then
    ColourNote ("teal", "", "No pending spellups.")
  else
    ColourNote ("green", "", "Doing fast cast of " .. count .. " pending spellup(s).")
    resume_buff_loop ("fast", line)
  end
  
  return true  -- done

end -- spellup_fast

function spellup_pause (name, line, wildcards)

  if not paused then
    ColourNote ("orangered", "", "Spellups paused, type 'spellup resume' to continue.")
    paused = true
  else
    ColourNote ("orangered", "", "Spellups already paused, type 'spellup resume' to continue.")
  end -- if 
  return true  -- done
      
end -- spellup_pause

function spellup_resume (name, line, wildcards)

  if paused then
    ColourNote ("green", "", "Spellups casting resumed.")
    paused = false
  else
    ColourNote ("green", "", "Spellups already active.")
  end  -- if

  return false -- keep going
  
end -- spellup_resume

function spellup_help (name, line, wildcards)
  ColourNote ("teal", "", world.GetPluginInfo (world.GetPluginID (), 3))
  return true -- done  
end -- spellup_help

function spellup_refresh (name, line, wildcards)

  if not playing then
   ColourNote ("orangered", "", "Cannot refresh if you are not playing.")
  elseif AFK then
   ColourNote ("orangered", "", "Cannot refresh if you you are AFK.")
  else
   ColourNote ("green", "", "Refreshing affected / learned / spellup list.")
   Send "slist noprompt"
   Send "slist affected noprompt"
   Send "slist learned noprompt"
   Send "slist spellup noprompt"
  end -- if      
  return true -- done    
end -- spellup_refresh

function spellup_brief (name, line, wildcards)
  ColourNote ("teal", "", "Brief spellup list will be shown. Type 'spellup full' to see more info.")
  brief = true
  show_current_buffs ()
  return true -- done  
end -- spellup_brief

function spellup_full (name, line, wildcards)
  ColourNote ("teal", "", "Full spellup list will be shown. Type 'spellup brief' to see less info.")
  brief = false
  show_current_buffs ()
  return true -- done  
end -- spellup_full

function spellup_other (who)
  
  for sn, v in pairs (spells) do
    if spells [sn].percent > 1 and   -- we know it
       spells [sn].spellup and       -- it is a spellup
       spells [sn].target == 2 and   -- can be cast on others
       spells [sn].skilltype == 1 then   -- spell not skill
         Send ("cast '" .. spells [sn].name .. "' " .. who)  -- cast it 
    end -- if possible to spellup this one
  end -- for

end -- spellup_other

spellup_options = {
  none    = spellup_none,     --> no spellups
  all     = spellup_all,      --> set all possible
  fast    = spellup_fast,     --> quickly cast what you can
  pause   = spellup_pause,    --> pause doing spellups
  resume  = spellup_resume,   --> resume after pause
  help    = spellup_help,     --> show help 
  refresh = spellup_refresh,  --> request affected / learned / spellup from server
  brief   = spellup_brief,    --> show brief list
  full    = spellup_full,     --> show full list
  }
  
function wanted_spellups (name, line, wildcards)

  if wildcards.list then
  
    wildcards.list = trim (wildcards.list):lower ()
    
    local who = string.match (wildcards.list, "^other (%a+)$")
    if who then
      spellup_other (who)
      return
    end -- if spellup some other guy
    
    local f = spellup_options [wildcards.list]
    
    if f then
      if f (name, line, wildcards) then
        return
      end -- all done
    else
      
      -- before we change anything, make sure they all exist
      local invalid = false
      
      new_wanted_list = {}
      
      -- check names are valid
      for item in string.gmatch(wildcards.list, "[^,]+") do 
        item = trim (item):lower ()
        local sn = tonumber (item)
        local name 
        
        -- see if numeric spell numbner given
        if sn and not spells [sn] then
          ColourNote ("red", "", "Spell number '" .. item .. "' does not exist.")
          invalid = true
          sn = nil
        elseif not sn then
          -- look up word
          sn = spells_xref [item]  -- look for exact match first 
                                   -- (otherwise "bless" might match "bless weapon")
          if not sn then
            _, sn = PrefixCheck (spells_xref, item)
          end -- not found by exact match
          if not sn then
            ColourNote ("red", "", "Spell named '" .. item .. "' does not exist.")
            invalid = true
          end -- name mot found
        end -- if
        
        
        -- if found test it was ok
        if sn then
           name = spells [sn].name
           if spells [sn].percent == 0 then
             ColourNote ("red", "", "You have not learnt '" .. name .. "'.")
             invalid = true
           elseif spells [sn].percent == 1 then
             ColourNote ("red", "", "You have not practised '" .. name .. "'.")
             invalid = true
           elseif not spells [sn].spellup then
             ColourNote ("red", "", "Spell '" .. name .. "' is not a spellup.")
             invalid = true
          elseif spells [sn].skilltype ~= 1 then
             ColourNote ("red", "", "Spell '" .. name .. "' is a skill, not a spell.")
             invalid = true
           else
             table.insert (new_wanted_list, sn)
           end -- if 
         end -- if number or name found
      end  -- for each spell in the list
      
      if invalid then
        ColourNote ("white", "red", "Spellups list contains or or more problems. List not changed.")
        show_wanted ()
        return
      end -- if
  
      -- add to existing list?
      if wildcards.action == "+" then
        for _, v in ipairs (new_wanted_list) do
          remove_spellup (v) -- don't have it there twice
          table.insert (wanted_buffs, v)
          ColourNote ("lime", "", capitalize (spells [v].name) .. " added to end of spellups list.")
        end -- for       
      -- remove from list?  
      elseif wildcards.action == "-" then
        for _, v in ipairs (new_wanted_list) do
          if remove_spellup (v) then
            ColourNote ("yellow", "", capitalize (spells [v].name) .. " removed from spellups list.")
          else
            ColourNote ("yellow", "", capitalize (spells [v].name) .. " was NOT in the spellups list.")
          end -- if 
        end -- for         
      else
        wanted_buffs = new_wanted_list
      end -- if
    end -- new list
    
  end -- if wildcards

  show_wanted ()
  resume_buff_loop ("spellup", line)

  SaveState ()
end -- wanted_spellups


]]>
</script>

<!--  {affon}  -->

<triggers>

  <trigger
   enabled="y"
   omit_from_output="y"
   match="^\{affon\}(?<sn>\d+)\,(?<time>\d+)$"
   script="affect_on"
   regexp="y"
   sequence="100"
  >
  </trigger>
    
  <trigger
   enabled="y"
   omit_from_output="y"
   match="^\{affoff\}(?<sn>\d+)$"
   script="affect_off"
   regexp="y"
   sequence="100"
  >
  </trigger>
  
</triggers>


<script>
<![CDATA[
  

function affect_on (name, line, wildcards)
  local sn = tonumber (wildcards.sn)
  local time = tonumber (wildcards.time)
  
  cast_attempt_count [sn] = nil
  failed_attempt_count [sn] = nil
  skip [sn] = nil
  current_buffs [sn] = os.time () + time
  resume_buff_loop ("affecton", line)
  
  -- success? must have had required mana/moves
  low_mana = nil
  low_moves = nil

  if not spells [sn] then return end
  
  if spells [sn].target == 1 or    -- combat
     spells [sn].skilltype ~= 1 then  -- not a spell
       BroadcastPlugin (2, spells [sn].name)  -- notify we got bad spell
     else
       BroadcastPlugin (1, spells [sn].name)  -- notify we got good spell
  end -- if bad spell
  
end -- affect_on

function affect_off (name, line, wildcards)
  local sn = tonumber (wildcards.sn)
  
  cast_attempt_count [sn] = nil
  failed_attempt_count [sn] = nil
  current_buffs [sn] = nil

  -- with a spell worn off, retry all skipped spells
  for k in pairs (skip) do
    skip [k] = nil
    cast_attempt_count [k] = nil
  end -- for
  
  resume_buff_loop ("affectoff", line)
  
  if not spells [sn] then return end
  
  if spells [sn].target == 1 or    -- combat
     spells [sn].skilltype ~= 1 then  -- not a spell
       BroadcastPlugin (4, spells [sn].name)  -- notify we lost bad spell
     else
       BroadcastPlugin (3, spells [sn].name)  -- notify we lost good spell
  end -- if bad spell
    
end -- affect_off

]]>
</script>

<!--  {recon}  -->

<triggers>

    
 <trigger
   enabled="y"
   omit_from_output="y"
   match="^\{recon\}(?<sn>\d+)\,(?<time>\d+)$"
   script="recovery_on"
   regexp="y"
   sequence="100"
  >
  </trigger>
    
  <trigger
   enabled="y"
   omit_from_output="y"
   match="^\{recoff\}(?<sn>\d+)$"
   script="recovery_off"
   regexp="y"
   sequence="100"
  >
  </trigger>
  
</triggers>


<script>
<![CDATA[
  

function recovery_on (name, line, wildcards)
  local sn = tonumber (wildcards.sn)
  local time = tonumber (wildcards.time)
  
  cooldowns [sn] = os.time () + time
  resume_buff_loop ("recoveryon", line)
  
end -- recovery_on

function recovery_off (name, line, wildcards)
  local sn = tonumber (wildcards.sn)
  
  cooldowns [sn] = nil
  resume_buff_loop ("recoveryoff", line)
  
end -- recovery_off

]]>
</script>

<!--  {skillgain}  -->

<triggers>

      
 <trigger
   enabled="y"
   omit_from_output="y"
   match="^\{skillgain\}(?<sn>\d+)\,(?<percent>\d+)$"
   script="skillgain"
   regexp="y"
   sequence="100"
  >
  </trigger>
       
</triggers>


<script>
<![CDATA[    

-- we gained a skill
function skillgain (name, line, wildcards)
  local sn = tonumber (wildcards.sn)
  local percent = tonumber (wildcards.percent)

  spells [sn].percent = percent
  if spells [sn] then
    ColourNote ("yellow", "", string.format ("Your proficiency at %s is now %i%%.",
                spells [sn].name, percent))
  end --  if exists                
  resume_buff_loop ("skillgain", line)

end -- skillgain


]]>
</script>

<!--  heartbeat timer  -->

<timers>
  <timer name="affects_timer"
         enabled="y" 
         second="5.00" 
         script="heartbeat"
         >


  </timer>
 
 <timer name="display_timer"
         enabled="y" 
         second="1.00" 
         script="display_timer"
         active_closed="y"
         >
  </timer>
    
</timers>

<script>
<![CDATA[  

-- kick the loop along, in case nothing happening
function heartbeat (timername)
  resume_buff_loop "timer"
end -- heartbeat 

-- redisplay our status every second
function display_timer (timername)
  show_current_buffs ()
end -- heartbeat 

]]>
</script>

<!--  {sfail}  -->

<triggers>
  <trigger
   enabled="y"
   omit_from_output="y"
   match="^\{sfail\}(?<sn>\-?\d+)\,(?<target>\d+)\,(?<reason>\d+)\,(?<recovery>\-?\d+)$"
   script="spell_failure"
   regexp="y"
   sequence="100"
  >
  </trigger>
  
  <trigger
   custom_colour="2"
   enabled="y"
   match="You do not know a '*' spell."
   script="unknown_spell"
   sequence="100"
  >
  </trigger>

</triggers>


<script>
<![CDATA[  

--[[
   1: Regular fail
   2: Already affected
   3: Recovery blocked it - 4th value is recovery or -1 (notarg)
   4: Not enough mana
   5: Nocast room.  (notarg)
   6: Can't concentrate (in combat) (notarg)
   7: Spell disabled (notarg)
   8: Spell not known (notarg)
   9: Tried to cast self-only spell on another.
  10: Resting / sleeping
  11: Other.
  12: Not enough moves (some skills require moves not mana).

  --]]
  


function spell_failure (name, line, wildcards)
  local sn = tonumber (wildcards.sn)
  local target = tonumber (wildcards.target)
  local reason = tonumber (wildcards.reason)
  local recovery = tonumber (wildcards.recovery)
 
--  print ("spell failure: sn=", sn, "target=", target, "reason=", reason, "recovery=", recovery)
  
  -- REGULAR FAIL
  if reason == 1 then --  Regular fail - recast
     failed_attempt_count [sn] = (failed_attempt_count [sn] or 0) + 1
     if failed_attempt_count [sn] > MAX_RETRIES then
       ColourNote ("red", "", "Failed to cast '" .. capitalize (spells [sn].name) .. 
                   "' "  .. MAX_RETRIES .. " times. Removing from spellup list.")
       remove_spellup (sn)
     end -- if
     
  -- ALREADY AFFECTED
  elseif reason == 2 then  -- Already affected - note that
     current_buffs [sn] =  current_buffs [sn] or true  -- maybe it just got cast

  -- ON COOLDOWN
  elseif reason == 3 then  -- On recovery
     cooldowns [recovery] =  cooldowns [recovery] or true  -- maybe it just got cast
     
  -- NOT ENOUGH MANA
  elseif reason == 4 then  -- No mana, skip it
     low_mana = tonumber (stats.mana)

  -- SPELL DISABLED
  elseif reason == 7 then  -- OooO  - spell disabled
     ColourNote ("red", "", "Spell '" .. capitalize (spells [sn].name) .. 
                 "' disabled. Removing from spellup list.")
     remove_spellup (sn)
     
  -- SPELL NOT KNOWN
  elseif reason == 8 then  -- Not known - maybe remorted or something
     if sn ~= -1 and spells [sn] then
       if remove_spellup (sn) then
         ColourNote ("red", "", "Unknown spell '" .. capitalize (spells [sn].name) .. 
                     "'. Removing from spellup list.")
       end -- if in list
     end -- if known

  -- OTHER REASON
  elseif reason == 11 then  -- Other - who knows?
     if sn ~= -1 and spells [sn] then
       if remove_spellup (sn) then
         ColourNote ("red", "", "Problem casting '" .. capitalize (spells [sn].name) .. 
                     "'. Removing from spellup list.")
       end -- if in list
     end -- if known
                
  -- NOT ENOUGH MOVES
  elseif reason == 12 then  -- No moves, skip it
     low_moves = tonumber (stats.moves)
     
  end -- if 
  
  resume_buff_loop ("sfail", line)

end -- spell_failure

-- eg. You do not know a 'berserk' spell.

function unknown_spell (name, line, wildcards)
  local sn = spells_xref [wildcards [1]:lower ()]
  
  if sn then
    if remove_spellup (sn) then
      ColourNote ("red", "", "Unknown spell '" .. capitalize (spells [sn].name) .. 
                   "'. Removing from spellup list.")
    end -- in list in the first place    
  end -- if
    
end -- unknown_spell

]]>
</script>


<!--  MAIN LOOP  -->

<script>
<![CDATA[



--[[

positions:
   0 = dead
   1 = sleeping
   2 = resting
   3 = sitting  
   4 = fighting
   5 = standing
 

--]]


-- called by coroutine to attempt one spellup

function try_to_buff_us (reason)

  local fast = reason == "fast"
  
  -- need a certain minimal amount of information to proceed
  if next (spells) == nil or 
     next (recoveries) == nil or
     stats.hp == nil then
    return
  end -- tables not set up  

  -- proceed through wanted ones until we find one we can cast
  for _, sn in ipairs (wanted_buffs) do
  
    -- uh oh, they want a non-existant spell
    if not spells [sn] then
      return
    end -- spell doesn't exist, strange
    
    local active = current_buffs [sn]  -- is it already active?
    local cooldown = cooldowns [spells [sn].recovery]  -- is its cooldown active?
    
--    print ("sn=", sn, "name=", spells [sn].name, "active=", active)
    
    -- recast this one if it is not active, and the cooldown has expired
    if not active and       -- not already on us
       not cooldown and     -- is not cooling down
       (fast or not skip [sn]) then   -- not skipping because we couldn't cast
      cast_attempt_count [sn] = (cast_attempt_count [sn] or 0) + 1
      if cast_attempt_count [sn] > MAX_RETRIES then
        skip [sn] = true
        ColourNote ("yellow", "", "Too many attempts to cast '" .. capitalize (spells [sn].name) ..
                    "', skipping it for now.")
      else
        Send ("cast '" .. spells [sn].name .. "'")
        last_cast_time = os.time ()
        spells [sn].last_cast = os.time ()
        if not fast then
          return  -- stop so we cast in correct order
        end -- unless they want the lot
      end -- if
    end -- not currently on us
  end -- for

end -- try_to_buff_us

--- Called by coroutine to see if we need to request the affected list

function check_we_know_times ()
local ok = true

  for sn, time in pairs (current_buffs) do
    if not tonumber (time) then
      ok = false
    end -- if
  end -- for
  
  if ok then
    return
  end -- all times known

  if os.time () > (last_time_check + 30) then 
    SendNoEcho "slist affected noprompt"
    last_time_check = os.time ()
  end -- if 
  
end -- check_we_know_times

--- COROUTINE HERE ----

function buff_loop (reason)
 
  local other_reason
  have_slist = false
  
  last_time_check = last_time_check or (os.time () - 60)
  last_cast_time = os.time ()
  
  low_mana = nil
  low_moves = nil

  while reason ~= "disconnect" do
 --   print ("coroutine kicked, reason:", reason, other_reason)

    if stats.position_str == "standing" and    -- need to be standing
       playing and           -- and playing
       not AFK and           -- not AFK
       not low_mana and      -- not recently out of mana
       not low_moves and     -- not recently out of moves
       reason ~= "recoveryon" and  -- recovery on is usually followed by {affon}
       have_slist and        -- have received list of spells
       not paused and        -- they haven't paused us
       (reason == "affecton" or reason == "fast" or last_cast_time ~= os.time ()) then  -- only once a second, or we thrash
      try_to_buff_us (reason)    
    end -- OK to cast
       
    -- early on, we need to request lists of known spells etc.
    if playing and not AFK then    
   
      if have_slist then
        check_we_know_times ()
      else
        if os.time () > (last_time_check + 45) then 
          if next (spells) == nil then
            SendNoEcho "slist noprompt"
          end -- if no spells known      
          SendNoEcho "slist affected noprompt"
          SendNoEcho "slist learned noprompt"
          SendNoEcho "slist spellup noprompt"
          last_time_check = os.time ()
        end -- if 
      end

    end -- if playing
    
    ResetTimer ("affects_timer")  -- heartbeat not needed for a while yet
    reason, other_reason = coroutine.yield ()  
  end -- main loop

--  print "coroutine finished."
  
end -- buff_loop

]]>
</script>

</muclient>