blob: 1331d574b132309b471f9718bc9e62c5f033b047 [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>
15
16#include <properties.h>
17#include <config.h>
18#include <language.h>
19
20#define NEED_PROTOTYPES
21#include "channel.h"
22
23#define CMNAME "<MasteR>"
24#define CHANNEL_SAVE "/p/daemon/save/channeld"
25
26#define MEMORY "/secure/memory"
27
28// channels - contains the simple channel information
29// ([ channelname : ({ I_MEMBER, I_ACCESS, I_INFO, I_MASTER })...])
30private nosave mapping lowerch;
31private nosave mapping channels;
32private nosave mapping channelH;
33private nosave mapping stats;
34
35private mapping channelC;
36private mapping channelB;
37
38private int save_me_soon;
39
40// BEGIN OF THE CHANNEL MASTER ADMINISTRATIVE PART
41
42#define RECV 0
43#define SEND 1
44#define FLAG 2
45
46// Channel flags
47// Levelbeschraenkungen gegen Magierlevel (query_wiz_level) pruefen, nicht
48// P_LEVEL.
49#define F_WIZARD 1
50// Keine Gaeste. ;-)
51#define F_NOGUEST 2
52
53private nosave mapping admin = m_allocate(0, 3);
54
55int check(string ch, object pl, string cmd)
56{
57 int level;
58
59 if((admin[ch, FLAG] & F_NOGUEST) && pl->QueryGuest()) return 0;
60
61 if((admin[ch, FLAG] & F_WIZARD) && query_wiz_level(pl) < SEER_LVL) return 0;
62 level = (admin[ch, FLAG] & F_WIZARD
63 ? query_wiz_level(pl)
64 : pl->QueryProp(P_LEVEL));
65
66 switch(cmd)
67 {
68 case C_FIND:
69 case C_LIST:
70 case C_JOIN:
71 if(admin[ch, RECV] == -1) return 0;
72 if(admin[ch, RECV] <= level) return 1;
73 break;
74 case C_SEND:
75 if(admin[ch, SEND] == -1) return 0;
76 if(admin[ch, SEND] <= level) return 1;
77 break;
78 case C_LEAVE:
79 return 1;
80 default: break;
81 }
82 return(0);
83}
84
85private int CountUser(mapping l)
86{
87 mapping n;
88 n = ([]);
89 walk_mapping(l, lambda(({'i/*'*/, 'a/*'*/, 'n/*'*/}),
90 ({#'+=/*'*/, 'n/*'*/,
91 ({#'mkmapping/*'*/,
92 ({#'[/*'*/, 'a/*'*/, 0})})})),
93 &n);
94 return sizeof(n);
95}
96
97private void banned(string n, mixed cmds, string res)
98{
99 res += sprintf("%s [%s], ", capitalize(n), implode(cmds, ","));
100}
101
102private mapping Tcmd = ([]);
103#define TIMEOUT (time() - 60)
104
105void ChannelMessage(mixed msg)
106{
107 string ret, mesg;
108 mixed lag;
109 int max, rekord;
110 string tmp;
111 if(msg[1] == this_object() || !stringp(msg[2]) ||
112 msg[0] != CMNAME || previous_object() != this_object()) return;
113
114
115 mesg = lower_case(msg[2]);
116
117 if(!strstr("hilfe", mesg) && sizeof(mesg) <= 5)
118 ret = "Folgende Kommandos gibt es: hilfe, lag, up[time], statistik, bann";
119 else
120 if(!strstr("lag", mesg) && sizeof(mesg) <= 3)
121 {
122 if(Tcmd["lag"] > TIMEOUT) return;
123 Tcmd["lag"] = time();
124 lag = "/p/daemon/lag-o-daemon"->read_ext_lag_data();
125 ret = sprintf("Lag: %.1f%%/60, %.1f%%/15, %.1f%%/5, %.1f%%/1, "
126 "%.1f%%/20s, %.1f%%/2s",
127 lag[5], lag[4], lag[3], lag[2], lag[1], lag[0]);
128 call_out(#'send/*'*/, 2, CMNAME, this_object(), ret);
129 ret = query_load_average();
130 } else
131 if(!strstr("uptime", mesg) && sizeof(mesg) <= 6)
132 {
133 if(Tcmd["uptime"] > TIMEOUT) return;
134 Tcmd["uptime"] = time();
135 if(file_size("/etc/maxusers") <= 0) {
136 ret = "Diese Information liegt nicht vor.";
137 } else {
138 sscanf(read_file("/etc/maxusers"), "%d %s", max, tmp);
139 sscanf(read_file("/etc/maxusers.ever"), "%d %s", rekord, tmp);
140 ret = sprintf("Das MUD laeuft jetzt %s. Es sind momentan %d Spieler "
141 "eingeloggt; das Maximum lag heute bei %d und der Rekord "
142 "bisher ist %d.", uptime(), sizeof(users()), max, rekord);
143 }
144 } else
145 if(!strstr("statistik", mesg) && sizeof(mesg) <= 9)
146 {
147 if(Tcmd["statistik"] > TIMEOUT) return;
148 Tcmd["statistik"] = time();
149 ret = sprintf(
150 "Im Moment sind insgesamt %d Ebenen mit %d Teilnehmern aktiv.\n"
151 "Der %s wurde das letzte mal am %s von %s neu gestartet.\n"
152 "Seitdem wurden %d Ebenen neu erzeugt und %d zerstoert.\n",
153 sizeof(channels), CountUser(channels), CMNAME,
154 dtime(stats["time"]), stats["boot"], stats["new"], stats["dispose"]);
155 } else
156 if(!strstr(mesg, "bann"))
157 {
158 string pl, cmd;
159 if(mesg == "bann")
160 if(sizeof(channelB))
161 {
162 ret = "";
163 walk_mapping(channelB, #'banned/*'*/, &ret);
164 ret = "Fuer folgende Spieler besteht ein Bann: " + ret;
165 } else ret = "Zur Zeit ist kein Bann aktiv.";
166 else
167 {
168 if(sscanf(mesg, "bann %s %s", pl, cmd) == 2 &&
169 IS_DEPUTY(msg[1]))
170 {
171# define CMDS ({C_FIND, C_LIST, C_JOIN, C_LEAVE, C_SEND, C_NEW})
172 pl = lower_case(pl); cmd = lower_case(cmd);
173 if(member(CMDS, cmd) != -1)
174 {
175 if(!pointerp(channelB[pl]))
176 channelB[pl] = ({});
177 if(member(channelB[pl], cmd) != -1)
178 channelB[pl] -= ({ cmd });
179 else
180 channelB[pl] += ({ cmd });
181 ret = "Fuer '"+capitalize(pl)+"' besteht "
182 + (sizeof(channelB[pl]) ?
183 "folgender Bann: "+implode(channelB[pl], ", ") :
184 "kein Bann mehr.");
185 if(!sizeof(channelB[pl]))
186 channelB = m_copy_delete(channelB, pl);
187 save_object(CHANNEL_SAVE);
188 }
189 else ret = "Das Kommando '"+cmd+"' ist unbekannt. Erlaubte Kommandos: "
190 + implode(CMDS, ", ");
191 }
192 else
193 {
194 if(!IS_ARCH(msg[1])) return;
195 else ret = "Syntax: bann <name> <kommando>";
196 }
197 }
198 }
199 else if(mesg == "lust")
200 {
201 mixed t, up;
202 if(Tcmd["lag"] > TIMEOUT ||
203 Tcmd["statistik"] > TIMEOUT ||
204 Tcmd["uptime"] > TIMEOUT) return;
205 Tcmd["lag"] = time();
206 Tcmd["statistik"] = time();
207 Tcmd["uptime"] = time();
208 lag = "/p/daemon/lag-o-daemon"->read_lag_data();
209
210 sscanf(read_file("/etc/maxusers"), "%d %s", max, tmp);
211 sscanf(read_file("/etc/maxusers.ever"), "%d %s", rekord, tmp);
212
213 t=time()-last_reboot_time();
214 up="";
215 if(t >= 86400)
216 up += sprintf("%dT", t/86400);
217 if(t >= 3600)
218 up += sprintf("%dh", (t=t%86400)/3600);
219 if(t > 60)
220 up += sprintf("%dm", (t=t%3600)/60);
221 up += sprintf("%ds", t%60);
222
223 ret = sprintf("%.1f%%/15 %.1f%%/1 %s %d:%d:%d E:%d T:%d",
224 lag[1], lag[2], up, sizeof(users()), max, rekord,
225 sizeof(channels), CountUser(channels));
226 } else return;
227
228 call_out(#'send/*'*/, 2, CMNAME, this_object(), ret);
229}
230
231// setup() -- set up a channel and register it
232// arguments are stored in the following order:
233// ({ channel name,
234// receive level, send level,
235// flags,
236// description,
237// master obj
238// })
239private void setup(mixed c)
240{
241 closure cl;
242 object m;
243 string d;
244 d = "- Keine Beschreibung -";
245 m = this_object();
246 if(sizeof(c) && sizeof(c[0]) > 1 && c[0][0] == '\\')
247 c[0] = c[0][1..];
248
249 switch(sizeof(c))
250 {
251 case 6:
252 if(!stringp(c[5]) || !sizeof(c[5])
253 || (catch(m=load_object(c[5]);publish) || !objectp(m) ))
254 m = this_object();
255 case 5: d = stringp(c[4]) || closurep(c[4]) ? c[4] : d;
256 case 4: admin[c[0], FLAG] = to_int(c[3]);
257 case 3: admin[c[0], SEND] = to_int(c[2]);
258 case 2: admin[c[0], RECV] = to_int(c[1]);
259 break;
260 case 0:
261 default:
262 return;
263 }
264 switch(new(c[0], m, d))
265 {
266 case E_ACCESS_DENIED:
267 log_file("CHANNEL", sprintf("[%s] %s: %O: error, access denied\n",
268 dtime(time()), c[0], m));
269 break;
270 default:
271 break;
272 }
273 return;
274}
275
276void initialize()
277{
278 mixed tmp;
279 tmp = read_file(object_name(this_object())+".init");
280 tmp = regexp(old_explode(tmp, "\n"), "^[^#]");
281 tmp = map(tmp, #'regexplode/*'*/, "[^:][^:]*$|[ \\t]*:[ \\t]*");
282 tmp = map(tmp, #'regexp/*'*/, "^[^: \\t]");
283 map(tmp, #'setup/*'*/);
284}
285
286// BEGIN OF THE CHANNEL MASTER IMPLEMENTATION
287
288void create()
289{
290 seteuid(getuid());
291 restore_object(CHANNEL_SAVE);
292 if(!channelC) channelC = ([]);
293 if(!channelB) channelB = ([]);
294 channels = ([]);
295
296 /* Die Channel-History wird nicht nur lokal sondern auch noch im Memory
297 gespeichert, dadurch bleibt sie auch ueber ein Reload erhalten.
298 Der folgende Code versucht, den Zeiger aus dem Memory zu holen. Falls
299 das nicht moeglich ist, wird ein neuer erzeugt und gegebenenfalls im
300 Memory abgelegt. */
301
302 // Hab ich die noetigen Rechte im Memory?
303 if (call_other(MEMORY,"HaveRights")) {
304 // Objektpointer laden
305 channelH = (mixed) call_other(MEMORY,"Load","History");
306
307 // Wenns nich geklappt hat, hat der Memory noch keinen Zeiger, dann
308 if (!mappingp(channelH)){
309 // Zeiger erzeugen
310 channelH = ([]);
311 // und in den Memory schreiben
312 call_other(MEMORY,"Save","History",channelH);
313 }
314 } else {
315 // Keine Rechte im Memory, dann wird mit einem lokalen Zeiger gearbeitet.
316 channelH = ([]);
317 }
318
319 stats = (["time": time(),
320 "boot": capitalize(getuid(previous_object())||"<Unbekannt>")]);
321 new(CMNAME, this_object(), "Zentrale Informationen zu den Ebenen");
322 initialize();
323 map_objects(efun::users(), "RegisterChannels");
324 this_object()->send(CMNAME, this_object(),
325 sprintf("%d Ebenen mit %d Teilnehmern initialisiert.",
326 sizeof(channels),
327 CountUser(channels)));
328}
329
330// reset() and cache_to() - Cache Timeout, remove timed out cached channels
331// SEE: new, send
332private int cache_to(string key, mapping m, int t)
333{
334 if(!pointerp(m[key]) || m[key][2] + 43200 > t) return 1;
335 return(0);
336}
337
338varargs void reset(int nonstd)
339{
340 channelC = filter_indices(channelC, #'cache_to/*'*/, channelC, time());
341 if (save_me_soon)
342 {
343 save_me_soon=0;
344 save_object(CHANNEL_SAVE);
345 }
346}
347
348// name() - define the name of this object.
349string name() { return CMNAME; }
350string Name() { return CMNAME; }
351
352// access() - check access by looking for the right argument types and
353// calling access closures respectively
354// SEE: new, join, leave, send, list, users
355// Note: <pl> is usually an object, only the master supplies a string during
356// runtime error handling.
357varargs private int access(mixed ch, mixed pl, string cmd, string txt)
358{
359 mixed co, m;
360
361 if(!stringp(ch) || !sizeof(ch = lower_case(ch)) || !channels[ch])
362 return 0;
363 if(!channels[ch][I_ACCESS]||!previous_object(1)||!extern_call()||
364 previous_object(1)==this_object()||
365 (stringp(channels[ch][I_MASTER])&&
366 previous_object(1)==find_object(channels[ch][I_MASTER]))||
367 getuid(previous_object(1)) == ROOTID)
368 return 2;
369 if(!objectp(pl) ||
370 ((previous_object(1)!=pl) &&(previous_object(1)!=this_object())))
371 return 0;
372 if(pointerp(channelB[getuid(pl)]) &&
373 member(channelB[getuid(pl)], cmd) != -1)
374 return 0;
375 if(stringp(channels[ch][I_MASTER]) &&
376 (!(m = find_object(channels[ch][I_MASTER])) ||
377 (!to_object(channels[ch][I_ACCESS]) ||
378 get_type_info(channels[ch][I_ACCESS])[1])))
379 {
380 string err;
381 if(!objectp(m)) err = catch(load_object(channels[ch][I_MASTER]);publish);
382 if(!err &&
383 ((!to_object(channels[ch][I_ACCESS]) ||
384 get_type_info(channels[ch][I_ACCESS])[1]) &&
385 !closurep(channels[ch][I_ACCESS] =
386 symbol_function("check",
387 find_object(channels[ch][I_MASTER])))))
388 {
389 log_file("CHANNEL", sprintf("[%s] %O -> %O\n",
390 dtime(time()), channels[ch][I_MASTER],
391 err));
392 channels = m_copy_delete(channels, ch);
393 return 0;
394 }
395 this_object()->join(ch, find_object(channels[ch][I_MASTER]));
396 }
397 if(closurep(channels[ch][I_ACCESS]))
398 return funcall(channels[ch][I_ACCESS],
399 channels[ch][I_NAME], pl, cmd, &txt);
400}
401
402// new() - create a new channel
403// a channel with name 'ch' is created, the player is the master
404// info may contain a string which describes the channel or a closure
405// to display up-to-date information, check may contain a closure
406// called when a join/leave/send/list/users message is received
407// SEE: access
408
409#define IGNORE "^/xx"
410
411varargs int new(string ch, object pl, mixed info)
412{
413 mixed pls;
414
415 if(!objectp(pl) || !stringp(ch) || !sizeof(ch) || channels[lower_case(ch)] ||
416 (pl == this_object() && extern_call()) ||
417 sizeof(channels) >= MAX_CHANNELS ||
418 sizeof(regexp(({ object_name(pl) }), IGNORE)) ||
419 (pointerp(channelB[getuid(pl)]) &&
420 member(channelB[getuid(pl)], C_NEW) != -1))
421 return E_ACCESS_DENIED;
422
423 if(!info) {
424 if(channelC[lower_case(ch)]) {
425 ch = channelC[lower_case(ch)][0];
426 info = channelC[lower_case(ch)][1];
427 }
428 else return E_ACCESS_DENIED;
429 }
430 else channelC[lower_case(ch)] = ({ ch, info, time() });
431
432 pls = ({ pl });
433
434 channels[lower_case(ch)] = ({ pls,
435 symbol_function("check", pl) ||
436 #'check/*'*/, info,
437 (!living(pl) &&
438 !clonep(pl) &&
439 pl != this_object()
440 ? object_name(pl)
441 : pl),
442 ch,
443 });
444
445 // ChannelH fuer einen Kanal nur dann initialisieren, wenn es sie noch nich gibt.
446 if ( !pointerp(channelH[lower_case(ch)]) )
447 channelH[lower_case(ch)] = ({});
448
449 if(pl != this_object())
450 log_file("CHANNEL.new", sprintf("[%s] %O: %O %O\n",
451 dtime(time()), ch, pl, info));
452 if(!pl->QueryProp(P_INVIS))
453 this_object()->send(CMNAME, pl,
454 "laesst die Ebene '"+ch+"' entstehen.", MSG_EMOTE);
455 stats["new"]++;
456
457 save_me_soon=1;
458 return(0);
459}
460
461// join() - join a channel
462// this function checks whether the player 'pl' is allowed to join
463// the channel 'ch' and add if successful, one cannot join a channel
464// twice
465// SEE: leave, access
466int join(string ch, object pl)
467{
468 if(!funcall(#'access,&ch, pl, C_JOIN)) return E_ACCESS_DENIED;
469 if(member(channels[ch][I_MEMBER], pl) != -1) return E_ALREADY_JOINED;
470 channels[ch][I_MEMBER] += ({ pl });
471 return(0);
472}
473
474// leave() - leave a channel
475// the access check in this function is just there for completeness
476// one should always be allowed to leave a channel.
477// if there are no players left on the channel it will vanish, unless
478// its master is this object.
479// SEE: join, access
480int leave(string ch, object pl)
481{
482 int pos;
483 if(!funcall(#'access,&ch, pl, C_LEAVE)) return E_ACCESS_DENIED;
484 channels[ch][I_MEMBER] -= ({0}); // kaputte Objekte erstmal raus
485 if((pos = member(channels[ch][I_MEMBER], pl)) == -1) return E_NOT_MEMBER;
486 if(pl == channels[ch][I_MASTER] && sizeof(channels[ch][I_MEMBER]) > 1)
487 {
488 channels[ch][I_MASTER] = channels[ch][I_MEMBER][1];
489 if(!pl->QueryProp(P_INVIS))
490 this_object()->send(ch, pl, "uebergibt die Ebene an "
491 +channels[ch][I_MASTER]->name(WEN)+".", MSG_EMOTE);
492 }
493 channels[ch][I_MEMBER][pos..pos] = ({ });
494
495
496 if(!sizeof(channels[ch][I_MEMBER]) &&
497 !stringp(channels[ch][I_MASTER]))
498 {
499 // delete the channel that has no members
500 if(!pl->QueryProp(P_INVIS))
501 this_object()->send(CMNAME, pl,
502 "verlaesst als "
503 +(pl->QueryProp(P_GENDER) == 1 ? "Letzter" :
504 "Letzte")
505 +" die Ebene '"
506 +channels[ch][I_NAME]
507 +"', worauf diese sich in einem Blitz oktarinen "
508 +"Lichts aufloest.", MSG_EMOTE);
509 channelC[lower_case(ch)] = ({ channels[ch][I_NAME],
510 channels[ch][I_INFO], time() });
511 m_delete(channels, lower_case(ch));
512
513 // Wird ein Channel entfernt, wird auch seine History geloescht
514 channelH = m_copy_delete(channelH, lower_case(ch));
515
516 stats["dispose"]++;
517 save_me_soon=1;
518 }
519 return(0);
520}
521
522// send() - send a message to all recipients of the specified channel 'ch'
523// checks if 'pl' is allowed to send a message and sends if success-
524// ful a message with type 'type'
525// 'pl' must be an object, the message is attributed to it. e.g.
526// ignore checks use it. It can be != previous_object()
527// SEE: access, ch.h
528varargs int send(string ch, object pl, string msg, int type)
529{
530 int a;
531
532 if(!(a = funcall(#'access,&ch, pl, C_SEND, &msg))) return E_ACCESS_DENIED;
533 if(a < 2 && member(channels[ch][I_MEMBER], pl) == -1) return E_NOT_MEMBER;
534 if(!msg || !stringp(msg) || !sizeof(msg)) return E_EMPTY_MESSAGE;
535 map_objects(channels[ch][I_MEMBER],
536 "ChannelMessage", ({ channels[ch][I_NAME], pl, msg, type }));
537 if(sizeof(channelH[ch]) > MAX_HIST_SIZE)
538 channelH[ch] = channelH[ch][1..];
539 channelH[ch] += ({ ({ channels[ch][I_NAME],
540 (stringp(pl)
541 ? pl
542 : (pl->QueryProp(P_INVIS)
543 ? "/("+capitalize(getuid(pl))+")$" : "")
544 + (pl->Name(WER, 2) || "<Unbekannt>")),
545 msg+" <"+strftime("%a, %H:%M:%S")+">\n",
546 type }) });
547 return(0);
548}
549
550// list() - list all channels, that are at least receivable by 'pl'
551// returns a mapping,
552// SEE: access, channels
553private void clean(string n, mixed a) { a[0] -= ({ 0 }); }
554mixed list(object pl)
555{
556 mapping chs;
557
558 chs = filter_indices(channels, #'access/*'*/, pl, C_LIST);
559 walk_mapping(chs, #'clean/*'*/);
560 if(!sizeof(chs)) return E_ACCESS_DENIED;
561 return deep_copy(chs);
562}
563
564// find() - find a channel by its name (may be partial)
565// returns an array for multiple results and 0 for no matching name
566// SEE: access
567mixed find(string ch, object pl)
568{
569 mixed chs, s;
570 if(stringp(ch)) ch = lower_case(ch);
571 if( !sizeof(regexp(({ch}),"^[<>a-z0-9#-]*$")) ) return 0; // RUM
572 if(!sizeof(chs = regexp(m_indices(channels), "^"+ch+"$")))
573 chs = regexp(m_indices(channels), "^"+ch);
574 if((s = sizeof(chs)) > 1)
575 if(sizeof(chs = filter(chs, #'access/*'*/, pl, C_FIND)) == 1)
576 return channels[chs[0]][I_NAME];
577 else return chs;
578 return ((s && funcall(#'access,chs[0], pl, C_FIND)) ? channels[chs[0]][I_NAME] : 0);
579}
580
581// history() - get the history of a channel
582// SEE: access
583mixed history(string ch, object pl)
584{
585 if(!funcall(#'access,&ch, pl, C_JOIN))
586 return E_ACCESS_DENIED;
587 return deep_copy(channelH[ch]);
588}
589
590// remove - remove a channel (wird aus der Shell aufgerufen)
591// SEE: new
592mixed remove(string ch, object pl)
593{
594 mixed members;
595
596 if(previous_object() != this_object())
597 if(!stringp(ch) ||
598 pl != this_player() || this_player() != this_interactive() ||
599 this_interactive() != previous_object() ||
600 !IS_ARCH(this_interactive()))
601 return E_ACCESS_DENIED;
602
603 if(channels[lower_case(ch)]) {
604 channels[lower_case(ch)][I_MEMBER] =
605 filter_objects(channels[lower_case(ch)][I_MEMBER],
606 "QueryProp", P_CHANNELS);
607 map(channels[lower_case(ch)][I_MEMBER],
608 lambda(({'u/*'*/}), ({#'call_other/*'*/, 'u, /*'*/
609 "SetProp", P_CHANNELS,
610 ({#'-/*'*/,
611 ({#'call_other/*'*/, 'u, /*'*/
612 "QueryProp", P_CHANNELS}),
613 '({ lower_case(ch) })/*'*/,})
614 })));
615 channels = m_copy_delete(channels, lower_case(ch));
616
617 // Wird ein Channel entfernt, wird auch seine History geloescht
618 if( pointerp(channelH[lower_case(ch)] ))
619 channelH = m_copy_delete(channelH, lower_case(ch));
620
621 stats["dispose"]++;
622 }
623 if(!channelC[lower_case(ch)])
624 return E_ACCESS_DENIED;
625 channelC = m_copy_delete(channelC, lower_case(ch));
626 save_me_soon=1;
627 return(0);
628}
629
630// Wird aus der Shell aufgerufen
631mixed clear_history(string ch)
632{
633 mixed members;
634 // Sicherheitsabfragen
635 if(previous_object() != this_object())
636 if(!stringp(ch) ||
637 this_player() != this_interactive() ||
638 this_interactive() != previous_object() ||
639 !IS_ARCH(this_interactive()))
640 return E_ACCESS_DENIED;
641
642 // History des Channels loeschen
643 if( pointerp(channelH[lower_case(ch)] ))
644 channelH[lower_case(ch)]=({});
645
646 return 0;
647}