forked from jp-ganis/JPS
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjpconditionparser.lua
executable file
·557 lines (496 loc) · 16.9 KB
/
jpconditionparser.lua
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
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
--[[[
@module Static Spell Tables
@description
Static Tables hava a significant advantage over old-style rotations - memory usage and to some extend execution time. Instead of
creating a new Table every Update Interval the table is only created once and used over and over again. This needs some major modifications
to your rotation - you can find all relevant information on Transforming your Rotations in the PG forums.
]]--
local parser = {}
parser.testMode = false
local function fnMessageEval(message)
if message == nil then
return ""
elseif type(message) == "string" then
return message
end
end
local function fnTargetEval(target)
if target == nil then
return "target"
elseif type(target) == "function" then
return target()
else
return target
end
end
local function fnConditionEval(conditions)
if conditions == nil then
return true
elseif type(conditions) == "boolean" then
return conditions
elseif type(conditions) == "number" then
return conditions ~= 0
elseif type(conditions) == "function" then
return conditions()
else
return false
end
end
local function fnParseMacro(macro, condition, target)
if condition then
if type(macro) == "string" then
local macroSpell = macro
if string.find(macro,"%s") == nil then -- {"macro","/startattack"}
macroSpell = macro
else
macroSpell = select(3,string.find(macro,"%s(.*)")) -- {"macro","/cast Sanguinaire"}
end
if not jps.Casting then jps.Macro(macro) end -- Avoid interrupt Channeling with Macro
if jps.Debug then macrowrite(macroSpell,"|cff1eff00",target,"|cffffffff",jps.Message) end
elseif type(macro) == "table" then
for _,sequence in ipairs (macro) do
local spellname = tostring(GetSpellInfo(sequence))
if jps.canCast(spellname,target) then
local macroText = "/cast "..spellname
if not jps.Casting then jps.Macro(macroText) end -- Avoid interrupt Channeling with Macro
if jps.Debug then macrowrite(spellname,"|cff1eff00",macroTarget,"|cffffffff",jps.Message) end
end
end
else
jps.Macro("/cast " .. tostring(GetSpellInfo(macro)))
end
end
end
parser.compiledTables = {}
--[[[
@function parseStaticSpellTable
@description
Parses a static spell table and returns the spell which should be cast or nil if no spell can be cast.
Spell Tables are Tables containing other Tables:[br]
[code]
{[br]
[--]...[br]
[--]{[SPELL], [CONDITION], [TARGET]},[br]
[--]{"nested", [CONDITION], [NESTED SPELL TABLE]},[br]
[--]{[MACRO], [CONDITION], [TARGET]},[br]
[--]...[br]
}[br]
[/code]
e.g:[br]
[code]
{[br]
[--]...[br]
[--]{"Greater Heal", 'jps.hp("target") <= 0.5', "target"},[br]
[--]{{"macro", "/cast Flash Heal"}, 'jps.hp("player") < 0.6', "player"},[br]
[--]{"nested", 'jps.MultiTarget', {...}},[br]
[--]...[br]
}[br]
[/code][br]
[i]SPELL[/i]:[br]
Can either be a spell name or a spell id - id's are preferred since they will work on all client languages! This can also be
the keyword [code]"nested"[/code] - in this case the third paramter is not the target, but a nested spell table which should
be executed if the condition is true.[br]
[br]
[i]MACRO[/i]:[br]
Macro is a table (see example) which replaces the spell and has two elements: they keyword [code]"macro"[/code] and the macro itself.[br]
[br]
[i]CONDITION[/i]:[br]
The condition determines if the spell should be executed - it can either be a boolean value, a function returning a boolean value
or a string. The string must contain a valid boolean expression which will then be re-evaluated every update interval. If there is no
condition the spell will be used on cooldown[br]
[br]
[i]TARGET[/i]:[br]
A WoW unit String or player name - can also be a function which returns this string! If there is no target [code]"target"[/code]
will be used as a default value.[br]
[br]
@param hydraTable static spell table
@returns Tupel [code]spell,target[/code] if a spell should be cast, else [code]nil[/code]
]]--
function parseStaticSpellTable( hydraTable )
if jps.firstInitializingLoop == true then return nil,"target" end
if not parser.compiledTables[tostring(hydraTable)] then
jps.compileSpellTable(hydraTable)
parser.compiledTables[tostring(hydraTable)] = true
end
for _, spellTable in pairs(hydraTable) do
if type(spellTable) == "function" then spellTable = spellTable() end
local spell = nil
local conditions = nil
local target = nil
local message = fnMessageEval(spellTable[4])
if jps.Message ~= message then jps.Message = message end
if type(spellTable[1]) == "table" and spellTable[1][1] == "macro" then
fnParseMacro(spellTable[1][2], fnConditionEval(spellTable[2]), fnTargetEval(spellTable[3]))
-- Nested Table
elseif spellTable[1] == "nested" then
if fnConditionEval(spellTable[2]) then
spell, target = parseStaticSpellTable( spellTable[3] )
conditions = spell ~= nil
end
-- Default: {spell[[, condition[, target]]}
else
spell = spellTable[1]
conditions = fnConditionEval(spellTable[2])
target = fnTargetEval(spellTable[3])
end
-- Return spell if conditions are true and spell is castable.
if spell ~= nil and conditions and jps.canCast(spell,target) then
return spell,target
end
end
return nil
end
--[[
FUNCTIONS USED IN SPELL TABLE
]]
local function FN(fn,...)
local params = {...}
local params_exec = {}
return function()
for i,v in ipairs(params) do
if type(v) == "function" then
params_exec[i] = v()
else
params_exec[i] = v
end
end
return fn()(unpack(params_exec))
end
end
local function AND(...)
local functions = {...}
return function()
for _,fn in pairs(functions) do
if not fn() then if not parser.testMode then return false end end
end
return true
end
end
local function OR(...)
local functions = {...}
return function()
for _,fn in pairs(functions) do
if fn() then if not parser.testMode then return true end end
end
return false
end
end
local function NOT(fn)
return function()
return not fn()
end
end
local function LT(o1, o2)
return function()
return o1() < o2()
end
end
local function LE(o1, o2)
return function()
return o1() <= o2()
end
end
local function EQ(o1, o2)
return function()
return o1() == o2()
end
end
local function NEQ(o1, o2)
return function()
return o1() ~= o2()
end
end
local function GE(o1, o2)
return function()
return o1() >= o2()
end
end
local function GT(o1, o2)
return function()
return o1() > o2()
end
end
local function VALUE(val)
return function()
return val
end
end
local function GLOBAL_IDENTIFIER(id)
return function()
return _G[id]
end
end
local function ACCESSOR(base, key)
return function()
return base()[key]
end
end
local function ERROR(condition,msg)
return function()
print("Your rotation has an error in: \n" .. tostring(condition) .. "\n---" ..tostring(msg))
return false
end
end
--[[
PARSER:
conditions = <condition> | <condition> 'and' <conditions> | <condition> 'or' <conditions>
condition = 'not' <condition> | '(' <conditions> ')' | <comparison>
comparison = <value> <comparator> <value>
comparator = '<' | '<=' | '=' | '==' | '~=' | '>=' | '>'
value = <identifier> | STRING | NUMBER | BOOLEAN | 'nil'
identifier = IDEN | IDEN'.'<accessor> | IDEN '(' ')' | IDEN'('<parameterlist>')
accessor = IDEN | IDEN.<accessor>
parameterlist = <value> | <value> ',' <parameterlist>
]]
---[[[ Internal Parsing function - DON'T USE !!! ]]--
function parser.pop(tokens)
local t,v = unpack(tokens[1])
table.remove(tokens, 1)
return t,v
end
---[[[ Internal Parsing function - DON'T USE !!! ]]--
function parser.lookahead(tokens)
if tokens[1] then
local t,v = unpack(tokens[1])
return t,v
else
return nil
end
end
---[[[ Internal Parsing function - DON'T USE !!! ]]--
function parser.lookaheadType(tokens)
return parser.lookahead(tokens)
end
---[[[ Internal Parsing function - DON'T USE !!! ]]--
function parser.lookaheadData(tokens)
return select(2,parser.lookahead(tokens))
end
---[[[ Internal Parsing function - DON'T USE !!! conditions = <condition> | <condition> 'and' <conditions> | <condition> 'or' <conditions> ]]--
function parser.conditions(tokens, bracketLevel)
local condition1 = parser.condition(tokens, bracketLevel)
if tokens[1] then
local t, v = parser.pop(tokens)
if t == "keyword" then
if v == 'and' then
local condition2 = parser.conditions(tokens, bracketLevel)
return AND(condition1, condition2)
elseif v == 'or' then
local condition2 = parser.conditions(tokens, bracketLevel)
return OR(condition1, condition2)
else
error("Unexpected " .. tostring(t) .. ":" .. tostring(v) .. " conditions must be combined using keywords 'and' or 'or'!")
end
elseif bracketLevel > 0 then
if t == ")" then
return condition1
else
error("Unexpected " .. tostring(t) .. ":" .. tostring(v) .. " missing ')'!")
end
else
error("Unexpected " .. tostring(t) .. ":" .. tostring(v) .. " conditions must be combined using keywords 'and' or 'or'!")
end
elseif bracketLevel > 0 then
error("Unexpected " .. tostring(t) .. ":" .. tostring(v) .. " missing ')'!")
else
return condition1
end
end
---[[[ Internal Parsing function - DON'T USE !!! -- condition = 'not' <condition> | '(' <conditions> ')' | <comparison> ]]--
function parser.condition(tokens, bracketLevel)
local t, v = parser.lookahead(tokens)
if t == "keyword" and v == "not" then
parser.pop(tokens)
return NOT(parser.condition(tokens, bracketLevel))
elseif t == "(" then
parser.pop(tokens)
return parser.conditions(tokens, bracketLevel + 1)
else
return parser.comparison(tokens)
end
end
---[[[ Internal Parsing function - DON'T USE !!! -- comparison = <value> <comparator> <value> -- comparator = '<' | '<=' | '=' | '==' | '~=' | '>=' | '>' ]]--
function parser.comparison(tokens)
local value1 = parser.value(tokens)
local t = parser.lookaheadType(tokens)
if t == "<" then
local t, v = parser.pop(tokens)
local value2 = parser.value(tokens)
return LT(value1, value2)
elseif t == "<=" then
local t, v = parser.pop(tokens)
local value2 = parser.value(tokens)
return LE(value1, value2)
elseif t == "=" or t == "==" then
local t, v = parser.pop(tokens)
local value2 = parser.value(tokens)
return EQ(value1, value2)
elseif t == "~=" then
local t, v = parser.pop(tokens)
local value2 = parser.value(tokens)
return NEQ(value1, value2)
elseif t == ">=" then
local t, v = parser.pop(tokens)
local value2 = parser.value(tokens)
return GE(value1, value2)
elseif t == ">" then
local t, v = parser.pop(tokens)
local value2 = parser.value(tokens)
return GT(value1, value2)
else
return value1
end
end
---[[[ Internal Parsing function - DON'T USE !!! -- value = <identifier> | STRING | NUMBER | BOOLEAN | 'nil']]--
function parser.value(tokens)
local t, v = parser.lookahead(tokens)
if t == "number" or t == "string" then
parser.pop(tokens)
return VALUE(v)
elseif t == "keyword" and v == "true" then
parser.pop(tokens)
return VALUE(true)
elseif t == "keyword" and v == "false" then
parser.pop(tokens)
return VALUE(false)
elseif t == "keyword" and v == "nil" then
parser.pop(tokens)
return VALUE(nil)
end
return parser.identifier(tokens)
end
---[[[ Internal Parsing function - DON'T USE !!! -- identifier = IDEN | IDEN'.'<accessor> | IDEN '(' ')' | IDEN'('<parameterlist>')]]--
function parser.identifier(tokens)
local t, v = parser.pop(tokens)
if t ~= "iden" then
error("Invalid identifier '" .. tostring(v) .. "'!")
end
local symbol = GLOBAL_IDENTIFIER(v)
if parser.lookaheadType(tokens) == "." then
parser.pop(tokens)
symbol = parser.accessor(tokens, symbol)
end
if parser.lookaheadType(tokens) == "(" then
parser.pop(tokens)
if parser.lookaheadType(tokens) == ")" then
parser.pop(tokens)
return FN(symbol)
else
local parameterList = parser.parameterlist(tokens)
return FN(symbol, unpack(parameterList))
end
else
return symbol
end
end
---[[[ Internal Parsing function - DON'T USE !!! -- accessor = IDEN | IDEN.<accessor>]]--
function parser.accessor(tokens, base)
local t, v = parser.pop(tokens)
if t ~= "iden" then
error("Invalid identifier '" .. tostring(v) .. "'!")
end
local symbol = ACCESSOR(base, v)
if parser.lookaheadType(tokens) == "." then
parser.pop(tokens)
symbol = parser.accessor(tokens, symbol)
end
return symbol
end
---[[[ Internal Parsing function - DON'T USE !!! -- parameterlist = <value> | <value> ',' <parameterlist>]]--
function parser.parameterlist(tokens)
if parser.lookaheadType(tokens) == ")" then
parser.pop(tokens)
return nil
end
local value = parser.value(tokens)
local nextToken = parser.lookaheadType(tokens)
if nextToken == "," then
parser.pop(tokens)
return {value, unpack(parser.parameterlist(tokens))}
elseif nextToken == ")" then
parser.pop(tokens)
return {value}
else
error("Invalid Token " .. tostring(nextToken) .. " in parameter list!")
end
end
---[[[ Internal Parsing function - DON'T USE !!! ]]--
local function alwaysTrue() return true end
function jps.conditionParser(str)
if type(str) == "function" then return str end
if str == "onCD" then return alwaysTrue() end
local tokens = {}
local i = 0
for t,v in jps.lexer.lua(str) do
i = i+1
tokens[i] = {t,v}
end
local retOK, fn = pcall(parser.conditions, tokens, 0)
if not retOK then
return ERROR(str,fn)
end
parser.testMode = true
local retOK, err = pcall(fn)
parser.testMode = false
if not retOK then
return ERROR(str,err)
end
return fn
end
---[[[ Internal Parsing function - DON'T USE !!! ]]--
function jps.compileSpellTable(unparsedTable)
local spell = nil
local conditions = nil
local target = nil
local message = nil
for i, spellTable in pairs(unparsedTable) do
if type(spellTable) == "table" then
spell = spellTable[1]
conditions = spellTable[2]
if conditions ~= nil and type(conditions)=="string" then
spellTable[2] = jps.conditionParser(conditions)
end
if spell == "nested" then
jps.compileSpellTable(spellTable[3])
end
end
end
return unparsedTable
end
---[[[ Internal Parsing function - DON'T USE !!! ]]--
function jps.compileRaidSpellTable(unparsedTable)
local spell = nil
local conditions = nil
local target = nil
local message = nil
for i, spellTable in pairs(unparsedTable) do
if type(spellTable) == "table" then
spell = spellTable[1]
conditions = spellTable[3]
if conditions ~= nil and type(conditions)=="string" then
spellTable[3] = jps.conditionParser(conditions)
end
end
end
return unparsedTable
end
--[[[
@function jps.cachedValue
@description
This function generates a function which will store a value which might be too expensive to generate everytime. You must provide
a function which generates the value which will be called every [code]updateInterval[/code] seconds to refresh the cached value.
@param fn function which generates the value
@param updateInterval [i]Optional:[/i] max age in seconds before the value is fetched again from the function - defaults to [code]jps.UpdateInterval[/code]
@returns A function which will return the cached value
]]--
function jps.cachedValue(fn,updateInterval)
if not updateInterval then updateInterval = jps.UpdateInterval end
local value = fn()
local maxAge = GetTime() + updateInterval
return function()
if maxAge < GetTime() then
value = fn()
maxAge = GetTime() + updateInterval
end
return value
end
end