ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/DC/Protocol.pm
Revision: 1.112
Committed: Tue Jul 10 16:25:16 2007 UTC (16 years, 10 months ago) by root
Branch: MAIN
Changes since 1.111: +1 -1 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.69 package CFPlus::Protocol;
2 root 1.1
3     use utf8;
4     use strict;
5    
6 root 1.2 use Crossfire::Protocol::Constants;
7    
8 root 1.69 use CFPlus;
9 root 1.96 use CFPlus::DB;
10 root 1.69 use CFPlus::UI;
11     use CFPlus::Pod;
12 root 1.91 use CFPlus::Macro;
13     use CFPlus::Item;
14 root 1.1
15 root 1.87 use Crossfire::Protocol::Base 0.95;
16    
17 root 1.1 use base 'Crossfire::Protocol::Base';
18    
19     sub new {
20 root 1.101 my ($class, %arg) = @_;
21 root 1.1
22 root 1.101 my $self = $class->SUPER::new (%arg,
23 root 1.99 setup_req => {
24 root 1.101 extmap => 1,
25 root 1.103 excmd => 1,
26 root 1.112 ywidget => 1,
27 root 1.101 %{$arg{setup_req} || {}},
28 root 1.99 },
29     );
30 root 1.1
31     $self->{map_widget}->clr_commands;
32    
33 root 1.72 my @cmd_help = map {
34     $_->{kw}[0] =~ /^(\S+) (?:\s+ \( ([^\)]*) \) )?/x
35     or die "unparseable command help: $_->{kw}[0]";
36    
37     my $cmd = $1;
38     my @args = split /\|/, $2;
39     @args = (".*") unless @args;
40    
41     my (undef, @par) = CFPlus::Pod::section_of $_;
42     my $text = CFPlus::Pod::as_label @par;
43    
44     $_ = $_ eq ".*" ? "" : " $_"
45     for @args;
46    
47     map ["$cmd$_", $text],
48     sort { (length $a) <=> (length $b) }
49     @args
50 root 1.96 } sort { $a->{par} <=> $b->{par} }
51     CFPlus::Pod::find command => "*";
52 root 1.1
53 root 1.96 $self->connect_ext (event_capabilities => sub {
54     my ($cap) = @_;
55 root 1.95
56 root 1.96 if (my $ts = $cap->{tileset}) {
57     if (my ($default) = grep $_->[2] & 1, @$ts) {
58     $self->{tileset} = $default;
59     $self->{tilesize} = $default->[3];
60     $self->setup_req (tileset => $default->[0]);
61 root 1.95
62 root 1.96 my $w = int $self->{mapw} * 32 / $self->{tilesize};
63     my $h = int $self->{maph} * 32 / $self->{tilesize};
64 root 1.1
65 root 1.96 $self->setup_req (mapsize => "${w}x${h}");
66     }
67     }
68     });
69    
70 root 1.111 $self->{json_coder}
71     ->convert_blessed
72     ->filter_json_single_key_object (__widget_ref__ => sub {
73     $self->{widget}{$_[0]}
74     });
75    
76 root 1.109 $self->connect_ext (ws_n => sub {
77     my ($arg) = @_;
78    
79     $self->{widgetset}{$arg{id}} = {
80     w => {},
81     };
82     });
83    
84     $self->connect_ext (ws_d => sub {
85     my ($arg) = @_;
86    
87     my $ws = delete $self->{widgetset}{$arg{id}}
88     or return;
89    
90     $_->destroy
91     for values %{$ws->{w}};
92     });
93    
94     $self->connect_ext (ws_c => sub {
95     my ($arg) = @_;
96    
97     my $args = $arg->{args} || {};
98    
99     for my $ev (grep /^on_/, keys %$args) {
100     $args->{$ev} = sub {
101     my $id = shift->{s_id};
102     $self->send_exti_msg (w_e => id => $id, name => $ev, args => \@_);
103    
104     1
105     };
106     }
107    
108     if (my $widget = eval {
109 root 1.110 local $SIG{__DIE__};
110 root 1.109 "CFPlus::UI::$arg->{class}"->new (
111     %$args,
112     s_ws => $arg->{ws},
113     s_id => $arg->{id},
114     )
115     }
116     ) {
117     $self->{widget}{$arg->{id}}
118     = $self->{widgetset}{$arg->{ws}}{w}{$arg->{id}}
119     = $widget;
120    
121     $widget->connect (on_destroy => sub {
122     my ($widget) = @_;
123    
124     delete $self->{widget}{$widget->{s_id}};
125     delete $self->{widgetset}{$widget->{s_ws}}{$widget->{s_id}};
126     });
127     } else {
128     warn "server failed creating client-side widget " . (CFPlus::to_json $arg) . ": $@\n";
129     $self->send_exti_msg (w_e => id => $arg->{id}, name => "destroy");
130     }
131     });
132    
133     $self->connect_ext (w_c => sub {
134     my ($arg) = @_;
135    
136     my $w = $self->{widget}{$arg->{id}}
137     or return;
138     my $m = $arg->{name};
139    
140 root 1.111 my $a = $arg->{args} || [];
141 root 1.109
142     if (exists $arg->{rid}) {
143 root 1.111 $self->send_exti_msg (w_r => rid => $arg->{rid}, res => [$w->$m (@$a)]);
144 root 1.109 } else {
145 root 1.111 $w->$m (@$a);
146 root 1.109 }
147     });
148    
149     $self->connect_ext (w_s => sub {
150     my ($arg) = @_;
151    
152     my $w = $self->{widget}{$arg->{id}}
153     or return;
154    
155     $w->{$arg->{name}} = $arg->{value};
156     });
157    
158     $self->connect_ext (w_g => sub {
159     my ($arg) = @_;
160    
161     my $w = $self->{widget}{$arg->{id}}
162     or return;
163    
164     $self->send_exti_msg (w_r => rid => $arg->{rid}, res => [$w->{$arg->{name}}]);
165     });
166    
167     $self->{on_stop_game_guard} = $self->{map_widget}{root}->connect (stop_game => sub {
168     for my $ws (values %{delete $self->{widgetset} || {}}) {
169     $_->destroy
170     for values %{delete $ws->{w} || {}};
171     }
172     });
173    
174 root 1.96 $self->{map_widget}->add_command (@$_)
175     for @cmd_help;
176 root 1.1
177 root 1.83 {
178     $self->{dialogue} = my $tex = new_from_file CFPlus::Texture
179     CFPlus::find_rcfile "dialogue.png", minify => 1, mipmap => 1;
180     $self->{map}->set_texture (1, @$tex{qw(name w h s t)}, @{$tex->{minified}});
181     }
182    
183 root 1.96 {
184     $self->{noface} = my $tex = new_from_file CFPlus::Texture
185     CFPlus::find_rcfile "noface.png", minify => 1, mipmap => 1;
186     $self->{map}->set_texture (2, @$tex{qw(name w h s t)}, @{$tex->{minified}});
187     }
188    
189 root 1.1 $self->{open_container} = 0;
190    
191     # per server
192 root 1.96 $self->{mapcache} = "mapcache_$self->{host}_$self->{port}";
193 root 1.1
194     $self
195     }
196    
197 elmex 1.34 sub logprint {
198     my ($self, @a) = @_;
199    
200 root 1.59 $self->{log_fh} ||= do {
201     my $path = "$Crossfire::VARDIR/log.$self->{host}";
202    
203     open my $fh, ">>:utf8", $path
204     or die "Couldn't open logfile $path: $!";
205 elmex 1.34
206 root 1.59 $fh->autoflush (1);
207    
208     $fh;
209     };
210 elmex 1.34
211 root 1.59 my ($sec, $min, $hour, $mday, $mon, $year) = localtime time;
212 elmex 1.34
213     my $ts = sprintf "%04d-%02d-%02d %02d:%02d:%02d",
214     $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
215    
216 root 1.59 print {$self->{log_fh}} "$ts ", @a, "\n";
217 elmex 1.34 }
218    
219 root 1.52 sub _stat_numdiff {
220 root 1.54 my ($self, $name, $old, $new) = @_;
221 root 1.52
222     my $diff = $new - $old;
223 root 1.54
224     $diff = 0.01 * int $diff * 100;
225    
226     0.1 >= abs $diff ? ()
227     : $diff < 0 ? "$name$diff" : "$name+$diff"
228 root 1.52 }
229    
230     sub _stat_skillmaskdiff {
231 root 1.54 my ($self, $name, $old, $new) = @_;
232 root 1.52
233 root 1.54 my $diff = $old ^ $new
234     or return;
235 root 1.52
236 root 1.53 my @diff = map
237     {
238     $diff & $_
239     ? (($new & $_ ? "+" : "-") . $self->{spell_paths}{$_})
240     : ()
241     }
242     sort { $a <=> $b } keys %{$self->{spell_paths}};
243 root 1.52
244     join "", @diff
245     }
246    
247     # all stats that are chacked against changes
248     my @statchange = (
249     [&CS_STAT_STR => \&_stat_numdiff, "Str"],
250     [&CS_STAT_INT => \&_stat_numdiff, "Int"],
251     [&CS_STAT_WIS => \&_stat_numdiff, "Wis"],
252     [&CS_STAT_DEX => \&_stat_numdiff, "Dex"],
253     [&CS_STAT_CON => \&_stat_numdiff, "Con"],
254     [&CS_STAT_CHA => \&_stat_numdiff, "Cha"],
255     [&CS_STAT_POW => \&_stat_numdiff, "Pow"],
256     [&CS_STAT_WC => \&_stat_numdiff, "Wc"],
257     [&CS_STAT_AC => \&_stat_numdiff, "Ac"],
258     [&CS_STAT_DAM => \&_stat_numdiff, "Dam"],
259     [&CS_STAT_SPEED => \&_stat_numdiff, "Speed"],
260     [&CS_STAT_WEAP_SP => \&_stat_numdiff, "WSp"],
261     [&CS_STAT_MAXHP => \&_stat_numdiff, "HP"],
262     [&CS_STAT_MAXSP => \&_stat_numdiff, "Mana"],
263     [&CS_STAT_MAXGRACE => \&_stat_numdiff, "Grace"],
264     [&CS_STAT_WEIGHT_LIM => \&_stat_numdiff, "Weight"],
265     [&CS_STAT_SPELL_ATTUNE => \&_stat_skillmaskdiff, "attuned"],
266     [&CS_STAT_SPELL_REPEL => \&_stat_skillmaskdiff, "repelled"],
267     [&CS_STAT_SPELL_DENY => \&_stat_skillmaskdiff, "denied"],
268     [&CS_STAT_RES_PHYS => \&_stat_numdiff, "phys"],
269     [&CS_STAT_RES_MAG => \&_stat_numdiff, "magic"],
270     [&CS_STAT_RES_FIRE => \&_stat_numdiff, "fire"],
271     [&CS_STAT_RES_ELEC => \&_stat_numdiff, "electricity"],
272     [&CS_STAT_RES_COLD => \&_stat_numdiff, "cold"],
273     [&CS_STAT_RES_CONF => \&_stat_numdiff, "confusion"],
274     [&CS_STAT_RES_ACID => \&_stat_numdiff, "acid"],
275     [&CS_STAT_RES_DRAIN => \&_stat_numdiff, "drain"],
276     [&CS_STAT_RES_GHOSTHIT => \&_stat_numdiff, "ghosthit"],
277     [&CS_STAT_RES_POISON => \&_stat_numdiff, "poison"],
278     [&CS_STAT_RES_SLOW => \&_stat_numdiff, "slow"],
279     [&CS_STAT_RES_PARA => \&_stat_numdiff, "paralyse"],
280     [&CS_STAT_TURN_UNDEAD => \&_stat_numdiff, "turnundead"],
281     [&CS_STAT_RES_FEAR => \&_stat_numdiff, "fear"],
282     [&CS_STAT_RES_DEPLETE => \&_stat_numdiff, "depletion"],
283     [&CS_STAT_RES_DEATH => \&_stat_numdiff, "death"],
284     [&CS_STAT_RES_HOLYWORD => \&_stat_numdiff, "godpower"],
285     [&CS_STAT_RES_BLIND => \&_stat_numdiff, "blind"],
286     );
287    
288 root 1.1 sub stats_update {
289     my ($self, $stats) = @_;
290    
291 root 1.59 my $prev = $self->{prev_stats} || { };
292    
293     if (my @diffs =
294     (
295     ($stats->{+CS_STAT_EXP64} > $prev->{+CS_STAT_EXP64} ? ($stats->{+CS_STAT_EXP64} - $prev->{+CS_STAT_EXP64}) . " experience gained" : ()),
296     map {
297     $stats->{$_} && $prev->{$_}
298     && $stats->{$_}[1] > $prev->{$_}[1] ? "($self->{skill_info}{$_}+" . ($stats->{$_}[1] - $prev->{$_}[1]) . ")" : ()
299     } sort { $a <=> $b } keys %{$self->{skill_info}}
300     )
301     ) {
302     my $msg = join " ", @diffs;
303     $self->{statusbox}->add ($msg, group => "experience $msg", fg => [0.5, 1, 0.5, 0.8], timeout => 5);
304     }
305    
306     if (
307     my @diffs = map $_->[1]->($self, $_->[2], $prev->{$_->[0]}, $stats->{$_->[0]}), @statchange
308     ) {
309     my $msg = "<b>stat change</b>: " . (join " ", @diffs);
310     $self->{statusbox}->add ($msg, group => "stat $msg", fg => [0.8, 1, 0.2, 1], timeout => 10);
311     }
312    
313     $self->update_stats_window ($stats, $prev);
314    
315     $self->{prev_stats} = { %$stats };
316     }
317    
318     my %RES_TBL = (
319     phys => CS_STAT_RES_PHYS,
320     magic => CS_STAT_RES_MAG,
321     fire => CS_STAT_RES_FIRE,
322     elec => CS_STAT_RES_ELEC,
323     cold => CS_STAT_RES_COLD,
324     conf => CS_STAT_RES_CONF,
325     acid => CS_STAT_RES_ACID,
326     drain => CS_STAT_RES_DRAIN,
327     ghit => CS_STAT_RES_GHOSTHIT,
328     pois => CS_STAT_RES_POISON,
329     slow => CS_STAT_RES_SLOW,
330     para => CS_STAT_RES_PARA,
331     tund => CS_STAT_TURN_UNDEAD,
332     fear => CS_STAT_RES_FEAR,
333     depl => CS_STAT_RES_DEPLETE,
334     deat => CS_STAT_RES_DEATH,
335     holyw => CS_STAT_RES_HOLYWORD,
336     blind => CS_STAT_RES_BLIND,
337     );
338    
339     sub update_stats_window {
340     my ($self, $stats, $prev) = @_;
341    
342     # I love text protocols...
343 root 1.52
344 root 1.59 my $hp = $stats->{+CS_STAT_HP} * 1;
345     my $hp_m = $stats->{+CS_STAT_MAXHP} * 1;
346     my $sp = $stats->{+CS_STAT_SP} * 1;
347     my $sp_m = $stats->{+CS_STAT_MAXSP} * 1;
348     my $fo = $stats->{+CS_STAT_FOOD} * 1;
349     my $fo_m = 999;
350     my $gr = $stats->{+CS_STAT_GRACE} * 1;
351     my $gr_m = $stats->{+CS_STAT_MAXGRACE} * 1;
352    
353     $::GAUGES->{hp} ->set_value ($hp, $hp_m);
354     $::GAUGES->{mana} ->set_value ($sp, $sp_m);
355     $::GAUGES->{food} ->set_value ($fo, $fo_m);
356     $::GAUGES->{grace} ->set_value ($gr, $gr_m);
357     $::GAUGES->{exp} ->set_text ("Exp: " . (::formsep ($stats->{+CS_STAT_EXP64}))
358     . " (lvl " . ($stats->{+CS_STAT_LEVEL} * 1) . ")");
359 root 1.106 $::GAUGES->{range} ->set_text ($stats->{+CS_STAT_RANGE});
360 root 1.59 my $title = $stats->{+CS_STAT_TITLE};
361     $title =~ s/^Player: //;
362     $::STATWIDS->{title} ->set_text ("Title: " . $title);
363    
364     $::STATWIDS->{st_str} ->set_text (sprintf "%d" , $stats->{+CS_STAT_STR});
365     $::STATWIDS->{st_dex} ->set_text (sprintf "%d" , $stats->{+CS_STAT_DEX});
366     $::STATWIDS->{st_con} ->set_text (sprintf "%d" , $stats->{+CS_STAT_CON});
367     $::STATWIDS->{st_int} ->set_text (sprintf "%d" , $stats->{+CS_STAT_INT});
368     $::STATWIDS->{st_wis} ->set_text (sprintf "%d" , $stats->{+CS_STAT_WIS});
369     $::STATWIDS->{st_pow} ->set_text (sprintf "%d" , $stats->{+CS_STAT_POW});
370     $::STATWIDS->{st_cha} ->set_text (sprintf "%d" , $stats->{+CS_STAT_CHA});
371     $::STATWIDS->{st_wc} ->set_text (sprintf "%d" , $stats->{+CS_STAT_WC});
372     $::STATWIDS->{st_ac} ->set_text (sprintf "%d" , $stats->{+CS_STAT_AC});
373     $::STATWIDS->{st_dam} ->set_text (sprintf "%d" , $stats->{+CS_STAT_DAM});
374     $::STATWIDS->{st_arm} ->set_text (sprintf "%d" , $stats->{+CS_STAT_RES_PHYS});
375     $::STATWIDS->{st_spd} ->set_text (sprintf "%.1f", $stats->{+CS_STAT_SPEED});
376     $::STATWIDS->{st_wspd}->set_text (sprintf "%.1f", $stats->{+CS_STAT_WEAP_SP});
377    
378 root 1.64 $self->update_weight;
379 root 1.59
380     $::STATWIDS->{"res_$_"}->set_text (sprintf "%d%", $stats->{$RES_TBL{$_}})
381     for keys %RES_TBL;
382    
383     my $sktbl = $::STATWIDS->{skill_tbl};
384     my @skills = keys %{ $self->{skill_info} };
385    
386     if (grep +(exists $stats->{$_}) != (exists $prev->{$_}), @skills) {
387     $sktbl->clear;
388    
389 root 1.69 $sktbl->add (0, 0, new CFPlus::UI::Label text => "Experience", align => 1);
390     $sktbl->add (1, 0, new CFPlus::UI::Label text => "Lvl.", align => 1);
391     $sktbl->add (2, 0, new CFPlus::UI::Label text => "Skill", expand => 1);
392     $sktbl->add (3, 0, new CFPlus::UI::Label text => "Experience", align => 1);
393     $sktbl->add (4, 0, new CFPlus::UI::Label text => "Lvl.", align => 1);
394     $sktbl->add (5, 0, new CFPlus::UI::Label text => "Skill", expand => 1);
395 root 1.59
396 elmex 1.68 my $TOOLTIP_ALL = "\n\n<small>Left click - ready skill\nMiddle click - use spell\nRight click - further options</small>";
397    
398     my @TOOLTIP_LVL = (tooltip => "<b>Level</b>. The level of the skill.$TOOLTIP_ALL", can_events => 1, can_hover => 1);
399     my @TOOLTIP_EXP = (tooltip => "<b>Experience</b>. The experience points you have in this skill.$TOOLTIP_ALL", can_events => 1, can_hover => 1);
400    
401 root 1.65 my ($x, $y) = (0, 1);
402 root 1.59 for (
403     sort { $stats->{$b->[0]}[1] <=> $stats->{$a->[0]}[1] or $a->[1] cmp $b->[1] }
404     map [$_, $self->{skill_info}{$_}],
405     grep exists $stats->{$_},
406     @skills
407 root 1.52 ) {
408 root 1.59 my ($idx, $name) = @$_;
409    
410 elmex 1.68 my $spell_cb = sub {
411     my ($widget, $ev) = @_;
412    
413     if ($ev->{button} == 1) {
414     $::CONN->user_send ("ready_skill $name");
415     } elsif ($ev->{button} == 2) {
416     $::CONN->user_send ("use_skill $name");
417     } elsif ($ev->{button} == 3) {
418 root 1.90 my $shortname = CFPlus::shorten $name, 14;
419 root 1.69 (new CFPlus::UI::Menu
420 elmex 1.68 items => [
421 root 1.91 ["bind <i>ready_skill $shortname</i> to a key" => sub { CFPlus::Macro::quick_macro ["ready_skill $name"] }],
422     ["bind <i>use_skill $shortname</i> to a key" => sub { CFPlus::Macro::quick_macro ["use_skill $name"] }],
423 elmex 1.68 ],
424     )->popup ($ev);
425     } else {
426     return 0;
427     }
428    
429     1
430     };
431    
432 root 1.69 $sktbl->add ($x * 3 + 0, $y, $self->{stat_widget_exp}{$idx} = new CFPlus::UI::Label
433 elmex 1.68 text => "0", align => 1, font => $::FONT_FIXED, fg => [1, 1, 0], on_button_down => $spell_cb, @TOOLTIP_EXP);
434 root 1.69 $sktbl->add ($x * 3 + 1, $y, $self->{stat_widget_lvl}{$idx} = new CFPlus::UI::Label
435 elmex 1.68 text => "0", align => 1, font => $::FONT_FIXED, fg => [0, 1, 0], padding_x => 4, on_button_down => $spell_cb, @TOOLTIP_LVL);
436 root 1.74 $sktbl->add ($x * 3 + 2, $y, new CFPlus::UI::Label text => $name, on_button_down => $spell_cb,
437     can_events => 1, can_hover => 1, tooltip => (CFPlus::Pod::section_label skill_description => $name) . $TOOLTIP_ALL);
438 root 1.59
439     $x++ and ($x, $y) = (0, $y + 1);
440 root 1.52 }
441 root 1.1 }
442    
443 root 1.59 for (grep exists $stats->{$_}, @skills) {
444     $self->{stat_widget_exp}{$_}->set_text (::formsep ($stats->{$_}[1]));
445     $self->{stat_widget_lvl}{$_}->set_text ($stats->{$_}[0] * 1);
446     }
447 root 1.1 }
448    
449 root 1.91 sub macro_send {
450     my ($self, $macro) = @_;
451    
452     for my $cmd (@{ $macro->{action} }) {
453     $self->send_command ($cmd);
454     }
455     }
456    
457 root 1.1 sub user_send {
458     my ($self, $command) = @_;
459    
460 root 1.91 $self->{record}->($command)
461 root 1.87 if $self->{record};
462 elmex 1.9
463 elmex 1.34 $self->logprint ("send: ", $command);
464 root 1.1 $self->send_command ($command);
465 root 1.45 ::status ($command);
466 root 1.1 }
467    
468 root 1.91 sub record {
469     my ($self, $cb) = @_;
470 elmex 1.9
471 root 1.91 $self->{record} = $cb;
472 elmex 1.9 }
473    
474 root 1.1 sub map_scroll {
475     my ($self, $dx, $dy) = @_;
476    
477     $self->{map}->scroll ($dx, $dy);
478     }
479    
480     sub feed_map1a {
481     my ($self, $data) = @_;
482    
483 root 1.79 $self->{map}->map1a_update ($data, $self->{setup}{extmap});
484 root 1.1 $self->{map_widget}->update;
485     }
486    
487 root 1.36 sub magicmap {
488     my ($self, $w, $h, $x, $y, $data) = @_;
489    
490     $self->{map_widget}->set_magicmap ($w, $h, $x, $y, $data);
491     }
492    
493 root 1.1 sub flush_map {
494     my ($self) = @_;
495    
496     my $map_info = delete $self->{map_info}
497     or return;
498    
499     my ($hash, $x, $y, $w, $h) = @$map_info;
500    
501     my $data = $self->{map}->get_rect ($x, $y, $w, $h);
502 root 1.96 CFPlus::DB::put $self->{mapcache} => $hash => Compress::LZF::compress $data, sub { };
503 root 1.1 #warn sprintf "SAVEmap[%s] length %d\n", $hash, length $data;#d#
504     }
505    
506     sub map_clear {
507     my ($self) = @_;
508    
509     $self->flush_map;
510     delete $self->{neigh_map};
511    
512     $self->{map}->clear;
513 root 1.37 delete $self->{map_widget}{magicmap};
514 root 1.1 }
515    
516 root 1.97 sub bg_fetch {
517     my ($self) = @_;
518    
519     my $id;
520    
521     do {
522     $id = pop @{$self->{bg_fetch}}
523     or return;
524     } while $self->{texture}[$id];
525    
526     CFPlus::DB::get tilecache => $id, sub {
527     my ($data) = @_;
528    
529 root 1.98 return unless $self->{map}; # stop when destroyed
530    
531 root 1.97 $self->set_texture ($id => $data)
532     if defined $data;
533    
534     $self->bg_fetch;
535     };
536     }
537 root 1.1
538     sub load_map($$$) {
539     my ($self, $hash, $x, $y) = @_;
540    
541 root 1.100 my $gen = $self->{map_change_gen};
542    
543 root 1.96 CFPlus::DB::get $self->{mapcache} => $hash, sub {
544 root 1.100 return unless $gen == $self->{map_change_gen};
545    
546 root 1.96 my ($data) = @_;
547    
548     if (defined $data) {
549     $data = Compress::LZF::decompress $data;
550     #warn sprintf "LOADmap[%s,%d,%d] length %d\n", $hash, $x, $y, length $data;#d#
551 root 1.1
552 root 1.97 my $inprogress = @{ $self->{bg_fetch} || [] };
553 root 1.98 unshift @{ $self->{bg_fetch} }, $self->{map}->set_rect ($x, $y, $data);
554 root 1.97 $self->bg_fetch unless $inprogress;
555 root 1.1 }
556 root 1.96 };
557 root 1.1 }
558    
559     # hardcode /world/world_xxx_xxx map names, the savings are enourmous,
560     # (server resource,s latency, bandwidth), so this hack is warranted.
561     # the right fix is to make real tiled maps with an overview file
562     sub send_mapinfo {
563     my ($self, $data, $cb) = @_;
564    
565     if ($self->{map_info}[0] =~ m%^/world/world_(\d\d\d)_(\d\d\d)$%) {
566     my ($wx, $wy) = ($1, $2);
567    
568     if ($data =~ /^spatial ([1-4]+)$/) {
569     my @dx = (0, 0, 1, 0, -1);
570     my @dy = (0, -1, 0, 1, 0);
571     my ($dx, $dy);
572    
573     for (split //, $1) {
574     $dx += $dx[$_];
575     $dy += $dy[$_];
576     }
577    
578     $cb->(spatial => 15,
579     $self->{map_info}[1] - $self->{map}->ox + $dx * 50,
580     $self->{map_info}[2] - $self->{map}->oy + $dy * 50,
581     50, 50,
582     sprintf "/world/world_%03d_%03d", $wx + $dx, $wy + $dy
583     );
584    
585     return;
586     }
587     }
588    
589     $self->SUPER::send_mapinfo ($data, $cb);
590     }
591    
592     # this method does a "flood fill" into every tile direction
593     # it assumes that tiles are arranged in a rectangular grid,
594     # i.e. a map is the same as the left of the right map etc.
595     # failure to comply are harmless and result in display errors
596     # at worst.
597     sub flood_fill {
598     my ($self, $block, $gx, $gy, $path, $hash, $flags) = @_;
599    
600     # the server does not allow map paths > 6
601     return if 7 <= length $path;
602    
603     my ($x0, $y0, $x1, $y1) = @{$self->{neigh_rect}};
604    
605     for (
606     [1, 3, 0, -1],
607     [2, 4, 1, 0],
608     [3, 1, 0, 1],
609     [4, 2, -1, 0],
610     ) {
611     my ($tile, $tile2, $dx, $dy) = @$_;
612    
613     next if $block & (1 << $tile);
614     my $block = $block | (1 << $tile2);
615    
616     my $gx = $gx + $dx;
617     my $gy = $gy + $dy;
618    
619     next unless $flags & (1 << ($tile - 1));
620     next if $self->{neigh_grid}{$gx, $gy}++;
621    
622     my $neigh = $self->{neigh_map}{$hash} ||= [];
623     if (my $info = $neigh->[$tile]) {
624     my ($flags, $x, $y, $w, $h, $hash) = @$info;
625    
626     $self->flood_fill ($block, $gx, $gy, "$path$tile", $hash, $flags)
627     if $x >= $x0 && $x + $w < $x1 && $y >= $y0 && $y + $h < $y1;
628    
629     } else {
630 root 1.100 my $gen = $self->{map_change_gen};
631 root 1.1 $self->send_mapinfo ("spatial $path$tile", sub {
632 root 1.100 return unless $gen == $self->{map_change_gen};
633    
634 root 1.1 my ($mode, $flags, $x, $y, $w, $h, $hash) = @_;
635    
636     return if $mode ne "spatial";
637    
638     $x += $self->{map}->ox;
639     $y += $self->{map}->oy;
640    
641     $self->load_map ($hash, $x, $y)
642     unless $self->{neigh_map}{$hash}[5]++;#d#
643    
644     $neigh->[$tile] = [$flags, $x, $y, $w, $h, $hash];
645    
646     $self->flood_fill ($block, $gx, $gy, "$path$tile", $hash, $flags)
647     if $x >= $x0 && $x + $w < $x1 && $y >= $y0 && $y + $h < $y1;
648     });
649     }
650     }
651     }
652    
653     sub map_change {
654     my ($self, $mode, $flags, $x, $y, $w, $h, $hash) = @_;
655    
656     $self->flush_map;
657    
658 root 1.100 ++$self->{map_change_gen};
659    
660 root 1.1 my ($ox, $oy) = ($::MAP->ox, $::MAP->oy);
661    
662     my $mapmapw = $self->{mapmap}->{w};
663     my $mapmaph = $self->{mapmap}->{h};
664    
665     $self->{neigh_rect} = [
666     $ox - $mapmapw * 0.5, $oy - $mapmapw * 0.5,
667     $ox + $mapmapw * 0.5 + $w, $oy + $mapmapw * 0.5 + $h,
668     ];
669    
670     delete $self->{neigh_grid};
671    
672     $x += $ox;
673     $y += $oy;
674    
675     $self->{map_info} = [$hash, $x, $y, $w, $h];
676    
677     (my $map = $hash) =~ s/^.*?\/([^\/]+)$/\1/;
678     $::STATWIDS->{map}->set_text ("Map: " . $map);
679    
680     $self->load_map ($hash, $x, $y);
681     $self->flood_fill (0, 0, 0, "", $hash, $flags);
682     }
683    
684     sub face_find {
685 root 1.96 my ($self, $facenum, $face, $cb) = @_;
686 root 1.1
687     my $hash = "$face->{chksum},$face->{name}";
688    
689 root 1.96 my $id = CFPlus::DB::get_tile_id_sync $hash;
690 root 1.1
691 root 1.96 $face->{id} = $id;
692     $self->{faceid}[$facenum] = $id;
693 root 1.1
694 root 1.96 $self->{map}->set_tileid ($facenum => $id);
695 root 1.1
696 root 1.96 CFPlus::DB::get tilecache => $id, $cb;
697 root 1.1 }
698    
699     sub face_update {
700 root 1.58 my ($self, $facenum, $face, $changed) = @_;
701 root 1.1
702 root 1.96 CFPlus::DB::put tilecache => $face->{id} => $face->{image}, sub { }
703     if $changed;
704 root 1.1
705     $self->set_texture ($face->{id} => delete $face->{image});
706     }
707    
708 root 1.99 sub smooth_update {
709     my ($self, $facenum, $face) = @_;
710    
711     $self->{map}->set_smooth ($facenum, $face->{smoothface}, $face->{smoothlevel});
712     }
713    
714 root 1.1 sub set_texture {
715     my ($self, $id, $data) = @_;
716    
717 root 1.98 $self->{texture}[$id] = my $tex =
718     new_from_image CFPlus::Texture
719     $data, minify => 1, mipmap => 1;
720 root 1.1
721 root 1.98 $self->{map}->set_texture ($id, @$tex{qw(name w h s t)}, @{$tex->{minified}});
722     $self->{map_widget}->update;
723 root 1.1 }
724    
725     sub sound_play {
726     my ($self, $x, $y, $soundnum, $type) = @_;
727    
728     $self->{sound_play}->($x, $y, $soundnum, $type);
729     }
730    
731     my $LAST_QUERY; # server is stupid, stupid, stupid
732    
733     sub query {
734     my ($self, $flags, $prompt) = @_;
735    
736     $prompt = $LAST_QUERY unless length $prompt;
737     $LAST_QUERY = $prompt;
738    
739 root 1.22 $self->{query}-> ($self, $flags, $prompt);
740 root 1.1 }
741    
742 root 1.108 sub sanitise_xml($) {
743     local $_ = shift;
744    
745     # we now weed out all tags we do not support
746     s%<(?!/?i>|/?u>|/?b>|fg |/fg>)%&lt;%g;
747     # now all entities
748     s/&(?!amp;|lt;|gt;|apos;|quot;|#[0-9]+;|#x[0-9a-fA-F]+;)/&amp;/g;
749    
750     # handle some elements
751     s/<fg name='([^']*)'>(.*?)<\/fg>/<span foreground='$1'>$2<\/span>/gs;
752     s/<fg name="([^"]*)">(.*?)<\/fg>/<span foreground="$1">$2<\/span>/gs;
753    
754     $_
755     }
756    
757 root 1.105 our %NAME_TO_COLOR = (
758     black => 0,
759     white => 1,
760     darkblue => 2,
761     red => 3,
762     orange => 4,
763     lightblue => 5,
764     darkorange => 6,
765     green => 7,
766     darkgreen => 8,
767     grey => 9,
768     brown => 10,
769     yellow => 11,
770     tan => 12,
771     );
772    
773 root 1.103 our @CF_COLOR = (
774     [1.00, 1.00, 1.00], #[0.00, 0.00, 0.00],
775     [1.00, 1.00, 1.00],
776     [0.50, 0.50, 1.00], #[0.00, 0.00, 0.55]
777     [1.00, 0.00, 0.00],
778     [1.00, 0.54, 0.00],
779     [0.11, 0.56, 1.00],
780     [0.93, 0.46, 0.00],
781     [0.18, 0.54, 0.34],
782     [0.56, 0.73, 0.56],
783     [0.80, 0.80, 0.80],
784     [0.75, 0.61, 0.20],
785     [0.99, 0.77, 0.26],
786     [0.74, 0.65, 0.41],
787     );
788    
789 root 1.105 sub msg {
790     my ($self, $color, $type, $text, @extra) = @_;
791 root 1.1
792 root 1.108 $text = sanitise_xml $text;
793 root 1.107
794 root 1.105 if (my $cb = $self->{cb_msg}{$type}) {
795     $_->($self, $color, $type, $text, @extra) for values %$cb;
796     } elsif ($type =~ /^(?:chargen-race-title|chargen-race-description)$/) {
797     $type =~ s/-/_/g;
798     $self->{$type} = $text;
799     } else {
800     $self->logprint ("msg: ", $text);
801     return if $color < 0; # negative color == ignore if not understood
802 root 1.93
803 root 1.105 my $fg = $CF_COLOR[$color % @CF_COLOR];
804 elmex 1.34
805 root 1.105 ## try to create single paragraphs of multiple lines sent by the server
806     # no longer neecssary with TRT servers
807     #$text =~ s/(?<=\S)\n(?=\w)/ /g;
808    
809     ::message ({ fg => $fg, markup => $_ })
810     for split /\n/, $text;
811    
812     $self->{statusbox}->add ($text,
813     group => $text,
814     fg => $fg,
815     timeout => $color >= 2 ? 180 : 10,
816     tooltip_font => $::FONT_FIXED,
817     );
818     }
819 root 1.1 }
820    
821     sub spell_add {
822     my ($self, $spell) = @_;
823    
824 root 1.66 # try to create single paragraphs out of the multiple lines sent by the server
825 root 1.27 $spell->{message} =~ s/(?<=\S)\n(?=\w)/ /g;
826     $spell->{message} =~ s/\n+$//;
827     $spell->{message} ||= "Server did not provide a description for this spell.";
828    
829 root 1.88 $::SPELL_LIST->add_spell ($spell);
830 elmex 1.14
831 root 1.69 $self->{map_widget}->add_command ("invoke $spell->{name}", CFPlus::asxml $spell->{message});
832     $self->{map_widget}->add_command ("cast $spell->{name}", CFPlus::asxml $spell->{message});
833 root 1.1 }
834    
835     sub spell_delete {
836     my ($self, $spell) = @_;
837 root 1.51
838 root 1.88 $::SPELL_LIST->remove_spell ($spell);
839 root 1.1 }
840    
841 root 1.87 sub setup {
842     my ($self, $setup) = @_;
843    
844 root 1.95 $self->{map_widget}->set_tilesize ($self->{tilesize});
845 root 1.87 $::MAP->resize ($self->{mapw}, $self->{maph});
846     }
847    
848 root 1.1 sub addme_success {
849     my ($self) = @_;
850    
851 root 1.72 my %skill_help;
852 root 1.21
853 root 1.72 for my $node (CFPlus::Pod::find skill_description => "*") {
854     my (undef, @par) = CFPlus::Pod::section_of $node;
855     $skill_help{$node->{kw}[0]} = CFPlus::Pod::as_label @par;
856 root 1.21 };
857 root 1.72
858 root 1.1 for my $skill (values %{$self->{skill_info}}) {
859     $self->{map_widget}->add_command ("ready_skill $skill",
860 root 1.69 (CFPlus::asxml "Ready the skill '$skill'\n\n")
861 root 1.72 . $skill_help{$skill});
862 root 1.1 $self->{map_widget}->add_command ("use_skill $skill",
863 root 1.69 (CFPlus::asxml "Immediately use the skill '$skill'\n\n")
864 root 1.72 . $skill_help{$skill});
865 root 1.1 }
866     }
867    
868     sub eof {
869     my ($self) = @_;
870    
871     $self->{map_widget}->clr_commands;
872    
873     ::stop_game ();
874     }
875    
876     sub image_info {
877     my ($self, $numfaces) = @_;
878    
879     $self->{num_faces} = $numfaces;
880     $self->{face_prefetch} = [1 .. $numfaces];
881     $self->face_prefetch;
882     }
883    
884     sub face_prefetch {
885     my ($self) = @_;
886    
887     return unless $::CFG->{face_prefetch};
888    
889     if ($self->{num_faces}) {
890     return if @{ $self->{send_queue} || [] };
891     my $todo = @{ $self->{face_prefetch} }
892     or return;
893    
894     my ($face) = splice @{ $self->{face_prefetch} }, + rand @{ $self->{face_prefetch} }, 1, ();
895    
896     $self->send ("requestinfo image_sums $face $face");
897    
898 root 1.69 $self->{statusbox}->add (CFPlus::asxml "prefetching $todo",
899 root 1.19 group => "prefetch", timeout => 3, fg => [1, 1, 0, 0.5]);
900 root 1.1 } elsif (!exists $self->{num_faces}) {
901     $self->send ("requestinfo image_info");
902    
903     $self->{num_faces} = 0;
904    
905 root 1.69 $self->{statusbox}->add (CFPlus::asxml "starting to prefetch",
906 root 1.19 group => "prefetch", timeout => 3, fg => [1, 1, 0, 0.5]);
907 root 1.1 }
908     }
909    
910     sub update_floorbox {
911 root 1.69 $CFPlus::UI::ROOT->on_refresh ($::FLOORBOX => sub {
912 root 1.1 return unless $::CONN;
913 elmex 1.62
914 root 1.1 $::FLOORBOX->clear;
915    
916     my $row;
917 elmex 1.62 for (sort { $a->{count} <=> $b->{count} } values %{ $::CONN->{container}{$::CONN->{open_container} || 0} }) {
918 root 1.25 if ($row < 6) {
919 root 1.1 local $_->{face_widget}; # hack to force recreation of widget
920     local $_->{desc_widget}; # hack to force recreation of widget
921 root 1.69 CFPlus::Item::update_widgets $_;
922 root 1.1
923     $::FLOORBOX->add (0, $row, $_->{face_widget});
924     $::FLOORBOX->add (1, $row, $_->{desc_widget});
925    
926     $row++;
927     } else {
928 root 1.69 $::FLOORBOX->add (1, $row, new CFPlus::UI::Button
929 root 1.30 text => "More...",
930 root 1.51 on_activate => sub { ::toggle_player_page ($::INVENTORY_PAGE); 0 },
931 root 1.25 );
932 root 1.1 last;
933     }
934     }
935 elmex 1.62 if ($::CONN->{open_container}) {
936 root 1.69 $::FLOORBOX->add (1, $row++, new CFPlus::UI::Button
937 elmex 1.62 text => "Close container",
938     on_activate => sub { $::CONN->send ("apply $::CONN->{open_container}") }
939     );
940     }
941 root 1.1 });
942 elmex 1.62
943 root 1.1 $::WANT_REFRESH++;
944     }
945    
946     sub set_opencont {
947     my ($conn, $tag, $name) = @_;
948     $conn->{open_container} = $tag;
949 elmex 1.62 update_floorbox;
950 elmex 1.10
951     $::INV_RIGHT_HB->clear ();
952 root 1.69 $::INV_RIGHT_HB->add (new CFPlus::UI::Label align => 0, expand => 1, text => $name);
953 elmex 1.10
954     if ($tag != 0) { # Floor isn't closable, is it?
955 root 1.69 $::INV_RIGHT_HB->add (new CFPlus::UI::Button
956 elmex 1.10 text => "Close container",
957     tooltip => "Close the currently open container (if one is open)",
958     on_activate => sub {
959     $::CONN->send ("apply $tag") # $::CONN->{open_container}")
960     if $tag != 0;
961     #if $CONN->{open_container} != 0;
962 root 1.38 0
963 elmex 1.10 },
964     );
965     }
966    
967 root 1.1 $::INVR->set_items ($conn->{container}{$tag});
968     }
969    
970 root 1.47 sub update_containers {
971     my ($self) = @_;
972 root 1.1
973 root 1.69 $CFPlus::UI::ROOT->on_refresh ("update_containers_$self" => sub {
974 root 1.55 my $todo = delete $self->{update_container}
975     or return;
976    
977     for my $tag (keys %$todo) {
978 elmex 1.62 update_floorbox if $tag == 0 or $tag == $self->{open_container};
979 root 1.47 if ($tag == 0) {
980 root 1.48 $::INVR->set_items ($self->{container}{0})
981     if $tag == $self->{open_container};
982 root 1.47 } elsif ($tag == $self->{player}{tag}) {
983     $::INV->set_items ($self->{container}{$tag})
984     } else {
985 root 1.48 $::INVR->set_items ($self->{container}{$tag})
986     if $tag == $self->{open_container};
987 root 1.47 }
988     }
989     });
990 root 1.1 }
991    
992 root 1.48 sub container_add {
993     my ($self, $tag, $items) = @_;
994    
995     $self->{update_container}{$tag}++;
996     $self->update_containers;
997     }
998    
999 root 1.1 sub container_clear {
1000     my ($self, $tag) = @_;
1001    
1002 root 1.47 $self->{update_container}{$tag}++;
1003     $self->update_containers;
1004 root 1.1 }
1005    
1006     sub item_delete {
1007     my ($self, @items) = @_;
1008    
1009 root 1.49 $self->{update_container}{$_->{container}}++
1010 root 1.47 for @items;
1011    
1012     $self->update_containers;
1013 root 1.1 }
1014    
1015     sub item_update {
1016     my ($self, $item) = @_;
1017    
1018     #d# print "item_update: $item->{tag} in $item->{container} ($self->{player}{tag}) ($::CONN->{open_container})\n";
1019    
1020 root 1.69 CFPlus::Item::update_widgets $item;
1021 root 1.1
1022 root 1.2 if ($item->{tag} == $::CONN->{open_container} && not ($item->{flags} & F_OPEN)) {
1023 root 1.1 set_opencont ($::CONN, 0, "Floor");
1024    
1025 root 1.2 } elsif ($item->{flags} & F_OPEN) {
1026 root 1.69 set_opencont ($::CONN, $item->{tag}, CFPlus::Item::desc_string $item);
1027 root 1.47
1028 root 1.1 } else {
1029 root 1.47 $self->{update_container}{$item->{container}}++;
1030     $self->update_containers;
1031 root 1.1 }
1032     }
1033    
1034 elmex 1.3 sub player_update {
1035     my ($self, $player) = @_;
1036 root 1.64
1037     $self->update_weight;
1038     }
1039    
1040     sub update_weight {
1041     my ($self) = @_;
1042    
1043     my $weight = .001 * $self->{player}{weight};
1044     my $limit = .001 * $self->{stat}{+CS_STAT_WEIGHT_LIM};
1045    
1046     $::STATWIDS->{weight}->set_text (sprintf "Weight: %.1fkg", $weight);
1047     $::STATWIDS->{m_weight}->set_text (sprintf "%.1fkg", $limit);
1048     $::STATWIDS->{i_weight}->set_text (sprintf "%.1f/%.1fkg", $weight, $limit);
1049 root 1.28 }
1050    
1051 root 1.32 sub update_server_info {
1052     my ($self) = @_;
1053    
1054     my @yesno = ("<span foreground='red'>no</span>", "<span foreground='green'>yes</span>");
1055    
1056     $::SERVER_INFO->set_markup (
1057     "server <tt>$self->{host}:$self->{port}</tt>\n"
1058     . "protocol version <tt>$self->{version}</tt>\n"
1059     . "minimap support $yesno[$self->{setup}{mapinfocmd} > 0]\n"
1060     . "extended command support $yesno[$self->{setup}{extcmd} > 0]\n"
1061 root 1.104 . "examine command support $yesno[$self->{setup}{excmd} > 0]\n"
1062 root 1.86 . "editing support $yesno[!!$self->{editor_support}]\n"
1063 root 1.80 . "map attributes $yesno[$self->{setup}{extmap} > 0]\n"
1064 root 1.104 . "big image protocol support $yesno[$self->{setup}{fxix} > 0]\n"
1065 root 1.32 . "cfplus support $yesno[$self->{cfplus_ext} > 0]"
1066     . ($self->{cfplus_ext} > 0 ? ", version $self->{cfplus_ext}" : "") ."\n"
1067     . "map size $self->{mapw}×$self->{maph}\n"
1068     );
1069 elmex 1.89
1070     ::setup_build_button ($self->{editor_support}->{builder_ui});
1071 root 1.32 }
1072    
1073 root 1.28 sub logged_in {
1074     my ($self) = @_;
1075    
1076 root 1.70 $self->send_ext_req (cfplus_support => version => 1, sub {
1077     $self->{cfplus_ext} = $_[0]{version};
1078 root 1.32 $self->update_server_info;
1079 root 1.70
1080 root 1.85 if ($self->{cfplus_ext} >= 2) {
1081     $self->send_ext_req ("editor_support", sub {
1082 root 1.86 $self->{editor_support} = $_[0];
1083 root 1.85 $self->update_server_info;
1084    
1085     0
1086     });
1087     }
1088    
1089 root 1.70 0
1090 root 1.32 });
1091 root 1.31
1092 root 1.32 $self->update_server_info;
1093 root 1.13
1094     $self->send_command ("output-sync $::CFG->{output_sync}");
1095     $self->send_command ("output-count $::CFG->{output_count}");
1096 root 1.94 $self->send_command ("output-rate $::CFG->{output_rate}") if $::CFG->{output_rate} > 0;
1097 root 1.29 $self->send_command ("pickup $::CFG->{pickup}");
1098 elmex 1.3 }
1099    
1100 elmex 1.89 sub buildat {
1101     my ($self, $builditem, $x, $y) = @_;
1102    
1103     if ($self->{cfplus_ext}) {
1104     $self->send_ext_msg (builder_build => dx => $x, dy => $y, (ref ($builditem) eq 'HASH') ? %$builditem : (item => $builditem));
1105     }
1106     }
1107    
1108 root 1.32 sub lookat {
1109     my ($self, $x, $y) = @_;
1110    
1111     if ($self->{cfplus_ext}) {
1112 root 1.70 $self->send_ext_req (lookat => dx => $x, dy => $y, sub {
1113     my ($msg) = @_;
1114 root 1.32
1115 root 1.70 if (exists $msg->{npc_dialog}) {
1116 root 1.32 # start npc chat dialog
1117 root 1.69 $self->{npc_dialog} = new CFPlus::NPCDialog::
1118 root 1.32 dx => $x,
1119     dy => $y,
1120 root 1.70 title => "$msg->{npc_dialog} (NPC)",
1121 root 1.32 conn => $self,
1122     ;
1123     }
1124     });
1125     }
1126    
1127     $self->send ("lookat $x $y");
1128     }
1129    
1130     sub destroy {
1131     my ($self) = @_;
1132    
1133 root 1.79 (delete $self->{npc_dialog})->destroy
1134 root 1.32 if $self->{npc_dialog};
1135    
1136     $self->SUPER::destroy;
1137 root 1.111
1138     %$self = ();
1139 root 1.32 }
1140    
1141 root 1.69 package CFPlus::NPCDialog;
1142 root 1.32
1143 root 1.76 our @ISA = 'CFPlus::UI::Toplevel';
1144 root 1.32
1145     sub new {
1146     my $class = shift;
1147    
1148     my $self = $class->SUPER::new (
1149     x => 'center',
1150     y => 'center',
1151     name => "npc_dialog",
1152     force_w => $::WIDTH * 0.7,
1153     force_h => $::HEIGHT * 0.7,
1154     title => "NPC Dialog",
1155     kw => { hi => 0, yes => 0, no => 0 },
1156 root 1.64 has_close_button => 1,
1157 root 1.32 @_,
1158     );
1159    
1160 root 1.92 CFPlus::weaken (my $this = $self);
1161 root 1.32
1162 root 1.64 $self->connect (delete => sub { $this->destroy; 1 });
1163    
1164 root 1.32 # better use a pane...
1165 root 1.69 $self->add (my $hbox = new CFPlus::UI::HBox);
1166     $hbox->add ($self->{textview} = new CFPlus::UI::TextScroller expand => 1);
1167 root 1.32
1168 root 1.69 $hbox->add (my $vbox = new CFPlus::UI::VBox);
1169 root 1.32
1170 root 1.69 $vbox->add (new CFPlus::UI::Label text => "Message Entry:");
1171     $vbox->add ($self->{entry} = new CFPlus::UI::Entry
1172 root 1.73 tooltip => "#npc_message_entry",
1173 root 1.32 on_activate => sub {
1174     my ($entry, $text) = @_;
1175    
1176     return unless $text =~ /\S/;
1177    
1178     $entry->set_text ("");
1179     $this->send ($text);
1180 root 1.38
1181     0
1182 root 1.32 },
1183     );
1184    
1185 root 1.69 $vbox->add ($self->{options} = new CFPlus::UI::VBox);
1186 root 1.32
1187 root 1.69 $self->{bye_button} = new CFPlus::UI::Button
1188 root 1.32 text => "Bye (close)",
1189     tooltip => "Use this button to end talking to the NPC. This also closes the dialog window.",
1190 root 1.64 on_activate => sub { $this->destroy; 1 },
1191 root 1.32 ;
1192    
1193     $self->update_options;
1194    
1195 root 1.70 $self->{id} = $self->{conn}->send_ext_req (
1196     npc_dialog_begin => dx => $self->{dx}, dy => $self->{dy},
1197     sub { $this && $this->feed (@_) }
1198     );
1199 root 1.32
1200 root 1.38 $self->{entry}->grab_focus;
1201 root 1.32
1202 root 1.67 $self->{textview}->add_paragraph ({
1203     fg => [1, 1, 0, 1],
1204     markup => "<small>[starting conversation with <b>$self->{title}</b>]</small>\n\n",
1205     });
1206 root 1.32
1207     $self->show;
1208     $self
1209     };
1210    
1211     sub update_options {
1212     my ($self) = @_;
1213    
1214 root 1.92 CFPlus::weaken $self;
1215 root 1.32
1216     $self->{options}->clear;
1217 root 1.39 $self->{options}->add ($self->{bye_button});
1218 root 1.32
1219     for my $kw (sort keys %{ $self->{kw} }) {
1220 root 1.69 $self->{options}->add (new CFPlus::UI::Button
1221 root 1.32 text => $kw,
1222     on_activate => sub {
1223     $self->send ($kw);
1224 root 1.38 0
1225 root 1.32 },
1226     );
1227     }
1228     }
1229    
1230     sub feed {
1231 root 1.70 my ($self, $msg) = @_;
1232 root 1.32
1233 root 1.92 CFPlus::weaken $self;
1234 root 1.42
1235 root 1.70 if ($msg->{msgtype} eq "reply") {
1236     $self->{kw}{$_} = 1 for @{$msg->{add_topics} || []};
1237     $self->{kw}{$_} = 0 for @{$msg->{del_topics} || []};
1238 root 1.61
1239 root 1.108 my $text = "\n" . CFPlus::Protocol::sanitise_xml $msg->{msg};
1240 root 1.32 my $match = join "|", map "\\b\Q$_\E\\b", sort { (length $b) <=> (length $a) } keys %{ $self->{kw} };
1241 root 1.41 my @link;
1242 root 1.70 $text =~ s{
1243 root 1.41 ($match)
1244     }{
1245 root 1.42 my $kw = $1;
1246    
1247 root 1.69 push @link, new CFPlus::UI::Label
1248 root 1.42 markup => "<span foreground='#c0c0ff' underline='single'>$kw</span>",
1249 root 1.41 can_hover => 1,
1250 root 1.42 can_events => 1,
1251 root 1.43 padding_x => 0,
1252     padding_y => 0,
1253 root 1.42 on_button_up => sub {
1254     $self->send ($kw);
1255     };
1256 root 1.41
1257 root 1.75 "\x{fffc}"
1258 root 1.41 }giex;
1259 root 1.32
1260 root 1.70 $self->{textview}->add_paragraph ({ markup => $text, widget => \@link });
1261 root 1.42 $self->{textview}->scroll_to_bottom;
1262 root 1.32 $self->update_options;
1263     } else {
1264     $self->destroy;
1265     }
1266    
1267     1
1268     }
1269    
1270     sub send {
1271     my ($self, $msg) = @_;
1272    
1273 root 1.69 $self->{textview}->add_paragraph ({ markup => "\n" . CFPlus::asxml $msg });
1274 root 1.42 $self->{textview}->scroll_to_bottom;
1275 root 1.61
1276 root 1.70 $self->{conn}->send_ext_msg (npc_dialog_tell => msgid => $self->{id}, msg => $msg);
1277 root 1.32 }
1278    
1279     sub destroy {
1280     my ($self) = @_;
1281    
1282 root 1.39 #Carp::cluck "debug\n";#d# #todo# enable: destroy gets called twice because scalar keys {} is 1
1283 root 1.32
1284 root 1.70 if ($self->{conn}) {
1285     $self->{conn}->send_ext_msg (npc_dialog_end => msgid => $self->{id}) if $self->{id};
1286     delete $self->{conn}{npc_dialog};
1287     $self->{conn}->disconnect_ext ($self->{id});
1288     }
1289 root 1.32
1290     $self->SUPER::destroy;
1291     }
1292    
1293 root 1.67 1
1294 root 1.84