Class: Lich::Common::XMLParser Private

Inherits:
Object
  • Object
show all
Defined in:
documented/common/xmlparser.rb

Overview

This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.

Parses the XML game server stream into game state and script-visible attributes.

This parser drives the Lich engine's understanding of the player's character, room, inventory, spell durations, creatures, and other real-time game state. The server sends a continuous stream of XML fragments describing each command's effects (room arrival, cast success, inventory change, etc.). XMLParser accumulates these fragments, hands them to the Ox SAX parser, and routes the resulting events to tag_start, tag_end, text, and attr callbacks that update game state and fire hooks for scripts to observe.

Most instance attributes are exposed as attr_reader; accessing XMLData.attribute from a script returns the parsed value. The parser also maintains internal caches for staging incomplete operations (e.g., container fills that span multiple text nodes) and coordinates with related systems like Creature, GameObj, and Claim.

Supports both GemStone IV and DragonRealms game streams, with game-specific handling for room-object identification, creature status flagging, and player roster tracking.

Constant Summary collapse

DECADE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Sentinel duration for indefinite spell effects.

Equal to 315,360,000 seconds (10 years). Used as a placeholder expiry time when a PSM 3.0 progress bar spell has "Indefinite" or "OM" (Omen/Manna) duration, representing an effect that does not expire during a session.

10 * 31_536_000
PSM_3_DIALOG_IDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Dialog category IDs that track active spell/buff effects in PSM 3.0.

When a progressBar tag appears with an id that is one of these categories, its effect name and expiry are cached in @dialogs. Other categories (e.g., "Quest Progress") are not parsed by parse_psm3_progressbar.

["Buffs", "Active Spells", "Debuffs", "Cooldowns"]
ASSESS_RANGES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

assess stream parsing

{ 'melee' => :melee, 'pole weapon' => :pole, 'missile' => :missile }.freeze
ASSESS_RELATION =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Regex pattern matching the relation and target in an assess (combat situation) line.

Captures the positional relation of one combatant to another (e.g., "flanking", "behind", "advancing on") and the target's name. Used by parse_assess_line to break down a creature description into structured fields.

Examples:

Match a flanking relation

ASSESS_RELATION.match("flanking a goblin") #=> MatchData with relation='flanking', target='a goblin'

See Also:

/^(?<relation>moving to flank|flanking|facing|behind|in front of|beside|advancing on|next to|to (?:the )?(?:left|right) of)\s+(?<target>.+)$/.freeze
@@warned_deprecated_spellfront =

This classvariable is part of a private API. You should avoid using this classvariable if possible, as it may be removed or be changed in the future.

0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ XMLParser

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Initializes the parser with empty game state and default values.

All mutable state is reset to sane defaults: strings to empty, arrays to [], integers to 0, nil to nil. Room ID defaults to 0 (not nil) so that checks like room_id.zero? and room_id > N in map layers do not raise NoMethodError before a



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'documented/common/xmlparser.rb', line 58

def initialize
  @buffer = String.new
  # @unescape = { 'lt' => '<', 'gt' => '>', 'quot' => '"', 'apos' => "'", 'amp' => '&' }
  @bold = false
  @active_tags = Array.new
  @active_ids = Array.new
  @last_tag = String.new
  @last_id = String.new
  @current_stream = String.new
  @current_style = String.new
  @stow_container_id = nil
  @obj_location = nil
  @obj_exist = nil
  @obj_noun = nil
  @obj_before_name = nil
  @obj_name = nil
  @obj_after_name = nil
  @pc = nil
  @pc_status = nil
  @last_obj = nil
  @last_npc = nil
  @in_stream = false
  @player_status = nil
  @fam_mode = String.new
  @room_window_disabled = false
  @wound_gsl = String.new
  @scar_gsl = String.new
  @send_fake_tags = false
  @prompt = String.new
  @nerve_tracker_num = 0
  @nerve_tracker_active = 'no'
  @server_time = Time.now.to_i
  @server_time_offset = 0.0
  @roundtime_end = 0
  @cast_roundtime_end = 0
  @last_pulse = Time.now.to_i
  @level = 0
  @next_level_value = 0
  @next_level_text = String.new
  @current_target_ids = Array.new
  @pending_crtr_status = Hash.new
  # DragonRealms stream-order name backfill: bold room-objs names and the
  # <crtrStatus> batch ids are both captured in order, then paired at the
  # following <prompt> - but only when their counts match exactly (an
  # all-or-nothing gate; see the prompt handler). DR-only; unused by
  # GemStone, which carries the name inline on the bold <a> tag.
  @dr_room_npc_names = []
  @dr_crtr_ids = []

  @room_count = 0
  @room_title = String.new
  @room_name = String.new
  @room_description = String.new
  @room_exits = Array.new
  @room_exits_string = String.new
  @room_climate = 0
  @room_terrain = 0
  @room_weather = 0
  @room_bonfire = 0
  @room_inside = 0
  @room_water = 0
  @room_sanctuary = 0
  @room_realm = 0

  @familiar_room_title = String.new
  @familiar_room_description = String.new
  @familiar_room_exits = Array.new

  @bounty_task = String.new
  @society_task = String.new

  @dr_active_spells = Hash.new
  @dr_active_spells_clear = false
  @dr_active_spells_tmp = Hash.new
  @dr_active_spell_tracking = false
  @dr_active_spells_stellar_percentage = 0
  @dr_active_spells_slivers = false
  @name = String.new
  @game = String.new
  @player_id = String.new
  @mana = 0
  @max_mana = 0
  @health = 0
  @max_health = 0
  @spirit = 0
  @max_spirit = 0
  @last_spirit = nil
  @stamina = 0
  @max_stamina = 0
  @concentration = 0
  @max_concentration = 0
  @stance_text = String.new
  @stance_value = 0
  @mind_text = String.new
  @mind_value = 0
  @field_exp = 0
  @max_field_exp = 0
  @ascension_exp = 0
  @exp = 0
  @until_next = 0
  @fashlonae = nil
  @lumnis = nil
  @rpa = nil
  @prepared_spell = 'None'
  @encumbrance_text = String.new
  @encumbrance_full_text = String.new
  @encumbrance_value = 0
  @indicator = Hash.new
  @injuries = { 'back' => { 'scar' => 0, 'wound' => 0 }, 'leftHand' => { 'scar' => 0, 'wound' => 0 }, 'rightHand' => { 'scar' => 0, 'wound' => 0 }, 'head' => { 'scar' => 0, 'wound' => 0 }, 'rightArm' => { 'scar' => 0, 'wound' => 0 }, 'abdomen' => { 'scar' => 0, 'wound' => 0 }, 'leftEye' => { 'scar' => 0, 'wound' => 0 }, 'leftArm' => { 'scar' => 0, 'wound' => 0 }, 'chest' => { 'scar' => 0, 'wound' => 0 }, 'leftFoot' => { 'scar' => 0, 'wound' => 0 }, 'rightFoot' => { 'scar' => 0, 'wound' => 0 }, 'rightLeg' => { 'scar' => 0, 'wound' => 0 }, 'neck' => { 'scar' => 0, 'wound' => 0 }, 'leftLeg' => { 'scar' => 0, 'wound' => 0 }, 'nsys' => { 'scar' => 0, 'wound' => 0 }, 'rightEye' => { 'scar' => 0, 'wound' => 0 } }
  @injury_mode = 0

  # psm 3.0 dialogdata updates
  @dialogs = {}

  # real id updates
  # Default 0 (not nil) so a read before the first <nav> tag is a valid "no UID" value
  # rather than a NoMethodError on room_id.zero? (DR) or room_id > N (GS) in the map layer.
  @room_id = 0
  @previous_nav_rm = nil
  # True once <nav> has supplied a real (non-zero) UID for the current
  # arrival and the streamWindow subtitle has not yet consumed it. Keeps a
  # ShowRoomID-on subtitle from overwriting an authoritative nav UID in the
  # same arrival (see the streamWindow handler).
  @nav_uid_pending = false
  # True when the game itself embedded the RealID marker in the room title
  # this arrival (i.e. the DragonRealms ShowRoomID flag is ON), e.g. the
  # subtitle " - [Room] (230008)" or the no-UID " - [Room] (**)". Distinct
  # from a UID merely known via <nav>: it records whether the *game* chose
  # to display it, so Lich's room-name/subtitle rewrite can preserve that
  # choice instead of second-guessing the flag (see the DR streamWindow
  # branch and Lich::DragonRealms::GameInstance#modify_room_display).
  @show_room_id = false

  # Lich::Claim update
  @arrival_pcs = []
  @check_obvious_hiding = false
  @room_player_hidden = false

  # assess (combat situation) stream tracking
  @assess = []
  @assess_buffer = nil
  @assess_ids = []

  # Ox SAX bridge state (see start_element/attr/attrs_done/error below):
  # the element/attributes being accumulated and parse errors for the
  # fragment currently being parsed.
  @sax_element = nil
  @sax_attributes = {}
  @sax_parse_errors = []
end

Instance Attribute Details

