MIDGARDAudhumbla 0.7

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 ist die intuitivste Lösung — sie liest sich wie ein Satz, sie funktioniert sofort, und sie ist leicht zu verstehen.

Das funktioniert auch perfekt, solange genau ein Ding auf den Hebel reagieren muss. Aber sobald ein zweites System (z.B. Alarm, Quest, Log, Statistik) auch auf den Hebel reagieren soll, musst du den Hebel anfassen und um einen weiteren Aufruf erweitern. Beim dritten System wieder. Beim vierten wieder. Nach einer Weile ist dein Hebel-Code voller Aufrufe an Objekte, die mit „Hebel“ eigentlich nichts zu tun haben.

Das ist fehleranfällig, weil:

  • Dein Hebel weiß zu viel — er kennt Pfade zu Tür, Alarm, Questmaster, Stats-Daemon. Wenn sich einer dieser Pfade ändert, brichts.
  • Du vermischst Verantwortungen. Eigentlich sollte der Hebel nur „gezogen werden“ — das Aufzeichnen von Statistiken ist nicht seine Aufgabe.
  • Bei Änderungen fasst du den Hebel an, obwohl die Änderung eigentlich woanders ist (z.B. neue Quest hinzufügen). Jede Änderung am Hebel ist eine Risiko-Stelle.
  • Test-Pfade vervielfachen sich: Was passiert, wenn die Tür fehlt? Wenn der Questmaster zerstört wurde? Wenn die Alarmglocke noch nicht geladen ist? Du musst all das im Hebel berücksichtigen.

Hooks/Events lösen das, indem du das Ereignis „Hebel wurde gezogen“ publizierst, ohne zu wissen, wer darauf reagiert. Wer reagieren möchte, registriert sich selbst — der Hebel weiß weder, dass es einen Alarm gibt, noch dass es eine Quest gibt. Damit verschiebt sich das Wissen dorthin, wo es hingehört: zu den Objekten, die eigentlich an dem Ereignis interessiert sind.

Das Prinzip heißt lose Kopplung: Beteiligte Komponenten kennen sich nur über ein abstraktes Interface (das Ereignis), nicht direkt. Es ist eines der wichtigsten Software-Design-Prinzipien überhaupt — und in einem MUD mit hundert beteiligten Magierinnen und Magiern wird es zur Pflicht.

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.9a Das echte Hook-System der Midgard-Mudlib

Bisher haben wir Hooks didaktisch erklärt. Die Midgard-Mudlib bringt aber bereits ein vollständiges Hook-System mit. Du baust es nicht selbst – du benutzt es. Konzept: Ein Objekt ist Hook-Provider (loest aus), andere Objekte sind Hook-Consumer (reagieren).

Die wichtigsten Standard-Hooks

Definiert in <hook.h>. Wichtige Hook-IDs:

  • H_HOOK_MOVE – Lebewesen wird bewegt ([ Bewegung ])
  • H_HOOK_DIE – Lebewesen stirbt ([ Tod ])
  • H_HOOK_DEFEND – Verteidigung gegen Angriff
  • H_HOOK_ATTACK – Angriff
  • H_HOOK_HP, H_HOOK_SP – LP/KP änderten sich ([ Heilung ], [ Leben ])
  • H_HOOK_FOOD, H_HOOK_DRINK, H_HOOK_ALCOHOL ([ Hunger ], [ Essen & Trinken ])
  • H_HOOK_INSERT – Item kommt ins Inventar
  • H_HOOK_EXIT_USE, H_HOOK_INIT – raumbezogen

Consumer-Typen (was darf der Listener?)

  • H_LISTENER – nur zuhören, nichts verändern (max. 5 pro Hook)
  • H_DATA_MODIFICATOR – Hook-Daten verändern (max. 3)
  • H_HOOK_MODIFICATOR – Daten verändern oder Ereignis abbrechen (max. 2)
  • H_HOOK_SURVEYOR – entscheidet, was andere duerfen (max. 1, RM-genehmigungspflichtig)

Beispiel: Listener registrieren


#include <hook.h>

void inserted_into_player(object pl) {
  pl->HRegisterToHook(
    H_HOOK_MOVE,                 // bei Bewegung des Spielers
    this_object(),               // dieses Objekt soll Callback bekommen
    H_HOOK_OTHERPRIO(2),         // Prioritaet
    H_LISTENER,                  // nur zuhören
    600                          // 600 Sekunden Lebensdauer
  );
}

// Diese Methode wird vom Hook-Provider gerufen:
mixed HookCallback(object src, int hookid, mixed data) {
  if (hookid == H_HOOK_MOVE)
    log_file("travel", "Spieler bewegt sich.\n");
  return ({ H_NO_MOD, data });    // nichts geändert
}
      

Jeder Callback liefert ein 2-elementiges Array zurück: ({ H_RETCODE, H_RETDATA }). H_RETCODE ist H_NO_MOD (nichts geändert), H_ALTERED (Daten geändert) oder H_CANCELLED (Ereignis abbrechen).

Komplette Doku unter /doc/std/hooks und /doc/concepts/hooks.

10.9b Events – das einfachere Geschwister

Neben Hooks gibt es im Midgard ein leichtes Event-System (z.B. EVT_LIB_CLOCK, das jede Stunde feuert – siehe std/inpc/schedule). Events sind nur „Zuhören + Reagieren“, ohne Daten-Manipulation. Sie eignen sich für Tagesablauf-NPCs, Wettersysteme, periodisches Aufräumen.

Faustregel:

  • Du willst nur informiert werden? → Event oder H_LISTENER-Hook
  • Du willst Daten anpassen (z.B. Schaden modifizieren)? → Hook (H_DATA_MODIFICATOR)
  • Du willst etwas abbrechen können? → Hook (H_HOOK_MODIFICATOR)

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