1 | #! perl |
1 | #! perl |
2 | |
2 | |
3 | # why this is an extension is a good question. next question. |
3 | # why this is an extension is a good question. next question. |
4 | |
4 | |
5 | use Digest::MD5; |
|
|
6 | use Fcntl; |
5 | use Fcntl; |
7 | |
6 | |
8 | use Coro; |
7 | use Coro; |
9 | use Coro::AIO; |
8 | use Coro::AIO; |
10 | |
9 | |
11 | our $emergency_position = $cf::CFG{emergency_position} || ["/world/world_105_115", 5, 37]; |
10 | our $emergency_position = $cf::CFG{emergency_position} || ["/world/world_105_115", 5, 37]; |
12 | |
11 | |
13 | our $DEACTIVATE_TIMEOUT = 60; # number of seconds after which maps get deactivated to save cpu |
12 | our $DEACTIVATE_TIMEOUT = 60; # number of seconds after which maps get deactivated to save cpu |
14 | our $SWAP_TIMEOUT = 600; # number of seconds after which maps inactive get swapped out |
13 | our $SWAP_TIMEOUT = 600; # number of seconds after which maps inactive get swapped out |
15 | our $SCHEDULE_INTERVAL = 8; # time the map scheduler sleeps between runs |
14 | our $SCHEDULE_INTERVAL = 8; # time the map scheduler sleeps between runs |
16 | our $SAVE_TIMEOUT = 60; # save maps every n seconds |
15 | our $SAVE_TIMEOUT = 60; # save maps every n seconds |
17 | our $SAVE_INTERVAL = 0.4; # save at max. one map every $SAVE_HOLD |
16 | our $SAVE_INTERVAL = 0.4; # save at max. one map every $SAVE_HOLD |
18 | our $MAX_RESET = 7200; |
17 | our $MAX_RESET = 7200; |
19 | |
|
|
20 | our $RANDOM_MAPS = cf::localdir . "/random"; |
|
|
21 | mkdir $RANDOM_MAPS; |
|
|
22 | |
18 | |
23 | $DEACTIVATE_TIMEOUT = 3;#d# |
19 | $DEACTIVATE_TIMEOUT = 3;#d# |
24 | $SWAP_TIMEOUT = 5;#d# |
20 | $SWAP_TIMEOUT = 5;#d# |
25 | $SCHEDULE_INTERVAL = 1; |
21 | $SCHEDULE_INTERVAL = 1; |
26 | |
22 | |
27 | $cf::LINK_MAP ||= do { |
|
|
28 | my $map = cf::map::new; |
|
|
29 | |
|
|
30 | $map->width (41); |
|
|
31 | $map->height (41); |
|
|
32 | $map->alloc; |
|
|
33 | $map->path ("{link}"); |
|
|
34 | $map->{path} = bless { path => "{link}" }, "cf::path"; |
|
|
35 | $map->in_memory (cf::MAP_IN_MEMORY); |
|
|
36 | |
|
|
37 | $map |
|
|
38 | }; |
|
|
39 | |
|
|
40 | { |
|
|
41 | package cf::path; |
|
|
42 | |
|
|
43 | sub new { |
|
|
44 | my ($class, $path, $base) = @_; |
|
|
45 | |
|
|
46 | my $self = bless { }, $class; |
|
|
47 | |
|
|
48 | if ($path =~ s{^\?random/}{}) { |
|
|
49 | $self->{random} = cf::from_json $path; |
|
|
50 | } else { |
|
|
51 | if ($path =~ s{^~([^/]+)?}{}) { |
|
|
52 | $self->{user_rel} = 1; |
|
|
53 | |
|
|
54 | if (defined $1) { |
|
|
55 | $self->{user} = $1; |
|
|
56 | } elsif ($base =~ m{^~([^/]+)/}) { |
|
|
57 | $self->{user} = $1; |
|
|
58 | } else { |
|
|
59 | warn "cannot resolve user-relative path without user <$path,$base>\n"; |
|
|
60 | } |
|
|
61 | } elsif ($path =~ /^\//) { |
|
|
62 | # already absolute |
|
|
63 | } else { |
|
|
64 | $base =~ s{[^/]+/?$}{}; |
|
|
65 | return $class->new ("$base/$path"); |
|
|
66 | } |
|
|
67 | |
|
|
68 | for ($path) { |
|
|
69 | redo if s{/\.?/}{/}; |
|
|
70 | redo if s{/[^/]+/\.\./}{/}; |
|
|
71 | } |
|
|
72 | } |
|
|
73 | |
|
|
74 | $self->{path} = $path; |
|
|
75 | |
|
|
76 | $self |
|
|
77 | } |
|
|
78 | |
|
|
79 | # the name / primary key / in-game path |
|
|
80 | sub as_string { |
|
|
81 | my ($self) = @_; |
|
|
82 | |
|
|
83 | $self->{user_rel} ? "~$self->{user}$self->{path}" |
|
|
84 | : $self->{random} ? "?random/$self->{path}" |
|
|
85 | : $self->{path} |
|
|
86 | } |
|
|
87 | |
|
|
88 | # the displayed name, this is a one way mapping |
|
|
89 | sub visible_name { |
|
|
90 | my ($self) = @_; |
|
|
91 | |
|
|
92 | $self->{random} ? "?random/$self->{random}{origin_map}+$self->{random}{origin_x}+$self->{random}{origin_y}/$self->{random}{dungeon_level}" |
|
|
93 | : $self->as_string |
|
|
94 | } |
|
|
95 | |
|
|
96 | # escape the /'s in the path |
|
|
97 | sub _escaped_path { |
|
|
98 | # ∕ is U+2215 |
|
|
99 | (my $path = $_[0]{path}) =~ s/\//∕/g; |
|
|
100 | $path |
|
|
101 | } |
|
|
102 | |
|
|
103 | # the original (read-only) location |
|
|
104 | sub load_path { |
|
|
105 | my ($self) = @_; |
|
|
106 | |
|
|
107 | sprintf "%s/%s/%s", cf::datadir, cf::mapdir, $self->{path} |
|
|
108 | } |
|
|
109 | |
|
|
110 | # the temporary/swap location |
|
|
111 | sub save_path { |
|
|
112 | my ($self) = @_; |
|
|
113 | |
|
|
114 | $self->{user_rel} ? sprintf "%s/%s/%s/%s", cf::localdir, cf::playerdir, $self->{user}, $self->_escaped_path |
|
|
115 | : $self->{random} ? sprintf "%s/%s", $RANDOM_MAPS, Digest::MD5::md5_hex $self->{path} |
|
|
116 | : sprintf "%s/%s/%s", cf::localdir, cf::tmpdir, $self->_escaped_path |
|
|
117 | } |
|
|
118 | |
|
|
119 | # the unique path, might be eq to save_path |
|
|
120 | sub uniq_path { |
|
|
121 | my ($self) = @_; |
|
|
122 | |
|
|
123 | $self->{user_rel} || $self->{random} |
|
|
124 | ? undef |
|
|
125 | : sprintf "%s/%s/%s", cf::localdir, cf::uniquedir, $self->_escaped_path |
|
|
126 | } |
|
|
127 | |
|
|
128 | # return random map parameters, or undef |
|
|
129 | sub random_map_params { |
|
|
130 | my ($self) = @_; |
|
|
131 | |
|
|
132 | $self->{random} |
|
|
133 | } |
|
|
134 | |
|
|
135 | # this is somewhat ugly, but style maps do need special treatment |
|
|
136 | sub is_style_map { |
|
|
137 | $_[0]{path} =~ m{^/styles/} |
|
|
138 | } |
|
|
139 | } |
|
|
140 | |
|
|
141 | sub write_runtime { |
|
|
142 | my $runtime = cf::localdir . "/runtime"; |
|
|
143 | |
|
|
144 | my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 |
|
|
145 | or return; |
|
|
146 | |
|
|
147 | my $value = $cf::RUNTIME; |
|
|
148 | (aio_write $fh, 0, (length $value), $value, 0) <= 0 |
|
|
149 | and return; |
|
|
150 | |
|
|
151 | aio_fsync $fh |
|
|
152 | and return; |
|
|
153 | |
|
|
154 | close $fh |
|
|
155 | or return; |
|
|
156 | |
|
|
157 | aio_rename "$runtime~", $runtime |
|
|
158 | and return; |
|
|
159 | |
|
|
160 | 1 |
|
|
161 | } |
|
|
162 | |
|
|
163 | (Coro::async { |
|
|
164 | unless (write_runtime) { |
|
|
165 | warn "unable to write runtime file: $!"; |
|
|
166 | exit 1; |
|
|
167 | } |
|
|
168 | })->prio (Coro::PRIO_MAX); |
|
|
169 | |
|
|
170 | our $SCHEDULER = cf::coro { |
23 | our $SCHEDULER = cf::coro { |
171 | Coro::Timer::sleep 3600;#d#TODO#for debugging only |
|
|
172 | while () { |
24 | while () { |
173 | Coro::Timer::sleep $SCHEDULE_INTERVAL; |
25 | Coro::Timer::sleep $SCHEDULE_INTERVAL; |
174 | |
|
|
175 | write_runtime |
|
|
176 | or warn "unable to write runtime file: $!"; |
|
|
177 | |
26 | |
178 | for my $map (values %cf::MAP) { |
27 | for my $map (values %cf::MAP) { |
179 | eval { |
28 | eval { |
180 | next if $map->in_memory != cf::MAP_IN_MEMORY; |
29 | next if $map->in_memory != cf::MAP_IN_MEMORY; |
181 | next if $map->players; |
30 | next if $map->players; |
… | |
… | |
204 | $SCHEDULER->prio (-2); |
53 | $SCHEDULER->prio (-2); |
205 | |
54 | |
206 | sub generate_random_map { |
55 | sub generate_random_map { |
207 | my ($path, $rmp) = @_; |
56 | my ($path, $rmp) = @_; |
208 | |
57 | |
209 | # mit "rum" bekleckert, nicht |
58 | # mit "rum" bekleckern, nicht |
210 | cf::map::_create_random_map |
59 | cf::map::_create_random_map |
211 | $path, |
60 | $path, |
212 | $rmp->{wallstyle}, $rmp->{wall_name}, $rmp->{floorstyle}, $rmp->{monsterstyle}, |
61 | $rmp->{wallstyle}, $rmp->{wall_name}, $rmp->{floorstyle}, $rmp->{monsterstyle}, |
213 | $rmp->{treasurestyle}, $rmp->{layoutstyle}, $rmp->{doorstyle}, $rmp->{decorstyle}, |
62 | $rmp->{treasurestyle}, $rmp->{layoutstyle}, $rmp->{doorstyle}, $rmp->{decorstyle}, |
214 | $rmp->{origin_map}, $rmp->{final_map}, $rmp->{exitstyle}, $rmp->{this_map}, |
63 | $rmp->{origin_map}, $rmp->{final_map}, $rmp->{exitstyle}, $rmp->{this_map}, |
… | |
… | |
217 | $rmp->{expand2x}, $rmp->{layoutoptions1}, $rmp->{layoutoptions2}, $rmp->{layoutoptions3}, |
66 | $rmp->{expand2x}, $rmp->{layoutoptions1}, $rmp->{layoutoptions2}, $rmp->{layoutoptions3}, |
218 | $rmp->{symmetry}, $rmp->{difficulty}, $rmp->{difficulty_given}, $rmp->{difficulty_increase}, |
67 | $rmp->{symmetry}, $rmp->{difficulty}, $rmp->{difficulty_given}, $rmp->{difficulty_increase}, |
219 | $rmp->{dungeon_level}, $rmp->{dungeon_depth}, $rmp->{decoroptions}, $rmp->{orientation}, |
68 | $rmp->{dungeon_level}, $rmp->{dungeon_depth}, $rmp->{decoroptions}, $rmp->{orientation}, |
220 | $rmp->{origin_y}, $rmp->{origin_x}, $rmp->{random_seed}, $rmp->{total_map_hp}, |
69 | $rmp->{origin_y}, $rmp->{origin_x}, $rmp->{random_seed}, $rmp->{total_map_hp}, |
221 | $rmp->{map_layout_style}, $rmp->{treasureoptions}, $rmp->{symmetry_used}, |
70 | $rmp->{map_layout_style}, $rmp->{treasureoptions}, $rmp->{symmetry_used}, |
222 | $rmp->{region} |
71 | (cf::region::find $rmp->{region}) |
223 | } |
72 | } |
224 | |
73 | |
225 | sub parse_random_map_params { |
74 | sub parse_random_map_params { |
226 | my ($spec) = @_; |
75 | my ($spec) = @_; |
227 | |
76 | |
… | |
… | |
240 | } |
89 | } |
241 | |
90 | |
242 | sub prepare_random_map { |
91 | sub prepare_random_map { |
243 | my ($exit) = @_; |
92 | my ($exit) = @_; |
244 | |
93 | |
245 | # all this does is basically rpelace the /! path by |
94 | # all this does is basically replace the /! path by |
246 | # a new random map path (?random/...) with a seed |
95 | # a new random map path (?random/...) with a seed |
247 | # that depends on the exit object |
96 | # that depends on the exit object |
248 | |
97 | |
249 | my $rmp = parse_random_map_params $exit->msg; |
98 | my $rmp = parse_random_map_params $exit->msg; |
250 | |
99 | |
251 | if ($exit->map) { |
100 | if ($exit->map) { |
252 | $rmp->{region} = $exit->map->region ? $exit->map->region->name : undef; |
101 | $rmp->{region} = $exit->map->region_name; |
253 | $rmp->{origin_map} = $exit->map->path; |
102 | $rmp->{origin_map} = $exit->map->path; |
254 | $rmp->{origin_x} = $exit->x; |
103 | $rmp->{origin_x} = $exit->x; |
255 | $rmp->{origin_y} = $exit->y; |
104 | $rmp->{origin_y} = $exit->y; |
256 | } |
105 | } |
257 | |
106 | |
258 | $rmp->{random_seed} = $exit->random_seed; |
107 | $rmp->{random_seed} ||= $exit->random_seed; |
259 | |
108 | |
260 | $exit->slaying ("?random/" . cf::to_json $rmp); |
109 | my $data = cf::to_json $rmp; |
|
|
110 | my $md5 = Digest::MD5::md5_hex $data; |
|
|
111 | |
|
|
112 | if (my $fh = aio_open "$cf::RANDOM_MAPS/$md5.meta", O_WRONLY | O_CREAT, 0666) { |
|
|
113 | aio_write $fh, 0, (length $data), $data, 0; |
|
|
114 | |
|
|
115 | $exit->slaying ("?random/$md5"); |
261 | $exit->msg (undef); |
116 | $exit->msg (undef); |
|
|
117 | } |
262 | } |
118 | } |
263 | |
119 | |
264 | # and all this just because we cannot iterate over |
120 | # and all this just because we cannot iterate over |
265 | # all maps in C++... |
121 | # all maps in C++... |
266 | sub cf::map::change_all_map_light { |
122 | sub cf::map::change_all_map_light { |
… | |
… | |
280 | or return; |
136 | or return; |
281 | |
137 | |
282 | $map->load_header ($path) |
138 | $map->load_header ($path) |
283 | or return; |
139 | or return; |
284 | |
140 | |
285 | $map->reset_time (0) if $map->reset_time > $cf::RUNTIME; |
|
|
286 | $map->reset_timeout (10);#d# |
|
|
287 | |
|
|
288 | $map->{load_path} = $path; |
141 | $map->{load_path} = $path; |
|
|
142 | use Data::Dumper; warn Dumper $map;#d# |
289 | |
143 | |
290 | $map |
144 | $map |
291 | } |
145 | } |
292 | |
146 | |
293 | sub cf::map::find_map { |
147 | sub cf::map::find_map { |
294 | my ($path, $origin) = @_; |
148 | my ($path, $origin) = @_; |
295 | |
149 | |
296 | warn "find_map<$path,$origin>\n";#d# |
150 | #warn "find_map<$path,$origin>\n";#d# |
297 | |
151 | |
298 | $path = ref $path ? $path : new cf::path $path, $origin && $origin->path; |
152 | $path = ref $path ? $path : new cf::path $path, $origin && $origin->path; |
299 | my $key = $path->as_string; |
153 | my $key = $path->as_string; |
300 | |
154 | |
301 | $cf::MAP{$key} || do { |
155 | $cf::MAP{$key} || do { |
302 | # do it the slow way |
156 | # do it the slow way |
303 | my $map = try_load_header $path->save_path; |
157 | my $map = try_load_header $path->save_path; |
304 | |
158 | |
305 | if (!$map) { |
159 | if ($map) { |
|
|
160 | # safety |
|
|
161 | $map->{reset_time} = $cf::RUNTIME + $MAX_RESET |
|
|
162 | if $map->{reset_time} > $cf::RUNTIME + $MAX_RESET; |
|
|
163 | } else { |
306 | if (my $rmp = $path->random_map_params) { |
164 | if (my $rmp = $path->random_map_params) { |
307 | $map = generate_random_map $key, $rmp; |
165 | $map = generate_random_map $key, $rmp; |
308 | } else { |
166 | } else { |
309 | $map = try_load_header $path->load_path; |
167 | $map = try_load_header $path->load_path; |
310 | } |
168 | } |
311 | |
169 | |
312 | $map or return; |
170 | $map or return; |
313 | |
171 | |
314 | $map->{reset_time} = $cf::RUNTIME + $map->reset_timeout; |
172 | $map->{reset_time} = $cf::RUNTIME + ($map->reset_timeout || 3600); |
315 | $map->instantiate; |
173 | $map->instantiate; |
316 | |
174 | |
317 | # per-player maps become, after loading, normal maps |
175 | # per-player maps become, after loading, normal maps |
318 | $map->per_player (0) if $path->{user_rel}; |
176 | $map->per_player (0) if $path->{user_rel}; |
319 | } |
177 | } |
320 | |
178 | |
321 | $map->path ($key); |
179 | $map->path ($key); |
322 | $map->{path} = $path; |
180 | $map->{path} = $path; |
|
|
181 | $map->last_access ($cf::RUNTIME); |
323 | |
182 | |
324 | $map->reset if $map->should_reset; |
183 | $map->reset if $map->should_reset; |
325 | |
184 | |
326 | $cf::MAP{$key} = $map |
185 | $cf::MAP{$key} = $map |
327 | } |
186 | } |
… | |
… | |
365 | } |
224 | } |
366 | |
225 | |
367 | sub cf::map::load_map_sync { |
226 | sub cf::map::load_map_sync { |
368 | my ($path, $origin) = @_; |
227 | my ($path, $origin) = @_; |
369 | |
228 | |
370 | warn "load_map_sync<$path, $origin>\n";#d# |
229 | #warn "load_map_sync<$path, $origin>\n";#d# |
371 | |
|
|
372 | cf::abort if $path =~ /CVS/;#d# |
|
|
373 | |
230 | |
374 | cf::sync_job { |
231 | cf::sync_job { |
375 | my $map = cf::map::find_map $path, $origin |
232 | my $map = cf::map::find_map $path, $origin |
376 | or return; |
233 | or return; |
377 | $map->load; |
234 | $map->load; |
… | |
… | |
415 | } |
272 | } |
416 | |
273 | |
417 | sub cf::map::should_reset { |
274 | sub cf::map::should_reset { |
418 | my ($map) = @_; |
275 | my ($map) = @_; |
419 | |
276 | |
420 | return;#d# |
|
|
421 | # TODO: safety, remove and allow resettable per-player maps |
277 | # TODO: safety, remove and allow resettable per-player maps |
422 | return if $map->{path}{user_rel};#d# |
278 | return if $map->{path}{user_rel};#d# |
423 | return unless $map->reset_timeout; |
279 | #return unless $map->reset_timeout; |
424 | |
280 | |
425 | my $time = $map->fixed_resettime ? $map->{reset_time} : $map->last_access; |
281 | my $time = $map->fixed_resettime ? $map->{reset_time} : $map->last_access; |
426 | |
282 | |
427 | $time + $map->reset_timeout < $cf::RUNTIME |
283 | $time < $cf::RUNTIME |
428 | } |
284 | } |
429 | |
285 | |
430 | sub cf::map::reset { |
286 | sub cf::map::reset { |
431 | my ($self) = @_; |
287 | my ($self) = @_; |
432 | |
288 | |
… | |
… | |
490 | $map->load; |
346 | $map->load; |
491 | 1; |
347 | 1; |
492 | }) { |
348 | }) { |
493 | ($map, $x, $y) = ($oldmap, $oldx, $oldy); |
349 | ($map, $x, $y) = ($oldmap, $oldx, $oldy); |
494 | |
350 | |
495 | $ob->message ("Something went wrong within the server. " |
351 | $ob->message ("Something went wrong deep within the crossfire server. " |
496 | . "I'll try to bring you back to the map you were before. " |
352 | . "I'll try to bring you back to the map you were before. " |
497 | . "Please report this to the dungeon master", |
353 | . "Please report this to the dungeon master", |
498 | cf::NDI_UNIQUE | cf::NDI_RED); |
354 | cf::NDI_UNIQUE | cf::NDI_RED); |
499 | |
355 | |
500 | warn "ERROR in enter_exit: $@"; |
356 | warn "ERROR in enter_exit: $@"; |