#! perl # mandatory our $SCHEDULE_INTERVAL = $cf::CFG{extractor_schedule_interval} || 3600; use JSON::XS; my $db_mapinfo = cf::sync_job { cf::db_table "tag-mapinfo" }; # info/cache for maps my $db_target = cf::sync_job { cf::db_table "tag-target" }; # tag => maps sub remove_tag_target { my ($txn, $tag, $target) = @_; # - U O utf8::encode $tag; BDB::db_get $db_target, $txn, $tag, my $data; my @tags = split /\x00/, $data; @tags = grep $_ ne $target, @tags; if (@tags) { BDB::db_put $db_target, $txn, $tag, join "\x00", @tags; } else { BDB::db_del $db_target, $txn, $tag; } } sub add_tag_target { my ($txn, $tag, $target) = @_; utf8::encode $tag; BDB::db_put $db_target, $txn, $tag, my $data; my @tags = split /\x00/, $data; push @tags, $target; BDB::db_put $db_target, $txn, $tag, join "\x00", @tags; } sub scan_map($$) { my ($key, $file) = @_; # unicode key, octets file Coro::AIO::aio_stat $file and next; my $hash = join ",", 1, (stat _)[7,9], $file; my $old_tags; my $txn = $cf::DB_ENV->txn_begin; utf8::encode $key; BDB::db_get $db_mapinfo, $txn, $key, my $data; unless ($!) { $data = from_json $data; return if $data->{hash} eq $hash; $old_tags = $data->{tags}; } $old_tags ||= []; my $f = new_from_file cf::object::thawer $file or return; my @tags = sort $f->extract_tags; $data = to_json { hash => $hash, tags => \@tags }; BDB::db_put $db_mapinfo, $txn, $key, $data; # 1. remove tags no longer existing for my $tag (@$old_tags) { next if grep $_ eq $tag, @tags; remove_tag_target $txn, $tag, $key; } # 2. add tags that are new for my $tag (@tags) { next if grep $_ eq $tag, @$old_tags; add_tag_target $txn, $tag, $key; } # we don't actually care if it succeeds or not, as we # will just retry an hour later BDB::db_txn_finish $txn; warn "tag-updated $file (= $key) <@tags>\n" if @tags; } sub scan_static { my ($dir, $map) = @_; my ($dirs, $files) = Coro::AIO::aio_scandir $dir, 2 or return; for my $file (@$files) { my $name = $file; next unless $name =~ s/\.map$//; utf8::decode $name; scan_map "s$map$name", "$dir/$file"; } &scan_static ("$dir/$_", "$map$_/") for @$dirs; } sub reload { my $guard = cf::lock_acquire "map-tags::reload"; my $start = EV::time; # 1. check for maps no longer existing { my @delkeys; my $cursor = $db_mapinfo->cursor; for (;;) { BDB::db_c_get $cursor, my $key, my $data, BDB::NEXT; last if $!; my $data = JSON::XS::from_json $data; my ($ver, undef, undef, $path) = split /,/, $data->{hash}, 4; push @delkeys, [$key, $data->{tags}] if $ver != 1 || Coro::AIO::aio_stat $path; } BDB::db_c_close $cursor; for (@delkeys) { my ($key, $tags) = @$_; my $txn = $cf::DB_ENV->txn_begin; BDB::db_del $db_mapinfo, $txn, $key; for my $tag (@{ $tags || [] }) { remove_tag_target $txn, $tag, $key; } BDB::db_txn_finish $txn; } } # 2. scan all static maps scan_static $cf::MAPDIR, "/"; # 3. scan all dynamic maps for my $path (@{ cf::map::tmp_maps or [] }, @{ cf::map::random_maps or [] }) { # my $map = cf::map::find $path; # extract_map_tags "t/$map", $path; } # now hunt for all per-player maps # scan_dir $cf::PLAYERDIR # for my $login (@{ cf::player::list_logins or [] }) { # for my $path (@{ cf::player::maps $login or [] }) { # cf::cede_to_tick; # # $path =~ /^~[^\/]+(\/.*)$/ # or next; # doh # # my $base = cf::map::find $1; # # # skip maps without base maps on the assumption # # that those are old, unresettable maps # next unless $base; # # # skip unresettable maps, for speed # next if $base->{deny_reset}; # # my $map = cf::map::find $path; # # if ($map->{deny_reset}) { # warn "found noreset map with resettable base map, resetting: $path\n"; # delete $map->{deny_reset}; # } # } # } warn sprintf "map-tag scan (%fs)", EV::time - $start; } our $RELOAD_SCHEDULER = cf::periodic $SCHEDULE_INTERVAL, sub { cfd::async { $Coro::current->prio (Coro::PRIO_MIN); $Coro::current->desc ("map-tag scanner"); reload; }; }; # find all objects with the given tag, or at least try to sub find($) { my ($tag) = @_; utf8::encode (my $key = $tag); BDB::db_get $db_target, undef, $key, my $data; utf8::decode $data; map { $_->load; $_->find_tagged_objects ($tag) } grep $_, map { cf::map::find $_ } grep s/^s//, split /\x00/, $data }