MIDGARDAudhumbla 0.7

Kapitel 3: Datentypen in LPC

In diesem Kapitel lernst du die wichtigsten Datentypen von LPC kennen. Datentypen sind das Fundament jeder Programmiersprache – sie bestimmen, was ein Wert ist, was man damit tun darf und was nicht. Wir gehen langsam vor, mit vielen Beispielen und Erklärungen für absolute Anfänger.

Weiter zu Kapitel 4

Danach: Variablen, Gültigkeitsbereiche und Speicher.

3.1 Was sind Datentypen überhaupt?

Ein Datentyp beschreibt, welche Art von Information in einer Variable gespeichert wird. Stell dir verschiedene Behälter im Haus vor: einen Briefkasten für Briefe, einen Geldbeutel für Münzen, eine Vase für Blumen. Du kommst nicht auf die Idee, Münzen in den Briefkasten zu werfen oder eine Blume in den Geldbeutel zu stecken — jeder Behälter hat seinen Zweck. Genauso ist es in LPC: jede Variable hat einen Typ, und der Typ legt fest, was hineindarf und welche Operationen erlaubt sind.

Warum ist das wichtig? Weil der Driver (das Programm, das deinen LPC-Code ausführt) bestimmte Operationen nur auf passenden Typen sinnvoll machen kann. Du kannst "Hallo" - "Welt" nicht subtrahieren, weil das keinen Sinn ergibt. Du kannst aber 5 + 3 rechnen oder "Hallo" + "Welt" aneinanderhängen. Ein Name ist etwas anderes als eine Zahl. Ein Objekt ist etwas anderes als ein Text. LPC muss das wissen, um korrekt arbeiten zu können.

Vergleiche aus dem echten Leben:

  • Eine Zahl → kannst du addieren, vergleichen, in einer Schleife hochzählen
  • Ein Text → kannst du anzeigen, durchsuchen, mit anderem Text verbinden
  • Ein Objekt (z.B. ein Spieler) → kannst du bewegen, dessen Properties abfragen, eine Funktion darauf aufrufen
  • Eine Liste (Array) → kannst du durchgehen, das n-te Element holen, vergrößern
  • Ein Wörterbuch (Mapping) → kannst du nach einem Schlüssel fragen, neue Einträge hinzufügen

LPC ist beim Thema Typen streng, aber nicht kleinlich. Standardmäßig prüft der Compiler bei Zuweisungen nur grob — wenn du aber #pragma strict_types oder #pragma rtt_checks oben in dein Programm schreibst, wird er strenger und fängt viele Fehler schon beim Kompilieren ab. Genau das ist im Midgard MUD die Empfehlung, und du wirst diese Pragmas in fast jedem Code-Beispiel sehen.

Viele Fehler von Anfängern entstehen, weil Datentypen verwechselt werden: eine Zahl wird wie ein String behandelt, ein Objekt wird angesprochen, das in Wahrheit gerade 0 ist (zerstört wurde), ein Mapping wird wie ein Array indiziert. Wenn du die Datentypen sicher unterscheiden kannst, hast du eine der größten Fehlerquellen schon im Griff. Genau das ist das Ziel dieses Kapitels.

3.2 Der Datentyp int – ganze Zahlen

int steht für Integer, also ganze Zahlen. Das sind Zahlen ohne Komma — positiv oder negativ. Im Midgard MUD ist int der mit Abstand am häufigsten verwendete Datentyp, weil sehr viele Werte ganzzahlig sind: Lebenspunkte, Spielerlevel, Schaden, Gewicht in Gramm, Counter, Flags und Bitmasken.

Ein int ist im Midgard mindestens 32 Bit groß (auf modernen Servern oft 64 Bit). Das bedeutet: positive Werte bis weit über zwei Milliarden passen rein. Für alle praktischen Zwecke im MUD ist „der Wertebereich“ kein Thema — du kannst beruhigt mit int rechnen.