#arrival_pcs ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def arrival_pcs
  @arrival_pcs
end

#ascension_exp ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def ascension_exp
  @ascension_exp
end

#assess ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def assess
  @assess
end

#bounty_task ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def bounty_task
  @bounty_task
end

#cast_roundtime_end ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def cast_roundtime_end
  @cast_roundtime_end
end

#concentration ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def concentration
  @concentration
end

#current_target_id ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def current_target_id
  @current_target_id
end

#current_target_ids ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def current_target_ids
  @current_target_ids
end

#dialogs ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def dialogs
  @dialogs
end

#dr_active_spells ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def dr_active_spells
  @dr_active_spells
end

#dr_active_spells_slivers ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def dr_active_spells_slivers
  @dr_active_spells_slivers
end

#dr_active_spells_stellar_percentage ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def dr_active_spells_stellar_percentage
  @dr_active_spells_stellar_percentage
end

#encumbrance_full_text ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def encumbrance_full_text
  @encumbrance_full_text
end

#encumbrance_text ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def encumbrance_text
  @encumbrance_text
end

#encumbrance_value ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def encumbrance_value
  @encumbrance_value
end

#exp ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def exp
  @exp
end

#familiar_room_description ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def familiar_room_description
  @familiar_room_description
end

#familiar_room_exits ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def familiar_room_exits
  @familiar_room_exits
end

#familiar_room_title ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def familiar_room_title
  @familiar_room_title
end

#fashlonae ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def fashlonae
  @fashlonae
end

#field_exp ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def field_exp
  @field_exp
end

#game ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def game
  @game
end

#health ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def health
  @health
end

#in_stream ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def in_stream
  @in_stream
end

#indicator ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def indicator
  @indicator
end

#injuries ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def injuries
  @injuries
end

#injury_mode ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def injury_mode
  @injury_mode
end

#last_pulse ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def last_pulse
  @last_pulse
end

#last_spirit ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def last_spirit
  @last_spirit
end

#level ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def level
  @level
end

#lumnis ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def lumnis
  @lumnis
end

#mana ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def mana
  @mana
end

#max_concentration ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def max_concentration
  @max_concentration
end

#max_field_exp ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def max_field_exp
  @max_field_exp
end

#max_health ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def max_health
  @max_health
end

#max_mana ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def max_mana
  @max_mana
end

#max_spirit ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def max_spirit
  @max_spirit
end

#max_stamina ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def max_stamina
  @max_stamina
end

#mind_text ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def mind_text
  @mind_text
end

#mind_value ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def mind_value
  @mind_value
end

#name ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def name
  @name
end

#next_level_text ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def next_level_text
  @next_level_text
end

#next_level_value ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def next_level_value
  @next_level_value
end

#player_id ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def player_id
  @player_id
end

#prepared_spell ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def prepared_spell
  @prepared_spell
end

#previous_nav_rm ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def previous_nav_rm
  @previous_nav_rm
end

#prompt ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def prompt
  @prompt
end

#room_bonfire ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_bonfire
  @room_bonfire
end

#room_climate ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_climate
  @room_climate
end

#room_count ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_count
  @room_count
end

#room_description ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_description
  @room_description
end

#room_exits ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_exits
  @room_exits
end

#room_exits_string ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_exits_string
  @room_exits_string
end

#room_id ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_id
  @room_id
end

#room_inside ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_inside
  @room_inside
end

#room_name ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_name
  @room_name
end

#room_player_hidden ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_player_hidden
  @room_player_hidden
end

#room_realm ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_realm
  @room_realm
end

#room_sanctuary ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_sanctuary
  @room_sanctuary
end

#room_terrain ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_terrain
  @room_terrain
end

#room_title ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_title
  @room_title
end

#room_water ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_water
  @room_water
end

#room_weather ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_weather
  @room_weather
end

#room_window_disabled ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def room_window_disabled
  @room_window_disabled
end

#roundtime_end ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def roundtime_end
  @roundtime_end
end

#rpa ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def rpa
  @rpa
end

#sax_parse_errors ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Ox reports parse problems here instead of raising, then keeps parsing, auto-balancing whatever was malformed. Collect the messages so Game.process_xml_data can tell Simu's routine almost-XML from a genuinely truncated (desynced) fragment once the parse completes. The caller clears this between fragments.



520
521
522
# File 'documented/common/xmlparser.rb', line 520

def sax_parse_errors
  @sax_parse_errors
end

#send_fake_tags ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



45
46
47
# File 'documented/common/xmlparser.rb', line 45

def send_fake_tags
  @send_fake_tags
end

#server_time ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def server_time
  @server_time
end

#server_time_offset ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def server_time_offset
  @server_time_offset
end

#show_room_id ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def show_room_id
  @show_room_id
end

#society_task ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def society_task
  @society_task
end

#spirit ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def spirit
  @spirit
end

#stamina ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def stamina
  @stamina
end

#stance_text ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def stance_text
  @stance_text
end

#stance_value ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def stance_value
  @stance_value
end

#stow_container_id ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def stow_container_id
  @stow_container_id
end

#until_next ⇒ Object (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
# File 'documented/common/xmlparser.rb', line 30

def until_next
  @until_next
end

Instance Method Details

#active_spells ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

for backwards compatibility



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'documented/common/xmlparser.rb', line 210

def active_spells
  z = {}
  XMLData.dialogs.sort.each do |a, b|
    b.each do |k, v|
      case a
      when /Active Spells|Buffs/
        z.merge!(k => v) if k.instance_of?(String)
      when /Cooldowns/
        if k.to_s =~ /Recovery/
          z.merge!(k => v) if k.instance_of?(String)
        else
          z.merge!("#{k} Cooldown" => v) if k.instance_of?(String)
        end
      when /Debuffs/

        # need to deal with that pesky 'Silenced' versus 'Silence' from XML
        if k == "Silenced"
          k = 'Silence'
        end
        z.merge!(k => v) if k.instance_of?(String)
      end
    end
  end
  z
end

#assess_creatures ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

convenience: just the creatures (positive ids; excludes self and PCs)



460
461
462
# File 'documented/common/xmlparser.rb', line 460

def assess_creatures
  @assess.reject { |e| e[:self] || e[:pc] }
end

#attr(name, value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Values are left in Ox's native encoding rather than retagged: REXML handed these callbacks UTF-8 (effectively ASCII for the scrubbed game stream), so force-tagging Windows-1252 was both a divergence from the pre-Ox behavior and the source of the entity corruption. Ox runs with convert_special: false, so XmlEntities.decode restores the standard entities here.



480
481
482
# File 'documented/common/xmlparser.rb', line 480

def attr(name, value)
  @sax_attributes[name] = XmlEntities.decode(value)
end

#attrs_done ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Fires after all attributes for an XML element have been accumulated.

Delegates to tag_start with the accumulated element name and attributes hash, matching the old REXML-style single tag_start(name, hash) call. Part of the Ox SAX parser callback sequence: start_element -> attr (repeated) -> attrs_done -> text/children -> end_element.



494
495
496
# File 'documented/common/xmlparser.rb', line 494

def attrs_done
  tag_start(@sax_element, @sax_attributes)
end

#cdata(value) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

REXML routed CDATA through text handling; do the same.



511
512
513
# File 'documented/common/xmlparser.rb', line 511

def cdata(value)
  text(value)
end

#end_element(name) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Fires when an XML closing tag is encountered.

Delegates to tag_end(name), mirroring the Ox SAX parser interface.

Parameters:

  • name (String) —

    the element name (tag) being closed



506
507
508
# File 'documented/common/xmlparser.rb', line 506

def end_element(name)
  tag_end(name)
end

#error(message, line, column) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Records a non-fatal parse error from Ox SAX.

Ox does not raise on malformed XML; instead, it calls this method, auto-balances the structure, and continues parsing. The parser accumulates these messages so Game.process_xml_data can distinguish between SIMU's intentional almost-XML and a genuinely truncated or desynced fragment once parsing completes. Messages are cleared between fragments by the reset method.

Parameters:

  • message (String) —

    the error description

  • line (Integer) —

    1-indexed line number in the fragment

  • column (Integer) —

    0-indexed column number



536
537
538
# File 'documented/common/xmlparser.rb', line 536

def error(message, line, column)
  @sax_parse_errors << "#{message} (line #{line}, column #{column})"
end

#make_scar_gsl ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Encodes scar injury levels into a binary GSL tag.

Constructs a 16-body-part bitfield for the GSL \034GSV vital stats packet, encoding the scar rank (0-3) of each tracked injury location in the same order as make_wound_gsl. Called when scars update or when fake GSL tags are sent to the Wizard frontend.



318
319
320
# File 'documented/common/xmlparser.rb', line 318

def make_scar_gsl
  @scar_gsl = sprintf("0b0%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b", @injuries['nsys']['scar'], @injuries['leftEye']['scar'], @injuries['rightEye']['scar'], @injuries['back']['scar'], @injuries['abdomen']['scar'], @injuries['chest']['scar'], @injuries['leftHand']['scar'], @injuries['rightHand']['scar'], @injuries['leftLeg']['scar'], @injuries['rightLeg']['scar'], @injuries['leftArm']['scar'], @injuries['rightArm']['scar'], @injuries['neck']['scar'], @injuries['head']['scar'])
