ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/ext/login.ext
Revision: 1.49
Committed: Sun Jun 10 03:11:16 2007 UTC (16 years, 11 months ago) by root
Branch: MAIN
Changes since 1.48: +3 -3 lines
Log Message:
adjust player destroy code for extra reference from observe

File Contents

# User Rev Content
1 root 1.25 #! perl # MANDATORY
2 root 1.1
3     # login handling
4    
5     use Fcntl;
6     use Coro::AIO;
7 root 1.32 use List::Util qw(min max);
8 root 1.1
9 root 1.9 # paranoia function to overwrite a string-in-place
10     sub nuke_str {
11     substr $_[0], 0, (length $_[0]), "x" x length $_[0]
12     }
13 root 1.1
14     sub query {
15     my ($ns, $flags, $text) = @_;
16    
17     my $current = $Coro::current;
18 root 1.38 $ns->query ($flags, $text, sub { $current->ready; $current = $_[0] });
19 root 1.1 Coro::schedule while ref $current;
20    
21     $current
22     }
23    
24     sub can_cleanup {
25 root 1.19 my ($pl, $mtime) = @_;
26 root 1.1
27     my $age = time - $mtime;
28 root 1.19 my $level = $pl->ob->level;
29 root 1.1
30     ($level <= 3 && $age > 7 * 86400) # 7 days for level 0..3
31     || ($level <= 9 && $age > 90 * 86400) # 3 months for level 4..9
32     || ($level <= 20 && $age > 180 * 86400) # 6 months for level 10..20
33     || $age > 700 * 86400 # 2 years for everybody else
34     }
35    
36     sub check_playing {
37     my ($ns, $user) = @_;
38    
39 root 1.11 return unless cf::player::find_active $user;
40 root 1.1
41     $ns->send_drawinfo (
42     "That player is already logged in on this server. "
43     . "If you want to create a new player, choose another name. "
44     . "If you are already a registered player, make sure nobody "
45     . "else is using your account at this time. If you lost your conenction "
46     . "then the server will likely timeout within a minute. If you still "
47     . "cannot log-in after a minute, you are still logged in. Make sure "
48     . "you do not have another client running. If you use windows, reboot, "
49     . "this will fix anything.",
50     cf::NDI_RED
51     );
52    
53     1
54     }
55    
56 root 1.48 sub enter_map {
57 root 1.11 my ($pl) = @_;
58    
59 root 1.48 my ($map, $x, $y)
60     = $pl->ob->{_link_pos}
61     ? @{delete $pl->ob->{_link_pos}}
62     : ($pl->maplevel, $pl->ob->x, $pl->ob->y);
63    
64     $pl->ob->enter_link;
65    
66 root 1.26 if (my $time = delete $pl->{unclean_save}) {
67 root 1.48 if (my $m = cf::map::find $map) {
68     if ($time < $m->{instantiate_time}) {
69     # the map was reset in the meantime
70     my $age = $cf::RUNTIME - $time;
71     warn $pl->ob->name, " map reset after logout, logout age $age\n";#d#
72    
73     # for now, just go back to savebed
74     ($map, $x, $y) = $pl->savebed;
75    
76     $pl->ns->send_drawinfo (
77     "You didn't use a bed to reality to leave this realm, leaving your body in great danger. "
78     . "Fortunately, some friendly dwellers found you, checked your passport, and brought you to safety. "
79     . "Better use a savebed next time, much worse things could have happened...",
80     cf::NDI_RED
81     );
82     } else {
83     $pl->ns->send_drawinfo (
84     "You didn't use a bed to reality to leave this realm. This is very dangerous, "
85     . "as lots of things could happen when you leave by other means, such as cave-ins, "
86     . "or monsters suddenly snapping your body. Better use a savebed next time.",
87     cf::NDI_RED
88     );
89     }
90     }
91 root 1.11 }
92 root 1.48
93     $pl->ob->goto ($map, $x, $y);
94 root 1.11 }
95    
96 root 1.1 # delete a player directory, be non-blocking AND synchronous...
97     # (thats hard, so we crap out and fork).
98     sub nuke_playerdir {
99     my ($user) = @_;
100    
101     aio_stat "$PLAYERDIR/$user";
102     system "cd \Q$PLAYERDIR\E "
103     . "&& mv \Q$user\E ~\Q$Coro::current\E~deleting~ 2>/dev/null "
104     . "&& (rm -rf ~\Q$Coro::current\E~deleting~ &)";
105     }
106    
107 root 1.35 sub send_capabilities {
108     my ($ns) = @_;
109    
110     return unless $ns->extcmd;
111    
112     $ns->ext_event (capabilities =>
113     # id, name, flags (1 == 2d), edge length
114     tileset => [[1, "default 64x64 faceset", 1, 64], [0, "default 32x32 faceset", 1, 32]],
115     );
116     }
117    
118 root 1.33 sub setup {
119     my ($ns, $args) = @_;
120    
121     # run through the cmds of setup
122     # syntax is setup <cmdname1> <parameter> <cmdname2> <parameter> ...
123     #
124     # we send the status of the cmd back, or a FALSE is the cmd is the server unknown
125     # The client then must sort this out
126    
127     my %setup = split / +/, $args;
128     while (my ($k, $v) = each %setup) {
129     if ($k eq "sound") {
130 root 1.43 $ns->sound ($v);
131 root 1.33
132     } elsif ($k eq "exp64") {
133 root 1.43 $setup{$k} = 1;
134 root 1.33
135     } elsif ($k eq "spellmon") {
136 root 1.43 $ns->monitor_spells ($v);
137 root 1.33
138     } elsif ($k eq "darkness") {
139 root 1.43 $ns->darkness ($v);
140 root 1.33
141     } elsif ($k eq "map1cmd") {
142 root 1.43 $ns->mapmode (cf::Map1Cmd) if $v > 0;
143 root 1.33
144     } elsif ($k eq "map1acmd") {
145 root 1.43 $ns->mapmode (cf::Map1aCmd) if $v > 0;
146 root 1.33
147     } elsif ($k eq "map2cmd") {
148 root 1.43 # gcfclient bug, map1acmd is sent too late
149     $ns->mapmode (cf::Map1aCmd);
150     $setup{$k} = "FALSE";
151 root 1.33
152     } elsif ($k eq "newmapcmd") {
153 root 1.43 $ns->newmapcmd ($v);
154 root 1.33
155     } elsif ($k eq "mapinfocmd") {
156 root 1.43 $ns->mapinfocmd ($v);
157 root 1.33
158     } elsif ($k eq "extcmd") {
159 root 1.43 $ns->extcmd ($v > 0);
160     send_capabilities $ns;
161 root 1.33
162     } elsif ($k eq "extmap") {
163 root 1.43 $ns->extmap ($v);
164 root 1.33
165     } elsif ($k eq "facecache") {
166 root 1.43 if (!$v) {
167     $v = 1;
168     $setup{$k} = $v;
169     $ns->send_drawinfo ("(trying to forcefully enable facecaching)", cf::NDI_RED);
170     }
171 root 1.40
172 root 1.43 $ns->facecache ($v);
173 root 1.33
174     } elsif ($k eq "faceset") {
175 root 1.43 $ns->faceset (0);
176     $setup{$k} = 0;
177     # $ns->image2 (1)
178 root 1.33
179 root 1.35 } elsif ($k eq "tileset") {
180 root 1.43 $setup{$k} = $ns->faceset ($v & 1);
181 root 1.35
182 root 1.33 } elsif ($k eq "itemcmd") {
183 root 1.43 # Version of the item protocol command to use. Currently,
184     # only supported versions are 1 and 2. Using a numeric
185     # value will make it very easy to extend this in the future.
186     $ns->itemcmd ($v) if $v >= 1 && $v <= 2;
187 root 1.33
188 root 1.43 $setup{$k} = $ns->itemcmd;
189 root 1.33
190     } elsif ($k eq "mapsize") {
191 root 1.43 my ($x, $y) = split /x/, $v;
192 root 1.33
193 root 1.43 $ns->mapx ($x = max 9, min cf::MAP_CLIENT_X, ($x - 1) | 1);
194     $ns->mapy ($y = max 9, min cf::MAP_CLIENT_Y, ($y - 1) | 1);
195 root 1.33
196 root 1.43 $setup{$k} = "${x}x${y}";
197 root 1.33
198     } elsif ($k eq "extendedMapInfos") {
199 root 1.43 $ns->ext_mapinfos ($v);
200 root 1.33
201     } elsif ($k eq "extendedTextInfos") {
202 root 1.43 $ns->has_readable_type ($v);
203 root 1.33
204 root 1.38 } elsif ($k eq "smoothing") { # cfplus-style smoothing
205 root 1.43 $ns->smoothing ($v);
206 root 1.38
207 root 1.34 } elsif ($k eq "fxix") {
208 root 1.43 $ns->fxix ($v);
209    
210     } elsif ($k eq "msg") {
211 root 1.45 $ns->can_msg ($v);
212 root 1.34
213 root 1.44 } elsif ($k eq "excmd") {
214     # we support it
215    
216 root 1.33 } else {
217 root 1.43 # other commands:
218     # sexp: no idea, probably for oudated servers
219 root 1.45 # tick: more stupidity, server should send a tick per tick
220 root 1.33
221 root 1.43 $setup{$k} = "FALSE";
222 root 1.33 }
223     }
224    
225     $ns->send_packet (join " ", setup => %setup);
226    
227     cf::datalog setup =>
228     request => $args,
229     reply => \%setup,
230     client => $ns->version,
231     ;
232     }
233    
234 root 1.8 sub addme {
235 root 1.1 my ($ns) = @_;
236    
237 root 1.32 if (!$ns->facecache)
238     {
239     $ns->send_drawinfo (<<EOF, cf::NDI_RED);
240    
241    
242     ***
243     *** WARNING:
244     *** Your client does not support face/image caching,
245     *** or it has been disabled. Face caching is mandatory
246     *** so please enable it or use a newer client.
247     ***
248     *** Look at your client preferences:
249     ***
250     *** CFPlus: all known versions automatically enable the facecache.
251     *** cfclient: use the -cache commandline option.
252     *** cfclient: map will not redraw automatically (bug).
253     *** gcfclient: use -cache commandline option, or enable
254     *** gcfclient: Client=>Configure=>Map & Image=>Cache Images.
255     *** jcrossclient: your client is broken, use CFPlus or gcfclient.
256     ***
257     ***
258     EOF
259     if ($ns->version =~ /jcrossclient/) {
260     # let them, for now
261     } else {
262     $ns->flush;
263     return $ns->destroy;
264     }
265    
266     # $ns->facecache = true;
267 root 1.33 }
268    
269     if ($ns->mapmode < cf::Map1aCmd) {
270     $ns->send_drawinfo (<<EOF, cf::NDI_RED);
271    
272    
273     ***
274     *** WARNING:
275     *** Your client is too old. Please upgrade to a newer version.
276     EOF
277    
278     $ns->flush;
279     return $ns->destroy;
280     }
281 root 1.32
282     $ns->pl and return $ns->destroy;
283 root 1.1
284 root 1.10 $ns->async (sub {
285 root 1.1 my ($user, $pass);
286    
287     $ns->send_packet ("addme_success");
288    
289     for (;;) {
290     $ns->send_drawinfo (
291     "Please enter your username now. If you are a new user, "
292     . "make one up that describes your character best. "
293     . "Only letters and digits are allowed, though.",
294     cf::NDI_BLUE
295     );
296    
297     # read username
298     while () {
299     $user = query $ns, 0, "What is your name?\n:";
300 root 1.3
301     if ($cf::LOGIN_LOCK{$user}) {
302     $ns->send_drawinfo (
303     "That username is currently used in another login session. "
304     . "Chose another, or wait till the other session has ended.",
305     cf::NDI_RED
306     );
307 root 1.30 } elsif ($user =~ /^[a-zA-Z0-9][a-zA-Z0-9\-_]{2,17}\z/) {
308 root 1.3 last;
309     } else {
310     $ns->send_drawinfo (
311     "Your username contains illegal characters "
312     . "(only a-z, A-Z and 0-9 are allowed), "
313     . "or is not between 3 and 18 characters in length.",
314     cf::NDI_RED
315     );
316     }
317 root 1.1 }
318    
319     check_playing $ns, $user and next;
320    
321     $ns->send_drawinfo (
322     "Welcome $user, please enter your password now. "
323     . "New users should now choose a password. "
324     . "Anything your client lets you enter is fine.",
325     cf::NDI_BLUE
326     );
327    
328     # read password
329     while () {
330     $pass = query $ns, cf::CS_QUERY_HIDEINPUT, "What is your password?\n:";
331     last if $pass =~ /.../;
332     $ns->send_drawinfo (
333     "Try to use at least three characters as your password please, "
334     . "that cannot be too much to ask for :)",
335     cf::NDI_RED
336     );
337     }
338    
339 root 1.3 # lock this username for the remainder of this login session
340     if ($cf::LOGIN_LOCK{$user}) {
341     $ns->send_drawinfo (
342     "That username is currently used in another login session. "
343     . "Chose another, or wait till the other session has ended.",
344     cf::NDI_RED
345     );
346     next;
347     }
348     local $cf::LOGIN_LOCK{$user} = 1;
349    
350     check_playing $ns, $user and next;
351    
352 root 1.1 # try to read the user file and check the password
353 root 1.19 if (my $pl = cf::player::find $user) {
354     aio_stat $pl->path and next;
355     my $mtime = (stat _)[9];
356     my $hash = $pl->password;
357 root 1.1
358 root 1.19 if ($cf::CFG{ext_login_nocheck} or $hash eq crypt $pass, $hash) {
359 root 1.9 nuke_str $pass;
360 root 1.1 # password matches, wonderful
361 root 1.11 my $pl = cf::player::find $user or next;
362 root 1.1 $pl->connect ($ns);
363 root 1.48 enter_map $pl;
364 root 1.1 last;
365 root 1.19 } elsif (can_cleanup $pl, $mtime) {
366 root 1.1 Coro::Timer::sleep 1;
367    
368     $ns->send_drawinfo (
369 root 1.3 "Player exists, but password does not match. If this is your account, "
370     . "please try again. If not, you can now decide to take over this account "
371 root 1.1 . "because it has not been in-use for some time.",
372     cf::NDI_RED
373     );
374    
375 root 1.9 #TODO: nuke_str
376 root 1.1 (query $ns, cf::CS_QUERY_SINGLECHAR, "Delete existing account and create a new one (Y/N)?") =~ /^[yY]/
377     or next;
378    
379     # check if the file hasn't changed
380 root 1.11 aio_stat cf::player::path $user and next;
381 root 1.1 $mtime == (stat _)[9] or next;
382    
383 root 1.19 $pl->quit_character;
384 root 1.1
385     # fall through to creation
386     } else {
387 root 1.9 nuke_str $pass;
388    
389 root 1.1 Coro::Timer::sleep 1;
390    
391     $ns->send_drawinfo (
392     "Wrong username or password. Please try again "
393     . "(check for Numlock and other semi-obvious error sources).",
394     cf::NDI_RED
395     );
396     next;
397     }
398 root 1.37 } else {
399     # unable to load the playerfile:
400     # check wether the player dir exists, which means the file is corrupted or
401     # something very similar.
402     if (!aio_stat cf::player::playerdir $user) {
403     $ns->send_drawinfo (
404     "Unable to retrieve this player. It might be a locked or broken account. "
405     . "If this is your account, ask a dungeon master for assistance. "
406     . "Otherwise choose a different login name.",
407     cf::NDI_RED
408     );
409     next;
410     }
411 root 1.1 }
412    
413     # the rest of this function is character creation
414    
415 root 1.3 # just to make sure nothing is left over
416 root 1.1 nuke_playerdir $user;
417    
418 root 1.3 my $pass2 = query $ns, cf::CS_QUERY_HIDEINPUT, "Please type your password again.";
419    
420     if ($pass2 ne $pass) {
421 root 1.9 nuke_str $pass;
422     nuke_str $pass2;
423 root 1.3 $ns->send_drawinfo (
424     "The passwords do not match, please try again.",
425     cf::NDI_RED
426     );
427     next;
428     }
429    
430 root 1.9 nuke_str $pass2;
431    
432 root 1.11 my $pl = cf::player::new $user;
433 root 1.1 $pl->password (crypt $pass, join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64]);
434 root 1.9 nuke_str $pass;
435 root 1.1 $pl->connect ($ns);
436 root 1.48 my $ob = $pl->ob;
437 root 1.3
438 root 1.48 $ob->goto ($pl->maplevel, $ob->x, $ob->y);
439 root 1.1
440     while () {
441     $ob->update_stats;
442     $pl->save_stats;
443    
444     my $res = query $ns, cf::CS_QUERY_SINGLECHAR,
445     "[y] to roll new stats [n] to use stats\n[1-7] [1-7] to swap stats.\nRoll again (y/n/1-7)?";
446    
447     if ($res =~ /^[Nn]/) {
448     last;
449     } elsif ($res > 0 && $res <= 7) {
450     my $swap = query $ns, cf::CS_QUERY_SINGLECHAR, "Swap stat with (will not roll new stats) [1-7]?";
451    
452     if ($swap > 0 && $swap <= 7) {
453     $ob->swap_stats ($res - 1, $swap - 1);
454     }
455     } else {
456     $ob->roll_stats;
457     }
458     }
459    
460     $ob->set_animation (2);
461     $ob->add_statbonus;
462    
463 root 1.45 while () {
464     $ns->send_msg (-1, "chargen-race-title", ucfirst $pl->title);
465     my $msg = $ob->msg;
466 root 1.46 $msg =~ s/(?<=\S)\n(?=\S)/ /g;
467 root 1.45 $ns->send_msg (cf::NDI_BLUE, "chargen-race-description", $msg);
468    
469     my $res = query $ns, cf::CS_QUERY_SINGLECHAR,
470     "Now choose a character.\nPress any key to change outlook.\nPress `d' when you're pleased.\n";
471    
472     last if $res =~ /[dD]/;
473    
474     $pl->chargen_race_next;
475     }
476    
477     $pl->chargen_race_done;
478 root 1.1
479 root 1.45 delete $pl->{deny_save};
480 root 1.1
481     last;
482     }
483     });
484     }
485    
486 root 1.12 cf::register_command quit => sub {
487     my ($ob, $arg) = @_;
488    
489     $ob->reply (undef,
490     "Quitting will delete your character PERMANENTLY: It will be gone forever and any progress will be lost. "
491     . "If you are sure you want to do this, then use the quit_character command instead of quit.",
492     cf::NDI_UNIQUE | cf::NDI_RED);
493     };
494    
495     cf::register_command quit_character => sub {
496     my ($ob, $arg) = @_;
497    
498     my $pl = $ob->contr;
499    
500     $pl->ns->query (cf::CS_QUERY_SINGLECHAR, "Do you want to PERMANENTLY delete your character and all associated data (y/n)?", sub {
501     if ($_[0] !~ /^[yY]/) {
502     $ob->reply (undef,
503     "Ok, not not quitting then.",
504     cf::NDI_UNIQUE | cf::NDI_RED);
505     } else {
506     $ob->reply (undef,
507     "Ok, quitting, hope to see you again.",
508     cf::NDI_UNIQUE | cf::NDI_RED);
509     $pl->ns->flush;
510 root 1.31 cf::async { $pl->quit_character };
511 root 1.12 }
512     });
513     };
514 root 1.11
515 root 1.1 cf::object->attach (
516     type => cf::SAVEBED,
517     on_apply => sub {
518     my ($bed, $ob) = @_;
519    
520     return cf::override 0 unless $ob->type == cf::PLAYER;
521    
522 root 1.15 my $pl = $ob->contr;
523 root 1.11
524 root 1.1 # update respawn position
525 root 1.11 $pl->savebed ($bed->map->path, $bed->x, $bed->y);
526 root 1.22 cf::async { $pl->save };
527 root 1.1
528 root 1.11 $pl->killer ("left");
529 root 1.5 $ob->check_score;
530 root 1.1
531     $ob->reply (undef, "In the future, you will wake up here when you die.");
532    
533 root 1.11 $pl->ns->query (cf::CS_QUERY_SINGLECHAR, "Do you want to continue playing (y/n)?", sub {
534 root 1.6 if ($_[0] !~ /^[yY]/) {
535 root 1.11 $pl->invoke (cf::EVENT_PLAYER_LOGOUT, 1);
536     $pl->deactivate;
537     $pl->ns->destroy;
538 root 1.7 } else {
539 root 1.13 cf::async { $pl->save };
540 root 1.6 }
541 root 1.1 });
542     },
543     );
544    
545 root 1.8 cf::player->attach (
546     on_login => sub {
547     my ($pl) = @_;
548     my $name = $pl->ob->name;
549    
550     $_->ob->message ("$name has entered the game.", cf::NDI_DK_ORANGE | cf::NDI_UNIQUE) for cf::player::list;
551     },
552     on_logout => sub {
553     my ($pl, $cleanly) = @_;
554     my $name = $pl->ob->name;
555    
556     if ($cleanly) {
557     $_->ob->message ("$name left the game.", cf::NDI_DK_ORANGE | cf::NDI_UNIQUE) for cf::player::list;
558     } else {
559     $_->ob->message ("$name uncerimoniously disconnected.", cf::NDI_DK_ORANGE | cf::NDI_UNIQUE) for cf::player::list;
560 root 1.26 $pl->{unclean_save} = $cf::RUNTIME;
561 root 1.8 }
562     },
563     );
564    
565     cf::client->attach (
566     on_addme => \&addme,
567 root 1.32 on_setup => \&setup,
568 root 1.8 );
569 root 1.1
570 root 1.11 #############################################################################
571    
572 root 1.21 our $SCHEDULE_INTERVAL = 10; # time the player scheduler sleeps between runs
573     our $SAVE_TIMEOUT = 20; # save players every n seconds
574 root 1.11
575     our $SCHEDULER = cf::async_ext {
576 root 1.27 my $schedule_interval = Coro::Event->timer (after => 1, interval => $SCHEDULE_INTERVAL);
577 root 1.11 while () {
578 root 1.27 $schedule_interval->next;
579 root 1.11
580     # this weird form of iteration over values is used because
581     # the hash changes underneath us frequently, and for
582     # keeps a direct reference to the value without (in 5.8 perls)
583     # keeping a reference, so this is prone to crashes or worse.
584     my @players = keys %cf::PLAYER;
585     for (@players) {
586     my $pl = $cf::PLAYER{$_}
587     or next;
588     $pl->valid or next;
589    
590     eval {
591     if ($pl->{last_save} + $SAVE_TIMEOUT <= $cf::RUNTIME) {
592 root 1.39 cf::wait_for_tick_begin;
593 root 1.11 $pl->save;
594 root 1.17
595 root 1.49 unless ($pl->active || $pl->ns) {
596 root 1.21 # check refcounts, this is tricky and needs to be adjusted to fit server internals
597     my $ob = $pl->ob;
598     Scalar::Util::weaken $pl;
599     Scalar::Util::weaken $ob;
600 root 1.23 my $a_ = $pl->refcnt;#d#
601     my $b_ = $ob->refcnt;#d#
602 root 1.21 my $pl_ref = $pl->refcnt_cnt;
603     my $ob_ref = $ob->refcnt_cnt;
604    
605 root 1.20 ## pl_ref == one from object + one from cf::PLAYER
606 root 1.49 ## ob_ref == one from simply being an object + one from pl->observe
607     if ($pl_ref == 2 && $ob_ref == 2) {
608 root 1.21 warn "player-scheduler destroy ", $ob->name;#d#
609    
610     # remove from sight and get fresh "copies"
611     $pl = delete $cf::PLAYER{$ob->name};
612     $ob = $pl->ob;
613    
614     $ob->destroy;
615     $pl->destroy;
616     } else {
617 root 1.47 warn "player-scheduler refcnt ", $ob->name, " pp$pl_ref,pc$a_ op$ob_ref,oc$b_\n";#d#
618 root 1.21 }
619 root 1.17 }
620     }
621 root 1.11 };
622     warn $@ if $@;
623     Coro::cede;
624     };
625     }
626     };
627    
628     $SCHEDULER->prio (1);
629