Beispiele für int-Werte

0
1
-5
42
100000
        

Typische Verwendung im Midgard MUD:

  • Lebenspunkte (P_HP) und maximale Lebenspunkte (P_MAX_HP)
  • Level (P_LEVEL) eines NPCs oder Spielers
  • Zähler: wie oft wurde ein Hebel gezogen, wie viele Items wurden gefunden
  • Gewicht in Gramm (P_WEIGHT)
  • Schaden, der ausgeteilt oder eingesteckt wird
  • Flags als 0/1: ist die Tür offen? Hat der Spieler die Quest gelöst?
  • Zufallswerte aus random(n)
  • Zeitstempel: time() liefert Sekunden seit 1970 als int
int in einer Variable: Schaden und Heilung

int hp;       // Variable deklarieren (Wert: 0)
hp = 50;      // Wert zuweisen

hp = hp - 10; // Schaden — hp ist jetzt 40
hp = hp + 5;  // Heilung — hp ist jetzt 45

// Kürzer mit kombinierten Operatoren:
hp -= 10;     // identisch zu hp = hp - 10
hp += 5;      // identisch zu hp = hp + 5
hp++;         // hp = hp + 1
hp--;         // hp = hp - 1
        

Rechnen mit int

Du hast die üblichen mathematischen Operatoren:


int a = 10 + 3;   // 13   Addition
int b = 10 - 3;   //  7   Subtraktion
int c = 10 * 3;   // 30   Multiplikation
int d = 10 / 3;   //  3   Ganzzahl-Division (KEIN Komma!)
int e = 10 % 3;   //  1   Modulo (Rest)
int f = 2 * 7 + 1;//  15  Punkt vor Strich
      

Anfängerfalle: Ganzzahl-Division

10 / 3 ist in LPC nicht 3.33..., sondern 3. Der Rest fällt einfach weg, wenn beide Operanden int sind. Wenn du Kommazahlen brauchst, musst du float verwenden (siehe Abschnitt 3.8a).

Wahrheitswerte sind int

LPC hat keinen separaten bool-Typ wie andere Sprachen. Stattdessen gilt: 0 bedeutet „falsch“, alles andere bedeutet „wahr“. Das ist eine der wichtigsten Konventionen in LPC:


int found = 0;            // "noch nicht gefunden"
if (found) write("Hurra!\n");      // wird NICHT ausgeführt

found = 1;                // "gefunden"
if (found) write("Hurra!\n");      // wird ausgeführt

// Auch typische Tests:
if (hp)         { /* hp ist nicht 0 */ }
if (!hp)        { /* hp ist 0 */ }
if (hp > 0)     { /* explizit größer als 0 */ }
      

Wichtig: Es gibt keine Kommazahlen mit int. 3.5 ist kein gültiger int-Wert — wenn du das schreibst, ist es automatisch ein float.

3.3 Der Datentyp string – Texte

Ein string ist Text — also eine Folge von Zeichen, die für Menschen lesbar ist. Alles, was in doppelten Anführungszeichen steht, ist ein String. In einem MUD ist string nach int der zweitwichtigste Datentyp, weil ständig Texte ausgegeben, gespeichert und manipuliert werden: Namen, Beschreibungen, Ausgaben an Spieler, IDs, Befehle, Lognachrichten.

Im Midgard MUD sind Strings in echtem Unicode (UTF-8) gespeichert. Das heißt: Du kannst und sollst Umlaute (ä, ö, ü), ein ß und auch andere Sonderzeichen direkt schreiben — das ist explizite Konvention. Du musst sie also nicht als ae, oe, ue oder ss umschreiben.

Strings in LPC

"Hallo Welt"
"Ein langer Text mit Leerzeichen, Umlauten ä ö ü und ß"
"123"                  // das ist Text, keine Zahl!
""                     // leerer String
"Mit Zeilenumbruch:\n"  // \n ist ein Steuerzeichen
"Mit \"Anführungszeichen\" mittendrin"  // \" ist ein escaptes "
        

