1 |
#! perl # depends=irc mandatory |
2 |
|
3 |
# statistics-package |
4 |
|
5 |
use Fcntl; |
6 |
use Coro::AIO; |
7 |
|
8 |
CONF EXPORT_RECENTLOG = undef |
9 |
CONF EXPORT_RECENTLOG_INTERVAL = 300 |
10 |
|
11 |
############################################################################# |
12 |
# stuffs |
13 |
|
14 |
our %PLAYERSEEN; |
15 |
|
16 |
# EV, because we call ->start |
17 |
our $UPDATE_LOGINS = EV::idle sub { |
18 |
$_[0]->stop; |
19 |
|
20 |
cf::async { |
21 |
my ($status, @pl) = ext::commands::who_listing; |
22 |
|
23 |
my $fh = aio_open "$LOCALDIR/usercount", O_WRONLY | O_CREAT | O_TRUNC, 0644 |
24 |
or return; |
25 |
aio_write $fh, 0, undef, scalar @pl, 0; |
26 |
aio_close $fh; |
27 |
|
28 |
my $who; |
29 |
for ($status, @pl) { |
30 |
s/[<&]//g; |
31 |
$who .= "$_<br />\n"; |
32 |
} |
33 |
|
34 |
my $fh = aio_open "$LOCALDIR/userlisting.html", O_WRONLY | O_CREAT | O_TRUNC, 0644 |
35 |
or return; |
36 |
aio_write $fh, 0, undef, $who, 0; |
37 |
aio_close $fh; |
38 |
}; |
39 |
}; |
40 |
|
41 |
cf::object->attach ( |
42 |
on_kill => sub { |
43 |
my ($ob, $hitter) = @_; |
44 |
|
45 |
return unless $hitter; |
46 |
$hitter = $hitter->outer_owner; |
47 |
my $pl = $hitter->contr |
48 |
or return; |
49 |
|
50 |
++$hitter->{stats_kill}{$ob->name}; |
51 |
}, |
52 |
); |
53 |
|
54 |
cf::player->attach ( |
55 |
on_login => sub { |
56 |
my ($pl) = @_; |
57 |
|
58 |
$pl->ob->kv_set (schmorplog_last_login => time); |
59 |
|
60 |
$pl->ob->kv_set (schmorplog_login_count => |
61 |
1 + $pl->ob->kv_get ("schmorplog_login_count")); |
62 |
|
63 |
$pl->ob->kv_set (schmorplog_client => $pl->ns->{who_version}); |
64 |
|
65 |
my $name = $pl->ob->name; |
66 |
|
67 |
ext::irc::do_notice $name . " logged in"; |
68 |
|
69 |
undef $PLAYERSEEN{$name}; |
70 |
$UPDATE_LOGINS->start; |
71 |
|
72 |
cf::trace "LOGIN: ", $pl->ob->name, " from ", $pl->ns->host; |
73 |
}, |
74 |
on_logout => sub { |
75 |
my ($pl, $cleanly) = @_; |
76 |
$pl->ob->kv_set (schmorplog_last_logout => time); |
77 |
ext::irc::do_notice $pl->ob->name . " left"; |
78 |
|
79 |
$UPDATE_LOGINS->start; |
80 |
|
81 |
cf::trace "LOGOUT: ", $pl->ob->name, " from ", $pl->ns->host, " ($cleanly)"; |
82 |
}, |
83 |
on_birth => sub { |
84 |
my ($pl) = @_; |
85 |
$pl->ob->kv_set (schmorplog_birthdate => time); |
86 |
ext::irc::do_notice $pl->ob->name . " was just born"; |
87 |
|
88 |
cf::trace "BIRTH: ", $pl->ob->name, " from ", $pl->ns->host; |
89 |
}, |
90 |
on_quit => sub { |
91 |
my ($pl) = @_; |
92 |
ext::irc::do_notice $pl->ob->name . " quit the game"; |
93 |
|
94 |
cf::trace "QUIT: ", $pl->ob->name, " from ", $pl->ns->host; |
95 |
}, |
96 |
on_death => sub { |
97 |
my ($pl) = @_; |
98 |
|
99 |
my $msg = $pl->expand_cfpod ($pl->ob->name . " was killed by " . $pl->killer_name . "."); |
100 |
ext::irc::do_notice $msg; |
101 |
|
102 |
++$pl->ob->{stats_death}{$pl->killer_name}; |
103 |
|
104 |
$pl->ob->kv_set (schmorplog_death_count => |
105 |
1 + $pl->ob->kv_get ("schmorplog_death_count")); |
106 |
|
107 |
$_->send_msg ("" => $msg, cf::NDI_VERBATIM) |
108 |
for cf::player::list; |
109 |
}, |
110 |
on_load => sub { |
111 |
my ($pl, $path) = @_; |
112 |
$pl->ob->kv_set (schmorplog_last_load => time); |
113 |
}, |
114 |
on_save => sub { |
115 |
my ($pl, $path) = @_; |
116 |
$pl->ob->kv_set (schmorplog_last_save => time); |
117 |
}, |
118 |
); |
119 |
|
120 |
cf::register_script_function "statistician::talk" => sub { |
121 |
my ($who, $msg, $npc) = @_; |
122 |
|
123 |
my ($cmd, $args) = split /\s+/, $msg, 2; |
124 |
|
125 |
$args ||= $who->name; |
126 |
|
127 |
if ($cmd eq "deaths") { |
128 |
cf::async { |
129 |
my $pl = cf::player::find $args |
130 |
or return $who->reply ($npc, "I don't know any person named '$args'."); |
131 |
|
132 |
my $s = $pl->ob->{stats_death} |
133 |
or return $who->reply ($npc, "$args didn't die even once."); |
134 |
|
135 |
my $rep = "$args died a total of " . $pl->ob->kv_get ("schmorplog_death_count") . " times, among them:\n\n"; |
136 |
|
137 |
for (sort { $s->{$b} <=> $s->{$a} } keys %$s) { |
138 |
$rep .= sprintf " C<%4d> time(s) due to %s.\n", $s->{$_}, $_; |
139 |
} |
140 |
|
141 |
$who->reply ($npc, $rep); |
142 |
}; |
143 |
} elsif ($cmd eq "kills") { |
144 |
cf::async { |
145 |
my $pl = cf::player::find $args |
146 |
or return $who->reply ($npc, "I don't know any person named '$args'."); |
147 |
|
148 |
my $s = $pl->ob->{stats_kill} |
149 |
or return $who->reply ($npc, "I don't know of I<anything> that $args has killed so far."); |
150 |
|
151 |
my $rep = sprintf "Here is a list of all the kills I know about:\n\n"; |
152 |
for (sort { $s->{$b} <=> $s->{$a} } keys %$s) { |
153 |
$rep .= sprintf " C<%6s> %s\n", $s->{$_}, $_; |
154 |
} |
155 |
|
156 |
$who->reply ($npc, $rep); |
157 |
}; |
158 |
} elsif ($cmd eq "hi") { |
159 |
$who->reply ($npc, "Hello!\n\n" |
160 |
. "I am a statistician, I keep statistics about all people here.\n\n" |
161 |
. " - To know how often somebody died, ask C<deaths> I<playername>\n" |
162 |
. " - To know how many kills somebody scored, ask C<kills> I<playername>"); |
163 |
} else { |
164 |
$who->reply ($npc, "No idea what you want of me, how about saying 'hi' first?"); |
165 |
} |
166 |
|
167 |
$cmd = lc $cmd; |
168 |
}; |
169 |
|
170 |
# log "crash" messages, i.e. client exit reasons |
171 |
cf::register_exticmd clientlog => sub { |
172 |
my ($ns, $msg) = @_; |
173 |
|
174 |
my $name = $ns->pl && $ns->pl->ob ? $ns->pl->ob->name : "<unknown>"; |
175 |
|
176 |
$msg =~ y/\x0a\x20-\x7f//cd; |
177 |
$msg =~ s/\s+$//; |
178 |
|
179 |
cf::error sprintf "clientlog [%s/%s]: %s\n", $ns->host, $name, $msg; |
180 |
|
181 |
() |
182 |
}; |
183 |
|
184 |
############################################################################# |
185 |
# log max playercount every minute |
186 |
|
187 |
our $STATSDIR = "$LOCALDIR/maxplayers"; |
188 |
|
189 |
mkdir $STATSDIR; |
190 |
|
191 |
our $WRITE_MAXPLAYERS = EV::periodic 0, 60, undef, sub { |
192 |
my $now = AE::now; |
193 |
my $cnt = scalar keys %PLAYERSEEN; |
194 |
|
195 |
%PLAYERSEEN = map +($_->ob->name => undef), grep $_->ns, cf::player::list; |
196 |
|
197 |
my @time = gmtime $now; |
198 |
|
199 |
my $path = sprintf "%s/%04d-%02d-%02d", $STATSDIR, $time[5] + 1900, $time[4] + 1, $time[3]; |
200 |
my $offs = $time [2] * 60 + $time [1]; |
201 |
|
202 |
$cnt++; |
203 |
$cnt = 254 if $cnt > 254; |
204 |
$cnt = chr $cnt; |
205 |
|
206 |
IO::AIO::aio_open $path, O_WRONLY | O_CREAT, 0666, sub { |
207 |
my $fh = shift |
208 |
or return; |
209 |
|
210 |
# the truncate is 1440 extra syscalls, but saves 1439 |
211 |
# slow metadata updates. |
212 |
IO::AIO::aio_truncate $fh, 1440, sub { |
213 |
IO::AIO::aio_write $fh, $offs, 1, $cnt, 0, sub { |
214 |
IO::AIO::aio_close $fh; |
215 |
}; |
216 |
}; |
217 |
}; |
218 |
}; |
219 |
|
220 |
############################################################################# |
221 |
# export recentlog |
222 |
|
223 |
our %RECENT; |
224 |
our $EXPORT_RECENTLOG_GRACE = 120; |
225 |
|
226 |
our $update_w; |
227 |
our %need_update; |
228 |
|
229 |
sub _update_login { |
230 |
my ($login) = @_; |
231 |
|
232 |
my $path = (cf::player::playerdir $login) . "/playerdata"; |
233 |
|
234 |
if (0 >= aio_load $path, my $data) { |
235 |
delete $RECENT{$login}; |
236 |
} else { |
237 |
local $_ = $data; |
238 |
|
239 |
my $birthdate = /^schmorplog_birthdate (\S+)$/m ? $1 : undef; |
240 |
my $login_count = /^schmorplog_login_count (\S+)$/m ? $1 : 0; |
241 |
my $death_count = /^schmorplog_death_count (\S+)$/m ? $1 : 0; |
242 |
my $last_save = /^schmorplog_last_save (\S+)$/m ? $1 : undef; |
243 |
my $last_login = /^schmorplog_last_login (\S+)$/m ? $1 : undef; |
244 |
my $last_logout = /^schmorplog_last_logout (\S+)$/m ? $1 : undef; |
245 |
my $client = /^schmorplog_client (.*)$/m ? $1 : "?"; |
246 |
my $map = /^map (.*)$/m ? $1 : "?"; |
247 |
|
248 |
return delete $RECENT{$login} unless $last_login; |
249 |
|
250 |
$last_logout = $last_save if $last_save > $last_logout && $last_login > $last_logout && $last_save < $NOW - 10 * 60; |
251 |
$last_logout = undef if $last_logout < $last_login; |
252 |
|
253 |
return delete $RECENT{$login} unless $last_login > $NOW - 86400 * ($login_count * 7 + 10); |
254 |
|
255 |
# next if $count < 3 && $login < $NOW - 86400*2; |
256 |
$RECENT{$login} = [$login, $birthdate, $last_login, $login_count, $last_logout, $client, $death_count, $map]; |
257 |
} |
258 |
} |
259 |
|
260 |
sub _update { |
261 |
cf::async_ext { |
262 |
$Coro::current->nice (1); |
263 |
$Coro::current->{desc} = "recentlog updater"; |
264 |
|
265 |
my $lock = cf::lock_acquire "export_recentlog"; |
266 |
|
267 |
my @logins = keys %need_update; %need_update = (); |
268 |
undef $update_w; |
269 |
|
270 |
Coro::AnyEvent::sleep $EXPORT_RECENTLOG_GRACE; # grace time to allow file-saves |
271 |
|
272 |
my $t0 = EV::now; |
273 |
|
274 |
_update_login $_ |
275 |
for @logins; |
276 |
|
277 |
cf::get_slot 0.1, 0, "recentlog serialise"; |
278 |
|
279 |
my $NOW = $cf::NOW; |
280 |
|
281 |
cf::replace_file $EXPORT_RECENTLOG, cf::encode_json { |
282 |
version => 1, |
283 |
date => $NOW, |
284 |
data => [ |
285 |
sort { ($b->[4] || $NOW) <=> ($a->[4] || $NOW) } |
286 |
values %RECENT |
287 |
], |
288 |
} or warn "$EXPORT_RECENTLOG: $!"; |
289 |
|
290 |
cf::trace "recentlog updated (", EV::now - $t0, "s).\n"; |
291 |
}; |
292 |
} |
293 |
|
294 |
sub update { |
295 |
return unless defined $EXPORT_RECENTLOG; |
296 |
|
297 |
$update_w ||= AE::timer $EXPORT_RECENTLOG_INTERVAL - $EXPORT_RECENTLOG_GRACE, 0, \&_update; |
298 |
} |
299 |
|
300 |
sub reload { |
301 |
return unless defined $EXPORT_RECENTLOG; |
302 |
|
303 |
my $lock = cf::lock_acquire "export_recentlog"; |
304 |
|
305 |
cf::async_ext { |
306 |
$lock; |
307 |
|
308 |
$Coro::current->{desc} = "recentlog reloader"; |
309 |
|
310 |
undef $need_update{$_} |
311 |
for @{ +cf::player::list_logins }; |
312 |
|
313 |
_update; |
314 |
}; |
315 |
} |
316 |
|
317 |
cf::player->attach ( |
318 |
on_login => sub { undef $need_update{$_[0]->ob->name}; update }, |
319 |
on_logout => sub { undef $need_update{$_[0]->ob->name}; update }, |
320 |
on_birth => sub { undef $need_update{$_[0]->ob->name}; update }, |
321 |
on_death => sub { undef $need_update{$_[0]->ob->name}; update }, |
322 |
# on_load => sub { undef $need_update{$_[0]->ob->name}; update }, |
323 |
on_save => sub { undef $need_update{$_[0]->ob->name}; update }, |
324 |
); |
325 |
|
326 |
cf::post_init { |
327 |
reload; |
328 |
}; |
329 |
|