mScriptBox Tutorial
Introduction into Hash Tables

Written by Pasmal
Published with permission.

Table Of Contents

  1. Introduction
  2. Hash Table Theory
  3. Creating a Hash Table
  4. Adding an Item to a Hash Table
  5. Removing an Item from a Hash Table
  6. Saving a Hash Table
  7. Loading a Hash Table
  8. Destroying a Hash Table
  9. Setting up the Hash Tables for our Example
  10. The $hget() Identifier
  11. The Bar Script Example
  12. Hash Table Editor
  13. dac's Example script
  14. Ideas for the Mind


1. Introduction  Back to Top

Hash Tables were introduced in mIRC 5.8. They allow for large data storage and very fast access, tests showing that they can out performs variables and INI files.

What they effectively allow you to do is create arrays and more (I'm speaking from a non hard core programming perspective so I'm very limited in my explanation, sorry).

whatis.com describes arrays as:

  1. In general, an array is a number of items arranged in some specified way - for example, in a list or in a three-dimensional table.

  2. In computer programming languages, an array is a group of objects with the same attributes that can be addressed individually, using such techniques as subscripting.

However asking some of my friends I've discovered that hash tables are far more efficient and faster than pure arrays.
I'm sure these comments will sound incorrect to people who are more educated in this area, please don't email me about it =)



2. Hash Table Theory  Back to Top

The structure of a hash table can be crudely represented by the following table:

Table Name
Item1 Value1
Item2 Value2
Item[N] Value[N]

You could basically call it an INI file stored in memory. While INI has the structure <file> <heading> <item> <value>, hash tables only have <table name> <item> <value>.

So why use hash tables over INI files?

  • Hash Tables are faster than INI files because they are in the system memory, not in a file
  • Hash Tables allow you to use control codes in your value, e.g. ^K, which is normallystripped in INI's
  • INI's are under most circumstances limited to 64k, this is not the case with Hash Tables

For the majority of this introduction, my examples will relate to a simplistic bar script. For this bar we will end up having two hash tables. The first we will call 'bar' and will store what items you can buy at the bar, how much they cost, and a description of each one. The second hash table will be called 'bank' and will contain the money that each user can spend at the bar (heh, some economics and accounting chucked in on the side =)).

Bar
Item1 <cost> <description>
Item2 <cost> <description>
Item[N] <cost> <description>

Bank
Account1 <balance>
Account2 <balance>
Account[N] <balance>

 


3. Creating a Hash Table  Back to Top

Before you can add/retrieve values to a hash table, you have to first create it. To create a hash table you need to use the /hmake command:

/hmake -s <name> <N>

This creates a table with N slots. N is not an upper limit, you can exceed it. "A hash table can store an unlimited number of items regardless of the N size you choose, however the bigger N is the faster it will work, depending on the number of items stored. " - versions.txt. eg. if you expect that you'll be storing 1000 items in the table, a table of N set to 100 is quite sufficient.

The -s switch just displays debug information about what just happened. This is similar to the -s switch in /set and the opposite of prefixing commands with a period (.).

So for our example bar, we will make a table called 'bar' with a N of 5. Remember that N is not an upper-limit, only an estimate to give mIRC a fair idea of 'how much space to allocate to the table'. So lets create our table:

/hmake bar 5

We also need to make the 'bank' table. I'll also use a N of 5 for this table:

/hmake bank 5


4. Adding an Item to a Hash Table  Back to Top

This procedure follows a similar format to the writeini command:

/hadd -s <name> <item> <value>

So lets say we wanted to create a new item to sell at our bar. All the items you can buy are stored in the 'bar' hash table. Lets say we want to sell popcorn in our bar for 2 dollars, we would:

/hadd bar popcorn 2.00 Salty Buttered, simply delicious... POPCORN!

Now what about adding a new user to the bank, so we can buy stuff? We simply need to add to the 'bank' hash table:

/hadd bank CustomerBob 50.00

Woohoo, CustomerBob now has $50 to spend!


5. Removing an Item from a Hash Table  Back to Top

Once again it follows a similar format to the remini command:

/hdel -sw <name> <item or wildcard>

If you specify the -w switch you can use wildcards for your item.