Wichtige Steuerzeichen (Escape-Sequenzen)

Manche Zeichen kannst du nicht einfach schreiben, weil sie eine Sonderbedeutung haben. Dafür gibt es Escape-Sequenzen mit dem Backslash:


\n     // Zeilenumbruch (Newline) - sehr häufig!
\t     // Tabulator
\\     // ein einzelner Backslash
\"     // ein Anführungszeichen innerhalb eines Strings
\r     // Carriage Return (selten gebraucht)
      

Im Midgard endet fast jede Zeile, die du an einen Spieler ausgibst, mit \n. Wenn dein Text seltsam zusammenklebt, hast du das wahrscheinlich vergessen.

Strings verbinden (Konkatenation)

Mit + kannst du zwei Strings aneinanderhängen. Du kannst auch Zahlen mit Strings verbinden — der Driver wandelt die Zahl automatisch in Text um:


string gruss = "Hallo " + "Welt!";
// → "Hallo Welt!"

int level = 12;
string nachricht = "Du bist Stufe " + level + ".";
// → "Du bist Stufe 12."

string name = this_player()->Name(WER);
write("Willkommen, " + name + "!\n");
      

Mehrzeilige Strings ohne +

Ein Trick im Midgard: Wenn du mehrere String-Literale direkt nebeneinander schreibst, fügt der Compiler sie automatisch zusammen. Das ist eleganter als viele Pluszeichen:


SetProp(P_LONG,
  "Du stehst auf einer windgeschützten Lichtung. "
  "Hohe Fichten rahmen den Ort ein, und der Boden "
  "ist weich von Moos und Nadeln.\n");
// Wird zu einem einzigen langen String zusammengefügt.
      

Strings werden sehr oft benutzt

  • Namen (P_NAME) und Beschreibungen (P_SHORT, P_LONG)
  • Ausgaben an Spieler über write(), tell_object()
  • IDs, mit denen man Objekte ansprechen kann ("schwert", "kleiner stein")
  • Befehle wie "norden", "iss", "untersuche"
  • Schlüssel in Mappings: map["str"]
  • Pfade: "/players/odin/work/test.c"
String-Variable in der Praxis

string name;

name = "Ein alter Baum";
write(name + "\n");

// Strings haben eine Länge:
int laenge = sizeof(name);   // 14

// Du kannst auf einzelne Zeichen zugreifen:
int erstes_zeichen = name[0];   // 'E' (= 69 in ASCII)

// Und Teilstrings ausschneiden:
string anfang = name[0..2];     // "Ein"
string ende   = name[<4..];     // "Baum"  (4 Zeichen vom Ende)
        

Häufiger Anfängerfehler: "10" ist nicht dasselbe wie 10. Das erste ist Text (drei Zeichen, falls du es so siehst — eigentlich zwei: '1' und '0'), das zweite eine Zahl. Du kannst nicht einfach mit ihnen rechnen, ohne vorher zu konvertieren. Mit to_int("10") wird aus dem String die Zahl, mit to_string(10) aus der Zahl der String — oder noch einfacher: "" + 10 wandelt die Zahl in Text um, weil String-Konkatenation automatisch konvertiert.

3.4 Der Datentyp object – Dinge in der Welt

Einer der wichtigsten Datentypen in LPC ist object. Ein „Objekt“ ist in einem MUD alles, was eine Datei mit Code als Bauplan hat und im Speicher des Drivers existiert. Dazu gehören:

  • Spieler — interaktive Objekte mit einer Verbindung
  • Räume — die Schauplätze des Spiels
  • NPCs — Monster, Händler, Wachen, Questgeber
  • Items — Schwerter, Rüstungen, Tränke, Bücher
  • Container — Beutel, Truhen, Schränke
  • Daemons — Hintergrundobjekte ohne Inventar (z.B. der Wetterdaemon)