end

#make_wound_gsl ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Encodes wound injury levels into a binary GSL (Gemstone Scripting Language) tag.

Constructs a 16-body-part bitfield for the GSL \034GSV vital stats packet, encoding the wound rank (0-3) of each tracked injury location. The bit order is fixed: nsys, leftEye, rightEye, back, abdomen, chest, leftHand, rightHand, leftLeg, rightLeg, leftArm, rightArm, neck, head. Called when injuries update or when fake GSL tags are sent to the Wizard frontend.



304
305
306
# File 'documented/common/xmlparser.rb', line 304

def make_wound_gsl
  @wound_gsl = sprintf("0b0%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b%02b", @injuries['nsys']['wound'], @injuries['leftEye']['wound'], @injuries['rightEye']['wound'], @injuries['back']['wound'], @injuries['abdomen']['wound'], @injuries['chest']['wound'], @injuries['leftHand']['wound'], @injuries['rightHand']['wound'], @injuries['leftLeg']['wound'], @injuries['rightLeg']['wound'], @injuries['leftArm']['wound'], @injuries['rightArm']['wound'], @injuries['neck']['wound'], @injuries['head']['wound'])
end

#parse_assess_line(text, ids) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parse a single reconstructed line from the 'assess' (combat situation) stream into a structured entry. ids is the ordered list of look-target ids pulled from the line's tags (subject first, then target). Returns a Hash, or nil for the header / unparseable lines.



408
409
410
411
412
413
414
415
416
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
# File 'documented/common/xmlparser.rb', line 408

def parse_assess_line(text, ids)
  text = text.to_s.strip
  return nil if text.empty?
  return nil if text =~ /assess your combat situation/i

  # drop the trailing "  | F" face-hint (the F lives in a <d cmd='face #id'> tag);
  # anchored to the pipe + trailing token so a stray '|' mid-line can't eat text
  text = text.sub(/\s+\|\s+\S+\s*$/, '').strip

  m = text.match(/^(?<name>.+?)\s+\((?:(?<number>\d+):\s*)?(?<status>[^)]*)\)\s+(?:is|are)\s+(?<rest>.+?)\s+at\s+(?<range>melee|pole weapon|missile)\s+range\b/i)
  return nil unless m

  name   = m[:name].strip
  number = m[:number] && m[:number].to_i
  status = m[:status].strip
  range  = ASSESS_RANGES[m[:range].downcase]
  rest   = m[:rest].strip

  # the target may carry its own assess number, e.g. "facing a jeol moradu (2)"
  target_number = nil
  if rest =~ /\((\d+)\)\s*$/
    target_number = $1.to_i
    rest = rest.sub(/\s*\(\d+\)\s*$/, '').strip
  end

  if (rm = rest.match(ASSESS_RELATION))
    relation = rm[:relation]
    target   = rm[:target].strip
  else
    relation = rest
    target   = nil
  end
  relation = 'flanking' if relation == 'moving to flank'

  is_self = name.casecmp?('you')
  if is_self
    subject_id = nil
    target_id  = ids[0]
  else
    subject_id = ids[0]
    target_id  = (target && target =~ /^you$/i) ? nil : ids[1]
  end
  is_pc = subject_id.to_s.start_with?('-')

  {
    name: name, id: subject_id, number: number, status: status,
    relation: relation, target: target, target_id: target_id,
    target_number: target_number, range: range, self: is_self, pc: is_pc
  }
end

#parse_psm3_progressbar(kind, attributes) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Parses a PSM 3.0 progress bar (spell/buff/debuff/cooldown effect) into the dialogs cache.

Stores the effect's expiry time (Time.now + duration) in @dialogs keyed by both the effect name string and its numeric id. If the duration is "Indefinite" or "OM", sets expiry to DECADE (10 years). Otherwise, parses HH:MM:SS format and computes the expiry as current time plus that many seconds. The result is immediately visible to scripts via XMLData.dialogs.

Parameters:

  • kind (String) —

    the dialog/effect category, e.g. "Buffs", "Cooldowns"

  • attributes (Hash{String => String}) —

    XML attributes of the progressBar tag:

    • "id": numeric effect id (parsed to_i)
    • "text": effect name (e.g., "Heroism")
    • "time": duration as "HH:MM:SS" or "Indefinite" or "OM"


367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'documented/common/xmlparser.rb', line 367

def parse_psm3_progressbar(kind, attributes)
  @dialogs[kind] ||= {}
  id = attributes["id"].to_i
  name = attributes["text"]
  value = attributes["time"]
  return unless name && value
  # set the expiry for a decade for infinite duration effects
  return @dialogs[kind][name] = @dialogs[kind][id] = Time.now + DECADE if value.downcase.eql?("indefinite")

  # in psm 3.0 progress bars now have second precision!
  hour, minute, second = value.split(':')
  @dialogs[kind][name] = @dialogs[kind][id] = Time.now + (hour.to_i * 3600) + (minute.to_i * 60) + second.to_i
end

#reset ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Clears mid-parse state after a fragment error or sync loss.

Drops any in-flight XML tag stack, active style context, staged GameObj refreshes (incomplete container fills, room component updates), and cached creature status/name pairings that were not yet committed. This prevents a truncated or malformed fragment from leaving stale data that would pollute the next arrival's state.

Called automatically by tag_start and text when they catch an exception, and between server fragments by Game.process_xml_data.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'documented/common/xmlparser.rb', line 248

def reset
  @active_tags = Array.new
  @active_ids = Array.new
  @current_stream = String.new
  @current_style = String.new
  @sax_parse_errors = []
  # Any staged GameObj refresh still open here is incomplete. Left in
  # place, an interrupted container fill would be published as
  # authoritative by the next <prompt>, so drop the in-flight buffers and
  # keep the previously published snapshot visible instead.
  GameObj.discard_staged_refreshes
  # A <crtrStatus> tag can be fully parsed and cached here while the
  # matching bold <a> text is still in an as-yet-unparsed remainder of
  # the fragment. If a malformed/truncated fragment forces a reset in
  # that window, an uncleared entry would sit here indefinitely and
  # could misapply to an unrelated creature that later reuses the same
  # exist id (ids are recycled - see Creature.targets' notes).
  @pending_crtr_status.clear
  # A reset mid-fragment invalidates the room-objs<->crtrStatus pairing, so
  # drop any captured names and collected ids.
  @dr_room_npc_names = []
  @dr_crtr_ids = []
end

#safe_to_respond? ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Checks whether it is safe for a script to send a command right now.

In DragonRealms, returns false if the parser is mid-stream (in_stream true), mid-bold block (@bold true), or in an active inline style; returns true if safe. GemStone IV always returns true (no flow control needed).

This gate prevents scripts from sending commands that would land in the middle of a room component update or other structured multi-line server response, which could corrupt the parser's state or break the command-response pairing.

Returns:

  • (Boolean) —

    true if a command may be sent safely; false otherwise



285
286
287
288
289
290
291
# File 'documented/common/xmlparser.rb', line 285

def safe_to_respond?
  if @game =~ /^DR/
    !in_stream && !@bold && (!@current_style || @current_style.empty?)
  else
    return true
  end
end

#spellfront ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

here for backwards compatibility, but spellfront xml isn't sent by the game anymore



1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
# File 'documented/common/xmlparser.rb', line 1528

def spellfront
  if (Time.now.to_i - @@warned_deprecated_spellfront) > 300
    @@warned_deprecated_spellfront = Time.now.to_i
    unless (script_name = Script.current.name)
      script_name = 'unknown script'
    end
    respond "--- warning: #{script_name} is using deprecated method XMLData.spellfront; this method will be removed in a future version of Lich"
  end
  @active_spells.keys
end

#start_element(name) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Ox::Sax interface: Ox parses the server stream directly into XMLData (see Game.process_xml_data). Ox fires start_element, then attr per attribute, then attrs_done, then text/children, then end_element. Attributes are accumulated and flushed to tag_start (matching the old REXML-style single tag_start(name, hash) call). The game stream is Windows-1252, so byte content is tagged with that encoding; names are ASCII.



470
471
472
473
# File 'documented/common/xmlparser.rb', line 470

def start_element(name)
  @sax_element = name
  @sax_attributes = {}
end

#tag_end(name) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Routes a closing XML tag to final object creation and stream commitment.

Called once per element close by the Ox SAX parser (via end_element). Pops the tag name from the stack and handles closing logic: committing staged room components (room objs, players, description) when their container closes, finalizing inventory items with stored location and surrounding text, and incrementing room counters on compass close.

Prevents spurious handler invocation on synthetic closes (Ox-synthesized ends for desynced orphan tags) by checking that the closing tag name matches the top of the active stack; mismatches are silently ignored.

If an exception is raised, logs the error, sleeps briefly, and calls reset() to discard in-flight state.

