--- deliantra/server/lib/cf.pm 2008/09/22 05:42:41 1.451 +++ deliantra/server/lib/cf.pm 2010/04/12 17:13:36 1.514 @@ -1,29 +1,30 @@ -# +# # This file is part of Deliantra, the Roguelike Realtime MMORPG. # -# Copyright (©) 2006,2007,2008 Marc Alexander Lehmann / Robin Redeker / the Deliantra team +# Copyright (©) 2006,2007,2008,2009,2010 Marc Alexander Lehmann / Robin Redeker / the Deliantra team # -# Deliantra is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# Deliantra is free software: you can redistribute it and/or modify it under +# the terms of the Affero GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# You should have received a copy of the Affero GNU General Public License +# and the GNU General Public License along with this program. If not, see +# . # # The authors can be reached via e-mail to -# +# package cf; use 5.10.0; use utf8; -use strict "vars", "subs"; +use strict qw(vars subs); use Symbol; use List::Util; @@ -33,7 +34,9 @@ use Safe; use Safe::Hole; use Storable (); +use Carp (); +use Guard (); use Coro (); use Coro::State; use Coro::Handle; @@ -42,6 +45,7 @@ use Coro::Timer; use Coro::Signal; use Coro::Semaphore; +use Coro::SemaphoreSet; use Coro::AnyEvent; use Coro::AIO; use Coro::BDB 1.6; @@ -51,9 +55,8 @@ use JSON::XS 2.01 (); use BDB (); use Data::Dumper; -use Digest::MD5; use Fcntl; -use YAML (); +use YAML::XS (); use IO::AIO (); use Time::HiRes; use Compress::LZF; @@ -72,6 +75,9 @@ Compress::LZF::set_serializer "Storable", "Storable::net_mstore", "Storable::mretrieve"; Compress::LZF::sfreeze_cr { }; # prime Compress::LZF so it does not use require later +# strictly for debugging +$SIG{QUIT} = sub { Carp::cluck "SIGQUIT" }; + sub WF_AUTOCANCEL () { 1 } # automatically cancel this watcher on reload our %COMMAND = (); @@ -85,6 +91,8 @@ our $RELOAD; # number of reloads so far, non-zero while in reload our @EVENT; +our @REFLECT; # set by XS +our %REFLECT; # set by us our $CONFDIR = confdir; our $DATADIR = datadir; @@ -102,16 +110,21 @@ our %RESOURCE; +our $OUTPUT_RATE_MIN = 4000; +our $OUTPUT_RATE_MAX = 100000; + our $TICK = MAX_TIME * 1e-6; # this is a CONSTANT(!) our $NEXT_RUNTIME_WRITE; # when should the runtime file be written our $NEXT_TICK; -our $USE_FSYNC = 1; # use fsync to write maps - default off +our $USE_FSYNC = 1; # use fsync to write maps - default on our $BDB_DEADLOCK_WATCHER; our $BDB_CHECKPOINT_WATCHER; our $BDB_TRICKLE_WATCHER; our $DB_ENV; +our @EXTRA_MODULES = qw(pod match mapscript); + our %CFG; our $UPTIME; $UPTIME ||= time; @@ -130,6 +143,11 @@ our $JITTER; # average jitter our $TICK_START; # for load detecting purposes +our @POST_INIT; + +our $REATTACH_ON_RELOAD; # set to true to force object reattach on reload (slow) +our $REALLY_UNLOOP; # never set to true, please :) + binmode STDOUT; binmode STDERR; @@ -140,6 +158,8 @@ $RUNTIME = <$fh> + 0.; } +eval "sub TICK() { $TICK } 1" or die; + mkdir $_ for $LOCALDIR, $TMPDIR, $UNIQUEDIR, $PLAYERDIR, $RANDOMDIR, $BDBDIR; @@ -149,6 +169,17 @@ ############################################################################# +%REFLECT = (); +for (@REFLECT) { + my $reflect = JSON::XS::decode_json $_; + $REFLECT{$reflect->{class}} = $reflect; +} + +# this is decidedly evil +$REFLECT{object}{flags} = { map +($_ => undef), grep $_, map /^FLAG_([A-Z0-9_]+)$/ && lc $1, keys %{"cf::"} }; + +############################################################################# + =head2 GLOBAL VARIABLES =over 4 @@ -202,42 +233,45 @@ =item @cf::INVOKE_RESULTS -This array contains the results of the last C call. When +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. +=item %cf::REFLECT + +Contains, for each (C++) class name, a hash reference with information +about object members (methods, scalars, arrays and flags) and other +metadata, which is useful for introspection. + =back =cut -BEGIN { - *CORE::GLOBAL::warn = sub { - my $msg = join "", @_; +$Coro::State::WARNHOOK = sub { + my $msg = join "", @_; - $msg .= "\n" - unless $msg =~ /\n$/; + $msg .= "\n" + unless $msg =~ /\n$/; - $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; + $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; - LOG llevError, $msg; - }; -} + LOG llevError, $msg; +}; $Coro::State::DIEHOOK = sub { return unless $^S eq 0; # "eq", not "==" + warn Carp::longmess $_[0]; + if ($Coro::current == $Coro::main) {#d# warn "DIEHOOK called in main context, Coro bug?\n";#d# return;#d# }#d# # kill coroutine otherwise - warn Carp::longmess $_[0]; Coro::terminate }; -$SIG{__DIE__} = sub { }; #d#? - @safe::cf::global::ISA = @cf::global::ISA = 'cf::attachable'; @safe::cf::object::ISA = @cf::object::ISA = 'cf::attachable'; @safe::cf::player::ISA = @cf::player::ISA = 'cf::attachable'; @@ -260,7 +294,7 @@ } $EV::DIED = sub { - warn "error in event callback: @_"; + Carp::cluck "error in event callback: @_"; }; ############################################################################# @@ -307,6 +341,20 @@ sub encode_json($) { $json_coder->encode ($_[0]) } sub decode_json($) { $json_coder->decode ($_[0]) } +=item cf::post_init { BLOCK } + +Execute the given codeblock, I all extensions have been (re-)loaded, +but I the server starts ticking again. + +The cdoeblock will have a single boolean argument to indicate whether this +is a reload or not. + +=cut + +sub post_init(&) { + push @POST_INIT, shift; +} + =item cf::lock_wait $string Wait until the given lock is available. See cf::lock_acquire. @@ -314,7 +362,7 @@ =item my $lock = cf::lock_acquire $string Wait until the given lock is available and then acquires it and returns -a Coro::guard object. If the guard object gets destroyed (goes out of scope, +a L object. If the guard object gets destroyed (goes out of scope, for example when the coroutine gets canceled), the lock is automatically returned. @@ -330,57 +378,30 @@ =cut -our %LOCK; -our %LOCKER;#d# +our $LOCKS = new Coro::SemaphoreSet; sub lock_wait($) { - my ($key) = @_; - - if ($LOCKER{$key} == $Coro::current) {#d# - Carp::cluck "lock_wait($key) for already-acquired lock";#d# - return;#d# - }#d# - - # wait for lock, if any - while ($LOCK{$key}) { - #local $Coro::current->{desc} = "$Coro::current->{desc} "; - push @{ $LOCK{$key} }, $Coro::current; - Coro::schedule; - } + $LOCKS->wait ($_[0]); } sub lock_acquire($) { - my ($key) = @_; - - # wait, to be sure we are not locked - lock_wait $key; - - $LOCK{$key} = []; - $LOCKER{$key} = $Coro::current;#d# - - Coro::guard { - delete $LOCKER{$key};#d# - # wake up all waiters, to be on the safe side - $_->ready for @{ delete $LOCK{$key} }; - } + $LOCKS->guard ($_[0]) } sub lock_active($) { - my ($key) = @_; - - ! ! $LOCK{$key} + $LOCKS->count ($_[0]) < 1 } sub freeze_mainloop { tick_inhibit_inc; - Coro::guard \&tick_inhibit_dec; + &Guard::guard (\&tick_inhibit_dec); } =item cf::periodic $interval, $cb Like EV::periodic, but randomly selects a starting point so that the actions -get spread over timer. +get spread over time. =cut @@ -407,24 +428,29 @@ our @SLOT_QUEUE; our $SLOT_QUEUE; +our $SLOT_DECAY = 0.9; $SLOT_QUEUE->cancel if $SLOT_QUEUE; $SLOT_QUEUE = Coro::async { $Coro::current->desc ("timeslot manager"); my $signal = new Coro::Signal; + my $busy; 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; - } + + for (0 .. $#SLOT_QUEUE) { + if ($SLOT_QUEUE[$_][0] <= $avail) { + $busy = 0; + my $job = splice @SLOT_QUEUE, $_, 1, (); + $job->[2]->send; + Coro::cede; + goto next_job; + } else { + $SLOT_QUEUE[$_][0] *= $SLOT_DECAY; } } @@ -433,6 +459,7 @@ push @cf::WAIT_FOR_TICK, $signal; $signal->wait; } else { + $busy = 0; Coro::schedule; } } @@ -443,7 +470,8 @@ my ($time, $pri, $name) = @_; - $time = $TICK * .6 if $time > $TICK * .6; + $time = clamp $time, 0.01, $TICK * .6; + my $sig = new Coro::Signal; push @SLOT_QUEUE, [$time, $pri, $sig, $name]; @@ -480,7 +508,7 @@ my ($job) = @_; if ($Coro::current == $Coro::main) { - my $time = EV::time; + my $time = AE::time; # this is the main coro, too bad, we have to block # till the operation succeeds, freezing the server :/ @@ -507,7 +535,7 @@ } } - my $time = EV::time - $time; + my $time = AE::time - $time; $TICK_START += $time; # do not account sync jobs to server load @@ -563,6 +591,17 @@ wantarray ? @res : $res[-1] } +sub objinfo { + ( + "counter value" => cf::object::object_count, + "objects created" => cf::object::create_count, + "objects destroyed" => cf::object::destroy_count, + "freelist size" => cf::object::free_count, + "allocated objects" => cf::object::objects_size, + "active objects" => cf::object::actives_size, + ) +} + =item $coin = coin_from_name $name =cut @@ -1156,7 +1195,10 @@ if (my $fh = aio_open "$filename~", O_WRONLY | O_CREAT, 0600) { aio_chmod $fh, SAVE_MODE; aio_write $fh, 0, (length $$rdata), $$rdata, 0; - aio_fsync $fh if $cf::USE_FSYNC; + if ($cf::USE_FSYNC) { + aio_sync_file_range $fh, 0, 0, IO::AIO::SYNC_FILE_RANGE_WAIT_BEFORE | IO::AIO::SYNC_FILE_RANGE_WRITE | IO::AIO::SYNC_FILE_RANGE_WAIT_AFTER; + aio_fsync $fh; + } aio_close $fh; if (@$objs) { @@ -1164,7 +1206,10 @@ aio_chmod $fh, SAVE_MODE; my $data = Coro::Storable::nfreeze { version => 1, objs => $objs }; aio_write $fh, 0, (length $data), $data, 0; - aio_fsync $fh if $cf::USE_FSYNC; + if ($cf::USE_FSYNC) { + aio_sync_file_range $fh, 0, 0, IO::AIO::SYNC_FILE_RANGE_WAIT_BEFORE | IO::AIO::SYNC_FILE_RANGE_WRITE | IO::AIO::SYNC_FILE_RANGE_WAIT_AFTER; + aio_fsync $fh; + } aio_close $fh; aio_rename "$filename.pst~", "$filename.pst"; } @@ -1173,8 +1218,11 @@ } aio_rename "$filename~", $filename; + + $filename =~ s%/[^/]+$%%; + aio_pathsync $filename if $cf::USE_FSYNC; } else { - warn "FATAL: $filename~: $!\n"; + warn "unable to save objects: $filename~: $!\n"; } } else { aio_unlink $filename; @@ -1275,8 +1323,10 @@ $EXTICMD{$name} = $cb; } +use File::Glob (); + cf::player->attach ( - on_command => sub { + on_unknown_command => sub { my ($pl, $name, $params) = @_; my $cb = $COMMAND{$name} @@ -1316,6 +1366,19 @@ }, ); +# "readahead" all extensions +sub cache_extensions { + my $grp = IO::AIO::aio_group; + + add $grp IO::AIO::aio_readdirx $LIBDIR, IO::AIO::READDIR_STAT_ORDER, sub { + for (grep /\.ext$/, @{$_[0]}) { + add $grp IO::AIO::aio_load "$LIBDIR/$_", my $data; + } + }; + + $grp +} + sub load_extensions { cf::sync_job { my %todo; @@ -1352,38 +1415,47 @@ $todo{$base} = \%ext; } + my $pass = 0; my %done; while (%todo) { my $progress; + ++$pass; + + ext: while (my ($k, $v) = each %todo) { for (split /,\s*/, $v->{meta}{depends}) { - goto skip + next ext unless exists $done{$_}; } - warn "... loading '$k' into '$v->{pkg}'\n"; + warn "... pass $pass, 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; - cf::cleanup "mandatory extension failed to load, exiting."; - } - - warn $msg; - } + my $active = eval $v->{source}; + + if (length $@) { + warn "$v->{path}: $@\n"; + + cf::cleanup "mandatory extension '$k' failed to load, exiting." + if exists $v->{meta}{mandatory}; + } else { + $done{$k} = delete $todo{$k}; + push @EXTS, $v->{pkg}; + $progress = 1; - $done{$k} = delete $todo{$k}; - push @EXTS, $v->{pkg}; - $progress = 1; + warn "$v->{base}: extension inactive.\n" + unless $active; + } } - skip: - die "cannot load " . (join ", ", keys %todo) . ": unable to resolve dependencies\n" - unless $progress; + unless ($progress) { + warn "cannot load " . (join ", ", keys %todo) . ": unable to resolve dependencies\n"; + + while (my ($k, $v) = each %todo) { + cf::cleanup "mandatory extension '$k' has unresolved dependencies, exiting." + if exists $v->{meta}{mandatory}; + } + } } }; } @@ -1447,7 +1519,7 @@ my ($login) = @_; $cf::PLAYER{$login} - or cf::sync_job { !aio_stat path $login } + or !aio_stat path $login } sub find($) { @@ -1477,6 +1549,19 @@ } } +cf::player->attach ( + on_load => sub { + my ($pl, $path) = @_; + + # restore slots saved in save, below + my $slots = delete $pl->{_slots}; + + $pl->ob->current_weapon ($slots->[0]); + $pl->combat_ob ($slots->[1]); + $pl->ranged_ob ($slots->[2]); + }, +); + sub save($) { my ($pl) = @_; @@ -1492,6 +1577,9 @@ cf::get_slot 0.01; + # save slots, to be restored later + local $pl->{_slots} = [$pl->ob->current_weapon, $pl->combat_ob, $pl->ranged_ob]; + $pl->save_pl ($path); cf::cede_to_tick; } @@ -1713,7 +1801,7 @@ # mit "rum" bekleckern, nicht $self->_create_random_map ( $rmp->{wallstyle}, $rmp->{wall_name}, $rmp->{floorstyle}, $rmp->{monsterstyle}, - $rmp->{treasurestyle}, $rmp->{layoutstyle}, $rmp->{doorstyle}, $rmp->{decorstyle}, + $rmp->{treasurestyle}, $rmp->{layoutstyle}, $rmp->{doorstyle}, $rmp->{decorstyle}, $rmp->{miningstyle}, $rmp->{origin_map}, $rmp->{final_map}, $rmp->{exitstyle}, $rmp->{this_map}, $rmp->{exit_on_final_map}, $rmp->{xsize}, $rmp->{ysize}, @@ -1853,15 +1941,6 @@ "$UNIQUEDIR/$path" } -# and all this just because we cannot iterate over -# all maps in C++... -sub change_all_map_light { - my ($change) = @_; - - $_->change_map_light ($change) - for grep $_->outdoor, values %cf::MAP; -} - sub decay_objects { my ($self) = @_; @@ -1953,13 +2032,10 @@ $path = normalise $path, $origin && $origin->path; - cf::lock_wait "map_data:$path";#d#remove - cf::lock_wait "map_find:$path"; + my $guard1 = cf::lock_acquire "map_data:$path";#d#remove + my $guard2 = cf::lock_acquire "map_find:$path"; $cf::MAP{$path} || do { - my $guard1 = cf::lock_acquire "map_data:$path"; # just for the fun of it - my $guard2 = cf::lock_acquire "map_find:$path"; - my $map = new_from_path cf::map $path or return; @@ -1981,8 +2057,8 @@ } } -sub pre_load { } -sub post_load { } +sub pre_load { } +#sub post_load { } # XS sub load { my ($self) = @_; @@ -2037,8 +2113,6 @@ $self->fix_auto_apply; $self->update_buttons; cf::cede_to_tick; - $self->set_darkness_map; - cf::cede_to_tick; $self->activate; } @@ -2051,6 +2125,9 @@ $self->post_load; } +# customize the map for a given player, i.e. +# return the _real_ map. used by e.g. per-player +# maps to change the path to ~playername/mappath sub customise_for { my ($self, $ob) = @_; @@ -2214,7 +2291,7 @@ return if $self->players; - warn "resetting map ", $self->path; + warn "resetting map ", $self->path, "\n"; $self->in_memory (cf::MAP_SWAPPED); @@ -2315,6 +2392,38 @@ ] } +=item cf::map::static_maps + +Returns an arrayref if paths of all static maps (all preinstalled F<.map> +file in the shared directory excluding F and F). May +block. + +=cut + +sub static_maps() { + my @dirs = ""; + my @maps; + + while (@dirs) { + my $dir = shift @dirs; + + next if $dir eq "/styles" || $dir eq "/editor"; + + my ($dirs, $files) = Coro::AIO::aio_scandir "$MAPDIR$dir", 2 + or return; + + for (@$files) { + s/\.map$// or next; + utf8::decode $_; + push @maps, "$dir/$_"; + } + + push @dirs, map "$dir/$_", @$dirs; + } + + \@maps +} + =back =head3 cf::object @@ -2389,7 +2498,7 @@ id => "say", title => "Map", reply => "say ", - tooltip => "Things said to and replied from npcs near you and other players on the same map only.", + tooltip => "Things said to and replied from NPCs near you and other players on the same map only.", }; our $CHAT_CHANNEL = { @@ -2519,17 +2628,18 @@ # use -1 or undef as default coordinates, not 0, 0 ($x, $y) = ($map->enter_x, $map->enter_y) - if $x <=0 && $y <= 0; + if $x <= 0 && $y <= 0; $map->load; $map->load_neighbours; return unless $self->contr->active; - $self->flag (cf::FLAG_DEBUG, 0);#d# temp - $self->activate_recursive; local $self->{_prev_pos} = $link_pos; # ugly hack for rent.ext $self->enter_map ($map, $x, $y); + + # only activate afterwards, to support waiting in hooks + $self->activate_recursive; } =item $player_object->goto ($path, $x, $y[, $check->($map)[, $done->()]]) @@ -2727,6 +2837,31 @@ $self->send_packet (sprintf "drawinfo %d %s", $flags || cf::NDI_BLACK, $text); } +=item $client->send_big_packet ($pkt) + +Like C, but tries to compress large packets, and fragments +them as required. + +=cut + +our $MAXFRAGSIZE = cf::MAXSOCKBUF - 64; + +sub cf::client::send_big_packet { + my ($self, $pkt) = @_; + + # try lzf for large packets + $pkt = "lzf " . Compress::LZF::compress $pkt + if 1024 <= length $pkt and $self->{can_lzf}; + + # split very large packets + if ($MAXFRAGSIZE < length $pkt and $self->{can_lzf}) { + $self->send_packet ("frag $_") for unpack "(a$MAXFRAGSIZE)*", $pkt; + $pkt = "frag"; + } + + $self->send_packet ($pkt); +} + =item $client->send_msg ($channel, $msg, $color, [extra...]) Send a drawinfo or msg packet to the client, formatting the msg for the @@ -2738,6 +2873,12 @@ # non-persistent channels (usually the info channel) our %CHANNEL = ( + "c/motd" => { + id => "infobox", + title => "MOTD", + reply => undef, + tooltip => "The message of the day", + }, "c/identify" => { id => "infobox", title => "Identify", @@ -2750,6 +2891,12 @@ reply => undef, tooltip => "Signs and other items you examined", }, + "c/shopinfo" => { + id => "infobox", + title => "Shop Info", + reply => undef, + tooltip => "What your bargaining skill tells you about the shop", + }, "c/book" => { id => "infobox", title => "Book", @@ -2774,12 +2921,42 @@ reply => undef, tooltip => "Shows which body parts you posess and are available", }, + "c/statistics" => { + id => "infobox", + title => "Statistics", + reply => undef, + tooltip => "Shows your primary statistics", + }, "c/skills" => { id => "infobox", title => "Skills", reply => undef, tooltip => "Shows your experience per skill and item power", }, + "c/shopitems" => { + id => "infobox", + title => "Shop Items", + reply => undef, + tooltip => "Shows the items currently for sale in this shop", + }, + "c/resistances" => { + id => "infobox", + title => "Resistances", + reply => undef, + tooltip => "Shows your resistances", + }, + "c/pets" => { + id => "infobox", + title => "Pets", + reply => undef, + tooltip => "Shows information abotu your pets/a specific pet", + }, + "c/perceiveself" => { + id => "infobox", + title => "Perceive Self", + reply => undef, + tooltip => "You gained detailed knowledge about yourself", + }, "c/uptime" => { id => "infobox", title => "Uptime", @@ -2798,6 +2975,14 @@ reply => "gsay ", tooltip => "Messages and chat related to your party", }, + "c/death" => { + id => "death", + title => "Death", + reply => undef, + tooltip => "Reason for and more info about your most recent death", + }, + "c/say" => $SAY_CHANNEL, + "c/chat" => $CHAT_CHANNEL, ); sub cf::client::send_msg { @@ -2812,17 +2997,14 @@ if ($CHANNEL{$channel}) { $channel = $CHANNEL{$channel}; - $self->ext_msg (channel_info => $channel) - if $self->can_msg; - + $self->ext_msg (channel_info => $channel); $channel = $channel->{id}; } elsif (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) - if $self->can_msg; + $self->ext_msg (channel_info => $channel); } $channel = $channel->{id}; @@ -2830,52 +3012,16 @@ 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; - - my $pkt = "msg " - . $self->{json_coder}->encode ( - [$color & cf::NDI_CLIENT_MASK, $channel, $msg, @extra] - ); - - # try lzf for large packets - $pkt = "lzf " . Compress::LZF::compress $pkt - if 1024 <= length $pkt and $self->{can_lzf}; - - # split very large packets - if (8192 < length $pkt and $self->{can_lzf}) { - $self->send_packet ("frag $_") for unpack "(a8192)*", $pkt; - $pkt = "frag"; - } + # default colour, mask it out + $color &= ~(cf::NDI_COLOR_MASK | cf::NDI_DEF) + if $color & cf::NDI_DEF; + + my $pkt = "msg " + . $self->{json_coder}->encode ( + [$color & cf::NDI_CLIENT_MASK, $channel, $msg, @extra] + ); - $self->send_packet ($pkt); - } 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") - } - } - } + $self->send_big_packet ($pkt); } =item $client->ext_msg ($type, @msg) @@ -2888,10 +3034,10 @@ my ($self, $type, @msg) = @_; if ($self->extcmd == 2) { - $self->send_packet ("ext " . $self->{json_coder}->encode ([$type, @msg])); + $self->send_big_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})); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ({@msg})); } } @@ -2905,11 +3051,11 @@ my ($self, $id, @msg) = @_; if ($self->extcmd == 2) { - $self->send_packet ("ext " . $self->{json_coder}->encode (["reply-$id", @msg])); + $self->send_big_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})); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ({@msg})); } } @@ -3020,7 +3166,7 @@ } cf::client->attach ( - on_destroy => sub { + on_client_destroy => sub { my ($ns) = @_; $_->cancel for values %{ (delete $ns->{_coro}) || {} }; @@ -3046,7 +3192,7 @@ $SIG{FPE} = 'IGNORE'; $safe->permit_only (Opcode::opset qw( - :base_core :base_mem :base_orig :base_math + :base_core :base_mem :base_orig :base_math :base_loop grepstart grepwhile mapstart mapwhile sort time )); @@ -3060,6 +3206,7 @@ cf::object contr pay_amount pay_player map x y force_find force_add destroy insert remove name archname title slaying race decrease split + value cf::object::player player @@ -3075,9 +3222,9 @@ for ( ["cf::object" => qw(contr pay_amount pay_player map force_find force_add x y insert remove inv nrof name archname title slaying race - decrease split destroy change_exp)], + decrease split destroy change_exp value msg lore send_msg)], ["cf::object::player" => qw(player)], - ["cf::player" => qw(peaceful)], + ["cf::player" => qw(peaceful send_msg)], ["cf::map" => qw(trigger)], ) { no strict 'refs'; @@ -3105,8 +3252,10 @@ $qcode =~ s/"/‟/g; # not allowed in #line filenames $qcode =~ s/\n/\\n/g; + %vars = (_dummy => 0) unless %vars; + + my @res; local $_; - local @safe::cf::_safe_eval_args = values %vars; my $eval = "do {\n" @@ -3116,9 +3265,15 @@ . "\n}" ; - sub_generation_inc; - my @res = wantarray ? $safe->reval ($eval) : scalar $safe->reval ($eval); - sub_generation_inc; + if ($CFG{safe_eval}) { + sub_generation_inc; + local @safe::cf::_safe_eval_args = values %vars; + @res = wantarray ? $safe->reval ($eval) : scalar $safe->reval ($eval); + sub_generation_inc; + } else { + local @cf::_safe_eval_args = values %vars; + @res = wantarray ? eval eval : scalar eval $eval; + } if ($@) { warn "$@"; @@ -3147,8 +3302,8 @@ sub register_script_function { my ($fun, $cb) = @_; - no strict 'refs'; - *{"safe::$fun"} = $safe_hole->wrap ($cb); + $fun = "safe::$fun" if $CFG{safe_eval}; + *$fun = $safe_hole->wrap ($cb); } =back @@ -3179,9 +3334,11 @@ or cf::cleanup "$path: version mismatch, cannot proceed."; # patch in the exptable + my $exp_table = $enc->encode ([map cf::level_to_min_exp $_, 1 .. cf::settings->max_level]); $facedata->{resource}{"res/exp_table"} = { type => FT_RSRC, - data => $enc->encode ([map cf::level_to_min_exp $_, 1 .. cf::settings->max_level]), + data => $exp_table, + hash => (Digest::MD5::md5 $exp_table), }; cf::cede_to_tick; @@ -3193,8 +3350,8 @@ 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::face::set_data $idx, 0, $info->{data32}, $info->{hash32}; + cf::face::set_data $idx, 1, $info->{data64}, $info->{hash64}; cf::cede_to_tick; } @@ -3228,29 +3385,13 @@ } { - # TODO: for gcfclient pleasure, we should give resources - # that gcfclient doesn't grok a >10000 face index. my $res = $facedata->{resource}; while (my ($name, $info) = each %$res) { if (defined $info->{type}) { 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_data $idx, 0, $info->{data}, $info->{hash}; cf::face::set_type $idx, $info->{type}; } else { $RESOURCE{$name} = $info; @@ -3343,20 +3484,14 @@ warn "finished reloading resource files\n"; } -sub init { - my $guard = freeze_mainloop; - - evthread_start IO::AIO::poll_fileno; - - reload_resources; -} - sub reload_config { + warn "reloading config file...\n"; + open my $fh, "<:utf8", "$CONFDIR/config" or return; local $/; - *CFG = YAML::Load <$fh>; + *CFG = YAML::XS::Load scalar <$fh>; $EMERGENCY_POSITION = $CFG{emergency_position} || ["/world/world_105_115", 5, 37]; @@ -3370,6 +3505,8 @@ }; warn $@ if $@; } + + warn "finished reloading resource files\n"; } sub pidfile() { @@ -3391,8 +3528,29 @@ print $fh $$; } +sub main_loop { + warn "EV::loop starting\n"; + if (1) { + EV::loop; + } + warn "EV::loop returned\n"; + goto &main_loop unless $REALLY_UNLOOP; +} + sub main { - atomic; + cf::init_globals; # initialise logging + + LOG llevInfo, "Welcome to Deliantra, v" . VERSION; + LOG llevInfo, "Copyright (C) 2005-2008 Marc Alexander Lehmann / Robin Redeker / the Deliantra team."; + LOG llevInfo, "Copyright (C) 1994 Mark Wedel."; + LOG llevInfo, "Copyright (C) 1992 Frank Tore Johansen."; + + cf::init_experience; + cf::init_anim; + cf::init_attackmess; + cf::init_dynamic; + + $Coro::current->prio (Coro::PRIO_MAX); # give the main loop max. priority # we must not ever block the main coroutine local $Coro::idle = sub { @@ -3403,21 +3561,36 @@ })->prio (Coro::PRIO_MAX); }; - { - my $guard = freeze_mainloop; + evthread_start IO::AIO::poll_fileno; + + cf::sync_job { + cf::load_settings; + cf::load_materials; + + reload_resources; reload_config; db_init; + + cf::init_uuid; + cf::init_signals; + cf::init_skills; + + cf::init_beforeplay; + + atomic; + load_extensions; - $Coro::current->prio (Coro::PRIO_MAX); # give the main loop max. priority - } + utime time, time, $RUNTIMEFILE; - utime time, time, $RUNTIMEFILE; + # no (long-running) fork's whatsoever before this point(!) + use POSIX (); + POSIX::close delete $ENV{LOCKUTIL_LOCK_FD} if exists $ENV{LOCKUTIL_LOCK_FD}; - # no (long-running) fork's whatsoever before this point(!) - POSIX::close delete $ENV{LOCKUTIL_LOCK_FD} if exists $ENV{LOCKUTIL_LOCK_FD}; + (pop @POST_INIT)->(0) while @POST_INIT; + }; - EV::loop; + main_loop; } ############################################################################# @@ -3427,13 +3600,15 @@ BEGIN { our %SIGWATCHER = (); for my $signal (qw(INT HUP TERM)) { - $SIGWATCHER{$signal} = EV::signal $signal, sub { + $SIGWATCHER{$signal} = AE::signal $signal, sub { cf::cleanup "SIG$signal"; }; } } sub write_runtime_sync { + my $t0 = AE::time; + # first touch the runtime file to show we are still running: # the fsync below can take a very very long time. @@ -3441,7 +3616,7 @@ my $guard = cf::lock_acquire "write_runtime"; - my $fh = aio_open "$RUNTIMEFILE~", O_WRONLY | O_CREAT, 0644 + my $fh = aio_open "$RUNTIMEFILE~", O_WRONLY | O_CREAT | O_TRUNC, 0644 or return; my $value = $cf::RUNTIME + 90 + 10; @@ -3464,7 +3639,7 @@ aio_rename "$RUNTIMEFILE~", $RUNTIMEFILE and return; - warn "runtime file written.\n"; + warn sprintf "runtime file written (%gs).\n", AE::time - $t0; 1 } @@ -3483,7 +3658,14 @@ my $fh = aio_open "$uuid~", O_WRONLY | O_CREAT, 0644 or return; - my $value = uuid_str $uuid_skip + uuid_seq uuid_cur; + my $value = uuid_seq uuid_cur; + + unless ($value) { + warn "cowardly refusing to write zero uuid value!\n"; + return; + } + + my $value = uuid_str $value + $uuid_skip; $uuid_skip = 0; (aio_write $fh, 0, (length $value), $value, 0) <= 0 @@ -3515,39 +3697,49 @@ sub emergency_save() { my $freeze_guard = cf::freeze_mainloop; - warn "enter emergency perl save\n"; + warn "emergency_perl_save: enter\n"; cf::sync_job { + # this is a trade-off: we want to be very quick here, so + # save all maps without fsync, and later call a global sync + # (which in turn might be very very slow) + local $USE_FSYNC = 0; + # use a peculiar iteration method to avoid tripping on perl # refcount bugs in for. also avoids problems with players # and maps saved/destroyed asynchronously. - warn "begin emergency player save\n"; + warn "emergency_perl_save: begin player save\n"; for my $login (keys %cf::PLAYER) { my $pl = $cf::PLAYER{$login} or next; $pl->valid or next; delete $pl->{unclean_save}; # not strictly necessary, but cannot hurt $pl->save; } - warn "end emergency player save\n"; + warn "emergency_perl_save: end player save\n"; - warn "begin emergency map save\n"; + warn "emergency_perl_save: begin map save\n"; for my $path (keys %cf::MAP) { my $map = $cf::MAP{$path} or next; $map->valid or next; $map->save; } - warn "end emergency map save\n"; + warn "emergency_perl_save: end map save\n"; - warn "begin emergency database checkpoint\n"; + warn "emergency_perl_save: begin database checkpoint\n"; BDB::db_env_txn_checkpoint $DB_ENV; - warn "end emergency database checkpoint\n"; + warn "emergency_perl_save: end database checkpoint\n"; - warn "begin write uuid\n"; + warn "emergency_perl_save: begin write uuid\n"; write_uuid_sync 1; - warn "end write uuid\n"; + warn "emergency_perl_save: end write uuid\n"; }; - warn "leave emergency perl save\n"; + warn "emergency_perl_save: starting sync()\n"; + IO::AIO::aio_sync sub { + warn "emergency_perl_save: finished sync()\n"; + }; + + warn "emergency_perl_save: leave\n"; } sub post_cleanup { @@ -3595,6 +3787,8 @@ return if $RELOAD++; + my $t1 = AE::time; + while ($RELOAD) { warn "reloading..."; @@ -3664,7 +3858,7 @@ warn "unloading cf.pm \"a bit\""; delete $INC{"cf.pm"}; - delete $INC{"cf/pod.pm"}; + delete $INC{"cf/$_.pm"} for @EXTRA_MODULES; # don't, removes xs symbols, too, # and global variables created in xs @@ -3674,7 +3868,7 @@ warn "reloading cf.pm"; require cf; - cf::_connect_to_perl; # nominally unnecessary, but cannot hurt + cf::_connect_to_perl_1; warn "loading config and database again"; cf::reload_config; @@ -3682,12 +3876,17 @@ warn "loading extensions"; cf::load_extensions; - warn "reattaching attachments to objects/players"; - _global_reattach; # objects, sockets - warn "reattaching attachments to maps"; - reattach $_ for values %MAP; - warn "reattaching attachments to players"; - reattach $_ for values %PLAYER; + if ($REATTACH_ON_RELOAD) { + warn "reattaching attachments to objects/players"; + _global_reattach; # objects, sockets + warn "reattaching attachments to maps"; + reattach $_ for values %MAP; + warn "reattaching attachments to players"; + reattach $_ for values %PLAYER; + } + + warn "running post_init jobs"; + (pop @POST_INIT)->(1) while @POST_INIT; warn "leaving sync_job"; @@ -3700,6 +3899,9 @@ warn "reloaded"; --$RELOAD; } + + $t1 = AE::time - $t1; + warn "reload completed in ${t1}s\n"; }; our $RELOAD_WATCHER; # used only during reload @@ -3708,9 +3910,13 @@ # doing reload synchronously and two reloads happen back-to-back, # coro crashes during coro_state_free->destroy here. - $RELOAD_WATCHER ||= EV::timer $TICK * 1.5, 0, sub { - do_reload_perl; - undef $RELOAD_WATCHER; + $RELOAD_WATCHER ||= cf::async { + Coro::AIO::aio_wait cache_extensions; + + $RELOAD_WATCHER = AE::timer $TICK * 1.5, 0, sub { + do_reload_perl; + undef $RELOAD_WATCHER; + }; }; } @@ -3734,7 +3940,7 @@ our @WAIT_FOR_TICK_BEGIN; sub wait_for_tick { - return if tick_inhibit || $Coro::current == $Coro::main; + return Coro::cede if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK, $signal; @@ -3742,7 +3948,7 @@ } sub wait_for_tick_begin { - return if tick_inhibit || $Coro::current == $Coro::main; + return Coro::cede if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK_BEGIN, $signal; @@ -3758,6 +3964,8 @@ cf::server_tick; # one server iteration + #for(1..3e6){} AE::now_update; $NOW=AE::now; # generate load #d# + if ($NOW >= $NEXT_RUNTIME_WRITE) { $NEXT_RUNTIME_WRITE = List::Util::max $NEXT_RUNTIME_WRITE + 10, $NOW + 5.; Coro::async_pool { @@ -3789,7 +3997,7 @@ { # configure BDB - BDB::min_parallel 8; + BDB::min_parallel 16; BDB::max_poll_reqs $TICK * 0.1; $AnyEvent::BDB::WATCHER->priority (1); @@ -3879,7 +4087,8 @@ } # load additional modules -use cf::pod; +require "cf/$_.pm" for @EXTRA_MODULES; +cf::_connect_to_perl_2; END { cf::emergency_save }