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.
.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:
- PLY_ROOM()
- ROOM_AREA()
- TEMPLATE()
- GET_NEW_RECORD()
- GI$(), LGI$() and variants
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:
- Accessing database properties (use DBalias or a getter function)
Databases
Most new features will require a database of some kind.Database Styles
We use Rapture databases in three main ways:
- Databases as Psuedo-Classes
- Databases as Pivot Records
- Databases as Static Data Structures
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.
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
.
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 variable
s, 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 $.
.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:
- ply is the only global variable you should realistically ever use.
- Avoid numbers, unless they're for standard abbreviations (e.g. '3p' for third-person)
- Do not re-use variables for different purposes in the same closure.
- Name your iterators intelligently.
i
is almost never the right answer - When creating a new database, pick an iterator for it and stick to it. (e.g.
skrec
for skillrec) - Avoid stringlists whenever possible. Opt for databases (including pivot databases) or vectors instead.
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:
- Clarifies logical use of data.
- Eliminates unnecessary repetition of the iterator function, if applicable
- 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:
- Nonstandard initialization of the loop variable (easy to forget)
- Giant nested conditionals (which can be much, much worse)
- Hacky repetition of the iterator to enable
continue
- Explicitly checks for rollover. Why?
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:
- If you're looping through a vector, use
for
! - Use the right datatype in your opening statement: don't pull a
string$
and then immediately useval
, or vice-versa. - Use exclusionary conditions rather than inclusive conditions -- If a record DOESN'T meet your standards,
continue
the loop. This reduces indentation and redundant processing.
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:
- Formatting doesn't match our style
- Nested inclusive conditionals that could be exclusive.
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.