MIDGARD · Multi User Dungeon Alpha 0.1

Kapitel 10: Hooks, Events und erweiterte Interaktionsmuster

In diesem Kapitel lernst du, wie LPC-Code auf Ereignisse reagieren kann, ohne alles direkt miteinander zu verdrahten. Hooks und Events sind ein zentrales Werkzeug für sauberen, erweiterbaren Code im Midgard MUD.

Weiter zu Kapitel 11

Sicherheit, UID/EUID und Schutzmechanismen

10.1 Das Problem mit direkter Kopplung

Als Anfänger ist es naheliegend, alles direkt zu verknüpfen: Der Hebel kennt die Tür und ruft tuer->Open() auf. Das funktioniert – aber es skaliert schlecht. Sobald ein zweites System (z.B. Alarm, Quest, Log, Statistik) auf den Hebel reagieren soll, musst du den Hebel immer wieder anfassen und erweitern.

Das ist fehleranfällig, weil dein Hebel irgendwann „alles“ kann und du bei Änderungen nie sicher bist, ob du etwas anderes aus Versehen kaputt machst. Hooks/Events lösen das, indem du das Ereignis „Hebel wurde gezogen“ publizierst, ohne zu wissen, wer darauf reagiert.

Beispiel: „Schlechte“ direkte Lösung


// hebel.c (stark gekoppelt, schwer erweiterbar)
int cmd_ziehen(string str) {
  object tuer, alarm, qm;

  if (!id(str)) return 0;

  tuer = present("tuer", environment(this_player()));
  if (tuer) tuer->OpenDoor();

  alarm = present("alarmglocke", environment(this_player()));
  if (alarm) alarm->Ring();

  qm = find_object("/obj/questmaster");
  if (qm) qm->AdvanceQuest(this_player(), "hebel_gezogen");

  write("Du ziehst am Hebel.\n");
  say(this_player()->Name(WER) + " zieht an einem Hebel.\n");
  return 1;
}
      

Diese Variante hat mehrere Probleme: Der Hebel muss wissen, wie Tür, Alarm und Questmaster heißen, wo sie liegen und welche Funktionen sie besitzen. Jede Änderung (z.B. Tür anders benennen oder Alarm auslagern) zwingt dich, den Hebelcode zu ändern.

10.2 Ereignisdenken: „Es passiert etwas“ statt „Tu X“

In einem MUD passieren viele Dinge gleichzeitig. Darum ist es sinnvoll, in Ereignissen zu denken: Ein Objekt meldet, dass etwas passiert ist – und andere Objekte können darauf reagieren. Das auslösende Objekt muss nicht wissen, wer reagiert.

Typische Ereignisse:

  • Spieler betritt Raum
  • Objekt wird bewegt (genommen, gedroppt, teleportiert)
  • Spieler benutzt ein Item
  • Status ändert sich (vergiftet, unsichtbar, verzaubert)
  • Kampf beginnt/endet

Minimal-Beispiel: Ereignis als Funktionsaufruf (ohne Hook-System)

Selbst ohne offizielles Hook-System kannst du Ereignisse als „Callback“-Mechanismus bauen: Du definierst eine Funktion, die „Event passiert“ bedeutet, und rufst sie in interessierten Objekten auf. Das ist kein echter Hook, aber das Denken ist gleich.


// raum.c (vereinfachtes Muster)
void notify_lever_pulled(object who) {
  // Raum informiert Dinge, die er kennt (hier: alle Inventory-Objekte)
  foreach(object ob : all_inventory(this_object())) {
    if (function_exists("OnLeverPulled", ob))
      ob->OnLeverPulled(who);
  }
}
      

Jetzt kann jedes Objekt im Raum optional OnLeverPulled() implementieren, ohne dass der Hebel dieses Objekt kennen muss. In der echten Mudlib erledigt das ein Hook-System eleganter und sicherer.

10.3 Was ist ein Hook konkret?

Ein Hook ist ein definierter Einhängepunkt, an dem du Reaktionen registrieren kannst. Er besteht im Kern aus drei Teilen:

  • Auslöser: „Ereignis ist passiert“
  • Registrierung: „Objekt X will informiert werden“
  • Dispatcher: „Rufe alle registrierten Reaktionen auf“

Im Midgard MUD ist dieser Mechanismus bereits vorhanden. Aber als Anfänger hilft es, ein einfaches Hook-System einmal „im Kopf“ zu verstehen.

Mini-Hook-System (didaktisches Beispiel)


// hook_demo.c (didaktisch, nicht 1:1 Midgard-Mudlib)
mapping hooks = ([]);

