--- deliantra/server/lib/cf.pm 2007/06/04 12:19:08 1.273 +++ deliantra/server/lib/cf.pm 2007/08/30 05:16:09 1.347 @@ -12,7 +12,7 @@ use Safe; use Safe::Hole; -use Coro 3.61 (); +use Coro 3.64 (); use Coro::State; use Coro::Handle; use Coro::Event; @@ -21,7 +21,9 @@ use Coro::Semaphore; use Coro::AIO; use Coro::Storable; +use Coro::Util (); +use JSON::XS (); use BDB (); use Data::Dumper; use Digest::MD5; @@ -30,6 +32,7 @@ use IO::AIO 2.32 (); use Time::HiRes; use Compress::LZF; +use Digest::MD5 (); # configure various modules to our taste # @@ -51,6 +54,7 @@ our @EXTS = (); # list of extension package names our %EXTCMD = (); +our %EXTICMD = (); our %EXT_CORO = (); # coroutines bound to extensions our %EXT_MAP = (); # pluggable maps @@ -166,6 +170,12 @@ 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. +=item @cf::INVOKE_RESULTS + +This array contains the results of the last C call. When +C is called C<@cf::INVOKE_RESULTS> is set to the parameters of +that call. + =back =cut @@ -209,6 +219,8 @@ warn "error in event callback: @_"; }; +############################################################################# + =head2 UTILITY FUNCTIONS =over 4 @@ -236,8 +248,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. @@ -246,6 +256,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. @@ -257,6 +274,9 @@ for example when the coroutine gets canceled), the lock is automatically returned. +Locks are *not* recursive, locking from the same coro twice results in a +deadlocked coro. + Lock names should begin with a unique identifier (for example, cf::map::find uses map_find and cf::map::load uses map_load). @@ -308,6 +328,62 @@ $guard } +=item cf::get_slot $time[, $priority[, $name]] + +Allocate $time seconds of blocking CPU time at priority C<$priority>: +This call blocks and returns only when you have at least C<$time> seconds +of cpu time till the next tick. The slot is only valid till the next cede. + +The optional C<$name> can be used to identify the job to run. It might be +used for statistical purposes and should identify the same time-class. + +Useful for short background jobs. + +=cut + +our @SLOT_QUEUE; +our $SLOT_QUEUE; + +$SLOT_QUEUE->cancel if $SLOT_QUEUE; +$SLOT_QUEUE = Coro::async { + my $signal = new Coro::Signal; + + while () { + next_job: + my $avail = cf::till_tick; + if ($avail > 0.01) { + for (0 .. $#SLOT_QUEUE) { + if ($SLOT_QUEUE[$_][0] < $avail) { + my $job = splice @SLOT_QUEUE, $_, 1, (); + $job->[2]->send; + Coro::cede; + goto next_job; + } + } + } + + if (@SLOT_QUEUE) { + # we do not use wait_For_tick() as it returns immediately when tick is inactive + push @cf::WAIT_FOR_TICK, $signal; + $signal->wait; + } else { + Coro::schedule; + } + } +}; + +sub get_slot($;$$) { + my ($time, $pri, $name) = @_; + + $time = $TICK * .6 if $time > $TICK * .6; + my $sig = new Coro::Signal; + + push @SLOT_QUEUE, [$time, $pri, $sig, $name]; + @SLOT_QUEUE = sort { $b->[1] <=> $a->[1] } @SLOT_QUEUE; + $SLOT_QUEUE->ready; + $sig->wait; +} + =item cf::async { BLOCK } Currently the same as Coro::async_pool, meaning you cannot use @@ -320,9 +396,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 @@ -392,42 +469,144 @@ $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. - IO::AIO::aio_utime $runtime, undef, undef; +=cut - my $guard = cf::lock_acquire "write_runtime"; +sub fork_call(&@) { + my ($cb, @args) = @_; - my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 - or return; + # we seemingly have to make a local copy of the whole thing, + # otherwise perl prematurely frees the stuff :/ + # TODO: investigate and fix (liekly this will be rather laborious) - 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. + my @res = Coro::Util::fork_eval { + reset_signals; + &$cb + }, @args; - (aio_write $fh, 0, (length $value), $value, 0) <= 0 - and return; + wantarray ? @res : $res[-1] +} - # always fsync - this file is important - aio_fsync $fh - and return; +=item $value = cf::db_get $family => $key - # touch it again to show we are up-to-date - aio_utime $fh, undef, undef; +Returns a single value from the environment database. - close $fh - or return; +=item cf::db_put $family => $key => $value - aio_rename "$runtime~", $runtime - 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). - warn "runtime file written.\n";#d# +=cut - 1 +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 { + cf::cede_to_tick; + ($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, ... @@ -455,7 +634,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 @@ -667,7 +846,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; @@ -717,18 +896,18 @@ } our $override; -our @invoke_results = (); # referenced from .xs code. TODO: play tricks with reify and mortals? +our @INVOKE_RESULTS = (); # referenced from .xs code. TODO: play tricks with reify and mortals? sub override { $override = 1; - @invoke_results = (); + @INVOKE_RESULTS = (@_); } sub do_invoke { my $event = shift; my $callbacks = shift; - @invoke_results = (); + @INVOKE_RESULTS = (); local $override; @@ -755,7 +934,7 @@ This API is preliminary (most likely, the EVENT_CLASS_xxx prefix will be removed in future versions), and there is no public API to access override -results (if you must, access C<@cf::invoke_results> directly). +results (if you must, access C<@cf::INVOKE_RESULTS> directly). =back @@ -763,6 +942,17 @@ ############################################################################# # object support +# + +sub _can_merge { + my ($ob1, $ob2) = @_; + + local $Storable::canonical = 1; + my $fob1 = Storable::freeze $ob1; + my $fob2 = Storable::freeze $ob2; + + $fob1 eq $fob2 +} sub reattach { # basically do the same as instantiate, without calling instantiate @@ -875,10 +1065,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 @@ -900,7 +1096,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. @@ -913,6 +1124,12 @@ $EXTCMD{$name} = $cb; } +sub register_exticmd { + my ($name, $cb) = @_; + + $EXTICMD{$name} = $cb; +} + cf::player->attach ( on_command => sub { my ($pl, $name, $params) = @_; @@ -929,14 +1146,23 @@ 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}}) { - if (my %reply = $cb->($pl, $msg)) { - $pl->ext_reply ($msg->{msgid}, %reply); - } + my ($type, $reply, @payload) = + "ARRAY" eq ref $msg + ? @$msg + : ($msg->{msgtype}, $msg->{msgid}, %$msg); # TODO: version 1, remove + + my @reply; + + if (my $cb = $EXTCMD{$type}) { + @reply = $cb->($pl, @payload); } + + $pl->ext_reply ($reply, @reply) + if $reply; + } else { warn "player " . ($pl->ob->name) . " sent unparseable ext message: <$buf>\n"; } @@ -945,52 +1171,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. @@ -1070,7 +1327,7 @@ $pl->{last_save} = $cf::RUNTIME; $pl->save_pl ($path); - Coro::cede; + cf::cede_to_tick; } sub new($) { @@ -1086,6 +1343,16 @@ $self } +=item $player->send_msg ($channel, $msg, $color, [extra...]) + +=cut + +sub send_msg { + my $ns = shift->ns + or return; + $ns->send_msg (@_); +} + =item $pl->quit_character Nukes the player without looking back. If logged in, the connection will @@ -1187,30 +1454,78 @@ \@paths } -=item $player->ext_reply ($msgid, %msg) +=item $protocol_xml = $player->expand_cfpod ($crossfire_pod) -Sends an ext reply to the player. +Expand crossfire pod fragments into protocol xml. =cut -sub ext_reply($$%) { - my ($self, $id, %msg) = @_; +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} +} - $msg{msgid} = $id; +=item $player->ext_reply ($msgid, @msg) + +Sends an ext reply to the player. - $self->send ("ext " . cf::to_json \%msg); +=cut + +sub ext_reply($$@) { + my ($self, $id, @msg) = @_; + + $self->ns->ext_reply ($id, @msg) } -=item $player->ext_event ($type, %msg) +=item $player->ext_msg ($type, @msg) Sends an ext event to the client. =cut -sub ext_event($$%) { - my ($self, $type, %msg) = @_; +sub ext_msg($$@) { + my ($self, $type, @msg) = @_; - $self->ns->ext_event ($type, %msg); + $self->ns->ext_msg ($type, @msg); } =head3 cf::region @@ -1223,7 +1538,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 @@ -1233,7 +1548,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; } @@ -1307,8 +1622,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); } @@ -1365,7 +1678,7 @@ } } - Carp::carp "unable to resolve path '$path' (base '$base')."; + Carp::cluck "unable to resolve path '$path' (base '$base')."; () } @@ -1418,6 +1731,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) = @_; @@ -1483,6 +1804,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; @@ -1507,7 +1831,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; @@ -1530,7 +1854,8 @@ my $path = $self->{path}; { - my $guard = cf::lock_acquire "map_load:$path"; + my $guard1 = cf::lock_acquire "map_data:$path"; + my $guard2 = cf::lock_acquire "map_load:$path"; return if $self->in_memory != cf::MAP_SWAPPED; @@ -1539,7 +1864,7 @@ $self->alloc; $self->pre_load; - Coro::cede; + cf::cede_to_tick; $self->_load_objects ($self->{load_path}, 1) or return; @@ -1555,23 +1880,26 @@ } } - Coro::cede; + cf::cede_to_tick; # now do the right thing for maps $self->link_multipart_objects; $self->difficulty ($self->estimate_difficulty) unless $self->difficulty; - Coro::cede; + cf::cede_to_tick; unless ($self->{deny_activate}) { $self->decay_objects; $self->fix_auto_apply; $self->update_buttons; - Coro::cede; + cf::cede_to_tick; $self->set_darkness_map; - Coro::cede; + cf::cede_to_tick; $self->activate; } + $self->{last_save} = $cf::RUNTIME; + $self->last_access ($cf::RUNTIME); + $self->in_memory (cf::MAP_IN_MEMORY); } @@ -1584,14 +1912,17 @@ return find "~" . $ob->name . "/" . $self->{path} if $self->per_player; +# return find "?party/" . $ob->name . "/" . $self->{path} +# if $self->per_party; + $self } # find and load all maps in the 3x3 area around a map -sub load_diag { +sub load_neighbours { my ($map) = @_; - my @diag; # diagonal neighbours + my @neigh; # diagonal neighbours for (0 .. 3) { my $neigh = $map->tile_path ($_) @@ -1600,12 +1931,14 @@ or next; $neigh->load; - push @diag, [$neigh->tile_path (($_ + 3) % 4), $neigh], - [$neigh->tile_path (($_ + 1) % 4), $neigh]; + push @neigh, + [$neigh->tile_path (($_ + 3) % 4), $neigh], + [$neigh->tile_path (($_ + 1) % 4), $neigh]; } - for (@diag) { - my $neigh = find @$_ + for (grep defined $_->[0], @neigh) { + my ($path, $origin) = @$_; + my $neigh = find $path, $origin or next; $neigh->load; } @@ -1620,6 +1953,9 @@ sub do_load_sync { my ($map) = @_; + cf::LOG cf::llevDebug | cf::logBacktrace, "do_load_sync" + if $Coro::current == $Coro::main; + cf::sync_job { $map->load }; } @@ -1627,23 +1963,24 @@ our $MAP_PREFETCHER = undef; sub find_async { - my ($path, $origin) = @_; + my ($path, $origin, $load) = @_; $path = normalise $path, $origin && $origin->{path}; if (my $map = $cf::MAP{$path}) { - return $map if $map->in_memory == cf::MAP_IN_MEMORY; + return $map if !$load || $map->in_memory == cf::MAP_IN_MEMORY; } - undef $MAP_PREFETCH{$path}; + $MAP_PREFETCH{$path} |= $load; + $MAP_PREFETCHER ||= cf::async { while (%MAP_PREFETCH) { - for my $path (keys %MAP_PREFETCH) { - my $map = find $path - or next; - $map->load; + while (my ($k, $v) = each %MAP_PREFETCH) { + if (my $map = find $k) { + $map->load if $v; + } - delete $MAP_PREFETCH{$path}; + delete $MAP_PREFETCH{$k}; } } undef $MAP_PREFETCHER; @@ -1656,7 +1993,7 @@ sub save { my ($self) = @_; - my $lock = cf::lock_acquire "map_data:" . $self->path; + my $lock = cf::lock_acquire "map_data:$self->{path}"; $self->{last_save} = $cf::RUNTIME; @@ -1689,7 +2026,7 @@ # save first because save cedes $self->save; - my $lock = cf::lock_acquire "map_data:" . $self->path; + my $lock = cf::lock_acquire "map_data:$self->{path}"; return if $self->players; return if $self->in_memory != cf::MAP_IN_MEMORY; @@ -1703,7 +2040,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; @@ -1724,9 +2060,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); @@ -1768,28 +2103,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; @@ -1845,12 +2210,31 @@ 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 $dialog = $pl->{npc_dialog}; + $dialog->{pl}->ext_msg ($dialog->{id}, update => msg => $dialog->{pl}->expand_cfpod ($msg)); + + } else { + $msg = $npc->name . " says: $msg" if $npc; + $self->message ($msg, $flags); + } } } +=item $object->send_msg ($channel, $msg, $color, [extra...]) + +=cut + +sub cf::object::send_msg { + my $pl = shift->contr + or return; + $pl->send_msg (@_); +} + =item $player_object->may ("access") Returns wether the given player is authorized to access resource "access" @@ -1937,7 +2321,7 @@ if $x <=0 && $y <= 0; $map->load; - $map->load_diag; + $map->load_neighbours; return unless $self->contr->active; $self->activate_recursive; @@ -1946,19 +2330,20 @@ $self->enter_map ($map, $x, $y); } -=item $player_object->goto ($path, $x, $y[, $check->($map)]) +=item $player_object->goto ($path, $x, $y[, $check->($map)[, $done->()]]) Moves the player to the given map-path and coordinates by first freezing her, loading and preparing them map, calling the provided $check callback that has to return the map if sucecssful, and then unfreezes the player on -the new (success) or old (failed) map position. +the new (success) or old (failed) map position. In either case, $done will +be called at the end of this process. =cut our $GOTOGEN; sub cf::object::player::goto { - my ($self, $path, $x, $y, $check) = @_; + my ($self, $path, $x, $y, $check, $done) = @_; # do generation counting so two concurrent goto's will be executed in-order my $gen = $self->{_goto_generation} = ++$GOTOGEN; @@ -1988,6 +2373,8 @@ delete $self->{_goto_generation}; $self->leave_link ($map, $x, $y); } + + $done->() if $done; })->prio (1); } @@ -2066,6 +2453,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 { @@ -2102,17 +2493,101 @@ $self->send_packet (sprintf "drawinfo %d %s", $flags || cf::NDI_BLACK, $text); } -=item $client->ext_event ($type, %msg) +=item $client->send_msg ($channel, $msg, $color, [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, $channel, $msg, $color, @extra) = @_; + + $msg = $self->pl->expand_cfpod ($msg); + + $color &= cf::NDI_CLIENT_MASK; # just in case... + + if (ref $channel) { + # send meta info to client, if not yet sent + unless (exists $self->{channel}{$channel->{id}}) { + $self->{channel}{$channel->{id}} = $channel; + $self->ext_msg (channel_info => $channel); + } + + $channel = $channel->{id}; + } + + return unless @extra || length $msg; + + if ($self->can_msg) { + # default colour, mask it out + $color &= ~(cf::NDI_COLOR_MASK | cf::NDI_DEF) + if $color & cf::NDI_DEF; + + $self->send_packet ("msg " . $self->{json_coder}->encode ( + [$color & cf::NDI_CLIENT_MASK, $channel, $msg, @extra])); + } else { + if ($color >= 0) { + # 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]/; + } + + $color &= cf::NDI_COLOR_MASK; + + utf8::encode $msg; + + if (0 && $msg =~ /\[/) { + # COMMAND/INFO + $self->send_packet ("drawextinfo $color 10 8 $msg") + } else { + $msg =~ s/\[\/?(?:b|i|u|fixed|color)[^\]]*\]//g; + $self->send_packet ("drawinfo $color $msg") + } + } + } +} + +=item $client->ext_msg ($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) = @_; +sub cf::client::ext_msg($$@) { + my ($self, $type, @msg) = @_; - $msg{msgtype} = "event_$type"; - $self->send_packet ("ext " . cf::to_json \%msg); + if ($self->extcmd == 2) { + $self->send_packet ("ext " . $self->{json_coder}->encode ([$type, @msg])); + } elsif ($self->extcmd == 1) { # TODO: remove + push @msg, msgtype => "event_$type"; + $self->send_packet ("ext " . $self->{json_coder}->encode ({@msg})); + } +} + +=item $client->ext_reply ($msgid, @msg) + +Sends an ext reply to the client. + +=cut + +sub cf::client::ext_reply($$@) { + my ($self, $id, @msg) = @_; + + if ($self->extcmd == 2) { + $self->send_packet ("ext " . $self->{json_coder}->encode (["reply-$id", @msg])); + } elsif ($self->extcmd == 1) { + #TODO: version 1, remove + unshift @msg, msgtype => "reply", msgid => $id; + $self->send_packet ("ext " . $self->{json_coder}->encode ({@msg})); + } } =item $success = $client->query ($flags, "text", \&cb) @@ -2121,8 +2596,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 @@ -2140,9 +2615,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) = @_; @@ -2164,6 +2646,32 @@ } } }, + on_exticmd => sub { + my ($ns, $buf) = @_; + + my $msg = eval { $ns->{json_coder}->decode ($buf) }; + + if (ref $msg) { + my ($type, $reply, @payload) = + "ARRAY" eq ref $msg + ? @$msg + : ($msg->{msgtype}, $msg->{msgid}, %$msg); # TODO: version 1, remove + + my @reply; + + if (my $cb = $EXTICMD{$type}) { + @reply = $cb->($ns, @payload); + } + + $ns->ext_reply ($reply, @reply) + if $reply; + + } else { + warn "client " . ($ns->pl ? $ns->pl->ob->name : $ns->host) . " sent unparseable exti message: <$buf>\n"; + } + + cf::override; + }, ); =item $client->async (\&cb) @@ -2214,7 +2722,11 @@ $SIG{FPE} = 'IGNORE'; -$safe->permit_only (Opcode::opset qw(:base_core :base_mem :base_orig :base_math sort time)); +$safe->permit_only (Opcode::opset qw( + :base_core :base_mem :base_orig :base_math + grepstart grepwhile mapstart mapwhile + sort time +)); # here we export the classes and methods available to script code @@ -2222,15 +2734,25 @@ 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 name archname title slaying race decrease_ob_nr + + 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 inv name archname title slaying race + decrease_ob_nr)], ["cf::object::player" => qw(player)], ["cf::player" => qw(peaceful)], ["cf::map" => qw(trigger)], @@ -2311,183 +2833,13 @@ =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($) { my ($path) = @_; + my $enc = JSON::XS->new->utf8->canonical->relaxed; + warn "loading facedata from $path\n"; my $facedata; @@ -2499,15 +2851,24 @@ $facedata->{version} == 2 or cf::cleanup "$path: version mismatch, cannot proceed."; + # patch in the exptable + $facedata->{resource}{"res/exp_table"} = { + type => FT_RSRC, + data => $enc->encode ([map cf::level_to_min_exp $_, 1 .. cf::settings->max_level]), + }; + cf::cede_to_tick; + { 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; + cf::face::set_visibility $idx, $info->{visibility}; + cf::face::set_magicmap $idx, $info->{magicmap}; + cf::face::set_data $idx, 0, $info->{data32}, Digest::MD5::md5 $info->{data32}; + cf::face::set_data $idx, 1, $info->{data64}, Digest::MD5::md5 $info->{data64}; + + cf::cede_to_tick; } while (my ($face, $info) = each %$faces) { @@ -2515,11 +2876,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; } } @@ -2528,18 +2891,80 @@ 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 $soundconf = delete $res->{"res/sound.conf"}; + + while (my ($name, $info) = each %$res) { + my $idx = (cf::face::find $name) || cf::face::alloc $name; + my $data; + + if ($info->{type} & 1) { + # prepend meta info + + my $meta = $enc->encode ({ + name => $name, + %{ $info->{meta} || {} }, + }); + + $data = pack "(w/a*)*", $meta, $info->{data}; + } else { + $data = $info->{data}; + } + + cf::face::set_data $idx, 0, $data, Digest::MD5::md5 $data; + cf::face::set_type $idx, $info->{type}; + + cf::cede_to_tick; + } + + if ($soundconf) { + $soundconf = $enc->decode (delete $soundconf->{data}); + + for (0 .. SOUND_CAST_SPELL_0 - 1) { + my $sound = $soundconf->{compat}[$_] + or next; + + my $face = cf::face::find "sound/$sound->[1]"; + cf::sound::set $sound->[0] => $face; + cf::sound::old_sound_index $_, $face; # gcfclient-compat + } + + while (my ($k, $v) = each %{$soundconf->{event}}) { + my $face = cf::face::find "sound/$v"; + cf::sound::set $k => $face; + } + } + } + 1 } +register_exticmd fx_want => sub { + my ($ns, $want) = @_; + + while (my ($k, $v) = each %$want) { + $ns->fx_want ($k, $v); + } +}; + 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 { @@ -2550,6 +2975,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 { @@ -2562,6 +2992,7 @@ reload_regions; reload_facedata; + #reload_archetypes;#d# reload_archetypes; reload_treasures; @@ -2572,7 +3003,7 @@ reload_resources; } -sub cfg_load { +sub reload_config { open my $fh, "<:utf8", "$CONFDIR/config" or return; @@ -2602,7 +3033,7 @@ })->prio (Coro::PRIO_MAX); }; - cfg_load; + reload_config; db_init; load_extensions; @@ -2628,6 +3059,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; @@ -2712,8 +3181,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) { @@ -2766,7 +3236,7 @@ cf::_connect_to_perl; # nominally unnecessary, but cannot hurt warn "loading config and database again"; - cf::cfg_load; + cf::reload_config; warn "loading extensions"; cf::load_extensions;