The NakedMud Tutorial :: Storage Sets
Storage Sets
Storage sets are a big part of NakedMud. They serve a few important purposes: They simplify the process of saving data from files by eliminating your need to come up with formatting schemes for your flatfiles. They also eliminate your need to write file parsers to extract data from files; the process of retrieving information from a file is reduced to querying for the value of some key. As we will learn later, they also play an integral role in the process of saving and loading auxiliary data.

In this section, we will learn the ropes of storage sets. We'll see how to store and read lists and strings. The other data types storage sets can deal with (ints, bools, doubles, longs) are handled in the exact same way as strings, except with different function names. After this tutorial, you should be able to extrapolate how these other data types interact with storage sets. In the next section on auxiliary data, we will examine how storage sets work in conjunction with auxiliary data.
The Mail Module
In the previous section, we designed a 'proof of concept' for a mail system. This section will build on top of it. If you did not complete the previous tutorial on the mail module, you can download the source code here.
Storage Set Basics
The first thing that is needed are the headers for interacting with storage sets. Along with all of the other includes, add:
#include "../handler.h"       // for giving mail to characters
#include "../storage.h"       // for saving/loading mail
Next, define a file where mail will be stored when the mud is down. The MUD's lib directory seems like an ideal candidate directory. Define the location for storing mail by where the table for storing mail is located:
// this is the file we will save all unreceived mail in, when the mud is down
#define MAIL_FILE "../lib/misc/mail"

// maps charName to a list of mail they have received
HASHTABLE *mail_table = NULL;
Two things needed are functions for converting both ways between MAIL_DATA and STORAGE_SETS. By convention, these functions are called xxxStore and xxxRead, where xxx is what is being converted to and from a STORAGE_SET. Get those functions set up, just below the newMail and deleteMail functions:
// parse a piece of mail from a storage set
MAIL_DATA *mailRead(STORAGE_SET *set) {
  // allocate some memory for the mail
  MAIL_DATA *mail = malloc(sizeof(MAIL_DATA));
  mail->mssg      = newBuffer(1);

  // read in all of our values
  mail->sender = strdup(read_string(set, "sender"));
  mail->time   = strdup(read_string(set, "time"));
  bufferCat(mail->mssg, read_string(set, "mssg"));
  return mail;
}

// represent a piece of mail as a storage set
STORAGE_SET *mailStore(MAIL_DATA *mail) {
  // create a new storage set
  STORAGE_SET *set = new_storage_set();
  store_string(set, "sender", mail->sender);
  store_string(set, "time",   mail->time);
  store_string(set, "mssg",   bufferString(mail->mssg));
  return set;
}
Now that there is actually the ability to store and read mail, create two functions for actually doing the storing and reading:
// saves all of our unreceived mail to disk
void save_mail(void) {
  // make a storage set to hold all our mail
  STORAGE_SET *set = new_storage_set();

  // make a list of name:mail pairs, and store it in the set
  STORAGE_SET_LIST *list = new_storage_list();

  // iterate across all of the people who have not received mail, and
  // store their names in the storage list, along with their mail
  HASH_ITERATOR *mail_i = newHashIterator(mail_table);
  const char      *name = NULL;
  LIST            *mail = NULL;
  ITERATE_HASH(name, mail, mail_i) {
    // create a new storage set that holds each name:mail pair,
    // and add it to our list of all name:mail pairs
    STORAGE_SET *one_pair = new_storage_set();
    store_string    (one_pair, "name", name);
    store_list      (one_pair, "mail", gen_store_list(mail, mailStore));
    storage_list_put(list, one_pair);
  } deleteHashIterator(mail_i);

  // make sure we add the list of name:mail pairs we want to save
  store_list(set, "list", list);

  // now, store our set in the mail file, and clean up our mess
  storage_write(set, MAIL_FILE);
  storage_close(set);
}

// loads all of our unreceived mail from disk
void load_mail(void) {
  // parse our storage set
  STORAGE_SET *set = storage_read(MAIL_FILE);

  // make sure the file existed and wasn't empty
  if(set == NULL) return;

  // get the list of all name:mail pairs, and parse each one
  STORAGE_SET_LIST *list = read_list(set, "list");
  STORAGE_SET  *one_pair = NULL;
  while( (one_pair = storage_list_next(list)) != NULL) {
    const char *name = read_string(one_pair, "name");
    LIST       *mail = gen_read_list(read_list(one_pair, "mail"), mailRead);
    hashPut(mail_table, name, mail);
  }

  // Everything is parsed! Now it's time to clean up our mess
  storage_close(set);
}
This code may be a bit ugly to the untrained eye. However, there are some very useful nuggets of knowledge buried within it. If you are having troubles understanding what is going on, it is highly suggested that you take a few minutes to trace through these two functions and figure out what is going on. Once we are completely done this section, it may also help to write a couple mails to yourself and examine what the mail file looks like. The file's structure might help elucidate many of the things that are going on in these two functions. Now that our save and load functions are written, we have to make sure they are called appropriately. We will want to load up unread mail when the mail module initialized, and we will want to make sure we update the contents of the mail file whenever mail is sent or received. At the end of cmd_mail, make sure mail is saved:
    // let the character know we've sent the mail
    send_to_char(ch, "You send a message to %s.\r\n", arg);
    
    // save all unread mail
    save_mail();    
At the end of cmd_receive, make sure mail is saved:
    // let the character know how much mail he received
    send_to_char(ch, "You receive %d letter%s.\r\n", 
                 listSize(mail_list), (listSize(mail_list) == 1 ? "" : "s"));
    
    // update the unread mail in our mail file
    save_mail();
Finally, ensure we load up all unread mail when we initialize the module:
// boot up the mail module
void init_mail(void) {
  // initialize our mail table
  mail_table = newHashtable();

  // parse any unread mail
  load_mail();
Unreceived mail will now be persistent across reboots and crashes. You may have noticed that saving of mail is rather inefficient; every time someone receives a mail or sends a mail, we have to re-save all unreceived mails. Ideally, we would like to change it so we have to re-save as little information as possible. That is the problem we will tackle in the section on auxiliary data.