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 #include or #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_other strings, 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(), or heart_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.");