MIDGARDAudhumbla 0.7

Kapitel 12: Best Practices & Architektur

In diesem letzten Kapitel des Manuals fügen wir alles zusammen. Du lernst, wie guter LPC-Code langfristig aussieht, welche Denkweisen dir helfen, Fehler zu vermeiden, und wie du deine Objekte so strukturierst, dass sie auch nach Jahren noch verständlich und wartbar bleiben.

Weiter zu Kapitel 13

Großes Lernbeispiel – Waldlichtung zur Wikingerzeit (Raum, Hebel, Versteck, NPC, Items)

12.1 Was bedeutet „guter Code“ im MUD?

„Guter Code“ ist ein Begriff, der schnell beliebig wirkt — was für eine Person elegant ist, ist für eine andere überkomplex. Aber im Kontext eines MUD gibt es klare Kriterien, weil ein MUD eine Reihe ungewöhnlicher Eigenschaften hat: Es läuft jahrelang ohne Neustart, es wird von vielen Magierinnen und Magiern gemeinsam entwickelt, und es muss persistent mit Hunderten von Objekten klarkommen, die zur selben Zeit aktiv sind.

Guter LPC-Code ist deshalb nicht der cleverste oder kürzeste Code. Cleverer Code ist oft schwer zu verstehen — und in einem MUD muss dein Code auch in zwei Jahren noch debuggbar sein, möglicherweise durch jemand anderen, der gerade neu in der Mudlib ist. In einem MUD ist guter Code vor allem:

  • Verständlich für andere (und für dich selbst in einem Jahr). Lesbarkeit schlägt Knappheit. Eine Variable, die schaden_durch_feuer heißt, ist besser als fdsm, auch wenn sie länger ist.
  • Robust gegenüber Fehlern und unerwarteten Situationen. Spieler tun verrückte Dinge, Objekte verschwinden, das MUD ist nie „im Standardzustand“. Dein Code muss damit rechnen.
  • Leicht erweiterbar. Heute ist es ein Hebel, der eine Tür öffnet — morgen soll er auch noch einen Alarm auslösen, eine Quest fortschreiben, ein Log schreiben. Wenn du das von Anfang an einplanst (z.B. über Hooks/Events), sparst du dir später viele Umbauten.
  • Sicher im Umgang mit fremden Objekten. Ein Item kann jederzeit zerstört werden, ein Spieler kann ausloggen, ein Raum kann neu geladen werden. Defensive Prüfungen sind Pflicht.
  • Konform mit der Mudlib. Wenn die Mudlib eine Standard-Lösung bietet (AddItem, SetProp, ReceiveMsg), benutze sie. Eigene Lösungen für gelöste Probleme bringen Bugs und sind schwerer zu warten.

Viele Anfänger versuchen, möglichst „mächtig“ zu programmieren — sie wollen ihre Fähigkeiten zeigen, raffinierte Lösungen finden, die alte Hasen beeindrucken. Erfahrene Magier dagegen versuchen, möglichst langweiligen Code zu schreiben: wenig Zeilen, viele Kommentare, alles geradeaus, keine Tricks. Warum? Langweiliger Code ist stabil. Er hat weniger Stellen, an denen Bugs sich verstecken können, weniger Wechselwirkungen, weniger Überraschungen. Wenn du einen Hebel programmierst, der „nur“ einen Hebel macht — und genau das sauber tut —, hast du gewonnen. Ein Hebel, der gleichzeitig drei verschiedene Tricks beherrscht und mit fünf Sonderzeichen-Befehlen experimentiert, wird in sechs Monaten als „zu komplex zum Anfassen“ gemieden.

Merksatz: „Code wird einmal geschrieben, aber zehnmal gelesen.“ Investiere die zusätzlichen Sekunden in Klarheit beim Schreiben — du sparst Stunden beim späteren Lesen und Debuggen.

12.2 Klarer Aufbau von Objekten

Ein häufiges Anfängerproblem ist chaotischer Code: Funktionen überall, globale Variablen ohne Struktur, Logik im falschen Objekt.

Bewährtes Grundschema für viele Objekte:


// 1. Includes & Inherits
#include <properties.h>
inherit "/std/thing";

// 2. Konstanten & Defines
#define MAX_USES 3

// 3. Membervariablen
int uses;

// 4. create()
protected void create() {
  ::create();
  uses = MAX_USES;
}

// 5. Öffentliche Schnittstellen (Spielerbefehle)
int cmd_use(string str) { ... }

// 6. Interne Hilfsfunktionen
protected void do_effect(object who) { ... }
      

