ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/lib/cf.pm
Revision: 1.36
Committed: Thu Aug 24 14:04:29 2006 UTC (17 years, 9 months ago) by root
Branch: MAIN
Changes since 1.35: +59 -48 lines
Log Message:
support perl_reload from dmshell

File Contents

# User Rev Content
1 root 1.1 package cf;
2    
3     use Symbol;
4     use List::Util;
5 root 1.6 use Storable;
6 root 1.23 use Opcode;
7     use Safe;
8     use Safe::Hole;
9 root 1.19
10 root 1.32 use Time::HiRes;
11 root 1.18 use Event;
12 root 1.19 $Event::Eval = 1; # no idea why this is required, but it is
13 root 1.1
14     use strict;
15    
16     our %COMMAND;
17     our @EVENT;
18     our %PROP_TYPE;
19     our %PROP_IDX;
20 root 1.27 our $LIBDIR = maps_directory "perl";
21 root 1.1
22 root 1.35 our $TICK = MAX_TIME * 1e-6;
23     our $TICK_WATCHER;
24     our $NEXT_TICK;
25    
26 root 1.1 BEGIN {
27     @EVENT = map lc, @EVENT;
28    
29     *CORE::GLOBAL::warn = sub {
30     my $msg = join "", @_;
31     $msg .= "\n"
32     unless $msg =~ /\n$/;
33    
34     print STDERR "cfperl: $msg";
35     LOG llevError, "cfperl: $msg";
36     };
37     }
38    
39 root 1.9 my %ignore_set = (MAP_PROP_PATH => 1); # I hate the plug-in api. Deeply!
40    
41 root 1.1 # generate property mutators
42     sub prop_gen {
43     my ($prefix, $class) = @_;
44    
45     no strict 'refs';
46    
47     for my $prop (keys %PROP_TYPE) {
48     $prop =~ /^\Q$prefix\E_(.*$)/ or next;
49     my $sub = lc $1;
50    
51     my $type = $PROP_TYPE{$prop};
52     my $idx = $PROP_IDX {$prop};
53    
54     *{"$class\::get_$sub"} = *{"$class\::$sub"} = sub {
55     $_[0]->get_property ($type, $idx)
56     };
57    
58     *{"$class\::set_$sub"} = sub {
59     $_[0]->set_property ($type, $idx, $_[1]);
60 root 1.9 } unless $ignore_set{$prop};
61 root 1.1 }
62     }
63    
64     # auto-generate most of the API
65    
66     prop_gen OBJECT_PROP => "cf::object";
67     # CFAPI_OBJECT_ANIMATION?
68     prop_gen PLAYER_PROP => "cf::object::player";
69    
70     prop_gen MAP_PROP => "cf::map";
71     prop_gen ARCH_PROP => "cf::arch";
72    
73     # guessed hierarchies
74    
75 root 1.25 @ext::cf::object::player::ISA = @cf::object::player::ISA = 'cf::object';
76     @ext::cf::object::map::ISA = @cf::object::map::ISA = 'cf::object';
77    
78     # we bless all objects into derived classes to force a method lookup
79     # within the Safe compartment.
80 elmex 1.30 for my $pkg (qw(cf::object cf::object::map cf::object::player cf::player cf::map cf::party cf::region cf::arch)) {
81 root 1.25 no strict 'refs';
82     @{"ext::$pkg\::wrap::ISA"} = @{"$pkg\::wrap::ISA"} = $pkg;
83     }
84 root 1.1
85 root 1.18 $Event::DIED = sub {
86     warn "error in event callback: @_";
87     };
88    
89 root 1.5 my %ext_pkg;
90 root 1.1 my @exts;
91     my @hook;
92     my %command;
93 root 1.15 my %extcmd;
94 root 1.1
95     sub inject_event {
96 root 1.14 my $extension = shift;
97     my $event_code = shift;
98 root 1.1
99 root 1.14 my $cb = $hook[$event_code]{$extension}
100 root 1.5 or return;
101    
102 root 1.14 &$cb
103 root 1.5 }
104    
105     sub inject_global_event {
106 root 1.12 my $event = shift;
107 root 1.5
108 root 1.12 my $cb = $hook[$event]
109 root 1.1 or return;
110    
111 root 1.12 List::Util::max map &$_, values %$cb
112 root 1.1 }
113    
114     sub inject_command {
115     my ($name, $obj, $params) = @_;
116    
117     for my $cmd (@{ $command{$name} }) {
118     $cmd->[1]->($obj, $params);
119     }
120    
121     -1
122     }
123    
124     sub register_command {
125     my ($name, $time, $cb) = @_;
126    
127     my $caller = caller;
128 root 1.16 #warn "registering command '$name/$time' to '$caller'";
129 root 1.4
130 root 1.1 push @{ $command{$name} }, [$time, $cb, $caller];
131     $COMMAND{"$name\000"} = List::Util::max map $_->[0], @{ $command{$name} };
132     }
133    
134 root 1.16 sub register_extcmd {
135     my ($name, $cb) = @_;
136    
137     my $caller = caller;
138     #warn "registering extcmd '$name' to '$caller'";
139    
140     $extcmd{$name} = [$cb, $caller];
141     }
142    
143 root 1.6 sub register {
144     my ($base, $pkg) = @_;
145    
146     for my $idx (0 .. $#EVENT) {
147     if (my $ref = $pkg->can ("on_$EVENT[$idx]")) {
148 root 1.16 #warn "registering $EVENT[$idx] hook to '$pkg'\n";
149 root 1.6 $hook[$idx]{$base} = $ref;
150     }
151     }
152     }
153    
154 root 1.1 sub load_extension {
155     my ($path) = @_;
156    
157     $path =~ /([^\/\\]+)\.ext$/ or die "$path";
158 root 1.5 my $base = $1;
159 root 1.1 my $pkg = $1;
160     $pkg =~ s/[^[:word:]]/_/g;
161     $pkg = "cf::ext::$pkg";
162    
163     warn "loading '$path' into '$pkg'\n";
164    
165     open my $fh, "<:utf8", $path
166     or die "$path: $!";
167    
168     my $source =
169     "package $pkg; use strict; use utf8;\n"
170     . "#line 1 \"$path\"\n{\n"
171     . (do { local $/; <$fh> })
172     . "\n};\n1";
173    
174     eval $source
175     or die "$path: $@";
176    
177     push @exts, $pkg;
178 root 1.5 $ext_pkg{$base} = $pkg;
179 root 1.1
180 root 1.6 # no strict 'refs';
181 root 1.23 # @{"$pkg\::ISA"} = ext::;
182 root 1.1
183 root 1.6 register $base, $pkg;
184 root 1.1 }
185    
186     sub unload_extension {
187     my ($pkg) = @_;
188    
189     warn "removing extension $pkg\n";
190    
191     # remove hooks
192     for my $idx (0 .. $#EVENT) {
193     delete $hook[$idx]{$pkg};
194     }
195    
196     # remove commands
197     for my $name (keys %command) {
198     my @cb = grep $_->[2] ne $pkg, @{ $command{$name} };
199    
200     if (@cb) {
201     $command{$name} = \@cb;
202     $COMMAND{"$name\000"} = List::Util::max map $_->[0], @cb;
203     } else {
204     delete $command{$name};
205     delete $COMMAND{"$name\000"};
206     }
207     }
208    
209 root 1.15 # remove extcmds
210 root 1.16 for my $name (grep $extcmd{$_}[1] eq $pkg, keys %extcmd) {
211     delete $extcmd{$name};
212 root 1.15 }
213    
214 elmex 1.31 if (my $cb = $pkg->can ("on_unload")) {
215     eval {
216     $cb->($pkg);
217     1
218     } or warn "$pkg unloaded, but with errors: $@";
219     }
220    
221 root 1.1 Symbol::delete_package $pkg;
222     }
223    
224     sub load_extensions {
225     my $LIBDIR = maps_directory "perl";
226    
227     for my $ext (<$LIBDIR/*.ext>) {
228 root 1.3 next unless -r $ext;
229 root 1.2 eval {
230     load_extension $ext;
231     1
232     } or warn "$ext not loaded: $@";
233 root 1.1 }
234     }
235    
236 root 1.36 sub _perl_reload(&) {
237     my ($msg) = @_;
238    
239     $msg->("reloading...");
240    
241     eval {
242     # 1. cancel all watchers
243     $_->cancel for Event::all_watchers;
244    
245     # 2. unload all extensions
246     for (@exts) {
247     $msg->("unloading <$_>");
248     unload_extension $_;
249     }
250    
251     # 3. unload all modules loaded from $LIBDIR
252     while (my ($k, $v) = each %INC) {
253     next unless $v =~ /^\Q$LIBDIR\E\/.*\.pm$/;
254    
255     $msg->("removing <$k>");
256     delete $INC{$k};
257 root 1.1
258 root 1.36 $k =~ s/\.pm$//;
259     $k =~ s/\//::/g;
260 root 1.3
261 root 1.36 if (my $cb = $k->can ("unload_module")) {
262     $cb->();
263 root 1.27 }
264    
265 root 1.36 Symbol::delete_package $k;
266     }
267 root 1.27
268 root 1.36 # 4. get rid of ext::, as good as possible
269     Symbol::delete_package "ext::$_"
270     for qw(cf::object cf::object::map cf::object::player cf::player cf::map cf::party cf::region);
271    
272     # 5. remove register_script_function callbacks
273     # TODO
274    
275     # 6. unload cf.pm "a bit"
276     delete $INC{"cf.pm"};
277    
278     # don't, removes xs symbols, too
279     #Symbol::delete_package __PACKAGE__;
280    
281     # 7. reload cf.pm
282     $msg->("reloading cf.pm");
283     require cf;
284     };
285     $msg->($@) if $@;
286 root 1.27
287 root 1.36 $msg->("reloaded");
288     };
289 root 1.27
290 root 1.36 sub perl_reload() {
291     _perl_reload {
292     warn $_[0];
293     print "$_[0]\n";
294     };
295     }
296 root 1.27
297 root 1.36 register_command "perl-reload", 0, sub {
298     my ($who, $arg) = @_;
299 root 1.27
300 root 1.36 if ($who->flag (FLAG_WIZ)) {
301     _perl_reload {
302     warn $_[0];
303     $who->message ($_[0]);
304 root 1.4 };
305 root 1.1 }
306     };
307    
308 root 1.8 #############################################################################
309 root 1.28 # utility functions
310    
311     use JSON::Syck (); # TODO# replace by JSON::PC once working
312    
313     sub from_json($) {
314 root 1.29 $JSON::Syck::ImplicitUnicode = 1; # work around JSON::Syck bugs
315 root 1.28 JSON::Syck::Load $_[0]
316     }
317    
318     sub to_json($) {
319 root 1.29 $JSON::Syck::ImplicitUnicode = 0; # work around JSON::Syck bugs
320 root 1.28 JSON::Syck::Dump $_[0]
321     }
322    
323     #############################################################################
324     # extcmd framework, basically convert ext <msg>
325 root 1.15 # into pkg::->on_extcmd_arg1 (...) while shortcutting a few
326    
327     sub on_extcmd {
328     my ($pl, $buf) = @_;
329    
330 root 1.28 my $msg = eval { from_json $buf };
331    
332     if (ref $msg) {
333     if (my $cb = $extcmd{$msg->{msgtype}}) {
334     if (my %reply = $cb->[0]->($pl, $msg)) {
335     $pl->ext_reply ($msg->{msgid}, %reply);
336     }
337     }
338     } else {
339     warn "player " . ($pl->ob->name) . " sent unparseable ext message: <$buf>\n";
340     }
341 root 1.15
342 root 1.28 1
343 root 1.15 }
344    
345     #############################################################################
346 root 1.8 # load/save/clean perl data associated with a map
347    
348 root 1.7 *on_mapclean = sub {
349 root 1.13 my ($map) = @_;
350 root 1.7
351     my $path = $map->tmpname;
352     defined $path or return;
353    
354     unlink "$path.cfperl";
355     };
356    
357 root 1.6 *on_mapin =
358     *on_mapload = sub {
359 root 1.13 my ($map) = @_;
360 root 1.6
361     my $path = $map->tmpname;
362     $path = $map->path unless defined $path;
363    
364     open my $fh, "<:raw", "$path.cfperl"
365     or return; # no perl data
366    
367     my $data = Storable::thaw do { local $/; <$fh> };
368    
369     $data->{version} <= 1
370     or return; # too new
371    
372     $map->_set_obs ($data->{obs});
373     };
374    
375     *on_mapout = sub {
376 root 1.13 my ($map) = @_;
377 root 1.6
378     my $path = $map->tmpname;
379     $path = $map->path unless defined $path;
380    
381     my $obs = $map->_get_obs;
382    
383     if (defined $obs) {
384     open my $fh, ">:raw", "$path.cfperl"
385     or die "$path.cfperl: $!";
386    
387 root 1.8 stat $path;
388    
389     print $fh Storable::nfreeze {
390     size => (stat _)[7],
391     time => (stat _)[9],
392     version => 1,
393     obs => $obs,
394     };
395    
396     chmod SAVE_MODE, "$path.cfperl"; # very racy, but cf-compatible *g*
397     } else {
398     unlink "$path.cfperl";
399     }
400     };
401    
402     #############################################################################
403     # load/save perl data associated with player->ob objects
404    
405 root 1.33 sub all_objects(@) {
406     @_, map all_objects ($_->inv), @_
407     }
408    
409 root 1.8 *on_player_load = sub {
410 root 1.13 my ($ob, $path) = @_;
411 root 1.8
412 root 1.33 for my $o (all_objects $ob) {
413 root 1.11 if (my $value = $o->get_ob_key_value ("_perl_data")) {
414     $o->set_ob_key_value ("_perl_data");
415    
416     %$o = %{ Storable::thaw pack "H*", $value };
417     }
418     }
419 root 1.8 };
420    
421     *on_player_save = sub {
422 root 1.13 my ($ob, $path) = @_;
423 root 1.8
424 root 1.11 $_->set_ob_key_value (_perl_data => unpack "H*", Storable::nfreeze $_)
425 root 1.33 for grep %$_, all_objects $ob;
426 root 1.6 };
427    
428 root 1.22 #############################################################################
429     # core extensions - in perl
430    
431 root 1.23 =item cf::player::exists $login
432    
433     Returns true when the given account exists.
434    
435     =cut
436    
437     sub cf::player::exists($) {
438     cf::player::find $_[0]
439     or -f sprintf "%s/%s/%s/%s.pl", cf::localdir, cf::playerdir, ($_[0]) x 2;
440     }
441    
442 root 1.28 =item $player->reply ($npc, $msg[, $flags])
443    
444     Sends a message to the player, as if the npc C<$npc> replied. C<$npc>
445     can be C<undef>. Does the right thing when the player is currently in a
446     dialogue with the given NPC character.
447    
448     =cut
449    
450 root 1.22 # rough implementation of a future "reply" method that works
451     # with dialog boxes.
452 root 1.23 sub cf::object::player::reply($$$;$) {
453     my ($self, $npc, $msg, $flags) = @_;
454    
455     $flags = cf::NDI_BROWN | cf::NDI_UNIQUE unless @_ >= 4;
456 root 1.22
457 root 1.24 if ($self->{record_replies}) {
458     push @{ $self->{record_replies} }, [$npc, $msg, $flags];
459     } else {
460     $msg = $npc->name . " says: $msg" if $npc;
461     $self->message ($msg, $flags);
462     }
463 root 1.22 }
464    
465 root 1.28 =item $player->ext_reply ($msgid, $msgtype, %msg)
466    
467     Sends an ext reply to the player.
468    
469     =cut
470    
471     sub cf::player::ext_reply($$$%) {
472     my ($self, $id, %msg) = @_;
473    
474     $msg{msgid} = $id;
475    
476     $self->send ("ext " . to_json \%msg);
477     }
478    
479 root 1.22 #############################################################################
480 root 1.23 # map scripting support
481    
482     our $safe = new Safe "ext";
483     our $safe_hole = new Safe::Hole;
484    
485     $SIG{FPE} = 'IGNORE';
486    
487     $safe->permit_only (Opcode::opset qw(:base_core :base_mem :base_orig :base_math sort time));
488    
489 root 1.25 # here we export the classes and methods available to script code
490    
491     for (
492 root 1.27 ["cf::object" => qw(contr pay_amount pay_player)],
493 root 1.25 ["cf::object::player" => qw(player)],
494     ["cf::player" => qw(peaceful)],
495     ) {
496     no strict 'refs';
497     my ($pkg, @funs) = @$_;
498     *{"ext::$pkg\::$_"} = $safe_hole->wrap (\&{"$pkg\::$_"})
499     for @funs;
500     }
501 root 1.23
502     sub safe_eval($;@) {
503     my ($code, %vars) = @_;
504    
505     my $qcode = $code;
506     $qcode =~ s/"/‟/g; # not allowed in #line filenames
507     $qcode =~ s/\n/\\n/g;
508    
509     local $_;
510     local @ext::cf::_safe_eval_args = values %vars;
511    
512 root 1.25 $code =
513 root 1.23 "do {\n"
514     . "my (" . (join ",", map "\$$_", keys %vars) . ") = \@cf::_safe_eval_args;\n"
515     . "#line 0 \"{$qcode}\"\n"
516     . $code
517     . "\n}"
518 root 1.25 ;
519    
520     sub_generation_inc;
521     my @res = wantarray ? $safe->reval ($code) : scalar $safe->reval ($code);
522     sub_generation_inc;
523    
524     wantarray ? @res : $res[0]
525 root 1.23 }
526    
527     sub register_script_function {
528     my ($fun, $cb) = @_;
529    
530     no strict 'refs';
531     *{"ext::$fun"} = $safe_hole->wrap ($cb);
532     }
533    
534     #############################################################################
535 root 1.34 # the server's main()
536    
537     sub run {
538     Event::loop;
539     }
540    
541     #############################################################################
542 root 1.22 # initialisation
543    
544 root 1.6 register "<global>", __PACKAGE__;
545    
546 root 1.27 unshift @INC, $LIBDIR;
547 root 1.17
548 root 1.1 load_extensions;
549    
550 root 1.35 $TICK_WATCHER = Event->timer (
551     prio => 1,
552     at => $NEXT_TICK || 1,
553     cb => sub {
554     cf::server_tick; # one server iteration
555    
556     my $NOW = Event::time;
557     $NEXT_TICK += $TICK;
558    
559     # if we are delayed by > 0.25 second, skip ticks
560     $NEXT_TICK = $NOW if $NOW >= $NEXT_TICK + .25;
561    
562     $TICK_WATCHER->at ($NEXT_TICK);
563     $TICK_WATCHER->start;
564     },
565     );
566    
567 root 1.1 1
568