Module: Lich::Common::MapBase::ClassMethods
- Defined in:
- documented/common/map/map_base.rb
Overview
Class methods shared across all Map implementations
Constant Summary collapse
- TAG_INDEX_BUILD_ATTEMPTS =
Tag name => Array of room ids. Private: callers must go through #rooms_by_tag or #tag_names so the memo cannot be mutated in place.
Rebuild attempts before giving up on caching the result.
3- TAG_INDEX_MUTEX =
Serialises publishing the memo against invalidating it. Only ever held across a couple of assignments, never across a build or a load, so it cannot invert the ordering against the load mutex.
Mutex.new
Instance Method Summary collapse
-
#[](val) ⇒ Object?
Look up a room by id, uid string, or fuzzy title/description text.
-
#apply_wayto_overrides ⇒ void
Applies personal map wayto overrides and custom targets from YAML settings.
-
#dijkstra(source, destination = nil) ⇒ Array<Hash>?
(also: #dijkstra_hashes)
Class-level dijkstra dispatcher.
-
#estimate_time(array) ⇒ Float
Estimate total travel time for a path.
-
#findpath(source, destination) ⇒ Object
Find path between two rooms.
-
#get_free_id ⇒ Integer
Get the next available room ID.
-
#ids_from_uid(uid) ⇒ Object
Get room IDs from a UID.
-
#json_map_files ⇒ Array<String>
JSON map databases in the data directory, newest first.
-
#legacy_map_files ⇒ Array<String>
Legacy map files sitting in the data directory, basenames only.
-
#load(filename = nil) ⇒ Boolean
Load the newest JSON map database, or a specific file.
-
#load_json(filename = nil) ⇒ Boolean
Load the newest usable JSON map database, falling back to older candidates when one is unreadable.
-
#map_loaded_message(filename) ⇒ String
Announced once a database has loaded.
-
#match_multi_ids(ids) ⇒ Integer?
Narrow a set of candidate ids to the one reachable from the current room.
-
#match_no_uid ⇒ Object?
Resolve the current room when the game gave no usable uid.
-
#normalize_tag_lists(rooms = list) ⇒ nil
Re-wrap plain Array tags as TagList.
-
#parse_map_json(filename) ⇒ Boolean
False when the file was unusable.
-
#previous_uid ⇒ Integer?
The uid the game last navigated away from.
-
#reload ⇒ Object
Reload the map database.
-
#report_unsupported_map_files(files) ⇒ nil
Explain why an old map database no longer loads.
-
#reset_tag_index ⇒ nil
Drop the tag memo.
-
#rooms_by_tag(tag_name) ⇒ Array<Integer>
Room ids carrying a tag, nearest-agnostic and in room id order.
-
#save_json(filename = nil) ⇒ Object
(also: #save)
Save map as JSON file.
-
#set_current(id) ⇒ Object?
Record the room the game moved to, remembering the one it left.
-
#set_fuzzy(id) ⇒ Object?
As #set_current, but a nil id leaves the previous room untouched.
-
#tag_names ⇒ Array<String>
Tag names present anywhere in the room list, in room id order.
-
#tags ⇒ Array<String>
Tag names present anywhere in the room list.
-
#to_json(*args) ⇒ Object
Convert map to JSON.
-
#uids_add(uid, id) ⇒ Object
Add a UID mapping.
-
#validate_room_json!(room, filename) ⇒ nil
The id is the only field a room cannot do without: it indexes the room into the backing array, and a non-Integer would raise there with a message that says nothing about the database.
Instance Method Details
#[](val) ⇒ Object?
Look up a room by id, uid string, or fuzzy title/description text
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 |
# File 'documented/common/map/map_base.rb', line 499 def [](val) # One load attempt via the accessor, then work off that array; calling # #list again would retry the load on every lookup when it failed. rooms = list if val.is_a?(Integer) || val =~ /^[0-9]+$/ rooms[val.to_i] elsif val =~ /^u(-?\d+)$/i uid_request = ::Regexp.last_match(1).dup.to_i # nil.to_i is 0, so an unknown uid used to resolve to room 0. id = ids_from_uid(uid_request)[0] id.nil? ? nil : rooms[id.to_i] else chkre = /#{val.strip.sub(/\.$/, '').gsub(/\.(?:\.\.)?/, '|')}/i chk = /#{Regexp.escape(val.strip)}/i # Title and exact-description matches share one pass; the loose # regex pass only runs when neither found anything. Same precedence # as the three sequential scans this replaces. live = rooms.compact by_title = nil by_desc = nil live.each do |room| if room.title.find { |title| title =~ chk } by_title = room break end by_desc = room if by_desc.nil? && room.description.find { |desc| desc =~ chk } end by_title || by_desc || live.find { |room| room.description.find { |desc| desc =~ chkre } } end end |
#apply_wayto_overrides ⇒ void
This method returns an undefined value.
Applies personal map wayto overrides and custom targets from YAML settings. Reads base_wayto_overrides, personal_wayto_overrides, and personal_map_targets from the user's profile via get_settings. Ensures the map is loaded before accessing room data, consistent with other ClassMethods.
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 |
# File 'documented/common/map/map_base.rb', line 691 def apply_wayto_overrides self.load unless loaded? settings = get_settings base_overrides = settings.base_wayto_overrides || {} personal_overrides = settings.personal_wayto_overrides || {} wayto_overrides = base_overrides.merge(personal_overrides) wayto_overrides.each do |_key, values| next unless values.is_a?(Hash) && values['start_room'] && values['end_room'] start_room_id = values['start_room'].to_i end_room_id = values['end_room'].to_s start_room = list[start_room_id] next unless start_room if values['str_proc'] start_room.wayto[end_room_id] = StringProc.new(values['str_proc'].to_s) end if values['travel_time'] new_timeto = Float(values['travel_time'], exception: false) new_timeto ||= StringProc.new(values['travel_time'].to_s) start_room.timeto[end_room_id] = new_timeto end end personal_map_targets = settings.personal_map_targets if personal_map_targets.is_a?(Hash) custom_targets = (GameSettings['custom targets'] || {}) custom_targets.merge!(personal_map_targets) GameSettings['custom targets'] = custom_targets end end |
#dijkstra(source, destination = nil) ⇒ Array<Hash>? Also known as: dijkstra_hashes
Class-level dijkstra dispatcher
561 562 563 564 565 566 567 568 569 570 |
# File 'documented/common/map/map_base.rb', line 561 def dijkstra(source, destination = nil) if source.is_a?(self) source.dijkstra(destination) elsif (room = self[source]) room.dijkstra(destination) else echo 'Map.dijkstra: error: invalid source room' nil end end |
#estimate_time(array) ⇒ Float
Estimate total travel time for a path
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'documented/common/map/map_base.rb', line 270 def estimate_time(array) self.load unless loaded? unless array.is_a?(Array) raise Exception.exception('MapError'), 'Map.estimate_time was given something not an array!' end time = 0.0 until array.length < 2 room = array.shift # A path can name a room that is gone. Under the NilClass patch the # lookup yielded nil and the 0.2 default below applied; keep that # without relying on the patch. current = self[room] t = current.nil? ? nil : current.timeto[array.first.to_s] if t time += t.is_a?(StringProc) ? t.call.to_f : t.to_f else time += 0.2 end end time end |
#findpath(source, destination) ⇒ Object
Find path between two rooms
624 625 626 627 628 629 630 631 632 633 |
# File 'documented/common/map/map_base.rb', line 624 def findpath(source, destination) if source.is_a?(self) source.path_to(destination) elsif (room = self[source]) room.path_to(destination) else echo 'Map.findpath: error: invalid source room' nil end end |
#get_free_id ⇒ Integer
Get the next available room ID
193 194 195 196 197 198 199 200 |
# File 'documented/common/map/map_base.rb', line 193 def get_free_id rooms = list.compact # An empty map yields 1, which is what nil.id + 1 produced via Lich's # NilClass patch. Stating it means this no longer depends on that. return 1 if rooms.empty? rooms.max_by(&:id).id + 1 end |
#ids_from_uid(uid) ⇒ Object
Get room IDs from a UID
648 649 650 |
# File 'documented/common/map/map_base.rb', line 648 def ids_from_uid(uid) uids[uid] || [] end |
#json_map_files ⇒ Array<String>
JSON map databases in the data directory, newest first
462 463 464 465 466 467 468 469 470 471 |
# File 'documented/common/map/map_base.rb', line 462 def json_map_files directory = File.join(DATA_DIR, XMLData.game) return [] unless Dir.exist?(directory) Dir.entries(directory) .find_all { |fn| fn =~ /^map-[0-9]+\.json$/i } .collect { |fn| File.join(directory, fn) } .sort .reverse end |
#legacy_map_files ⇒ Array<String>
Legacy map files sitting in the data directory, basenames only
475 476 477 478 479 480 |
# File 'documented/common/map/map_base.rb', line 475 def legacy_map_files directory = File.join(DATA_DIR, XMLData.game) return [] unless Dir.exist?(directory) Dir.entries(directory).grep(/^map(?:-[0-9]+)?\.(?:dat|xml)$/i).sort end |
#load(filename = nil) ⇒ Boolean
Load the newest JSON map database, or a specific file
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 |
# File 'documented/common/map/map_base.rb', line 534 def load(filename = nil) file_list = filename.nil? ? json_map_files : [filename] # An explicitly named .dat or .xml would otherwise reach load_json and # raise a parse error rather than saying why it cannot be loaded. unsupported, file_list = file_list.partition { |fn| fn =~ /\.(?:dat|xml)\z/i } if file_list.empty? if unsupported.empty? respond '--- Lich: error: no map database found' report_unsupported_map_files(legacy_map_files) else report_unsupported_map_files(unsupported.map { |fn| File.basename(fn) }) end return false end while (filename = file_list.shift) return true if load_json(filename) end false end |
#load_json(filename = nil) ⇒ Boolean
Load the newest usable JSON map database, falling back to older candidates when one is unreadable. The two game classes differ only in how a parsed room is constructed and what they announce, so those are hooks: #room_from_json and #map_loaded_message.
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'documented/common/map/map_base.rb', line 371 def load_json(filename = nil) synchronize_load do return true if loaded? file_list = filename ? [filename] : json_map_files if file_list.empty? respond '--- Lich: error: no map database found' return false end while (filename = file_list.shift) next unless File.exist?(filename) next unless parse_map_json(filename) respond (filename) mark_loaded load_uids return true end false end end |
#map_loaded_message(filename) ⇒ String
Announced once a database has loaded. Names the script that triggered the load when there is one; a load can also happen with no script running, and nil.name only survived via Lich's NilClass patch.
438 439 440 441 |
# File 'documented/common/map/map_base.rb', line 438 def (filename) name = Script.current&.name name ? "--- #{name} Map loaded #{filename}" : "--- Map loaded #{filename}" end |
#match_multi_ids(ids) ⇒ Integer?
Narrow a set of candidate ids to the one reachable from the current room
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'documented/common/map/map_base.rb', line 229 def match_multi_ids(ids) # current_room_id can be nil, stale, or point at a hole. Under Lich's # NilClass patch a nil id made Array#[] hand back the whole room list # and the subsequent .wayto then yielded nil, so the result was no # matches either way. Check before indexing rather than relying on it. return nil if current_room_id.nil? current = list[current_room_id] return nil if current.nil? matches = ids.find_all { |s| current.wayto.keys.include?(s.to_s) } return matches[0] if matches.size == 1 nil end |
#match_no_uid ⇒ Object?
Resolve the current room when the game gave no usable uid. Delegates to the game-specific matchers.
217 218 219 220 221 222 223 |
# File 'documented/common/map/map_base.rb', line 217 def match_no_uid if (script = Script.current) set_current(match_current(script)) else set_fuzzy(match_fuzzy) end end |
#normalize_tag_lists(rooms = list) ⇒ nil
Re-wrap plain Array tags as TagList. Rooms that reach the list without going through the constructor, such as a caller assigning a list it built itself, otherwise hold tags that cannot invalidate the index.
Callers inside a load must pass the rooms explicitly, because the #list accessor triggers #load when the map is not yet loaded, and #load holds a non-reentrant mutex.
611 612 613 614 615 616 617 618 619 620 621 |
# File 'documented/common/map/map_base.rb', line 611 def normalize_tag_lists(rooms = list) rooms.compact.each do |room| existing = room. next if existing.is_a?(TagList) # Round trip through the writer, which is what rewraps the plain # Array as a TagList bound to this class. room. = existing end reset_tag_index end |
#parse_map_json(filename) ⇒ Boolean
Returns false when the file was unusable.
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'documented/common/map/map_base.rb', line 397 def parse_map_json(filename) File.open(filename) do |f| JSON.parse(f.read).each do |room| validate_room_json!(room, filename) # Defaulted before the loops below read .keys on them. # Every field except the id is optional in a real mapdb. room['title'] ||= [] room['description'] ||= [] room['paths'] ||= [] room['wayto'] ||= {} room['timeto'] ||= {} room['tags'] ||= [] room['uid'] ||= [] room['wayto'].keys.each do |k| room['wayto'][k] = StringProc.new(room['wayto'][k][3..]) if room['wayto'][k][0..2] == ';e ' end room['timeto'].keys.each do |k| if room['timeto'][k].is_a?(String) && room['timeto'][k][0..2] == ';e ' room['timeto'][k] = StringProc.new(room['timeto'][k][3..]) end end room_from_json(room) end end true rescue StandardError => e # A corrupt or unreadable database must not abort the load or leave a # half-built map behind. Report it, drop whatever was registered, and # let the caller try an older candidate. raw_list because the load # mutex is held and #list would re-enter it. respond "--- Lich: error: failed to load #{filename}: #{e.}" raw_list.clear false end |
#previous_uid ⇒ Integer?
The uid the game last navigated away from
210 211 212 |
# File 'documented/common/map/map_base.rb', line 210 def previous_uid XMLData.previous_nav_rm end |
#reload ⇒ Object
Reload the map database
636 637 638 639 |
# File 'documented/common/map/map_base.rb', line 636 def reload clear load end |
#report_unsupported_map_files(files) ⇒ nil
Explain why an old map database no longer loads. The Marshal (.dat) and XML formats were deprecated for years and support has been removed, so "no map database found" on its own would be misleading for anyone whose data directory still holds one.
488 489 490 491 492 493 494 |
# File 'documented/common/map/map_base.rb', line 488 def report_unsupported_map_files(files) return if files.empty? respond "--- Lich: found map data in a format that is no longer supported: #{files.sort.join(', ')}" respond '--- Lich: download the current JSON map database to continue.' nil end |
#reset_tag_index ⇒ nil
Drop the tag memo. Call after mutating any room's tags in place.
590 591 592 593 594 595 596 597 598 599 600 |
# File 'documented/common/map/map_base.rb', line 590 def reset_tag_index host = tag_cache_host TAG_INDEX_MUTEX.synchronize do # Under the same mutex as publication: bumping the generation outside # it could land between a publisher's validation and its assignment. host.instance_variable_set(:@tag_index_generation, (host.instance_variable_get(:@tag_index_generation) || 0) + 1) host.instance_variable_set(:@tag_index, nil) end nil end |
#rooms_by_tag(tag_name) ⇒ Array<Integer>
Room ids carrying a tag, nearest-agnostic and in room id order
584 585 586 |
# File 'documented/common/map/map_base.rb', line 584 def rooms_by_tag(tag_name) (tag_index[tag_name] || []).dup end |
#save_json(filename = nil) ⇒ Object Also known as: save
Save map as JSON file
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 |
# File 'documented/common/map/map_base.rb', line 659 def save_json(filename = nil) filename ||= File.join(DATA_DIR, XMLData.game, "map-#{Time.now.to_i}.json") if File.exist?(filename) respond 'File exists! Backing it up before proceeding...' begin File.open(filename, 'rb') do |infile| File.open("#{filename}.bak", 'wb:UTF-8') do |outfile| outfile.write(infile.read) end end rescue StandardError => e respond "--- Lich: error: #{e}\n\t#{e.backtrace[0..1].join("\n\t")}" Lich.log "error: #{e}\n\t#{e.backtrace.join("\n\t")}" end end File.open(filename, 'wb:UTF-8') { |file| file.write(to_json) } respond "#{filename} saved" # Reload if the map index appears corrupted: the last entry's id should # index back to itself. Nothing to check when the map holds no rooms, # where self[-1] is nil and only the NilClass patch made this pass. last = list.compact.last reload if !last.nil? && self[last.id]&.id != last.id end |
#set_current(id) ⇒ Object?
Record the room the game moved to, remembering the one it left
248 249 250 251 252 253 254 |
# File 'documented/common/map/map_base.rb', line 248 def set_current(id) self.previous_room_id = current_room_id if id != current_room_id self.current_room_id = id return nil if id.nil? list[id] end |
#set_fuzzy(id) ⇒ Object?
As #set_current, but a nil id leaves the previous room untouched
259 260 261 262 263 264 265 |
# File 'documented/common/map/map_base.rb', line 259 def set_fuzzy(id) self.previous_room_id = current_room_id if !id.nil? && id != current_room_id self.current_room_id = id return nil if id.nil? list[id] end |
#tag_names ⇒ Array<String>
Tag names present anywhere in the room list, in room id order
577 578 579 |
# File 'documented/common/map/map_base.rb', line 577 def tag_names tag_index.keys end |
#tags ⇒ Array<String>
Tag names present anywhere in the room list
204 205 206 |
# File 'documented/common/map/map_base.rb', line 204 def tag_names end |
#to_json(*args) ⇒ Object
Convert map to JSON
653 654 655 656 |
# File 'documented/common/map/map_base.rb', line 653 def to_json(*args) list.delete_if(&:nil?) list.sort_by(&:id).to_json(args) end |
#uids_add(uid, id) ⇒ Object
Add a UID mapping
642 643 644 645 |
# File 'documented/common/map/map_base.rb', line 642 def uids_add(uid, id) uids[uid] ||= [] uids[uid] << id unless uids[uid].include?(id) end |
#validate_room_json!(room, filename) ⇒ nil
The id is the only field a room cannot do without: it indexes the room into the backing array, and a non-Integer would raise there with a message that says nothing about the database. Everything else is optional and defaulted in #parse_map_json - the shipped mapdb has rooms with no description or paths, such as the fog transitions, so requiring those fields would reject a valid database outright.
453 454 455 456 457 458 |
# File 'documented/common/map/map_base.rb', line 453 def validate_room_json!(room, filename) id = room['id'] return nil if id.is_a?(Integer) raise "#{File.basename(filename)}: room id is not an Integer: #{id.inspect}" end |