Eine object-Variable speichert eine Referenz auf so ein Objekt, nicht das Objekt selbst. Du kannst dir das wie einen Zettel mit der Adresse vorstellen: Die Variable enthält den „Verweis", über den der Driver weiß, wo das Objekt im Speicher liegt. Wenn das Objekt zerstört wird (destruct()), zeigt deine Variable automatisch auf 0.

Object-Variable: Raum des Spielers holen

object room;

room = environment(this_player());
// room zeigt jetzt auf den Raum, in dem this_player() steht.

if (room)                                     // immer prüfen!
  write("Du bist in: " + object_name(room) + "\n");
        

Wie kommt man an Objekte?

Es gibt mehrere Standardwege, an Objekte zu kommen — sie zu erhalten gehört zum täglichen Brot beim Coden:


object pl   = this_player();           // wer hat das Kommando ausgelöst?
object self = this_object();           // ich selbst (das aktuelle Objekt)
object env  = environment(self);       // mein Container/Raum
object prev = previous_object();       // wer hat MICH gerufen?

object t = present("schwert", pl);     // hat pl ein "schwert" dabei?
object o = find_object("/std/room");   // schon geladenes Objekt finden
object n = find_player("odin");        // Spieler "odin" online?
object l = find_living("hrafn");       // ein Lebewesen "hrafn"?

object new = clone_object("/std/thing"); // einen frischen Clone erzeugen
      

Funktionen auf Objekten aufrufen

Das ist die Kernidee der Objektorientierung: Ein Objekt hat eingebaute Funktionen („Methoden“), die du von außen aufrufen kannst. Dazu gibt es den Pfeil-Operator ->:

Funktion auf Objekt aufrufen

object pl;

pl = this_player();
write(pl->Name(WER) + "\n");           // Standard-Name im Nominativ
write(pl->QueryProp(P_RACE) + "\n");   // Rasse als String
int hp = pl->QueryProp(P_HP);           // Lebenspunkte
        

Wenn du pl->QueryProp(P_HP) schreibst, sagst du dem Driver: „Geh zum Objekt pl, finde dort die Funktion QueryProp, rufe sie mit dem Argument P_HP auf und gib mir das Ergebnis zurück.“ Das wirkt unscheinbar, ist aber das Herz des MUD-Codes — fast jede Spielmechanik besteht aus solchen Aufrufen.

Wichtigste Faustregel: Immer auf 0 prüfen!

Eine object-Variable kann jederzeit den Wert 0 haben:

  • Sie wurde nie zugewiesen (nur deklariert)
  • find_player() hat niemand gefunden
  • present() hat das Objekt nicht im Container gefunden
  • Das Objekt wurde zwischendurch destruct()-et

object enemy = find_living("ork");

if (!enemy) {
  write("Kein Ork in der Nähe.\n");
  return;
}
// Ab hier ist enemy garantiert kein 0:
enemy->Defend(20, ({DT_BLUDGEON}), 0, this_object());
      

Wer eine Funktion auf einem 0-Objekt aufruft, bekommt im Driver eine Fehlermeldung wie „call to a not loaded object“ oder einen Runtime-Error. Das ist eine der häufigsten Bugquellen für Anfänger.

Merksatz: Ohne Objekte gibt es kein MUD. LPC ist eine objektorientierte Sprache, und Objekte sind buchstäblich überall — sogar du selbst, während du den Code schreibst, bist im Spiel ein Objekt.

3.5 Der Datentyp mixed – alles und nichts

mixed ist ein besonderer Typ — eine Art „Joker“. Er kann jeden anderen Datentyp enthalten: heute eine Zahl, morgen einen String, übermorgen ein Objekt. Der Compiler prüft bei einer mixed-Variable nicht, was du hineinsteckst.