Parameters:

  • name (String) —

    the element tag name being closed (lowercase)



1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
# File 'documented/common/xmlparser.rb', line 1450

def tag_end(name)
  # Called once per element close. Ox synthesizes an end for a stray closing
  # tag -- a close with no matching open (e.g. a desynced </prompt> whose
  # <prompt> was in a prior, truncated read). tag_start never pushed it (Ox
  # fires no attrs_done for a synthetic start), so popping here would remove
  # the wrong element and the inv/compass end-handlers would fire spuriously.
  # Ignore any close that does not match the currently-open tag.
  return unless @active_tags.last == name

  begin
    if @game =~ /^DR/
      if name == 'compass' and $nav_seen
        $nav_seen = false
        @second_compass = true
      end
      if name == 'compass' and @second_compass
        @second_compass = false
        @room_count += 1
        $room_count += 1
      end
    end

    if name == 'inv'
      if @obj_exist == @obj_location
        if @obj_after_name == 'is closed.'
          GameObj.delete_container(@stow_container_id)
        end
      elsif @obj_exist
        GameObj.new_inv(@obj_exist, @obj_noun, @obj_name, @obj_location, @obj_before_name, @obj_after_name)
      end
    elsif @send_fake_tags and (@active_ids.last == 'room exits')
      gsl_exits = String.new
      @room_exits.each { |exit| gsl_exits.concat(DIRMAP[SHORTDIR[exit]].to_s) }
      $_CLIENT_.puts "\034GSj#{sprintf('%-20s', gsl_exits)}\r\n"
      gsl_exits = nil
    elsif @room_window_disabled and (name == 'compass')
      if defined?(Lich::Claim) && Lich::Claim::Lock.owned?
        if @room_id == 0
          @room_id = Digest::MD5.hexdigest([@room_title, @room_description, @room_exits_string].to_s).to_i(16)
        end
        if @room_player_hidden
          @arrival_pcs.push(:hidden)
          @room_player_hidden = false
        end
        @check_obvious_hiding = false
        Lich::Claim.parser_handle(@room_id, @arrival_pcs)
        Lich::Claim.unlock
      end
      @room_description = @room_description.strip
      @room_exits_string.concat " #{@room_exits.join(', ')}" unless @room_exits.empty?
      if @send_fake_tags
        gsl_exits = String.new
        @room_exits.each { |exit| gsl_exits.concat(DIRMAP[SHORTDIR[exit]].to_s) }
        $_CLIENT_.puts "\034GSj#{sprintf('%-20s', gsl_exits)}\r\n"
        gsl_exits = nil
      end
      @room_count += 1
      $room_count += 1
    end
    # Commit a staged room component when it closes. @active_ids.last is
    # the component's own id at this point (inner <a> ids already popped).
    if (name == 'component') or (name == 'compDef')
      case @active_ids.last
      when 'room objs'    then GameObj.commit_room_objs
      when 'room players' then GameObj.commit_room_players
      when 'room desc'    then GameObj.commit_room_desc
      end
    end
    @last_tag = @active_tags.pop
    @last_id = @active_ids.pop
  rescue
    Lich.log "error: XMLParser.tag_end: #{$!}\n\t#{$!.backtrace.join("\n\t")}"
    sleep 0.1
    reset
  end
end

#tag_start(name, attributes) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Routes an opening XML tag to game-state updates and side effects.

Called once per element by the Ox SAX parser (via attrs_done). Pushes the tag name and id attribute onto stacks, then handles 100+ tag types to extract and cache game state: room arrival (nav), room components (compDef/component), spell durations (progressBar), creature status (crtrStatus), inventory (inv), player status, injuries, server time, roundtime, and numerous UI elements.

Complex operations span multiple callbacks: container fills begin here and close at the prompt; creature name/status pairings are staged here and committed at tag_end; and room-object names (DragonRealms only) are captured during bold text and paired to creature ids at the next prompt.

If an exception is raised, logs the error, sleeps briefly, and calls reset() to discard in-flight state.

Parameters:

  • name (String) —

    the element tag name (lowercase)

  • attributes (Hash{String => String}) —

    the element's attributes



561
562
563
564
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
593
594
595
596
597
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
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
# File 'documented/common/xmlparser.rb', line 561

