… | |
… | |
2 | |
2 | |
3 | our $SCHEDULE_INTERVAL = $cf::CFG{extractor_schedule_interval} || 3600; |
3 | our $SCHEDULE_INTERVAL = $cf::CFG{extractor_schedule_interval} || 3600; |
4 | |
4 | |
5 | use JSON::XS; |
5 | use JSON::XS; |
6 | |
6 | |
7 | my $db_mapinfo = cf::sync_job { cf::db_table "tag-mapinfo" }; # info/cache for maps |
7 | our $db_mapinfo = cf::db_table "tag-mapinfo"; # info/cache for maps |
8 | my $db_target = cf::sync_job { cf::db_table "tag-target" }; # tag => maps |
8 | our $db_target = cf::db_table "tag-target"; # tag => maps |
9 | |
9 | |
10 | sub remove_tag_target { |
10 | sub remove_tag_target { |
11 | my ($txn, $tag, $target) = @_; |
11 | my ($txn, $tag, $target) = @_; |
12 | # - U O |
12 | # - U O |
13 | |
13 | |
… | |
… | |
40 | Coro::AIO::aio_stat $file |
40 | Coro::AIO::aio_stat $file |
41 | and next; |
41 | and next; |
42 | |
42 | |
43 | my $hash = join ",", 1, (stat _)[7,9], $file; |
43 | my $hash = join ",", 1, (stat _)[7,9], $file; |
44 | |
44 | |
45 | my $old_tags; |
|
|
46 | |
|
|
47 | my $txn = $cf::DB_ENV->txn_begin; |
45 | my $txn = $cf::DB_ENV->txn_begin; |
48 | |
46 | |
49 | utf8::encode $key; |
47 | utf8::encode $key; |
50 | BDB::db_get $db_mapinfo, $txn, $key, my $data; |
48 | BDB::db_get $db_mapinfo, $txn, $key, my $data; |
51 | |
49 | |
52 | unless ($!) { |
50 | unless ($!) { |
53 | $data = from_json $data; |
51 | $data = decode_json $data; |
54 | return if $data->{hash} eq $hash; |
52 | return if $data->{hash} eq $hash; |
55 | $old_tags = $data->{tags}; |
53 | |
|
|
54 | # remove all old tags unconditionally |
|
|
55 | remove_tag_target $txn, $_, $key |
|
|
56 | for @{ $data->{tags} }; |
56 | } |
57 | } |
57 | |
|
|
58 | $old_tags ||= []; |
|
|
59 | |
58 | |
60 | my $f = new_from_file cf::object::thawer $file |
59 | my $f = new_from_file cf::object::thawer $file |
61 | or return; |
60 | or return; |
62 | |
61 | |
63 | my @tags = sort $f->extract_tags; |
62 | my @tags = sort $f->extract_tags; |
64 | $data = to_json { hash => $hash, tags => \@tags }; |
63 | $data = encode_json { hash => $hash, tags => \@tags }; |
65 | |
64 | |
66 | BDB::db_put $db_mapinfo, $txn, $key, $data; |
65 | BDB::db_put $db_mapinfo, $txn, $key, $data; |
67 | |
66 | |
68 | # 1. remove tags no longer existing |
67 | # add all tags |
69 | for my $tag (@$old_tags) { |
|
|
70 | next if grep $_ eq $tag, @tags; |
|
|
71 | remove_tag_target $txn, $tag, $key; |
|
|
72 | } |
|
|
73 | |
|
|
74 | # 2. add tags that are new |
|
|
75 | for my $tag (@tags) { |
|
|
76 | next if grep $_ eq $tag, @$old_tags; |
|
|
77 | add_tag_target $txn, $tag, $key; |
68 | add_tag_target $txn, $_, $key |
78 | } |
69 | for @tags; |
79 | |
70 | |
80 | # we don't actually care if it succeeds or not, as we |
71 | # we don't actually care if it succeeds or not, as we |
81 | # will just retry an hour later |
72 | # will just retry an hour later |
82 | BDB::db_txn_commit $txn; |
73 | BDB::db_txn_finish $txn; |
83 | |
74 | |
84 | # warn "tag-updated $file (= $key) $hash\n";#d# |
75 | warn "tag-updated $file (= $key) <@tags>\n" |
|
|
76 | if @tags; |
85 | } |
77 | } |
86 | |
78 | |
87 | sub scan_static { |
79 | sub scan_static { |
88 | my ($dir, $map) = @_; |
80 | my $maps = cf::map::static_maps; |
89 | |
81 | |
90 | my ($dirs, $files) = Coro::AIO::aio_scandir $dir, 2 |
82 | scan_map "s$_", "$cf::MAPDIR$_.map" |
91 | or return; |
83 | for @$maps; |
|
|
84 | } |
92 | |
85 | |
93 | for my $file (@$files) { |
86 | sub reload { |
94 | my $name = $file; |
87 | my $guard = cf::lock_acquire "map-tags::reload"; |
95 | next unless $name =~ s/\.map$//; |
|
|
96 | utf8::decode $name; |
|
|
97 | |
88 | |
98 | scan_map "s$map$name", "$dir/$file"; |
89 | my $start = AE::time; |
|
|
90 | |
|
|
91 | # 1. check for maps no longer existing |
|
|
92 | { |
|
|
93 | my @delkeys; |
|
|
94 | |
|
|
95 | my $cursor = $db_mapinfo->cursor; |
|
|
96 | for (;;) { |
|
|
97 | BDB::db_c_get $cursor, my $key, my $data, BDB::NEXT; |
|
|
98 | last if $!; |
|
|
99 | |
|
|
100 | my $data = JSON::XS::decode_json $data; |
|
|
101 | my ($ver, undef, undef, $path) = split /,/, $data->{hash}, 4; |
|
|
102 | push @delkeys, [$key, $data->{tags}] |
|
|
103 | if $ver != 1 || Coro::AIO::aio_stat $path; |
|
|
104 | } |
|
|
105 | BDB::db_c_close $cursor; |
|
|
106 | |
|
|
107 | for (@delkeys) { |
|
|
108 | my ($key, $tags) = @$_; |
|
|
109 | my $txn = $cf::DB_ENV->txn_begin; |
|
|
110 | BDB::db_del $db_mapinfo, $txn, $key; |
|
|
111 | for my $tag (@{ $tags || [] }) { |
|
|
112 | remove_tag_target $txn, $tag, $key; |
|
|
113 | } |
|
|
114 | BDB::db_txn_finish $txn; |
|
|
115 | } |
99 | } |
116 | } |
100 | |
117 | |
101 | &scan_static ("$dir/$_", "$map$_/") |
118 | # 2. scan all static maps |
102 | for @$dirs; |
119 | scan_static $cf::MAPDIR, "/"; |
103 | } |
|
|
104 | |
120 | |
105 | cf::async_ext { |
|
|
106 | $Coro::current->prio (Coro::PRIO_MIN); |
|
|
107 | |
|
|
108 | while () { |
|
|
109 | my $start = Event::time; |
|
|
110 | |
|
|
111 | # 1. check for maps no longer existing |
|
|
112 | |
|
|
113 | # 2. scan all static maps |
|
|
114 | scan_static $cf::MAPDIR, "/"; |
|
|
115 | |
|
|
116 | # 3. scan all dynamic maps |
121 | # 3. scan all dynamic maps |
117 | for my $path (@{ cf::map::tmp_maps or [] }, @{ cf::map::random_maps or [] }) { |
122 | for my $path (@{ cf::map::tmp_maps or [] }, @{ cf::map::random_maps or [] }) { |
118 | # my $map = cf::map::find $path; |
123 | # my $map = cf::map::find $path; |
119 | # extract_map_tags "t/$map", $path; |
124 | # extract_map_tags "t/$map", $path; |
120 | } |
125 | } |
121 | |
126 | |
122 | # now hunt for all per-player maps |
127 | # now hunt for all per-player maps |
123 | # scan_dir $cf::PLAYERDIR |
128 | # scan_dir $cf::PLAYERDIR |
124 | # for my $login (@{ cf::player::list_logins or [] }) { |
129 | # for my $login (@{ cf::player::list_logins or [] }) { |
125 | # for my $path (@{ cf::player::maps $login or [] }) { |
130 | # for my $path (@{ cf::player::maps $login or [] }) { |
126 | # cf::cede_to_tick; |
131 | # cf::cede_to_tick; |
… | |
… | |
144 | # delete $map->{deny_reset}; |
149 | # delete $map->{deny_reset}; |
145 | # } |
150 | # } |
146 | # } |
151 | # } |
147 | # } |
152 | # } |
148 | |
153 | |
149 | warn sprintf "map-tag scan (%fs)", Event::time - $start; |
154 | warn sprintf "map-tag scan finished (%fs)\n", AE::time - $start; |
150 | Coro::Timer::sleep $SCHEDULE_INTERVAL; |
155 | } |
151 | } |
156 | |
|
|
157 | our $RELOAD_SCHEDULER = cf::periodic $SCHEDULE_INTERVAL, Coro::unblock_sub { |
|
|
158 | $Coro::current->prio (Coro::PRIO_MIN); |
|
|
159 | $Coro::current->desc ("map-tag scanner"); |
|
|
160 | reload; |
152 | }; |
161 | }; |
153 | |
162 | |
|
|
163 | cf::post_init { |
|
|
164 | $RELOAD_SCHEDULER->invoke (0); # force at startup |
|
|
165 | }; |
|
|
166 | |
|
|
167 | # find all objects with the given tag, or at least try to |
154 | sub find($) { |
168 | sub find($) { |
155 | my ($tag) = @_; |
169 | my ($tag) = @_; |
156 | |
|
|
157 | my @res; |
|
|
158 | |
170 | |
159 | utf8::encode (my $key = $tag); |
171 | utf8::encode (my $key = $tag); |
160 | BDB::db_get $db_target, undef, $key, my $data; |
172 | BDB::db_get $db_target, undef, $key, my $data; |
161 | utf8::decode $data; |
173 | utf8::decode $data; |
162 | |
174 | |
163 | for my $map ( |
175 | map { $_->load; $_->find_tagged_objects ($tag) } |
164 | grep $_, |
176 | grep $_, |
165 | map { cf::map::find $_ } |
177 | map { cf::map::find $_ } |
166 | grep s/^s//, |
178 | grep s/^s//, |
167 | split /\x00/, $data |
179 | split /\x00/, $data |
168 | ) { |
180 | } |
169 | warn "map $map,$tag\n";#d# |
|
|
170 | $map->load; |
|
|
171 | |
181 | |
172 | push @res, $map->find_tagged_objects ($tag); |
182 | sub unload { |
|
|
183 | my $guard = cf::lock_acquire "map-tags::reload"; |
173 | |
184 | |
174 | warn "tag<$tag>map<$map>res<@res>\n";#d# |
185 | BDB::db_close $db_target; |
175 | } |
186 | BDB::db_close $db_mapinfo; |
176 | |
|
|
177 | @res |
|
|
178 | } |
187 | } |