Module: Lich::DragonRealms::DRParser
- Defined in:
- documented/dragonrealms/drinfomon/drparser.rb
Overview
Parses server output from DragonRealms and updates game state objects.
Handles inventory scrapes, skill experience, spell/ability discovery, account info, room contents, and game shutdown announcements. Routes parsed data to DRStats, DRSkill, DRSpells, DRRoom, GameObj, and related game-state models.
Defined Under Namespace
Modules: Pattern Classes: InventoryItemSax
Constant Summary collapse
- @@parsing_exp_mods_output =
Class variables for parsing state (must be @@ not @ for module-level state)
false- @@parsing_inventory_get =
false- @@inventory_partial =
true while the active inventory scrape is PARTIAL (INV SEARCH / category) -> items are upserted; false for a COMPLETE INV LIST -> items replace.
false- @@shutdown_at =
Wall-clock time the game is expected to go down for maintenance, set from a shutdown announcement. nil when no shutdown is pending.
nil
Class Method Summary collapse
-
.check_events(server_string) ⇒ String
Checks server output against registered Flag matchers.
-
.check_exp_mods(server_string) ⇒ String
Parses 'exp mods' output and updates DRSkill.exp_modifiers.
-
.check_game_shutdown(line) ⇒ void
Detects scheduled maintenance shutdown notices and records the target time, so any script can read DRParser.shutting_down? and DRParser.shutdown_minutes to wind down cleanly before the disconnect.
-
.check_known_barbarian_abilities(server_string) ⇒ String
Parses 'ability' command output for Barbarians.
-
.check_known_spells(server_string) ⇒ String
Parses 'spells' command output for magic users.
-
.check_known_thief_khri(server_string) ⇒ String
Parses 'ability' command output for Thieves.
-
.parse(line) ⇒ void
Main parser entry point.
-
.populate_inventory_get(server_string) ⇒ String
Parses inventory search output and populates GameObj inventory.
-
.shutdown_minutes ⇒ Integer?
Minutes remaining until the announced shutdown, counted down in real time (never negative).
-
.shutting_down? ⇒ Boolean
True once a maintenance shutdown has been announced.
-
.store_inv_item(id, noun, name, container, before, after) ⇒ void
Routes a parsed inventory item into GameObj according to the active scrape's completeness: a COMPLETE INV LIST replaces (items were cleared up front, so a plain add rebuilds the model), while a PARTIAL INV SEARCH / category scrape upserts by id so it refreshes only the matched items and never wipes unrelated inventory.
Class Method Details
.check_events(server_string) ⇒ String
Checks server output against registered Flag matchers. Updates Flags.flags hash when a pattern matches.
123 124 125 126 127 128 129 130 131 132 133 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 123 def self.check_events(server_string) Flags.matchers.each do |key, regexes| regexes.each do |regex| if (matches = server_string.match(regex)) Flags.flags[key] = matches break end end end server_string end |
.check_exp_mods(server_string) ⇒ String
Parses 'exp mods' output and updates DRSkill.exp_modifiers. Called for each line when @@parsing_exp_mods_output is true.
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 367 def self.check_exp_mods(server_string) # This method parses the output from `exp mods` command # and updates the DRSkill.exp_modifiers hash with the skill and value. # This is primarily used by the `skill-recorder` script. # # Example output without any modifiers: # The following skills are currently under the influence of a modifier: # <output class="mono"/> # None # <output class=""/> # # Example output with modifiers (XML format): # The following skills are currently under the influence of a modifier: # <output class="mono"/> # <preset id="speech">+75 Athletics</preset> # <preset id="thought">--10 Evasion</preset> # <output class=""/> # # Zero or more skills may be listed between the <output> tags # but exactly one skill and its skill modifier are listed per line. # Positive modifiers use <preset id="speech"> with single + sign. # Negative modifiers use <preset id="thought"> with double -- sign. # case server_string when Pattern::OutputClassEmpty if @@parsing_exp_mods_output @@parsing_exp_mods_output = false end else if @@parsing_exp_mods_output # Sample lines: # <preset id="speech">+79 Attunement</preset> (positive, single +) # <preset id="thought">--10 Evasion</preset> (negative, double --) if (match = server_string.strip.match(Pattern::ExpModLine)) skill = match[:skill].strip sign = match[:sign] value = match[:value].to_i value = -value if sign == '--' DRSkill.update_mods(skill, value) end end end server_string end |
.check_game_shutdown(line) ⇒ void
This method returns an undefined value.
Detects scheduled maintenance shutdown notices and records the target time, so any script can read DRParser.shutting_down? and DRParser.shutdown_minutes to wind down cleanly before the disconnect. Recomputing the target on every announcement keeps the estimate accurate as the warnings count down; a nil count (a final notice with no number) means shutdown is now.
143 144 145 146 147 148 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 143 def self.check_game_shutdown(line) return unless (match = line.match(Pattern::GameShutdown)) minutes = match[:minutes]&.to_i @@shutdown_at = minutes ? Time.now + (minutes * 60) : Time.now end |
.check_known_barbarian_abilities(server_string) ⇒ String
Parses 'ability' command output for Barbarians. Populates DRSpells.known_spells (abilities) and DRSpells.known_feats (masteries).
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 519 def self.(server_string) # This method parses the output from `ability` command for Barbarians # and populates the known spells/feats based on the known abilities/masteries. # # The XML stream for DragonRealms is whack at best. # # Examples of the data we're looking for: # You know the Berserks:<pushBold/> Avalanche, Drought. # <popBold/>You know the Forms:<pushBold/> Monkey. # <popBold/>You know the Roars:<pushBold/> Anger the Earth. # <popBold/>You know the Meditations:<pushBold/> Flame, Power, Contemplation. # <popBold/>You know the Masteries:<pushBold/> Juggernaut, Duelist. # <popBold/> # You recall that you have 0 training sessions remaining with the Guild. case server_string when Pattern::BarbarianAbility if DRSpells. server_string .sub(Pattern::BarbarianAbility, '') .sub('.', '') .split(',') .map(&:strip) .reject { |ability| ability.nil? || ability.empty? } .each { |ability| DRSpells.known_spells[ability] = true } end when Pattern::BarbarianMastery # Barbarian masteries are the equivalent of magical feats. if DRSpells. server_string .sub(Pattern::BarbarianMastery, '') .sub('.', '') .split(',') .map(&:strip) .reject { |mastery| mastery.nil? || mastery.empty? } .each { |mastery| DRSpells.known_feats[mastery] = true } end when Pattern::BarbarianTrainingRemaining DRSpells. = false end server_string end |
.check_known_spells(server_string) ⇒ String
Parses 'spells' command output for magic users. Populates DRSpells.known_spells and DRSpells.known_feats. Handles both column-formatted and non-column output formats.
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 417 def self.check_known_spells(server_string) # This method parses the output from `spells` command for magic users # and populates the known spells/feats based on the output. # # As of June 2022, There are two different output formats: column-formatted and non-column. # https://elanthipedia.play.net/Post:Tuesday_Tidings_-_120_-_Spells_-_06/07/2022_-_18:34 # # The XML stream for DragonRealms is whack at best. # # Examples of non-column output: # You recall the spells you have learned from your training. # In the chapter entitled "Analogous Patterns", you have notes on the Manifest Force [maf] and Gauge Flow [gaf] spells. # You have temporarily memorized the Tailwind [tw] spell. # You recall proficiency with the magic feats of Sorcerous Patterns, Alternate Preparation and Augmentation Mastery. # You are NOT currently set to recognize known spells when prepared by someone else in the area. (Use SPELL RECOGNIZE ON to change this.) # You are currently set to display full cast messaging. (Use SPELL BRIEFMSG ON to change this.) # You are currently attempting to hide your spell preparing. (Use PREPARE /HIDE to change this.) # You can use SPELL STANCE [HELP] to view or modify your spellcasting preferences. # # Examples of column-formatted output: # You recall the spells you have learned from your training. # <output class="mono"/> # <pushBold/> # Analogous Patterns: # <popBold/> maf Manifest Force Slot(s): 1 Min Prep: 1 Max Prep: 100 # gaf Gauge Flow Slot(s): 2 Min Prep: 5 Max Prep: 100 # <pushBold/> # Synthetic Creation: # <popBold/> acs Acid Splash Slot(s): 1 Min Prep: 1 Max Prep: 50 # vs Viscous Solution Slot(s): 2 Min Prep: 10 Max Prep: 66 # <output class=""/> # <output class="mono"/> # <pushBold/> # Temporarily Memorized: # <popBold/> tw Tailwind Slot(s): 1 Min Prep: 5 Max Prep: 100 # <output class=""/> # You recall proficiency with the magic feats of Sorcerous Patterns, Alternate Preparation and Augmentation Mastery. # You are NOT currently set to recognize known spells when prepared by someone else in the area. (Use SPELL RECOGNIZE ON to change this.) # You are currently set to display full cast messaging. (Use SPELL BRIEFMSG ON to change this.) # You are currently attempting to hide your spell preparing. (Use PREPARE /HIDE to change this.) # You can use SPELL STANCE [HELP] to view or modify your spellcasting preferences. # # One or more spells may be listed between a <popBold/> <pushBold/> pair, # but only one spell and its information are ever listed per line. case server_string when Pattern::OutputClassMono # Matched an xml tag while parsing spells, must be column-formatted output if DRSpells.grabbing_known_spells DRSpells.spellbook_format = 'column-formatted' end when Pattern::SpellbookName # Matched the spellbook name in column-formatted output, ignore when Pattern::SpellSlotInfo # Matched the spell info in column-formatted output, parse if DRSpells.grabbing_known_spells && DRSpells.spellbook_format == 'column-formatted' spell = server_string .sub(Pattern::PopBoldTag, '') # remove xml tag at start of some lines .slice(10, 32) # grab the spell name, after the alias and before Slots .strip DRSpells.known_spells[spell] = true unless spell.empty? end # Preserve the pop bold command we removed from start of spell line # otherwise lots of game text suddenly are highlighted yellow when Pattern::SpellChapterStart if DRSpells.grabbing_known_spells server_string .sub(Pattern::ChapterEntitled, '') .sub(Pattern::TemporarilyMemorized, '') .sub(Pattern::ApprenticeshipPractice, '') .sub(Pattern::SpellSuffix, '') .sub(Pattern::CommaAnd, ',') .split(',') .map { |mapped_spell| mapped_spell.include?('[') ? mapped_spell.slice(0, mapped_spell.index('[')) : mapped_spell } .map(&:strip) .reject { |rejected_spell| rejected_spell.nil? || rejected_spell.empty? } .each { |each_spell| DRSpells.known_spells[each_spell] = true } end when Pattern::SpellFeatsStart if DRSpells.grabbing_known_spells # The feats are listed without the Oxford comma separating the last item. # This makes splitting the string by comma difficult because the next to last and last # items would be captured together. The workaround is we'll replace ' and ' with a comma # and hope no feats ever have the word 'and' in them... server_string .sub(Pattern::SpellFeatsStart, '') .sub(Pattern::CommaAnd, ',') .sub('.', '') .split(',') .map(&:strip) .reject { |feat| feat.nil? || feat.empty? } .each { |feat| DRSpells.known_feats[feat] = true } end when Pattern::SpellsEnd DRSpells.grabbing_known_spells = false end server_string end |
.check_known_thief_khri(server_string) ⇒ String
Parses 'ability' command output for Thieves. Populates DRSpells.known_spells with known khri abilities.
565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 565 def self.check_known_thief_khri(server_string) # This method parses the output from `ability` command for Thieves # and populates the known spells/feats based on the known khri. # # The XML stream for DragonRealms is whack at best. # # Examples of the data we're looking for: # From the Subtlety tree, you know the following khri: Darken (Aug), Dampen (Util/Ward), Strike (Aug), Silence (Util), Shadowstep (Util), Harrier (Aug) # From the Finesse tree, you know the following khri: Hasten (Util), Safe (Aug), Avoidance (Aug), Plunder (Aug), Flight (Aug/Ward), Elusion (Aug), Slight (Util) # From the Potence tree, you know the following khri: Focus (Aug), Prowess (Debil), Sight (Aug), Calm (Util), Steady (Aug), Eliminate (Debil), Serenity (Ward), Sagacity (Ward), Terrify (Debil) # You have 7 available slots. case server_string when Pattern::ThiefKhriTree if DRSpells.grabbing_known_khri server_string .sub(Pattern::ThiefKhriTree, '') .sub('.', '') .gsub(Pattern::KhriType, '') .split(',') .map(&:strip) .reject { |ability| ability.nil? || ability.empty? } .each { |ability| DRSpells.known_spells[ability] = true } end when Pattern::ThiefAvailableSlots DRSpells.grabbing_known_khri = false end server_string end |
.parse(line) ⇒ void
This method returns an undefined value.
Main parser entry point. Processes a line of server output and updates various game state objects (DRStats, DRRoom, DRSkill, etc.).
598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 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 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 598 def self.parse(line) check_events(line) begin check_game_shutdown(line) if Pattern::InventoryListStart.match?(line) # COMPLETE inventory (INV LIST): stage a full replacement and publish # it only on the clean terminator (see populate_inventory_get). The # live model stays visible until then, so an interrupted listing keeps # the previous inventory instead of wiping it. GameObj.begin_inv GameObj.begin_all_containers @@parsing_inventory_get = true @@inventory_partial = false elsif Pattern::InventorySearchStart.match?(line) # PARTIAL inventory (INV SEARCH <word> / INV <category> full): only # matching items are listed, so upsert them and leave the rest of the # GameObj model intact -- no clear. @@parsing_inventory_get = true @@inventory_partial = true elsif (match = line.match(Pattern::GenderAgeCircle)) DRStats.gender = match[:gender] DRStats.age = match[:age].to_i DRStats.circle = match[:circle].to_i elsif (match = line.match(Pattern::NameRaceGuild)) DRStats.race = match[:race] DRStats.guild = match[:guild] elsif (match = line.match(Pattern::EncumbranceValue)) DRStats.encumbrance = match[:encumbrance] elsif (match = line.match(Pattern::LuckValue)) DRStats.luck = match[:luck].to_i elsif Pattern::StatValue.match?(line) line.scan(Pattern::StatValue) do |stat, value| DRStats.send("#{stat.downcase}=", value.to_i) end elsif (match = line.match(Pattern::TDPValue)) DRStats.tdps = match[:tdp].to_i elsif (match = line.match(Pattern::BalanceValue)) DRStats.balance = DR_BALANCE_VALUES.index(match[:balance]) if (position_match = line.match(Pattern::PositionValue)) DRStats.position = DR_POSITION_VALUES[position_match[:position]] end elsif Pattern::RoomPlayersEmpty.match?(line) DRRoom.pcs = [] elsif (match = line.match(Pattern::RoomPlayers)) players = match[:players]&.dup || '' DRRoom.pcs = find_pcs(players) DRRoom.pcs_prone = find_pcs_prone(players) DRRoom.pcs_sitting = find_pcs_sitting(players) elsif (match = line.match(Pattern::RoomObjs)) objs = match[:objs]&.dup || '' DRRoom.npcs = find_npcs(objs) UserVars.npcs = DRRoom.npcs DRRoom.dead_npcs = find_dead_npcs(objs) DRRoom.room_objs = find_objects(objs) elsif Pattern::RoomObjsEmpty.match?(line) DRRoom.npcs = [] DRRoom.dead_npcs = [] DRRoom.room_objs = [] elsif Pattern::GroupMembersEmpty.match?(line) DRRoom.group_members = [] elsif (match = line.match(Pattern::GroupMembers)) DRRoom.group_members << match[:member] elsif (match = line.match(Pattern::BriefExpOn)) || (match = line.match(Pattern::BriefExpOff)) skill = match[:skill] rank = match[:rank].to_i rate_raw = match[:rate] rate = rate_raw.to_i > 0 ? rate_raw : DR_LEARNING_RATES.index(rate_raw) percent = match[:percent] DRSkill.update(skill, rank, rate, percent) # Inline display of cumulative gained experience (from DRExpMonitor) if DRExpMonitor.inline_display? if rate_raw.to_i > 0 # BRIEFEXP ON format (rate is numeric) line.replace(DRExpMonitor.format_briefexp_on(line, skill)) else # BRIEFEXP OFF format (rate is word like "learning", "pondering") line.replace(DRExpMonitor.format_briefexp_off(line, skill, rate_raw.strip)) end end elsif (match = line.match(Pattern::ExpClearMindstate)) skill = match[:skill] DRSkill.clear_mind(skill) elsif Pattern::ExpColumns.match?(line) line.scan(Pattern::ExpColumns) do |skill_value, rank_value, percent_value, rate_as_word| rate_as_number = DR_LEARNING_RATES.index(rate_as_word) # convert word to number DRSkill.update(skill_value, rank_value, rate_as_number, percent_value) end elsif Pattern::ExpModsStart.match?(line) @@parsing_exp_mods_output = true DRSkill.exp_modifiers.clear elsif (match = line.match(Pattern::SpellBookFormat)) # Parse `toggle spellbook` command DRSpells.spellbook_format = match[:format] elsif Pattern::KnownSpellsStart.match?(line) DRSpells.grabbing_known_spells = true DRSpells.known_spells.clear DRSpells.known_feats.clear DRSpells.spellbook_format = 'non-column' # assume original format elsif Pattern::BarbarianAbilitiesStart.match?(line) DRSpells. = true DRSpells.known_spells.clear DRSpells.known_feats.clear elsif Pattern::ThiefKhriStart.match?(line) DRSpells.grabbing_known_khri = true DRSpells.known_spells.clear DRSpells.known_feats.clear elsif (match = line.match(Pattern::PlayedAccount)) if Lich::Common::Account.name.nil? Lich::Common::Account.name = match[:account].upcase end elsif (match = line.match(Pattern::PlayedSubscription)) if Lich::Common::Account.subscription.nil? Lich::Common::Account.subscription = match[:subscription].gsub('Basic', 'Normal').gsub('F2P', 'Free').gsub('Platinum', 'Premium').upcase end UserVars.account_type = match[:subscription].gsub('Basic', 'Normal').gsub('F2P', 'Free').upcase if Lich::Common::Account.subscription == 'PREMIUM' || XMLData.game == 'DRX' || XMLData.game == 'DRF' UserVars.premium = true else UserVars.premium = false end elsif (match = line.match(Pattern::LastLogoff)) month = Date::ABBR_MONTHNAMES.find_index(match[:month]) weekdays = [nil, 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] weekday_index = weekdays.find_index(match[:weekday]) # Guard: only process if weekday is recognized if weekday_index dst_check = match[:day].to_i - weekday_index if month.between?(4, 10) || (month == 3 && dst_check >= 7) || (month == 11 && dst_check < 0) tz = '-0400' else tz = '-0500' end $last_logoff = Time.new(match[:year].to_i, month, match[:day].to_i, match[:hour].to_i, match[:minute].to_i, match[:second].to_i, tz).getlocal end elsif (match = line.match(Pattern::Rested_EXP)) DRSkill.update_rested_exp(match[:stored].strip, match[:usable].strip, match[:refresh].strip) elsif Pattern::Rested_EXP_F2P.match?(line) # f2p characters without brain boost don't get rested exp DRSkill.update_rested_exp('none', 'none', 'none') elsif (match = line.match(Pattern::TDPValue_XPWindow)) DRStats.tdps = match[:tdp].to_i elsif (match = line.match(Pattern::FavorValue_XPWindow)) DRStats.favors = match[:favor].to_i end # Safety valve: a scrape that never saw its closing <output class=""/> # (interrupted or truncated stream) must not stay open and hijack later # <d cmd> links in ordinary output. Any <prompt> ends the exchange, so # reset there. In normal flow the flag is already false by the prompt. if @@parsing_inventory_get && line.start_with?('<prompt') # Reaching the prompt with the flag still set means the closing tag was # never seen -- the listing was interrupted/truncated. For a COMPLETE # INV LIST that means the staged full replacement is incomplete, so # discard it (keeping the previously published model) and tell the user # rather than silently leaving inventory half-updated. unless @@inventory_partial GameObj.discard_inv_refresh Lich::Messaging.msg('warn', "DRParser: 'inv list' did not finish; keeping previous inventory. Re-run 'inv list' to refresh.") end @@parsing_inventory_get = false @@inventory_partial = false end populate_inventory_get(line) if @@parsing_inventory_get check_exp_mods(line) if @@parsing_exp_mods_output (line) if DRSpells. check_known_thief_khri(line) if DRSpells.grabbing_known_khri check_known_spells(line) if DRSpells.grabbing_known_spells # Parse bank transactions passively Lich::DragonRealms::DRBanking.parse(line) rescue StandardError => e Lich::Messaging.msg("bold", "DRParser: error in parse: #{e.}") Lich::Messaging.msg("bold", "DRParser: line: #{line}") Lich.log "error: DRParser.parse: #{e.}\n\t#{e.backtrace.join("\n\t")}" Lich.log "error: line: #{line}\n\t" end end |
.populate_inventory_get(server_string) ⇒ String
Parses inventory search output and populates GameObj inventory. Called for each line when @@parsing_inventory_get is true.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 246 def self.populate_inventory_get(server_string) case server_string when Pattern::OutputClassEmpty if @@parsing_inventory_get # Clean terminator: a COMPLETE INV LIST finished, so publish the # staged full replacement atomically. PARTIAL scrapes upsert as they # go and stage nothing, so there is nothing to commit for them. unless @@inventory_partial GameObj.commit_inv GameObj.commit_all_containers_full end @@parsing_inventory_get = false @@inventory_partial = false end else # This block parses a single line from the output of the `inv search <string>` verb, # which lists items on your character. Each line is an XML-like string. # Example: <d cmd='get #12345'>a small pouch</d> # INV LIST nests a container's contents beneath it and prefixes each # nested line with a dash (e.g. " -<d cmd='get #1 in #2'>..."), while # INV SEARCH output is flat. Strip a leading dash so both forms parse. if @@parsing_inventory_get && (stripped = server_string.strip.sub(/\A-\s*/, '')).start_with?('<d cmd=') # Pull the cmd attribute and item name out of the line's leading <d> element # with a SAX handler. The element is captured as it streams in, before any # trailing prose (e.g. "... is in your right hand.") -- which is not valid XML # and would break a tree parse -- is reached. handler = InventoryItemSax.new # convert_special: false matches Game.process_xml_data: Ox never turns a # numeric entity into UTF-8. Ox leaves the standard entities literal, so # XmlEntities.decode restores them in the item name below. Ox.sax_parse(handler, stripped, convert_special: false, symbolize: false, skip: :skip_none) return server_string unless handler.cmd # Normalize the item name by removing a leading article ('a', 'an', # 'some'), case-insensitively: some inventory verbs lowercase the # article ("a dirty inkpot") while others capitalize it ("A soft gem # pouch"), and the same item must normalize identically either way. item_name = Lich::Common::XmlEntities.decode(handler.name.to_s).sub(/^(?:a|an|some)\s/i, '').strip # Extract the command and the unique item ID from the 'cmd' attribute. cmd = handler.cmd.to_s.downcase.strip # INV SEARCH links every item with a GET command. INV LIST links # worn (top-level) items with a REMOVE command and their contents # with GET, so accept either verb: worn items (no container) go back # into GameObj.inv, nested items into their container's contents. id_match = /(?:get|remove) (?<itemID>#\d+)(?: in (?<container1>#\d+|[^']+))?(?: in (?<container2>#\d+|[^']+))?/.match(cmd) # <!-- Regex to capture item and container IDs: cmd='(?:get|remove) (?<itemID>#\d+)(?: in (?<container1>#\d+|[^']+))?(?: in (?<container2>#\d+|[^']+))?' --> # <d cmd='get #8286821 in #8286816 in #8286762'>A papyrus parchment</d> is in a black winter cloak crafted from thick cashmere, which is in a scuffed traveler's pack. # <d cmd='get #8735861 in #8735860 in watery portal'>Some arzumodine cloth</d> is in a lumpy canvas sack, which is in an effervescent eddy of honey-hued light captured by a sungold frame. # <d cmd='get #8761784'>A seagull feather quill with dyed snowy white barbs</d> is lying at your feet. # <d cmd='get #8761784'>A seagull feather quill with dyed snowy white barbs</d> is in your right hand. if id_match id = id_match[:itemID].strip.delete('#').to_s noun = nil # This isn't exposed in the DR XML stream name = item_name # An item reported "... is in your right/left hand" is held, not worn # or in a container: its placement is the hand slot, tracked # separately by the <right>/<left> stream. Storing it here (cmd has no # "in #container", so container=nil -> worn inv) would recreate the # held/worn duplicate that hand reconciliation removes on pickup. Drop # any stale placement and skip the worn/container store. if stripped =~ /\bis in your (?:right|left) hand\b/i GameObj.remove_inv_item(id) return server_string end # Only container1 (the item's immediate parent) is used -- it is the # single placement each line establishes. The regex still captures # container2 (the grandparent in a doubly-nested line) because it is # needed to bound container1 correctly, but we do not read it: see # the note below store_inv_item. container1 = id_match[:container1].strip.delete('#') if id_match[:container1] container = container1 || nil before = cmd after = nil # Store the parsed item into its immediate container (or worn inv). # DRItems.update_item(item, id, cmd, full_description) Lich.log("DRParser: Adding inventory item - ID: #{id}, Noun: #{noun}, Name: #{name}, Container: #{container}, Before: #{before}, After: #{after}") store_inv_item(id, noun, name, container, before, after) # A doubly-nested line (get #item in #mid in <parent>) also reveals # that the middle container #mid sits inside <parent>, but we do NOT # synthesize that placement here. A COMPLETE INV LIST is a recursive # scrape, and #mid always carries its own id (the only idless item in # DR is the portal, which only ever appears as <parent>), so #mid is # always emitted on its own line -- "get #mid in <parent>" -- which # registers it into <parent> with its REAL name via store_inv_item # above. Re-deriving that edge from a child line could only add a # second copy of #mid with name=nil (the child line never carries the # middle container's name); because find_or_create keys on # "id|noun|name", the nil-name copy never collides with the real one, # leaving a phantom duplicate in GameObj.containers[<parent>]. A # PARTIAL (INV SEARCH / category) scrape never listed the middle # container either -- routing it through upsert_inv would blank its # name and yank a worn parent out of GameObj.inv -- so both modes # agree: only an item's own line establishes its placement. end end end server_string end |
.shutdown_minutes ⇒ Integer?
Minutes remaining until the announced shutdown, counted down in real time (never negative). nil when no shutdown is pending.
158 159 160 161 162 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 158 def self.shutdown_minutes return nil unless @@shutdown_at [(@@shutdown_at - Time.now) / 60.0, 0].max.ceil end |
.shutting_down? ⇒ Boolean
Returns true once a maintenance shutdown has been announced.
151 152 153 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 151 def self.shutting_down? !@@shutdown_at.nil? end |
.store_inv_item(id, noun, name, container, before, after) ⇒ void
This method returns an undefined value.
Routes a parsed inventory item into GameObj according to the active scrape's completeness: a COMPLETE INV LIST replaces (items were cleared up front, so a plain add rebuilds the model), while a PARTIAL INV SEARCH / category scrape upserts by id so it refreshes only the matched items and never wipes unrelated inventory.
355 356 357 358 359 360 361 |
# File 'documented/dragonrealms/drinfomon/drparser.rb', line 355 def self.store_inv_item(id, noun, name, container, before, after) if @@inventory_partial GameObj.upsert_inv(id, noun, name, container, before, after) else GameObj.new_inv(id, noun, name, container, before, after) end end |