Coding Style
How LPC is written on Merentha (FluffOS 20240714). Every new file and every change you make should follow this tome. Consistent style makes code easier to review, easier to search, and lets the code coverage system instrument every branch.
Older code (much of it formatted with the ed I indent command) does not
always match. Follow the style in the lines you write or change, but do not
reformat untouched code by hand just to tidy it.
Spacing
- 4 spaces per indent level. No tabs.
- No trailing spaces, and every file ends with exactly one newline.
- Never more than one blank line in a row.
- One space after
//in a comment; no space after#in#includeor#define.
Spaces go around operators and after commas.
damage = strength * 2 + bonus;
tell_object(tp, "You feel stronger.");No space just inside parentheses or brackets, and none around ->.
first = targets[0];
first->set_name("goblin");No space after !. The ternary ? and : get a space on both sides.
msg = !living(target) ? "It does not answer." : "It ignores you.";Keywords that take a condition attach straight to it: if(, for(,
foreach(, while(, switch(. Always one space before an opening {.
if(!tp) {
return 0;
}Empty literals are written tight: ({}) for an array, ([]) for a
mapping.
ids = ({});
props = ([]);Increment and decrement keep a space before the operator: i ++,
count --.
for(int i = 0; i < sizeof(targets); i ++) {
tell_object(targets[i], "The ground shakes beneath you.");
}Braces
Always use braces. Every if, else, for, foreach, while, and do
body is braced and multiline, even a single statement. The code coverage
system needs a block to instrument every branch.
if(!living(target)) {
return 0;
}The opening brace stays on the statement’s line. A closing brace gets its
own line unless an else follows, which then sits on both sides of it:
} else {, } else if(.
if(hp < 10) {
write("You are near death.");
} else if(hp < 50) {
write("You are badly hurt.");
} else {
write("You feel fine.");
}switch is the one exception: case bodies are never braced. Case
labels do not open a scope, so a local declared inside a case is a FluffOS
syntax error. Declare it at the top of the function instead, as msg is
here. Indent case one level inside the switch, and include a default.
private string describe_health(int percent) {
string msg;
switch(percent / 25) {
case 0:
msg = "near death";
break;
case 1:
msg = "badly wounded";
break;
default:
msg = "in good shape";
break;
}
return msg;
}Declarations and visibility
Locals go at the top of their scope, then a blank line, then the work. This is the default.
public string query_condition() {
int percent;
string msg;
percent = query_hp() * 100 / query_max_hp();
msg = percent < 25 ? "near death" : "healthy";
return msg;
}Declaring mid-block is allowed when it genuinely helps: here target only
exists inside the branch that uses it.
public void taunt(object *enemies) {
if(sizeof(enemies)) {
object target = enemies[random(sizeof(enemies))];
tell_object(target, "The goblin spits at your feet.");
}
}Globals are private and named __PascalCase. The __ prefix marks
file-global scope at every use and stops a local from shadowing it. Add
nosave to anything that must not be saved with the object.
private int __Charges = 3;
private nosave object __Owner;
private string *__Ids = ({});Every function states its visibility. Default to private; widen to
protected or public only when another object or an inheritor really
needs it. Other objects should reach globals through accessors like this one,
not directly.
public object query_owner() {
return __Owner;
}
private void clear_owner() {
__Owner = 0;
}Naming
Everything is snake_case: functions, variables, and parameters.
Accessors start with query_, mutators with set_.
public int query_charges() {
return __Charges;
}
public void set_charges(int amount) {
__Charges = amount;
}Constants made with #define are ALL_CAPS_WITH_UNDERSCORES.
#define MAX_CHARGES 5
if(__Charges >= MAX_CHARGES) {
write("The wand hums, already full.");
}Private functions take no _ prefix: the private keyword already says it.
private void recharge() {
__Charges = MAX_CHARGES;
}A parameter you must accept but never use gets a leading _, so the LSP
stops warning that it is never read.
public string query_help(object _caller) {
return "Usage: zap <target>";
}Two more rules with nothing to show:
- Never name a local variable the same as a function in scope.
- Renaming a function? Search the whole codebase first and update every
call site: direct calls,
->calls,call_otherstrings, and string callbacks. A stale call site is a runtime error waiting to happen.
File layout
A file runs in this order: includes, inherits, forward declarations, globals, then functions, with the main ones first and helpers at the bottom. Split anything growing past roughly 1000 lines into modules.
Here is a complete small file in that order: a village healer who restores adventurers to full health for a fee.
A block comment at the top says what the file is for.
/*
* A village healer who restores adventurers to full health for a fee.
*/Includes come first, then inherits.
#include <std.h>
inherit MONSTER;Forward declarations come next. heal_player() calls charge_fee() before
it is defined further down, so it is declared here first.
private int charge_fee(object tp);Then globals.
private nosave int __Fee = 50;Then the main functions.
void create() {
::create();
set_name("healer");
set_id(({ "healer", "village healer" }));
set_short("a tired village healer");
}Function docs sit directly above the function. Blank lines split the locals, the work, and the return into their own groups.
/*
* Restore a player to full health. Returns 1 if they could pay.
*/
public int heal_player(object tp) {
int max_hp;
if(!charge_fee(tp)) {
return 0;
}
max_hp = tp->query_max_hp();
tp->set_hp(max_hp);
return 1;
}Helpers go at the bottom.
private int charge_fee(object tp) {
if(tp->query_money("gold") < __Fee) {
return 0;
}
tp->add_money("gold", -__Fee);
return 1;
}Line width: most lines land well under 80 columns on their own. Do not wrap a line that reads cleanly just because it is long. If a line is hard to read, fix the structure (an intermediate variable, a simpler expression) rather than breaking it mechanically.
Types, errors, and defensive code
This mudlib uses UNDEFINED, not null: test with undefinedp(), never
nullp(). Then confirm the type with intp(), stringp(), objectp(),
arrayp(), or mapp(), especially for anything arriving through
call_other.
public int set_charges(mixed value) {
if(undefinedp(value)) {
return 0;
}
if(!intp(value) || value < 0) {
return 0;
}
__Charges = value;
return 1;
}Never trust player input. Check that anything a player names actually exists
with objectp() before touching it.
public int cmd_zap(string arg) {
object target;
target = present(arg, environment(this_player()));
if(!objectp(target)) {
write("Zap what?");
return 1;
}
tell_object(target, "A spark leaps from the wand and stings you.");
return 1;
}Use catch where failure is expected and recoverable, and return an error
code rather than throwing. Save error() for fatal conditions.
public int safe_load(string path) {
object ob;
string err;
err = catch(ob = load_object(path));
if(err) {
return 0;
}
return 1;
}Hand back a copy() of a mapping or array, so callers cannot change your
internal state through the reference.
private nosave mapping __Properties = ([]);
public mapping query_properties() {
return copy(__Properties);
}Convert explicitly with to_int() or to_string() rather than relying on
implicit conversion.
amount = to_int(arg);LPC idioms
- Never rename driver applies such as
create(),init(), orheart_beat(): the driver calls them by name. this_object()is the current object;previous_object()is whoever called you.
Inherit through the macros from <std.h>, and reach the parent’s version of
a function with ::.
inherit MONSTER;
void create() {
::create();
}load_object() returns the one shared instance of a file, which is what you
want for rooms and daemons. new() makes a fresh copy, for items and
monsters.
entrance = load_object(CABEIRI + "goblin/room/r1.c");
coins = new("/std/obj/coins.c");varargs makes trailing parameters optional. Prefer filter, map, and
member_array over hand-written loops: here filter picks the living
things out of the room in one line.
public varargs void announce(string msg, object *exclude) {
object *listeners;
listeners = filter(all_inventory(environment()), (: living :));
if(exclude) {
listeners -= exclude;
}
message("info", msg, listeners);
}Performance
Every function runs under an eval cost limit. Keep frequently called
functions cheap, cache expensive results, and avoid allocating inside tight
loops. To wait, schedule a call_out instead of looping.
public void open_door() {
set_open("door", 1);
call_out("close_door", 60);
}Room specifics
A room’s create() calls run in this order. Monsters and objects use a
different order.
void create() {
::create();
set_properties(([ "light": 1, "indoors": 1 ]));
set_short("a narrow hallway");
set_long("A single torch burns in a stone sconce across from two frayed banners.");
set_smell("default", "Smoke and damp stone.");
set_listen("default", "A metal clank echoes from somewhere.");In set_items(), keep one entry per line: spaces inside the array braces,
no space before :, one after. Plurals are stripped automatically, so key
the plural (banners) and skip the singular.
set_items(([
({ "torch", "sconce" }): "A torch set into a sconce carved from the wall.",
"banners": "Two frayed cloth banners, their designs long faded.",
]));add_item() takes a single key. For aliases use add_items() with an
array of keys.
add_items(({ "floor", "flagstones" }), "Worn flagstones, slick with damp.");set_exits() must come after ::create(), which resets the exit
mapping. It also replaces every exit, so any add_exit() goes after it.
set_exits(([
"south": CABEIRI + "goblin/room/r25.c",
]));
add_exit("north", CABEIRI + "goblin/room/r17.c");What to spawn on each reset: file path and how many.
set_reset_objects(([
CABEIRI + "goblin/npc/warrior.c": 2,
]));
}write(), say(), and message() add the newline for you, so never end
their strings with \n. No em dashes in string literals either; use a comma
or a colon.
write("The banners stir in a cold draught.");