Auf den ersten Blick klingt das praktisch: keine Typprobleme, du kannst einfach loslegen. Aber damit verlierst du auch den Schutz, den der Compiler normalerweise bietet. Wenn die Variable mal ein int, mal ein string ist, weiß spätere Logik nicht mehr, womit sie es gerade zu tun hat. Genau deshalb gilt im Midgard MUD die klare Konvention: mixed wird vermieden, wenn ein konkreter Typ möglich ist.

mixed in Aktion (zur Demonstration)

mixed x;

x = 10;            // x ist jetzt ein int
x = "Hallo";       // x ist jetzt ein string
x = this_player(); // x ist jetzt ein object
x = ({ 1, 2 });    // x ist jetzt ein int*
x = ([ "a":1 ]);   // x ist jetzt ein mapping
        

Wann ist mixed doch sinnvoll?

Es gibt Situationen, in denen du mixed wirklich brauchst:

  • Eine Property, die wahlweise ein String, eine Closure oder ein Array sein kann (z.B. P_LONG akzeptiert string oder closure).
  • Ein Mapping, in dem die Werte unterschiedliche Typen haben: mixed *daten
  • Eine Funktion, die unterschiedliche Ergebnistypen liefern kann (selten — meistens besser auflösbar mit Union-Typen wie int|string).
  • Beim Lesen aus QueryProp() bekommst du oft mixed zurück, weil Properties beliebig sein können.

Bessere Alternative: Union-Typen

Wenn du genau weißt, dass eine Variable nur einer von zwei oder drei Typen sein kann, nutze einen Union-Typ statt mixed. Das ist viel sauberer:


int|string foo;     // foo ist int oder string, nichts anderes
object|closure cb;  // Callback: ein Objekt ODER eine Closure
      

Vor- und Nachteile

  • Flexibel — passt sich jedem Wert an
  • Compiler hilft nicht mehr — Tippfehler werden erst zur Laufzeit erkannt
  • Schwerer zu lesen — wer das Gesehene nicht kennt, weiß nicht, womit er rechnen muss
  • Mehr Bugs möglich — falsche Operationen schlagen erst zur Laufzeit fehl
  • Schlechtere Performance — strict_types kann nicht optimieren

Empfehlung: Verwende mixed nur, wenn du es wirklich brauchst. In 90 % der Fälle weißt du, was eine Variable enthält — dann benutze auch den passenden Typ. Wenn du in fremdem Code mixed siehst, lies sehr aufmerksam, was wirklich drinsteht.

3.6 Arrays – mehrere Werte in einer Variable

Ein Array ist eine geordnete Liste von Werten. Statt vieler einzelner Variablen, die nummeriert sind (name1, name2, …), steckst du sie alle in eine Variable, die eine Reihenfolge merkt. Im MUD wirst du Arrays ständig brauchen: das Inventar eines Spielers ist ein Array von Objekten, eine Liste von IDs für ein Item, eine Liste von Räumen einer Patrouille.

In LPC schreibt man Arrays mit den Klammern ({ ... }). Das ist die wichtigste Notation, die du dir merken musst — diese Klammerform unterscheidet ein Array vom „nichts“. Ein einfacher Stern * nach dem Typ markiert den Variablentyp als „Array von …“.

Array mit Zahlen

int *numbers;             // Variable: "Array von int"

numbers = ({ 1, 2, 3, 4 });
// Vier Zahlen, geordnet, mit Indizes 0, 1, 2, 3
        
Array mit Strings

string *names;

names = ({ "Thor", "Odin", "Freya" });
// names[0] = "Thor", names[1] = "Odin", names[2] = "Freya"
        

Index-Zugriff: Das erste Element ist 0!

Anders als im täglichen Leben („Das erste Brötchen, das zweite Brötchen, …“) zählt LPC ab 0. Das ist in fast allen Programmiersprachen so — gewöhne dich früh daran:

Array-Zugriff

write(names[0] + "\n");   // "Thor"   (erstes Element)
write(names[1] + "\n");   // "Odin"   (zweites Element)
write(names[2] + "\n");   // "Freya"  (drittes Element)