// registrieren: hooks["eventname"] += ({ callback })
void AddHook(string event, object ob, string fun) {
  if (!hooks[event]) hooks[event] = ({});
  hooks[event] += ({ ({ ob, fun }) });
}

// auslösen: alle Callbacks aufrufen
void TriggerHook(string event, mixed data) {
  if (!hooks[event]) return;

  foreach(mixed cb : hooks[event]) {
    object ob = cb[0];
    string fun = cb[1];
    if (ob)
      call_other(ob, fun, data);
  }
}
      

Das zeigt die Idee: Der Auslöser kennt nur den Event-Namen und Daten – nicht die konkreten Folgen. Die Reaktionsobjekte hängen sich ein und bestimmen selbst, was sie tun.

10.4 Beispiel: Hebel feuert Event, Tür reagiert

Wir nutzen das didaktische Hook-Prinzip von oben, um zu zeigen, wie ein Hebel „nur“ ein Event auslöst.

Hebel: nur auslösen


// hebel.c (Event-orientiert)
int cmd_ziehen(string str) {
  if (!id(str)) return 0;

  write("Du ziehst am Hebel.\n");
  say(this_player()->Name(WER) + " zieht an einem Hebel.\n");

  // statt Tür/Alarm/Quest direkt zu kennen:
  environment(this_player())->TriggerHook("lever_pulled", this_player());
  return 1;
}
      

Tür: reagiert, wenn sie registriert ist


// tuer.c (Reaktionsobjekt)
void OnLeverPulled(object who) {
  if (!who) return;
  // hier würde echte Türlogik stehen
  tell_room(environment(this_object()), "Die Tür klickt und öffnet sich.\n");
}
      

Registrierung (z.B. im Raum oder beim Laden)


// raum.c (Register beim Setup)
protected void create() {
  ::create();
  // Türobjekt laden/finden und registrieren
  object tuer = present("tuer", this_object());
  if (tuer)
    AddHook("lever_pulled", tuer, "OnLeverPulled");
}
      

Ergebnis: Der Hebel bleibt simpel. Du kannst später Alarm, Quest, Logging hinzufügen, indem du zusätzliche Listener registrierst – ohne Hebelcode zu ändern.

10.5 Beispiel: Quest reagiert ohne Hebelcode anzufassen

Stell dir vor, eine Quest soll nur dann fortschreiten, wenn ein Spieler den Hebel zieht und dabei mindestens Level 10 ist. Diese Regel gehört nicht in den Hebel – sie gehört in das Quest-System.


// quest_listener.c
void OnLeverPulled(object who) {
  if (!who) return;
  if (who->QueryProp(P_LEVEL) < 10) {
    tell_object(who, "Du spürst, dass dir noch Erfahrung fehlt, um etwas auszulösen.\n");
    return;
  }
  // hier: Quest fortschreiben
  tell_object(who, "Ein leises Klicken… irgendetwas hat sich verändert.\n");
}
      

Genau das ist die Stärke: Die Quest kann unabhängig vom Hebel entwickelt werden. Der Hebel meldet nur „passiert“.

10.6 Beispiel: Ein Event mit Daten (Payload)

Oft reicht es nicht, nur „Hebel gezogen“ zu melden. Man möchte Details übergeben: Wer hat gezogen? Wann? In welchem Raum? Welcher Hebel? Welche Richtung? Dafür übergibt man Daten – häufig als Mapping.

Event auslösen mit Mapping


// hebel.c
int cmd_ziehen(string str) {
  mapping data;

  if (!id(str)) return 0;

  data = ([
    "who"  : this_player(),
    "where": environment(this_player()),
    "what" : this_object(),
    "time" : time(),
    "mode" : "pull"
  ]);

  environment(this_player())->TriggerHook("lever_pulled", data);

  write("Du ziehst am Hebel.\n");
  say(this_player()->Name(WER) + " zieht an einem Hebel.\n");
  return 1;
}
      

Listener liest die Daten


// tuer.c
void OnLeverPulled(mapping data) {
  object who;

  if (!mappingp(data)) return;
  who = data["who"];

  tell_room(environment(this_object()), "Die Tür öffnet sich mit einem Knarren.\n");
  if (who)
    tell_object(who, "Du hörst, wie sich irgendwo eine Tür bewegt.\n");
}
      

Anfänger-Tipp: Nutze Mappings, wenn du merkst, dass du immer mehr Argumente mitschleppst. Ein Mapping ist selbsterklärend (über Keys) und leicht erweiterbar.

10.7 Reihenfolge und Rückgabewerte: Wer „gewinnt“?

