--- deliantra/server/lib/cf.pm 2007/04/01 00:36:34 1.229 +++ deliantra/server/lib/cf.pm 2007/04/19 16:23:47 1.255 @@ -5,19 +5,22 @@ use Symbol; use List::Util; +use Socket; use Storable; use Event; use Opcode; use Safe; use Safe::Hole; -use Coro 3.52 (); +use Coro 3.61 (); use Coro::State; +use Coro::Handle; use Coro::Event; use Coro::Timer; use Coro::Signal; use Coro::Semaphore; use Coro::AIO; +use Coro::Storable; use BDB (); use Data::Dumper; @@ -30,6 +33,7 @@ # configure various modules to our taste # +$Storable::canonical = 1; # reduce rsync transfers Coro::State::cctx_stacksize 256000; # 1-2MB stack, for deep recursions in maze generator Compress::LZF::sfreeze_cr { }; # prime Compress::LZF so it does not use require later @@ -52,9 +56,20 @@ our $RELOAD; # number of reloads so far our @EVENT; -our $LIBDIR = datadir . "/ext"; -our $TICK = MAX_TIME * 1e-6; +our $CONFDIR = confdir; +our $DATADIR = datadir; +our $LIBDIR = "$DATADIR/ext"; +our $PODDIR = "$DATADIR/pod"; +our $MAPDIR = "$DATADIR/" . mapdir; +our $LOCALDIR = localdir; +our $TMPDIR = "$LOCALDIR/" . tmpdir; +our $UNIQUEDIR = "$LOCALDIR/" . uniquedir; +our $PLAYERDIR = "$LOCALDIR/" . playerdir; +our $RANDOMDIR = "$LOCALDIR/random"; +our $BDBDIR = "$LOCALDIR/db"; + +our $TICK = MAX_TIME * 1e-6; # this is a CONSTANT(!) our $TICK_WATCHER; our $AIO_POLL_WATCHER; our $NEXT_RUNTIME_WRITE; # when should the runtime file be written @@ -73,11 +88,6 @@ our %PLAYER; # all users our %MAP; # all maps our $LINK_MAP; # the special {link} map, which is always available -our $RANDOM_MAPS = cf::localdir . "/random"; -our $BDB_ENV_DIR = cf::localdir . "/db"; - -our $WAIT_FOR_TICK; $WAIT_FOR_TICK ||= new Coro::Signal; -our $WAIT_FOR_TICK_ONE; $WAIT_FOR_TICK_ONE ||= new Coro::Signal; # used to convert map paths into valid unix filenames by replacing / by ∕ our $PATH_SEP = "∕"; # U+2215, chosen purely for visual reasons @@ -86,18 +96,14 @@ binmode STDERR; # read virtual server time, if available -unless ($RUNTIME || !-e cf::localdir . "/runtime") { - open my $fh, "<", cf::localdir . "/runtime" +unless ($RUNTIME || !-e "$LOCALDIR/runtime") { + open my $fh, "<", "$LOCALDIR/runtime" or die "unable to read runtime file: $!"; $RUNTIME = <$fh> + 0.; } -mkdir cf::localdir; -mkdir cf::localdir . "/" . cf::playerdir; -mkdir cf::localdir . "/" . cf::tmpdir; -mkdir cf::localdir . "/" . cf::uniquedir; -mkdir $RANDOM_MAPS; -mkdir $BDB_ENV_DIR; +mkdir $_ + for $LOCALDIR, $TMPDIR, $UNIQUEDIR, $PLAYERDIR, $RANDOMDIR, $BDBDIR; our $EMERGENCY_POSITION; @@ -118,10 +124,14 @@ The time this server has run, starts at 0 and is increased by $cf::TICK on every server tick. -=item $cf::LIBDIR - -The perl library directory, where extensions and cf-specific modules can -be found. It will be added to C<@INC> automatically. +=item $cf::CONFDIR $cf::DATADIR $cf::LIBDIR $cf::PODDIR +$cf::MAPDIR $cf::LOCALDIR $cf::TMPDIR $cf::UNIQUEDIR +$cf::PLAYERDIR $cf::RANDOMDIR $cf::BDBDIR + +Various directories - "/etc", read-only install directory, perl-library +directory, pod-directory, read-only maps directory, "/var", "/var/tmp", +unique-items directory, player file directory, random maps directory and +database environment. =item $cf::NOW @@ -136,12 +146,11 @@ Configuration for the server, loaded from C, or from wherever your confdir points to. -=item $cf::WAIT_FOR_TICK, $cf::WAIT_FOR_TICK_ONE +=item cf::wait_for_tick, cf::wait_for_tick_begin -These are Coro::Signal objects that are C<< ->broadcast >> (WAIT_FOR_TICK) -or C<< ->send >> (WAIT_FOR_TICK_ONE) on after normal server tick -processing has been done. Call C<< ->wait >> on them to maximise the -window of cpu time available, or simply to synchronise to the server tick. +These are functions that inhibit the current coroutine one tick. cf::wait_for_tick_begin only +returns directly I the tick processing (and consequently, can only wake one process +per tick), while cf::wait_for_tick wakes up all waiters after tick processing. =back @@ -150,11 +159,13 @@ BEGIN { *CORE::GLOBAL::warn = sub { my $msg = join "", @_; - utf8::encode $msg; $msg .= "\n" unless $msg =~ /\n$/; + $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; + + utf8::encode $msg; LOG llevError, $msg; }; } @@ -234,6 +245,10 @@ Lock names should begin with a unique identifier (for example, cf::map::find uses map_find and cf::map::load uses map_load). +=item $locked = cf::lock_active $string + +Return true if the lock is currently active, i.e. somebody has locked it. + =cut our %LOCK; @@ -262,6 +277,12 @@ } } +sub lock_active($) { + my ($key) = @_; + + ! ! $LOCK{$key} +} + sub freeze_mainloop { return unless $TICK_WATCHER->is_active; @@ -350,7 +371,7 @@ sub write_runtime { my $guard = cf::lock_acquire "write_runtime"; - my $runtime = cf::localdir . "/runtime"; + my $runtime = "$LOCALDIR/runtime"; my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 or return; @@ -375,6 +396,17 @@ 1 } +=item cf::datalog type => key => value, ... + +Log a datalog packet of the given type with the given key-value pairs. + +=cut + +sub datalog($@) { + my ($type, %kv) = @_; + warn "DATALOG ", JSON::XS->new->ascii->encode ({ %kv, type => $type }); +} + =back =cut @@ -944,17 +976,14 @@ =cut sub playerdir($) { - cf::localdir - . "/" - . cf::playerdir - . "/" + "$PLAYERDIR/" . (ref $_[0] ? $_[0]->ob->name : $_[0]) } sub path($) { my $login = ref $_[0] ? $_[0]->ob->name : $_[0]; - (playerdir $login) . "/$login.pl" + (playerdir $login) . "/playerdata" } sub find_active($) { @@ -977,6 +1006,13 @@ my $guard = cf::lock_acquire "user_find:$login"; $cf::PLAYER{$_[0]} || do { + # rename old playerfiles to new ones + #TODO: remove when no longer required + aio_link +(playerdir $login) . "/$login.pl.pst", (playerdir $login) . "/playerdata.pst"; + aio_link +(playerdir $login) . "/$login.pl" , (playerdir $login) . "/playerdata"; + aio_unlink +(playerdir $login) . "/$login.pl.pst"; + aio_unlink +(playerdir $login) . "/$login.pl"; + my $pl = load_pl path $login or return; $cf::PLAYER{$login} = $pl @@ -1070,7 +1106,7 @@ =cut sub list_logins { - my $dirs = aio_readdir cf::localdir . "/" . cf::playerdir + my $dirs = aio_readdir $PLAYERDIR or return []; my @logins; @@ -1115,13 +1151,13 @@ \@paths } -=item $player->ext_reply ($msgid, $msgtype, %msg) +=item $player->ext_reply ($msgid, %msg) Sends an ext reply to the player. =cut -sub ext_reply($$$%) { +sub ext_reply($$%) { my ($self, $id, %msg) = @_; $msg{msgid} = $id; @@ -1129,10 +1165,48 @@ $self->send ("ext " . cf::to_json \%msg); } -package cf; +=item $player->ext_event ($type, %msg) -=back +Sends an ext event to the client. +=cut + +sub ext_event($$%) { + my ($self, $type, %msg) = @_; + + $self->ns->ext_event ($type, %msg); +} + +=head3 cf::region + +=over 4 + +=cut + +package cf::region; + +=item cf::region::find_by_path $path + +Tries to decuce the probable region for a map knowing only its path. + +=cut + +sub find_by_path($) { + my ($path) = @_; + + my ($match, $specificity); + + for my $region (list) { + if ($region->match && $path =~ $region->match) { + ($match, $specificity) = ($region, $region->specificity) + if $region->specificity > $specificity; + } + } + + $match +} + +=back =head3 cf::map @@ -1280,7 +1354,7 @@ sub load_path { my ($self) = @_; - sprintf "%s/%s/%s.map", cf::datadir, cf::mapdir, $self->{path} + "$MAPDIR/$self->{path}.map" } # the temporary/swap location @@ -1288,7 +1362,7 @@ my ($self) = @_; (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; - sprintf "%s/%s/%s.map", cf::localdir, cf::tmpdir, $path + "$TMPDIR/$path.map" } # the unique path, undef == no special unique path @@ -1296,7 +1370,7 @@ my ($self) = @_; (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; - sprintf "%s/%s/%s", cf::localdir, cf::uniquedir, $path + "$UNIQUEDIR/$path" } # and all this just because we cannot iterate over @@ -1373,6 +1447,9 @@ $self->prepare_orig; } + $self->default_region (cf::region::find_by_path $self->{path}) + unless $self->default_region; + 1 } @@ -1424,6 +1501,7 @@ $self->alloc; $self->pre_load; + Coro::cede; $self->_load_objects ($self->{load_path}, 1) or return; @@ -1440,9 +1518,9 @@ } Coro::cede; - # now do the right thing for maps $self->link_multipart_objects; + Coro::cede; unless ($self->{deny_activate}) { $self->decay_objects; @@ -1454,9 +1532,11 @@ unless $self->difficulty; Coro::cede; $self->activate; + Coro::cede; } $self->post_load; + Coro::cede; $self->in_memory (cf::MAP_IN_MEMORY); } @@ -1659,7 +1739,7 @@ =cut sub unique_maps() { - my $files = aio_readdir cf::localdir . "/" . cf::uniquedir + my $files = aio_readdir $UNIQUEDIR or return; my @paths; @@ -1925,7 +2005,7 @@ my $data = cf::to_json $rmp; my $md5 = Digest::MD5::md5_hex $data; - my $meta = "$cf::RANDOM_MAPS/$md5.meta"; + my $meta = "$RANDOMDIR/$md5.meta"; if (my $fh = aio_open "$meta~", O_WRONLY | O_CREAT, 0666) { aio_write $fh, 0, (length $data), $data, 0; @@ -1944,6 +2024,8 @@ if ($exit->slaying eq "/!") { #TODO: this should de-fi-ni-te-ly not be a sync-job + # the problem is that $exit might not survive long enough + # so it needs to be done right now, right here cf::sync_job { prepare_random_map $exit }; } @@ -1961,9 +2043,9 @@ 1; }) { $self->message ("Something went wrong deep within the crossfire server. " - . "I'll try to bring you back to the map you were before. " - . "Please report this to the dungeon master!", - cf::NDI_UNIQUE | cf::NDI_RED); + . "I'll try to bring you back to the map you were before. " + . "Please report this to the dungeon master!", + cf::NDI_UNIQUE | cf::NDI_RED); warn "ERROR in enter_exit: $@"; $self->leave_link; @@ -1989,6 +2071,18 @@ $self->send_packet (sprintf "drawinfo %d %s", $flags || cf::NDI_BLACK, $text); } +=item $client->ext_event ($type, %msg) + +Sends an exti event to the client. + +=cut + +sub cf::client::ext_event($$%) { + my ($self, $type, %msg) = @_; + + $msg{msgtype} = "event_$type"; + $self->send_packet ("ext " . cf::to_json \%msg); +} =item $success = $client->query ($flags, "text", \&cb) @@ -2190,8 +2284,8 @@ =head2 EXTENSION DATABASE SUPPORT Crossfire maintains a very simple database for extension use. It can -currently store anything that can be serialised using Storable, which -excludes objects. +currently store binary data only (use Compress::LZF::sfreeze_cr/sthaw to +convert to/from binary). The parameter C<$family> should best start with the name of the extension using it, it should be unique. @@ -2224,20 +2318,6 @@ }; cf::cleanup "db_open(db): $@" if $@; }; - - my $path = cf::localdir . "/database.pst"; - if (stat $path) { - cf::sync_job { - my $pst = Storable::retrieve $path; - - cf::db_put (board => data => $pst->{board}); - cf::db_put (guildrules => data => $pst->{guildrules}); - cf::db_put (rent => balance => $pst->{rent}{balance}); - BDB::db_env_txn_checkpoint $DB_ENV; - - unlink $path; - }; - } } } @@ -2248,63 +2328,214 @@ BDB::db_get $DB, undef, $key, my $data; $! ? () - : Compress::LZF::sthaw $data + : $data } } sub db_put($$$) { BDB::dbreq_pri 4; - BDB::db_put $DB, undef, "$_[0]/$_[1]", Compress::LZF::sfreeze_cr $_[2], 0, sub { }; + BDB::db_put $DB, undef, "$_[0]/$_[1]", $_[2], 0, sub { }; +} + +=item cf::cache $id => [$paths...], $processversion => $process + +Generic caching function that returns the value of the resource $id, +caching and regenerating as required. + +This function can block. + +=cut + +sub cache { + my ($id, $src, $processversion, $process) = @_; + + my $meta = + join "\x00", + $processversion, + map { + aio_stat $_ + and Carp::croak "$_: $!"; + + ($_, (stat _)[7,9]) + } @$src; + + my $dbmeta = db_get cache => "$id/meta"; + if ($dbmeta ne $meta) { + # changed, we may need to process + + my @data; + my $md5; + + for (0 .. $#$src) { + 0 <= aio_load $src->[$_], $data[$_] + or Carp::croak "$src->[$_]: $!"; + } + + # if processing is expensive, check + # checksum first + if (1) { + $md5 = + join "\x00", + $processversion, + map { + Coro::cede; + ($src->[$_], Digest::MD5::md5_hex $data[$_]) + } 0.. $#$src; + + + my $dbmd5 = db_get cache => "$id/md5"; + if ($dbmd5 eq $md5) { + db_put cache => "$id/meta", $meta; + + return db_get cache => "$id/data"; + } + } + + my $t1 = Time::HiRes::time; + my $data = $process->(\@data); + my $t2 = Time::HiRes::time; + + warn "cache: '$id' processed in ", $t2 - $t1, "s\n"; + + db_put cache => "$id/data", $data; + db_put cache => "$id/md5" , $md5; + db_put cache => "$id/meta", $meta; + + return $data; + } + + db_get cache => "$id/data" +} + +=item fork_call { }, $args + +Executes the given code block with the given arguments in a seperate +process, returning the results. Everything must be serialisable with +Coro::Storable. May, of course, block. Note that the executed sub may +never block itself or use any form of Event handling. + +=cut + +sub fork_call(&@) { + my ($cb, @args) = @_; + +# socketpair my $fh1, my $fh2, Socket::AF_UNIX, Socket::SOCK_STREAM, Socket::PF_UNSPEC +# or die "socketpair: $!"; + pipe my $fh1, my $fh2 + or die "pipe: $!"; + + if (my $pid = fork) { + close $fh2; + + my $res = (Coro::Handle::unblock $fh1)->readline (undef); + $res = Coro::Storable::thaw $res; + + waitpid $pid, 0; # should not block anymore, we expect the child to simply behave + + die $$res unless "ARRAY" eq ref $res; + + return wantarray ? @$res : $res->[-1]; + } else { + local $SIG{__WARN__}; + eval { + local $SIG{__DIE__}; + close $fh1; + + my @res = eval { $cb->(@args) }; + syswrite $fh2, Coro::Storable::freeze +($@ ? \"$@" : \@res); + }; + + warn $@ if $@; + _exit 0; + } } + + ############################################################################# # the server's init and main functions -sub load_facedata { - my $path = sprintf "%s/facedata", cf::datadir; +sub load_facedata($) { + my ($path) = @_; warn "loading facedata from $path\n"; - my $faces; - 0 < aio_load $path, $faces + my $facedata; + 0 < aio_load $path, $facedata or die "$path: $!"; - Coro::cede; - $faces = Storable::thaw $faces; - Coro::cede; + $facedata = Coro::Storable::thaw $facedata; - my $meta = delete $faces->{""}; - $meta->{version} == 1 + $facedata->{version} == 2 or cf::cleanup "$path: version mismatch, cannot proceed."; - while (my ($face, $info) = each %$faces) { - my $idx = (cf::face::find $face) || cf::face::alloc $face; - cf::face::set $idx, $info->{visibility}, $info->{magicmap}; - cf::face::set_data $idx, 0, $info->{data32}, $info->{chksum32}; - cf::face::set_data $idx, 1, $info->{data64}, $info->{chksum64}; - Coro::cede; + { + my $faces = $facedata->{faceinfo}; + + while (my ($face, $info) = each %$faces) { + my $idx = (cf::face::find $face) || cf::face::alloc $face; + cf::face::set $idx, $info->{visibility}, $info->{magicmap}; + cf::face::set_data $idx, 0, $info->{data32}, $info->{chksum32}; + cf::face::set_data $idx, 1, $info->{data64}, $info->{chksum64}; + Coro::cede; + } + + while (my ($face, $info) = each %$faces) { + next unless $info->{smooth}; + my $idx = cf::face::find $face + or next; + if (my $smooth = cf::face::find $info->{smooth}) { + cf::face::set_smooth $idx, $smooth, $info->{smoothlevel}; + } else { + warn "smooth face '$info->{smooth}' not found for face '$face'"; + } + Coro::cede; + } } - while (my ($face, $info) = each %$faces) { - next unless $info->{smooth}; - my $idx = cf::face::find $face - or next; - if (my $smooth = cf::face::find $info->{smooth}) { - cf::face::set_smooth $idx, $smooth; - } else { - warn "smooth face '$info->{smooth}' not found for face '$face'"; + { + my $anims = $facedata->{animinfo}; + + while (my ($anim, $info) = each %$anims) { + cf::anim::set $anim, $info->{frames}, $info->{facings}; + Coro::cede; } - Coro::cede; + + cf::anim::invalidate_all; # d'oh } 1 } +sub reload_regions { + load_resource_file "$MAPDIR/regions" + or die "unable to load regions file\n"; +} + +sub reload_facedata { + load_facedata "$DATADIR/facedata" + or die "unable to load facedata\n"; +} + +sub reload_archetypes { + load_resource_file "$DATADIR/archetypes" + or die "unable to load archetypes\n"; +} + +sub reload_treasures { + load_resource_file "$DATADIR/treasures" + or die "unable to load treasurelists\n"; +} + sub reload_resources { - load_resource_file sprintf "%s/%s/regions", cf::datadir, cf::mapdir - or die "unable to load regions file\n";#d# - load_facedata - or die "unable to load facedata\n";#d# + warn "reloading resource files...\n"; + + reload_regions; + reload_facedata; + reload_archetypes; + reload_treasures; + + warn "finished reloading resource files\n"; } sub init { @@ -2312,7 +2543,7 @@ } sub cfg_load { - open my $fh, "<:utf8", cf::confdir . "/config" + open my $fh, "<:utf8", "$CONFDIR/config" or return; local $/; @@ -2407,7 +2638,7 @@ if $make_core; } -sub reload() { +sub do_reload_perl() { # can/must only be called in main if ($Coro::current != $Coro::main) { warn "can only reload from main coroutine"; @@ -2492,6 +2723,7 @@ warn "unloading cf.pm \"a bit\""; delete $INC{"cf.pm"}; + delete $INC{"cf/pod.pm"}; # don't, removes xs symbols, too, # and global variables created in xs @@ -2516,9 +2748,6 @@ warn "reattaching attachments to players"; reattach $_ for values %PLAYER; - warn "loading reloadable resources"; - reload_resources; - warn "leaving sync_job"; 1 @@ -2533,24 +2762,27 @@ our $RELOAD_WATCHER; # used only during reload +sub reload_perl() { + # doing reload synchronously and two reloads happen back-to-back, + # coro crashes during coro_state_free->destroy here. + + $RELOAD_WATCHER ||= Event->timer ( + reentrant => 0, + after => 0, + data => WF_AUTOCANCEL, + cb => sub { + do_reload_perl; + undef $RELOAD_WATCHER; + }, + ); +} + register_command "reload" => sub { my ($who, $arg) = @_; if ($who->flag (FLAG_WIZ)) { $who->message ("reloading server."); - - # doing reload synchronously and two reloads happen back-to-back, - # coro crashes during coro_state_free->destroy here. - - $RELOAD_WATCHER ||= Event->timer ( - reentrant => 0, - after => 0, - data => WF_AUTOCANCEL, - cb => sub { - reload; - undef $RELOAD_WATCHER; - }, - ); + async { reload_perl }; } }; @@ -2558,6 +2790,27 @@ my $bug_warning = 0; +our @WAIT_FOR_TICK; +our @WAIT_FOR_TICK_BEGIN; + +sub wait_for_tick { + return unless $TICK_WATCHER->is_active; + return if $Coro::current == $Coro::main; + + my $signal = new Coro::Signal; + push @WAIT_FOR_TICK, $signal; + $signal->wait; +} + +sub wait_for_tick_begin { + return unless $TICK_WATCHER->is_active; + return if $Coro::current == $Coro::main; + + my $signal = new Coro::Signal; + push @WAIT_FOR_TICK_BEGIN, $signal; + $signal->wait; +} + $TICK_WATCHER = Event->timer ( reentrant => 0, parked => 1, @@ -2574,6 +2827,7 @@ $NOW = Event::time; cf::server_tick; # one server iteration + $RUNTIME += $TICK; $NEXT_TICK += $TICK; @@ -2585,9 +2839,6 @@ }; } - $WAIT_FOR_TICK->broadcast; - $WAIT_FOR_TICK_ONE->send if $WAIT_FOR_TICK_ONE->awaited; - # my $AFTER = Event::time; # warn $AFTER - $NOW;#d# @@ -2596,6 +2847,15 @@ $TICK_WATCHER->at ($NEXT_TICK); $TICK_WATCHER->start; + + if (my $sig = shift @WAIT_FOR_TICK_BEGIN) { + $sig->send; + } + while (my $sig = shift @WAIT_FOR_TICK) { + $sig->send; + } + + _post_tick; }, ); @@ -2633,12 +2893,12 @@ eval { BDB::db_env_open $DB_ENV, - $BDB_ENV_DIR, + $BDBDIR, BDB::INIT_LOCK | BDB::INIT_LOG | BDB::INIT_MPOOL | BDB::INIT_TXN | BDB::RECOVER | BDB::REGISTER | BDB::USE_ENVIRON | BDB::CREATE, 0666; - cf::cleanup "db_env_open($BDB_ENV_DIR): $!" if $!; + cf::cleanup "db_env_open($BDBDIR): $!" if $!; $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT | BDB::TXN_NOSYNC, 1); $DB_ENV->set_lk_detect; @@ -2664,6 +2924,9 @@ ); } +# load additional modules +use cf::pod; + END { cf::emergency_save } 1