Rapture Style Guide

Background

This guide is meant to explain the formatting style and best practices for coding in Lusternia's Rapture codebase. While our older code will not adhere to all (or many) of these standards, and there are occasional exceptions to the 'best' practices, this will provide a concise understanding of how we develop our new code.

The format and code behind this guide are liberally borrowed from Google's styleguide project, found here.

Codebase Organization

Once you're comfortable with Rapture, it'll be time for you to add new features instead of just modifying existing code. These are some guidelines.

File Division

As the size of a codebase grows, it can be tempting to slot new features into existing .r files in the interest of keeping the file listing short. However, short, concise, feature-scoped files improve code maintainability, readability, and portability. Rather than contributing to the horrorshows of verbs.r and main.r, strive for a clean file containing only your feature's centralized code. An example is artishop.r, which contains everything relevant to the artifact shop.

Header Files

Each .r file should have an accompanying .rh file to hold forward declarations, constants, and DB aliases, if applicable. #include header files in the accompanying .r file and in lusternia.rh.

Header Guards

Use Header Guards to prevent multiple inclusion. Header guards work as such:

#ifndef __CLASS_RH__
#define __CLASS_RH__

[header content]

#endif
[required -- blank line]
Name Header Guards with the format __<FILENAME>_RH__.

Constants

Constants are crucial for avoiding ugly magic-number code. Using constants and databases or vectors, we can emulate Enumerated List types and state machines. In general, it is best to start enumerating constants at 1 and use 0 to represent error states or 'unset' statuses.

Because many constants will be externally-declared, it's important to provide a namespace of sorts when naming your constants. This improves readability and reduces the chance of conflicting definitions. Define constants as such:

#define REQSTATUS_UNSUBMITTED    1
#define REQSTATUS_SUBMITTED      2
#define REQSTATUS_APPROVED       3
#define REQSTATUS_REJECTED       4
#define REQSTATUS_COMPLETE       5

In this case, REQSTATUS_ serves two purposes: it separates these state indicators from the many others in use in Lusternia and communicates how to interpret the information, i.e. the status of a request. Consistent use of this naming convention for every enumerated constant will significantly improve readability.

If the relevant resource (usually a database) is not externally-declared, place the constants in the appropriate .r file with the initialization so that their scope is only as wide as necessary.

Macros

Macros can be a great way to condense lots of repeated code into smaller portions. However, they can be abused terribly (as is evident in lots of our older code). We do accept a few macros as useful shorthand, to maintain consistency:

Defining new macros is an important part of creating static databases, but it's rare that you'll need to define new macros outside of that case. Most importantly, you should NEVER create macros for the following:

Databases

Most new features will require a database of some kind.

Database Styles

We use Rapture databases in three main ways:

  1. Databases as Psuedo-Classes
  2. Databases are most commonly used as rough imitations of the classes found in object-oriented languages. Replica, room, and persona are all examples of psuedo-classes. These are persistent objects, often created and modified through user interface layers.

  3. Databases as Pivot Records
  4. Good relational database design requires the use of pivot tables, which associate records in one database with records in another in direct One-to-Many or Many-to-Many relationships. While many older features in Lusternia use stringlist database properties as a list of related 'children', this is inefficient and inflexible. We use databases such as orgreq_comment to make pivot associations. As an example:

    dbalias orgreq_comment
    {
       Time        =>    data[1],
       
       Filer       =>    variable[1],
       Request     =>    variable[2],
       
       MortalVis   =>    flag[1],
       
       Comment$    =>    string$[1],
    }

    Request points to a record in the Orgreq database and Filer refers to a Persona.

  5. Databases as Static Data Structures
  6. Because Rapture lacks flexible associative arrays, we used to fall back on gobs of boilerplate code. One would define a constant, add a line in a function to find its name, add a line in the function to recognize its name, and then add lines in any number of functions to return associated other data. With that amount of work, it's very easy to make mistakes and omit important information.

    However, it's not necessary to use interfaces for modifying this data in-game, so a psuedo-class database is overkill. Instead, we want to continue to define the information in our code, so that Git can handle updates without a need to keep game data synced. With static databases and intelligent use of macros, we can reduce that boilerplate to two lines per record: one for defining the ID number constant and one for setting the properties. Observe:

    We can use

    DEF_SKILL(SKILL_TEKURA, "Tekura", SKTYPE_CLASS);
    to associate a name and skill_type with the skill constant. DEF_SKILL, locally scoped in skills.r, looks like this:
    #define DEF_SKILL(sk, name$, type) skill[sk].Name$ = name$; skill[sk].Type = type;

    As such, this function:

    function get_skill_name$(sk) 
    {
       if sk = SKILL_HIGHMAGIC        then return "highmagic";
       else if sk = SKILL_LOWMAGIC     then return "lowmagic";
       [+ 100 or so more skill records]
    }
    
    becomes
    function get_skill_name$(sk)
    {
       return skill[sk].Name$;
    }
    
    Especially when you extrapolate out to every possible property, this is an immediate boost toward readable, maintainable code.