def tag_start(name, attributes)
  # This is called once per element by REXML in games.rb
  # https://ruby-doc.org/stdlib-2.6.1/libdoc/rexml/rdoc/REXML/StreamListener.html
  begin
    @active_tags.push(name)
    @active_ids.push(attributes['id'].to_s)

    if name == 'nav'
      Lich::Claim.lock if defined?(Lich::Claim)
      Lich::Gemstone::Overwatch.room_with_hiders_reset if defined?(Lich::Gemstone::Overwatch)
      GameObj.clear_loot
      GameObj.clear_npcs
      GameObj.clear_pcs
      GameObj.clear_room_desc
      # Creature tracks its own room roster independently of GameObj
      # (see lib/gemstone/creature.rb / lib/dragonrealms/creature.rb).
      # Only one game's Creature is ever loaded per session.
      Lich::Gemstone::Creature.clear_room if defined?(Lich::Gemstone::Creature)
      Lich::DragonRealms::Creature.clear_room if defined?(Lich::DragonRealms::Creature)
      # Any <crtrStatus> cached for the room being left is scoped to
      # that room - don't let it survive to misapply if the id gets
      # reused elsewhere.
      @pending_crtr_status.clear
      @check_obvious_hiding = true
      # The <nav rm='NNNN'/> tag is the authoritative room UID for every game, including
      # DragonRealms, which now emits it on every arrival (a plain <nav/> with no rm
      # attribute for a room that has no UID, which yields 0 here). Capturing it on nav
      # gives early room knowledge before the room text streams, and keeps
      # @previous_nav_rm accurate for Map.previous_uid.
      @previous_nav_rm = @room_id
      @room_id = attributes['rm'].to_i
      # A real nav UID this arrival is authoritative; flag it so the
      # streamWindow subtitle below treats its own UID marker as a fallback
      # and does not overwrite this value. A bare <nav/> (no UID, room_id 0)
      # leaves the subtitle free to supply one.
      @nav_uid_pending = @room_id.positive?
      @arrival_pcs = []
      $nav_seen = true
    end

    if name == 'compass'
      if defined?(Lich::Claim) && Lich::Claim::Lock.owned?
        if @room_id == 0
          @room_id = Digest::MD5.hexdigest([@room_title, @room_description, @room_exits_string].to_s).to_i(16)
        end
        if @room_player_hidden
          @arrival_pcs.push(:hidden)
          @room_player_hidden = false
        end
        @check_obvious_hiding = false
        Lich::Claim.parser_handle(@room_id, @arrival_pcs)
        Lich::Claim.unlock
      end
      if @current_stream == 'familiar'
        @fam_mode = String.new
      elsif @room_window_disabled
        @room_exits = Array.new
      end
    end

    if (name == 'compDef') or (name == 'component')
      if attributes['id'] == 'room objs'
        GameObj.begin_room_objs
        Lich::Gemstone::Creature.clear_room if defined?(Lich::Gemstone::Creature)
        # DR rebuilds the roster from the <crtrStatus> batch that follows
        # this component; clearing here gives that batch a clean snapshot.
        Lich::DragonRealms::Creature.clear_room if defined?(Lich::DragonRealms::Creature)
        @pending_crtr_status.clear
        # Start a fresh room-objs<->crtrStatus pairing for this refresh: the
        # bold names captured below and the crtrStatus ids that follow are
        # zipped at the next <prompt>, gated on equal counts.
        @dr_room_npc_names = []
        @dr_crtr_ids = []
      elsif attributes['id'] == 'room players'
        GameObj.begin_room_players
      elsif attributes['id'] == 'room exits'
        @room_exits = Array.new
        @room_exits_string = String.new
      elsif attributes['id'] == 'room desc'
        @room_description = String.new
        GameObj.begin_room_desc
      end
    end

    if name =~ /^(?:a|right|left)$/
      @obj_exist = attributes['exist']
      @obj_noun = attributes['noun']
    end
    if name == 'crtrStatus'
      # Self-closing and self-contained (carries its own id), so it needs
      # no surrounding-tag context, unlike the bolded <a> name path below.
      # Always stash rather than applying directly even when the
      # creature is already known: Creature.register (and the room-in
      # marking it does) only ever runs from the <a> text path below, so
      # syncing here and skipping that path would update the instance's
      # flags correctly while silently leaving it out of the room
      # roster after the next clear_room - it would sync but never
      # reappear in Creature.targets/.in_room. Deferring to the text()
      # handler keeps registration/room-marking and flag application on
      # the same path for both new and already-known creatures.
      #
      # DragonRealms differs: its room-objs carry no per-creature <a exist>
      # tag, so there is no text() path to defer to. Its <crtrStatus> tags
      # arrive batched after the room-objs component and carry their own id,
      # so flags are applied immediately, id-first. The name is backfilled
      # two ways: the stream-order pairing (bold room-objs names zipped to
      # this batch's ids at the next <prompt>, gated on equal counts) and,
      # authoritatively, later from the assess stream (see
      # lib/dragonrealms/creature.rb).
      crtr_id = attributes['exist']
      if crtr_id
        crtr_flags = attributes.reject { |k, _| k == 'exist' }
        if XMLData.game =~ /^DR/
          # Apply flags now (id-first, name-less); collect the id in arrival
          # order so the prompt handler can pair it to a bold name only if
          # the counts match for this refresh.
          Lich::DragonRealms::Creature.sync(crtr_id, crtr_flags) if defined?(Lich::DragonRealms::Creature)
          @dr_crtr_ids << crtr_id
        else
          @pending_crtr_status[crtr_id] = crtr_flags
        end
      end
    end
    if name == 'inv'
      if attributes['id'] == 'stow'
        @obj_location = @stow_container_id
      else
        @obj_location = attributes['id']
      end
      @obj_exist = nil
      @obj_noun = nil
      @obj_name = nil
      @obj_before_name = nil
      @obj_after_name = nil
    end
    if name == 'dialogData' and attributes['clear'] == 't' and PSM_3_DIALOG_IDS.include?(attributes["id"])
      @dialogs[attributes["id"]] ||= {}
      @dialogs[attributes["id"]].clear
      # detect a clear board request for effects, and send to activespell
      ActiveSpell.request_update
    end
    if name == 'resource'
      nil
    end
    if name == 'roommeta'
      @room_weather = attributes['weather'].to_i if attributes['weather']
      @room_bonfire = attributes['bonfire'].to_i if attributes['bonfire']
      @room_inside = attributes['inside'].to_i if attributes['inside']
      @room_water = attributes['water'].to_i if attributes['water']
      @room_sanctuary = attributes['sanctuary'].to_i if attributes['sanctuary']
      @room_realm = attributes['realm'].to_i if attributes['realm']
      @room_climate = attributes['climate'].to_i if attributes['climate']
      @room_terrain = attributes['terrain'].to_i if attributes['terrain']
    end
    if name == 'pushStream'
      @in_stream = true
      @current_stream = attributes['id'].to_s
      if attributes['id'].to_s == 'assess'
        @assess_buffer = String.new
        @assess_ids = []
      end
      if XMLData.game =~ /^GS/
        GameObj.begin_inv if attributes['id'].to_s == 'inv'
        GameObj.begin_reserve if attributes['id'].to_s == 'reserve'
      end
    end

    if name == 'd' && @current_stream == 'assess'
      @assess_ids << $1 if attributes['cmd'].to_s =~ /look #(-?\d+)/
    end
    if name == 'popStream'
      if @current_stream == 'assess' && @assess_buffer
        entry = parse_assess_line(@assess_buffer, @assess_ids)
        if entry
          @assess << entry
          # The assess stream is DragonRealms' only tie between an exist id
          # and a creature name/position, so it backfills the id-first
          # instances created from <crtrStatus>. Self and PCs are skipped.
          if XMLData.game =~ /^DR/ && !entry[:self] && !entry[:pc] && defined?(Lich::DragonRealms::Creature)
            Lich::DragonRealms::Creature.feed_assess(entry)
          end
        end
        @assess_buffer = nil
      end
      if attributes['id'] == 'room'
        unless @room_window_disabled
          @room_count += 1
          $room_count += 1
        end
      end
      @in_stream = false
      # @current_stream still names the closing stream here; commit_* is a
      # no-op when the matching refresh was never opened (e.g. in DR).
      GameObj.commit_inv      if @current_stream == 'inv'
      GameObj.commit_reserve  if @current_stream == 'reserve'
      GameObj.commit_familiar if @current_stream == 'familiar'
      if attributes['id'] == 'bounty'
        @bounty_task.strip!
      end
      @current_stream = String.new
    end
    if name == 'pushBold'
      @bold = true
    end
    if name == 'popBold'
      @bold = false
    end
    if (name == 'streamWindow')
      if (attributes['id'] == 'main') and attributes['subtitle']
        unless attributes['subtitle'].empty? || attributes['subtitle'].nil?
          if XMLData.game =~ /^GS/
            if Lich.display_uid == false && attributes['subtitle'][3..-1] =~ / - \d+$/
              Lich.display_uid = true
            end
            @room_title = '[' + attributes['subtitle'][3..-1].gsub(/ - \d+$/, '') + ']'
          elsif XMLData.game =~ /^DR/
            # - [Bosque Deriel, Hermit's Shacks] (a trailing UID marker is present only when
            # the game's ShowRoomID flag is ON): " (230008)" for a room that has a UID, or
            # " (**)" for a room that has none. The <nav rm=.../> tag is the PRIMARY UID
            # source (see the nav handler above); this subtitle marker is only a FALLBACK
            # for arrivals where nav arrived late, was absent, or was a bare no-uid <nav/>.
            # When nav already supplied a real UID this arrival (@nav_uid_pending), leave
            # room_id alone so the subtitle never clobbers the authoritative nav value; the
            # subtitle consumes that flag either way, so the next arrival's marker (if its
            # nav is missing) is free to act as the fallback. When nav did not supply one, a
            # numeric marker sets the UID and "(**)" clears it to 0 (explicit "no UID",
            # dropping any stale id from a prior room). A ShowRoomID-OFF subtitle has no
            # marker at all, so room_id is left untouched (never write 0 blindly). The title
            # stays UID-free in every case. Guard the match: a blank/identity-less subtitle
            # (e.g. " - ") has no "[...]" and returns nil, so leave the prior title untouched
            # rather than crash.
            room = attributes['subtitle'].match(%r{(?<roomtitle>\[.*?\])(?:\s\((?<uid>\d+|\*+)\))?})
            if room
              @room_title = "[#{room[:roomtitle]}]"
              @room_id = (room[:uid] =~ /\A\d+\z/ ? room[:uid].to_i : 0) if room[:uid] && !@nav_uid_pending
              @nav_uid_pending = false
              # Record whether the game displayed the RealID this arrival (a UID
              # marker present == ShowRoomID ON) so the outbound room-name/subtitle
              # rewrite can preserve that choice GS-style. A marker-less subtitle
              # (flag OFF) leaves the previous room's flag stale, so clear it here.
              @show_room_id = !room[:uid].nil?
            end
          else
            @room_title = String.new
          end
        end
      end
    end
    if name == 'style'
      @current_style = attributes['id']
    end
    if (name == 'clearStream' && attributes['id'] == 'percWindow')
      @dr_active_spells_clear = true
    end

    if (name == 'pushStream' && attributes['id'] == 'percWindow')
      @dr_active_spell_tracking = true
      @dr_active_spells_clear = false
    end

    if name == 'prompt'
      @server_time = attributes['time'].to_i
      @server_time_offset = (Time.now.to_f - @server_time)
      $_CLIENT_.puts "\034GSq#{sprintf('%010d', @server_time)}\r\n" if @send_fake_tags

      # A prompt terminates the command burst and is the reliable close
      # signal for clearContainer/inv container fills, which have no
      # closing tag of their own. No-op when no container refresh is open.
      GameObj.commit_all_containers

      if @dr_active_spell_tracking
        @dr_active_spell_tracking = false
        @dr_active_spells_slivers = false
        @dr_active_spells = @dr_active_spells_tmp
        @dr_active_spells_tmp = {}
      elsif @dr_active_spells_clear
        @dr_active_spells = {}
      end

      # DragonRealms stream-order name backfill, applied as an
      # all-or-nothing batch at the end of the room-objs + crtrStatus
      # sequence. Only when the captured bold-name count exactly matches
      # this batch's crtrStatus id count do we pair them by position; on any
      # mismatch (e.g. a bold room entity that emits no crtrStatus) we skip
      # naming rather than risk a shifted mis-pair - assess still backfills
      # names by id. Reset after every prompt so a later lone crtrStatus
      # batch can't reuse a previous refresh's names.
      if XMLData.game =~ /^DR/ && defined?(Lich::DragonRealms::Creature)
        if !@dr_crtr_ids.empty? && @dr_crtr_ids.length == @dr_room_npc_names.length
          @dr_crtr_ids.each_with_index do |id, i|
            Lich::DragonRealms::Creature[id]&.apply_room_name(@dr_room_npc_names[i])
          end
        end
        @dr_room_npc_names = []
        @dr_crtr_ids = []
      end
    end

    if name == 'clearContainer'
      if attributes['id'] == 'stow'
        GameObj.begin_container(@stow_container_id)
      else
        GameObj.begin_container(attributes['id'])
      end
    end
    if name == 'deleteContainer'
      GameObj.delete_container(attributes['id'])
    end
    if name == 'progressBar'
      if attributes['id'] == 'pbarStance'
        @stance_text = attributes['text'].split.first
        @stance_value = attributes['value'].to_i
        $_CLIENT_.puts "\034GSg#{sprintf('%010d', @stance_value)}\r\n" if @send_fake_tags
      elsif attributes['id'] == 'mana'
        last_mana = @mana
        @mana, @max_mana = attributes['text'].scan(/-?\d+/).collect { |num| num.to_i }
        difference = @mana - last_mana
        # fixme: enhancives screw this up
        unless XMLData.name.empty?
          if (difference == noded_pulse) or (difference == unnoded_pulse) or ((@mana == @max_mana) and (last_mana + noded_pulse > @max_mana))
            @last_pulse = Time.now.to_i
            if @send_fake_tags
              $_CLIENT_.puts "\034GSZ#{sprintf('%010d', (@mana + 1))}\n"
              $_CLIENT_.puts "\034GSZ#{sprintf('%010d', @mana)}\n"
            end
          end
        end
        if @send_fake_tags
          $_CLIENT_.puts "\034GSV#{sprintf('%010d%010d%010d%010d%010d%010d%010d%010d', @max_health.to_i, @health.to_i, @max_spirit.to_i, @spirit.to_i, @max_mana.to_i, @mana.to_i, @wound_gsl, @scar_gsl)}\r\n"
        end
      elsif attributes['id'] == 'stamina'
        @stamina, @max_stamina = attributes['text'].scan(/-?\d+/).collect { |num| num.to_i }
      elsif attributes['id'] == 'mindState'
        @mind_text = attributes['text']
        @mind_value = attributes['value'].to_i
        @field_exp = attributes['field_exp'].to_i if attributes['field_exp']
        @max_field_exp = attributes['max_field_exp'].to_i if attributes['max_field_exp']
        @ascension_exp = attributes['ascension_exp'].to_i if attributes['ascension_exp']
        @exp = attributes['exp'].to_i if attributes['exp']
        @until_next = attributes['until_next'].to_i if attributes['until_next']
        # lumnis and rpa are only sent while active; fashlonae is sent
        # whenever an orb is redeemed (1 = redeemed/inactive, 2 = active).
        # All three are cleared back to nil whenever a fresh mindState
        # progressBar omits them. rpa can be fractional (e.g. 1.5), so it
        # is parsed as a float.
        @fashlonae = attributes['fashlonae'] ? attributes['fashlonae'].to_i : nil
        @lumnis = attributes['lumnis'] ? attributes['lumnis'].to_i : nil
        @rpa = attributes['rpa'] ? attributes['rpa'].to_f : nil
        $_CLIENT_.puts "\034GSr#{MINDMAP[@mind_text]}\r\n" if @send_fake_tags
      elsif attributes['id'] == 'health'
        @health, @max_health = attributes['text'].scan(/-?\d+/).collect { |num| num.to_i }
        $_CLIENT_.puts "\034GSV#{sprintf('%010d%010d%010d%010d%010d%010d%010d%010d', @max_health.to_i, @health.to_i, @max_spirit.to_i, @spirit.to_i, @max_mana.to_i, @mana.to_i, @wound_gsl, @scar_gsl)}\r\n" if @send_fake_tags
      elsif attributes['id'] == 'spirit'
        @last_spirit = @spirit if @last_spirit
        @spirit, @max_spirit = attributes['text'].scan(/-?\d+/).collect { |num| num.to_i }
        @last_spirit = @spirit unless @last_spirit
        $_CLIENT_.puts "\034GSV#{sprintf('%010d%010d%010d%010d%010d%010d%010d%010d', @max_health.to_i, @health.to_i, @max_spirit.to_i, @spirit.to_i, @max_mana.to_i, @mana.to_i, @wound_gsl, @scar_gsl)}\r\n" if @send_fake_tags
      elsif attributes['id'] == 'nextLvlPB'
        Gift.pulse unless @next_level_text == attributes['text']
        @next_level_value = attributes['value'].to_i
        @next_level_text = attributes['text']
      elsif attributes['id'] == 'encumlevel'
        @encumbrance_value = attributes['value'].to_i
        @encumbrance_text = attributes['text']
      elsif attributes['id'] == 'concentration'
        @concentration, @max_concentration = attributes['text'].scan(/-?\d+/).collect { |num| num.to_i }
      elsif PSM_3_DIALOG_IDS.include?(@active_ids[-2])
        # puts "kind=(%s) name=%s attributes=%s" % [@active_ids[-2], name, attributes]
        self.parse_psm3_progressbar(@active_ids[-2], attributes)
        # since we received an updated spell duration, let's signal infomon to update
        ActiveSpell.request_update
      end
    end
    if name == 'roundTime'
      @roundtime_end = attributes['value'].to_i
      $_CLIENT_.puts "\034GSQ#{sprintf('%010d', @roundtime_end)}\r\n" if @send_fake_tags
    end
    if name == 'castTime'
      @cast_roundtime_end = attributes['value'].to_i
    end
    if name == 'dropDownBox'
      if attributes['id'] == 'dDBTarget'
        @current_target_ids.clear
        attributes['content_value'].split(',').each { |t|
          if t =~ /^\#(\-?\d+)(?:,|$)/
            @current_target_ids.push($1)
          end
        }
        if attributes['content_value'] =~ /^\#(\-?\d+)(?:,|$)/
          @current_target_id = $1
        else
          @current_target_id = nil
        end
      end
    end
    if name == 'indicator'
      @indicator[attributes['id']] = attributes['visible']
      if @send_fake_tags
        if attributes['id'] == 'IconPOISONED'
          if attributes['visible'] == 'y'
            $_CLIENT_.puts "\034GSJ0000000000000000000100000000001\r\n"
          else
            $_CLIENT_.puts "\034GSJ0000000000000000000000000000000\r\n"
          end
        elsif attributes['id'] == 'IconDISEASED'
          if attributes['visible'] == 'y'
            $_CLIENT_.puts "\034GSK0000000000000000000100000000001\r\n"
          else
            $_CLIENT_.puts "\034GSK0000000000000000000000000000000\r\n"
          end
        else
          gsl_prompt = String.new; ICONMAP.keys.each { |icon| gsl_prompt += ICONMAP[icon] if @indicator[icon] == 'y' }
          $_CLIENT_.puts "\034GSP#{sprintf('%-30s', gsl_prompt)}\r\n"
        end
      end
    end
    if (name == 'image') and @active_ids.include?('injuries')
      if @injuries.keys.include?(attributes['id'])
        if attributes['name'] =~ /Injury/i
          @injuries[attributes['id']]['wound'] = attributes['name'].slice(/\d/).to_i
        elsif attributes['name'] =~ /Scar/i
          @injuries[attributes['id']]['wound'] = 0
          @injuries[attributes['id']]['scar'] = attributes['name'].slice(/\d/).to_i
        elsif attributes['name'] =~ /Nsys/i
          rank = attributes['name'].slice(/\d/).to_i
          if rank == 0
            @injuries['nsys']['wound'] = 0
            @injuries['nsys']['scar'] = 0
          else
            Thread.new {
              wait_while { dead? }
              action = proc { |server_string|
                if (@nerve_tracker_active == 'maybe')
                  if @nerve_tracker_active == 'maybe'
                    if server_string =~ /^You/
                      @nerve_tracker_active = 'yes'
                      @injuries['nsys']['wound'] = 0
                      @injuries['nsys']['scar'] = 0
                    else
                      @nerve_tracker_active = 'no'
                    end
                  end
                end
                if @nerve_tracker_active == 'yes'
                  if server_string =~ /<output class=['"]['"]\/>/
                    @nerve_tracker_active = 'no'
                    @nerve_tracker_num -= 1
                    DownstreamHook.remove('nerve_tracker') if @nerve_tracker_num < 1
                    $_CLIENT_.puts "\034GSV#{sprintf('%010d%010d%010d%010d%010d%010d%010d%010d', @max_health.to_i, @health.to_i, @max_spirit.to_i, @spirit.to_i, @max_mana.to_i, @mana.to_i, make_wound_gsl, make_scar_gsl)}\r\n" if @send_fake_tags
                    server_string
                  elsif server_string =~ /a case of uncontrollable convulsions/
                    @injuries['nsys']['wound'] = 3
                    nil
                  elsif server_string =~ /a case of sporadic convulsions/
                    @injuries['nsys']['wound'] = 2
                    nil
                  elsif server_string =~ /a strange case of muscle twitching/
                    @injuries['nsys']['wound'] = 1
                    nil
                  elsif server_string =~ /a very difficult time with muscle control/
                    @injuries['nsys']['scar'] = 3
                    nil
                  elsif server_string =~ /constant muscle spasms/
                    @injuries['nsys']['scar'] = 2
                    nil
                  elsif server_string =~ /developed slurred speech/
                    @injuries['nsys']['scar'] = 1
                    nil
                  end
                else
                  if server_string =~ /<output class=['"]mono['"]\/>/
                    @nerve_tracker_active = 'maybe'
                  end
                  server_string
                end
              }
              @nerve_tracker_num += 1
              DownstreamHook.add('nerve_tracker', action, persist: true) # engine-managed, toggled by the parser
              Game._puts "#{$cmd_prefix}health"
            }
          end
        else
          @injuries[attributes['id']]['wound'] = 0
          @injuries[attributes['id']]['scar'] = 0
        end
      end
      $_CLIENT_.puts "\034GSV#{sprintf('%010d%010d%010d%010d%010d%010d%010d%010d', @max_health.to_i, @health.to_i, @max_spirit.to_i, @spirit.to_i, @max_mana.to_i, @mana.to_i, make_wound_gsl, make_scar_gsl)}\r\n" if @send_fake_tags
    end
    if @room_window_disabled and (name == 'dir') and @active_tags.include?('compass')
      @room_exits.push(LONGDIR[attributes['value']])
    end
    if name == 'radio'
      if attributes['id'] == 'injrRad'
        @injury_mode = 0 if attributes['value'] == '1'
      elsif attributes['id'] == 'scarRad'
        @injury_mode = 1 if attributes['value'] == '1'
      elsif attributes['id'] == 'bothRad'
        @injury_mode = 2 if attributes['value'] == '1'
      end
    end
    if name == 'label'
      if attributes['id'] == 'yourLvl'
        @level = attributes['value'].slice(/\d+/).to_i
      elsif attributes['id'] == 'encumblurb'
        @encumbrance_full_text = attributes['value']
      end
    end
    if (name == 'container') and (attributes['id'] == 'stow')
      @stow_container_id = attributes['target'].sub('#', '')
    end
    if (name == 'clearStream')
      if attributes['id'] == 'bounty'
        @bounty_task = String.new
      elsif attributes['id'] == 'assess'
        @assess = []
      end
    end
    if (name == 'playerID')
      @player_id = attributes['id']
      unless Frontend.supports_gsl?
        if Lich.inventory_boxes(@player_id)
          DownstreamHook.remove('inventory_boxes_off')
        end
      end
    end
    if name == 'settingsInfo'
      if (game = attributes['instance'])
        if game == 'GS4'
          @game = 'GSIV'
        elsif (game == 'GSX') or (game == 'GS4X')
          @game = 'GSPlat'
        else
          @game = game # covers DR, DRT, DRF, GST, GSF
        end
      end
    end
    if (name == 'app') and (@name = attributes['char'])
      if @game.nil? or @game.empty?
        @game = 'unknown'
      end
      unless File.exist?("#{DATA_DIR}/#{@game}")
        Dir.mkdir("#{DATA_DIR}/#{@game}")
      end
      unless File.exist?("#{DATA_DIR}/#{@game}/#{@name}")
        Dir.mkdir("#{DATA_DIR}/#{@game}/#{@name}")
      end
      if Frontend.supports_gsl?
        Game._puts "#{$cmd_prefix}_flag Display Dialog Boxes 0"
        sleep 0.05
        Game._puts "#{$cmd_prefix}_injury 2"
        sleep 0.05
        # fixme: game name hardcoded as Gemstone IV; maybe doesn't make any difference to the client
        $_CLIENT_.puts "\034GSB0000000000#{attributes['char']}\r\n\034GSA#{Time.now.to_i}GemStone IV\034GSD\r\n"
        # Sending fake GSL tags to the Wizard FE is disabled until now, because it doesn't accept the tags and just gives errors until initialized with the above line
        @send_fake_tags = true
        # Send all the tags we missed out on
        $_CLIENT_.puts "\034GSV#{sprintf('%010d%010d%010d%010d%010d%010d%010d%010d', @max_health.to_i, @health.to_i, @max_spirit.to_i, @spirit.to_i, @max_mana.to_i, @mana.to_i, make_wound_gsl, make_scar_gsl)}\r\n"
        $_CLIENT_.puts "\034GSg#{sprintf('%010d', @stance_value)}\r\n"
        $_CLIENT_.puts "\034GSr#{MINDMAP[@mind_text]}\r\n"
        gsl_prompt = String.new
        @indicator.keys.each { |icon| gsl_prompt += ICONMAP[icon] if @indicator[icon] == 'y' }
        $_CLIENT_.puts "\034GSP#{sprintf('%-30s', gsl_prompt)}\r\n"
        gsl_prompt = nil
        gsl_exits = String.new
        @room_exits.each { |exit| gsl_exits.concat(DIRMAP[SHORTDIR[exit]].to_s) }
        $_CLIENT_.puts "\034GSj#{sprintf('%-20s', gsl_exits)}\r\n"
        gsl_exits = nil
        $_CLIENT_.puts "\034GSn#{sprintf('%-14s', @prepared_spell)}\r\n"
        $_CLIENT_.puts "\034GSm#{sprintf('%-45s', GameObj.right_hand.name)}\r\n"
        $_CLIENT_.puts "\034GSl#{sprintf('%-45s', GameObj.left_hand.name)}\r\n"
        $_CLIENT_.puts "\034GSq#{sprintf('%010d', @server_time)}\r\n"
        $_CLIENT_.puts "\034GSQ#{sprintf('%010d', @roundtime_end)}\r\n" if @roundtime_end > 0
      end
      Game._puts("#{$cmd_prefix}_flag Display Inventory Boxes 1")
    end
  rescue
    Lich.log "error: XMLParser.tag_start: #{$!}\n\t#{$!.backtrace.join("\n\t")}"
    sleep 0.1
    reset
  end
end

#text(text_string) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Routes text nodes to game-state updates and message capture.

Called once per text node by the Ox SAX parser. First decodes standard XML entities (since Ox runs with convert_special: false). Then dispatches to state handlers based on active tag stack and current stream context:

  • Inventory items: builds object name and surrounding text
  • Room components: registers NPCs, items, players, and descriptions
  • Spell tracking: extracts spell names and durations from percWindow stream
  • Familiar room: tracks the familiar's location and contents
  • Bounty/society tasks: appends to multi-line content
  • Assess stream: builds combat situation descriptions

In DragonRealms, captures bold NPC names in stream order so they can be paired to creature status ids at the next prompt. In GemStone, applies creature status immediately via the bold tag path.

If an exception is raised, logs the error, sleeps briefly, and calls reset() to discard in-flight state.

Parameters:



1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
# File 'documented/common/xmlparser.rb', line 1166

def text(text_string)
  # Called by Ox once per text node. Decode the standard XML entities (Ox
  # runs with convert_special: false; see Lich::Common::XmlEntities).
  text_string = XmlEntities.decode(text_string)
  begin
    # fixme: /<stream id="Spells">.*?<\/stream>/m
    # $_CLIENT_.write(text_string) unless ($frontend != 'suks') or (@current_stream =~ /^(?:spellfront|inv|bounty|society)$/) or @active_tags.any? { |tag| tag =~ /^(?:compDef|inv|component|right|left|spell)$/ } or (@active_tags.include?('stream') and @active_ids.include?('Spells')) or (text_string == "\n" and (@last_tag =~ /^(?:popStream|prompt|compDef|dialogData|openDialog|switchQuickBar|component)$/))

    # DR Active Spell tracking and handling
    if @dr_active_spell_tracking
      spell = nil
      duration = nil
      case text_string
      when /(?<spell>^[^\(]+)\((?<duration>\d+|Indefinite|OM|Fading)\s*(?:%|roisae?n)?\)/i
        # Spell with known duration remaining
        # XML looks like:
        # Hydra Hex  (Indefinite)
        # Persistence of Mana  (OM)
        # Cure Disease  (Fading)
        # Osrel Meraud  (94%)
        # Landslide (4 roisaen)
        # Khri Sagacity  (1 roisan)
        spell = Regexp.last_match[:spell]
        duration = Regexp.last_match[:duration]

        if duration.match?(/Indefinite|OM/)
          duration = 1000
        elsif duration.match?(/Fading/)
          duration = 0
        else
          duration = duration.to_i
        end
      when /(?<spell>Stellar Collector)\s+\((?<percentage>\d+)%,\s*(?<duration>\d+)?\s*(?<unit>(?:roisae?n|anlaen|fading))/
        # Stellar collector special case
        # XML looks like:
        # Stellar Collector  (0%, 4 anlaen)
        # Stellar Collector  (0%, fading)
        spell = Regexp.last_match[:spell]
        duration = Regexp.last_match[:duration].to_i
        @dr_active_spells_stellar_percentage = Regexp.last_match[:percentage].to_i
        unit = Regexp.last_match[:unit]
        duration = unit == 'anlaen' ? duration * 30 : duration
      when /(?<spell>^[^\(]+)\(.+\)/i
        # Spells with inexact duration verbiage, such as with
        # Barbarians without knowledge of Power Monger mastery
        spell = Regexp.last_match[:spell]
        duration = 1000
      when /.*orbiting sliver.*/i
        # Moon Mage slivers
        @dr_active_spells_slivers = true
      end
      spell.strip!
      if spell
        @dr_active_spells_tmp[spell] = duration
      end
    end

    if @current_style == 'roomName'
      @room_name = text_string.match(/(?<roomname>\[.*?\])/)[:roomname]
    end

    if @active_tags.include?('inv')
      if @active_tags[-1] == 'a'
        @obj_name = text_string
      elsif @obj_name.nil?
        @obj_before_name = text_string.strip
      else
        @obj_after_name = text_string.strip
      end
    elsif @active_tags.last == 'prompt'
      @prompt = text_string
    elsif @active_tags.include?('right')
      GameObj.new_right_hand(@obj_exist, @obj_noun, text_string)
      # DR only: an item now held in hand is no longer worn or in a
      # container, so drop any stale placement of it. DR has no passive
      # container stream to self-correct this, and its containers are
      # rebuilt only on an explicit INV LIST/SEARCH. Gated so GemStone
      # (which relies on its own inv stream) is unaffected. Empty hands
      # carry no exist id (@obj_exist nil), making this a safe no-op.
      GameObj.remove_inv_item(@obj_exist) if XMLData.game =~ /^DR/
      $_CLIENT_.puts "\034GSm#{sprintf('%-45s', text_string)}\r\n" if @send_fake_tags
    elsif @active_tags.include?('left')
      GameObj.new_left_hand(@obj_exist, @obj_noun, text_string)
      GameObj.remove_inv_item(@obj_exist) if XMLData.game =~ /^DR/ # see 'right' above
      $_CLIENT_.puts "\034GSl#{sprintf('%-45s', text_string)}\r\n" if @send_fake_tags
    elsif @active_tags.include?('spell')
      @prepared_spell = text_string
      $_CLIENT_.puts "\034GSn#{sprintf('%-14s', text_string)}\r\n" if @send_fake_tags
    elsif @active_tags.include?('compDef') or @active_tags.include?('component')
      if @active_ids.include?('room objs')
        if @active_tags.include?('a')
          if @bold
            @last_npc = GameObj.new_npc(@obj_exist, @obj_noun, text_string)
            if XMLData.game =~ /^DR/
              # Future-proofing: DragonRealms room-objs creatures currently
              # carry no <a> tag, but if that ever changes to GemStone's
              # <a exist noun> shape, register id-first with the inline
              # name/noun via DR's own Creature (never the unqualified
              # Gemstone Creature below, which is not loaded in DR). With an
              # <a> present the stream-order capture branch is skipped, so
              # this becomes the sole name source - no double-set.
              if @obj_exist && defined?(Lich::DragonRealms::Creature)
                dr_creature = Lich::DragonRealms::Creature.register(text_string, @obj_exist, @obj_noun)
                dr_creature&.apply_room_name(text_string)
              end
            elsif XMLData.current_target_ids.include?(@obj_exist) || @pending_crtr_status.key?(@obj_exist)
              creature = Creature.register(text_string, @obj_exist, @obj_noun)
              if creature && (pending_flags = @pending_crtr_status.delete(@obj_exist))
                creature.sync_crtr_status(pending_flags)
              end
            end
          else
            GameObj.new_loot(@obj_exist, @obj_noun, text_string)
          end
        elsif @bold && XMLData.game =~ /^DR/
          # DragonRealms room-objs bold NPC names carry no <a> tag. Capture
          # them in stream order so the <crtrStatus> batch that follows can
          # pair the Nth name to the Nth id (see the crtrStatus handler).
          # Only bold runs are names; the non-bold "(dead)"/"(immobile)"
          # status runs fall through to the annotation branch below.
          @dr_room_npc_names << text_string
        elsif (text_string =~ /that (?:is|appears) ([\w\s]+)(?:,| and|\.)/) or (text_string =~ / \(([^\(]+)\)/)
          # @last_npc is nil in DragonRealms here (DR room-objs bold names
          # carry no <a> tag, so the new_npc branch above never runs and
          # never sets it), so the &. keeps this a safe no-op in DR while
          # still annotating the last GemStone npc.
          @last_npc&.status = $1
        end
      elsif @active_ids.include?('room players')
        if @active_tags.include?('a')
          if @obj_exist.to_s.start_with?('-')
            @pc = GameObj.new_pc(@obj_exist, @obj_noun, "#{@player_title}#{text_string}", @player_status)
            # Track the status we just staged for this pc. The annotation
            # branch below appends to this instead of reading it back
            # through GameObj#status, which cannot see the in-flight value.
            @pc_status = @player_status
            @arrival_pcs.push(@pc.noun) if (defined?(Lich::Claim) && Lich::Claim::Lock.owned?)
          else
            @pc = nil
            @pc_status = nil
          end
          @player_status = nil
          @player_title = nil
        else
          if @game =~ /^DR/
            GameObj.begin_room_players
            text_string.sub(/^Also here\: /, '').sub(/ and ([^,]+)\./) { ", #{$1}" }.split(', ').each { |player|
              if player =~ / who is (.+)/
                status = $1
                player.sub!(/ who is .+/, '')
              elsif player =~ / \((.+)\)/
                status = $1
                player.sub!(/ \(.+\)/, '')
              else
                status = nil
              end
              noun = player.slice(/\b[A-Z][a-z]+$/)
              if player =~ /the body of /
                player.sub!('the body of ', '')
                if status
                  status.concat ' dead'
                else
                  status = 'dead'
                end
              end
              if player =~ /a stunned /
                player.sub!('a stunned ', '')
                if status
                  status.concat ' stunned'
                else
                  status = 'stunned'
                end
              end
              GameObj.new_pc(nil, noun, player, status)
            }
            GameObj.commit_room_players
          else
            if @pc && ((text_string =~ /^ who (?:is|appears) ([\w\s]+)(?:,| and|\.|$)/) || (text_string =~ / \(([\w\s]+)\)(?: \(([\w\s]+)\))?/))
              @pc_status = @pc_status ? "#{@pc_status} #{$1}" : $1
              @pc_status = "#{@pc_status} #{$2}" if $2
              @pc.status = @pc_status
            end
            if text_string =~ /(?:^Also here: |, )(?:a )?([a-z\s]+)?([\w\s\-!\?',]+)?$/
              @player_status = ($1.strip.gsub('the body of', 'dead')) if $1
              @player_title = $2
            end
          end
        end
      elsif @active_ids.include?('room desc')
        if text_string == '[Room window disabled at this location.]'
          @room_window_disabled = true
        else
          @room_window_disabled = false
          @room_description.concat(text_string)
          if @active_tags.include?('a')
            GameObj.new_room_desc(@obj_exist, @obj_noun, text_string)
          end
        end
      elsif @active_ids.include?('room exits')
        @room_exits_string.concat(text_string)
        @room_exits.push(text_string) if @active_tags.include?('d')
      end
    elsif @current_stream == 'bounty'
      @bounty_task += text_string
    elsif @current_stream == 'society'
      @society_task = text_string
    elsif @current_stream == 'assess'
      @assess_buffer.concat(text_string) if @assess_buffer
    elsif (@current_stream == 'inv') and @active_tags.include?('a')
      GameObj.new_inv(@obj_exist, @obj_noun, text_string, nil)
    elsif (@current_stream == 'reserve') and @active_tags.include?('a')
      GameObj.new_reserve(@obj_exist, @obj_noun, text_string)
    elsif @check_obvious_hiding && text_string =~ /obvious signs of someone hiding/
      @room_player_hidden = true
    elsif @current_stream == 'familiar'
      # fixme: familiar room tracking does not (can not?) auto update, status of pcs and npcs isn't tracked at all, titles of pcs aren't tracked
      if @current_style == 'roomName'
        @familiar_room_title = text_string
        @familiar_room_description = String.new
        @familiar_room_exits = Array.new
        GameObj.begin_familiar
        @fam_mode = String.new
      elsif @current_style == 'roomDesc'
        @familiar_room_description.concat(text_string)
        if @active_tags.include?('a')
          GameObj.new_fam_room_desc(@obj_exist, @obj_noun, text_string)
        end
      elsif text_string =~ /^You also see/
        @fam_mode = 'things'
      elsif text_string =~ /^Also here/
        @fam_mode = 'people'
      elsif text_string =~ /Obvious (?:paths|exits)/
        @fam_mode = 'paths'
      elsif @fam_mode == 'things'
        if @active_tags.include?('a')
          if @bold
            GameObj.new_fam_npc(@obj_exist, @obj_noun, text_string)
          else
            GameObj.new_fam_loot(@obj_exist, @obj_noun, text_string)
          end
        end
        # puts 'things: ' + text_string
      elsif @fam_mode == 'people' and @active_tags.include?('a')
        GameObj.new_fam_pc(@obj_exist, @obj_noun, text_string)
        # puts 'people: ' + text_string
      elsif (@fam_mode == 'paths') and @active_tags.include?('a')
        @familiar_room_exits.push(text_string)
      end
    elsif @room_window_disabled
      if @current_style == 'roomDesc'
        @room_description.concat(text_string)
        if @active_tags.include?('a')
          GameObj.new_room_desc(@obj_exist, @obj_noun, text_string)
        end
      elsif text_string =~ /^Obvious (?:paths|exits): (?:none)?$/
        @room_exits_string = text_string.strip
      end
    end
  rescue
    Lich.log "error: XMLParser.text: #{$!}\n\t#{$!.backtrace.join("\n\t")}"
    sleep 0.1
    reset
  end
end