Ein häufiger Anfängerpunkt ist: Was passiert, wenn mehrere Listener widersprechen? Beispiel: Ein Listener will den Hebel erlauben, ein anderer will ihn verbieten. Dafür braucht man Regeln.

Ein verbreitetes Muster ist: Listener geben einen Wert zurück. Wenn einer „stop“ meldet, wird abgebrochen.

Didaktische Variante: Listener kann verhindern


// TriggerHook() - erweitert
int TriggerHook(string event, mixed data) {
  if (!hooks[event]) return 1;

  foreach(mixed cb : hooks[event]) {
    object ob = cb[0];
    string fun = cb[1];

    if (!ob) continue;

    mixed res = call_other(ob, fun, data);

    // Konvention: 0 => verhindern/abbrechen
    if (intp(res) && res == 0)
      return 0;
  }
  return 1;
}
      

Listener, der blockiert (z.B. nur nachts)


// guard_listener.c
int OnLeverPulled(mapping data) {
  if (!mappingp(data)) return 1;

  object who = data["who"];
  int hour = to_int(ctime(time())[11..12]); // sehr grob, nur Demo

  if (hour >= 6 && hour < 20) {
    if (who) tell_object(who, "Der Hebel lässt sich tagsüber nicht bewegen.\n");
    return 0; // blockiert
  }
  return 1; // erlaubt
}
      

Hinweis: Das ist ein didaktisches Modell. In echten Midgard-Hooks gibt es definierte Regeln, Prioritäten und Rückgabekonzepte. Aber das Prinzip „Listener können entscheiden“ ist wichtig zu verstehen.

10.8 Typische Anfängerfehler (und wie du sie vermeidest)

Fehler 1: Hook enthält Spiellogik

Ein Hook ist nicht der Ort, um komplizierte Entscheidungen zu treffen. Der Hook soll nur auslösen, nicht „alles regeln“.

Fehler 2: Zu viele Events für Kleinkram

Wenn nur eine Tür auf einen Hebel reagiert und sonst nichts, ist ein direkter Aufruf völlig ok. Hooks lohnen sich, wenn mehrere unabhängige Systeme reagieren sollen oder Erweiterbarkeit wichtig ist.

Fehler 3: Kein „Payload“-Standard

Wenn du Event-Daten übergibst, bleib konsistent: Keys wie who, where, what sind verständlich. Ändere solche Keys nicht ständig, sonst werden Listener schwer wartbar.

Fehler 4: Keine Robustheit

Listener sollten defensiv programmieren: prüfe mappingp(), prüfe objectp(), rechne mit 0. In einem MUD können Objekte jederzeit verschwinden.

10.9 „Event“ ohne Hook: Das Observer-Muster im Kleinen

Manchmal hast du kein Hook-System zur Hand oder willst schnell ein kleines Ereignis bauen. Dann hilft ein simples Observer-Muster: Du speicherst eine Liste interessierter Objekte und informierst sie.


// notifier.c (kleines Observer-System)
object *listeners = ({});

void AddListener(object ob) {
  if (!ob) return;
  if (member(listeners, ob) < 0)
    listeners += ({ ob });
}

void RemoveListener(object ob) {
  listeners -= ({ ob });
}

void Notify(string msg) {
  listeners -= ({ 0 });
  foreach(object ob : listeners) {
    if (ob)
      ob->OnNotify(msg);
  }
}
      

Das ist kein vollwertiges Hook-System, aber für kleine Mechaniken (z.B. mehrere Lampen, die auf einen Schalter reagieren) sehr praktisch.

10.10 Zusammenfassung

Hooks und Events trennen „es passiert etwas“ von „was genau passiert dann“. Das macht deinen Code flexibler, robuster und leichter erweiterbar. Du kannst neue Reaktionen hinzufügen, ohne bestehende Objekte zu verändern.

Du hast in diesem Kapitel gelernt:

  • warum starke Kopplung langfristig Probleme verursacht
  • wie Ereignisdenken funktioniert
  • wie ein Hook prinzipiell aufgebaut ist
  • wie man Event-Daten (Payload) übergibt
  • wie mehrere Listener koordiniert werden können
  • welche Anfängerfehler häufig sind

Im nächsten Kapitel geht es um Sicherheit im Midgard MUD: UID, EUID, Rechte, Zugriffskontrolle – und warum das wichtig ist, bevor du „mächtige“ Dinge programmierst.

Weiter zu Kapitel 11

Kapitel 11 zeigt dir die Sicherheitsgrundlagen im Midgard MUD: Wer darf was? Wie schützt die Mudlib dich – und wie kannst du dich selbst schützen?

Kapitel 11 lesen

Sicherheit, UID/EUID und Schutzmechanismen