--- deliantra/server/lib/cf.pm 2007/10/04 11:36:54 1.378 +++ deliantra/server/lib/cf.pm 2009/10/24 09:18:44 1.491 @@ -1,51 +1,82 @@ +# +# This file is part of Deliantra, the Roguelike Realtime MMORPG. +# +# Copyright (©) 2006,2007,2008 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 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 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; +use strict qw(vars subs); use Symbol; use List::Util; use Socket; -use Storable; -use Event; +use EV; use Opcode; use Safe; use Safe::Hole; +use Storable (); -use Coro 3.64 (); +use Guard (); +use Coro (); use Coro::State; use Coro::Handle; -use Coro::Event; +use Coro::EV; +use Coro::AnyEvent; use Coro::Timer; use Coro::Signal; use Coro::Semaphore; +use Coro::SemaphoreSet; +use Coro::AnyEvent; use Coro::AIO; +use Coro::BDB 1.6; use Coro::Storable; use Coro::Util (); -use JSON::XS (); +use JSON::XS 2.01 (); use BDB (); use Data::Dumper; use Digest::MD5; use Fcntl; -use YAML::Syck (); -use IO::AIO 2.32 (); +use YAML::XS (); +use IO::AIO (); use Time::HiRes; use Compress::LZF; use Digest::MD5 (); +AnyEvent::detect; + # configure various modules to our taste # $Storable::canonical = 1; # reduce rsync transfers Coro::State::cctx_stacksize 256000; # 1-2MB stack, for deep recursions in maze generator -Compress::LZF::sfreeze_cr { }; # prime Compress::LZF so it does not use require later -$Event::Eval = 1; # no idea why this is required, but it is +$Coro::main->prio (Coro::PRIO_MAX); # run main coroutine ("the server") with very high priority -# work around bug in YAML::Syck - bad news for perl6, will it be as broken wrt. unicode? -$YAML::Syck::ImplicitUnicode = 1; +# make sure c-lzf reinitialises itself +Compress::LZF::set_serializer "Storable", "Storable::net_mstore", "Storable::mretrieve"; +Compress::LZF::sfreeze_cr { }; # prime Compress::LZF so it does not use require later -$Coro::main->prio (Coro::PRIO_MAX); # run main coroutine ("the server") with very high priority +# strictly for debugging +$SIG{QUIT} = sub { Carp::cluck "SIGQUIT" }; sub WF_AUTOCANCEL () { 1 } # automatically cancel this watcher on reload @@ -58,39 +89,44 @@ our %EXT_CORO = (); # coroutines bound to extensions our %EXT_MAP = (); # pluggable maps -our $RELOAD; # number of reloads so far +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; +our $LIBDIR = "$DATADIR/ext"; +our $PODDIR = "$DATADIR/pod"; +our $MAPDIR = "$DATADIR/" . mapdir; +our $LOCALDIR = localdir; +our $TMPDIR = "$LOCALDIR/" . tmpdir; +our $UNIQUEDIR = "$LOCALDIR/" . uniquedir; +our $PLAYERDIR = "$LOCALDIR/" . playerdir; +our $RANDOMDIR = "$LOCALDIR/random"; +our $BDBDIR = "$LOCALDIR/db"; +our $PIDFILE = "$LOCALDIR/pid"; +our $RUNTIMEFILE = "$LOCALDIR/runtime"; -our $CONFDIR = confdir; -our $DATADIR = datadir; -our $LIBDIR = "$DATADIR/ext"; -our $PODDIR = "$DATADIR/pod"; -our $MAPDIR = "$DATADIR/" . mapdir; -our $LOCALDIR = localdir; -our $TMPDIR = "$LOCALDIR/" . tmpdir; -our $UNIQUEDIR = "$LOCALDIR/" . uniquedir; -our $PLAYERDIR = "$LOCALDIR/" . playerdir; -our $RANDOMDIR = "$LOCALDIR/random"; -our $BDBDIR = "$LOCALDIR/db"; +our %RESOURCE; our $TICK = MAX_TIME * 1e-6; # this is a CONSTANT(!) -our $TICK_WATCHER; -our $AIO_POLL_WATCHER; our $NEXT_RUNTIME_WRITE; # when should the runtime file be written our $NEXT_TICK; -our $NOW; -our $USE_FSYNC = 1; # use fsync to write maps - default off +our $USE_FSYNC = 1; # use fsync to write maps - default on -our $BDB_POLL_WATCHER; 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; our $RUNTIME; +our $NOW; our (%PLAYER, %PLAYER_LOADING); # all users our (%MAP, %MAP_LOADING ); # all maps @@ -101,18 +137,26 @@ our $LOAD; # a number between 0 (idle) and 1 (too many objects) our $LOADAVG; # same thing, but with alpha-smoothing -our $tick_start; # for load detecting purposes +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; # read virtual server time, if available -unless ($RUNTIME || !-e "$LOCALDIR/runtime") { - open my $fh, "<", "$LOCALDIR/runtime" - or die "unable to read runtime file: $!"; +unless ($RUNTIME || !-e $RUNTIMEFILE) { + open my $fh, "<", $RUNTIMEFILE + or die "unable to read $RUNTIMEFILE file: $!"; $RUNTIME = <$fh> + 0.; } +eval "sub TICK() { $TICK } 1" or die; + mkdir $_ for $LOCALDIR, $TMPDIR, $UNIQUEDIR, $PLAYERDIR, $RANDOMDIR, $BDBDIR; @@ -122,6 +166,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 @@ -164,7 +219,7 @@ =item %cf::CFG -Configuration for the server, loaded from C, or +Configuration for the server, loaded from C, or from wherever your confdir points to. =item cf::wait_for_tick, cf::wait_for_tick_begin @@ -175,26 +230,44 @@ =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 + Coro::terminate +}; @safe::cf::global::ISA = @cf::global::ISA = 'cf::attachable'; @safe::cf::object::ISA = @cf::object::ISA = 'cf::attachable'; @@ -211,13 +284,13 @@ cf::object cf::object::player cf::client cf::player cf::arch cf::living - cf::map cf::party cf::region + cf::map cf::mapspace + cf::party cf::region )) { - no strict 'refs'; @{"safe::$pkg\::wrap::ISA"} = @{"$pkg\::wrap::ISA"} = $pkg; } -$Event::DIED = sub { +$EV::DIED = sub { warn "error in event callback: @_"; }; @@ -250,11 +323,11 @@ } || "[unable to dump $_[0]: '$@']"; } -=item $ref = cf::from_json $json +=item $ref = cf::decode_json $json Converts a JSON string into the corresponding perl data structure. -=item $json = cf::to_json $ref +=item $json = cf::encode_json $ref Converts a perl data structure into its JSON representation. @@ -262,8 +335,22 @@ 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]) } +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 @@ -272,7 +359,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. @@ -288,54 +375,39 @@ =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}) { - push @{ $LOCK{$key} }, $Coro::current; - Coro::schedule; - } + $LOCKS->wait ($_[0]); } sub lock_acquire($) { - my ($key) = @_; + $LOCKS->guard ($_[0]) +} - # wait, to be sure we are not locked - lock_wait $key; +sub lock_active($) { + $LOCKS->count ($_[0]) < 1 +} - $LOCK{$key} = []; - $LOCKER{$key} = $Coro::current;#d# +sub freeze_mainloop { + tick_inhibit_inc; - Coro::guard { - delete $LOCKER{$key};#d# - # wake up all waiters, to be on the safe side - $_->ready for @{ delete $LOCK{$key} }; - } + &Guard::guard (\&tick_inhibit_dec); } -sub lock_active($) { - my ($key) = @_; +=item cf::periodic $interval, $cb - ! ! $LOCK{$key} -} +Like EV::periodic, but randomly selects a starting point so that the actions +get spread over timer. -sub freeze_mainloop { - return unless $TICK_WATCHER->is_active; +=cut - my $guard = Coro::guard { - $TICK_WATCHER->start; - }; - $TICK_WATCHER->stop; - $guard +sub periodic($$) { + my ($interval, $cb) = @_; + + my $start = rand List::Util::min 180, $interval; + + EV::periodic $start, $interval, 0, $cb } =item cf::get_slot $time[, $priority[, $name]] @@ -375,7 +447,7 @@ } if (@SLOT_QUEUE) { - # we do not use wait_For_tick() as it returns immediately when tick is inactive + # we do not use wait_for_tick() as it returns immediately when tick is inactive push @cf::WAIT_FOR_TICK, $signal; $signal->wait; } else { @@ -385,6 +457,8 @@ }; sub get_slot($;$$) { + return if tick_inhibit || $Coro::current == $Coro::main; + my ($time, $pri, $name) = @_; $time = $TICK * .6 if $time > $TICK * .6; @@ -408,8 +482,8 @@ =item cf::sync_job { BLOCK } -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 +The design of Deliantra requires that the main coroutine ($Coro::main) +is always able to handle events or runnable, as Deliantra is only partly reentrant. Thus "blocking" it by e.g. waiting for I/O is not acceptable. @@ -424,13 +498,13 @@ my ($job) = @_; if ($Coro::current == $Coro::main) { - my $time = Event::time; + my $time = EV::time; # this is the main coro, too bad, we have to block # till the operation succeeds, freezing the server :/ - # TODO: use suspend/resume instead - # (but this is cancel-safe) + LOG llevError, Carp::longmess "sync job";#d# + my $freeze_guard = freeze_mainloop; my $busy = 1; @@ -444,15 +518,16 @@ })->prio (Coro::PRIO_MAX); while ($busy) { - Coro::cede or Event::one_event; + if (Coro::nready) { + Coro::cede_notself; + } else { + EV::loop EV::LOOP_ONESHOT; + } } - $time = Event::time - $time; - - LOG llevError | logBacktrace, Carp::longmess "long sync job" - if $time > $TICK * 0.5 && $TICK_WATCHER->is_active; + my $time = EV::time - $time; - $tick_start += $time; # do not account sync jobs to server load + $TICK_START += $time; # do not account sync jobs to server load wantarray ? @res : $res[0] } else { @@ -487,7 +562,7 @@ 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. +never block itself or use any form of event handling. =cut @@ -506,6 +581,33 @@ wantarray ? @res : $res[-1] } +=item $coin = coin_from_name $name + +=cut + +our %coin_alias = ( + "silver" => "silvercoin", + "silvercoin" => "silvercoin", + "silvercoins" => "silvercoin", + "gold" => "goldcoin", + "goldcoin" => "goldcoin", + "goldcoins" => "goldcoin", + "platinum" => "platinacoin", + "platinumcoin" => "platinacoin", + "platinumcoins" => "platinacoin", + "platina" => "platinacoin", + "platinacoin" => "platinacoin", + "platinacoins" => "platinacoin", + "royalty" => "royalty", + "royalties" => "royalty", +); + +sub coin_from_name($) { + $coin_alias{$_[0]} + ? cf::arch::find $coin_alias{$_[0]} + : undef +} + =item $value = cf::db_get $family => $key Returns a single value from the environment database. @@ -652,13 +754,13 @@ =head2 ATTACHABLE OBJECTS -Many objects in crossfire are so-called attachable objects. That means you can +Many objects in deliantra are so-called attachable objects. That means you can attach callbacks/event handlers (a collection of which is called an "attachment") to it. All such attachable objects support the following methods. In the following description, CLASS can be any of C, C C, C or C (i.e. the attachable objects in -Crossfire TRT). +Deliantra). =over 4 @@ -712,7 +814,7 @@ Register an attachment by C<$name> through which attachable objects of the given CLASS can refer to this attachment. -Some classes such as crossfire maps and objects can specify attachments +Some classes such as deliantra maps and objects can specify attachments that are attached at load/instantiate time, thus the need for a name. These calls expect any number of the following handler/hook descriptions: @@ -950,7 +1052,7 @@ 0 } -=item $bool = cf::global::invoke (EVENT_CLASS_XXX, ...) +=item $bool = cf::global->invoke (EVENT_CLASS_XXX, ...) =item $bool = $attachable->invoke (EVENT_CLASS_XXX, ...) @@ -966,23 +1068,55 @@ ############################################################################# # object support -# +sub _object_equal($$); +sub _object_equal($$) { + my ($a, $b) = @_; + + return 0 unless (ref $a) eq (ref $b); + + if ("HASH" eq ref $a) { + my @ka = keys %$a; + my @kb = keys %$b; + + return 0 if @ka != @kb; + + for (0 .. $#ka) { + return 0 unless $ka[$_] eq $kb[$_]; + return 0 unless _object_equal $a->{$ka[$_]}, $b->{$kb[$_]}; + } + + } elsif ("ARRAY" eq ref $a) { + + return 0 if @$a != @$b; + + for (0 .. $#$a) { + return 0 unless _object_equal $a->[$_], $b->[$_]; + } + + } elsif ($a ne $b) { + return 0; + } + + 1 +} + +our $SLOW_MERGES;#d# sub _can_merge { my ($ob1, $ob2) = @_; - local $Storable::canonical = 1; - my $fob1 = Storable::freeze $ob1; - my $fob2 = Storable::freeze $ob2; + ++$SLOW_MERGES;#d# - $fob1 eq $fob2 + # we do the slow way here + return _object_equal $ob1, $ob2 } sub reattach { # basically do the same as instantiate, without calling instantiate my ($obj) = @_; - bless $obj, ref $obj; # re-bless in case extensions have been reloaded + # no longer needed after getting rid of delete_package? + #bless $obj, ref $obj; # re-bless in case extensions have been reloaded my $registry = $obj->registry; @@ -1007,7 +1141,7 @@ on_instantiate => sub { my ($obj, $data) = @_; - $data = from_json $data; + $data = decode_json $data; for (@$data) { my ($name, $args) = @$_; @@ -1038,18 +1172,24 @@ $decname, length $$rdata, scalar @$objs; if (my $fh = aio_open "$filename~", O_WRONLY | O_CREAT, 0600) { - chmod SAVE_MODE, $fh; + aio_chmod $fh, SAVE_MODE; aio_write $fh, 0, (length $$rdata), $$rdata, 0; - aio_fsync $fh if $cf::USE_FSYNC; - close $fh; + 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) { if (my $fh = aio_open "$filename.pst~", O_WRONLY | O_CREAT, 0600) { - chmod SAVE_MODE, $fh; - my $data = Storable::nfreeze { version => 1, objs => $objs }; + 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; - close $fh; + 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"; } } else { @@ -1057,8 +1197,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; @@ -1087,7 +1230,8 @@ (aio_load "$filename.pst", $av) >= 0 or return; - $av = eval { (Storable::thaw $av)->{objs} }; + my $st = eval { Coro::Storable::thaw $av }; + $av = $st->{objs}; } utf8::decode (my $decname = $filename); @@ -1158,6 +1302,8 @@ $EXTICMD{$name} = $cb; } +use File::Glob (); + cf::player->attach ( on_command => sub { my ($pl, $name, $params) = @_; @@ -1199,6 +1345,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; @@ -1227,7 +1386,7 @@ if $source =~ /\A#!.*?perl.*?#\s*(.*)$/m; $ext{source} = - "package $pkg; use strict; use utf8;\n" + "package $pkg; use 5.10.0; use strict 'vars', 'subs'; use utf8;\n" . "#line 1 \"$path\"\n{\n" . $source . "\n};\n1"; @@ -1253,8 +1412,7 @@ if (exists $v->{meta}{mandatory}) { warn $msg; - warn "mandatory extension failed to load, exiting.\n"; - exit 1; + cf::cleanup "mandatory extension failed to load, exiting."; } warn $msg; @@ -1278,7 +1436,7 @@ =head2 CORE EXTENSIONS -Functions and methods that extend core crossfire objects. +Functions and methods that extend core deliantra objects. =cut @@ -1331,7 +1489,7 @@ my ($login) = @_; $cf::PLAYER{$login} - or cf::sync_job { !aio_stat path $login } + or !aio_stat path $login } sub find($) { @@ -1353,6 +1511,7 @@ my $pl = cf::player::load_pl $f or return; + local $cf::PLAYER_LOADING{$login} = $pl; $f->resolve_delayed_derefs; $cf::PLAYER{$login} = $pl @@ -1373,6 +1532,8 @@ aio_mkdir playerdir $pl, 0770; $pl->{last_save} = $cf::RUNTIME; + cf::get_slot 0.01; + $pl->save_pl ($path); cf::cede_to_tick; } @@ -1413,10 +1574,12 @@ my $name = $pl->ob->name; $pl->{deny_save} = 1; - $pl->password ("*"); # this should lock out the player until we nuked the dir + $pl->password ("*"); # this should lock out the player until we have nuked the dir $pl->invoke (cf::EVENT_PLAYER_LOGOUT, 1) if $pl->active; $pl->deactivate; + my $killer = cf::arch::get "killer_quit"; $pl->killer ($killer); $killer->destroy; + $pl->ob->check_score; $pl->invoke (cf::EVENT_PLAYER_QUIT); $pl->ns->destroy if $pl->ns; @@ -1465,7 +1628,8 @@ my $path = path $login; # a .pst is a dead give-away for a valid player - unless (-e "$path.pst") { + # if no pst file found, open and chekc for blocked users + if (aio_stat "$path.pst") { my $fh = aio_open $path, Fcntl::O_RDONLY, 0 or next; aio_read $fh, 0, 512, my $buf, 0 or next; $buf !~ /^password -------------$/m or next; # official not-valid tag @@ -1506,56 +1670,9 @@ \@paths } -=item $protocol_xml = $player->expand_cfpod ($crossfire_pod) +=item $protocol_xml = $player->expand_cfpod ($cfpod) -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>/ - || s/T<([^\>]*)>/$1<\/b><\/big>/ - # 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} -} +Expand deliantra pod fragments into protocol xml. =item $player->ext_reply ($msgid, @msg) @@ -1632,6 +1749,9 @@ sub generate_random_map { my ($self, $rmp) = @_; + + my $lock = cf::lock_acquire "generate_random_map"; # the random map generator is NOT reentrant ATM + # mit "rum" bekleckern, nicht $self->_create_random_map ( $rmp->{wallstyle}, $rmp->{wall_name}, $rmp->{floorstyle}, $rmp->{monsterstyle}, @@ -1763,7 +1883,7 @@ sub save_path { my ($self) = @_; - (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; + (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/go; "$TMPDIR/$path.map" } @@ -1771,19 +1891,10 @@ sub uniq_path { my ($self) = @_; - (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; + (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/go; "$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) = @_; @@ -1875,13 +1986,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_find:$path"; - my $guard2 = cf::lock_acquire "map_data:$path"; # just for the fun of it - my $map = new_from_path cf::map $path or return; @@ -1893,8 +2001,8 @@ 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; - undef $guard1; undef $guard2; + undef $guard1; $map->reset; return find $path; } @@ -1931,7 +2039,7 @@ $self->_load_objects ($f) or return; - $self->set_object_flag (cf::FLAG_OBJ_ORIGINAL, 1) + $self->post_load_original if delete $self->{load_original}; if (my $uniq = $self->uniq_path) { @@ -1959,15 +2067,13 @@ $self->fix_auto_apply; $self->update_buttons; cf::cede_to_tick; - $self->set_darkness_map; - cf::cede_to_tick; $self->activate; } $self->{last_save} = $cf::RUNTIME; $self->last_access ($cf::RUNTIME); - $self->in_memory (cf::MAP_IN_MEMORY); + $self->in_memory (cf::MAP_ACTIVE); } $self->post_load; @@ -2035,7 +2141,7 @@ $path = normalise $path, $origin && $origin->{path}; if (my $map = $cf::MAP{$path}) { - return $map if !$load || $map->in_memory == cf::MAP_IN_MEMORY; + return $map if !$load || $map->in_memory == cf::MAP_ACTIVE; } $MAP_PREFETCH{$path} |= $load; @@ -2082,6 +2188,8 @@ $_->contr->save for $self->players; }; + cf::get_slot 0.02; + if ($uniq) { $self->_save_objects ($save, cf::IO_HEADER | cf::IO_OBJECTS); $self->_save_objects ($uniq, cf::IO_UNIQUES); @@ -2099,7 +2207,7 @@ my $lock = cf::lock_acquire "map_data:$self->{path}"; return if $self->players; - return if $self->in_memory != cf::MAP_IN_MEMORY; + return if $self->in_memory != cf::MAP_ACTIVE; return if $self->{deny_save}; $self->in_memory (cf::MAP_SWAPPED); @@ -2134,7 +2242,7 @@ return if $self->players; - warn "resetting map ", $self->path; + warn "resetting map ", $self->path, "\n"; $self->in_memory (cf::MAP_SWAPPED); @@ -2227,14 +2335,45 @@ [ map { utf8::decode $_; - /\.map$/ - ? normalise $_ - : () + s/\.map$//; # TODO future compatibility hack + /\.pst$/ || !/^$PATH_SEP/o # TODO unique maps apparebntly lack the .map suffix :/ + ? () + : normalise $_ } @{ aio_readdir $UNIQUEDIR or [] } ] } -package cf; +=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 @@ -2248,7 +2387,8 @@ =item $ob->inv_recursive -Returns the inventory of the object _and_ their inventories, recursively. +Returns the inventory of the object I their inventories, recursively, +but I the object itself. =cut @@ -2263,7 +2403,7 @@ =item $ref = $ob->ref -creates and returns a persistent reference to an objetc that can be stored as a string. +Creates and returns a persistent reference to an object that can be stored as a string. =item $ob = cf::object::deref ($refstring) @@ -2305,6 +2445,20 @@ =cut +our $SAY_CHANNEL = { + id => "say", + title => "Map", + reply => "say ", + tooltip => "Things said to and replied from NPCs near you and other players on the same map only.", +}; + +our $CHAT_CHANNEL = { + id => "chat", + title => "Chat", + reply => "chat ", + tooltip => "Player chat and shouts, global to the server.", +}; + # rough implementation of a future "reply" method that works # with dialog boxes. #TODO: the first argument must go, split into a $npc->reply_to ( method @@ -2325,7 +2479,7 @@ } else { $msg = $npc->name . " says: $msg" if $npc; - $self->message ($msg, $flags); + $self->send_msg ($SAY_CHANNEL => $msg, $flags); } } } @@ -2360,8 +2514,10 @@ Freezes the player and moves him/her to a special map (C<{link}>). -The player should be reasonably safe there for short amounts of time. You -I call C as soon as possible, though. +The player should be reasonably safe there for short amounts of time (e.g. +for loading a map). You I call C as soon as possible, +though, as the palyer cannot control the character while it is on the link +map. Will never block. @@ -2429,10 +2585,12 @@ $map->load_neighbours; return unless $self->contr->active; - $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->()]]) @@ -2443,6 +2601,9 @@ the new (success) or old (failed) map position. In either case, $done will be called at the end of this process. +Note that $check will be called with a potentially non-loaded map, so if +it needs a loaded map it has to call C<< ->load >>. + =cut our $GOTOGEN; @@ -2530,8 +2691,6 @@ sub prepare_random_map { my ($exit) = @_; - my $guard = cf::lock_acquire "exit_prepare:$exit"; - # all this does is basically replace the /! path by # a new random map path (?random/...) with a seed # that depends on the exit object @@ -2543,11 +2702,13 @@ $rmp->{origin_map} = $exit->map->path; $rmp->{origin_x} = $exit->x; $rmp->{origin_y} = $exit->y; + + $exit->map->touch; } $rmp->{random_seed} ||= $exit->random_seed; - my $data = cf::to_json $rmp; + my $data = JSON::XS->new->utf8->pretty->canonical->encode ($rmp); my $md5 = Digest::MD5::md5_hex $data; my $meta = "$RANDOMDIR/$md5.meta"; @@ -2556,8 +2717,12 @@ undef $fh; aio_rename "$meta~", $meta; - $exit->slaying ("?random/$md5"); - $exit->msg (undef); + my $slaying = "?random/$md5"; + + if ($exit->valid) { + $exit->slaying ("?random/$md5"); + $exit->msg (undef); + } } } @@ -2566,33 +2731,35 @@ return unless $self->type == cf::PLAYER; - if ($exit->slaying eq "/!") { - #TODO: this should de-fi-ni-te-ly not be a sync-job - # the problem is that $exit might not survive long enough - # so it needs to be done right now, right here - cf::sync_job { prepare_random_map $exit }; - } - - my $slaying = cf::map::normalise $exit->slaying, $exit->map && $exit->map->path; - my $hp = $exit->stats->hp; - my $sp = $exit->stats->sp; - $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 { - $Coro::current->{desc} = "enter_exit $slaying $hp $sp"; + $Coro::current->{desc} = "enter_exit"; - $self->deactivate_recursive; # just to be sure unless (eval { - $self->goto ($slaying, $hp, $sp); + $self->deactivate_recursive; # just to be sure + + # random map handling + { + my $guard = cf::lock_acquire "exit_prepare:$exit"; - 1; + prepare_random_map $exit + if $exit->slaying eq "/!"; + } + + my $map = cf::map::normalise $exit->slaying, $exit->map && $exit->map->path; + my $x = $exit->stats->hp; + my $y = $exit->stats->sp; + + $self->goto ($map, $x, $y); + + # if exit is damned, update players death & WoR home-position + $self->contr->savebed ($map, $x, $y) + if $exit->flag (cf::FLAG_DAMNED); + + 1 }) { - $self->message ("Something went wrong deep within the crossfire server. " + $self->message ("Something went wrong deep within the deliantra server. " . "I'll try to bring you back to the map you were before. " . "Please report this to the dungeon master!", cf::NDI_UNIQUE | cf::NDI_RED); @@ -2630,7 +2797,14 @@ =cut +# 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", @@ -2643,18 +2817,105 @@ 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", + reply => undef, + tooltip => "The contents of a note or book", + }, "c/lookat" => { id => "infobox", title => "Look", reply => undef, tooltip => "What you saw there", }, + "c/who" => { + id => "infobox", + title => "Players", + reply => undef, + tooltip => "Shows players who are currently online", + }, + "c/body" => { + id => "infobox", + title => "Body Parts", + 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", + reply => undef, + tooltip => "How long the server has been running since last restart", + }, + "c/mapinfo" => { + id => "infobox", + title => "Map Info", + reply => undef, + tooltip => "Information related to the maps", + }, + "c/party" => { + id => "party", + title => "Party", + 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 { my ($self, $channel, $msg, $color, @extra) = @_; - $msg = $self->pl->expand_cfpod ($msg); + $msg = $self->pl->expand_cfpod ($msg) + unless $color & cf::NDI_VERBATIM; $color &= cf::NDI_CLIENT_MASK; # just in case... @@ -2662,17 +2923,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}; @@ -2680,38 +2938,26 @@ 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") - } - } + # 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"; } + + $self->send_packet ($pkt); } =item $client->ext_msg ($type, @msg) @@ -2894,8 +3140,9 @@ The following functions and methods are available within a safe environment: cf::object - contr pay_amount pay_player map x y force_find force_add - insert remove name archname title slaying race decrease_ob_nr + 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 @@ -2910,10 +3157,10 @@ for ( ["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)], + insert remove inv nrof name archname title slaying race + 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'; @@ -2941,6 +3188,8 @@ $qcode =~ s/"/‟/g; # not allowed in #line filenames $qcode =~ s/\n/\\n/g; + %vars = (_dummy => 0) unless %vars; + local $_; local @safe::cf::_safe_eval_args = values %vars; @@ -3026,6 +3275,7 @@ while (my ($face, $info) = each %$faces) { my $idx = (cf::face::find $face) || cf::face::alloc $face; + 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}; @@ -3036,8 +3286,10 @@ while (my ($face, $info) = each %$faces) { next unless $info->{smooth}; + my $idx = cf::face::find $face or next; + if (my $smooth = cf::face::find $info->{smooth}) { cf::face::set_smooth $idx, $smooth; cf::face::set_smoothlevel $idx, $info->{smoothlevel}; @@ -3065,52 +3317,58 @@ # 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 (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} || {} }, + }); - 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}; + } - $data = pack "(w/a*)*", $meta, $info->{data}; + cf::face::set_data $idx, 0, $data, Digest::MD5::md5 $data; + cf::face::set_type $idx, $info->{type}; } else { - $data = $info->{data}; + $RESOURCE{$name} = $info; } - 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}); + cf::global->invoke (EVENT_GLOBAL_RESOURCE_UPDATE); - for (0 .. SOUND_CAST_SPELL_0 - 1) { - my $sound = $soundconf->{compat}[$_] - or next; + 1 +} - my $face = cf::face::find "sound/$sound->[1]"; - cf::sound::set $sound->[0] => $face; - cf::sound::old_sound_index $_, $face; # gcfclient-compat - } +cf::global->attach (on_resource_update => sub { + if (my $soundconf = $RESOURCE{"res/sound.conf"}) { + $soundconf = JSON::XS->new->utf8->relaxed->decode ($soundconf->{data}); - while (my ($k, $v) = each %{$soundconf->{event}}) { - my $face = cf::face::find "sound/$v"; - cf::sound::set $k => $face; - } + 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 } - } - 1 -} + while (my ($k, $v) = each %{$soundconf->{event}}) { + my $face = cf::face::find "sound/$v"; + cf::sound::set $k => $face; + } + } +}); register_exticmd fx_want => sub { my ($ns, $want) = @_; @@ -3120,6 +3378,16 @@ } }; +sub load_resource_file($) { + my $guard = lock_acquire "load_resource_file"; + + my $status = load_resource_file_ $_[0]; + get_slot 0.1, 100; + cf::arch::commit_load; + + $status +} + sub reload_regions { # HACK to clear player env face cache, we need some signal framework # for this (global event?) @@ -3142,11 +3410,6 @@ 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 { @@ -3157,25 +3420,22 @@ sub reload_resources { warn "reloading resource files...\n"; - reload_regions; reload_facedata; - #reload_archetypes;#d# reload_archetypes; + reload_regions; reload_treasures; warn "finished reloading resource files\n"; } -sub init { - reload_resources; -} - sub reload_config { + warn "reloading config file...\n"; + open my $fh, "<:utf8", "$CONFDIR/config" or return; local $/; - *CFG = YAML::Syck::Load <$fh>; + *CFG = YAML::XS::Load scalar <$fh>; $EMERGENCY_POSITION = $CFG{emergency_position} || ["/world/world_105_115", 5, 37]; @@ -3189,24 +3449,92 @@ }; warn $@ if $@; } + + warn "finished reloading resource files\n"; +} + +sub pidfile() { + sysopen my $fh, $PIDFILE, O_RDWR | O_CREAT + or die "$PIDFILE: $!"; + flock $fh, &Fcntl::LOCK_EX + or die "$PIDFILE: flock: $!"; + $fh +} + +# make sure only one server instance is running at any one time +sub atomic { + my $fh = pidfile; + + my $pid = <$fh>; + kill 9, $pid if $pid > 0; + + seek $fh, 0, 0; + 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 { + 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 { Carp::cluck "FATAL: Coro::idle was called, major BUG, use cf::sync_job!\n";#d# (async { $Coro::current->{desc} = "IDLE BUG HANDLER"; - Event::one_event; + EV::loop EV::LOOP_ONESHOT; })->prio (Coro::PRIO_MAX); }; - reload_config; - db_init; - load_extensions; + evthread_start IO::AIO::poll_fileno; - $TICK_WATCHER->start; - Event::loop; + cf::sync_job { + reload_resources; + reload_config; + db_init; + + cf::load_settings; + cf::load_materials; + cf::init_uuid; + cf::init_signals; + cf::init_commands; + cf::init_skills; + + cf::init_beforeplay; + + atomic; + + load_extensions; + + 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}; + + (pop @POST_INIT)->(0) while @POST_INIT; + }; + + main_loop; } ############################################################################# @@ -3214,30 +3542,23 @@ # install some emergency cleanup handlers BEGIN { + our %SIGWATCHER = (); for my $signal (qw(INT HUP TERM)) { - Event->signal ( - reentrant => 0, - data => WF_AUTOCANCEL, - signal => $signal, - prio => 0, - cb => sub { - cf::cleanup "SIG$signal"; - }, - ); + $SIGWATCHER{$signal} = EV::signal $signal, sub { + cf::cleanup "SIG$signal"; + }; } } -sub write_runtime { - my $runtime = "$LOCALDIR/runtime"; - +sub write_runtime_sync { # 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; + IO::AIO::aio_utime $RUNTIMEFILE, undef, undef; my $guard = cf::lock_acquire "write_runtime"; - my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 + my $fh = aio_open "$RUNTIMEFILE~", O_WRONLY | O_CREAT, 0644 or return; my $value = $cf::RUNTIME + 90 + 10; @@ -3257,7 +3578,7 @@ close $fh or return; - aio_rename "$runtime~", $runtime + aio_rename "$RUNTIMEFILE~", $RUNTIMEFILE and return; warn "runtime file written.\n"; @@ -3265,37 +3586,102 @@ 1 } +our $uuid_lock; +our $uuid_skip; + +sub write_uuid_sync($) { + $uuid_skip ||= $_[0]; + + return if $uuid_lock; + local $uuid_lock = 1; + + my $uuid = "$LOCALDIR/uuid"; + + my $fh = aio_open "$uuid~", O_WRONLY | O_CREAT, 0644 + or return; + + 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 + and return; + + # always fsync - this file is important + aio_fsync $fh + and return; + + close $fh + or return; + + aio_rename "$uuid~", $uuid + and return; + + warn "uuid file written ($value).\n"; + + 1 + +} + +sub write_uuid($$) { + my ($skip, $sync) = @_; + + $sync ? write_uuid_sync $skip + : async { write_uuid_sync $skip }; +} + 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 "emergency_perl_save: begin write uuid\n"; + write_uuid_sync 1; + 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 { @@ -3303,6 +3689,35 @@ warn Carp::longmess "post_cleanup backtrace" if $make_core; + + my $fh = pidfile; + unlink $PIDFILE if <$fh> == $$; +} + +# a safer delete_package, copied from Symbol +sub clear_package($) { + my $pkg = shift; + + # expand to full symbol table name if needed + unless ($pkg =~ /^main::.*::$/) { + $pkg = "main$pkg" if $pkg =~ /^::/; + $pkg = "main::$pkg" unless $pkg =~ /^main::/; + $pkg .= '::' unless $pkg =~ /::$/; + } + + my($stem, $leaf) = $pkg =~ m/(.*::)(\w+::)$/; + my $stem_symtab = *{$stem}{HASH}; + + defined $stem_symtab and exists $stem_symtab->{$leaf} + or return; + + # clear all symbols + my $leaf_symtab = *{$stem_symtab->{$leaf}}{HASH}; + for my $name (keys %$leaf_symtab) { + _gv_clear *{"$pkg$name"}; +# use PApp::Util; PApp::Util::sv_dump *{"$pkg$name"}; + } + warn "cleared package $pkg\n";#d# } sub do_reload_perl() { @@ -3312,120 +3727,123 @@ return; } - warn "reloading..."; + return if $RELOAD++; - warn "entering sync_job"; + my $t1 = EV::time; - cf::sync_job { - cf::write_runtime; # external watchdog should not bark - cf::emergency_save; - cf::write_runtime; # external watchdog should not bark + while ($RELOAD) { + warn "reloading..."; - warn "syncing database to disk"; - BDB::db_env_txn_checkpoint $DB_ENV; + warn "entering sync_job"; - # if anything goes wrong in here, we should simply crash as we already saved + cf::sync_job { + cf::write_runtime_sync; # external watchdog should not bark + cf::emergency_save; + cf::write_runtime_sync; # external watchdog should not bark - warn "cancelling all WF_AUTOCANCEL watchers"; - for (Event::all_watchers) { - $_->cancel if $_->data & WF_AUTOCANCEL; - } + warn "syncing database to disk"; + BDB::db_env_txn_checkpoint $DB_ENV; - warn "flushing outstanding aio requests"; - for (;;) { - BDB::flush; - IO::AIO::flush; - Coro::cede; - last unless IO::AIO::nreqs || BDB::nreqs; - warn "iterate..."; - } + # if anything goes wrong in here, we should simply crash as we already saved - ++$RELOAD; + warn "flushing outstanding aio requests"; + while (IO::AIO::nreqs || BDB::nreqs) { + Coro::EV::timer_once 0.01; # let the sync_job do it's thing + } + + warn "cancelling all extension coros"; + $_->cancel for values %EXT_CORO; + %EXT_CORO = (); - warn "cancelling all extension coros"; - $_->cancel for values %EXT_CORO; - %EXT_CORO = (); + warn "removing commands"; + %COMMAND = (); - warn "removing commands"; - %COMMAND = (); + warn "removing ext/exti commands"; + %EXTCMD = (); + %EXTICMD = (); - warn "removing ext/exti commands"; - %EXTCMD = (); - %EXTICMD = (); + warn "unloading/nuking all extensions"; + for my $pkg (@EXTS) { + warn "... unloading $pkg"; - warn "unloading/nuking all extensions"; - for my $pkg (@EXTS) { - warn "... unloading $pkg"; + if (my $cb = $pkg->can ("unload")) { + eval { + $cb->($pkg); + 1 + } or warn "$pkg unloaded, but with errors: $@"; + } - if (my $cb = $pkg->can ("unload")) { - eval { - $cb->($pkg); - 1 - } or warn "$pkg unloaded, but with errors: $@"; + warn "... clearing $pkg"; + clear_package $pkg; } - warn "... nuking $pkg"; - Symbol::delete_package $pkg; - } + warn "unloading all perl modules loaded from $LIBDIR"; + while (my ($k, $v) = each %INC) { + next unless $v =~ /^\Q$LIBDIR\E\/.*\.pm$/; - warn "unloading all perl modules loaded from $LIBDIR"; - while (my ($k, $v) = each %INC) { - next unless $v =~ /^\Q$LIBDIR\E\/.*\.pm$/; + warn "... unloading $k"; + delete $INC{$k}; - warn "... unloading $k"; - delete $INC{$k}; + $k =~ s/\.pm$//; + $k =~ s/\//::/g; - $k =~ s/\.pm$//; - $k =~ s/\//::/g; + if (my $cb = $k->can ("unload_module")) { + $cb->(); + } - if (my $cb = $k->can ("unload_module")) { - $cb->(); + clear_package $k; } - Symbol::delete_package $k; - } + warn "getting rid of safe::, as good as possible"; + clear_package "safe::$_" + for qw(cf::attachable cf::object cf::object::player cf::client cf::player cf::map cf::party cf::region); - warn "getting rid of safe::, as good as possible"; - Symbol::delete_package "safe::$_" - for qw(cf::attachable cf::object cf::object::player cf::client cf::player cf::map cf::party cf::region); + warn "unloading cf.pm \"a bit\""; + delete $INC{"cf.pm"}; + delete $INC{"cf/$_.pm"} for @EXTRA_MODULES; - warn "unloading cf.pm \"a bit\""; - delete $INC{"cf.pm"}; - delete $INC{"cf/pod.pm"}; + # don't, removes xs symbols, too, + # and global variables created in xs + #clear_package __PACKAGE__; - # don't, removes xs symbols, too, - # and global variables created in xs - #Symbol::delete_package __PACKAGE__; + warn "unload completed, starting to reload now"; - warn "unload completed, starting to reload now"; + warn "reloading cf.pm"; + require cf; + cf::_connect_to_perl_1; - warn "reloading cf.pm"; - require cf; - cf::_connect_to_perl; # nominally unnecessary, but cannot hurt + warn "loading config and database again"; + cf::reload_config; - warn "loading config and database again"; - cf::reload_config; + warn "loading extensions"; + cf::load_extensions; - warn "loading extensions"; - cf::load_extensions; + 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 "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"; + warn "leaving sync_job"; - 1 - } or do { - warn $@; - warn "error while reloading, exiting."; - exit 1; - }; + 1 + } or do { + warn $@; + cf::cleanup "error while reloading, exiting."; + }; + + warn "reloaded"; + --$RELOAD; + } - warn "reloaded"; + $t1 = EV::time - $t1; + warn "reload completed in ${t1}s\n"; }; our $RELOAD_WATCHER; # used only during reload @@ -3434,15 +3852,14 @@ # doing reload synchronously and two reloads happen back-to-back, # coro crashes during coro_state_free->destroy here. - $RELOAD_WATCHER ||= Event->timer ( - reentrant => 0, - after => 0, - data => WF_AUTOCANCEL, - cb => sub { + $RELOAD_WATCHER ||= cf::async { + Coro::AIO::aio_wait cache_extensions; + + $RELOAD_WATCHER = EV::timer $TICK * 1.5, 0, sub { do_reload_perl; undef $RELOAD_WATCHER; - }, - ); + }; + }; } register_command "reload" => sub { @@ -3465,8 +3882,7 @@ our @WAIT_FOR_TICK_BEGIN; sub wait_for_tick { - return unless $TICK_WATCHER->is_active; - return if $Coro::current == $Coro::main; + return if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK, $signal; @@ -3474,96 +3890,62 @@ } sub wait_for_tick_begin { - return unless $TICK_WATCHER->is_active; - return if $Coro::current == $Coro::main; + return if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK_BEGIN, $signal; $signal->wait; } -$TICK_WATCHER = Event->timer ( - reentrant => 0, - parked => 1, - prio => 0, - at => $NEXT_TICK || $TICK, - data => WF_AUTOCANCEL, - cb => sub { - if ($Coro::current != $Coro::main) { - Carp::cluck "major BUG: server tick called outside of main coro, skipping it" - unless ++$bug_warning > 10; - return; - } - - $NOW = $tick_start = Event::time; - - cf::server_tick; # one server iteration - - $RUNTIME += $TICK; - $NEXT_TICK += $TICK; - - if ($NOW >= $NEXT_RUNTIME_WRITE) { - $NEXT_RUNTIME_WRITE = $NOW + 10; - Coro::async_pool { - $Coro::current->{desc} = "runtime saver"; - write_runtime - or warn "ERROR: unable to write runtime file: $!"; - }; - } - - if (my $sig = shift @WAIT_FOR_TICK_BEGIN) { - $sig->send; - } - while (my $sig = shift @WAIT_FOR_TICK) { - $sig->send; - } +sub tick { + if ($Coro::current != $Coro::main) { + Carp::cluck "major BUG: server tick called outside of main coro, skipping it" + unless ++$bug_warning > 10; + return; + } - $NOW = Event::time; + cf::server_tick; # one server iteration - # if we are delayed by four ticks or more, skip them all - $NEXT_TICK = $NOW if $NOW >= $NEXT_TICK + $TICK * 4; + if ($NOW >= $NEXT_RUNTIME_WRITE) { + $NEXT_RUNTIME_WRITE = List::Util::max $NEXT_RUNTIME_WRITE + 10, $NOW + 5.; + Coro::async_pool { + $Coro::current->{desc} = "runtime saver"; + write_runtime_sync + or warn "ERROR: unable to write runtime file: $!"; + }; + } - $TICK_WATCHER->at ($NEXT_TICK); - $TICK_WATCHER->start; + if (my $sig = shift @WAIT_FOR_TICK_BEGIN) { + $sig->send; + } + while (my $sig = shift @WAIT_FOR_TICK) { + $sig->send; + } - $LOAD = ($NOW - $tick_start) / $TICK; - $LOADAVG = $LOADAVG * 0.75 + $LOAD * 0.25; + $LOAD = ($NOW - $TICK_START) / $TICK; + $LOADAVG = $LOADAVG * 0.75 + $LOAD * 0.25; - _post_tick; - }, -); + if (0) { + if ($NEXT_TICK) { + my $jitter = $TICK_START - $NEXT_TICK; + $JITTER = $JITTER * 0.75 + $jitter * 0.25; + warn "jitter $JITTER\n";#d# + } + } +} { - BDB::min_parallel 8; - BDB::max_poll_time $TICK * 0.1; - $BDB_POLL_WATCHER = Event->io ( - reentrant => 0, - fd => BDB::poll_fileno, - poll => 'r', - prio => 0, - data => WF_AUTOCANCEL, - cb => \&BDB::poll_cb, - ); + # configure BDB - BDB::set_sync_prepare { - my $status; - my $current = $Coro::current; - ( - sub { - $status = $!; - $current->ready; undef $current; - }, - sub { - Coro::schedule while defined $current; - $! = $status; - }, - ) - }; + BDB::min_parallel 8; + BDB::max_poll_reqs $TICK * 0.1; + $AnyEvent::BDB::WATCHER->priority (1); unless ($DB_ENV) { $DB_ENV = BDB::db_env_create; - $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT | BDB::TXN_NOSYNC - | BDB::LOG_AUTOREMOVE, 1); + $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT); + $DB_ENV->set_flags (&BDB::LOG_AUTOREMOVE ) if BDB::VERSION v0, v4.7; + $DB_ENV->log_set_config (&BDB::LOG_AUTO_REMOVE) if BDB::VERSION v4.7; $DB_ENV->set_timeout (30, BDB::SET_TXN_TIMEOUT); $DB_ENV->set_timeout (30, BDB::SET_LOCK_TIMEOUT); @@ -3583,51 +3965,23 @@ }; } - $BDB_DEADLOCK_WATCHER = Event->timer ( - after => 3, - interval => 1, - hard => 1, - prio => 0, - data => WF_AUTOCANCEL, - cb => sub { - BDB::db_env_lock_detect $DB_ENV, 0, BDB::LOCK_DEFAULT, 0, sub { }; - }, - ); - $BDB_CHECKPOINT_WATCHER = Event->timer ( - after => 11, - interval => 60, - hard => 1, - prio => 0, - data => WF_AUTOCANCEL, - cb => sub { - BDB::db_env_txn_checkpoint $DB_ENV, 0, 0, 0, sub { }; - }, - ); - $BDB_TRICKLE_WATCHER = Event->timer ( - after => 5, - interval => 10, - hard => 1, - prio => 0, - data => WF_AUTOCANCEL, - cb => sub { - BDB::db_env_memp_trickle $DB_ENV, 20, 0, sub { }; - }, - ); + $BDB_DEADLOCK_WATCHER = EV::periodic 0, 3, 0, sub { + BDB::db_env_lock_detect $DB_ENV, 0, BDB::LOCK_DEFAULT, 0, sub { }; + }; + $BDB_CHECKPOINT_WATCHER = EV::periodic 0, 60, 0, sub { + BDB::db_env_txn_checkpoint $DB_ENV, 0, 0, 0, sub { }; + }; + $BDB_TRICKLE_WATCHER = EV::periodic 0, 10, 0, sub { + BDB::db_env_memp_trickle $DB_ENV, 20, 0, sub { }; + }; } { - IO::AIO::min_parallel 8; + # configure IO::AIO - undef $Coro::AIO::WATCHER; + IO::AIO::min_parallel 8; IO::AIO::max_poll_time $TICK * 0.1; - $AIO_POLL_WATCHER = Event->io ( - reentrant => 0, - data => WF_AUTOCANCEL, - fd => IO::AIO::poll_fileno, - poll => 'r', - prio => 0, - cb => \&IO::AIO::poll_cb, - ); + undef $AnyEvent::AIO::WATCHER; } my $_log_backtrace; @@ -3640,6 +3994,7 @@ # limit the # of concurrent backtraces if ($_log_backtrace < 2) { ++$_log_backtrace; + my $perl_bt = Carp::longmess $msg; async { $Coro::current->{desc} = "abt $msg"; @@ -3660,7 +4015,8 @@ @funcs }; - LOG llevInfo, "[ABT] $msg\n"; + LOG llevInfo, "[ABT] $perl_bt\n"; + LOG llevInfo, "[ABT] --- C backtrace follows ---\n"; LOG llevInfo, "[ABT] $_\n" for @bt; --$_log_backtrace; }; @@ -3671,7 +4027,8 @@ } # load additional modules -use cf::pod; +require "cf/$_.pm" for @EXTRA_MODULES; +cf::_connect_to_perl_2; END { cf::emergency_save }