#! perl # mandatory our $SCHEDULE_INTERVAL = $cf::CFG{extractor_schedule_interval} || 3600; use JSON::XS; our $db_mapinfo = cf::sync_job { cf::db_table "tag-mapinfo" }; # info/cache for maps our $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 $txn = $cf::DB_ENV->txn_begin; utf8::encode $key; BDB::db_get $db_mapinfo, $txn, $key, my $data; unless ($!) { $data = decode_json $data; return if $data->{hash} eq $hash; # remove all old tags unconditionally remove_tag_target $txn, $_, $key for @{ $data->{tags} }; } my $f = new_from_file cf::object::thawer $file or return; my @tags = sort $f->extract_tags; $data = encode_json { hash => $hash, tags => \@tags }; BDB::db_put $db_mapinfo, $txn, $key, $data; # add all tags add_tag_target $txn, $_, $key for @tags; # 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 $maps = cf::map::static_maps; scan_map "s$_", "$cf::MAPDIR$_.map" for @$maps; } 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::decode_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, Coro::unblock_sub { $Coro::current->prio (Coro::PRIO_MIN); $Coro::current->desc ("map-tag scanner"); reload; }; cf::post_init { $RELOAD_SCHEDULER->invoke (0); # force at startup }; # 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 } sub unload { my $guard = cf::lock_acquire "map-tags::reload"; BDB::db_close $db_target; BDB::db_close $db_mapinfo; }