--- deliantra/server/lib/cf.pm 2007/05/22 10:50:00 1.268 +++ deliantra/server/lib/cf.pm 2007/07/12 08:40:14 1.305 @@ -22,6 +22,7 @@ use Coro::AIO; use Coro::Storable; +use JSON::XS 1.4 (); use BDB (); use Data::Dumper; use Digest::MD5; @@ -30,6 +31,7 @@ use IO::AIO 2.32 (); use Time::HiRes; use Compress::LZF; +use Digest::MD5 (); # configure various modules to our taste # @@ -51,6 +53,7 @@ our @EXTS = (); # list of extension package names our %EXTCMD = (); +our %EXTICMD = (); our %EXT_CORO = (); # coroutines bound to extensions our %EXT_MAP = (); # pluggable maps @@ -189,7 +192,8 @@ @safe::cf::player::ISA = @cf::player::ISA = 'cf::attachable'; @safe::cf::client::ISA = @cf::client::ISA = 'cf::attachable'; @safe::cf::map::ISA = @cf::map::ISA = 'cf::attachable'; -@safe::cf::object::player::ISA = @cf::object::player::ISA = 'cf::object'; +@safe::cf::arch::ISA = @cf::arch::ISA = 'cf::object'; +@safe::cf::object::player::ISA = @cf::object::player::ISA = 'cf::object'; # not really true (yet) # we bless all objects into (empty) derived classes to force a method lookup # within the Safe compartment. @@ -208,6 +212,8 @@ warn "error in event callback: @_"; }; +############################################################################# + =head2 UTILITY FUNCTIONS =over 4 @@ -235,8 +241,6 @@ } || "[unable to dump $_[0]: '$@']"; } -use JSON::XS qw(to_json from_json); # TODO# replace by JSON::PC once working - =item $ref = cf::from_json $json Converts a JSON string into the corresponding perl data structure. @@ -245,6 +249,13 @@ Converts a perl data structure into its JSON representation. +=cut + +our $json_coder = JSON::XS->new->utf8->max_size (1e6); # accept ~1mb max + +sub to_json ($) { $json_coder->encode ($_[0]) } +sub from_json ($) { $json_coder->decode ($_[0]) } + =item cf::lock_wait $string Wait until the given lock is available. See cf::lock_acquire. @@ -319,9 +330,10 @@ =item cf::sync_job { BLOCK } -The design of crossfire+ requires that the main coro ($Coro::main) is -always able to handle events or runnable, as crossfire+ is only partly -reentrant. Thus "blocking" it by e.g. waiting for I/O is not acceptable. +The design of Crossfire TRT requires that the main coroutine ($Coro::main) +is always able to handle events or runnable, as Crossfire TRT is only +partly reentrant. Thus "blocking" it by e.g. waiting for I/O is not +acceptable. If it must be done, put the blocking parts into C. This will run the given BLOCK in another coroutine while waiting for the result. The @@ -391,44 +403,182 @@ $coro } -sub write_runtime { - my $runtime = "$LOCALDIR/runtime"; +=item fork_call { }, $args - # first touch the runtime file to show we are still running: - # the fsync below can take a very very long time. +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. - if (my $fh = aio_open $runtime, O_WRONLY, 0) { - utime undef, undef, $fh; +=cut + +sub _store_scalar { + open my $fh, ">", \my $buf + or die "fork_call: cannot open fh-to-buf in child : $!"; + Storable::store_fd $_[0], $fh; + close $fh; + + $buf +} + +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); + warn "pst<$res>" unless $res =~ /^pst/; + $res = Coro::Storable::thaw $res; + + waitpid $pid, 0; # should not block anymore, we expect the child to simply behave + + Carp::confess $$res unless "ARRAY" eq ref $res; + + return wantarray ? @$res : $res->[-1]; + } else { + reset_signals; + local $SIG{__WARN__}; + local $SIG{__DIE__}; + # just in case, this hack effectively disables event + # in the child. cleaner and slower would be canceling all watchers, + # but this works for the time being. + local $Coro::idle; + $Coro::current->prio (Coro::PRIO_MAX); + + eval { + close $fh1; + + my @res = eval { $cb->(@args) }; + + syswrite $fh2, _store_scalar $@ ? \"$@" : \@res; + close $fh2; + }; + + warn $@ if $@; + _exit 0; } +} - my $guard = cf::lock_acquire "write_runtime"; +=item $value = cf::db_get $family => $key - my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 - or return; +Returns a single value from the environment database. - my $value = $cf::RUNTIME + 90 + 10; - # 10 is the runtime save interval, for a monotonic clock - # 60 allows for the watchdog to kill the server. +=item cf::db_put $family => $key => $value - (aio_write $fh, 0, (length $value), $value, 0) <= 0 - and return; +Stores the given C<$value> in the family. It can currently store binary +data only (use Compress::LZF::sfreeze_cr/sthaw to convert to/from binary). - # always fsync - this file is important - aio_fsync $fh - and return; +=cut - # touch it again to show we are up-to-date - utime undef, undef, $fh; +our $DB; - close $fh - or return; +sub db_init { + unless ($DB) { + $DB = BDB::db_create $DB_ENV; - aio_rename "$runtime~", $runtime - and return; + cf::sync_job { + eval { + $DB->set_flags (BDB::CHKSUM); - warn "runtime file written.\n";#d# + BDB::db_open $DB, undef, "db", undef, BDB::BTREE, + BDB::CREATE | BDB::AUTO_COMMIT, 0666; + cf::cleanup "db_open(db): $!" if $!; + }; + cf::cleanup "db_open(db): $@" if $@; + }; + } +} - 1 +sub db_get($$) { + my $key = "$_[0]/$_[1]"; + + cf::sync_job { + BDB::db_get $DB, undef, $key, my $data; + + $! ? () + : $data + } +} + +sub db_put($$$) { + BDB::dbreq_pri 4; + 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 cf::datalog type => key => value, ... @@ -456,7 +606,7 @@ In the following description, CLASS can be any of C, C C, C or C (i.e. the attachable objects in -crossfire+). +Crossfire TRT). =over 4 @@ -668,7 +818,7 @@ $obj->{$name} = \%arg; } else { - warn "object uses attachment '$name' that is not available, postponing.\n"; + warn "object uses attachment '$name' which is not available, postponing.\n"; } $obj->{_attachment}{$name} = undef; @@ -876,10 +1026,16 @@ } warn sprintf "loading %s (%d)\n", - $filename, length $data, scalar @{$av || []};#d# + $filename, length $data, scalar @{$av || []}; return ($data, $av); } +=head2 COMMAND CALLBACKS + +=over 4 + +=cut + ############################################################################# # command handling &c @@ -901,7 +1057,22 @@ =item cf::register_extcmd $name => \&callback($pl,$packet); -Register a callbackf ro execution when the client sends an extcmd packet. +Register a callback for execution when the client sends an (synchronous) +extcmd packet. Ext commands will be processed in the order they are +received by the server, like other user commands. The first argument is +the logged-in player. Ext commands can only be processed after a player +has logged in successfully. + +If the callback returns something, it is sent back as if reply was being +called. + +=item cf::register_exticmd $name => \&callback($ns,$packet); + +Register a callback for execution when the client sends an (asynchronous) +exticmd packet. Exti commands are processed by the server as soon as they +are received, i.e. out of order w.r.t. other commands. The first argument +is a client socket. Exti commands can be received anytime, even before +log-in. If the callback returns something, it is sent back as if reply was being called. @@ -914,6 +1085,12 @@ $EXTCMD{$name} = $cb; } +sub register_exticmd { + my ($name, $cb) = @_; + + $EXTICMD{$name} = $cb; +} + cf::player->attach ( on_command => sub { my ($pl, $name, $params) = @_; @@ -930,7 +1107,7 @@ on_extcmd => sub { my ($pl, $buf) = @_; - my $msg = eval { from_json $buf }; + my $msg = eval { $pl->ns->{json_coder}->decode ($buf) }; if (ref $msg) { if (my $cb = $EXTCMD{$msg->{msgtype}}) { @@ -946,52 +1123,83 @@ }, ); -sub load_extension { - my ($path) = @_; +sub load_extensions { + cf::sync_job { + my %todo; - $path =~ /([^\/\\]+)\.ext$/ or die "$path"; - my $base = $1; - my $pkg = $1; - $pkg =~ s/[^[:word:]]/_/g; - $pkg = "ext::$pkg"; + for my $path (<$LIBDIR/*.ext>) { + next unless -r $path; - warn "... loading '$path' into '$pkg'\n"; + $path =~ /([^\/\\]+)\.ext$/ or die "$path"; + my $base = $1; + my $pkg = $1; + $pkg =~ s/[^[:word:]]/_/g; + $pkg = "ext::$pkg"; - open my $fh, "<:utf8", $path - or die "$path: $!"; + open my $fh, "<:utf8", $path + or die "$path: $!"; + + my $source = do { local $/; <$fh> }; + + my %ext = ( + path => $path, + base => $base, + pkg => $pkg, + ); + + $ext{meta} = { map { (split /=/, $_, 2)[0, 1] } split /\s+/, $1 } + if $source =~ /\A#!.*?perl.*?#\s*(.*)$/m; + + $ext{source} = + "package $pkg; use strict; use utf8;\n" + . "#line 1 \"$path\"\n{\n" + . $source + . "\n};\n1"; - my $source = - "package $pkg; use strict; use utf8;\n" - . "#line 1 \"$path\"\n{\n" - . (do { local $/; <$fh> }) - . "\n};\n1"; - - unless (eval $source) { - my $msg = $@ ? "$path: $@\n" - : "extension disabled.\n"; - if ($source =~ /^#!.*perl.*#.*MANDATORY/m) { # ugly match - warn $@; - warn "mandatory extension failed to load, exiting.\n"; - exit 1; + $todo{$base} = \%ext; } - die $@; - } - push @EXTS, $pkg; -} + my %done; + while (%todo) { + my $progress; -sub load_extensions { - for my $ext (<$LIBDIR/*.ext>) { - next unless -r $ext; - eval { - load_extension $ext; - 1 - } or warn "$ext not loaded: $@"; - } + while (my ($k, $v) = each %todo) { + for (split /,\s*/, $v->{meta}{depends}) { + goto skip + unless exists $done{$_}; + } + + warn "... loading '$k' into '$v->{pkg}'\n"; + + unless (eval $v->{source}) { + my $msg = $@ ? "$v->{path}: $@\n" + : "$v->{base}: extension inactive.\n"; + + if (exists $v->{meta}{mandatory}) { + warn $msg; + warn "mandatory extension failed to load, exiting.\n"; + exit 1; + } + + warn $msg; + } + + $done{$k} = delete $todo{$k}; + push @EXTS, $v->{pkg}; + $progress = 1; + } + + skip: + die "cannot load " . (join ", ", keys %todo) . ": unable to resolve dependencies\n" + unless $progress; + } + }; } ############################################################################# +=back + =head2 CORE EXTENSIONS Functions and methods that extend core crossfire objects. @@ -1188,6 +1396,56 @@ \@paths } +=item $protocol_xml = $player->expand_cfpod ($crossfire_pod) + +Expand crossfire pod fragments into protocol xml. + +=cut + +sub expand_cfpod { + ((my $self), (local $_)) = @_; + + # escape & and < + s/&/&/g; + s/(?, I<>, U<> etc. + s/B<([^\>]*)>/$1<\/b>/ + || s/I<([^\>]*)>/$1<\/i>/ + || s/U<([^\>]*)>/$1<\/u>/ + # replace G tags + || s{G<([^>|]*)\|([^>]*)>}{ + $self->gender ? $2 : $1 + }ge + # replace H + || s{H<([^\>]*)>} + { + ("[$1 (Use hintmode to suppress hints)]", + "[Hint suppressed, see hintmode]", + "") + [$self->{hintmode}] + }ge; + + # create single paragraphs (very hackish) + s/(?<=\S)\n(?=\w)/ /g; + + # compress some whitespace + s/\s+\n/\n/g; # ws line-ends + s/\n\n+/\n/g; # double lines + s/^\n+//; # beginning lines + s/\n+$//; # ending lines + + $_ +} + +sub hintmode { + $_[0]{hintmode} = $_[1] if @_ > 1; + $_[0]{hintmode} +} + =item $player->ext_reply ($msgid, %msg) Sends an ext reply to the player. @@ -1198,8 +1456,7 @@ my ($self, $id, %msg) = @_; $msg{msgid} = $id; - - $self->send ("ext " . cf::to_json \%msg); + $self->send ("ext " . $self->ns->{json_coder}->encode (\%msg)); } =item $player->ext_event ($type, %msg) @@ -1224,7 +1481,7 @@ =item cf::region::find_by_path $path -Tries to decuce the probable region for a map knowing only its path. +Tries to decuce the likely region for a map knowing only its path. =cut @@ -1234,7 +1491,7 @@ my ($match, $specificity); for my $region (list) { - if ($region->match && $path =~ $region->match) { + if ($region->{match} && $path =~ $region->{match}) { ($match, $specificity) = ($region, $region->specificity) if $region->specificity > $specificity; } @@ -1308,8 +1565,6 @@ # we have to keep some variables in memory intact local $self->{path}; local $self->{load_path}; - local $self->{deny_save}; - local $self->{deny_reset}; $self->SUPER::thawer_merge ($merge); } @@ -1419,6 +1674,14 @@ for grep $_->outdoor, values %cf::MAP; } +sub decay_objects { + my ($self) = @_; + + return if $self->{deny_reset}; + + $self->do_decay_objects; +} + sub unlink_save { my ($self) = @_; @@ -1484,6 +1747,9 @@ $self->prepare_orig; } + $self->{deny_reset} = 1 + if $self->no_reset; + $self->default_region (cf::region::find_by_path $self->{path}) unless $self->default_region; @@ -1508,7 +1774,7 @@ $map->load_header or return; - if ($map->should_reset && 0) {#d#TODO# disabled, crashy (locking issue?) + if ($map->should_reset) {#d#TODO# disabled, crashy (locking issue?) # doing this can freeze the server in a sync job, obviously #$cf::WAIT_FOR_TICK->wait; $map->reset; @@ -1585,6 +1851,9 @@ return find "~" . $ob->name . "/" . $self->{path} if $self->per_player; +# return find "?party/" . $ob->name . "/" . $self->{path} +# if $self->per_party; + $self } @@ -1704,7 +1973,6 @@ my ($self) = @_; # TODO: safety, remove and allow resettable per-player maps - return 1e99 if $self->isa ("ext::map_per_player");#d# return 1e99 if $self->{deny_reset}; my $time = $self->fixed_resettime ? $self->{instantiate_time} : $self->last_access; @@ -1725,9 +1993,8 @@ my $lock = cf::lock_acquire "map_data:$self->{path}"; return if $self->players; - return if $self->isa ("ext::map_per_player");#d# - warn "resetting map ", $self->path;#d# + warn "resetting map ", $self->path; $self->in_memory (cf::MAP_SWAPPED); @@ -1769,28 +2036,58 @@ $self->reset; # polite request, might not happen } -=item cf::map::unique_maps +=item $maps = cf::map::tmp_maps -Returns an arrayref of paths of all shared maps that have -instantiated unique items. May block. +Returns an arrayref with all map paths of currently instantiated and saved +maps. May block. =cut -sub unique_maps() { - my $files = aio_readdir $UNIQUEDIR - or return; +sub tmp_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise $_ + : () + } @{ aio_readdir $TMPDIR or [] } + ] +} - my @paths; +=item $maps = cf::map::random_maps - for (@$files) { - utf8::decode $_; - next if /\.pst$/; - next unless /^$PATH_SEP/o; +Returns an arrayref with all map paths of currently instantiated and saved +random maps. May block. - push @paths, cf::map::normalise $_; - } +=cut - \@paths +sub random_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise "?random/$_" + : () + } @{ aio_readdir $RANDOMDIR or [] } + ] +} + +=item cf::map::unique_maps + +Returns an arrayref of paths of all shared maps that have +instantiated unique items. May block. + +=cut + +sub unique_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise $_ + : () + } @{ aio_readdir $UNIQUEDIR or [] } + ] } package cf; @@ -1846,9 +2143,23 @@ if ($self->{record_replies}) { push @{ $self->{record_replies} }, [$npc, $msg, $flags]; + } else { - $msg = $npc->name . " says: $msg" if $npc; - $self->message ($msg, $flags); + my $pl = $self->contr; + + if ($pl->{npc_dialog} && $pl->{npc_dialog}->{id}) { + my $diag = $pl->{npc_dialog}; + $diag->{pl}->ext_reply ( + $diag->{id}, + msgtype => "reply", + msg => $diag->{pl}->expand_cfpod ($msg), + add_topics => [] + ); + + } else { + $msg = $npc->name . " says: $msg" if $npc; + $self->message ($msg, $flags); + } } } @@ -1906,7 +2217,7 @@ return if UNIVERSAL::isa $self->map, "ext::map_link"; $self->{_link_pos} ||= [$self->map->{path}, $self->x, $self->y] - if $self->map; + if $self->map && $self->map->{path} ne "{link}"; $self->enter_map ($LINK_MAP || link_map, 10, 10); } @@ -1914,6 +2225,8 @@ sub cf::object::player::leave_link { my ($self, $map, $x, $y) = @_; + return unless $self->contr->active; + my $link_pos = delete $self->{_link_pos}; unless ($map) { @@ -1945,38 +2258,6 @@ $self->enter_map ($map, $x, $y); } -cf::player->attach ( - on_logout => sub { - my ($pl) = @_; - - # abort map switching before logout - if ($pl->ob->{_link_pos}) { - cf::sync_job { - $pl->ob->leave_link - }; - } - }, - on_login => sub { - my ($pl) = @_; - - # try to abort aborted map switching on player login :) - # should happen only on crashes - if ($pl->ob->{_link_pos}) { - $pl->ob->enter_link; - (async { - $pl->ob->reply (undef, - "There was an internal problem at your last logout, " - . "the server will try to bring you to your intended destination in a second.", - cf::NDI_RED); - # we need this sleep as the login has a concurrent enter_exit running - # and this sleep increases chances of the player not ending up in scorn - Coro::Timer::sleep 1; - $pl->ob->leave_link; - })->prio (2); - } - }, -); - =item $player_object->goto ($path, $x, $y[, $check->($map)]) Moves the player to the given map-path and coordinates by first freezing @@ -1986,14 +2267,13 @@ =cut +our $GOTOGEN; + sub cf::object::player::goto { my ($self, $path, $x, $y, $check) = @_; - #d# #TODO# - if ($check && !ref $check) { - warn Carp::longmess "goto called with non-ref check argument";#d# - undef $check; - } + # do generation counting so two concurrent goto's will be executed in-order + my $gen = $self->{_goto_generation} = ++$GOTOGEN; $self->enter_link; @@ -2016,7 +2296,10 @@ LOG llevError | logBacktrace, Carp::longmess $@; } - $self->leave_link ($map, $x, $y); + if ($gen == $self->{_goto_generation}) { + delete $self->{_goto_generation}; + $self->leave_link ($map, $x, $y); + } })->prio (1); } @@ -2095,6 +2378,10 @@ $self->enter_link; + # if exit is damned, update players death & WoR home-position + $self->contr->savebed ($slaying, $hp, $sp) + if $exit->flag (FLAG_DAMNED); + (async { $self->deactivate_recursive; # just to be sure unless (eval { @@ -2131,17 +2418,59 @@ $self->send_packet (sprintf "drawinfo %d %s", $flags || cf::NDI_BLACK, $text); } +=item $client->send_msg ($color, $type, $msg, [extra...]) + +Send a drawinfo or msg packet to the client, formatting the msg for the +client if neccessary. C<$type> should be a string identifying the type of +the message, with C being the default. If C<$color> is negative, suppress +the message unless the client supports the msg packet. + +=cut + +sub cf::client::send_msg { + my ($self, $color, $type, $msg, @extra) = @_; + + $msg = $self->pl->expand_cfpod ($msg); + + return unless @extra || length $msg; + + if ($self->can_msg) { + $self->send_packet ("msg " . $self->{json_coder}->encode ([$color, $type, $msg, @extra])); + } else { + # replace some tags by gcfclient-compatible ones + for ($msg) { + 1 while + s/([^<]*)<\/b>/[b]${1}[\/b]/ + || s/([^<]*)<\/i>/[i]${1}[\/i]/ + || s/([^<]*)<\/u>/[ul]${1}[\/ul]/ + || s/([^<]*)<\/tt>/[fixed]${1}[\/fixed]/ + || s/([^<]*)<\/fg>/[color=$1]${2}[\/color]/; + } + + if ($color >= 0) { + if (0 && $msg =~ /\[/) { + $self->send_packet ("drawextinfo $color 4 0 $msg") + } else { + $msg =~ s/\[\/?(?:b|i|u|fixed|color)[^\]]*\]//g; + $self->send_packet ("drawinfo $color $msg") + } + } + } +} + =item $client->ext_event ($type, %msg) -Sends an exti event to the client. +Sends an ext event to the client. =cut sub cf::client::ext_event($$%) { my ($self, $type, %msg) = @_; + return unless $self->extcmd; + $msg{msgtype} = "event_$type"; - $self->send_packet ("ext " . cf::to_json \%msg); + $self->send_packet ("ext " . $self->{json_coder}->encode (\%msg)); } =item $success = $client->query ($flags, "text", \&cb) @@ -2150,8 +2479,8 @@ the reply text on a reply. flags can be C, C or C or C<0>. -Queries can fail, so check the return code. Or don't, as queries will become -reliable at some point in the future. +Queries can fail, so check the return code. Or don't, as queries will +become reliable at some point in the future. =cut @@ -2169,9 +2498,16 @@ $self->send_packet ($self->{query_queue}[0][0]) if @{ $self->{query_queue} } == 1; + + 1 } cf::client->attach ( + on_connect => sub { + my ($ns) = @_; + + $ns->{json_coder} = JSON::XS->new->utf8->max_size (1e6)->convert_blessed; + }, on_reply => sub { my ($ns, $msg) = @_; @@ -2193,6 +2529,24 @@ } } }, + on_exticmd => sub { + my ($ns, $buf) = @_; + + my $msg = eval { $ns->{json_coder}->decode ($buf) }; + + if (ref $msg) { + if (my $cb = $EXTICMD{$msg->{msgtype}}) { + if (my %reply = $cb->($ns, $msg)) { + $reply{msgid} = $msg->{msgid}; + $ns->send ("ext " . $ns->{json_coder}->encode (\%reply)); + } + } + } else { + warn "client " . ($ns->pl ? $ns->pl->ob->name : $ns->host) . " sent unparseable exti message: <$buf>\n"; + } + + cf::override; + }, ); =item $client->async (\&cb) @@ -2251,15 +2605,24 @@ The following functions and methods are available within a safe environment: - cf::object contr pay_amount pay_player map - cf::object::player player - cf::player peaceful - cf::map trigger + cf::object + contr pay_amount pay_player map x y force_find force_add + insert remove + + cf::object::player + player + + cf::player + peaceful + + cf::map + trigger =cut for ( - ["cf::object" => qw(contr pay_amount pay_player map)], + ["cf::object" => qw(contr pay_amount pay_player map force_find force_add x y + insert remove)], ["cf::object::player" => qw(player)], ["cf::player" => qw(peaceful)], ["cf::map" => qw(trigger)], @@ -2340,180 +2703,6 @@ =cut ############################################################################# - -=head2 EXTENSION DATABASE SUPPORT - -Crossfire maintains a very simple database for extension use. It can -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. - -=over 4 - -=item $value = cf::db_get $family => $key - -Returns a single value from the database. - -=item cf::db_put $family => $key => $value - -Stores the given C<$value> in the family. - -=cut - -our $DB; - -sub db_init { - unless ($DB) { - $DB = BDB::db_create $DB_ENV; - - cf::sync_job { - eval { - $DB->set_flags (BDB::CHKSUM); - - BDB::db_open $DB, undef, "db", undef, BDB::BTREE, - BDB::CREATE | BDB::AUTO_COMMIT, 0666; - cf::cleanup "db_open(db): $!" if $!; - }; - cf::cleanup "db_open(db): $@" if $@; - }; - } -} - -sub db_get($$) { - my $key = "$_[0]/$_[1]"; - - cf::sync_job { - BDB::db_get $DB, undef, $key, my $data; - - $! ? () - : $data - } -} - -sub db_put($$$) { - BDB::dbreq_pri 4; - 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 { - reset_signals; - local $SIG{__WARN__}; - local $SIG{__DIE__}; - eval { - 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($) { @@ -2535,10 +2724,12 @@ 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_visibility $idx, $info->{visibility}; + cf::face::set_magicmap $idx, $info->{magicmap}; cf::face::set_data $idx, 0, $info->{data32}, $info->{chksum32}; cf::face::set_data $idx, 1, $info->{data64}, $info->{chksum64}; - Coro::cede; + + cf::cede_to_tick; } while (my ($face, $info) = each %$faces) { @@ -2546,11 +2737,13 @@ my $idx = cf::face::find $face or next; if (my $smooth = cf::face::find $info->{smooth}) { - cf::face::set_smooth $idx, $smooth, $info->{smoothlevel}; + cf::face::set_smooth $idx, $smooth; + cf::face::set_smoothlevel $idx, $info->{smoothlevel}; } else { warn "smooth face '$info->{smooth}' not found for face '$face'"; } - Coro::cede; + + cf::cede_to_tick; } } @@ -2559,18 +2752,46 @@ while (my ($anim, $info) = each %$anims) { cf::anim::set $anim, $info->{frames}, $info->{facings}; - Coro::cede; + cf::cede_to_tick; } cf::anim::invalidate_all; # d'oh } + { + # TODO: for gcfclient pleasure, we should give resources + # that gcfclient doesn't grok a >10000 face index. + my $res = $facedata->{resource}; + my $enc = JSON::XS->new->utf8->canonical; + + while (my ($name, $info) = each %$res) { + my $meta = $enc->encode ({ + name => $name, + type => $info->{type}, + copyright => $info->{copyright}, #TODO# + }); + my $data = pack "(w/a*)*", $meta, $info->{data}; + my $chk = Digest::MD5::md5 "$info->{chksum},$meta"; # mangle data checksum and metadata + + my $idx = (cf::face::find $name) || cf::face::alloc $name; + cf::face::set_type $idx, 1; + cf::face::set_data $idx, 0, $data, $chk; + + cf::cede_to_tick; + } + } + 1 } sub reload_regions { load_resource_file "$MAPDIR/regions" or die "unable to load regions file\n"; + + for (cf::region::list) { + $_->{match} = qr/$_->{match}/ + if exists $_->{match}; + } } sub reload_facedata { @@ -2581,6 +2802,11 @@ sub reload_archetypes { load_resource_file "$DATADIR/archetypes" or die "unable to load archetypes\n"; + #d# NEED to laod twice to resolve forward references + # this really needs to be done in an extra post-pass + # (which needs to be synchronous, so solve it differently) + load_resource_file "$DATADIR/archetypes" + or die "unable to load archetypes\n"; } sub reload_treasures { @@ -2593,6 +2819,7 @@ reload_regions; reload_facedata; + #reload_archetypes;#d# reload_archetypes; reload_treasures; @@ -2659,6 +2886,44 @@ } } +sub write_runtime { + my $runtime = "$LOCALDIR/runtime"; + + # first touch the runtime file to show we are still running: + # the fsync below can take a very very long time. + + IO::AIO::aio_utime $runtime, undef, undef; + + my $guard = cf::lock_acquire "write_runtime"; + + my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 + or return; + + my $value = $cf::RUNTIME + 90 + 10; + # 10 is the runtime save interval, for a monotonic clock + # 60 allows for the watchdog to kill the server. + + (aio_write $fh, 0, (length $value), $value, 0) <= 0 + and return; + + # always fsync - this file is important + aio_fsync $fh + and return; + + # touch it again to show we are up-to-date + aio_utime $fh, undef, undef; + + close $fh + or return; + + aio_rename "$runtime~", $runtime + and return; + + warn "runtime file written.\n"; + + 1 +} + sub emergency_save() { my $freeze_guard = cf::freeze_mainloop; @@ -2743,8 +3008,9 @@ warn "removing commands"; %COMMAND = (); - warn "removing ext commands"; - %EXTCMD = (); + warn "removing ext/exti commands"; + %EXTCMD = (); + %EXTICMD = (); warn "unloading/nuking all extensions"; for my $pkg (@EXTS) {