Diese Reihenfolge ist keine Pflicht, aber sie hilft enorm, sich schnell im Code zurechtzufinden.

12.3 Trennung von Verantwortung

Eine der wichtigsten Regeln guter Architektur lautet: Ein Objekt sollte genau eine Hauptaufgabe haben.

Schlechte Beispiele:

  • Ein Raum, der Quests verwaltet
  • Ein Item, das Spielerlevel verändert
  • Ein NPC, der globale Spielzustände speichert

Bessere Aufteilung:

  • Raum: Umgebung & Beschreibung
  • Item: Interaktion & Effekte
  • Questobjekt: Questlogik
  • Serviceobjekt: Berechnungen, Verwaltung

Wenn du merkst, dass ein Objekt immer größer wird, ist das meist ein Zeichen, dass du es aufteilen solltest.

12.4 Lesbarkeit vor Cleverness

LPC erlaubt sehr kompakte Konstrukte. Aber kompakt heißt nicht automatisch gut.

Schwer lesbar


if(o&&living(o)&&interactive(o)&&o->QueryProp(P_HP)>0)
  o->SetProp(P_HP,o->QueryProp(P_HP)-10);
      

Besser lesbar


if (!objectp(o)) return;
if (!living(o)) return;
if (!interactive(o)) return;

int hp = o->QueryProp(P_HP);
if (hp <= 0) return;

o->SetProp(P_HP, hp - 10);
      

Ja, das ist länger. Aber Fehler lassen sich hier viel leichter finden.

12.5 Defensive Programmierung als Standard

In einem laufenden MUD können jederzeit Dinge passieren: Spieler loggen aus, Objekte werden zerstört, Räume neu geladen.

Deshalb gilt:

  • Jeder Objektzugriff kann fehlschlagen
  • Jeder Funktionsaufruf kann einen Fehler werfen
  • Jede Referenz kann plötzlich 0 sein

Beispiel mit catch()


mixed res;
if (catch(res = ob->DangerousCall())) {
  log_file("errors", "DangerousCall failed in "+object_name(this_object())+"\n");
  return;
}
      

Defensive Programmierung fühlt sich am Anfang „übertrieben“ an, spart dir aber später extrem viel Zeit.

12.6 Weniger globale Variablen

Globale Variablen sind bequem, aber gefährlich. Sie machen Abhängigkeiten unsichtbar und Fehler schwer nachvollziehbar.

Problematisch


object last_user;

void use(object who) {
  last_user = who;
}
      

Was passiert, wenn zwei Spieler gleichzeitig interagieren?

Besser


void use(object who) {
  // benutze who lokal
}
      

Speichere nur das, was wirklich dauerhaft zum Objekt gehört.

12.7 Logging statt stilles Scheitern

Ein häufiger Anfängerfehler ist: Fehler passieren — aber niemand merkt es. Dein Code prüft eine Bedingung, sie schlägt fehl, der Code kehrt einfach um, und der Spieler steht da und wundert sich, warum nichts passiert ist. Aus deiner Sicht ist alles in Ordnung („return 0 — geht halt nicht“), aber für den Spieler ist es ein Bug.

Schlimmer noch: Manchmal stimmt der Fehler gar nicht mit deiner Erwartung überein. Vielleicht hast du erwartet, dass ob ein bestimmtes Item ist — in Wahrheit ist es ein anderes oder gar 0. Ohne Logs hast du keine Chance, das herauszufinden, ohne stundenlang manuell zu testen.

Schlecht: stilles Scheitern


if (!ob) return;
      

Funktioniert technisch — aber dein zukünftiges Ich (oder ein anderer Magier) hat keine Ahnung, dass der Code an dieser Stelle abbricht. Wenn der Bug erst Wochen später auffällt, weißt du nicht, wann ob erstmals fehlte.

Besser: mit Log-Eintrag


if (!ob) {
  log_file("debug",
    "ob missing in " + object_name(this_object()) + "\n");
  return;
}
      

Jetzt kannst du in /log/debug nachsehen, wann der Fehler aufgetreten ist und in welchem Objekt. Das ist Gold wert beim Debuggen.

Was wann loggen?

Faustregel: Logge alles, was unerwartet ist. Erwartete Fehlerfälle (z.B. Spieler tippt ein falsches Kommando) musst du nicht loggen — sonst füllen sich deine Logs mit Nichts. Aber unerwartete Fehler — fehlende Objekte, ungültige Argumente, nicht erfüllte Vorbedingungen — solltest du dokumentieren:


