--- deliantra/server/lib/cf.pm 2007/06/19 18:15:34 1.282 +++ deliantra/server/lib/cf.pm 2007/07/11 16:55:18 1.304 @@ -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 @@ -238,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. @@ -248,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. @@ -404,6 +412,15 @@ =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) = @_; @@ -416,22 +433,31 @@ 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 - die $$res unless "ARRAY" eq ref $res; + 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, Coro::Storable::freeze +($@ ? \"$@" : \@res); + + syswrite $fh2, _store_scalar $@ ? \"$@" : \@res; + close $fh2; }; warn $@ if $@; @@ -1031,7 +1057,22 @@ =item cf::register_extcmd $name => \&callback($pl,$packet); -Register a callback fro 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. @@ -1044,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) = @_; @@ -1060,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}}) { @@ -1349,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. @@ -1359,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) @@ -1395,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; } @@ -2054,7 +2150,10 @@ if ($pl->{npc_dialog} && $pl->{npc_dialog}->{id}) { my $diag = $pl->{npc_dialog}; $diag->{pl}->ext_reply ( - $diag->{id}, msgtype => "reply", msg => $msg, add_topics => [] + $diag->{id}, + msgtype => "reply", + msg => $diag->{pl}->expand_cfpod ($msg), + add_topics => [] ); } else { @@ -2279,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 { @@ -2315,9 +2418,49 @@ $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 @@ -2325,7 +2468,7 @@ my ($self, $type, %msg) = @_; $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) @@ -2334,8 +2477,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 @@ -2353,9 +2496,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) = @_; @@ -2377,6 +2527,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) @@ -2435,15 +2603,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)], @@ -2545,10 +2722,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) { @@ -2556,11 +2735,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; } } @@ -2569,18 +2750,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 { @@ -2591,6 +2800,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 { @@ -2792,8 +3006,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) {