// Vom Ende her zählen mit <:
write(names[<1] + "\n");  // "Freya"  (letztes Element)
write(names[<2] + "\n");  // "Odin"   (vorletztes)
        

Größe eines Arrays

Mit sizeof() erhältst du die Anzahl der Elemente. Das brauchst du bei Schleifen oder Existenz-Tests:


if (sizeof(names) > 0)        // Array nicht leer?
  write("Erstes: " + names[0] + "\n");

for (int i = 0; i < sizeof(names); i++)
  write(names[i] + "\n");
      

Arrays sind Referenzen — Vorsicht!

Wenn du eine Array-Variable einer anderen zuweist, hast du nicht zwei separate Arrays — beide Variablen zeigen auf dasselbe Array. Veränderungen an einem sind dann auch beim anderen sichtbar:


int *a = ({ 1, 2, 3 });
int *b = a;            // KEINE Kopie! Beide zeigen auf dieselbe Liste.
b[0] = 99;
write(a[0] + "\n");    // 99 ! a wurde mitverändert.

// Wenn du eine echte Kopie willst:
int *c = a + ({});     // a + leeres Array = neue Kopie
      

Diese „Referenz-Falle“ ist eine der häufigsten Bugquellen in LPC. Merken! (Mappings haben übrigens dasselbe Verhalten — siehe Kapitel 7.)

Array vs. Liste: Indizes überschreiten ist ein Fehler

Wenn du ein Element jenseits der Größe ansprichst, gibt es einen Runtime-Error. Prüfe also vorher, ob der Index gültig ist, oder verwende sicherere Konstrukte wie foreach:


int *a = ({ 1, 2, 3 });

write(a[10]);      // FEHLER: Index out of range

// Sicher:
if (10 < sizeof(a))
  write(a[10]);

// Noch besser: foreach
foreach(int x : a)
  write(x + "\n");
      

Mehr Operationen auf Arrays (Vereinigung, Subtraktion, Slicing) erklärt Kapitel 7 ausführlich.

3.7 Mappings – Schlüssel-Wert-Paare

Ein Mapping ist die zweite große Sammeldatenstruktur neben Arrays. Es ist vergleichbar mit einem Wörterbuch oder einem Karteikartenkasten: zu jedem Schlüssel gibt es einen Wert. Statt „Element Nr. 3“ wie beim Array fragst du beim Mapping nach einem benannten Eintrag — zum Beispiel: „Was steht unter dem Schlüssel str?“

Mappings sind im MUD ideal für alles, wo Zuordnungen wichtig sind und die Reihenfolge keine Rolle spielt: Spielerattribute, Konfigurationen, Suchindizes, Quest-Status, Detail-Beschreibungen eines Raums.

Mapping-Beispiel: Spielerattribute

mapping stats;

stats = ([
  "str" : 10,    // Stärke
  "dex" : 12,    // Geschick
  "int" : 15,    // Intelligenz
  "con" : 11,    // Konstitution
]);
// Ein Mapping mit 4 Eintraegen.
        

Die Notation: Mappings stehen in Klammern ([ ... ]) (mit eckigen Klammern, anders als Arrays, die runde Klammern verwenden: ({ ... })). Schlüssel und Wert werden mit einem Doppelpunkt getrennt; einzelne Einträge mit Komma.

Werte lesen

Wie bei Arrays nutzt du eckige Klammern [] — aber statt einer Zahl gibst du den Schlüssel an:

Wert aus Mapping lesen

write(stats["str"] + "\n");   // 10
write(stats["int"] + "\n");   // 15

// Wenn der Schluessel nicht existiert, gibt's KEINEN Fehler:
write(stats["foo"] + "\n");   // 0 (Default-Wert)
        

Das ist anders als bei Arrays: Wenn du einen nicht existierenden Schlüssel abfragst, bekommst du einfach 0 (oder den entsprechenden Default des Werttyps). Wenn du also wissen willst, ob ein Schlüssel wirklich existiert, nutze member(map, key).