So lets say we no longer want to sell popcorn in our bar (hey, we arn't a movie cinema, are we?), we would:

/hdel bar popcorn

Now if we wanted to remove all things from the bar that started with c, even though we don't have any items starting with c in our table JUST yet, (e.g. coke, chocolate, coffee, etc...) we could:

/hdel -w bar c*

Now lets close off CustomerBob's bank account =)

/hdel bank CustomerBob


6. Saving a Hash Table  Back to Top

If you use the hash table to store long term information then you'll probably want to keep the hash table each time you start mIRC. To do this you first need to save the table to a file. To do this you need to use the /hsave command. It's format is as follows:

/hsave -sbnoa <name> <filename>

By default the tables are saved as a text file, which means that any $cr and $lf that you might have in your values will be stripped. To solve this problem you can save the file in binary using the -b switch.

-o Overwrites the specified filename (if its sensitive information you may wish to backup first!)
-a Appends the table to an existing file
-n Saves only data/values only. Items are not included.

NOTE: Hash table items are not sorted in any specific order. Do not expect the 1st item to always be the same etc...


7. Loading a Hash Table  Back to Top

If you have saved a hash table, logic would presume that you would like to load it again. To do this you need to use the /hload command which has the following format:

/hload -sbn <name> <filename>

If you saved the hash table as a binary file then you need to load it with the -b switch. If the file contains values only (that is, no items) then use the -n switch to make mIRC automatically assign a number to each value (N starts at 1).

NOTE: You MUST have previously /hamke'd the hash table BEFORE attempting to /hload the filename to the table.


8. Destroying a Hash Table  Back to Top

If you have finished using a hash table, free up some memory and destroy it. If you need to keep the information in it, save it to a file using /hsave then destroy it. To destroy or free up a hash table you need to use the /hfree command:

/hfree -sw <name>

Note that it supports the -w switch, so you can use wildcards.

So for example, lets say we have finished using the bar script (awww!) we would save the information in the hash tables to some files, then free up the two tables:

hsave -o bank bank.htb
hsave -o bar bar.htb
hfree bank
hfree bar


9. Setting up the Hash Tables for our Example  Back to Top

Ok, now onto some practical applications with our bar script. First things first, we need to add some stuff to our bar to sell.

Create the bar table (if you have not already): /hmake bar 5

Add items to the bar table (in the format: /hadd bar <goods name> <price> <description of good>)

/hadd bar Coke 1.00 Ice cool, undeniable contour bottle
/hadd bar Pepsi 0.90 The closet substitute to Coke that you can get
/hadd bar Pizza 5.50 Cheezy, greezy, and ooo soo yummy!
/hadd bar Beer 3.50 Its brown and tastes bitter... what else could it be?
/hadd bar Vintage_Wine 25.50 Some of our more classier wine, bottled in 1952.
/hadd bar Cheap_Wine 9.95 Its cheap wine... what do you expect?
/hadd bar Chips 1.50 Crispy and Crunchy, Salt and Vinegar, Chicken, you name    it we have it
/hadd bar Vodka 12.50 This has to be some of Russia's finest booze
/hadd bar Gum 0.30 Yummy grape flavoured

Now we need to create our bank table (if you have not already): /hmake bank 5


10. The $hget() Identifier  Back to Top

$hget() can be used in two ways:

i. $hget(Name/N) - This is used to check if a table exists, or return the name of the N'th table, or if using 0 for N, return the total number of tables in use. You can also specifcy a .size property. Examples:

//echo -a There are $hget(0) tables, the first one named $hget(1)
//echo -a The N size of the $hget(1) table is $hget(1).size (Note this is the N size, not how many items are in the table)

ii. $hget(Name/N,Item/N).item - The .item is optional. This time round the $hget() identifier is used to find a value of an item, or find the name of an item. Examples (Assuming we have the 'bar' table loaded):

//echo -a There are $hget(bar,0).item items avaliable for sale at the bar.
//echo -a The first item found in the bar is $hget(bar,1).item $+ , it costs $gettok($hget(bar,$hget(bar,1).item),1,32)

If we knew the item name we don't need to specify a number:

//echo -a A coke costs $gettok($hget(bar,coke),1,32) and is described as: $deltok($hget(bar,coke),1,32)


11. The Bar Script Example  Back to Top

The bar script example's code can basically be split into three sections, the bank, buying from the bar, and adding more stock to the bar.

You can also get a file version here.

; Mr Bar Script/Demo Hash Table Script v1.0
; Pasmal, pasmal@zaz.net
;
; To open the bar: /open-bar
; To close the bar: /close-bar
; To get help: type !bar in the channel
; To setup a bank account: !bank in the channel
alias open-bar {
  ; This creates the hash tables, and if it previously saved
  hmake bar 5 | hmake bank 5 
  if ($isfile(bar.htb)) hload bar bar.htb
  if ($isfile(bank.htb)) hload bank bank.htb
  echo 3 -ea * /bar: Bar has been setup!
  ame has opened the bar! Type !bar for more help
}
alias close-bar {
  ; This saves the hash tables to files, and frees the hash tablesf
  hsave -o bar bar.htb | hsave -o bank bank.htb
  hfree bar | hfree bank
  echo 4 -ea * /bar: Bar has been closed
  ame has closed the bar!
}
on *:TEXT:!iteminfo *:*: {
  ; $hget(bar,$2) returns info about the item, in format: <price> <description>
  ; we need to use tokens to seperate the price and description
  if ($hget(bar,$2) == $null) .msg $target We don't sell ' $+ $2 $+ '
  else .msg $target $2 costs $ $+ $gettok($hget(bar,$2),1,32) and is $deltok($hget(bar,$2),1,32)
}
on *:TEXT:!buy *:*: {
  if ($2 ison #) { %nick = $2 | %item = $3 }
  else { %nick = $nick | %item = $2 }
  ; The above set of if statements merely checks to see if you specified a nick to give the item to
  if ($hget(bar,%item) == $null) .msg $target We don't sell ' $+ $2 $+ '
  elseif {$hget(bank,%nick) == $Null) || ($hget(bank,$nick) < $gettok($hget(bar,%item),1,32)) {
    ; We need to check the Bank hash table to see if the person can afford to buy the item
    .msg $target $nick has insufficient funds | return
  }
  else {
    .describe $chan gives %nick a %item $iif($nick != %nick, courtesty of $nick )
    var %new-bank-balance = $calc($hget(bank,$nick) - $gettok($hget(bar,%item),1,32))
    hadd bank $nick %new-bank-balance
    .msg $nick You have $ $+ %new-bank-balance left in your bank account
    if (%new-bank-balance <= 0) {
      ; Note how we delete the person's entry in the bank hash table if they don't have a positive
      ; balance (I decided not to allow an overdraft for the sake of this being an example script)
      hdel bank $nick 
      .msg $nick Your bank account has been closed due to insufficient funds! }
  }
}
on *:TEXT:!bar:*: {
  .msg $nick Mr Bar Script v1.0
  .msg $nick Commands:
  .msg $nick !iteminfo <item<<- tells you information about something in the bar
  .msg $nick !buy  <- buys the item
  .msg $nick !barlist <- tells you what items are for sale
  .msg $nick !additem <- adds a new line of stock to the bar
  .msg $nick !delitem <- removes a line of stock from the bar
  .msg $nick !bank <- gives you a bank account to buy stuff with, or shows existing balance
}
on *:TEXT:!barlist:*: {
  .msg $nick The following items are avaliable for sale at the bar, for more information on an item use !iteminfo <item>
  var %i = 0
  var %t = $hget(bar,0).item
  unset %list
  ; This while loop is inefficient (so says the help file), however we will still use it.
  ; Ideally I guess we would want some way of listing each item in the hash table without
  ; looping through it, possibly using some sort of index, maybe make a hash table item
  ; called index which holds a list of all other items (however string limits come into play...)
  while (%i < %t) {
    inc %i
    var %list = $addtok(%list,$hget(bar,%i).item,32)
  }
  .msg $nick %list  
}
on *:TEXT:!additem *:*: {
  if ($4 == $null) || ($3 !isnum) .msg $target Invalid format, please use: !additem <item> <cost> <description>
  else {
    ; This adds the specified item into the bar hash table, using the format hadd bar <item> <price> <description>
    hadd bar $2-
    .msg $target $2 is now avaliable for sale for only $3 $+ !!
  }
}
on *:TEXT:!delitem *:?: {
  if ($hget(bar,$2) == $null) .msg $target ' $+ $2 $+ ' is not in stock, so I can't delete it!
  else {
    ; This removes the specified item from the bar hash table
    hdel bar $2
    .msg $target $2 has been withdrawn from our selection, sorry!!
  }
}
on *:TEXT:!bank:*: {
  if ($hget(bank,$nick) != $null) .msg $nick You have $ $+ $hget(bank,$nick) in the ol' bank account
  else {
    ; This creates a new item in the bank hash table with a value of '50.00'
    hadd bank $nick 50.00
    .msg $nick You now have a bank account with $!50.00 in it!
  }
}


12. Hash Table Editor  Back to Top

MIMP and Variant have scripted a really neat Hash Table Editor dialog. It basically lets you, well, edit!, hash tables with a GUI. It should prove useful for those who, like us with IO's userlist, don't really need nor want to manually dig through 700+ hash table items via the basic command set. You can download the script here.


13. dac's Hash Table Example  Back to Top

I had a problem with one of my scripts taking a long time to calculate scores.

Here's the relevant code I had. Note that %letterval consists of letters and their values for a Scrabble kind of scoring in a game script of mine. (I used %letterval as a two level tokenised string, each letter.value pair separated by a comma)


alias hm_addscore {
  ;
  ; increment a person's score by the value of the letters
  ; uses the letterval token array to determine letter scores (same as scrabble)
  ; Usage:  hm_addscore  
  ; eg      hm_addscore Bozoman The Eiffel Tower is made of iron
  ; An array of letter values
  %temp1 = a.1,b.3,c.3,d.2,e.1,f.4,g.2,h.4,i.1,j.8,k.5,l.1,m.3,n.1,o.1,p.3,q.10,r.1,
  %temp2 = s.1,t.1,u.1,v.4,w.4,x.8,y.4,z.10,0.2,1.2,2.2,3.2,4.2,5.2,6.2,7.2,8.2,9.2
  %letterval = %temp1 $+ %temp2
  VAR %user = $1, %guess = $2-,  %score = 0, %y = $len(%guess)
  WHILE (%y > 0) {
    VAR %z = $numtok(%letterval,44)
    WHILE (%z > 0) {
      VAR %targ = $gettok(%letterval,%z,44)
      VAR %char = $gettok(%targ,1,46)
      VAR %cval = $gettok(%targ,2,46)
      IF  ($mid(%guess,%y,1) == %char) {
        %guess = $replace(%guess,%char,-)
        %score = $calc(%score + %cval)
        %z = 0
      }          
      DEC %z
    }
    DEC %y
  }
  hm_writeini letters $1 %score
}

Running this 50 times took about 20 seconds on my machine. Since my main logging routine was calling this hm_addscore routine in a busy channel, I was getting bogged down with the repeated gettok for -each- letter in the submitted string, going through -each- value in the %letterval token. It is just a very inefficient way of doing it.

So in comes the HASH solution.

First we CREATE a named hash (in this case 'hletters'), then we fill it with ITEM/VALUE pairs. In this case I'm using exactly the same %letterval structure to 'initialize' the hash.

The HADD line adds the item and its associated value to the hash.


alias hm_createhash {
 %temp1 = a.1,b.3,c.3,d.2,e.1,f.4,g.2,h.4,i.1,j.8,k.5,l.1,m.3,n.1,o.1,p.3,q.10,r.1,
 %temp2 = s.1,t.1,u.1,v.4,w.4,x.8,y.4,z.10,0.2,1.2,2.2,3.2,4.2,5.2,6.2,7.2,8.2,9.2
 %letterhash = %temp1 $+ %temp2		  
 IF ($hget(hletters) == $null) {
    hmake -s hletters 50
  }
  VAR %z = $numtok(%letterval,44)
  WHILE (%z > 0) {
    VAR %target = $gettok(%letterval,%z,44)
    VAR %cval   = $gettok(%target,1,46)
    VAR %cnum   = $gettok(%target,2,46)
    hadd -s hletters %cval %cnum
    DEC %z
  }
}

We only need to create this particular hash once, so as part of my INIT, I call hm_createhash.

Then in my logging, I call hm_addscore as normal.

Here is the modified hm_addscore, using a HASH instead of the token lookup loop, notice the use ot $hget to detemine the value of each letter in a string:


alias hm_addscore {
  ; increment a person's score by the value of the letter guessed
  ; uses a hash array to determine letter scores (same as scrabble
  ; Usage:  hm_addscore  
  ; eg      hm_addscore Bozoman The Eiffel Tower is made of iron

  VAR %user = $1, %guess = $2-,  %score = 0, %y = $len(%guess)
  WHILE (%y > 0) {
    VAR %char = $mid(%guess,%y,1)
    VAR %cval = $hget(hletters,%char)
    %score = $calc(%score + %cval)
    %guess = $replace(%guess,%char,-)
    DEC %y
  }
  hm_writeini letters $1 %score
}

When I changed to this method, 50 calls took less than a second. A vast improvement over the 20 seconds taken in method one.

That represents a massive saving. Hashes will become very useful, replacing the more cumbersome .ini files that I use in my scripts with internal FAST hashes.

Hope this helps =)


14. Ideas for the Mind  Back to Top

A lot of these ideas are user-orientated, however hash tables are perfect for them!

  • A Seen Script could make great use of the unlimited (in theory) space avaliable in hash tables. Say goodbye 64k INI limit.
    A good example is MishSeen, available for download here.
  • A Bot's userlist could be stored in hash tables.
  • A Trivia script could use hash tables to record scores for individuals, or you could have multiple teams each with there own hash table which would let you not only record total scores for each team, but also track individual scores.
  • A Clone scanner.
  • A Netsplit Detector.
  • You could store settings in hash tables, however I still like the idea of putting this sort of information in variables or INI files because if you close mIRC before saving (/hsave)the table, you will have lost any changes to those settings, and mIRC currently doesn't have an event to tell you when it is closing. You could also argue along the same lines for keeping a bots userlist in INI or variables, but I think that if you make a bot, you should backup/save its records frequently.