// Erwartet: Spieler hat keine Schluessel
if (!present("schluessel", pl))
  return notify_fail("Du hast keinen Schluessel.\n"), 0;

// UNerwartet: Tuer-Objekt fehlt im Raum
if (!objectp(tuer_ob)) {
  log_file("debug/raum",
    sprintf("[%s] Tuer-Objekt fehlt in %O\n",
            ctime(time()), this_object()));
  return 0;
}
      

Strukturierte Log-Dateien

Lege deine Logs unter sinnvollen Namen ab — nicht alles in eine einzige Riesendatei. Übliche Namensschemata: "debug/<modul>", "error/<modul>", "audit/<quest>":


log_file("debug/quest_drache", text);
log_file("error/movement",     text);
log_file("audit/login",        text);
      

Logs sind deine Augen, wenn etwas schiefgeht, während du nicht online bist. Sie sind oft der Unterschied zwischen „Bug nach 5 Minuten gefunden“ und „Bug nach 5 Tagen Tests immer noch nicht reproduziert“.

12.8 Zusammenarbeit & Code-Stil

LPC-Code wird selten nur von einer Person gelesen. Im Midgard MUD arbeiten viele Magierinnen und Magier gemeinsam an einer großen Welt. Selbst wenn du ein Gebiet komplett alleine baust: Eines Tages wird vielleicht jemand übernehmen, oder du selbst wirst nach drei Jahren zurückkommen und fragen: „Was hat sich der Verfasser dabei nur gedacht?“

Code-Stil ist deshalb keine Geschmacksfrage — er ist Kommunikation zwischen dir und allen, die später deinen Code lesen. Und das schließt dich selbst ein. Achte daher auf:

  • Einheitliche Einrückung (im Midgard üblich: 2 oder 4 Leerzeichen, niemals Tab-Stops mischen). Konsistenz ist wichtiger als die genaue Wahl.
  • Sinnvolle Funktions- und Variablennamencmd_oeffnen statt f1, last_attacker statt x. Jeder Name sollte beim Lesen sofort den Zweck verraten.
  • Kommentare bei nicht offensichtlicher Logik — besonders wenn du etwas Trickreiches machst, einen Workaround einbaust oder eine Begründung hast, die im Code nicht sichtbar ist.
  • Keine „magischen Zahlen“ ohne Erklärung. Wenn dein Code if (level > 27) schreibt, ist unklar, warum 27. Besser: #define MAX_NPC_LEVEL 27 oder zumindest ein Kommentar.
  • Konsistente Modifier-Reihenfolge: Im Midgard üblich private varargs int foo() — modifier vor varargs vor Rückgabetyp.
  • Strings in adjazenter Schreibweise statt mit +: das ist effizienter und liest sich besser.

Kommentare: Warum, nicht Was

Der Klassiker des schlechten Kommentars:


i++;   // erhöhe i um 1
      

Das sieht jeder. Schlechte Kommentare sind Lärm — sie blähen den Code auf, ohne Wert zu liefern. Bessere Kommentare erklären warum etwas passiert, nicht was:


// Wir starten bei 1, weil Index 0 die "leere" Eintragung ist —
// die ersten Spieler bekommen IDs ab 1.
int next_id = 1;

// 5 Sekunden Verzögerung, damit der Spieler die Animation sieht
// bevor das Objekt verschwindet.
call_out("destruct", 5);

// HACK: Diese Zeile umgeht einen Bug in /std/room/exits.c bis
// Issue #234 gefixt ist. Bitte beim nächsten Update entfernen.
SetProp(P_EXITS, ([ ]));
      

Kommentare sind aber auch nicht der Ersatz für Klarheit. Wenn du fünf Zeilen Kommentar brauchst, um drei Zeilen Code zu erklären — vielleicht ist der Code zu kompliziert. Refaktoriere in eine sprechend benannte Funktion.

Code-Reviews

Bevor du etwas Größeres ins MUD bringst, lass es einen erfahrenen Magier anschauen. Frischer Blick fängt Fehler, die du selbst nicht mehr siehst, weil du den Code zu lange im Kopf hattest.

12.8a Effizienz-Tipps (verdichtet aus /doc/concepts/effizienz)

Lesbarkeit hat im Midgard MUD Vorrang vor Effizienz – aber es gibt ein paar Faustregeln, die nichts kosten und einen messbaren Unterschied machen.