Werte schreiben

Auch das geht über die eckige Klammer — neue Schlüssel werden automatisch angelegt:


stats["str"] = 20;     // bestehenden Wert ändern
stats["wis"] = 8;      // neuen Schlüssel anlegen
      

Wofür man Mappings im MUD nutzt

  • Spielerattribute (siehe [ Hilfe: Attribute ]): P_ATTRIBUTES ist ein Mapping mit Schlüsseln "str", "int", "dex", "con"
  • Properties intern: das gesamte Property-System eines Objekts basiert auf Mappings
  • Konfigurationen: ein Raum definiert seine Details als Mapping (Schlüssel = ID, Wert = Beschreibung)
  • Quest-Status: Mapping mit Quest-Namen → 0/1 (gelöst?)
  • Caches: beim Suchen einmal gefunden, dann unter dem Suchbegriff abgelegt
  • Sets (Mengen): Mappings mit Width 0 (siehe Kapitel 7)

Mappings sind — wie Arrays — Referenzen. Wenn du eine Mapping-Variable einer anderen zuweist, hast du nicht zwei separate Mappings. Mehr dazu in Kapitel 7 inkl. copy() und Width.

3.8 void – kein Rückgabewert

void ist kein Datentyp im engeren Sinne — du kannst keine void-Variable anlegen. Stattdessen nutzt du void als Rückgabewert einer Funktion und sagst damit dem Compiler (und allen, die deinen Code lesen): „Diese Funktion gibt nichts zurück.“

Wann ergibt das Sinn? Immer dann, wenn eine Funktion etwas tun soll, aber kein Ergebnis liefert: Text ausgeben, einen Zustand ändern, ein anderes Objekt benachrichtigen. So eine Funktion ist quasi eine „Aktion“.

void-Funktion: einfach Text ausgeben

void say_hello() {
  write("Hallo!\n");
}

// Aufruf — kein Wert zum Speichern:
say_hello();

// Das wäre ein Fehler:
int x = say_hello();   // FEHLER: void hat keinen Wert
        

Vergleich


void   inform_room(string text) { tell_room(environment(), text); }
                                   // Rückgabe: nichts

int    query_hp()                { return hp; }
                                   // Rückgabe: ein int

string Name()                    { return p_name; }
                                   // Rückgabe: ein string

object find_owner()              { return owner; }
                                   // Rückgabe: ein object
      

Faustregel: Wenn deine Funktion irgendwann ein return wert; haben soll, muss der Rückgabetyp passen — nicht void. Wenn deine Funktion nur etwas tun soll, ist void die richtige Wahl, und ein einfaches return; (ohne Wert) reicht, um die Funktion vorzeitig zu beenden.

Im Midgard MUD-Code wirst du void sehr häufig sehen: alle Setter, alle create()-Funktionen, alle Hook-Callbacks, viele Aktion-Funktionen. Das ist die Norm.

3.8a Weitere LPC-Datentypen, die du kennen solltest

Neben int, string, object, mixed, Arrays und Mappings gibt es in LDMud (dem Driver, auf dem das Midgard MUD läuft) noch ein paar weitere Datentypen. Du brauchst sie nicht sofort – aber du solltest sie wiedererkennen, wenn du in fremdem Code darauf stoesst.

float – Gleitkommazahlen

Zahlen mit Komma. Im MUD selten, aber z.B. für Resistenzen (P_RESISTANCE_STRENGTHS) oder bei Wahrscheinlichkeiten in NPCs:


float chance = 0.5;        // 50%
float resist = -0.25;      // 25% Schadensreduktion
      

Wichtig: 1 ist ein int, 1.0 ist ein float.

bytes – rohe Byte-Sequenz

