blob: 0c375534b8de1102ecab05128ec2278feaea7983 [file] [log] [blame]
MG Mud User88f12472016-06-24 23:31:02 +02001// channeld.c
2//
3// $Id: channeld.c 9138 2015-02-03 21:46:56Z Zesstra $
4//
5
6#pragma strong_types
7#pragma no_shadow // keine Shadowing...
8#pragma no_clone
9#pragma no_inherit
10#pragma save_types
11
12#include <sys_debug.h>
13#include <lpctypes.h>
14#include <wizlevels.h>
Arathorn78c08372019-12-11 20:14:23 +010015#include <regexp.h>
MG Mud User88f12472016-06-24 23:31:02 +020016
17#include <properties.h>
18#include <config.h>
19#include <language.h>
20
21#define NEED_PROTOTYPES
22#include "channel.h"
23
Arathorn78c08372019-12-11 20:14:23 +010024#define CHANNEL_SAVE "/p/daemon/save/channeld"
25#define MEMORY "/secure/memory"
26#define MAX_HIST_SIZE 200
27#define MAX_CHANNELS 90
28#define CMDS ({C_FIND, C_LIST, C_JOIN, C_LEAVE, C_SEND, C_NEW})
MG Mud User88f12472016-06-24 23:31:02 +020029
Arathorn78c08372019-12-11 20:14:23 +010030
31/* Ebenenliste und die zugehoerigen Daten, z.B. Mitglieder oder Beschreibung
Arathorn19459eb2019-11-30 00:45:51 +010032 channels = ([string channelname : ({ ({object* members}),
33 closure access_rights,
Arathorn78c08372019-12-11 20:14:23 +010034 string channel_desc,
Zesstrae19391f2020-08-09 13:40:12 +020035 string|object supervisor,
Zesstra78310012020-08-09 12:21:48 +020036 string readable_channelname }) ])
37 The master_object is also called the supervisor.
38 */
Arathorn78c08372019-12-11 20:14:23 +010039private nosave mapping channels = ([]);
Arathorn19459eb2019-11-30 00:45:51 +010040//private nosave mapping lowerch; // unused
41
Arathorn78c08372019-12-11 20:14:23 +010042/* Ebenenhistory
43 mapping channelH = ([ string channelname : ({ ({string channelname,
44 string sender,
45 string msg,
46 int msg_type}) }) ]) */
47// channelH wird in create() geeignet initialisiert
48// HINWEIS: Bitte beachten, dass channelH immer nur so manipuliert werden
49// darf, dass keine Kopie erstellt wird, weder direkt noch implizit. Die
50// History wird via Referenz in /secure/memory hinterlegt, damit sie einen
51// Reload des Channeld ueberlebt. Das funktioniert aber nur, wenn die Mapping-
52// Referenz in Memory und Channeld dieselbe ist.
MG Mud User88f12472016-06-24 23:31:02 +020053private nosave mapping channelH;
Arathorn19459eb2019-11-30 00:45:51 +010054
Arathorn78c08372019-12-11 20:14:23 +010055/* Globale channeld-Stats (Startzeit, geladen von, Anzahl erstellte und
56 zerstoerte Ebenen.
57 mapping stats = ([ "time" : int object_time(),
58 "boot" : string getuid(previous_object()),
Arathorn19459eb2019-11-30 00:45:51 +010059 "new" : int total_channels_created,
60 "disposed" : int total_channels_removed ]) */
Arathorn78c08372019-12-11 20:14:23 +010061// stats wird in create() geeignet initialisiert
MG Mud User88f12472016-06-24 23:31:02 +020062private nosave mapping stats;
63
Arathorn78c08372019-12-11 20:14:23 +010064/* Ebenen-Cache, enthaelt Daten zu inaktiven Ebenen.
Arathorn19459eb2019-11-30 00:45:51 +010065 mapping channelC = ([ string channelname : ({ string I_NAME,
66 string I_INFO,
67 int time() }) ]) */
Arathorn78c08372019-12-11 20:14:23 +010068private mapping channelC = ([]);
Arathorn19459eb2019-11-30 00:45:51 +010069
Arathorn78c08372019-12-11 20:14:23 +010070/* Liste von Spielern, fuer die ein Bann besteht, mit den verbotenen Kommandos
71 mapping channelB = ([ string playername : string* banned_command ]) */
72private mapping channelB = ([]);
MG Mud User88f12472016-06-24 23:31:02 +020073
Arathorn78c08372019-12-11 20:14:23 +010074/* Timeout-Liste der Datenabfrage-Kommandos; die Timestamps werden verwendet,
75 um sicherzustellen, dass jedes Kommando max. 1x pro Minute benutzt werden
76 kann.
77
Arathorn19459eb2019-11-30 00:45:51 +010078 mapping Tcmd = ([ "lag": int timestamp,
79 "uptime": int timestamp,
80 "statistik": int timestamp]) */
81private mapping Tcmd = ([]);
82
Arathorn78c08372019-12-11 20:14:23 +010083/* Flag, das anzeigt, dass Daten veraendert wurden und beim naechsten
84 Speicherevent das Savefile geschrieben werden soll.
85 Wird auf 0 oder 1 gesetzt. */
Zesstraa2db5522020-08-11 22:14:55 +020086private nosave int save_me_soon;
MG Mud User88f12472016-06-24 23:31:02 +020087
Arathorn19459eb2019-11-30 00:45:51 +010088
MG Mud User88f12472016-06-24 23:31:02 +020089// BEGIN OF THE CHANNEL MASTER ADMINISTRATIVE PART
90
Arathorn78c08372019-12-11 20:14:23 +010091// Indizes fuer Zugriffe auf das Mapping <admin>.
MG Mud User88f12472016-06-24 23:31:02 +020092#define RECV 0
93#define SEND 1
94#define FLAG 2
95
Arathorn78c08372019-12-11 20:14:23 +010096// Ebenenflags, gespeichert in admin[ch, FLAG]
97// F_WIZARD kennzeichnet reine Magierebenen
MG Mud User88f12472016-06-24 23:31:02 +020098#define F_WIZARD 1
Arathorn78c08372019-12-11 20:14:23 +010099// Ebenen, auf denen keine Gaeste erlaubt sind, sind mit F_NOGUEST markiert.
MG Mud User88f12472016-06-24 23:31:02 +0200100#define F_NOGUEST 2
101
Arathorn78c08372019-12-11 20:14:23 +0100102/* Speichert Sende- und Empfangslevel sowie Flags zu den einzelnen Channeln.
103 Wird beim Laden des Masters via create() -> initalize() -> setup() mit den
104 Daten aus dem Init-File ./channeld.init befuellt.
105 mapping admin = ([ string channel_name : int RECV_LVL,
106 int SEND_LVL,
107 int FLAG ]) */
MG Mud User88f12472016-06-24 23:31:02 +0200108private nosave mapping admin = m_allocate(0, 3);
109
Arathorn78c08372019-12-11 20:14:23 +0100110// check_ch_access() prueft die Zugriffsberechtigungen auf Ebenen.
111//
112// Gibt 1 zurueck, wenn Aktion erlaubt, 0 sonst.
113// Wird von access() gerufen; access() gibt das Ergebnis von
114// check_ch_access() zurueck.
115//
116// Verlassen (C_LEAVE) ist immer erlaubt. Die anderen Aktionen sind in zwei
117// Gruppen eingeteilt:
118// 1) RECV. Die Aktionen dieser Gruppe sind Suchen (C_FIND), Auflisten
119// (C_LIST) und Betreten (C_JOIN).
120// 2) SEND. Die Aktion dieser Gruppe ist zur Zeit nur Senden (C_SEND).
121//
122// Aktionen werden zugelassen, wenn Spieler/MagierLevel groesser ist als die
123// fuer die jeweilige Aktionsgruppe RECV oder SEND festgelegte Stufe.
124// Handelt es sich um eine Magierebene (F_WIZARD), muss die Magierstufe
125// des Spielers groesser sein als die Mindeststufe der Ebene. Ansonsten
126// wird gegen den Spielerlevel geprueft.
127//
128// Wenn RECV_LVL oder SEND_LVL auf -1 gesetzt ist, sind die Aktionen der
129// jeweiligen Gruppen komplett geblockt.
MG Mud User88f12472016-06-24 23:31:02 +0200130
Arathorn739a4fa2020-08-06 21:52:58 +0200131public int check_ch_access(string ch, object pl, string cmd)
Arathorn78c08372019-12-11 20:14:23 +0100132{
133 // <pl> ist Gast, es sind aber keine Gaeste zugelassen? Koennen wir
134 // direkt ablehnen.
Arathorn19459eb2019-11-30 00:45:51 +0100135 if ((admin[ch, FLAG] & F_NOGUEST) && pl->QueryGuest())
136 return 0;
137
Arathorn78c08372019-12-11 20:14:23 +0100138 // Ebenso auf Magier- oder Seherebenen, wenn ein Spieler anfragt, der
139 // noch kein Seher ist.
Arathorn19459eb2019-11-30 00:45:51 +0100140 if ((admin[ch, FLAG] & F_WIZARD) && query_wiz_level(pl) < SEER_LVL)
141 return 0;
142
Arathorn78c08372019-12-11 20:14:23 +0100143 // Ebene ist Magierebene? Dann werden alle Stufenlimits gegen Magierlevel
144 // geprueft, ansonsten gegen Spielerlevel.
145 int level = (admin[ch, FLAG] & F_WIZARD
146 ? query_wiz_level(pl)
147 : pl->QueryProp(P_LEVEL));
MG Mud User88f12472016-06-24 23:31:02 +0200148
Arathorn19459eb2019-11-30 00:45:51 +0100149 switch (cmd)
MG Mud User88f12472016-06-24 23:31:02 +0200150 {
Arathorn19459eb2019-11-30 00:45:51 +0100151 case C_FIND:
152 case C_LIST:
153 case C_JOIN:
154 if (admin[ch, RECV] == -1)
155 return 0;
156 if (admin[ch, RECV] <= level)
157 return 1;
158 break;
159
160 case C_SEND:
161 if (admin[ch, SEND] == -1)
162 return 0;
163 if (admin[ch, SEND] <= level)
164 return 1;
165 break;
166
Arathorn78c08372019-12-11 20:14:23 +0100167 // Verlassen ist immer erlaubt
Arathorn19459eb2019-11-30 00:45:51 +0100168 case C_LEAVE:
169 return 1;
170
171 default:
172 break;
MG Mud User88f12472016-06-24 23:31:02 +0200173 }
Arathorn19459eb2019-11-30 00:45:51 +0100174 return (0);
MG Mud User88f12472016-06-24 23:31:02 +0200175}
176
Arathorn78c08372019-12-11 20:14:23 +0100177/* CountUsers() zaehlt die Anzahl Abonnenten aller Ebenen. */
178// TODO: Mapping- und Arrayvarianten bzgl. der Effizienz vergleichen
179private int CountUsers()
MG Mud User88f12472016-06-24 23:31:02 +0200180{
Arathorn78c08372019-12-11 20:14:23 +0100181 object* userlist = ({});
182 foreach(string ch_name, mixed* ch_data : channels)
183 {
184 userlist += ch_data[I_MEMBER];
185 }
186 // Das Mapping dient dazu, dass jeder Eintrag nur einmal vorkommt.
187 return sizeof(mkmapping(userlist));
MG Mud User88f12472016-06-24 23:31:02 +0200188}
189
Arathorn78c08372019-12-11 20:14:23 +0100190// Ist das Objekt <sender> Abonnent der Ebene <ch>?
191private int IsChannelMember(string ch, object sender)
MG Mud User88f12472016-06-24 23:31:02 +0200192{
Arathorn78c08372019-12-11 20:14:23 +0100193 return (member(channels[ch][I_MEMBER], sender) != -1);
MG Mud User88f12472016-06-24 23:31:02 +0200194}
195
Arathorn78c08372019-12-11 20:14:23 +0100196// Besteht fuer das Objekt <ob> ein Bann fuer die Ebenenfunktion <command>?
197private int IsBanned(string|object ob, string command)
MG Mud User88f12472016-06-24 23:31:02 +0200198{
Arathorn78c08372019-12-11 20:14:23 +0100199 if (objectp(ob))
200 ob = getuid(ob);
201 return(pointerp(channelB[ob]) &&
202 member(channelB[ob], command) != -1);
203}
MG Mud User88f12472016-06-24 23:31:02 +0200204
Arathorn78c08372019-12-11 20:14:23 +0100205private void banned(string plname, string* cmds, string res)
206{
207 res += sprintf("%s [%s], ", capitalize(plname), implode(cmds, ","));
208}
209
210#define TIMEOUT (time() - 60)
211
212// IsNotBlocked(): prueft fuer die Liste der uebergebenen Kommandos, ob
213// die Zeitsperre fuer alle abgelaufen ist und sie ausgefuehrt werden duerfen.
214// Dabei gilt jedes Kommando, dessen letzte Nutzung laenger als 60 s
215// zurueckliegt, als "nicht gesperrt".
216private int IsNotBlocked(string* cmd)
217{
218 string* res = filter(cmd, function int (string str) {
219 return (Tcmd[str] < TIMEOUT);
220 });
221 // Wenn das Ergebnis-Array genauso gross ist wie das Eingabe-Array, dann
222 // sind alle Kommandos frei. Sie werden direkt gesperrt; return 1
223 // signalisiert dem Aufrufer, dass das Kommando ausgefuehrt werden darf.
224 if (sizeof(res) == sizeof(cmd)) {
225 foreach(string str : cmd) {
226 Tcmd[str] = time();
227 }
228 return 1;
229 }
230 return 0;
231}
232
233// Prueft, ob der gesendete Befehl <cmd> als gueltiges Kommando <check>
234// zugelassen wird. Anforderungen:
235// 1) <cmd> muss Teilstring von <check> sein
236// 2) <cmd> muss am Anfang von <check> stehen
237// 3) <cmd> darf nicht laenger sein als <check>
238// 4) die Nutzung von <cmd> darf nur einmal pro Minute erfolgen
239// Beispiel: check = "statistik", cmd = "stat" ist gueltig, nicht aber
240// cmd = "statistiker" oder cmd = "tist"
241// Wenn die Syntax zugelassen wird, wird anschliessend geprueft
242private int IsValidChannelCommand(string cmd, string check) {
243 // Syntaxcheck (prueft Bedingungen 1 bis 3).
244 if ( strstr(check, cmd)==0 && sizeof(cmd) <= sizeof(check) ) {
245 string* cmd_to_check;
246 // Beim Kombi-Kommando "lust" muessen alle 3 Befehle gecheckt werden.
247 // Der Einfachheit halber werden auch Einzelkommandos als Array ueber-
248 // geben.
249 if ( cmd == "lust" )
250 cmd_to_check = ({"lag", "statistik", "uptime"});
251 else
252 cmd_to_check = ({cmd});
253 // Prueft die Zeitsperre (Bedingung 4).
254 return (IsNotBlocked(cmd_to_check));
255 }
256 return 0;
257}
258
259#define CH_NAME 0
260#define CH_SENDER 1
261#define CH_MSG 2
262#define CH_MSG_TYPE 3
263// Gibt die Channelmeldungen fuer die Kommandos up, stat, lag und bann des
264// <MasteR>-Channels aus. Auszugebende Informationen werden in <ret> gesammelt
265// und dieses per Callout an send() uebergeben.
266// Argument: ({string channels[ch][I_NAME], object pl, string msg, int type})
267// Funktion muss public sein, auch wenn der erste Check im Code das Gegenteil
268// nahezulegen scheint, weil sie von send() per call_other() gerufen wird,
269// was aber bei einer private oder protected Funktion nicht moeglich waere.
270public void ChannelMessage(<string|object|int>* msg)
271{
272 // Wir reagieren nur auf Meldungen, die wir uns selbst geschickt haben,
273 // aber nur dann, wenn sie auf der Ebene <MasteR> eingegangen sind.
274 if (msg[CH_SENDER] == this_object() || !stringp(msg[CH_MSG]) ||
275 msg[CH_NAME] != CMNAME || previous_object() != this_object())
Arathorn19459eb2019-11-30 00:45:51 +0100276 return;
MG Mud User88f12472016-06-24 23:31:02 +0200277
Arathorn78c08372019-12-11 20:14:23 +0100278 float* lag;
279 int max, rekord;
280 string ret;
Arathorn739a4fa2020-08-06 21:52:58 +0200281 string mesg = msg[CH_MSG];
MG Mud User88f12472016-06-24 23:31:02 +0200282
Arathorn78c08372019-12-11 20:14:23 +0100283 if (IsValidChannelCommand(mesg, "hilfe"))
MG Mud User88f12472016-06-24 23:31:02 +0200284 {
Arathorn78c08372019-12-11 20:14:23 +0100285 ret = "Folgende Kommandos gibt es: hilfe, lag, uptime, statistik, lust, "
286 "bann. Die Kommandos koennen abgekuerzt werden.";
Arathorn19459eb2019-11-30 00:45:51 +0100287 }
Arathorn78c08372019-12-11 20:14:23 +0100288 else if (IsValidChannelCommand(mesg, "lag"))
Arathorn19459eb2019-11-30 00:45:51 +0100289 {
MG Mud User88f12472016-06-24 23:31:02 +0200290 lag = "/p/daemon/lag-o-daemon"->read_ext_lag_data();
291 ret = sprintf("Lag: %.1f%%/60, %.1f%%/15, %.1f%%/5, %.1f%%/1, "
Arathorn19459eb2019-11-30 00:45:51 +0100292 "%.1f%%/20s, %.1f%%/2s",
293 lag[5], lag[4], lag[3], lag[2], lag[1], lag[0]);
Arathorn78c08372019-12-11 20:14:23 +0100294 // Erster Callout wird hier schon abgesetzt, um sicherzustellen, dass
295 // die Meldung in zwei Zeilen auf der Ebene erscheint.
Arathorn19459eb2019-11-30 00:45:51 +0100296 call_out(#'send, 2, CMNAME, this_object(), ret);
MG Mud User88f12472016-06-24 23:31:02 +0200297 ret = query_load_average();
Arathorn19459eb2019-11-30 00:45:51 +0100298 }
Arathorn78c08372019-12-11 20:14:23 +0100299 else if (IsValidChannelCommand(mesg, "uptime"))
MG Mud User88f12472016-06-24 23:31:02 +0200300 {
Arathorn78c08372019-12-11 20:14:23 +0100301 if (file_size("/etc/maxusers") > 0 && file_size("/etc/maxusers.ever"))
Arathorn19459eb2019-11-30 00:45:51 +0100302 {
Arathorn78c08372019-12-11 20:14:23 +0100303 string unused;
304 sscanf(read_file("/etc/maxusers"), "%d %s", max, unused);
305 sscanf(read_file("/etc/maxusers.ever"), "%d %s", rekord, unused);
306 ret = sprintf("Das MUD laeuft jetzt %s. Es sind momentan %d Spieler "
307 "eingeloggt; das Maximum lag heute bei %d und der Rekord "
308 "bisher ist %d.", uptime(), sizeof(users()), max, rekord);
Arathorn19459eb2019-11-30 00:45:51 +0100309 }
310 else
311 {
Arathorn78c08372019-12-11 20:14:23 +0100312 ret = "Diese Information liegt nicht vor.";
MG Mud User88f12472016-06-24 23:31:02 +0200313 }
Arathorn19459eb2019-11-30 00:45:51 +0100314 }
Arathorn78c08372019-12-11 20:14:23 +0100315 else if (IsValidChannelCommand(mesg, "statistik"))
MG Mud User88f12472016-06-24 23:31:02 +0200316 {
MG Mud User88f12472016-06-24 23:31:02 +0200317 ret = sprintf(
Arathorn78c08372019-12-11 20:14:23 +0100318 "Im Moment sind insgesamt %d Ebenen mit %d Teilnehmern aktiv. "
319 "Der %s wurde das letzte mal am %s von %s neu gestartet. "
320 "Seitdem wurden %d Ebenen neu erzeugt und %d zerstoert.",
321 sizeof(channels), CountUsers(), CMNAME,
Arathorn19459eb2019-11-30 00:45:51 +0100322 dtime(stats["time"]), stats["boot"], stats["new"], stats["dispose"]);
323 }
Arathorn78c08372019-12-11 20:14:23 +0100324 // Ebenenaktion beginnt mit "bann"?
325 else if (strstr(mesg, "bann")==0)
MG Mud User88f12472016-06-24 23:31:02 +0200326 {
327 string pl, cmd;
Arathorn19459eb2019-11-30 00:45:51 +0100328
329 if (mesg == "bann")
330 {
331 if (sizeof(channelB))
MG Mud User88f12472016-06-24 23:31:02 +0200332 {
Arathorn78c08372019-12-11 20:14:23 +0100333 ret = "Fuer folgende Spieler besteht ein Bann: ";
334 // Zwischenspeicher fuer die Einzeleintraege, um diese spaeter mit
335 // CountUp() in eine saubere Aufzaehlung umwandeln zu koennen.
336 string* banlist = ({});
337 foreach(string plname, string* banned_cmds : channelB) {
338 banlist += ({ sprintf("%s [%s]",
339 capitalize(plname), implode(banned_cmds, ", "))});
340 }
341 ret = CountUp(banlist);
MG Mud User88f12472016-06-24 23:31:02 +0200342 }
343 else
344 {
Arathorn19459eb2019-11-30 00:45:51 +0100345 ret = "Zur Zeit ist kein Bann aktiv.";
346 }
347 }
348 else
349 {
Arathorn78c08372019-12-11 20:14:23 +0100350 if (sscanf(mesg, "bann %s %s", pl, cmd) == 2 &&
351 IS_DEPUTY(msg[CH_SENDER]))
Arathorn19459eb2019-11-30 00:45:51 +0100352 {
353 pl = lower_case(pl);
354 cmd = lower_case(cmd);
355
356 if (member(CMDS, cmd) != -1)
357 {
Arathorn78c08372019-12-11 20:14:23 +0100358 // Kein Eintrag fuer <pl> in der Bannliste vorhanden, dann anlegen;
359 // ist der Eintrag kein Array, ist ohnehin was faul, dann wird
360 // ueberschrieben.
Arathorn19459eb2019-11-30 00:45:51 +0100361 if (!pointerp(channelB[pl]))
Arathorn78c08372019-12-11 20:14:23 +0100362 m_add(channelB, pl, ({}));
Arathorn19459eb2019-11-30 00:45:51 +0100363
Arathorn78c08372019-12-11 20:14:23 +0100364 if (IsBanned(pl, cmd))
Arathorn19459eb2019-11-30 00:45:51 +0100365 channelB[pl] -= ({ cmd });
366 else
367 channelB[pl] += ({ cmd });
Arathorn19459eb2019-11-30 00:45:51 +0100368
Arathorn78c08372019-12-11 20:14:23 +0100369 ret = "Fuer '" + capitalize(pl) + "' besteht " +
370 (sizeof(channelB[pl])
371 // TODO: implode() -> CountUp()?
372 ? "folgender Bann: " + implode(channelB[pl], ", ") + "."
373 : "kein Bann mehr.");
374
375 // Liste der gebannten Kommandos leer? Dann <pl> komplett austragen.
Arathorn19459eb2019-11-30 00:45:51 +0100376 if (!sizeof(channelB[pl]))
Arathorn78c08372019-12-11 20:14:23 +0100377 m_delete(channelB, pl);
Arathorn19459eb2019-11-30 00:45:51 +0100378
Zesstraa2db5522020-08-11 22:14:55 +0200379 //TODO: save_me_soon=1 sollte auch reichen...
Arathorn19459eb2019-11-30 00:45:51 +0100380 save_object(CHANNEL_SAVE);
381 }
382 else
383 {
384 ret = "Das Kommando '" + cmd + "' ist unbekannt. "
Arathorn78c08372019-12-11 20:14:23 +0100385 "Erlaubte Kommandos: "+ CountUp(CMDS);
Arathorn19459eb2019-11-30 00:45:51 +0100386 }
387 }
388 else
389 {
Arathorn78c08372019-12-11 20:14:23 +0100390 if (IS_ARCH(msg[CH_SENDER]))
Arathorn19459eb2019-11-30 00:45:51 +0100391 ret = "Syntax: bann <name> <kommando>";
MG Mud User88f12472016-06-24 23:31:02 +0200392 }
393 }
394 }
Arathorn78c08372019-12-11 20:14:23 +0100395 else if (IsValidChannelCommand(mesg, "lust"))
MG Mud User88f12472016-06-24 23:31:02 +0200396 {
MG Mud User88f12472016-06-24 23:31:02 +0200397 lag = "/p/daemon/lag-o-daemon"->read_lag_data();
Arathorn78c08372019-12-11 20:14:23 +0100398 if (file_size("/etc/maxusers") > 0 && file_size("/etc/maxusers.ever"))
399 {
400 string unused;
401 sscanf(read_file("/etc/maxusers"), "%d %s", max, unused);
402 sscanf(read_file("/etc/maxusers.ever"), "%d %s", rekord, unused);
403 }
MG Mud User88f12472016-06-24 23:31:02 +0200404
Arathorn78c08372019-12-11 20:14:23 +0100405 int t = time() - last_reboot_time();
406
407 // TODO: fuer solche Anwendungen ein separates Inheritfile bauen, da
408 // die Funktionalitaet oefter benoetigt wird als nur hier.
409 string up = "";
Arathorn19459eb2019-11-30 00:45:51 +0100410 if (t >= 86400)
411 up += sprintf("%dT", t / 86400);
MG Mud User88f12472016-06-24 23:31:02 +0200412
Arathorn78c08372019-12-11 20:14:23 +0100413 t %= 86400;
Arathorn19459eb2019-11-30 00:45:51 +0100414 if (t >= 3600)
Arathorn78c08372019-12-11 20:14:23 +0100415 up += sprintf("%dh", t / 3600);
Arathorn19459eb2019-11-30 00:45:51 +0100416
Arathorn78c08372019-12-11 20:14:23 +0100417 t %= 3600;
Arathorn19459eb2019-11-30 00:45:51 +0100418 if (t > 60)
Arathorn78c08372019-12-11 20:14:23 +0100419 up += sprintf("%dm", t / 60);
Arathorn19459eb2019-11-30 00:45:51 +0100420
421 up += sprintf("%ds", t % 60);
Arathorn78c08372019-12-11 20:14:23 +0100422
MG Mud User88f12472016-06-24 23:31:02 +0200423 ret = sprintf("%.1f%%/15 %.1f%%/1 %s %d:%d:%d E:%d T:%d",
Arathorn19459eb2019-11-30 00:45:51 +0100424 lag[1], lag[2], up, sizeof(users()), max, rekord,
Arathorn78c08372019-12-11 20:14:23 +0100425 sizeof(channels), CountUsers());
Arathorn19459eb2019-11-30 00:45:51 +0100426 }
427 else
428 {
429 return;
430 }
MG Mud User88f12472016-06-24 23:31:02 +0200431
Arathorn78c08372019-12-11 20:14:23 +0100432 // Nur die Ausgabe starten, wenn ein Ausgabestring vorliegt. Es kann
433 // vorkommen, dass weiter oben keiner zugewiesen wird, weil die Bedingungen
434 // nicht erfuellt sind.
435 if (stringp(ret) && sizeof(ret))
436 call_out(#'send, 2, CMNAME, this_object(), ret);
MG Mud User88f12472016-06-24 23:31:02 +0200437}
438
439// setup() -- set up a channel and register it
440// arguments are stored in the following order:
Arathorn78c08372019-12-11 20:14:23 +0100441// string* chinfo = ({ channel_name, receive_level, send_level,
Zesstrae19391f2020-08-09 13:40:12 +0200442// flags, description, supervisor })
Arathorn78c08372019-12-11 20:14:23 +0100443private void setup(string* chinfo)
MG Mud User88f12472016-06-24 23:31:02 +0200444{
Arathorn78c08372019-12-11 20:14:23 +0100445 string desc = "- Keine Beschreibung -";
Zesstrae19391f2020-08-09 13:40:12 +0200446 object supervisor = this_object();
Arathorn19459eb2019-11-30 00:45:51 +0100447
Arathorn78c08372019-12-11 20:14:23 +0100448 if (sizeof(chinfo) && sizeof(chinfo[0]) > 1 && chinfo[0][0] == '\\')
449 chinfo[0] = chinfo[0][1..];
MG Mud User88f12472016-06-24 23:31:02 +0200450
Arathorn78c08372019-12-11 20:14:23 +0100451 switch (sizeof(chinfo))
MG Mud User88f12472016-06-24 23:31:02 +0200452 {
Arathorn78c08372019-12-11 20:14:23 +0100453 // Alle Fallthroughs in dem switch() sind Absicht.
Arathorn19459eb2019-11-30 00:45:51 +0100454 case 6:
Arathorn78c08372019-12-11 20:14:23 +0100455 if (stringp(chinfo[5]) && sizeof(chinfo[5]))
Zesstrae19391f2020-08-09 13:40:12 +0200456 catch(supervisor = load_object(chinfo[5]); publish);
457 if (!objectp(supervisor))
458 supervisor = this_object();
Arathorn19459eb2019-11-30 00:45:51 +0100459
460 case 5:
Arathorn78c08372019-12-11 20:14:23 +0100461 if (stringp(chinfo[4]) || closurep(chinfo[4]))
462 desc = chinfo[4];
Zesstra26aaf1a2020-08-07 19:10:39 +0200463 // Die admin-Daten sind nicht fuer die Ebene wichtig, nur fuer die
464 // check_ch_access().
Arathorn19459eb2019-11-30 00:45:51 +0100465 case 4:
Arathorn739a4fa2020-08-06 21:52:58 +0200466 admin[lower_case(chinfo[0]), FLAG] = to_int(chinfo[3]);
Arathorn19459eb2019-11-30 00:45:51 +0100467
468 case 3:
Arathorn739a4fa2020-08-06 21:52:58 +0200469 admin[lower_case(chinfo[0]), SEND] = to_int(chinfo[2]);
Arathorn19459eb2019-11-30 00:45:51 +0100470
471 case 2:
Arathorn739a4fa2020-08-06 21:52:58 +0200472 admin[lower_case(chinfo[0]), RECV] = to_int(chinfo[1]);
Arathorn19459eb2019-11-30 00:45:51 +0100473 break;
474
475 case 0:
476 default:
477 return;
MG Mud User88f12472016-06-24 23:31:02 +0200478 }
Arathorn19459eb2019-11-30 00:45:51 +0100479
Zesstrae19391f2020-08-09 13:40:12 +0200480 if (new(chinfo[0], supervisor, desc) == E_ACCESS_DENIED)
MG Mud User88f12472016-06-24 23:31:02 +0200481 {
Arathorn78c08372019-12-11 20:14:23 +0100482 log_file("CHANNEL", sprintf("[%s] %s: %O: error, access denied\n",
Zesstrae19391f2020-08-09 13:40:12 +0200483 dtime(time()), chinfo[0], supervisor));
MG Mud User88f12472016-06-24 23:31:02 +0200484 }
485 return;
486}
487
Arathorn78c08372019-12-11 20:14:23 +0100488private void initialize()
MG Mud User88f12472016-06-24 23:31:02 +0200489{
Arathorn78c08372019-12-11 20:14:23 +0100490 string ch_list;
Zesstra@Morgengrauen3b569c82016-07-18 20:22:08 +0200491#if !defined(__TESTMUD__) && MUDNAME=="MorgenGrauen"
Arathorn78c08372019-12-11 20:14:23 +0100492 ch_list = read_file(object_name(this_object()) + ".init");
Zesstra@Morgengrauen3b569c82016-07-18 20:22:08 +0200493#else
Arathorn78c08372019-12-11 20:14:23 +0100494 ch_list = read_file(object_name(this_object()) + ".init.testmud");
Zesstra@Morgengrauen3b569c82016-07-18 20:22:08 +0200495#endif
Arathorn19459eb2019-11-30 00:45:51 +0100496
Arathorn78c08372019-12-11 20:14:23 +0100497 if (!stringp(ch_list))
Zesstra@Morgengrauen2b229372016-07-20 23:59:54 +0200498 return;
Arathorn19459eb2019-11-30 00:45:51 +0100499
Arathorn78c08372019-12-11 20:14:23 +0100500 // Channeldatensaetze erzeugen, dazu zuerst Datenfile in Zeilen zerlegen
501 // "Allgemein: 0: 0: 0:Allgemeine Unterhaltungsebene"
502 // Danach drueberlaufen und in Einzelfelder splitten, dabei gleich die
503 // Trennzeichen (Doppelpunkt, Tab und Space) rausfiltern.
504 foreach(string ch : old_explode(ch_list, "\n"))
505 {
506 if (ch[0]=='#')
507 continue;
508 setup( regexplode(ch, ":[ \t]*", RE_OMIT_DELIM) );
509 }
MG Mud User88f12472016-06-24 23:31:02 +0200510}
511
Arathorn78c08372019-12-11 20:14:23 +0100512// BEGIN OF THE CHANNEL MASTER IMPLEMENTATION
Zesstra@Morgengrauen2b229372016-07-20 23:59:54 +0200513protected void create()
MG Mud User88f12472016-06-24 23:31:02 +0200514{
515 seteuid(getuid());
516 restore_object(CHANNEL_SAVE);
Arathorn19459eb2019-11-30 00:45:51 +0100517
Zesstra26aaf1a2020-08-07 19:10:39 +0200518 //TODO: weitere Mappings im MEMORY speichern, Savefile ersetzen.
519
Arathorn19459eb2019-11-30 00:45:51 +0100520 /* Die Channel-History wird nicht nur lokal sondern auch noch im Memory
521 gespeichert, dadurch bleibt sie auch ueber ein Reload erhalten.
MG Mud User88f12472016-06-24 23:31:02 +0200522 Der folgende Code versucht, den Zeiger aus dem Memory zu holen. Falls
523 das nicht moeglich ist, wird ein neuer erzeugt und gegebenenfalls im
524 Memory abgelegt. */
525
526 // Hab ich die noetigen Rechte im Memory?
Arathorn19459eb2019-11-30 00:45:51 +0100527 if (call_other(MEMORY, "HaveRights"))
528 {
MG Mud User88f12472016-06-24 23:31:02 +0200529 // Objektpointer laden
Dominik Schaeferfa564d52020-08-05 20:50:27 +0200530 channelH = ({mapping}) call_other(MEMORY, "Load", "History");
MG Mud User88f12472016-06-24 23:31:02 +0200531
532 // Wenns nich geklappt hat, hat der Memory noch keinen Zeiger, dann
Arathorn78c08372019-12-11 20:14:23 +0100533 if (!mappingp(channelH))
534 {
MG Mud User88f12472016-06-24 23:31:02 +0200535 // Zeiger erzeugen
536 channelH = ([]);
537 // und in den Memory schreiben
Arathorn19459eb2019-11-30 00:45:51 +0100538 call_other(MEMORY, "Save", "History", channelH);
MG Mud User88f12472016-06-24 23:31:02 +0200539 }
Arathorn19459eb2019-11-30 00:45:51 +0100540 }
541 else
542 {
MG Mud User88f12472016-06-24 23:31:02 +0200543 // Keine Rechte im Memory, dann wird mit einem lokalen Zeiger gearbeitet.
544 channelH = ([]);
545 }
546
547 stats = (["time": time(),
Arathorn78c08372019-12-11 20:14:23 +0100548 "boot": capitalize(getuid(previous_object()) || "<Unbekannt>")]);
549
550 // <MasteR>-Ebene erstellen. Channeld wird Ebenenbesitzer und somit auch
551 // Zuhoerer, damit er auf Kommandos auf dieser Ebene reagieren kann.
552 new(CMNAME, this_object(), "Zentrale Informationen zu den Ebenen");
553
MG Mud User88f12472016-06-24 23:31:02 +0200554 initialize();
Arathorn78c08372019-12-11 20:14:23 +0100555 users()->RegisterChannels();
556
557 // Die Zugriffskontrolle auf die Ebenen wird von der Funktion access()
558 // erledigt. Weil sowohl externe Aufrufe aus dem Spielerobjekt, als auch
559 // interne Aufrufe aus diesem Objekt vorkommen koennen, wird hier ein
560 // explizites call_other() auf this_object() gemacht, damit der
561 // Caller-Stack bei dem internen Aufruf denselben Aufbau hat wie bei
562 // einem externen.
MG Mud User88f12472016-06-24 23:31:02 +0200563 this_object()->send(CMNAME, this_object(),
Arathorn19459eb2019-11-30 00:45:51 +0100564 sprintf("%d Ebenen mit %d Teilnehmern initialisiert.",
565 sizeof(channels),
Arathorn78c08372019-12-11 20:14:23 +0100566 CountUsers()));
MG Mud User88f12472016-06-24 23:31:02 +0200567}
568
Arathorn78c08372019-12-11 20:14:23 +0100569varargs void reset()
MG Mud User88f12472016-06-24 23:31:02 +0200570{
Zesstra26aaf1a2020-08-07 19:10:39 +0200571 //TODO reset nur 1-2mal am Tag mit etwas random.
572
573 // Cache bereinigen entsprechend dessen Timeout-Zeit (12 h)
574 // TODO: Zeit auf 2-3 Tage erhoehen.
575 // TODO 2: Zeit dynamisch machen und nur expiren, wenn mehr als n Eintraege.
576 // Zeit reduzieren, bis nur noch n/2 Eintraege verbleiben.
Zesstra8f5102c2020-08-08 12:51:52 +0200577 channelC = filter(channelC,
578 function int (string ch_name, <string|int>* data)
579 {
580 if (channelC[ch_name][2] + 43200 > time())
581 return 1;
582 // Ebenendaten koennen weg, inkl. History, die also auch loeschen
583 m_delete(channelH, ch_name);
584 return 0;
585 });
Arathorn19459eb2019-11-30 00:45:51 +0100586
MG Mud User88f12472016-06-24 23:31:02 +0200587 if (save_me_soon)
588 {
Arathorn19459eb2019-11-30 00:45:51 +0100589 save_me_soon = 0;
MG Mud User88f12472016-06-24 23:31:02 +0200590 save_object(CHANNEL_SAVE);
591 }
592}
593
594// name() - define the name of this object.
Arathorn19459eb2019-11-30 00:45:51 +0100595string name()
596{
597 return CMNAME;
598}
599
600string Name()
601{
602 return CMNAME;
603}
MG Mud User88f12472016-06-24 23:31:02 +0200604
Zesstra28986e12020-08-09 12:44:26 +0200605// Low-level function for adding members without access checks
606private int add_member(string ch, object m)
607{
608 if (IsChannelMember(ch, m))
609 return E_ALREADY_JOINED;
610
611 channels[ch][I_MEMBER] += ({ m });
612 return 0;
613}
614
Zesstra5b7f2fc2020-08-10 02:09:13 +0200615
Arathorn78c08372019-12-11 20:14:23 +0100616#define CHAN_NAME(x) channels[x][I_NAME]
Zesstrae19391f2020-08-09 13:40:12 +0200617#define SVISOR_OB(x) channels[x][I_SUPERVISOR]
Arathorn78c08372019-12-11 20:14:23 +0100618#define ACC_CLOSURE(x) channels[x][I_ACCESS]
619
Zesstra5b7f2fc2020-08-10 02:09:13 +0200620// Aendert das Supervisor-Objekt einer Ebene, ggf. mit Meldung.
621// Wenn kein neuer SV angegeben, wird der aelteste Zuhoerer gewaehlt.
622private int change_sv_object(string ch, object old_sv, object new_sv)
623{
624 if (!new_sv)
625 {
626 channels[ch][I_MEMBER] -= ({0});
627 if (sizeof(channels[ch][I_MEMBER]))
628 new_sv = channels[ch][I_MEMBER][0];
629 else
630 return 0; // kein neuer SV moeglich.
631 }
632 SVISOR_OB(ch) = new_sv;
633 if (old_sv && new_sv
634 && !old_sv->QueryProp(P_INVIS)
635 && !new_sv->QueryProp(P_INVIS))
636 {
637 // Die Zugriffskontrolle auf die Ebenen wird von der Funktion access()
638 // erledigt. Weil sowohl externe Aufrufe aus dem Spielerobjekt, als auch
639 // interne Aufrufe aus diesem Objekt vorkommen koennen, wird hier ein
640 // explizites call_other() auf this_object() gemacht, damit der
641 // Caller-Stack bei dem internen Aufruf denselben Aufbau hat wie bei
642 // einem externen.
643 this_object()->send(ch, old_sv,
644 sprintf("uebergibt die Ebene an %s.",new_sv->name(WEN)),
645 MSG_EMOTE);
646 }
647 else if (old_svn && !old_sv->QueryProp(P_INVIS))
648 {
649 this_object()->send(ch, old_sv,
650 "uebergibt die Ebene an jemand anderen.", MSG_EMOTE);
651 }
652 else if (new_sv && !new_sv->QueryProp(P_INVIS))
653 {
654 this_object()->send(ch, new_sv,
655 "uebernimmt die Ebene von jemand anderem.", MSG_EMOTE);
656 }
657 return 1;
658}
659
Zesstra56692c72020-08-09 13:03:10 +0200660// Stellt sicher, dass einen Ebenen-Supervisor gibt. Wenn dies nicht moeglich
661// ist (z.b. leere Ebene), dann wird die Ebene geloescht und 0
662// zurueckgegeben. Allerdings kann nach dieser Funktion sehr wohl die I_ACCESS
663// closure 0 sein, wenn der SV keine oeffentliche definiert! In diesem Fall
664// wird access() den Zugriff immer erlauben.
Zesstra78310012020-08-09 12:21:48 +0200665private int assert_supervisor(string ch)
MG Mud User88f12472016-06-24 23:31:02 +0200666{
Arathorn78c08372019-12-11 20:14:23 +0100667 // Es ist keine Closure vorhanden, d.h. der Ebenenbesitzer wurde zerstoert.
668 if (!closurep(ACC_CLOSURE(ch)))
MG Mud User88f12472016-06-24 23:31:02 +0200669 {
Arathorn78c08372019-12-11 20:14:23 +0100670 // Wenn der Ebenenbesitzer als String eingetragen ist, versuchen wir,
671 // die Closure wiederherzustellen. Dabei wird das Objekt gleichzeitig
Zesstrae19391f2020-08-09 13:40:12 +0200672 // neugeladen und eingetragen.
673 if (stringp(SVISOR_OB(ch)))
MG Mud User88f12472016-06-24 23:31:02 +0200674 {
Arathorn78c08372019-12-11 20:14:23 +0100675 closure new_acc_cl;
676 string err = catch(new_acc_cl=
Zesstrae19391f2020-08-09 13:40:12 +0200677 symbol_function("check_ch_access", SVISOR_OB(ch));
Arathorn78c08372019-12-11 20:14:23 +0100678 publish);
Zesstra56692c72020-08-09 13:03:10 +0200679 /* Wenn das SV-Objekt neu geladen werden konnte, wird es als Mitglied
680 * eingetragen. Auch die Closure wird neu eingetragen, allerdings kann
681 * sie 0 sein, wenn das SV-Objekt keine oeffentliche check_ch_access()
682 * mehr definiert. In diesem Fall gibt es zwar ein SV-Objekt, aber keine
683 * Zugriffrechte(pruefung) mehr. */
Arathorn78c08372019-12-11 20:14:23 +0100684 if (!err)
685 {
686 ACC_CLOSURE(ch) = new_acc_cl;
Zesstra28986e12020-08-09 12:44:26 +0200687 // Der neue Ebenenbesitzer tritt auch gleich der Ebene bei. Hierbei
688 // erfolgt keine Pruefung des Zugriffsrechtes (ist ja unsinnig, weil
689 // er sich ja selber genehmigen koennte), und auch um eine Rekursion
690 // zu vermeiden.
Zesstrae19391f2020-08-09 13:40:12 +0200691 add_member(ch, find_object(SVISOR_OB(ch)));
Zesstra56692c72020-08-09 13:03:10 +0200692 // Rueckgabewert ist 1, ein neues SV-Objekt ist eingetragen.
Arathorn78c08372019-12-11 20:14:23 +0100693 }
694 else
695 {
Zesstracc2de822020-08-09 12:30:16 +0200696 log_file("CHANNEL", sprintf("[%s] Channel deleted. %O -> %O\n",
Zesstrae19391f2020-08-09 13:40:12 +0200697 dtime(time()), SVISOR_OB(ch), err));
Arathorn78c08372019-12-11 20:14:23 +0100698 m_delete(channels, ch);
699 return 0;
700 }
MG Mud User88f12472016-06-24 23:31:02 +0200701 }
Zesstra56692c72020-08-09 13:03:10 +0200702 else
703 {
704 // In diesem Fall muss ein neues SV-Objekt gesucht und ggf. eingetragen
705 // werden. Wir nehmen das aelteste Mitglied der Ebene.
706 // TODO: kaputte Objekte raussortieren, neuen Master bestimmen, wenn
707 // dieser nicht mehr existiert.
Zesstrae19391f2020-08-09 13:40:12 +0200708
Zesstra56692c72020-08-09 13:03:10 +0200709 }
MG Mud User88f12472016-06-24 23:31:02 +0200710 }
Zesstra78310012020-08-09 12:21:48 +0200711 return 1;
712}
713
714// access() - check access by looking for the right argument types and
715// calling access closures respectively
716// SEE: new, join, leave, send, list, users
717// Note: <pl> is usually an object, only the master supplies a string during
718// runtime error handling.
719// Wertebereich: 0 fuer Zugriff verweigert, 1 fuer Zugriff erlaubt, 2 fuer
720// Zugriff erlaubt fuer privilegierte Objekte, die senden duerfen ohne
721// Zuhoerer zu sein. (Die Aufrufer akzeptieren aber auch alle negativen Werte
722// als Erfolg und alles ueber >2 als privilegiert.)
723varargs private int access(string ch, object|string pl, string cmd,
724 string txt)
725{
726 if (!sizeof(ch))
727 return 0;
728
729 ch = lower_case(ch);
730 if(!pointerp(channels[ch]))
731 return 0;
732
Zesstrafbfe6362020-08-09 13:30:21 +0200733 // Dieses Objekt und Root-Objekte duerfen auf der Ebene senden, ohne
734 // Mitglied zu sein. Das ist die Folge der zurueckgegebenen 2.
Zesstra78310012020-08-09 12:21:48 +0200735 if ( !previous_object(1) || !extern_call() ||
736 previous_object(1) == this_object() ||
Zesstrafbfe6362020-08-09 13:30:21 +0200737 getuid(previous_object(1)) == ROOTID)
Zesstra78310012020-08-09 12:21:48 +0200738 return 2;
Arathorn739a4fa2020-08-06 21:52:58 +0200739
Zesstra56692c72020-08-09 13:03:10 +0200740 // Nur dieses Objekt darf Meldungen im Namen anderer Objekte faken,
741 // ansonsten muss <pl> der Aufrufer sein.
Arathorn739a4fa2020-08-06 21:52:58 +0200742 if (!objectp(pl) ||
743 ((previous_object(1) != pl) && (previous_object(1) != this_object())))
744 return 0;
745
746 if (IsBanned(pl, cmd))
747 return 0;
748
Zesstra56692c72020-08-09 13:03:10 +0200749 // Wenn kein SV-Objekt mehr existiert und kein neues bestimmt werden konnte,
750 // wurde die Ebene ausfgeloest. In diesem Fall auch den Zugriff verweigern.
Zesstra78310012020-08-09 12:21:48 +0200751 if (!assert_supervisor(ch))
Zesstra56692c72020-08-09 13:03:10 +0200752 return 0;
753 // Wenn closure jetzt dennoch 0, wird der Zugriff erlaubt.
754 if (!ACC_CLOSURE(ch))
Arathorn739a4fa2020-08-06 21:52:58 +0200755 return 1;
756
Zesstra6fe46cd2020-08-09 13:12:15 +0200757 // Das SV-Objekt wird gefragt, ob der Zugriff erlaubt ist. Dieses erfolgt
758 // fuer EM+ aber nur, wenn der CHANNELD selber das SV-Objekt ist, damit
759 // nicht beliebige SV-Objekt EMs den Zugriff verweigern koennen. Ebenen mit
760 // CHANNELD als SV koennen aber natuerlich auch EM+ Zugriff verweigern.
761 if (IS_ARCH(previous_object(1))
Zesstrae19391f2020-08-09 13:40:12 +0200762 && find_object(SVISOR_OB(ch)) != this_object())
Zesstra6fe46cd2020-08-09 13:12:15 +0200763 return 1;
764
Arathorn739a4fa2020-08-06 21:52:58 +0200765 return funcall(ACC_CLOSURE(ch), ch, pl, cmd, &txt);
MG Mud User88f12472016-06-24 23:31:02 +0200766}
767
Arathorn78c08372019-12-11 20:14:23 +0100768// Neue Ebene <ch> erstellen mit <owner> als Ebenenbesitzer.
769// <info> kann die statische Beschreibung der Ebene sein oder eine Closure,
770// die dynamisch aktualisierte Infos ausgibt.
771// Das Objekt <owner> kann eine Funktion check_ch_access() definieren, die
772// gerufen wird, wenn eine Ebenenaktion vom Typ join/leave/send/list/users
773// eingeht.
774// check_ch_access() dient der Zugriffskontrolle und entscheidet, ob die
775// Nachricht gesendet werden darf oder nicht.
776
777// Ist keine Closure angegeben, wird die in diesem Objekt (Channeld)
778// definierte Funktion gleichen Namens verwendet.
MG Mud User88f12472016-06-24 23:31:02 +0200779#define IGNORE "^/xx"
780
Arathorn78c08372019-12-11 20:14:23 +0100781// TODO: KOMMENTAR
782//check may contain a closure
783// called when a join/leave/send/list/users message is received
784public varargs int new(string ch_name, object owner, string|closure info)
MG Mud User88f12472016-06-24 23:31:02 +0200785{
Arathorn78c08372019-12-11 20:14:23 +0100786 // Kein Channelmaster angegeben, oder wir sind es selbst, aber der Aufruf
787 // kam von ausserhalb. (Nur der channeld selbst darf sich als Channelmaster
788 // fuer eine neue Ebene eintragen.)
789 if (!objectp(owner) || (owner == this_object() && extern_call()) )
MG Mud User88f12472016-06-24 23:31:02 +0200790 return E_ACCESS_DENIED;
791
Arathorn78c08372019-12-11 20:14:23 +0100792 // Kein gescheiter Channelname angegeben.
793 if (!stringp(ch_name) || !sizeof(ch_name))
794 return E_ACCESS_DENIED;
795
796 // Channel schon vorhanden oder schon alle Channel-Slots belegt.
797 if (channels[lower_case(ch_name)] || sizeof(channels) >= MAX_CHANNELS)
798 return E_ACCESS_DENIED;
799
800 // Der angegebene Ebenenbesitzer darf keine Ebenen erstellen, wenn fuer ihn
801 // ein Bann auf die Aktion C_NEW besteht, oder das Ignore-Pattern auf
802 // seinen Objektnamen matcht.
803 if (IsBanned(owner,C_NEW) || regmatch(object_name(owner), IGNORE))
804 return E_ACCESS_DENIED;
805
806 // Keine Infos mitgeliefert? Dann holen wir sie aus dem Cache.
Arathorn19459eb2019-11-30 00:45:51 +0100807 if (!info)
808 {
Arathorn78c08372019-12-11 20:14:23 +0100809 if (channelC[lower_case(ch_name)])
Arathorn19459eb2019-11-30 00:45:51 +0100810 {
Arathorn78c08372019-12-11 20:14:23 +0100811 ch_name = channelC[lower_case(ch_name)][0];
812 info = channelC[lower_case(ch_name)][1];
MG Mud User88f12472016-06-24 23:31:02 +0200813 }
Arathorn19459eb2019-11-30 00:45:51 +0100814 else
815 {
816 return E_ACCESS_DENIED;
817 }
MG Mud User88f12472016-06-24 23:31:02 +0200818 }
Arathorn19459eb2019-11-30 00:45:51 +0100819 else
820 {
Arathorn78c08372019-12-11 20:14:23 +0100821 channelC[lower_case(ch_name)] = ({ ch_name, info, time() });
Arathorn19459eb2019-11-30 00:45:51 +0100822 }
MG Mud User88f12472016-06-24 23:31:02 +0200823
Arathorn78c08372019-12-11 20:14:23 +0100824 object* pls = ({ owner });
825 m_add(channels, lower_case(ch_name),
826 ({ pls,
827 symbol_function("check_ch_access", owner) || #'check_ch_access,
828 info,
829 (!living(owner) && !clonep(owner) && owner != this_object()
830 ? object_name(owner)
831 : owner),
832 ch_name }));
MG Mud User88f12472016-06-24 23:31:02 +0200833
Arathorn78c08372019-12-11 20:14:23 +0100834 // History fuer eine Ebene nur dann initialisieren, wenn es sie noch
835 // nicht gibt.
836 if (!pointerp(channelH[lower_case(ch_name)]))
837 channelH[lower_case(ch_name)] = ({});
MG Mud User88f12472016-06-24 23:31:02 +0200838
Arathorn78c08372019-12-11 20:14:23 +0100839 // Erstellen neuer Ebenen loggen, wenn wir nicht selbst der Ersteller sind.
840 if (owner != this_object())
MG Mud User88f12472016-06-24 23:31:02 +0200841 log_file("CHANNEL.new", sprintf("[%s] %O: %O %O\n",
Arathorn78c08372019-12-11 20:14:23 +0100842 dtime(time()), ch_name, owner, info));
Arathorn19459eb2019-11-30 00:45:51 +0100843
Arathorn78c08372019-12-11 20:14:23 +0100844 // Erfolgsmeldung ausgeben, ausser bei unsichtbarem Ebenenbesitzer.
845 if (!owner->QueryProp(P_INVIS))
846 {
847 // Die Zugriffskontrolle auf die Ebenen wird von der Funktion access()
848 // erledigt. Weil sowohl externe Aufrufe aus dem Spielerobjekt, als auch
849 // interne Aufrufe aus diesem Objekt vorkommen koennen, wird hier ein
850 // explizites call_other() auf this_object() gemacht, damit der
851 // Caller-Stack bei dem internen Aufruf denselben Aufbau hat wie bei
852 // einem externen.
853 this_object()->send(CMNAME, owner,
854 "laesst die Ebene '" + ch_name + "' entstehen.", MSG_EMOTE);
855 }
Arathorn19459eb2019-11-30 00:45:51 +0100856
MG Mud User88f12472016-06-24 23:31:02 +0200857 stats["new"]++;
Arathorn19459eb2019-11-30 00:45:51 +0100858 save_me_soon = 1;
859 return (0);
MG Mud User88f12472016-06-24 23:31:02 +0200860}
861
Arathorn78c08372019-12-11 20:14:23 +0100862// Objekt <pl> betritt Ebene <ch>. Dies wird zugelassen, wenn <pl> die
863// Berechtigung hat und noch nicht Mitglied ist. (Man kann einer Ebene nicht
864// zweimal beitreten.)
865public int join(string ch, object pl)
MG Mud User88f12472016-06-24 23:31:02 +0200866{
Arathorn78c08372019-12-11 20:14:23 +0100867 ch = lower_case(ch);
Arathorn739a4fa2020-08-06 21:52:58 +0200868 /* funcall() auf Closure-Operator, um einen neuen Eintrag im Caller Stack
869 zu erzeugen, weil access() mit extern_call() und previous_object()
870 arbeitet und sichergestellt sein muss, dass das in jedem Fall das
871 richtige ist. */
872 if (!funcall(#'access, ch, pl, C_JOIN))
Arathorn19459eb2019-11-30 00:45:51 +0100873 return E_ACCESS_DENIED;
874
Zesstra28986e12020-08-09 12:44:26 +0200875 return add_member(ch, pl);
MG Mud User88f12472016-06-24 23:31:02 +0200876}
877
Arathorn78c08372019-12-11 20:14:23 +0100878// Objekt <pl> verlaesst Ebene <ch>.
879// Zugriffsrechte werden nur der Vollstaendigkeit halber geprueft; es duerfte
880// normalerweise keinen Grund geben, das Verlassen einer Ebene zu verbieten.
881// Dies ist in check_ch_access() so geregelt, allerdings koennte dem Objekt
882// <pl> das Verlassen auf Grund eines Banns verboten sein.
883// Wenn kein Spieler mehr auf der Ebene ist, loest sie sich auf, sofern nicht
884// noch ein Ebenenbesitzer eingetragen ist.
885public int leave(string ch, object pl)
MG Mud User88f12472016-06-24 23:31:02 +0200886{
Arathorn78c08372019-12-11 20:14:23 +0100887 ch = lower_case(ch);
Zesstra877cb0a2020-08-10 02:10:21 +0200888
889 channels[ch][I_MEMBER] -= ({0}); // kaputte Objekte erstmal raus
890
891 if (!IsChannelMember(ch, pl))
892 return E_NOT_MEMBER;
893
Arathorn739a4fa2020-08-06 21:52:58 +0200894 /* funcall() auf Closure-Operator, um einen neuen Eintrag im Caller Stack
895 zu erzeugen, weil access() mit extern_call() und previous_object()
896 arbeitet und sichergestellt sein muss, dass das in jedem Fall das
897 richtige ist. */
898 if (!funcall(#'access, ch, pl, C_LEAVE))
Arathorn19459eb2019-11-30 00:45:51 +0100899 return E_ACCESS_DENIED;
900
Zesstrae6d33852020-08-09 14:37:53 +0200901 // Erstmal den Zuhoerer raus.
902 channels[ch][I_MEMBER] -= ({pl});
903
Zesstra5b7f2fc2020-08-10 02:09:13 +0200904 // Wenn auf der Ebene jetzt noch Objekte zuhoeren, muss ggf. der SV
905 // wechseln.
906 if (sizeof(channels[ch][I_MEMBER]))
MG Mud User88f12472016-06-24 23:31:02 +0200907 {
Zesstra5b7f2fc2020-08-10 02:09:13 +0200908 // Kontrolle an jemand anderen uebergeben, wenn der Ebenensupervisor
909 // diese verlassen hat. change_sv_object() waehlt per Default den
910 // aeltesten Zuhoerer.
911 if (pl == channels[ch][I_SUPERVISOR])
Arathorn78c08372019-12-11 20:14:23 +0100912 {
Zesstra5b7f2fc2020-08-10 02:09:13 +0200913 change_sv_object(ch, pl, 0);
Arathorn78c08372019-12-11 20:14:23 +0100914 }
MG Mud User88f12472016-06-24 23:31:02 +0200915 }
Zesstra137ea1c2020-08-10 02:15:20 +0200916 // ansonsten Ebene loeschen, wenn keiner zuhoert.
917 // Kommentar: Supervisoren sind auch Zuhoerer auf der Ebene. Wenn keine
918 // Zuhoerer mehr, folglich auch kein Supervisor mehr da.
919 else
MG Mud User88f12472016-06-24 23:31:02 +0200920 {
Arathorn78c08372019-12-11 20:14:23 +0100921 // Der Letzte macht das Licht aus, aber nur, wenn er nicht unsichtbar ist.
Zesstra137ea1c2020-08-10 02:15:20 +0200922 // Wenn Spieler, NPC, Clone oder Channeld als letztes die Ebene verlassen,
923 // wird diese zerstoert, mit Meldung.
Arathorn19459eb2019-11-30 00:45:51 +0100924 if (!pl->QueryProp(P_INVIS))
Arathorn78c08372019-12-11 20:14:23 +0100925 {
926 // Die Zugriffskontrolle auf die Ebenen wird von der Funktion access()
927 // erledigt. Weil sowohl externe Aufrufe aus dem Spielerobjekt, als auch
928 // interne Aufrufe aus diesem Objekt vorkommen koennen, wird hier ein
929 // explizites call_other() auf this_object() gemacht, damit der
930 // Caller-Stack bei dem internen Aufruf denselben Aufbau hat wie bei
931 // einem externen.
932 this_object()->send(CMNAME, pl,
933 "verlaesst als "+
934 (pl->QueryProp(P_GENDER) == 1 ? "Letzter" : "Letzte")+
935 " die Ebene '"+channels[ch][I_NAME]+"', worauf diese sich in "
936 "einem Blitz oktarinen Lichts aufloest.", MSG_EMOTE);
937 }
Zesstra137ea1c2020-08-10 02:15:20 +0200938 // Einige Daten merken, damit sie reaktiviert werden kann, wenn jemand
939 // einloggt, der die Ebene abonniert hat.
Arathorn19459eb2019-11-30 00:45:51 +0100940 channelC[lower_case(ch)] =
941 ({ channels[ch][I_NAME], channels[ch][I_INFO], time() });
Arathorn78c08372019-12-11 20:14:23 +0100942
Zesstra137ea1c2020-08-10 02:15:20 +0200943 // Ebene loeschen bzw. deaktivieren.
MG Mud User88f12472016-06-24 23:31:02 +0200944 m_delete(channels, lower_case(ch));
Zesstra8f5102c2020-08-08 12:51:52 +0200945 // History wird nicht geloescht, damit sie noch verfuegbar ist, wenn die
946 // Ebene spaeter nochmal neu erstellt wird. Sie wird dann bereinigt, wenn
947 // channelC bereinigt wird.
Arathorn78c08372019-12-11 20:14:23 +0100948
MG Mud User88f12472016-06-24 23:31:02 +0200949 stats["dispose"]++;
Arathorn19459eb2019-11-30 00:45:51 +0100950 save_me_soon = 1;
MG Mud User88f12472016-06-24 23:31:02 +0200951 }
Arathorn19459eb2019-11-30 00:45:51 +0100952 return (0);
MG Mud User88f12472016-06-24 23:31:02 +0200953}
954
Arathorn78c08372019-12-11 20:14:23 +0100955// Nachricht <msg> vom Typ <type> mit Absender <pl> auf der Ebene <ch> posten,
956// sofern <pl> dort senden darf.
957public varargs int send(string ch, object pl, string msg, int type)
MG Mud User88f12472016-06-24 23:31:02 +0200958{
Arathorn78c08372019-12-11 20:14:23 +0100959 ch = lower_case(ch);
Arathorn739a4fa2020-08-06 21:52:58 +0200960 /* funcall() auf Closure-Operator, um einen neuen Eintrag im Caller Stack
961 zu erzeugen, weil access() mit extern_call() und previous_object()
962 arbeitet und sichergestellt sein muss, dass das in jedem Fall das
963 richtige ist. */
964 int a = funcall(#'access, ch, pl, C_SEND, msg);
Arathorn78c08372019-12-11 20:14:23 +0100965 if (!a)
Arathorn19459eb2019-11-30 00:45:51 +0100966 return E_ACCESS_DENIED;
967
Zesstra26aaf1a2020-08-07 19:10:39 +0200968 // a<2 bedeutet effektiv a==1 (weil a==0 oben rausfaellt), was dem
969 // Rueckgabewert von check_ch_access() entspricht, wenn die Aktion zugelassen
970 // wird. access() allerdings 2 fuer "privilegierte" Objekte (z.B.
971 // ROOT-Objekte oder den channeld selber). Der Effekt ist, dass diese
972 // Objekte auf Ebenen senden duerfen, auf denen sie nicht zuhoeren.
Arathorn78c08372019-12-11 20:14:23 +0100973 if (a < 2 && !IsChannelMember(ch, pl))
Arathorn19459eb2019-11-30 00:45:51 +0100974 return E_NOT_MEMBER;
975
976 if (!msg || !stringp(msg) || !sizeof(msg))
977 return E_EMPTY_MESSAGE;
978
Arathorn78c08372019-12-11 20:14:23 +0100979 // Jedem Mitglied der Ebene wird die Nachricht ueber die Funktion
980 // ChannelMessage() zugestellt. Der Channeld selbst hat ebenfalls eine
981 // Funktion dieses Namens, so dass er, falls er Mitglied der Ebene ist, die
982 // Nachricht ebenfalls erhaelt.
983 // Um die Kommandos der Ebene <MasteR> verarbeiten zu koennen, muss er
984 // demzufolge Mitglied dieser Ebene sein. Da Ebenenbesitzer automatisch
985 // auch Mitglied sind, wird die Ebene <MasteR> im create() mittels new()
986 // erzeugt und der Channeld als Besitzer angegeben.
987 // Die Aufrufkette ist dann wie folgt:
988 // Eingabe "-< xyz" => pl::ChannelParser() => send() => ChannelMessage()
989 channels[ch][I_MEMBER]->ChannelMessage(
990 ({ channels[ch][I_NAME], pl, msg, type}));
Arathorn19459eb2019-11-30 00:45:51 +0100991
992 if (sizeof(channelH[ch]) > MAX_HIST_SIZE)
MG Mud User88f12472016-06-24 23:31:02 +0200993 channelH[ch] = channelH[ch][1..];
Arathorn19459eb2019-11-30 00:45:51 +0100994
995 channelH[ch] +=
996 ({ ({ channels[ch][I_NAME],
997 (stringp(pl)
998 ? pl
999 : (pl->QueryProp(P_INVIS)
1000 ? "/(" + capitalize(getuid(pl)) + ")$"
1001 : "")
1002 + (pl->Name(WER, 2) || "<Unbekannt>")),
1003 msg + " <" + strftime("%a, %H:%M:%S") + ">\n",
1004 type }) });
Arathorn78c08372019-12-11 20:14:23 +01001005 return (0);
MG Mud User88f12472016-06-24 23:31:02 +02001006}
1007
Arathorn78c08372019-12-11 20:14:23 +01001008// Gibt ein Mapping mit allen Ebenen aus, die das Objekt <pl> lesen kann,
1009// oder einen Integer-Fehlercode
1010public int|mapping list(object pl)
MG Mud User88f12472016-06-24 23:31:02 +02001011{
Arathorn78c08372019-12-11 20:14:23 +01001012 mapping chs = ([]);
1013 foreach(string chname, <object*|closure|string|object>* chdata : channels)
1014 {
Arathorn739a4fa2020-08-06 21:52:58 +02001015 /* funcall() auf Closure-Operator, um einen neuen Eintrag im Caller Stack
1016 zu erzeugen, weil access() mit extern_call() und previous_object()
1017 arbeitet und sichergestellt sein muss, dass das in jedem Fall das
1018 richtige ist. */
1019 if(funcall(#'access, chname, pl, C_LIST))
Arathorn78c08372019-12-11 20:14:23 +01001020 {
1021 m_add(chs, chname, chdata);
1022 chs[chname][I_MEMBER] = filter(chs[chname][I_MEMBER], #'objectp);
1023 }
1024 }
Arathorn19459eb2019-11-30 00:45:51 +01001025
1026 if (!sizeof(chs))
1027 return E_ACCESS_DENIED;
Arathorn78c08372019-12-11 20:14:23 +01001028 return (chs);
MG Mud User88f12472016-06-24 23:31:02 +02001029}
1030
Arathorn78c08372019-12-11 20:14:23 +01001031// Ebene suchen, deren Name <ch> enthaelt, und auf der Objekt <pl> senden darf
1032// Rueckgabewerte:
1033// - den gefundenen Namen als String
1034// - String-Array, wenn es mehrere Treffer gibt
1035// - 0, wenn es keinen Treffer gibt
1036public string|string* find(string ch, object pl)
MG Mud User88f12472016-06-24 23:31:02 +02001037{
Arathorn78c08372019-12-11 20:14:23 +01001038 ch = lower_case(ch);
Arathorn19459eb2019-11-30 00:45:51 +01001039
Arathorn78c08372019-12-11 20:14:23 +01001040 // Suchstring <ch> muss Formatanforderung erfuellen;
1041 // TODO: soll das ein Check auf gueltigen Ebenennamen als Input sein?
1042 // Wenn ja, muesste laut Manpage mehr geprueft werden:
1043 // "Gueltige Namen setzen sich zusammen aus den Buchstaben a-z, A-Z sowie
1044 // #$%&@<>-." Es wuerden also $%&@ fehlen.
1045 if (!regmatch(ch, "^[<>a-z0-9#-]+$"))
1046 return 0;
Arathorn19459eb2019-11-30 00:45:51 +01001047
Arathorn78c08372019-12-11 20:14:23 +01001048 // Der Anfang des Ebenennamens muss dem Suchstring entsprechen und das
1049 // Objekt <pl> muss auf dieser Ebene senden duerfen, damit der Ebenenname
1050 // in das Suchergebnis aufgenommen wird.
1051 string* chs = filter(m_indices(channels), function int (string chname) {
Arathorn739a4fa2020-08-06 21:52:58 +02001052 /* funcall() auf Closure-Operator, um einen neuen Eintrag
1053 im Caller Stack zu erzeugen, weil access() mit
1054 extern_call() und previous_object() arbeitet und
1055 sichergestellt sein muss, dass das in jedem Fall das
1056 richtige ist. */
Arathorn78c08372019-12-11 20:14:23 +01001057 return ( stringp(regmatch(chname, "^"+ch)) &&
Arathorn739a4fa2020-08-06 21:52:58 +02001058 funcall(#'access, chname, pl, C_SEND) );
Arathorn78c08372019-12-11 20:14:23 +01001059 });
Arathorn19459eb2019-11-30 00:45:51 +01001060
Arathorn78c08372019-12-11 20:14:23 +01001061 int num_channels = sizeof(chs);
1062 if (num_channels > 1)
1063 return chs;
1064 else if (num_channels == 1)
1065 return channels[chs[0]][I_NAME];
1066 else
1067 return 0;
MG Mud User88f12472016-06-24 23:31:02 +02001068}
1069
Arathorn78c08372019-12-11 20:14:23 +01001070// Ebenen-History abfragen.
1071public int|<int|string>** history(string ch, object pl)
MG Mud User88f12472016-06-24 23:31:02 +02001072{
Arathorn78c08372019-12-11 20:14:23 +01001073 ch = lower_case(ch);
Arathorn739a4fa2020-08-06 21:52:58 +02001074 /* funcall() auf Closure-Operator, um einen neuen Eintrag im Caller Stack
1075 zu erzeugen, weil access() mit extern_call() und previous_object()
1076 arbeitet und sichergestellt sein muss, dass das in jedem Fall das
1077 richtige ist. */
1078 if (!funcall(#'access, ch, pl, C_JOIN))
MG Mud User88f12472016-06-24 23:31:02 +02001079 return E_ACCESS_DENIED;
Arathorn78c08372019-12-11 20:14:23 +01001080 else
1081 return channelH[ch];
MG Mud User88f12472016-06-24 23:31:02 +02001082}
1083
Arathorn78c08372019-12-11 20:14:23 +01001084// Wird aus der Shell gerufen, fuer das Erzmagier-Kommando "kill".
1085public int remove_channel(string ch, object pl)
MG Mud User88f12472016-06-24 23:31:02 +02001086{
Zesstra26aaf1a2020-08-07 19:10:39 +02001087 //TODO: integrieren in access()?
Arathorn19459eb2019-11-30 00:45:51 +01001088 if (previous_object() != this_object())
1089 {
1090 if (!stringp(ch) ||
1091 pl != this_player() || this_player() != this_interactive() ||
1092 this_interactive() != previous_object() ||
1093 !IS_ARCH(this_interactive()))
MG Mud User88f12472016-06-24 23:31:02 +02001094 return E_ACCESS_DENIED;
Arathorn19459eb2019-11-30 00:45:51 +01001095 }
MG Mud User88f12472016-06-24 23:31:02 +02001096
Arathorn78c08372019-12-11 20:14:23 +01001097 if (member(channels, lower_case(ch)))
Arathorn19459eb2019-11-30 00:45:51 +01001098 {
Arathorn78c08372019-12-11 20:14:23 +01001099 // Einer geloeschten Ebene kann man nicht zuhoeren: Ebenenname aus der
1100 // Ebenenliste aller Mitglieder austragen. Dabei werden sowohl ein-, als
1101 // auch temporaer ausgeschaltete Ebenen beruecksichtigt.
1102 foreach(object listener : channels[lower_case(ch)][I_MEMBER])
1103 {
1104 string* pl_chans = listener->QueryProp(P_CHANNELS);
1105 if (pointerp(pl_chans))
Arathorn19459eb2019-11-30 00:45:51 +01001106 {
Arathorn78c08372019-12-11 20:14:23 +01001107 listener->SetProp(P_CHANNELS, pl_chans-({lower_case(ch)}));
1108 }
1109 pl_chans = listener->QueryProp(P_SWAP_CHANNELS);
1110 if (pointerp(pl_chans))
1111 {
1112 listener->SetProp(P_SWAP_CHANNELS, pl_chans-({lower_case(ch)}));
1113 }
1114 }
1115 // Anschliessend werden die Ebenendaten geloescht.
1116 m_delete(channels, lower_case(ch));
MG Mud User88f12472016-06-24 23:31:02 +02001117
Arathorn78c08372019-12-11 20:14:23 +01001118 // Zaehler fuer zerstoerte Ebenen in der Statistik erhoehen.
MG Mud User88f12472016-06-24 23:31:02 +02001119 stats["dispose"]++;
1120 }
Zesstra8f5102c2020-08-08 12:51:52 +02001121 // Dies auuserhalb des Blocks oben ermoeglicht es, inaktive Ebenen bzw.
1122 // deren Daten zu entfernen.
1123 m_delete(channelC, lower_case(ch));
1124 // In diesem Fall der gezielten Loeschung wird auch die History geloescht.
1125 m_delete(channelH, lower_case(ch));
Arathorn19459eb2019-11-30 00:45:51 +01001126
Arathorn19459eb2019-11-30 00:45:51 +01001127 save_me_soon = 1;
1128 return (0);
MG Mud User88f12472016-06-24 23:31:02 +02001129}
1130
Arathorn78c08372019-12-11 20:14:23 +01001131// Wird aus der Shell aufgerufen, fuer das Erzmagier-Kommando "clear".
1132public int clear_history(string ch)
MG Mud User88f12472016-06-24 23:31:02 +02001133{
Zesstra26aaf1a2020-08-07 19:10:39 +02001134 //TODO: mit access() vereinigen?
MG Mud User88f12472016-06-24 23:31:02 +02001135 // Sicherheitsabfragen
Arathorn19459eb2019-11-30 00:45:51 +01001136 if (previous_object() != this_object())
1137 {
1138 if (!stringp(ch) ||
1139 this_player() != this_interactive() ||
1140 this_interactive() != previous_object() ||
1141 !IS_ARCH(this_interactive()))
MG Mud User88f12472016-06-24 23:31:02 +02001142 return E_ACCESS_DENIED;
Arathorn19459eb2019-11-30 00:45:51 +01001143 }
MG Mud User88f12472016-06-24 23:31:02 +02001144
Zesstra26aaf1a2020-08-07 19:10:39 +02001145 // History des Channels loeschen (ohne die ebene als ganzes, daher Key nicht
1146 // aus dem mapping loeschen.)
Arathorn19459eb2019-11-30 00:45:51 +01001147 if (pointerp(channelH[lower_case(ch)]))
1148 channelH[lower_case(ch)] = ({});
MG Mud User88f12472016-06-24 23:31:02 +02001149
1150 return 0;
1151}