--- deliantra/server/ext/login.ext 2006/12/23 03:38:43 1.2 +++ deliantra/server/ext/login.ext 2007/01/07 02:39:14 1.11 @@ -7,7 +7,10 @@ my $PLAYERDIR = sprintf "%s/%s", cf::localdir, cf::playerdir; -# testbed for coroutines in crossfire : +# paranoia function to overwrite a string-in-place +sub nuke_str { + substr $_[0], 0, (length $_[0]), "x" x length $_[0] +} sub query { my ($ns, $flags, $text) = @_; @@ -34,7 +37,7 @@ sub check_playing { my ($ns, $user) = @_; - return unless cf::player::find $user; + return unless cf::player::find_active $user; $ns->send_drawinfo ( "That player is already logged in on this server. " @@ -51,6 +54,14 @@ 1 } +sub check_clean_save { + my ($pl) = @_; + + unless (delete $pl->{clean_save}) { + #d#TODO + } +} + # delete a player directory, be non-blocking AND synchronous... # (thats hard, so we crap out and fork). sub nuke_playerdir { @@ -62,12 +73,12 @@ . "&& (rm -rf ~\Q$Coro::current\E~deleting~ &)"; } -sub on_addme { +sub addme { my ($ns) = @_; $ns->destroy if $ns->pl; - $ns->coro (sub { + $ns->async (sub { my ($user, $pass); $ns->send_packet ("addme_success"); @@ -83,13 +94,23 @@ # read username while () { $user = query $ns, 0, "What is your name?\n:"; - last if $user =~ /^[a-zA-Z0-9][a-zA-Z0-9\-_]{2,17}$/; - $ns->send_drawinfo ( - "Your username contains illegal characters " - . "(only a-z, A-Z and 0-9 are allowed), " - . "or is not between 3 and 18 characters in length.", - cf::NDI_RED - ); + + if ($cf::LOGIN_LOCK{$user}) { + $ns->send_drawinfo ( + "That username is currently used in another login session. " + . "Chose another, or wait till the other session has ended.", + cf::NDI_RED + ); + } elsif ($user =~ /^[a-zA-Z0-9][a-zA-Z0-9\-_]{2,17}$/) { + last; + } else { + $ns->send_drawinfo ( + "Your username contains illegal characters " + . "(only a-z, A-Z and 0-9 are allowed), " + . "or is not between 3 and 18 characters in length.", + cf::NDI_RED + ); + } } check_playing $ns, $user and next; @@ -112,51 +133,58 @@ ); } - check_playing $ns, $user and next; + # lock this username for the remainder of this login session + if ($cf::LOGIN_LOCK{$user}) { + $ns->send_drawinfo ( + "That username is currently used in another login session. " + . "Chose another, or wait till the other session has ended.", + cf::NDI_RED + ); + next; + } + local $cf::LOGIN_LOCK{$user} = 1; - my $dir = "$PLAYERDIR/$user"; - my $plfile = "$dir/$user.pl"; + check_playing $ns, $user and next; # try to read the user file and check the password - if (my $fh = aio_open $plfile, O_RDONLY, 0) { + if (my $fh = aio_open cf::player::path $user, O_RDONLY, 0) { my $mtime = (stat $fh)[9]; 0 < aio_read $fh, 0, 16384, my $buf, 0 or next; $buf =~ /^password (\S+)$/m or next; my $hash = $1; - check_playing $ns, $user and next; - if ($hash eq crypt $pass, $hash) { + nuke_str $pass; # password matches, wonderful - my $pl = cf::player::load $plfile or next; - $pl->enable_save (1); + my $pl = cf::player::find $user or next; $pl->connect ($ns); + check_clean_save $pl; last; - } - - if (can_cleanup $buf, $mtime) { + } elsif (can_cleanup $buf, $mtime) { Coro::Timer::sleep 1; $ns->send_drawinfo ( - "Player exists, but password does not match. If this is you account, " - . "please try again. If not, you can now opt to take over this account " + "Player exists, but password does not match. If this is your account, " + . "please try again. If not, you can now decide to take over this account " . "because it has not been in-use for some time.", cf::NDI_RED ); + #TODO: nuke_str (query $ns, cf::CS_QUERY_SINGLECHAR, "Delete existing account and create a new one (Y/N)?") =~ /^[yY]/ or next; # check if the file hasn't changed - aio_stat $plfile or next; + aio_stat cf::player::path $user and next; $mtime == (stat _)[9] or next; - # nuke playerdir, this might block, but it needs to be atomic nuke_playerdir $user; # fall through to creation } else { + nuke_str $pass; + Coro::Timer::sleep 1; $ns->send_drawinfo ( @@ -169,14 +197,29 @@ } # the rest of this function is character creation - check_playing $ns, $user and next; + # just to make sure nothing is left over nuke_playerdir $user; - my $pl = cf::player::create; - $pl->ob->name ($user); + my $pass2 = query $ns, cf::CS_QUERY_HIDEINPUT, "Please type your password again."; + + if ($pass2 ne $pass) { + nuke_str $pass; + nuke_str $pass2; + $ns->send_drawinfo ( + "The passwords do not match, please try again.", + cf::NDI_RED + ); + next; + } + + nuke_str $pass2; + + my $pl = cf::player::new $user; $pl->password (crypt $pass, join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64]); + nuke_str $pass; $pl->connect ($ns); + my $ob = $pl->ob; while () { @@ -207,13 +250,17 @@ "Now choose a character.\nPress any key to change outlook.\nPress `d' when you're pleased.\n"); $ns->state (cf::ST_CHANGE_CLASS); - $pl->enable_save (1);#d# too early + delete $pl->{deny_save};#d# too early last; } }); } +# "Quitting will delete your character PERMANENTLY. If you are sure you want to do this, then use the quit_character command instead of quit." +# "Quitting will delete your character.\nAre you sure you want to quit (y/n):" +# quit, quit_character, save + cf::object->attach ( type => cf::SAVEBED, on_apply => sub { @@ -221,24 +268,86 @@ return cf::override 0 unless $ob->type == cf::PLAYER; + my $pl = $ob->pl; + # update respawn position - $ob->contr->savebed ($bed->map->path, $bed->x, $bed->y); + $pl->savebed ($bed->map->path, $bed->x, $bed->y); - #TODO? - #strcpy (pl->contr->killer, "left"); - #check_score (pl); /* Always check score */ + $pl->killer ("left"); + $ob->check_score; $ob->reply (undef, "In the future, you will wake up here when you die."); - $ob->contr->save (); - $ob->contr->ns->query (cf::CS_QUERY_SINGLECHAR, "Do you want to continue playing (y/n)?", sub { - if ($_[0] !~ /^[yY]/) { - $ob->contr->save (1); - $ob->contr->ns->destroy (); - } + $pl->ns->query (cf::CS_QUERY_SINGLECHAR, "Do you want to continue playing (y/n)?", sub { + if ($_[0] !~ /^[yY]/) { + $pl->invoke (cf::EVENT_PLAYER_LOGOUT, 1); + $pl->deactivate; + $pl->ns->destroy; + } else { + cf::async { + $pl->{clean_save} = 1; + $pl->save; + }; + } }); }, ); -cf::client->attach (package => __PACKAGE__); +cf::player->attach ( + on_login => sub { + my ($pl) = @_; + my $name = $pl->ob->name; + + $_->ob->message ("$name has entered the game.", cf::NDI_DK_ORANGE | cf::NDI_UNIQUE) for cf::player::list; + }, + on_logout => sub { + my ($pl, $cleanly) = @_; + my $name = $pl->ob->name; + + if ($cleanly) { + $_->ob->message ("$name left the game.", cf::NDI_DK_ORANGE | cf::NDI_UNIQUE) for cf::player::list; + } else { + $_->ob->message ("$name uncerimoniously disconnected.", cf::NDI_DK_ORANGE | cf::NDI_UNIQUE) for cf::player::list; + } + }, +); + +cf::client->attach ( + on_addme => \&addme, +); + +############################################################################# + +our $SCHEDULE_INTERVAL = 10; # time the player scheduler sleeps between runs +our $SWAP_TIMEOUT = 30; # time after which an unused player is evicted form memory +our $SAVE_TIMEOUT = 20; # save players every n seconds +our $SAVE_INTERVAL = 0.1; # save at max. one player every $SAVE_INTERVAL + +our $SCHEDULER = cf::async_ext { + while () { + Coro::Timer::sleep $SCHEDULE_INTERVAL; + + # this weird form of iteration over values is used because + # the hash changes underneath us frequently, and for + # keeps a direct reference to the value without (in 5.8 perls) + # keeping a reference, so this is prone to crashes or worse. + my @players = keys %cf::PLAYER; + for (@players) { + my $pl = $cf::PLAYER{$_} + or next; + $pl->valid or next; + + eval { + if ($pl->{last_save} + $SAVE_TIMEOUT <= $cf::RUNTIME) { + $pl->save; + Coro::Timer::sleep $SAVE_INTERVAL; + } + }; + warn $@ if $@; + Coro::cede; + }; + } +}; + +$SCHEDULER->prio (1);