Seit LDMud 3.6 wird zwischen string (Unicode-Text) und bytes (rohe Byte-Sequenz) unterschieden. Strings können Umlaute und alle Unicode-Zeichen, bytes sind binaere Daten. Die zwei Typen darfst du nicht mischen.


string s = "Hallo, Welt mit Umlauten: ä, ö, ü, ß";
bytes  b = b"binary data";
      

closure – Code als Wert

Eine Referenz auf eine Funktion. Du kannst sie speichern, weitergeben und später ausführen. Im Midgard sehr verbreitet: Set-/Querymethoden auf Properties, Filter, Sortierfunktionen, Hooks. Mehr dazu in Kapitel 5.


closure cl = #'this_player;
object pl = funcall(cl);     // ruft this_player()
      

symbol – benannte Marker

Symbole sehen aus wie Strings, sind aber „normalisiert“ und werden in Closures für Variablen-Namen genutzt: 'x steht für das Symbol mit dem Namen x. Du brauchst sie fast nur in Lambdas.

struct – benannte Datensaetze

Eine Sammlung mehrerer Werte mit benannten Feldern (vergleichbar mit C-Strukturen oder Datenklassen). Werden in der Midgard-Mudlib seltener verwendet, sind aber möglich:


struct Coordinate { int x; int y; };
struct Coordinate c = (<Coordinate> 5, 7);
write(c->x + ":" + c->y + "\n");   // 5:7
      

lwobject – Lightweight Objects

Eine leichtgewichtige Variante von Objekten ohne Inventar/Environment. Fuer Anfänger nicht relevant – aber gut zu wissen, falls du lwobject in modernem Code siehst.

Typ-Tests im Code

Bevor du auf einem Wert eine Operation ausführst, kannst du seinen Typ pruefen:


if (intp(x))      write("Ganzzahl\n");
if (stringp(x))   write("Text\n");
if (objectp(x))   write("Objekt\n");
if (pointerp(x))  write("Array\n");
if (mappingp(x))  write("Mapping\n");
if (closurep(x))  write("Closure\n");
if (floatp(x))    write("Float\n");
      

Diese Tests sind in defensivem Code Pflicht (siehe Kapitel 11/12).

3.8b Strings: Indizes, Slicing und Umlaute

LPC-Strings können wie Arrays indiziert werden – jedes Zeichen ist intern eine ganze Zahl (Unicode-Codepoint).


string s = "Test";
int    c = s[0];        // 'T' (= 84)
string sub = s[1..2];   // "es"
string end = s[<2..];   // "st" (von 2. von hinten bis zum Ende)
int    n = sizeof(s);   // 4
      

Mit <n zählst du vom Ende, mit .. machst du Slices. Strings sind veränderlich: s[0] = 't'; ändert tatsaechlich den String.

Im Midgard MUD werden Texte in echtem UTF-8 gespeichert. Schreib also "hübsch", "dass", "Würfel" – nicht "huebsch", "dass", "Wuerfel". Das ist Konvention und wird im Code-Review erwartet.

3.9 Typische Anfängerfehler

  • Zahl und String verwechseln
  • Objektfunktionen auf Strings anwenden
  • mixed überall benutzen
  • Array-Indizes falsch zählen
  • string und bytes mischen (geht nicht)
  • Umlaute kuenstlich ersetzen, statt direkt UTF-8 zu nutzen

Diese Fehler sind normal. Wichtig ist, sie zu erkennen und zu verstehen.

3.10 Zusammenfassung

  • int → ganze Zahlen
  • string → Text
  • object → Dinge in der Welt
  • mixed → alles (mit Vorsicht)
  • Arrays → Listen
  • Mappings → Schlüssel-Wert-Paare

Wenn du diese Datentypen sicher beherrschst, wird LPC deutlich einfacher.

Ausblick

Im nächsten Kapitel lernst du, wie Variablen definiert werden, wie lange sie existieren und wo sie sichtbar sind.

Weiter zu Kapitel 4

Variablen und Gültigkeitsbereiche.