ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/DC.pm
(Generate patch)

Comparing deliantra/Deliantra-Client/DC.pm (file contents):
Revision 1.121 by root, Fri Sep 29 00:56:05 2006 UTC vs.
Revision 1.188 by root, Tue Sep 2 16:27:34 2008 UTC

1=head1 NAME 1=head1 NAME
2 2
3CFPlus - undocumented utility garbage for our crossfire client 3DC - undocumented utility garbage for our deliantra client
4 4
5=head1 SYNOPSIS 5=head1 SYNOPSIS
6 6
7 use CFPlus; 7 use DC;
8 8
9=head1 DESCRIPTION 9=head1 DESCRIPTION
10 10
11=over 4 11=over 4
12 12
13=cut 13=cut
14 14
15package Deliantra::Client; # work around CPAN breakage
16package App::Deliantra; # try to reserve namespace
15package CFPlus; 17package DC;
16 18
17use Carp (); 19use Carp ();
18 20
21our $VERSION;
22
19BEGIN { 23BEGIN {
20 $VERSION = '0.52'; 24 $VERSION = '0.9975';
21 25
22 use XSLoader; 26 use XSLoader;
23 XSLoader::load "CFPlus", $VERSION; 27 XSLoader::load "Deliantra::Client", $VERSION;
24}
25
26BEGIN {
27 $SIG{__DIE__} = sub {
28 return if CFPlus::in_destruct;
29 #CFPlus::fatal $_[0];#d#
30 CFPlus::error Carp::longmess $_[0];#d#
31 die;#d#
32 };
33} 28}
34 29
35use utf8; 30use utf8;
31use strict qw(vars subs);
36 32
37use AnyEvent (); 33use AnyEvent ();
38use BerkeleyDB;
39use Pod::POM (); 34use Pod::POM ();
40use Scalar::Util (); 35use File::Path ();
41use Storable (); # finally 36use Storable (); # finally
37use Fcntl ();
38use JSON::XS qw(encode_json decode_json);
42 39
43=item guard { BLOCK } 40=item guard { BLOCK }
44 41
45Returns an object that executes the given block as soon as it is destroyed. 42Returns an object that executes the given block as soon as it is destroyed.
46 43
47=cut 44=cut
48 45
49sub guard(&) { 46sub guard(&) {
50 bless \(my $cb = $_[0]), "CFPlus::Guard" 47 bless \(my $cb = $_[0]), "DC::Guard"
51} 48}
52 49
53sub CFPlus::Guard::DESTROY { 50sub DC::Guard::DESTROY {
54 ${$_[0]}->() 51 ${$_[0]}->()
52}
53
54=item shorten $string[, $maxlength]
55
56=cut
57
58sub shorten($;$) {
59 my ($str, $len) = @_;
60 substr $str, $len, (length $str), "..." if $len + 3 <= length $str;
61 $str
55} 62}
56 63
57sub asxml($) { 64sub asxml($) {
58 local $_ = $_[0]; 65 local $_ = $_[0];
59 66
62 s/</&lt;/g; 69 s/</&lt;/g;
63 70
64 $_ 71 $_
65} 72}
66 73
67package CFPlus::Database; 74sub socketpipe() {
75 socketpair my $fh1, my $fh2, Socket::AF_UNIX, Socket::SOCK_STREAM, Socket::PF_UNSPEC
76 or die "cannot establish bidirectional pipe: $!\n";
68 77
69our @ISA = BerkeleyDB::Btree::; 78 ($fh1, $fh2)
70
71sub get($$) {
72 my $data;
73
74 $_[0]->db_get ($_[1], $data) == 0
75 ? $data
76 : ()
77} 79}
78 80
79my %DB_SYNC; 81sub background(&;&) {
82 my ($bg, $cb) = @_;
80 83
81sub put($$$) { 84 my ($fh_r, $fh_w) = DC::socketpipe;
82 my ($db, $key, $data) = @_;
83 85
84 my $hkey = $db + 0; 86 my $pid = fork;
85 Scalar::Util::weaken $db; 87
86 $DB_SYNC{$hkey} ||= AnyEvent->timer (after => 5, cb => sub { 88 if (defined $pid && !$pid) {
87 delete $DB_SYNC{$hkey}; 89 local $SIG{__DIE__};
88 $db->db_sync if $db; 90
91 open STDOUT, ">&", $fh_w;
92 open STDERR, ">&", $fh_w;
93 close $fh_r;
94 close $fh_w;
95
96 $| = 1;
97
98 eval { $bg->() };
99
100 if ($@) {
101 my $msg = $@;
102 $msg =~ s/\n+/\n/;
103 warn "FATAL: $msg";
104 DC::_exit 1;
105 }
106
107 # win32 is fucked up, of course. exit will clean stuff up,
108 # which destroys our database etc. _exit will exit ALL
109 # forked processes, because of the dreaded fork emulation.
110 DC::_exit 0;
111 }
112
113 close $fh_w;
114
115 my $buffer;
116
117 my $w; $w = AnyEvent->io (fh => $fh_r, poll => 'r', cb => sub {
118 unless (sysread $fh_r, $buffer, 4096, length $buffer) {
119 undef $w;
120 $cb->();
121 return;
122 }
123
124 while ($buffer =~ s/^(.*)\n//) {
125 my $line = $1;
126 $line =~ s/\s+$//;
127 utf8::decode $line;
128 if ($line =~ /^\x{e877}json_msg (.*)$/s) {
129 $cb->(JSON::XS->new->allow_nonref->decode ($1));
130 } else {
131 ::message ({
132 markup => "background($pid): " . DC::asxml $line,
133 });
134 }
135 }
89 }); 136 });
90
91 $db->db_put ($key => $data)
92} 137}
93 138
139sub background_msg {
140 my ($msg) = @_;
141
142 $msg = "\x{e877}json_msg " . JSON::XS->new->allow_nonref->encode ($msg);
143 $msg =~ s/\n//g;
144 utf8::encode $msg;
145 print $msg, "\n";
146}
147
94package CFPlus; 148package DC;
149
150our $RC_THEME = "theme-metal";
151our $RC_BASE;
152
153for (grep !ref, @INC) {
154 $RC_BASE = "$_/Deliantra/Client/private/resources";
155 last if -d $RC_BASE;
156}
95 157
96sub find_rcfile($) { 158sub find_rcfile($) {
97 my $path; 159 my $path;
98 160
99 for (grep !ref, @INC) { 161 $path = "$RC_BASE/$RC_THEME/$_[0]";
100 $path = "$_/CFPlus/resources/$_[0]";
101 return $path if -r $path; 162 return $path if -r $path;
102 }
103 163
164 $path = "$RC_BASE/$_[0]";
165 return $path if -r $path;
166
104 die "FATAL: can't find required file $_[0]\n"; 167 die "FATAL: can't find required file \"$_[0]\" in \"$RC_BASE\"\n";
105}
106
107BEGIN {
108 use Crossfire::Protocol::Base ();
109 *to_json = \&Crossfire::Protocol::Base::to_json;
110 *from_json = \&Crossfire::Protocol::Base::from_json;
111} 168}
112 169
113sub read_cfg { 170sub read_cfg {
114 my ($file) = @_; 171 my ($file) = @_;
115 172
117 or return; 174 or return;
118 175
119 local $/; 176 local $/;
120 my $CFG = <$fh>; 177 my $CFG = <$fh>;
121 178
122 if ($CFG =~ /^---/) { ## TODO compatibility cruft, remove
123 require YAML;
124 utf8::decode $CFG;
125 $::CFG = YAML::Load ($CFG);
126 } elsif ($CFG =~ /^\{/) {
127 $::CFG = from_json $CFG; 179 $::CFG = decode_json $CFG;
128 } else {
129 $::CFG = eval $CFG; ## todo comaptibility cruft
130 }
131} 180}
132 181
133sub write_cfg { 182sub write_cfg {
134 my ($file) = @_; 183 my $file = "$Deliantra::VARDIR/client.cf";
135 184
136 $::CFG->{VERSION} = $::VERSION; 185 $::CFG->{VERSION} = $::VERSION;
137 186
138 open my $fh, ">:utf8", $file 187 open my $fh, ">:utf8", $file
139 or return; 188 or return;
140 print $fh to_json $::CFG; 189 print $fh JSON::XS->new->utf8->pretty->encode ($::CFG);
141} 190}
142 191
143our $DB_ENV; 192sub http_proxy {
144our $DB_STATE; 193 my @proxy = win32_proxy_info;
145 194
146sub db_table($) { 195 if (@proxy) {
196 "http://" . (@proxy < 2 ? "" : @proxy < 3 ? "$proxy[1]\@" : "$proxy[1]:$proxy[2]\@") . $proxy[0]
197 } elsif (exists $ENV{http_proxy}) {
198 $ENV{http_proxy}
199 } else {
200 ()
201 }
202}
203
204sub set_proxy {
205 my $proxy = http_proxy
206 or return;
207
208 $ENV{http_proxy} = $proxy;
209}
210
211sub lwp_useragent {
212 require LWP::UserAgent;
213
214 DC::set_proxy;
215
216 my $ua = LWP::UserAgent->new (
217 agent => "deliantra $VERSION",
218 keep_alive => 1,
219 env_proxy => 1,
220 timeout => 30,
221 );
222}
223
224sub lwp_check($) {
147 my ($table) = @_; 225 my ($res) = @_;
148 226
149 $table =~ s/([^a-zA-Z0-9_\-])/sprintf "=%x=", ord $1/ge; 227 $res->is_error
228 and die $res->status_line;
150 229
151 new CFPlus::Database 230 $res
152 -Env => $DB_ENV,
153 -Filename => $table,
154# -Filename => "database",
155# -Subname => $table,
156 -Property => DB_CHKSUM,
157 -Flags => DB_CREATE | DB_UPGRADE,
158 or die "unable to create/open database table $_[0]: $BerkeleyDB::Error"
159} 231}
160 232
161{ 233sub fh_nonblocking($$) {
162 use strict; 234 my ($fh, $nb) = @_;
163 235
164 mkdir "$Crossfire::VARDIR/cfplus", 0777; 236 if ($^O eq "MSWin32") {
165 my $recover = $BerkeleyDB::db_version >= 4.4 237 $nb = (! ! $nb) + 0;
166 ? eval "DB_REGISTER | DB_RECOVER" 238 ioctl $fh, 0x8004667e, \$nb; # FIONBIO
167 : 0; 239 } else {
168 240 fcntl $fh, &Fcntl::F_SETFL, $nb ? &Fcntl::O_NONBLOCK : 0;
169 $DB_ENV = new BerkeleyDB::Env 241 }
170 -Home => "$Crossfire::VARDIR/cfplus",
171 -Cachesize => 1_000_000,
172 -ErrFile => "$Crossfire::VARDIR/cfplus/errorlog.txt",
173# -ErrPrefix => "DATABASE",
174 -Verbose => 1,
175 -Flags => DB_CREATE | DB_RECOVER | DB_INIT_MPOOL | DB_INIT_LOCK | DB_INIT_TXN | $recover,
176 -SetFlags => DB_AUTO_COMMIT | DB_LOG_AUTOREMOVE,
177 or die "unable to create/open database home $Crossfire::VARDIR/cfplus: $BerkeleyDB::Error";
178
179 $DB_STATE = db_table "state";
180} 242}
181 243
182package CFPlus::Layout; 244package DC::Layout;
183 245
184$CFPlus::OpenGL::SHUTDOWN_HOOK{"CFPlus::Layout"} = sub { 246$DC::OpenGL::INIT_HOOK{"DC::Layout"} = sub {
185 reset_glyph_cache; 247 glyph_cache_restore;
186}; 248};
187 249
188package CFPlus::Item; 250$DC::OpenGL::SHUTDOWN_HOOK{"DC::Layout"} = sub {
189 251 glyph_cache_backup;
190use strict; 252};
191use Crossfire::Protocol::Constants;
192
193my $last_enter_count = 1;
194
195sub desc_string {
196 my ($self) = @_;
197
198 my $desc =
199 $self->{nrof} < 2
200 ? $self->{name}
201 : "$self->{nrof} × $self->{name_pl}";
202
203 $self->{flags} & F_OPEN
204 and $desc .= " (open)";
205 $self->{flags} & F_APPLIED
206 and $desc .= " (applied)";
207 $self->{flags} & F_UNPAID
208 and $desc .= " (unpaid)";
209 $self->{flags} & F_MAGIC
210 and $desc .= " (magic)";
211 $self->{flags} & F_CURSED
212 and $desc .= " (cursed)";
213 $self->{flags} & F_DAMNED
214 and $desc .= " (damned)";
215 $self->{flags} & F_LOCKED
216 and $desc .= " *";
217
218 $desc
219}
220
221sub weight_string {
222 my ($self) = @_;
223
224 my $weight = ($self->{nrof} || 1) * $self->{weight};
225
226 $weight < 0 ? "?" : $weight * 0.001
227}
228
229sub do_n_dialog {
230 my ($cb) = @_;
231
232 my $w = new CFPlus::UI::Toplevel
233 on_delete => sub { $_[0]->destroy; 1 },
234 has_close_button => 1,
235 ;
236
237 $w->add (my $vb = new CFPlus::UI::VBox x => "center", y => "center");
238 $vb->add (new CFPlus::UI::Label text => "Enter item count:");
239 $vb->add (my $entry = new CFPlus::UI::Entry
240 text => $last_enter_count,
241 on_activate => sub {
242 my ($entry) = @_;
243 $last_enter_count = $entry->get_text;
244 $cb->($last_enter_count);
245 $w->hide;
246 $w->destroy;
247
248 0
249 },
250 on_escape => sub { $w->destroy; 1 },
251 );
252 $entry->grab_focus;
253 $w->show;
254}
255
256sub update_widgets {
257 my ($self) = @_;
258
259 # necessary to avoid cyclic references
260 Scalar::Util::weaken $self;
261
262 my $button_cb = sub {
263 my (undef, $ev, $x, $y) = @_;
264
265 my $targ = $::CONN->{player}{tag};
266
267 if ($self->{container} == $::CONN->{player}{tag}) {
268 $targ = $::CONN->{open_container};
269 }
270
271 if (($ev->{mod} & CFPlus::KMOD_SHIFT) && $ev->{button} == 1) {
272 $::CONN->send ("move $targ $self->{tag} 0")
273 if $targ || !($self->{flags} & F_LOCKED);
274 } elsif (($ev->{mod} & CFPlus::KMOD_SHIFT) && $ev->{button} == 2) {
275 $self->{flags} & F_LOCKED
276 ? $::CONN->send ("lock " . pack "CN", 0, $self->{tag})
277 : $::CONN->send ("lock " . pack "CN", 1, $self->{tag})
278 } elsif ($ev->{button} == 1) {
279 $::CONN->send ("examine $self->{tag}");
280 } elsif ($ev->{button} == 2) {
281 $::CONN->send ("apply $self->{tag}");
282 } elsif ($ev->{button} == 3) {
283 my $move_prefix = $::CONN->{open_container} ? 'put' : 'drop';
284 if ($self->{container} == $::CONN->{open_container}) {
285 $move_prefix = "take";
286 }
287
288 my @menu_items = (
289 ["examine", sub { $::CONN->send ("examine $self->{tag}") }],
290 ["mark", sub { $::CONN->send ("mark ". pack "N", $self->{tag}) }],
291 ["ignite/thaw", # first try of an easier use of flint&steel
292 sub {
293 $::CONN->send ("mark ". pack "N", $self->{tag});
294 $::CONN->send ("command apply flint and steel");
295 }
296 ],
297 ["inscribe", # first try of an easier use of flint&steel
298 sub {
299 &::open_string_query ("Text to inscribe", sub {
300 my ($entry, $txt) = @_;
301 $::CONN->send ("mark ". pack "N", $self->{tag});
302 $::CONN->send ("command use_skill inscription $txt");
303 });
304 }
305 ],
306 ["rename", # first try of an easier use of flint&steel
307 sub {
308 &::open_string_query ("Rename item to:", sub {
309 my ($entry, $txt) = @_;
310 $::CONN->send ("mark ". pack "N", $self->{tag});
311 $::CONN->send ("command rename to <$txt>");
312 }, $self->{name},
313 "If you input no name or erase the current custom name, the custom name will be unset");
314 }
315 ],
316 ["apply", sub { $::CONN->send ("apply $self->{tag}") }],
317 (
318 $self->{flags} & F_LOCKED
319 ? (
320 ["unlock", sub { $::CONN->send ("lock " . pack "CN", 0, $self->{tag}) }],
321 )
322 : (
323 ["lock", sub { $::CONN->send ("lock " . pack "CN", 1, $self->{tag}) }],
324 ["$move_prefix all", sub { $::CONN->send ("move $targ $self->{tag} 0") }],
325 ["$move_prefix &lt;n&gt;",
326 sub {
327 do_n_dialog (sub { $::CONN->send ("move $targ $self->{tag} $_[0]") })
328 }
329 ]
330 )
331 ),
332 );
333
334 CFPlus::UI::Menu->new (items => \@menu_items)->popup ($ev);
335 }
336
337 1
338 };
339
340 my $tooltip_std = "<small>"
341 . "Left click - examine item\n"
342 . "Shift-Left click - " . ($self->{container} ? "move or drop" : "take") . " item\n"
343 . "Middle click - apply\n"
344 . "Shift-Middle click - lock/unlock\n"
345 . "Right click - further options"
346 . "</small>\n";
347
348 my $bg = $self->{flags} & F_CURSED ? [1 , 0 , 0, 0.5]
349 : $self->{flags} & F_MAGIC ? [0.2, 0.2, 1, 0.5]
350 : undef;
351
352 $self->{face_widget} ||= new CFPlus::UI::Face
353 can_events => 1,
354 can_hover => 1,
355 anim => $self->{anim},
356 animspeed => $self->{animspeed}, # TODO# must be set at creation time
357 on_button_down => $button_cb,
358 ;
359 $self->{face_widget}{bg} = $bg;
360 $self->{face_widget}{face} = $self->{face};
361 $self->{face_widget}{anim} = $self->{anim};
362 $self->{face_widget}{animspeed} = $self->{animspeed};
363 $self->{face_widget}->set_tooltip (
364 "<b>Face/Animation.</b>\n"
365 . "Item uses face #$self->{face}. "
366 . ($self->{animspeed} ? "Item uses animation #$self->{anim} at " . (1 / $self->{animspeed}) . "fps. " : "Item is not animated. ")
367 . "\n\n$tooltip_std"
368 );
369
370 $self->{desc_widget} ||= new CFPlus::UI::Label
371 can_events => 1,
372 can_hover => 1,
373 ellipsise => 2,
374 align => -1,
375 on_button_down => $button_cb,
376 ;
377 my $desc = CFPlus::Item::desc_string $self;
378 $self->{desc_widget}{bg} = $bg;
379 $self->{desc_widget}->set_text ($desc);
380 $self->{desc_widget}->set_tooltip ("<b>$desc</b>.\n$tooltip_std");
381
382 $self->{weight_widget} ||= new CFPlus::UI::Label
383 can_events => 1,
384 can_hover => 1,
385 ellipsise => 0,
386 align => 0,
387 on_button_down => $button_cb,
388 ;
389 $self->{weight_widget}{bg} = $bg;
390 $self->{weight_widget}->set_text (CFPlus::Item::weight_string $self);
391 $self->{weight_widget}->set_tooltip (
392 "<b>Weight</b>.\n"
393 . ($self->{weight} >= 0 ? "One item weighs $self->{weight}g. " : "You have no idea how much this weighs. ")
394 . ($self->{nrof} ? "You have $self->{nrof} of it. " : "Item cannot stack with others of it's kind. ")
395 . "\n\n$tooltip_std"
396 );
397}
398 253
3991; 2541;
400 255
401=back 256=back
402 257

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines