paissaheavyindustries / Triggernometry

Triggernometry is a plugin for Advanced Combat Tracker, intended to extend its built-in trigger system with a variety of different actions and configuration options.
MIT License
253 stars 48 forks source link

Multiple bug fixes and enhancements about mathparser, functions, lists, tables, etc. #98

Closed MnFeN closed 8 months ago

MnFeN commented 11 months ago

View in Chinese:

MathParser

The core of the MathParser had been mainly rewritten:

Parsing Minus Signs:

Several bugs in the parsing logic for minus signs have been fixed:

These bugs have been fixed, and the entire logic for handling +/- signs has been rewritten for simplicity. Previously, special logic was used to treat +/- in both the lexer and the parser. Now, the lexer tokenizes every +/- without discrimination, leaving simple logic in the parser to handle them correctly.

Lexer Logic

Arguments

Indices and Slices

Negative Indices

Autofill

Dictionary Variables

Expressions and Functions

Special Variables:

Expression Description
_ETprecise Provides the exact minutes in the Eorzean day.
A more accurate version of _ffxivtime (_ET).
_idx Dynamic expression. Represents the current index.
_col Dynamic expression. Represents the current column index.
_row Dynamic expression. Represents the current row index.
_col[i] Dynamic expression. Represents the value of the row index i in the current column.
_row[i] Dynamic expression. Represents the value of the column index i in the current row.
_this Dynamic expression. Represents the value in the current grid.
_key
_val
Dynamic expression. Represents the current key/value.
_clipboard Current copied text in the system clipboard.
_config[x] Returns some user configurations that would impact the results of user operations or triggers. (See Autofill form for details)

For details on dynamic expressions, refer to the actions section.

Numeric Constants:

Expression Description
semitone 2^(1/12).
The frequency ratio between 2 adjacent semitones.
cent 2^(1/1200).
The frequency ratio between 2 adjacent cents.
ETmin2sec 35/12.
The ratio between 1 ET minute and 1 real second.
δ 1E-9.
The math tolerance for comparison. (check the previous part)

Numeric Functions:

Expression Description Examples
distance() Now supports 2 sets of n-dimensional coordinates.
Behaves the same as previous versions when n = 2.
distance(0,0,0, 3,4,12) = 13
projectdistance()
projectheight()
proj
e.g. Useful for calculations involving linear AoE.
projd(0,0, pi/6, -2,0) = -1
projh(0,0, pi/6, -2,0) = 1.732...
angle(x1, y1, x2, y2) = atan2(x2-x1, y2-y1) angle(100, 100, 120, 100) = 1.57...
relangle(θ1, θ2) Considering θ1 as relative north, it returns the direction of θ2. Normalized to [-π, π). relangle(0, -pi/2) = 1.57...
roundir(θ, ±n, digits = 0)
roundvec(x, y, ±n, digits = 0)
Matches the given direction (in radians for roundir; dx/dy offsets for roundvec) to a direction in a circle divided into \|n\| segments, then returns the index of that direction.
The sign of n indicates two division modes: north as a segment point or as a boundary between two segments, as shown below.
digits specifies the number of decimal places for rounding; a negative value means no rounding.
image
e.g. Useful for calculating the direction of an entity with multiple potential spawn points. Could be combined with func:pick(index) to easily output any direction as a string from radians or coordinates without complex arctan2 and mod calculations.
roundir(-1.57,4)
= 1 (West)
roundvec(8,-6,-4)
= 3 (NE)

Numeric String Functions:

Expression Description Examples
parsedmg(hex) Converts the given hex string for damage/healing in the 0x15/0x16 ACT log lines to the corresponding decimal value.
Rule: Padleft the hex string with 0s to 8 digits as XXXXYYZZ, then convert ZZXXXX to decimal.
parsedmg(A00000) = 160
freq(note, semitones = 0) Returns the frequency (Hz) of the specified note (using scientific pitch notation, accidentals represented as #, b, x) adjusted by the semitones offset. freq(A4)
= freq(G#4, 1)
= freq(Bb4, -1)
= freq(A5, -12)
= 440
nextETms(XX:XX) nextETms(ETminutes) Provides the time (ms) remaining until the next specified Eorzean time. The current ET is 1:00:
nextETms(2:00)
= nextETms(02:00.00)
=nextETms(120)
= 175000(2 min 55 s)

String Functions:

Expression Description Examples
parsedmg Same as the numeric function. No arguments accepted. func:parsedmg:A00000
= 160
slice(slices = ":") Accepts a "slices expression" as an argument.
Returns the specified slice of the string.
func:slice(-3):01234 = 2
func:slice(1:4):01234 = 123
func:slice(::-1):01234 = 43210
func:slice("1,3:"):01234 = 134
pick(index, separator = ",") Separates the given string by the specified separator.
Returns a substring based on the index, starting from 0.
Also supports negative indices.
func:pick(3):north,west,south,east
= east
func:pick(-1,", "):1, 22, 3, 44, 5
= 5
contain(str)
startwith(str)
endwith(str)
equal(str)
Returns 1 or 0. func:contain(23):1234 = 1
func:endwith(23):1234 = 0
func:equal(23):1234 = 0
ifcontain(str, t, f)
ifstartwith(str, t, f)
ifendwith(str, t, f)
ifequal(str, t, f)
Similar to if(). func:ifcontain(23, a, b):1234 = a
func:ifendwith(23, a, b):1234 = b
func:ifequal(23, a, b):1234 = b
indicesof(str, joiner = ",", slices = ":") Searches for all indices in the specified slices of the string, then joins them. func:indicesof(a):abcbabcba = 0,4,8
func:indicesof(a, "-", ::-1):abcbabcba = 8-4-0
match(str):regex Returns 1 or 0.
Note: regex should not contain { }.
{ should be escaped with full-width or __LB__;
} should be escaped with full-width or __RB__;
must be escaped with __FLB__ __FRB__.
Same regex rules apply to the next two functions.
func:match(404D):404[B-D] = 1
func:match(4000A3BF):4.{7} = 1
capture(str, group):regex Returns the captured string $groupindex or ${groupname}. If groupindex = 0, it returns the entire matched string.
If groupname isn't found or groupindex is out of range, it returns an empty string.
Adheres to the previously mentioned regex rules.
func:capture(Player NameGilgamesh, server):.+ .+(?<server>[A-Z].+)
= Gilgamesh
ifmatch(str, t, f):regex Similar to if(). func:ifmatch(404D, a, b):404[B-D] = a
replace(oldStr, newStr = "", isLooped = false) Replaces one string with another in the specified string. func:replace(" "):1 2 3
= 123
func:replace(aa,a):aaaaaa
= aaa
func:replace(aa,a,true):aaaaaa
= a
repeat(times, joiner = "") Repeats the string the specified number of times. func:repeat(3):a = aaa
func:repeat(3, +):1 = 1+1+1
padleft
padright
trim
trimleft
trimright
These functions now accept character arguments either as the character itself or its charcode.
0-9 are interpreted as characters rather than charcodes since ASCII 0-9 control characters are rarely used here.
Numbers ≥ 10 are seen as charcodes.
No need for the 5-digit charcodes of CJK-region characters (including full-width spaces).
func:trim(48, 2, a):abcd0320
= bcd03
func:padleft(0,8):1ABCD
= 0001ABCD

List Variables:

Expression Description Examples
${?lvar:...} Builds a temporary list directly from the expression split by ,, and uses any properties on it to return a string.
Uses the same splitting rule as for splitting arguments.
Building a variable might be slightly slower, but it provides a way to combine multiple actions with conditions into a single action.
${?lvar:a, b, c, d, e.indexof(c)} = 3
${?lvar:a, b, "c,c", d, e[3]} = c,c
sum(slices = ":") Returns the sum of the values in (the slices of) the list.
Only values that can be parsed into the double format are summed.
${lvar:test.sum} = 45
${lvar:test.sum(1:5)} = 10
count(str, slices = ":") Returns the number of times the string appears in (the slices of) the list. ${lvar:test.count(3)} = 1
${lvar:test.count(a)} = 0
join(joiner = ",", slices = ":") Joins (the slices of) the list using the specified joiner. ${lvar:test.join} = 1,2,3,4,5,6,7,8,9
${lvar:test.join(" ",5::-1)}
= 5 4 3 2 1
randjoin(joiner = ",", slices = ":") Similar to join(), but the selected elements are shuffled before being joined.. ${lvar:test.randjoin}
= 4,9,2,3,5,7,8,1,6
(random example)
contain(str, slices = ":") Returns 1 if (the slices of) the list contains the given string, otherwise 0. ...contain(3) = 1
...contain(3, 4:) = 0
ifcontain(str, trueExpe, falseExpr) Similar to if(). If the list contains the string, returns trueExpr; otherwise, returns falseExpr. ...ifcontain(3, found, missing) = found
...ifcontain(a, found, missing) = missing
indicesof(str, joiner = ",", slices = ":") Searches for all occurrences of the string in the given slices of the list and joins the indices into a string. Similar to the string function.
max(type = "n", slices = ":")
min(type = "n", slices = ":")
Returns the extremum value in (the slices of) the list, depending on the type: n for numeric, h for hex, s for string. ${lvar:test.max} = 9
...min(n, 3:) = 3

Table Variables:

11 21 31 41
12 22 32 42
13 23 33 43
14 24 34 44
Expression Description Examples
${?tvar:...} Builds a temporary table directly from the expression split by , and \|.
Similar to ${?lvar:}.
${?tvar: a,b | c,d [2][2]} = d
tvardl: ptvardl: Double-based lookup similar to tvarrl:/tvarcl:.
Returns the value identified by the column and row headers.
${tvardl:test[41][13]} = 43
sum(colSlices = ":", rowSlices = ":") Returns the sum of the values in (the sliced rows and columns of) the table.
Only values that can be parsed into the double format are summed.
${lvar:test.sum} = 440
...sum(1, :)
= 11 + 12 + 13 + 14
= 50
count(str, colSlices = ":", rowSlices = ":") Returns the number of times the string appears in (the sliced rows and columns of) the table. ${lvar:test.count(33)} = 1
${lvar:test.count(1)} = 0
hjoin(joiner1 = ",", joiner2 = "⏎", colSlices = ":", rowSlices = ":") Horizontally joins the table using the specified joiners. ...hjoin(",", ",", 1:3, 3:)
= 13,23,14,24
${tvar:test.hjoin} =
11,21,31,41
12,22,32,42
13,23,33,43
14,24,34,44
vjoin(joiner1 = ",", joiner2 = "⏎", colSlices = ":", rowSlices = ":") Vertically joins the table using the specified joiners. ${tvar:test.vjoin} =
11,12,13,14
21,22,23,24
31,32,33,34
41,42,43,44
hlookup(str, rowIndex, colSlices = ":") Searches for the string in the given row and returns the column index.
If not found, returns 0.
...hlookup(13,3) = 1
...hlookup(13,3,2:) = 0
vlookup(str, colIndex, rowSlices = ":") Searches for the string in the given column and returns the row index.
If not found, returns 0.
...vlookup(13,1) = 3
max(type = "n", colSlices = ":", rowSlices = ":")
min(type = "n", colSlices = ":", rowSlices = ":")
Same as the list method. (omitted)

Dict Variables:

Expression Description Examples
${?dvar:...} Builds a temporary dict directly from the expression split by =, ..
Similar to ${?lvar:}.
${?dvar: 7CD2=in, 7CD6=out, 7CD7=spread [7CD2] } = in
sumkeys() sum() Sum all the keys/values that can be parsed into double format. ${dvar:test.sumkey} = 0
${dvar:test.sum} = 12
count(value) Returns the count of the given value in the dict. ...countvalue(3) = 3
dvar: edvar:
pdvar: epdvar:
e (existing) / p (persist). Similar to other variables. ${epdvar:dictname}
${dvar:test[e]} = 3
length / size The number of keys in the dict. ${dvar:test.size} = 5
ekey(key) evalue(value) Check if the key/value exists in the dict (returns 0/1). ${dvar:test.ekey(a)} = 1
${dvar:test.evalue(4)} = 0
ifekey(key, t, f)
ifevalue(value, t, f)
Similar to string functions (returns string t/f). ...ifekey(a, found, missing) = found
keyof(value) Reverse lookup by value. Returns the first found key or an empty string if not found. ${dvar:test.keyof(1)} = a
${dvar:test.keyof(4)} = ``
keysof(value, joiner = ",") Lookup all keys matching the given value and join them with the joiner. ...keyof(3) = c,d,e
joinkeys(joiner = ",")
joinvalues(joiner = ",")
joinall(kvjoiner = "=", pairjoiner = ",")
Combine the keys/values/both using the joiners. ...joinkeys(-) = a-b-c-d-e
...joinall = a=1,b=2,c=3,d=3,e=3
max(type = "n")
min(type = "n")
maxkey(type = "n")
minkey(type = "n")
Same as the list methods. (omitted)

Job Properties:

Entity Properties:

Abbreviations to Enhance User Experience:

Full Abbrev.
${numeric:...} ${n:...}
${string:...} ${s:...}
${func:...} ${f:...}
${exvar:...} ${ev:} ${el:} ${et:} ${ed:}
${(p)var:...} ${(p)lvar:...}
${(p)tvar:...} ${(p)dvar:...}
${(p)v:} ${(p)l:}
${(p)t:} ${(p)d:}
${?lvar:...} ${?tvar:...} ${?dvar:...} ${?l:} ${?t:} ${?d:}
${_loopiterator} ${_i}
${_ffxiventity[...].prop} ${_entity[...].prop}
${_ffxivparty[...].prop} ${_party[...].prop}
${_ffxivplayer} ${_me}
${_ffxiventity[${_ffxivplayer}].prop} ${_me.prop}
indexof() i()
_ffxivtime _ET
pi π
distance() d()
projectdistance() projd()
projectheight() projh()
angle() θ()
relangle() relθ()
(table) .width .w
(table) .height .h
(table) .hlookup() .hl()
(table) .vlookup() .vl()
(entity) .heading .h

Actions

Fixed a bug in the list method Insert and table variable Resize:

Fixed a bug in the list method Set:

Fixed a bug in the list method Split:

Fixed a bug about the persistent button:

Add dynamic expression support for the Set action of lists, tables, and dictionaries:

Updated PopFirst / PopLast actions for list variables:

Added PopToList actions (set/insert):

Added basic actions for dict variables:

Added Build action for list/table/dict variables:

Added Filter action for list/table/dict variables:

Added SetLine / InsertLine action for table variables:

Added RemoveLine action for table variables:

Added SortByKeys for lists, and SortLine for tables:

Unset all types of variables matching the regex (in the scalar variable tabpage)

Copy the Value of the Variable/Expression to the Clipboard (in the Scalar Variable Tabpage)

Introduced the folder action "Cancel All Actions of Triggers Within Folder"

Refined Actions List Order

Adjusted Tab Index in Action Form

Expression Textbox

Trigger Form / Action Viewer

Log Form

Variable State Viewer / Editor

Main UI:

Others

Bug Fixes:

Enhancements:

Different Behaviours

Besides the bug fuxes and UI adjustments, the following behaviors are different comparing with the old version:

Known Issues

System.ArgumentOutOfRangeException - Index out of range.
   in System.Collections.ArrayList.get_Item(Int32 index)
   in System.Windows.Forms.DataGridViewSelectedRowCollection.get_Item(Int32 index)
   in Triggernometry.CustomControls.ActionViewer.btnEditAction_Click(Object sender, EventArgs e)
   in Triggernometry.CustomControls.ActionViewer.dgvActions_CellDoubleClick(Object sender, DataGridViewCellEventArgs e)
   in System.Windows.Forms.DataGridView.OnCellDoubleClick(DataGridViewCellEventArgs e)
   in System.Windows.Forms.DataGridView.OnDoubleClick(EventArgs e)
paissaheavyindustries commented 10 months ago

Thanks for the PR! Sorry for the delay; I've been pretty busy and there's a lot to go over here, but I'll get to reviewing it.

MnFeN commented 10 months ago

Thanks for the PR! Sorry for the delay; I've been pretty busy and there's a lot to go over here, but I'll get to reviewing it.

There are still some changes I want to combine with it and it will take some time. I will send another commit so maybe it's better to review it later. Thank you for your work!

paissaheavyindustries commented 9 months ago

I've been reviewing it but it's so many updates at once, so it'll still take some time!

MnFeN commented 9 months ago

I've been reviewing it but it's so many updates at once, so it'll still take some time!

Thank you for taking the time and effort to review this! I understand there are a lot of edited codes, so please feel free to reply if you have any questions or concerns. I'm happy to provide further explanations or clarifications.

I also plan to push a final small update soon, which is primarily aimed at adding more built-in help text for each variable operation in the ActionForm, since some of them are too complicated to describe with only a line of instruction in the combobox. Aside from that, there will be some minor UI tweaks and bug fixes such as in the Triggernometry Discord suggestions.

MnFeN commented 8 months ago

Here is the summary of the latest edit:

paissaheavyindustries commented 8 months ago

I went through it and it looks good honestly, just need a lot of testing :D