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