Database Declaration

Every database must be declared in the same .r file where it is initialized. Databases names should be singular and lowercase. For multiple-word names, use underscores (such as orgreq_comment.) Databases are declared like most other data types:

database skill;

By default, Rapture assumes you are using static databases, so it doesn't make space to store the data within, and everything will be cleared each time the engine resets (including loadsource calls). To enable data storage, just give Rapture a name for the storage block:

database orgreq = "OrgReq-Data";
By convention, data storage names should be upper-camel-case versions of the database name with "-Data" at the end.

In some cases, especially with Pivot table-style databases or a strong system of getters and setters, you'll only need to deal with your database in the file where you've defined it. In most cases, however, you'll want to directly access your database in other files. To do this, you'll need to expose it in your header file. To do so:

extern database skill;

Database Initialization

In our feature's .r file, we need to lay out the size and structure of the database. We do this, somewhat backasswardly, by setting the maxrecord of each datatype once each time Rapture resets. By convention, we name this subroutine <database>_init() or init_<database>() and call it in subroutine init_databases(). For our orgreq database, we'd initialize as such:

subroutine init_orgreq()
{
   make_at_least(orgreq.maxrecord, 10);
   make_at_least(orgreq.data.maxrecord, 1);
   make_at_least(orgreq.variable.maxrecord, 2);
   make_at_least(orgreq.byte.maxrecord, 1);
   make_at_least(orgreq.flag.maxrecord, 1);
   make_at_least(orgreq.string$.maxrecord, 4);

With this, we indicate that our orgreq database will include 1 data, 2 variables, 1 byte, 1 flag, and 4 string$s.

If you anticipate that one particular property in the database will regularly be the subject of .search() calls, you should set it as indexed in the same subroutine:

orgreq.Filer.indexed = TRUE;
Indexing improves search speeds but makes modification much slower (as things have to be reindexed). Use this primarily for what you'd call the 'keys' on a more traditional database.

Database Aliasing

Every database should have an accompanying dbalias structure, regardless of its use or scope. Raw access of database properties (e.g. database[rec].string$[1]) unnecessarily hurts readability.

DB aliases should use the following format:

   dbalias database
   {
      Property$      => string$[1],

      NumProperty    => byte[1],
   }
Group datatypes (bytes, flags, etc.) together within the dbalias. Aliases of string$ properties should always end with $.

If your database is externally declared, your dbalias should reside in the header file. Otherwise, place it with the initialization in the .r file. Please do not place anything new in the dbalias.rh file.

Formatting and Best Practices

While there are exceptions to the guidelines above, the formatting rules below should be considered firm.

Variables

While Rapture's strict typing precludes many common variable-related coding pitfalls, the obfuscated naming and inconsistent (or non-existent) standards of oldcode present a varied and confusing template for new coders. Below are some guidelines:

We invest a lot of signficance into certain variables. Try to use these variables in the following ways:

ply
The main node in use, either in commands or task loops.
p
The main persona in use, either in commands or task loops.
tply
The main target node.
tarp
The main target persona.
rep
The main replica in use.
mob
The main mobile in use.
r
The main room in use.

Assignment and Operators

Operators and assignment statements should include single spaces between each element, excepting parentheses. For example:

i+=4;

is better written as

i += 4;

and

output$="herp "+derp$;

is

output$ = "herp " + derp$;

Functions and Subroutines

When defining and invoking functions and subroutines, omit spaces around parentheses but use spaces after commas. For example:

def_strip_ply( ply, "insomnia", hidden );

OR

def_strip_ply(ply,"insomnia",hidden);

should be

def_strip_ply(ply, "insomnia", hidden);

When defining functions and subroutines, place the brace on the following line, not in-line with the definition. Names should be lowercase, using underscores for multiple words if desired. If overloading a function that uses nodes or personae in different forms, make persona the default and prepend ply to indicate the node usage.

function eat_toast(p, toast, uses_butter)
{

}

function ply_eat_toast(ply, toast, uses_butter)
{
   return eat_toast(node[ply].persona, toast, uses_butter);
}

Conditionals

Because Rapture is a language is styled after C, there are MANY ways of styling valid conditional statements. To create clean, easily maintained code, we style our code to be clearly structured and searchable.

By avoiding spaces around parentheses (and omitting the parentheses themselves wherever possible), we leave the conditional statement itself to only the most necessary logic. By placing the braces on their own lines, we help establish clear column delineations even without the help of an IDE highlighting indentation structure or selecting matching braces. With the operator rules, we keep statements as human-readable as possible, even without comments.

Example:

if (mace) then {unimsg(ply,"You already have a mace!"); return;}
if mace then
{
   unimsg(ply, "You already have a mace!");
   return;
}

In situations where a conditional needs only return, without further action or an else statement, a single line is acceptable.

if !valid_persona(p) then return;

Loops

Rapture enjoys two loop types: while and for.

While is most commonly used for iterating through databases, either by ID or the .search() function. In most cases, you'll want to structure your while with do at the top and the condition at the bottom. This has three main benefits:

  1. Clarifies logical use of data.
  2. Eliminates unnecessary repetition of the iterator function, if applicable
  3. Enables use of the continue keyword, which dramatically cleans up code

Let's take a while loop of the old style and refactor it to be pretty.

rite = 1;
while rite <= rites.maxrecord do 
{
   if valid_rite(rite) then
   {

      rites[rite].Timer -= 1;
      if !rites[rite].Timer or
          rites[rite].Timer > 60000 then
      {
         rites[rite].delete();
         rite += 1;
         continue;
      }

      riteType = rites[rite].Type;

      if riteType = RITE_DEMONS then 
      {
         push(r_demons@, rite);            
      }
      else if riteType = RITE_BANISHMENT then 
      {
         push(r_banishment@, rite);         
      }
      else if riteType = RITE_WARDING then 
      {
         push(r_warding@, rite);            
      }
   }

   rite += 1;
}

Problems we have:

Let's see what a difference a little cleanup can make.

rite = 0;
do
{
   rite += 1;
   if !valid_rite(rite) then continue;
   
   rites[rite].Timer -= 1;
   
   if !rites[rite].Timer then
   {
      rites[rite].delete();
      continue;
   }
   
   riteType = rites[rite].Type;

   if riteType = RITE_DEMONS then 
   {
      push(r_demons@, rite);            
   }
   else if riteType = RITE_BANISHMENT then 
   {
      push(r_banishment@, rite);         
   }
   else if riteType = RITE_WARDING then 
   {
      push(r_warding@, rite);            
   }
} while rite < rites.maxrecord;

With this layout, the flow of the loop is more readable with the iterator right up front, the indentation is decreased, and we can continue without worry if we change the iterator at any point.



Because for loops are only useful with vectors, their formatting is much more straightforward. Important reminders:

An example:

for entry$ in ab@ do {
   ab = val(words$(entry$,3,3));

   if !valid_abdb(ab) then 
   {
      debug("bad ab: "+ab+". Entry was: "+entry$+".\n");
   }
   else if !valid_abdb_header(abdb[ab].Heading) then 
   {
      if validskill_for_ply(ply, ab) or skill_has_exclusions(ab) then
      {
         [ snipped ... ]
      }
   }
}

Problems at a glance:

Let's clean up.

for entry$ in ab@ do 
{
   ab = val(words$(entry$, 3, 3));

   if !valid_abdb(ab) then 
   {
      debug("bad ab: " + ab + ". Entry was: " + entry$ + ".\n");
      continue;
   }
   
   if valid_abdb_header(abdb[ab].Heading) then continue;
   if !validskill_for_ply(ply, ab) and !skill_has_exclusions(ab) then continue;
              
   [ same snipped code ]
}

Just like that, we've made the roadmap easier to read, stripped off two levels of indentation (which would have been 5+ inside the snipped code before), and cleaned up the formatting.