forked from elanthia-online/dr-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathspellmonitor.lic
477 lines (436 loc) · 18.3 KB
/
spellmonitor.lic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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
208
209
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
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
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
=begin
Documentation: https://elanthipedia.play.net/Lich_script_development#spellmonitor
=end
$SPELLMONITOR_VERSION = '2.0.1'
no_kill_all
no_pause_all
silence_me
setpriority(0)
class DRSpells
@@active_spells = {}
@@known_spells = {}
@@known_feats = {}
@@refresh_data = {}
@@stellar_percentage = 0
@@slivers = false
@@spellbook_format = nil # 'column-formatted' or 'non-column'
@@grabbing_active_spells = false
@@grabbing_known_spells = false
# Use this to silence the initial output
# of calling 'spells' command to populate our data.
@@silence_known_spells_hook = false
def self.active_spells
@@active_spells
end
def self.known_spells
@@known_spells
end
def self.known_feats
@@known_feats
end
def self.refresh_data
@@refresh_data
end
def self.slivers
@@slivers
end
def self.slivers=(val)
@@slivers = val
end
def self.stellar_percentage
@@stellar_percentage
end
def self.stellar_percentage=(val)
@@stellar_percentage = val
end
def self.grabbing_active_spells
@@grabbing_active_spells
end
def self.grabbing_active_spells=(val)
@@grabbing_active_spells = val
end
def self.grabbing_known_spells
@@grabbing_known_spells
end
def self.grabbing_known_spells=(val)
@@grabbing_known_spells = val
end
def self.silence_known_spells_hook
@@silence_known_spells_hook
end
def self.silence_known_spells_hook=(val)
@@silence_known_spells_hook = val
end
def self.spellbook_format
@@spellbook_format
end
def self.spellbook_format=(val)
@@spellbook_format = val
end
# Call this method to cause the script to recheck for known spells and feats.
# Should be called whenever the character learns (or forgets) a spell or feat.
# Example scenarios: learning from a guild leader, Ozursus, scrolls, Throne City, rerolling, death.
def self.refresh_known_spells
@@silence_known_spells_hook = true
if DRStats.barbarian? || DRStats.thief?
fput('ability')
else
fput('spells')
end
pause 1
@@silence_known_spells_hook = false
end
end
# Some spells may last for an unknown duration,
# such as cyclic spells that last as long as
# the caster can harness mana for it.
# Or, barbarian abilities when the character
# doesn't have Power Monger mastery to see true
# durations but only vague guestimates.
# In those situations, we set use this value.
UNKNOWN_DURATION = 1000 unless defined?(UNKNOWN_DURATION)
# This method receives a line of data from DragonRealms
# and determines if it refers to active spells on the character.
# It keeps the global class DRSpells updated with such data
# that then other scripts can take advantage of.
#
# The XML stream for DragonRealms is whack at best.
#
# Examples of the data we're looking for:
# <clearStream id="percWindow"/>
# <pushStream id="percWindow"/> Wildfire (thoroughly inflamed)
# <popStream/><pushStream id="percWindow"/> Avalanche (thoroughly inflamed)
# <popStream/><pushStream id="percWindow"/>Righteous Wrath (8 roisaen)
# Rage of the Clans (4 roisaen)
# <popStream/><prompt time="1609379003">></prompt>
#
# <clearStream> indicates that the following tags are the latest active spells snapshot.
#
# <pushStream id="percWindow"> indicates a spell or spells that are active and their duration.
#
# <popStream/> indicates the end of content for the prior listed spell or spells for the <pushStream> tag.
#
# One or more spells may be listed between a <pushStream/> <popStream/> pair,
# but only one spell and its duration are ever listed per line.
#
active_spells_hook = proc do |server_string|
# New information about active spells, reset our cached data.
if server_string =~ %r{<clearStream id="percWindow"/>}
DRSpells.slivers = false
DRSpells.refresh_data.each_key { |k| DRSpells.refresh_data[k] = false }
begin
Thread.new do
sleep 0.5
DRSpells.refresh_data.each do |key, state|
unless state
DRSpells.active_spells.delete(key)
DRSpells.refresh_data.delete(key)
end
end
end
rescue StandardError => e
echo(e)
echo('Error in spell monitor')
end
next server_string
end
# Abilities may be prefixed or suffixed by
# a control character that appears like whitespace
# but isn't whitespace so 'strip' doesn't remove it.
# Replace all instances of codepoint 32 with ' '.
data = server_string.dup.gsub(32.chr, ' ').strip
# Indicates we're done with the prior <pushStream/> tag.
# However, the next characters in the line might be
# another <pushStream/> or something else, so keep going.
if data =~ %r{^<popStream/>}
DRSpells.grabbing_active_spells = false
data.slice!(0, 12).strip!
end
# The start of one or more active spells!
if data =~ %r{<pushStream id="percWindow"/>}
DRSpells.grabbing_active_spells = true
index = data.index("<pushStream id=\"percWindow\"/>")
data.slice!(0, index + 29).strip!
end
if DRSpells.grabbing_active_spells
spell = nil
duration = nil
case data
when /(?<spell>[^<>]+?)\s+\((?:\D*)(?<duration>\d+)\s*(?:%|roisae?n)\)/i
# Spell with known duration remaining
spell = Regexp.last_match[:spell]
duration = Regexp.last_match[:duration].to_i
when /(?<spell>[^<>]+?)\s+\(fading\)/i
# Spell fading away
spell = Regexp.last_match[:spell]
duration = 0
when /(?<spell>[^<>]+?)\s+\((?<duration>indefinite|om)\)/i
# Cyclic spell or Osrel Meraud cyclic spell
spell = Regexp.last_match[:spell]
duration = UNKNOWN_DURATION
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
DRSpells.stellar_percentage = Regexp.last_match[:percentage].to_i
unit = Regexp.last_match[:unit]
duration = unit == 'anlaen' ? duration * 30 : duration
when /(?<spell>[^<>]+?)\s+\(.+\)/i
# Spells with inexact duration verbiage, such as with
# Barbarians without knowledge of Power Monger mastery
spell = Regexp.last_match[:spell]
duration = UNKNOWN_DURATION
when /.*orbiting sliver.*/i
# Moon Mage slivers
DRSpells.slivers = true
when /^(.*)$/
# No idea what we received, just a general catch all
spell = Regexp.last_match(1)
duration = UNKNOWN_DURATION
end
spell.strip!
if spell
DRSpells.active_spells[spell] = duration
DRSpells.refresh_data[spell] = true
end
end
server_string
end
# This method parses the output from `spells` command for magic users
# and populates the known spells/feats based on the output.
#
# As of June 2022, There are two different output formats: column-formatted and non-column.
# https://elanthipedia.play.net/Post:Tuesday_Tidings_-_120_-_Spells_-_06/07/2022_-_18:34
#
# The XML stream for DragonRealms is whack at best.
#
# Examples of non-column output:
# You recall the spells you have learned from your training.
# In the chapter entitled "Analogous Patterns", you have notes on the Manifest Force [maf] and Gauge Flow [gaf] spells.
# You have temporarily memorized the Tailwind [tw] spell.
# You recall proficiency with the magic feats of Sorcerous Patterns, Alternate Preparation and Augmentation Mastery.
# You are NOT currently set to recognize known spells when prepared by someone else in the area. (Use SPELL RECOGNIZE ON to change this.)
# You are currently set to display full cast messaging. (Use SPELL BRIEFMSG ON to change this.)
# You are currently attempting to hide your spell preparing. (Use PREPARE /HIDE to change this.)
# You can use SPELL STANCE [HELP] to view or modify your spellcasting preferences.
#
# Examples of column-formatted output:
# You recall the spells you have learned from your training.
# <output class="mono"/>
# <pushBold/>
# Analogous Patterns:
# <popBold/> maf Manifest Force Slot(s): 1 Min Prep: 1 Max Prep: 100
# gaf Gauge Flow Slot(s): 2 Min Prep: 5 Max Prep: 100
# <pushBold/>
# Synthetic Creation:
# <popBold/> acs Acid Splash Slot(s): 1 Min Prep: 1 Max Prep: 50
# vs Viscous Solution Slot(s): 2 Min Prep: 10 Max Prep: 66
# <output class=""/>
# <output class="mono"/>
# <pushBold/>
# Temporarily Memorized:
# <popBold/> tw Tailwind Slot(s): 1 Min Prep: 5 Max Prep: 100
# <output class=""/>
# You recall proficiency with the magic feats of Sorcerous Patterns, Alternate Preparation and Augmentation Mastery.
# You are NOT currently set to recognize known spells when prepared by someone else in the area. (Use SPELL RECOGNIZE ON to change this.)
# You are currently set to display full cast messaging. (Use SPELL BRIEFMSG ON to change this.)
# You are currently attempting to hide your spell preparing. (Use PREPARE /HIDE to change this.)
# You can use SPELL STANCE [HELP] to view or modify your spellcasting preferences.
#
# One or more spells may be listed between a <popBold/> <pushBold/> pair,
# but only one spell and its information are ever listed per line.
#
known_mage_spells_hook = proc do |server_string|
case server_string
when /^You will .* (?<format>column-formatted|non-column) output for the SPELLS verb/
# Parse `toggle spellbook` command
DRSpells.spellbook_format = Regexp.last_match[:format]
server_string = nil if DRSpells.silence_known_spells_hook
when /^You recall the spells you have learned/
DRSpells.grabbing_known_spells = true
DRSpells.known_spells.clear()
DRSpells.known_feats.clear()
DRSpells.spellbook_format = 'non-column' # assume original format
server_string = nil if DRSpells.silence_known_spells_hook
when /^<output class="mono"\/>/
# Matched an xml tag while parsing spells, must be column-formatted output
if DRSpells.grabbing_known_spells
DRSpells.spellbook_format = 'column-formatted'
end
server_string = nil if DRSpells.silence_known_spells_hook
when /^[\w\s]+:/
# Matched the spellbook name in column-formatted output, ignore
server_string = nil if DRSpells.silence_known_spells_hook
when /Slot\(s\): \d+ \s+ Min Prep: \d+ \s+ Max Prep: \d+/
# Matched the spell info in column-formatted output, parse
if DRSpells.grabbing_known_spells && DRSpells.spellbook_format == 'column-formatted'
spell = server_string
.sub('<popBold/>', '') # remove xml tag at start of some lines
.slice(10, 32) # grab the spell name, after the alias and before Slots
.strip
if !spell.empty?
DRSpells.known_spells[spell] = true
end
end
# Preserve the pop bold command we removed from start of spell line
# otherwise lots of game text suddenly are highlighted yellow
server_string = '<popBold/>' if DRSpells.silence_known_spells_hook
when /^In the chapter entitled|^You have temporarily memorized|^From your apprenticeship you remember practicing/
if DRSpells.grabbing_known_spells
server_string
.sub(/^In the chapter entitled "[\w\s\'-]+", you have notes on the /, '')
.sub(/^You have temporarily memorized the /, '')
.sub(/^From your apprenticeship you remember practicing with the /, '')
.sub(/ spells?\./, '')
.sub(/,? and /, ',')
.split(',')
.map { |mapped_spell| mapped_spell.include?('[') ? mapped_spell.slice(0, mapped_spell.index('[')) : mapped_spell }
.map(&:strip)
.reject { |rejected_spell| rejected_spell.nil? || rejected_spell.empty? }
.each { |each_spell| DRSpells.known_spells[each_spell] = true }
end
server_string = nil if DRSpells.silence_known_spells_hook
when /^You recall proficiency with the magic feats of/
if DRSpells.grabbing_known_spells
# The feats are listed without the Oxford comma separating the last item.
# This makes splitting the string by comma difficult because the next to last and last
# items would be captured together. The workaround is we'll replace ' and ' with a comma
# and hope no feats ever have the word 'and' in them...
server_string
.sub(/^You recall proficiency with the magic feats of/, '')
.sub(/,? and /, ',')
.sub('.', '')
.split(',')
.map(&:strip)
.reject { |feat| feat.nil? || feat.empty? }
.each { |feat| DRSpells.known_feats[feat] = true }
end
server_string = nil if DRSpells.silence_known_spells_hook
when /^You have \d+ spell slots? available|You do not know any magic feats|You know the following .* cantrips?/
server_string = nil if DRSpells.silence_known_spells_hook
when /^You can use SPELL STANCE|^You have (no|yet to receive any) training in the magical arts|You have no desire to soil yourself with magical trickery|^You really shouldn't be loitering here|\(Use SPELL|\(Use PREPARE/
DRSpells.grabbing_known_spells = false
server_string = nil if DRSpells.silence_known_spells_hook
end
server_string
end
# This method parses the output from `ability` command for Barbarians
# and populates the known spells/feats based on the known abilities/masteries.
#
# The XML stream for DragonRealms is whack at best.
#
# Examples of the data we're looking for:
# You know the Berserks:<pushBold/> Avalanche, Drought.
# <popBold/>You know the Forms:<pushBold/> Monkey.
# <popBold/>You know the Roars:<pushBold/> Anger the Earth.
# <popBold/>You know the Meditations:<pushBold/> Flame, Power, Contemplation.
# <popBold/>You know the Masteries:<pushBold/> Juggernaut, Duelist.
# <popBold/>
# You recall that you have 0 training sessions remaining with the Guild.
#
known_barbarian_abilities_hook = proc do |server_string|
# The first line from the `ability` output is your known berserks.
# To keep code and conditions organized, this is pulled out to its own block.
if server_string =~ /^You know the (Berserks:)/
DRSpells.grabbing_known_spells = true
DRSpells.known_spells.clear()
DRSpells.known_feats.clear()
end
case server_string
when /^(<(push|pop)Bold\/>)?You know the (Berserks|Forms|Roars|Meditations):(<(push|pop)Bold\/>)?/
if DRSpells.grabbing_known_spells
server_string
.sub(/^(<(push|pop)Bold\/>)?You know the (Berserks|Forms|Roars|Meditations):(<(push|pop)Bold\/>)?/, '')
.sub('.', '')
.split(',')
.map(&:strip)
.reject { |ability| ability.nil? || ability.empty? }
.each { |ability| DRSpells.known_spells[ability] = true }
end
server_string = nil if DRSpells.silence_known_spells_hook
when /^(<(push|pop)Bold\/>)?You know the (Masteries):(<(push|pop)Bold\/>)?/
# Barbarian masteries are the equivalent of magical feats.
if DRSpells.grabbing_known_spells
server_string
.sub(/^(<(push|pop)Bold\/>)?You know the (Masteries):(<(push|pop)Bold\/>)?/, '')
.sub('.', '')
.split(',')
.map(&:strip)
.reject { |mastery| mastery.nil? || mastery.empty? }
.each { |mastery| DRSpells.known_feats[mastery] = true }
end
server_string = nil if DRSpells.silence_known_spells_hook
when /^You recall that you have (\d+) training sessions? remaining with the Guild/
DRSpells.grabbing_known_spells = false
server_string = nil if DRSpells.silence_known_spells_hook
end
server_string
end
# This method parses the output from `ability` command for Thieves
# and populates the known spells/feats based on the known khri.
#
# The XML stream for DragonRealms is whack at best.
#
# Examples of the data we're looking for:
# From the Subtlety tree, you know the following khri: Darken (Aug), Dampen (Util/Ward), Strike (Aug), Silence (Util), Shadowstep (Util), Harrier (Aug)
# From the Finesse tree, you know the following khri: Hasten (Util), Safe (Aug), Avoidance (Aug), Plunder (Aug), Flight (Aug/Ward), Elusion (Aug), Slight (Util)
# From the Potence tree, you know the following khri: Focus (Aug), Prowess (Debil), Sight (Aug), Calm (Util), Steady (Aug), Eliminate (Debil), Serenity (Ward), Sagacity (Ward), Terrify (Debil)
# You have 7 available slots.
#
known_thief_khri_hook = proc do |server_string|
# The first line from the `ability` output is what you know from the Subtlety tree.
# To keep code and conditions organized, this is pulled out to its own block.
if server_string =~ /^From the Subtlety tree, you know the following khri:/
DRSpells.grabbing_known_spells = true
DRSpells.known_spells.clear()
DRSpells.known_feats.clear()
end
case server_string
when /^From the (Subtlety|Finesse|Potence) tree, you know the following khri:/
if DRSpells.grabbing_known_spells
server_string
.sub(/^From the (Subtlety|Finesse|Potence) tree, you know the following khri:/, '')
.sub('.', '')
.gsub(/\(.+?\)/, '')
.split(',')
.map(&:strip)
.reject { |ability| ability.nil? || ability.empty? }
.each { |ability| DRSpells.known_spells[ability] = true }
end
server_string = nil if DRSpells.silence_known_spells_hook
when /^You have (\d+) available slots?/
DRSpells.grabbing_known_spells = false
server_string = nil if DRSpells.silence_known_spells_hook
end
server_string
end
known_spells_hook = proc do |server_string|
if DRStats.barbarian?
server_string = known_barbarian_abilities_hook.call(server_string)
elsif DRStats.thief?
server_string = known_thief_khri_hook.call(server_string)
else
server_string = known_mage_spells_hook.call(server_string)
end
server_string
end
# Unsure the historical significance of this pause
# https://github.com/rpherbig/dr-scripts/commit/04b7474b21c809d960ccb0c720808b44ae0f1ff8
pause 5
DownstreamHook.remove('active_spells_hook')
DownstreamHook.add('active_spells_hook', active_spells_hook)
DownstreamHook.remove('known_spells_hook')
DownstreamHook.add('known_spells_hook', known_spells_hook)
before_dying do
DownstreamHook.remove('active_spells_hook')
DownstreamHook.remove('known_spells_hook')
end
# Do an initial capture of known spells and feats.
DRSpells.refresh_known_spells
until script.gets.nil?
# Keep script alive while game is connected
# so that we continue to receive and parse data.
end