Schnelle Wins

  • foreach ist schneller und lesbarer als for+Index.
  • Lokale Arrays mit bekannter Größe vorab anlegen: int *x = allocate(10); statt x = ({}); x += ({i}); in der Schleife.
  • this_player(), this_object(), environment(), previous_object() sind so schnell wie Variablen – nur dann zwischenspeichern, wenn der Wert sich ändern kann.
  • present_clone() ist billiger als present()+geschützte IDs, wenn du ein konkretes Objekt suchst.
  • Mengen-Löschen aus Arrays: gleiche Werte auf 0 setzen, dann array -= ({0}).
  • x & y oder x | y auf Arrays geht oft schneller als handgeschriebene Schleifen.

Speicher

  • Globale Mappings/Arrays/Strings klein halten und nur bei Bedarf erweitern.
  • Wiederholten gleichen Code in eine geerbte Klasse ziehen – spart Programmgröße.
  • Blueprints sind teurer zu laden als Clones zu erzeugen. Blueprint-Initialisierung kann oft übersprungen werden:

protected void create() {
  if (!clonep(this_object())) {
    set_next_reset(-1);   // BP nicht resetten
    return;
  }
  ::create();
  // restliche Initialisierung
}
      

Lambda vermeiden, Inline-Closures bevorzugen

Lambdas sind schwer zu lesen und meistens langsamer als Inline-Closures. Im modernen Midgard-Code:


// schlecht
filter(users(),
       lambda(({'x}), ({#'call_other,'x,"QueryProp",P_SECOND})));

// besser (Inline-Closure)
filter(users(), function int (object u)
                { return u->QueryProp(P_SECOND); });

// am besten (eigene Lfun + Lfun-Closure)
private int _is_second(object u) { return u->QueryProp(P_SECOND); }
filter(users(), #'_is_second);
      

call_out & heart_beat sparsam

  • Jeder aktive call_out hält ein Objekt am Auswappen – das kostet Speicher.
  • NPCs sollten set_heart_beat(0) rufen, sobald sie alleine im Raum sind.
  • Für regelmäßige Tickjobs lieber set_next_reset()+reset() als call_out-Ketten.
  • call_out(..., 0) niemals in rekursiven Callouts – das läuft sofort wieder.

12.8b Style-Konventionen im Midgard MUD

  • Zeilenlänge max. 80 Zeichen – das ist immer noch der Standard.
  • Strings über Zeilen brechen mit nebeneinanderstehenden Literalen, nicht mit +:

SetProp(P_LONG,
  "Erste Zeile.\n"
  "Zweite Zeile.\n");
      
  • Kein this_object()->funktion() aus Faulheit – lokale Funktionen ohne -> sind schneller und klarer.
  • NPC-Sprache: nie Anführungszeichen um Gesagtes setzen (also nicht "sage \"Hallo\"") – im Midgard-Stil ist das ohne Quotes.
  • UTF-8: echte Umlaute in LPC-Strings (ä, ö, ü, ß), niemals ae/oe/ue/ss.
  • Pfade über zentrale #define-Header in local.h, nicht hardcodiert.
  • #pragma strong_types, #pragma save_types und #pragma rtt_checks sind ausdrücklich erwünscht – sie fangen Fehler früh ab.
  • Items, die ein Spieler über Logout behalten soll, brauchen SetProp(P_AUTOLOADOBJ, 1).

12.9 Typische Anfängerfallen – zusammengefasst

  • zu große Objekte
  • direktes Manipulieren fremder Objekte
  • fehlende Sicherheitsprüfungen
  • keine Fehlerbehandlung
  • übermäßig cleverer, schwer lesbarer Code

Fast jeder erfahrene Magier ist genau in diese Fallen getappt. Wichtig ist nicht, sie nie zu machen – sondern sie zu erkennen und daraus zu lernen.

12.10 Abschluss & Ausblick

Du hast jetzt ein vollständiges Grundlagenverständnis von LPC: von Datentypen über Objekte, Vererbung, Events, Sicherheit bis hin zu sauberer Architektur.

Der nächste Schritt ist Praxis: kleine Räume bauen, Items schreiben, Code lesen, mit erfahrenen Magiern sprechen.

Guter LPC-Code entsteht nicht über Nacht – sondern durch viele kleine Verbesserungen.

Willkommen in der Welt der MUD-Entwicklung.

Fast fertig!

Du hast das komplette Anfänger-Manual gelesen. Nutze es als Nachschlagewerk, kehre zu Kapiteln zurück und erweitere dein Wissen Schritt für Schritt.

In Kapitel 13 gibt es noch ein großes Beispielprojekt zum Lernen. Das wird toll!

Weiter zu Kapitel 13

Großes Lernbeispiel – Waldlichtung zur Wikingerzeit (Raum, Hebel, Versteck, NPC, Items)