queue.e the round-robin timed procedure looper
By Shark Byte
Nov 2000


-----------------------------------------------------------------------------
       purpose
-----------------------------------------------------------------------------
       provides an implicit main loop upon which you can
       dynamically add and remove procedures which will be
       called round-robin style.

       In addition, timing is provided, for procedures that
       are to be called after a delay.

       There isnt any prioritizing; they just take turns.

       (I first wrote this in pascal years ago, after looking
       at the "nextthink" multithreading that ID used in their
       Quake-C source for quake 1, then ported it to EU when
       I found EU much easier and more flexible than pascal)


-----------------------------------------------------------------------------
                brief list of stuff in queue
-----------------------------------------------------------------------------
 global variables:
  thread        List of the threads' names
  proc          List of the threads' routine_ids
  data          List of the data being passed those routines
  when          List of the times that the threads are scheduled to run
  me            Index of the current thread within the lists
  now           Queue stores time() in there once each looparound
  keypress      Specify a procedure to be called whenever a key is pressed

 procedures & functions:
  start         Start a new thread
  kill          Kill a thread
  instances     Tell how many instances of a thread are running
  run           The Main Loop which keeps all threads alive
  end_queue     Cause the main loop to exit

-----------------------------------------------------------------------------
                start
-----------------------------------------------------------------------------
usage: start({Name ,routine_id(Proc),Pars,When})
       adds a thread to the queue

where  Name is a string to identify the thread in the queue
            (name is only important for "instances" and "kill")
where  Proc is the name of the procedure to put in the queue
where  Pars is a sequence of the parameters to use
where  When tells when to call your procedure (as in now+2.6) to wait 2.6sec

       Once you "start" a procedure, it will be called
       once each time around the queue's mainloop,
       as long as the current EU time is later or equal to
       that procedure's set time.

 Note: You MAY find it useful to define a sequence
       containing the parameters used in start.
       (i'll call it "qrec" when describing it for some other stuff)


-----------------------------------------------------------------------------
                kill
-----------------------------------------------------------------------------
usage: kill("somename")
  or   kill(someinteger)
  or   kill(some_qrec)

where  someinteger is the index of one of the threads.
       usually the global variable: me
       as in: kill(me)   to end the current thread.

where  somename is the name you gave to a queue'd procedure
       when you "start"ed it.
       to kill another thread that you know the name of
       as in: kill("someotherthread")

where  qrec is the sequence you fed to "start"
       you can use this if you had predefined a sequence for
       the entire thread description including name,proc,data,when

yup, kill is the opposite of start, just as in start/kill an engine


-----------------------------------------------------------------------------
                instances
-----------------------------------------------------------------------------
usage: howmany=instances("somename")
where  somename is the name you used as Name when you called start
       tells how many threads have the name "somename"

   or: howmany=instances(qrec)
where  qrec is that sequence you fed to the "start" statement
       tells how many threads have the name mentioned in qrec

   or: howmany=instances(someinteger)
where  someinteger is the index of one of the threads.
       tells how many threads have the same name as thread[someinteger] does
       usually, for someinteger you will probably use the
       global variable: me


-----------------------------------------------------------------------------
                keypress
-----------------------------------------------------------------------------
Usage: keypress=routine_id("my_key_proc")
       tells queue to call your key handler whenever a key is pressed

where  my_key_proc is just some procedure in your program
       which takes one integer as its parameter;
       that integer will contain the keycode of the key that was pressed.
 Note: If you dont assign anything to keypress,
       it will simply be inactive and out of your way.
 Note: If after using keypress for a while, you want to
       deactivate it, just set it to -1. (thats its default value anyway)


-----------------------------------------------------------------------------
                run
-----------------------------------------------------------------------------
usage: run()
       run all queued procedures

       This is the main loop. all the looping takes place here.
       call run after you have started at least one procedure
       and the main loop will start looping, calling
       each procedure in its loop
       (oh, and one queued procedure can start or kill another)
       the queue's mainloop will keep looping until either:
        *  all queued procedures are removed from the loop
           and keypress has been put back to -1
        or
        *  abort() is called, dumping u to DOS
        or
        *  end_queue() is called, which empties the queue,
           causing your program to return from the procedure run()


-----------------------------------------------------------------------------
                end_queue
-----------------------------------------------------------------------------
usage: end_queue()
       quit running queued procedures and return from run() 

       call this inside a queued procedure, and
       All queued procedures will be killed, and
       the queue's mainloop will exit.
       your program will go to the next statement directly after
       the run() statement.


-----------------------------------------------------------------------------
                notes:
-----------------------------------------------------------------------------
 to get the index of a thread when you know its name, use:
      i=find(itsname,thread)
      and if it found it, i will have the index
      if not, i will be zero

 from within a thread, you can alter its listing in the queue
      by using statements such as:
      data[me]={some_new_data} -- new data to be used next looparound
      when[me]=now+some_delay  -- delay the next call to this thread
      proc[me]=routine_id("another") -- next call, a whole different proc

-----------------------------------------------------------------------------
                examples
-----------------------------------------------------------------------------

Example of a FOR loop adapted to run as a queued thread:

plain:   for x = 3 to 5 do
             blah blah blah
         end for

queued:  procedure whatever(integer x)
             blah blah blah
             x += 1
             if x > 5
                then kill(me)
                else data[me]={x+1}
             end if
         end procedure

         and, you must start the procedure like so:

         start({"whatever",routine_id("whatever"),{3},now})

yeah, i know its more code, and is only needed for long loops,
      like for x=1 to some huge number, or if the "blah blah blah"
      is something that takes some time,
      but for larger and more involved loops,
      especially code that is in procedures already, it wont make
      that much size difference.  And theres the advantage that
      other procedures can still run while your loop is running.

Example of a while loop, queued:

plain:   while z do
          blah blah blah
         end while

queued:  procedure mooo()
          blah blah blah
          if z=0 then kill(me)
         end procedure


Example of a delay, queued:

plain:   while looping
          blah blah blah
          sleep(3) -- this would allow other processes to run under
                   -- linux or windows, but NOT UNDER DOS
         end while

queued:  procedure myloop()
          blah blah blah
          when[me]=now+3 -- not an immediate delay.  This causes myloop to
                         -- not be called again for 3 seconds.  Meanwhile,
                         -- other queued procedures will still run.
          if looping=0 then kill(me) end if
         end procedure


       Note: don't use any non-exiting loops, or loops that take
       a long time to exit, within queued procedures, as that would
       prevent other queued procedures from running, thus basically
       defeating the purpose of queueing them in the first place.
       Instead, for looping routines let queue keep calling your routine.
       Thats what its there for.  For changing data within the loop,
       use a variable outside your procedure, or better yet,
       use the parameters of your procedure, and change them with
       newdata.


Other examples:
        play.ex -- shows use of qinput and bkmusic with queue

