├── LICENSE.md ├── README.md ├── action_parse.lua ├── display.lua ├── file_handle.lua ├── parse.lua ├── parse_info.xlsx ├── report.lua ├── retrieval.lua └── utility.lua /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2016-2023 Flippant 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 7 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse v1.63 2 | An FFXI Parser Addon for Windower. This addon parses both offensive and defensive data, stores WS/JAs/spells data by individual spell, tracks additional information like multihit rates, and provides the ability to export/import data in XML format and log individually recorded data. 3 | 4 | ### Settings 5 | For now, refer to the Excel Spreadsheet for a brief description of Parse settings. Note that at some point, the settings will be getting a bit of a face lift... 6 | 7 | ### Commands 8 | 9 | `//parse pause` 10 | Pauses the parser. 11 | 12 | `//parse reset` 13 | Resets the currently stored data. 14 | 15 | `//parse report [stat] [ability name] [chatmode]` Reports stat to party. 16 | If [stat] not provided, will report damage. Valid stats include, but aren't limited to: 17 | * damage (% reported is player's portion of total damage) 18 | * melee | ranged | spike | sc | add (% reported is hit rate) 19 | * crit | r_crit 20 | * multi (reports % and count of double attacks, triple attacks, etc., but does not distinguish between OAX, nor accommodates for killing blows; i.e. if you kill in one hit, it will only ever record as 1-hit) 21 | * block | parry | evade | retrate (Retaliation) (% reported is based on action hierarchy; for example, block % excludes evades and parry % excludes both evades and non-engaged hits taken) 22 | * ws | ja | spell | mb | enfeeb (reports averages for total category, and each individual spell; also reports hit rate % for total ws/ja) 23 | * ws_miss | ja_miss | enfeeb_miss (reports counts for individual spell) 24 | Note that Reprisal, Counters, and Retaliation damage all appear together under spike damage. However, retaliation rate is reported under retrate. 25 | 26 | If [ability name] is provided when reporting WS, JA, spell, MB, or enfeeb, it will only report that particular ability. **It must be an exact match to the database, and is thus case sensitive.** Replace all spaces with an underscore and omit all apostrophes and other special characters. For example: 27 | * `//parse report ws Rudras_Storm` 28 | * `//parse report mb Death l2` 29 | 30 | If [chatmode] not provided, will print to personal chatlog. Valid chatmodes include: 31 | * p: party 32 | * s: say 33 | * l: linkshell 34 | * l2: linkshell2 35 | * t [player name]: tell 36 | * echo: echo to chat (only you can see this) 37 | 38 | `//parse show (melee|ranged|magic|defense)` 39 | Toggles visibility of each display box. Note that while data is still parsed regardless of visibility, these displays are not updated unless visible, saving resources. 40 | 41 | `//parse filter (add|remove) [substring]` 42 | Adds/removes substring to monster filter list. Substring is not case sensitive; replace all spaces with underscores and omit special characters. If substring begins with '!' it will exclude any monsters with that substring. If substring begins with '^' it will only include exact matches. For example: 43 | * `schah` will include Schah and all of his minions. 44 | * `!schah` will exclude Schah and all of his minions (Schah's Bhata, etc.). 45 | * `^schah` will only include Schah, and not his minions. 46 | * `!^schah` will exclude only Schah. 47 | 48 | `//parse filter clear` 49 | Clears filter list. 50 | 51 | `//parse list (mobs|players)` 52 | Lists mobs and players that are found in database. 53 | 54 | `//parse rename [player/monster name] [new name]` 55 | Renames a player or monster to a new name for all future, incoming data. To rename again, always use the original name. Replace any spaces with _ and omit all special characters. 56 | 57 | `//parse (export|import) [file name]` 58 | Exports/imports data to/from the "parse/data/export" folder. Imported data is merged with any current in-game data. If file name is taken, it will append os.clock. NOTE: Exported data will be saved according to any current filters. 59 | 60 | `//parse autoexport [file name]` 61 | Automatically exports database every 500 actions. This interval can be changed in settings under autoexport_interval. Use command again with no file name, or 'off' to turn it off. 62 | 63 | `//parse log` 64 | Toggles logging. 65 | 66 | `//parse interval [number]` 67 | Changes the interval rate at which the display boxes are updated in seconds. Default interval is in settings. 68 | 69 | ### Display 70 | 71 | Up to four draggable, customizable UIs can appear on screen, "melee", "ranged", "magic", and "defense." The visibility of each one can be toggled in-game and the default visibility and visual appearance can be changed in the settings file. 72 | 73 | Despite their designated titles, attributes displayed on each of these are completely customizable from the settings. Any of recorded stats can be added to or removed from each of the displays, as well as the data type (total damage, average damage, percentage (how percentage is calculated depends on the stat), and tally). 74 | 75 | ### Filtering 76 | 77 | Mob filters can be added/removed easily to filter data. Filters are used as substrings (not exact results) and are not case-sensitive. Multiple filters may be added. Note that special characters in monster names are stored differently: spaces are replaced with underscores and apostrophes are removed entirely. When in doubt, you can also use the list command to list all the monsters that have been registered in the current data set. 78 | 79 | If substring begins with '!' it will exclude any monsters with that substring. If substring begins with '^' it will only include exact matches. For example: 80 | 81 | * `schah` will include Schah and all of his minions. 82 | * `!schah` will exclude Schah and all of his minions (Schah's Bhata, etc.). 83 | * `^schah` will only include Schah, and not his minions. 84 | * `!^schah` will exclude only Schah. 85 | 86 | ### Report 87 | 88 | The report feature can be used to report either to a chat mode (including tells) or personal chatlog. All stats can be reported, including damage. When using this feature to report weaponskills, job abilities, and spells, results are shown both for the entire category and each individual ability/spell. 89 | 90 | ### Renaming 91 | 92 | Both players and monsters can be "renamed" for new, incoming data. This can help you distinguish between multiple instanced enemies of the same name, or assist you when testing and comparing various situations. 93 | 94 | Always rename using the original name. For example, `//parse rename Kirin Kirin2; //parse rename Kirin Kirin3.` Also exclude any apostrophes and replace any spaces with underscore. Do not include Special Indexing (read more below). 95 | 96 | ### Special Indexing 97 | 98 | Special features have been added specifically to assist parsing block rate and parry rate. Buff detection requires the addon to be loaded before you apply the buff. These can be enabled/disabled through settings. 99 | 100 | * Indexing by subweapon/shield, represented by the first three letters. 101 | * Indexing by Reprisal, represented with "R" when on. 102 | * Indexing by Palisade, represented with "P" when on. 103 | * Indexing by Battuta, represented with "B" when on. 104 | 105 | Note these features only work on the player's own character, and are appended after a name is potentially renamed. 106 | 107 | ### Export/Import 108 | 109 | Parses can be exported to and imported from XML format so that you can view or continue a parse at any time. When importing, the imported data is merged with any current data. 110 | 111 | Data is saved according to mob filters, if there are any currently being used. Keep this in mind when exporting a parse, as any non-included monsters will not be saved. 112 | 113 | You can also toggle the autoexport command with //autoexport myFile. By using this command, the addon with automatically export after every 500 recordable actions. This can be useful when testing results while away from the computer for extended periods. 114 | 115 | ### Logging 116 | As opposed to export, which saves the in-game database to an XML file, logging records each individual action's parameters to a file. For example, export will only save the total damage and total count of Flippant's Rudra's Storms against Wild Rabbits; but logging will save how much *each* Rudra's Storm did. 117 | 118 | Logging data is automatic, as long as the player being recorded is listed in the logger option of your settings. This is case-sensitive, and wildcard (\*) at the end of a name is permitted (this allows defensive data to be recorded easily, despite changes in name due to special indexing). 119 | 120 | Data is saved to /parse/data/log, in folders designated according to the *recording* player, to a file named after the recorded player, monster name, and stat (melee, ws, etc.). Data is *not* logged if it does not have a damage parameter—for example, it will not record parries, enfeebles, misses, etc. Category sections (ws, ja, spell, mb) will save the spell name next to the damage. 121 | 122 | If data has not been saved to that file since the last time Parse was loaded, it will first append time and date for quick reference. 123 | -------------------------------------------------------------------------------- /action_parse.lua: -------------------------------------------------------------------------------- 1 | --[[ TO DO 2 | 3 | -- Weird SC bug (also occurs in SB) 288,289 4 | -- Need to count strikes that are blinked/parried by mob towards multihit_count 5 | -- Need to count kicks 6 | 7 | ]] 8 | spike_effect_valid = {true,false,false,false,false,false,false,false,false,false,false,false,false,false,false} 9 | add_effect_valid = {true,true,true,true,false,false,false,false,false,false,true,false,true,false,false} 10 | skillchain_messages = T{288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,385,386,387,388,389,390,391,392,393,394,395,396,397,398,732,767,768,769,770} 11 | add_effect_messages = T{161,163,229} 12 | skillchain_names = { 13 | [288] = "Skillchain: Light", 14 | [289] = "Skillchain: Darkness", 15 | [290] = "Skillchain: Gravitation", 16 | [291] = "Skillchain: Fragmentation", 17 | [292] = "Skillchain: Distortion", 18 | [293] = "Skillchain: Fusion", 19 | [294] = "Skillchain: Compression", 20 | [295] = "Skillchain: Liquefaction", 21 | [296] = "Skillchain: Induration", 22 | [297] = "Skillchain: Reverberation", 23 | [298] = "Skillchain: Transfixion", 24 | [299] = "Skillchain: Scission", 25 | [300] = "Skillchain: Detonation", 26 | [301] = "Skillchain: Impaction", 27 | [302] = "Skillchain: Cosmic Elucidation", 28 | [385] = "Skillchain: Light", 29 | [386] = "Skillchain: Darkness", 30 | [387] = "Skillchain: Gravitation", 31 | [388] = "Skillchain: Fragmentation", 32 | [389] = "Skillchain: Distortion", 33 | [390] = "Skillchain: Fusion", 34 | [391] = "Skillchain: Compression", 35 | [392] = "Skillchain: Liquefaction", 36 | [393] = "Skillchain: Induration", 37 | [394] = "Skillchain: Reverberation", 38 | [395] = "Skillchain: Transfixion", 39 | [396] = "Skillchain: Scission", 40 | [397] = "Skillchain: Detonation", 41 | [398] = "Skillchain: Impaction", 42 | [732] = "Skillchain: Universal Enlightenment", 43 | [767] = "Skillchain: Radiance", 44 | [768] = "Skillchain: Umbra", 45 | [769] = "Skillchain: Radiance", 46 | [770] = "Skillchain: Umbra", 47 | } 48 | local defense_action_messages = { 49 | [1] = 'hit', 50 | [67] = 'hit', --crit 51 | [106] = 'intimidate', 52 | [15] = 'evade', [282] = 'evade', 53 | [373] = 'absorb', 54 | [536] = 'retaliate', [535] = 'retaliate' 55 | } 56 | local offense_action_messages = { 57 | [1] = 'melee', 58 | [67] = 'crit', 59 | [15] = 'miss', [63] = 'miss', 60 | [352] = 'ranged', [576] = 'ranged', [577] = 'ranged', 61 | [353] = 'r_crit', 62 | [354] = 'r_miss', 63 | [185] = 'ws', [197] = 'ws', [187] = 'ws', 64 | [188] = 'ws_miss', 65 | [2] = 'spell', [227] = 'spell', 66 | [252] = 'mb', [265] = 'mb', [274] = 'mb', [379] = 'mb', [747] = 'mb', [748] = 'mb', 67 | [82] = 'enfeeb', [236] = 'enfeeb', [754] = 'enfeeb', [755] = 'enfeeb', 68 | [85] = 'enfeeb_miss', [284] = 'enfeeb_miss', [653] = 'enfeeb_miss', [654] = 'enfeeb_miss', [655] = 'enfeeb_miss', [656] = 'enfeeb_miss', 69 | [110] = 'ja', [317] = 'ja', [522] = 'ja', [802] = 'ja', 70 | [158] = 'ja_miss', [324] = 'ja_miss', 71 | [157] = 'Barrage', 72 | [77] = 'Sange', 73 | [264] = 'aoe' 74 | } 75 | 76 | function parse_action_packet(act) 77 | if pause then return end 78 | 79 | local actionpacket = ActionPacket.new(act) 80 | local player = windower.ffxi.get_player() 81 | local NPC_name, PC_name 82 | 83 | act.actor = player_info(act.actor_id) 84 | if not act.actor then 85 | return 86 | end 87 | 88 | local multihit_count,multihit_count2 = nil 89 | local aoe_type = 'ws' 90 | 91 | for i,targ in pairs(act.targets) do 92 | multihit_count,multihit_count2 = 0,0 93 | for n,m in pairs(targ.actions) do 94 | if m.message ~= 0 and res.action_messages[m.message] ~= nil then 95 | target = player_info(targ.id) 96 | -- if mob is actor, record defensive data 97 | if act.actor.type == 'mob' and settings.record[target.type] then 98 | NPC_name = nickname(act.actor.name:gsub(" ","_"):gsub("'","")) 99 | PC_name = construct_PC_name(target) 100 | if target.name == player.name then 101 | if settings.index_shield and get_shield() then 102 | PC_name = PC_name:sub(1, 6)..'-'..get_shield():sub(1, 3)..'' 103 | end 104 | if settings.index_reprisal and buffs.Reprisal then PC_name = PC_name .. 'R' end 105 | if settings.index_palisade and buffs.Palisade then PC_name = PC_name .. 'P' end 106 | if settings.index_battuta and buffs.Battuta then PC_name = PC_name .. 'B' end 107 | end 108 | 109 | local action = defense_action_messages[m.message] 110 | local engaged = (target.status==1) and true or false 111 | 112 | if m.reaction == 12 and act.category == 1 then --block 113 | register_data(NPC_name,PC_name,'block',m.param) 114 | if engaged then 115 | register_data(NPC_name,PC_name,'nonparry') 116 | end 117 | elseif m.reaction == 11 and act.category == 1 then --parry 118 | register_data(NPC_name,PC_name,'parry') 119 | elseif action == 'hit' then --hit or crit 120 | register_data(NPC_name,PC_name,action,m.param) 121 | if engaged then 122 | register_data(NPC_name,PC_name,'nonparry') 123 | if buffs.Retaliation and not m.has_spike_effect then 124 | register_data(NPC_name,PC_name,'nonret') 125 | end 126 | end 127 | if act.category == 1 then 128 | register_data(NPC_name,PC_name,'nonblock',m.param) 129 | end 130 | elseif T{'intimidate','evade'}:contains(action) then --intimidate 131 | register_data(NPC_name,PC_name,action) 132 | end 133 | 134 | if action == 'absorb' then --absorb (can happen during block) 135 | register_data(NPC_name,PC_name,'absorb',m.param) 136 | end 137 | 138 | if m.has_spike_effect then --offensive data (when player has Reprisal or counters, etc.) spike_effect_effect = 2 for counters 139 | local spike_action = defense_action_messages[m.spike_effect_message] 140 | if m.spike_effect_param then 141 | register_data(NPC_name,PC_name,'spike',m.spike_effect_param) 142 | end 143 | if spike_action == 'retaliate' then 144 | register_data(NPC_name,PC_name,'retrate') 145 | end 146 | end 147 | 148 | -- if player is actor, record offensive data 149 | elseif target.type == 'mob' and settings.record[act.actor.type] then 150 | NPC_name = nickname(target.name:gsub(" ","_"):gsub("'","")) 151 | PC_name = construct_PC_name(act.actor) 152 | 153 | local action = offense_action_messages[m.message] 154 | 155 | if T{'melee','crit','miss'}:contains(action) then 156 | register_data(NPC_name,PC_name,action,m.param) 157 | if m.animation==0 then --main hand 158 | multihit_count = multihit_count + 1 159 | elseif m.animation==1 then --off hand 160 | multihit_count2 = multihit_count2 + 1 161 | end 162 | elseif T{'ranged','r_crit','r_miss'}:contains(action) then 163 | register_data(NPC_name,PC_name,action,m.param) 164 | elseif T{'ws','ws_miss'}:contains(action) then 165 | register_data(NPC_name,PC_name,action,m.param,'ws',act.param) 166 | aoe_type = 'ws' 167 | elseif T{'spell','mb'}:contains(action) then 168 | register_data(NPC_name,PC_name,action,m.param,'spell',act.param) 169 | aoe_type = 'spell' 170 | elseif T{'enfeeb','enfeeb_miss'}:contains(action) then 171 | register_data(NPC_name,PC_name,action,nil,'spell',act.param) 172 | elseif T{'ja','ja_miss'}:contains(action) then 173 | register_data(NPC_name,PC_name,action,m.param,'ja',act.param) 174 | aoe_type = 'ja' 175 | elseif T{'Barrage','Sange'}:contains(action) then 176 | register_data(NPC_name,PC_name,'ja',m.param,'ja',action) 177 | elseif action == 'aoe' then 178 | register_data(NPC_name,PC_name,aoe_type,m.param,aoe_type,act.param) 179 | end 180 | 181 | if m.has_add_effect and m.add_effect_message ~= 0 and add_effect_valid[act.category] then 182 | if skillchain_messages:contains(m.add_effect_message) then 183 | PC_name = "SC-"..PC_name:sub(1, 3) 184 | register_data(NPC_name,PC_name,'sc',m.add_effect_param) 185 | if skillchain_names and skillchain_names[m.add_effect_message] then debug('sc ('..PC_name..') '..skillchain_names[m.add_effect_message]..' '..m.add_effect_param) end 186 | elseif add_effect_messages:contains(m.add_effect_message) and m.add_effect_param > 0 then 187 | register_data(NPC_name,PC_name,'add',m.add_effect_param) 188 | end 189 | end 190 | 191 | if m.has_spike_effect and m.spike_effect_message ~= 0 and spike_effect_valid[act.category] then --defensive data (when mob counters, has blazespikes, etc.) // Can you block a counter, and can I tell that you blocked a counter? 192 | --debug('Monster spikes: Effect: '..m.spike_effect_effect) 193 | end 194 | end 195 | end 196 | end 197 | end 198 | 199 | if multihit_count and multihit_count > 0 then 200 | register_data(NPC_name,PC_name,tostring(multihit_count)) 201 | end 202 | if multihit_count2 and multihit_count2 > 0 then 203 | register_data(NPC_name,PC_name,tostring(multihit_count2)) 204 | end 205 | 206 | --Handle auto-export 207 | if PC_name and autoexport and autoexport_tracker == autoexport_interval then 208 | export_parse(autoexport) 209 | end 210 | autoexport_tracker = (autoexport_tracker % autoexport_interval) + 1 211 | end 212 | 213 | --------------------------------------------------------- 214 | -- Function credit to Suji 215 | --------------------------------------------------------- 216 | function construct_PC_name(PC) 217 | local name = PC.name 218 | local result = '' 219 | if PC.owner then 220 | if string.len(name) > 7 then 221 | result = string.sub(name, 1, 6) 222 | else 223 | result = name 224 | end 225 | result = result..'-'..string.sub(nickname(PC.owner.name), 1, 4)..'' 226 | else 227 | result = nickname(name) 228 | end 229 | return string.sub(result,1,10) 230 | end 231 | 232 | function nickname(player_name) 233 | if renames[player_name] then 234 | return renames[player_name] 235 | else 236 | return player_name 237 | end 238 | end 239 | 240 | function init_mob_player_table(mob_name,player_name) 241 | if not database[mob_name] then 242 | database[mob_name] = {} 243 | end 244 | database[mob_name][player_name] = {} 245 | end 246 | 247 | function register_data(NPC_name,PC_name,stat,val,spell_type,spell_id) 248 | if not database[NPC_name] or not database[NPC_name][PC_name] then 249 | init_mob_player_table(NPC_name,PC_name) 250 | end 251 | 252 | local spell_name = nil 253 | local stat_type = get_stat_type(stat) or 'unknown' 254 | 255 | local mob_player_table = database[NPC_name][PC_name] 256 | if not mob_player_table[stat_type] then 257 | mob_player_table[stat_type] = {} 258 | end 259 | 260 | if not mob_player_table[stat_type][stat] then 261 | mob_player_table[stat_type][stat] = {} 262 | end 263 | 264 | if stat_type == "category" then --handle WS, spells, and JA 265 | if type(spell_id) == 'number' then 266 | if spell_type == "ws" and res.weapon_skills[spell_id] then spell_name = res.weapon_skills[spell_id].english 267 | elseif spell_type == "ja" and res.job_abilities[spell_id] then spell_name = res.job_abilities[spell_id].english 268 | elseif spell_type == "spell" and res.spells[spell_id] then spell_name = res.spells[spell_id].english 269 | else spell_name = "unknown" end 270 | elseif type(spell_id) == 'string' then spell_name = spell_id end 271 | 272 | if not spell_name then 273 | message('There was an error recording that action...') 274 | return 275 | end 276 | 277 | spell_name = spell_name:gsub(" ","_"):gsub("'",""):gsub(":","") 278 | 279 | if not mob_player_table[stat_type][stat][spell_name] then 280 | mob_player_table[stat_type][stat][spell_name] = {['tally'] = 0} 281 | end 282 | 283 | mob_player_table[stat_type][stat][spell_name].tally = mob_player_table[stat_type][stat][spell_name].tally + 1 284 | 285 | if val then 286 | if not mob_player_table[stat_type][stat][spell_name].damage then 287 | mob_player_table[stat_type][stat][spell_name].damage = val 288 | else 289 | mob_player_table[stat_type][stat][spell_name].damage = mob_player_table[stat_type][stat][spell_name].damage + val 290 | end 291 | 292 | if damage_types:contains(stat) then 293 | if not mob_player_table.total_damage then 294 | mob_player_table.total_damage = val 295 | else 296 | mob_player_table.total_damage = mob_player_table.total_damage + val 297 | end 298 | end 299 | end 300 | else --handle everything else 301 | if not mob_player_table[stat_type][stat].tally then 302 | mob_player_table[stat_type][stat].tally = 0 303 | end 304 | 305 | mob_player_table[stat_type][stat].tally = mob_player_table[stat_type][stat].tally + 1 306 | 307 | if val then 308 | if not mob_player_table[stat_type][stat].damage then 309 | mob_player_table[stat_type][stat].damage = val 310 | else 311 | mob_player_table[stat_type][stat].damage = mob_player_table[stat_type][stat].damage + val 312 | end 313 | 314 | if damage_types:contains(stat) then 315 | if not mob_player_table.total_damage then 316 | mob_player_table.total_damage = val 317 | else 318 | mob_player_table.total_damage = mob_player_table.total_damage + val 319 | end 320 | end 321 | end 322 | end 323 | 324 | if val and settings.logger:find(function(el) if PC_name==el or (el:endswith('*') and PC_name:startswith(tostring(el:gsub('*','')))) then return true end return false end) then 325 | log_data(PC_name,NPC_name,stat,val,spell_name) 326 | end 327 | end 328 | 329 | 330 | function get_shield() 331 | local current_equip = windower.ffxi.get_items().equipment 332 | local shield_id, shield_bag = 0,0 333 | for i,v in pairs(current_equip) do 334 | if i == 'sub' then 335 | shield_id = v 336 | elseif i=='sub_bag' then 337 | shield_bag = v 338 | end 339 | end 340 | 341 | if shield_id==0 then 342 | return nil 343 | end 344 | 345 | -- res.items[shield] 346 | shield = windower.ffxi.get_items(shield_bag,shield_id) 347 | return res.items[shield.id].english 348 | end 349 | 350 | 351 | --------------------------------------------------------- 352 | -- Function credit to Byrth 353 | --------------------------------------------------------- 354 | function player_info(id) 355 | local player_table = windower.ffxi.get_mob_by_id(id) 356 | local typ,owner 357 | 358 | if player_table == nil then 359 | return {name=nil,id=nil,type='debug',owner=nil} 360 | end 361 | 362 | for i,v in pairs(windower.ffxi.get_party()) do 363 | if type(v) == 'table' and v.mob and v.mob.id == player_table.id then 364 | if i == 'p0' then 365 | typ = 'me' 366 | elseif i:sub(1,1) == 'p' then 367 | typ = 'party' 368 | if player_table.is_npc then typ = 'trust' end 369 | else 370 | typ = 'alliance' 371 | end 372 | end 373 | end 374 | 375 | if not typ then 376 | if player_table.is_npc then 377 | if player_table.id%4096>2047 then 378 | for i,v in pairs(windower.ffxi.get_party()) do 379 | if type(v) == 'table' and v.mob and v.mob.pet_index and v.mob.pet_index == player_table.index then 380 | typ = 'pet' 381 | owner = v 382 | elseif type(v) == 'table' and v.mob and v.mob.fellow_index and v.mob.fellow_index == player_table.index then 383 | typ = 'fellow' 384 | owner = v 385 | break 386 | end 387 | end 388 | else 389 | typ = 'mob' 390 | end 391 | else 392 | typ = 'other' 393 | end 394 | end 395 | if not typ then typ = 'debug' end 396 | return {name=player_table.name,status=player_table.status,id=id,type=typ,owner=(owner or nil)} 397 | end 398 | 399 | 400 | --Copyright (c) 2013~2016, F.R 401 | --All rights reserved. 402 | 403 | --Redistribution and use in source and binary forms, with or without 404 | --modification, are permitted provided that the following conditions are met: 405 | 406 | -- * Redistributions of source code must retain the above copyright 407 | -- notice, this list of conditions and the following disclaimer. 408 | -- * Redistributions in binary form must reproduce the above copyright 409 | -- notice, this list of conditions and the following disclaimer in the 410 | -- documentation and/or other materials provided with the distribution. 411 | -- * Neither the name of nor the 412 | -- names of its contributors may be used to endorse or promote products 413 | -- derived from this software without specific prior written permission. 414 | 415 | --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 416 | --ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 417 | --WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 418 | --DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 419 | --DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 420 | --(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 421 | --LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 422 | --ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 423 | --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 424 | --SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 425 | -------------------------------------------------------------------------------- /display.lua: -------------------------------------------------------------------------------- 1 | --[[ TO DO 2 | 3 | -- Update filter header directly 4 | 5 | ]] 6 | 7 | function init_boxes() 8 | for box,__ in pairs(settings.display) do 9 | create_text(box) 10 | end 11 | end 12 | 13 | function create_text(stat_type) 14 | local t_settings = settings.display[stat_type] 15 | 16 | text_box[stat_type] = texts.new(t_settings) 17 | text_box[stat_type]:hide() 18 | update_text(text_box[stat_type]) 19 | end 20 | 21 | function update_text(stat_type) 22 | -- Don't update if box wasn't properly added, there are no settings, or it is not set to visible 23 | if not text_box[stat_type] or not settings.display[stat_type] or not settings.display[stat_type].visible or not windower.ffxi.get_info().logged_in then 24 | return 25 | end 26 | 27 | local info = {} 28 | local head = L{} 29 | local to_be_sorted = {} 30 | local sorted_players = L{} 31 | local all_damage = 0 32 | 33 | if settings.display[stat_type]["type"] == "offense" then 34 | sort_type = "damage" 35 | else 36 | sort_type = "defense" 37 | end 38 | 39 | -- add data to info table 40 | for __,player_name in pairs(get_players()) do 41 | 42 | if (settings.display and settings.display[stat_type]) then 43 | to_be_sorted[player_name] = get_player_stat_tally('parry',player_name) + get_player_stat_tally('hit',player_name) + get_player_stat_tally('evade',player_name) 44 | info[player_name] = '\\cs('..label_colors('player')..')'..string.format('%-13s',player_name..' ')..'\\cr' 45 | for stat in settings.display[stat_type].order:it() do 46 | if settings.display[stat_type].data_types[stat] then 47 | local d = {} 48 | for report_type,__ in pairs(settings.display[stat_type].data_types[stat]) do 49 | if report_type=="total" then 50 | local total = get_player_damage(player_name) -- getting player's damage 51 | d[report_type] = total or "--" 52 | all_damage = all_damage + total 53 | if sort_type=='damage' then to_be_sorted[player_name] = total end 54 | elseif report_type=="total-percent" then 55 | d[report_type] = get_player_stat_percent(stat,player_name) or "--" 56 | --d[report_type] = (total or get_player_damage(player_name)) / get_player_damage() or "--" 57 | elseif report_type=="avg" then 58 | d[report_type] = get_player_stat_avg(stat,player_name) or "--" 59 | elseif report_type=="percent" then 60 | d[report_type] = get_player_stat_percent(stat,player_name) or "--" 61 | elseif report_type=="tally" then 62 | d[report_type] = get_player_stat_tally(stat,player_name) or "--" 63 | elseif report_type=="damage" then 64 | d[report_type] = get_player_stat_damage(player_name) or "--" 65 | else 66 | d[report_type] = "--" 67 | end 68 | end 69 | info[player_name] = info[player_name] .. (format_display_data(d)) 70 | end 71 | end 72 | end 73 | end 74 | 75 | -- sort players 76 | for i=1,settings.display[stat_type].max,+1 do 77 | p_name = nil 78 | top_result = 0 79 | for player_name,sort_num in pairs(to_be_sorted) do 80 | if sort_num > top_result and not sorted_players:contains('${'..player_name..'}') then 81 | top_result = sort_num 82 | p_name = player_name 83 | end 84 | end 85 | if p_name then sorted_players:append('${'..p_name..'}') end 86 | end 87 | 88 | head:append('[ ${title} ] ${filters} ${pause}') 89 | info['title'] = stat_type 90 | 91 | info['filters'] = update_filters() 92 | 93 | if pause then 94 | info['pause'] = "- PARSE PAUSED -" 95 | end 96 | 97 | head:append('${header}') 98 | info['header'] = format_display_head(stat_type) 99 | 100 | if sorted_players:length() == 0 then 101 | head:append('No data found') 102 | end 103 | 104 | if text_box[stat_type] then 105 | text_box[stat_type]:clear() 106 | text_box[stat_type]:append(head:concat('\n')) 107 | text_box[stat_type]:append('\n') 108 | text_box[stat_type]:append(sorted_players:concat('\n')) 109 | text_box[stat_type]:update(info) 110 | 111 | if settings.display[stat_type].visible then 112 | text_box[stat_type]:show() 113 | end 114 | end 115 | 116 | end 117 | 118 | function format_display_head(box_name) 119 | local text = string.format('%-13s',' ') 120 | for stat in settings.display[box_name].order:it() do 121 | if settings.display[box_name].data_types[stat] then 122 | characters = 0 123 | for i,v in pairs(settings.display[box_name].data_types[stat]) do 124 | characters = characters + 7 125 | if i=='total' then characters = characters +1 end 126 | end 127 | text = text .. '\\cs('..label_colors('stat')..')' .. string.format('%-'..characters..'s',stat) .. '\\cr' 128 | end 129 | end 130 | return text 131 | end 132 | 133 | function label_colors(label) 134 | local r, b, g = 255, 255, 255 135 | 136 | if settings.label[label] then 137 | r = settings.label[label].red or 255 138 | b = settings.label[label].blue or 255 139 | g = settings.label[label].green or 255 140 | end 141 | 142 | return tostring(r)..','..tostring(g)..','..tostring(b) 143 | end 144 | 145 | function format_display_data(data) 146 | line = "" 147 | 148 | if data["total-percent"] then 149 | line = line .. string.format('%-7s',data["total-percent"] .. '% ') 150 | end 151 | 152 | if data["percent"] then 153 | line = line .. string.format('%-7s',data["percent"] .. '% ') 154 | end 155 | 156 | if data["total"] then 157 | line = line .. string.format('%-8s',data["total"] .. ' ') 158 | end 159 | 160 | if data["avg"] then 161 | line = line .. string.format('%-7s','~' .. data["avg"] .. ' ') 162 | end 163 | 164 | if data["tally"] then 165 | if data["damage"] then 166 | line = line .. string.format('%-7s',data["damage"] ..' ') 167 | end 168 | line = line .. string.format('%-7s','#' .. data["tally"]) 169 | elseif data["damage"] then 170 | line = line .. string.format('%-7s',data["damage"]) 171 | end 172 | 173 | return line 174 | end 175 | 176 | function update_texts() 177 | for v,__ in pairs(text_box) do 178 | update_text(v) 179 | end 180 | end 181 | 182 | -- I want this to edit the text boxes directly 183 | function update_filters() 184 | local text = "" 185 | if filters['mob'] and filters['mob']:tostring()~="{}" then 186 | text = text .. ('Monsters: ' .. filters['mob']:tostring()) 187 | end 188 | if filters['player'] and filters['player']:tostring()~="{}" then 189 | text = text .. ('\nPlayers: ' .. filters['player']:tostring()) 190 | end 191 | return text 192 | end 193 | 194 | 195 | --Copyright (c) 2013~2016, F.R 196 | --All rights reserved. 197 | 198 | --Redistribution and use in source and binary forms, with or without 199 | --modification, are permitted provided that the following conditions are met: 200 | 201 | -- * Redistributions of source code must retain the above copyright 202 | -- notice, this list of conditions and the following disclaimer. 203 | -- * Redistributions in binary form must reproduce the above copyright 204 | -- notice, this list of conditions and the following disclaimer in the 205 | -- documentation and/or other materials provided with the distribution. 206 | -- * Neither the name of nor the 207 | -- names of its contributors may be used to endorse or promote products 208 | -- derived from this software without specific prior written permission. 209 | 210 | --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 211 | --ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 212 | --WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 213 | --DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 214 | --DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 215 | --(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 216 | --LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 217 | --ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 218 | --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 219 | --SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /file_handle.lua: -------------------------------------------------------------------------------- 1 | --[[ TO DO: 2 | 3 | -- Clean construct_database of extraneous details 4 | 5 | ]] 6 | 7 | files = require('files') 8 | xml = require('xml') 9 | 10 | 11 | function import_parse(file_name) 12 | local path = '/data/export/'..file_name 13 | 14 | import = files.new(path..'.xml', true) 15 | parsed, err = xml.read(import) 16 | 17 | if not parsed then 18 | message(err or 'XML error: Unknown error.') 19 | return 20 | end 21 | 22 | imported_database = construct_database(parsed) 23 | merge_tables(database,imported_database) 24 | 25 | -- Add nonblocks in for old version 26 | for mob,players in pairs(database) do 27 | for player,player_table in pairs(players) do 28 | if player_table['defense'] and player_table['defense']['block'] and not player_table['defense']['nonblock'] then 29 | player_table['defense']['nonblock'] = player_table['defense']['hit'] 30 | end 31 | end 32 | end 33 | 34 | -- Add total_damage in for old version 35 | for mob,players in pairs(database) do 36 | for player,player_table in pairs(players) do 37 | if not player_table.total_damage then 38 | player_table.total_damage = find_total_damage(player,mob) 39 | end 40 | end 41 | end 42 | 43 | message('Parse ['..file_name..'] was imported to database!') 44 | end 45 | 46 | function export_parse(file_name) 47 | if not windower.dir_exists(windower.addon_path..'data') then 48 | windower.create_dir(windower.addon_path..'data') 49 | end 50 | if not windower.dir_exists(windower.addon_path..'data/export') then 51 | windower.create_dir(windower.addon_path..'data/export') 52 | end 53 | 54 | local path = windower.addon_path..'data/export/' 55 | if file_name then 56 | path = path..file_name 57 | else 58 | path = path..os.date(' %H %M %S%p %y-%d-%m') 59 | end 60 | 61 | if windower.file_exists(path..'.xml') then 62 | path = path..'_'..os.clock() 63 | end 64 | 65 | local f = io.open(path..'.xml','w+') 66 | f:write('\n') 67 | 68 | --filter mobs 69 | for mob,data in pairs(database) do 70 | if check_filters('mob',mob) then 71 | f:write(' <'..mob..'>\n') 72 | f:write(to_xml(data,' ')) 73 | f:write(' \n') 74 | end 75 | end 76 | 77 | f:write('') 78 | f:close() 79 | 80 | message('Database was exported to '..path..'.xml!') 81 | if get_filters()~="" then 82 | message('Note that the database was filtered by [ '..get_filters()..' ]') 83 | end 84 | end 85 | 86 | function to_xml(t,indent_string) 87 | local indent = indent_string or ' ' 88 | local xml_string = "" 89 | for key,value in pairs(t) do 90 | key = tostring(key) 91 | xml_string = xml_string .. indent .. '<'..key:gsub(" ","_")..'>' 92 | if type(value)=='number' then 93 | xml_string = xml_string .. value 94 | xml_string = xml_string .. '\n' 95 | elseif type(value)=='table' then 96 | xml_string = xml_string .. '\n' .. to_xml(value,indent..' ') 97 | xml_string = xml_string .. indent .. '\n' 98 | end 99 | 100 | end 101 | 102 | return xml_string 103 | end 104 | 105 | --------------------------------------------------------- 106 | -- Function credit to the Windower Luacore config library 107 | --------------------------------------------------------- 108 | function construct_database(node, settings, key, meta) 109 | settings = settings or T{} 110 | key = key or 'settings' 111 | meta = meta 112 | 113 | local t = T{} 114 | if node.type ~= 'tag' then 115 | return t 116 | end 117 | 118 | if not node.children:all(function(n) 119 | return n.type == 'tag' or n.type == 'comment' 120 | end) and not (#node.children == 1 and node.children[1].type == 'text') then 121 | error('Malformatted settings file.') 122 | return t 123 | end 124 | 125 | -- TODO: Type checking necessary? merge should take care of that. 126 | if #node.children == 1 and node.children[1].type == 'text' then 127 | local val = node.children[1].value 128 | if node.children[1].cdata then 129 | --meta.cdata:add(key) 130 | return val 131 | end 132 | 133 | if val:lower() == 'false' then 134 | return false 135 | elseif val:lower() == 'true' then 136 | return true 137 | end 138 | 139 | local num = tonumber(val) 140 | if num ~= nil then 141 | return num 142 | end 143 | 144 | return val 145 | end 146 | 147 | for child in node.children:it() do 148 | if child.type == 'comment' then 149 | meta.comments[key] = child.value:trim() 150 | elseif child.type == 'tag' then 151 | key = child.name 152 | local childdict 153 | if table.containskey(settings, key) then 154 | childdict = table.copy(settings) 155 | else 156 | childdict = settings 157 | end 158 | t[child.name] = construct_database(child, childdict, key, meta) 159 | end 160 | end 161 | 162 | return t 163 | end 164 | 165 | function log_data(player,mob,action_type,value,spellName) 166 | if not logging then return end 167 | 168 | if not windower.dir_exists(windower.addon_path..'data') then 169 | windower.create_dir(windower.addon_path..'data') 170 | end 171 | if not windower.dir_exists(windower.addon_path..'data/log') then 172 | windower.create_dir(windower.addon_path..'data/log') 173 | end 174 | if not windower.dir_exists(windower.addon_path..'data/log/'..windower.ffxi.get_player().name) then 175 | windower.create_dir(windower.addon_path..'data/log/'..windower.ffxi.get_player().name) 176 | end 177 | 178 | local file = files.new('data/log/'..windower.ffxi.get_player().name..'/'..player..'_'..mob..'_'..action_type..'.log') 179 | if not file:exists() then 180 | file:create() 181 | end 182 | 183 | if not logs[player..'_'..mob..'_'..action_type] then 184 | file:append(os.date('======= %H:%M:%S %p %m-%d-%y =======')..'\n') 185 | logs[player..'_'..mob..'_'..action_type] = true 186 | end 187 | 188 | file:append('%s %s\n':format(spellName or '',value or '')) 189 | end 190 | 191 | --Copyright (c) 2013~2016, F.R 192 | --All rights reserved. 193 | 194 | --Redistribution and use in source and binary forms, with or without 195 | --modification, are permitted provided that the following conditions are met: 196 | 197 | -- * Redistributions of source code must retain the above copyright 198 | -- notice, this list of conditions and the following disclaimer. 199 | -- * Redistributions in binary form must reproduce the above copyright 200 | -- notice, this list of conditions and the following disclaimer in the 201 | -- documentation and/or other materials provided with the distribution. 202 | -- * Neither the name of nor the 203 | -- names of its contributors may be used to endorse or promote products 204 | -- derived from this software without specific prior written permission. 205 | 206 | --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 207 | --ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 208 | --WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 209 | --DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 210 | --DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 211 | --(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 212 | --LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 213 | --ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 214 | --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 215 | --SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /parse.lua: -------------------------------------------------------------------------------- 1 | _addon.version = '1.64' 2 | _addon.name = 'Parse' 3 | _addon.author = 'Flippant' 4 | _addon.commands = {'parse','p'} 5 | 6 | require 'tables' 7 | require 'sets' 8 | require 'strings' 9 | require 'actions' 10 | config = require('config') 11 | texts = require('texts') 12 | res = require 'resources' 13 | 14 | messageColor = 200 15 | 16 | default_settings = {} 17 | default_settings.update_interval = 1 18 | default_settings.autoexport_interval = 500 19 | default_settings.debug = false 20 | default_settings.index_shield = false 21 | default_settings.index_reprisal = true 22 | default_settings.index_palisade = true 23 | default_settings.index_battuta = true 24 | default_settings.record = { 25 | ["me"] = true, 26 | ["party"] = true, 27 | ["trust"] = true, 28 | ["alliance"] = true, 29 | ["pet"] = true, 30 | ["fellow"] = true 31 | } 32 | default_settings.logger = S{"Flipp*"} 33 | default_settings.label = { 34 | ["player"] = {red=100,green=200,blue=200}, 35 | ["stat"] = {red=225,green=150,blue=0}, 36 | } 37 | default_settings.display = {} 38 | default_settings.display.melee = { 39 | ["visible"] = true, 40 | ["type"] = "offense", 41 | ["pos"] = {x=570,y=50}, 42 | ["order"] = L{"damage","melee","ws"}, 43 | ["max"] = 6, 44 | ["data_types"] = { 45 | ["damage"] = S{'total','total-percent'}, 46 | ["melee"] = S{'percent'}, 47 | ["miss"] = S{'tally'}, 48 | ["crit"] = S{'percent'}, 49 | ["ws"] = S{'avg'}, 50 | ["ja"] = S{'avg'}, 51 | ["multi"] = S{'avg'}, 52 | ["ws_miss"] = S{'tally'} 53 | }, 54 | ["bg"] = {visible=true,alpha=50,red=0,green=0,blue=0}, 55 | ["text"] = {size=10,font="consolas",alpha=255,red=255,green=255,blue=255,stroke={width=1,alpha=200,red=0,green=0,blue=0}}, 56 | ["padding"] = 4, 57 | ["flags"] = {draggable=true,right=false,bottom=false,bold=true} 58 | } 59 | default_settings.display.defense = { 60 | ["visible"] = false, 61 | ["type"] = "defense", 62 | ["pos"] = {x=150,y=440}, 63 | ["order"] = L{"block","hit","parry",}, 64 | ["max"] = 2, 65 | ["data_types"] = { 66 | ["block"] = S{'avg','percent'}, 67 | ["evade"] = S{'percent'}, 68 | ["hit"] = S{'avg'}, 69 | ["parry"] = S{'percent'}, 70 | ["absorb"] = S{'percent'}, 71 | ["intimidate"] = S{'percent'}, 72 | }, 73 | ["bg"] = {visible=true,alpha=50,red=0,green=0,blue=0}, 74 | ["text"] = {size=10,font="consolas",alpha=255,red=255,green=255,blue=255,stroke={width=1,alpha=200,red=0,green=0,blue=0}}, 75 | ["padding"] = 4, 76 | ["flags"] = {draggable=true,right=false,bottom=false,bold=true} 77 | } 78 | default_settings.display.ranged = { 79 | ["visible"] = false, 80 | ["type"] = "offense", 81 | ["pos"] = {x=570,y=200}, 82 | ["order"] = L{"damage","ranged","ws"}, 83 | ["max"] = 6, 84 | ["data_types"] = { 85 | ["damage"] = S{'total','total-percent'}, 86 | ["ranged"] = S{'percent'}, 87 | ["r_crit"] = S{'percent'}, 88 | ["ws"] = S{'avg'}, 89 | }, 90 | ["bg"] = {visible=true,alpha=50,red=0,green=0,blue=0}, 91 | ["text"] = {size=10,font="consolas",alpha=255,red=255,green=255,blue=255,stroke={width=1,alpha=200,red=0,green=0,blue=0}}, 92 | ["padding"] = 4, 93 | ["flags"] = {draggable=true,right=false,bottom=false,bold=true} 94 | } 95 | default_settings.display.magic = { 96 | ["visible"] = false, 97 | ["type"] = "offense", 98 | ["pos"] = {x=570,y=50}, 99 | ["order"] = L{"damage","spell"}, 100 | ["max"] = 6, 101 | ["data_types"] = { 102 | ["damage"] = S{'total','total-percent'}, 103 | ["spell"] = S{'avg'}, 104 | }, 105 | ["bg"] = {visible=true,alpha=50,red=0,green=0,blue=0}, 106 | ["text"] = {size=10,font="consolas",alpha=255,red=255,green=255,blue=255,stroke={width=1,alpha=200,red=0,green=0,blue=0}}, 107 | ["padding"] = 4, 108 | ["flags"] = {draggable=true,right=false,bottom=false,bold=true} 109 | } 110 | 111 | settings = config.load(default_settings) 112 | config.save(settings) 113 | 114 | update_tracker,update_interval = 0,settings.update_interval 115 | autoexport = nil 116 | autoexport_tracker,autoexport_interval = 0,settings.autoexport_interval 117 | pause = false 118 | logging = true 119 | buffs = {["Palisade"] = false, ["Reprisal"] = false, ["Battuta"] = false, ["Retaliation"] = false} 120 | 121 | database = {} 122 | filters = { 123 | ['mob'] = S{}, 124 | ['player'] = S{} 125 | } 126 | renames = {} 127 | text_box = {} 128 | logs = {} 129 | 130 | stat_types = {} 131 | stat_types.defense = S{"hit","block","evade","parry","intimidate","absorb","shadow","anticipate","nonparry","nonblock","retrate","nonret"} 132 | stat_types.melee = S{"melee","miss","crit"} 133 | stat_types.ranged = S{"ranged","r_miss","r_crit"} 134 | stat_types.category = S{"ws","ja","spell","mb","enfeeb","ws_miss","ja_miss","enfeeb_miss"} 135 | stat_types.other = S{"spike","sc","add"} 136 | stat_types.multi = S{'1','2','3','4','5','6','7','8'} 137 | 138 | damage_types = S{"melee","crit","ranged","r_crit","ws","ja","spell","mb","spike","sc","add"} 139 | 140 | require 'utility' 141 | require 'retrieval' 142 | require 'display' 143 | require 'action_parse' 144 | require 'report' 145 | require 'file_handle' 146 | 147 | ActionPacket.open_listener(parse_action_packet) 148 | init_boxes() 149 | 150 | windower.register_event('addon command', function(...) 151 | local args = {...} 152 | if args[1] == 'report' then 153 | report_data(args[2],args[3],args[4],args[5]) 154 | elseif (args[1] == 'filter' or args[1] == 'f') and args[2] then 155 | edit_filters(args[2],args[3],args[4]) 156 | update_texts() 157 | elseif (args[1] == 'list' or args[1] == 'l') then 158 | print_list(args[2]) 159 | elseif (args[1] == 'show' or args[1] == 's' or args[1] == 'display' or args[1] == 'd') then 160 | toggle_box(args[2]) 161 | update_texts() 162 | elseif args[1] == 'reset' then 163 | reset_parse() 164 | update_texts() 165 | elseif args[1] == 'pause' or args[1] == 'p' then 166 | if pause then pause=false else pause=true end 167 | update_texts() 168 | elseif args[1] == 'rename' and args[2] and args[3] then 169 | if args[3]:gsub('[%w_]','')=="" then 170 | renames[args[2]:gsub("^%l", string.upper)] = args[3] 171 | message('Data for player/mob '..args[2]:gsub("^%l", string.upper)..' will now be indexed as '..args[3]) 172 | return 173 | end 174 | message('Invalid character found. You may only use alphanumeric characters or underscores.') 175 | elseif args[1] == 'interval' then 176 | if type(tonumber(args[2]))=='number' then update_tracker,update_interval = 0, tonumber(args[2]) end 177 | message('Your current update interval is every '..update_interval..' actions.') 178 | elseif args[1] == 'export' then 179 | export_parse(args[2]) 180 | elseif args[1] == 'autoexport' then 181 | if (autoexport and not args[2]) or args[2] == 'off' then 182 | autoexport = nil message('Autoexport turned off.') 183 | else 184 | autoexport = args[2] or 'autoexport' 185 | message('Autoexport now on. Saving under file name "'..autoexport..'" every '..autoexport_interval..' recorded actions.') 186 | end 187 | elseif args[1] == 'import' and args[2] then 188 | import_parse(args[2]) 189 | update_texts() 190 | elseif args[1] == 'log' then 191 | if logging then logging=false message('Logging has been turned off.') else logging=true message('Logging has been turned on.') end 192 | elseif args[1] == 'help' then 193 | message('report [stat] [chatmode] : Reports stat to designated chatmode. Defaults to damage.') 194 | message('filter/f [add/+ | remove/- | clear/reset] [string] : Adds/removes/clears mob filter.') 195 | message('show/s [melee/ranged/magic/defense] : Shows/hides display box. "melee" is the default.') 196 | message('pause/p : Pauses/unpauses parse. When paused, data is not recorded.') 197 | message('reset : Resets parse.') 198 | message('rename [player name] [new name] : Renames a player or monster for NEW incoming data.') 199 | message('import/export [file name] : Imports/exports an XML file to/from database. Only filtered monsters are exported.') 200 | message('autoexport [file name] : Automatically exports an XML file every '..autoexport_interval..' recorded actions.') 201 | message('log : Toggles logging feature.') 202 | message('list/l [mobs/players] : Lists the mobs and players currently in the database. "mobs" is the default.') 203 | message('interval [number] : Defines how many actions it takes before displays are updated.') 204 | else 205 | message('That command was not found. Use //parse help for a list of commands.') 206 | end 207 | end ) 208 | 209 | tracked_buffs = { 210 | [403] = "Reprisal", 211 | [478] = "Palisade", 212 | [570] = "Battuta", 213 | [405] = "Retaliation" 214 | } 215 | 216 | windower.register_event('gain buff', function(id) 217 | if tracked_buffs[id] then 218 | buffs[tracked_buffs[id]] = true 219 | end 220 | end ) 221 | 222 | windower.register_event('lose buff', function(id) 223 | if tracked_buffs[id] then 224 | buffs[tracked_buffs[id]] = true 225 | end 226 | end ) 227 | 228 | function get_stat_type(stat) 229 | for stat_type,stats in pairs(stat_types) do 230 | if stats:contains(stat) then 231 | return stat_type 232 | end 233 | end 234 | return nil 235 | end 236 | 237 | function reset_parse() 238 | database = {} 239 | end 240 | 241 | function toggle_box(box_name) 242 | if not box_name then 243 | box_name = 'melee' 244 | end 245 | if text_box[box_name] then 246 | if settings.display[box_name].visible then 247 | text_box[box_name]:hide() 248 | settings.display[box_name].visible = false 249 | else 250 | text_box[box_name]:show() 251 | settings.display[box_name].visible = true 252 | end 253 | else 254 | message('That display was not found. Display names are: melee, defense, ranged, magic.') 255 | end 256 | end 257 | 258 | function edit_filters(filter_action,str,filter_type) 259 | if not filter_type or not filters[filter_type] then 260 | filter_type = 'mob' 261 | end 262 | 263 | if filter_action=='add' or filter_action=="+" then 264 | if not str then message("Please provide string to add to filters.") return end 265 | filters[filter_type]:add(str) 266 | message('"'..str..'" has been added to '..filter_type..' filters.') 267 | elseif filter_action=='remove' or filter_action=="-" then 268 | if not str then message("Please provide string to remove from filters.") return end 269 | filters[filter_type]:remove(str) 270 | message('"'..str..'" has been removed from '..filter_type..' filters.') 271 | elseif filter_action=='clear' or filter_action=="reset" then 272 | filters[filter_type] = S{} 273 | message(filter_type..' filters have been cleared.') 274 | end 275 | end 276 | 277 | function get_filters() 278 | local text = "" 279 | if filters['mob'] and filters['mob']:tostring()~="{}" then 280 | text = text .. ('Monsters: ' .. filters['mob']:tostring()) 281 | end 282 | if filters['player'] and filters['player']:tostring()~="{}" then 283 | text = text .. ('\nPlayers: ' .. filters['player']:tostring()) 284 | end 285 | return text 286 | end 287 | 288 | function print_list(list_type) 289 | if not list_type or list_type=="monsters" or list_type=="m" then 290 | list_type="mobs" 291 | elseif list_type=="p" then 292 | list_type="players" 293 | end 294 | 295 | local lst = S{} 296 | if list_type=='mobs' then 297 | lst = get_mobs() 298 | elseif list_type=='players' then 299 | lst = get_players() 300 | else 301 | message('List type not found. Valid list types: mobs, players') 302 | return 303 | end 304 | 305 | if lst:length()==0 then message('No data found. Nothing to list!') return end 306 | 307 | lst['n'] = nil 308 | local msg = "" 309 | for __,i in pairs(lst) do 310 | msg = msg .. i .. ', ' 311 | end 312 | 313 | msg = msg:slice(1,#msg-2) 314 | 315 | msg = prepare_string(msg,100) 316 | msg['n'] = nil 317 | 318 | for i,line in pairs(msg) do 319 | message(line) 320 | end 321 | end 322 | 323 | -- Returns true if monster is not filtered, false if monster is filtered out 324 | function check_filters(filter_type,mob_name) 325 | if not filters[filter_type] or filters[filter_type]:tostring()=="{}" then 326 | return true 327 | end 328 | 329 | local response = false 330 | local only_excludes = true 331 | for v,__ in pairs(filters[filter_type]) do 332 | if v:lower():startswith('!^') then --exact exclusion filter 333 | if v:lower():gsub('%!',''):gsub('%^','')==mob_name:lower() then --immediately return false 334 | return false 335 | end 336 | elseif v:lower():startswith('!') then --exclusion filter 337 | if string.find(mob_name:lower(),v:lower():gsub('%!','')) then --immediately return false 338 | return false 339 | end 340 | elseif v:lower():startswith('^') then --exact match filter 341 | if v:lower():gsub('%^','')==mob_name:lower() then 342 | response = true 343 | end 344 | only_excludes = false 345 | elseif string.find(mob_name:lower(),v:lower()) then --wildcard filter (default behavior) 346 | response = true 347 | only_excludes = false 348 | else 349 | only_excludes = false 350 | end 351 | end 352 | if not response and only_excludes then 353 | response = true 354 | end 355 | return response 356 | end 357 | 358 | config.register(settings, function(settings) 359 | update_texts:loop(settings.update_interval) 360 | end) 361 | 362 | --Copyright (c) 2013~2016, F.R 363 | --All rights reserved. 364 | 365 | --Redistribution and use in source and binary forms, with or without 366 | --modification, are permitted provided that the following conditions are met: 367 | 368 | -- * Redistributions of source code must retain the above copyright 369 | -- notice, this list of conditions and the following disclaimer. 370 | -- * Redistributions in binary form must reproduce the above copyright 371 | -- notice, this list of conditions and the following disclaimer in the 372 | -- documentation and/or other materials provided with the distribution. 373 | -- * Neither the name of nor the 374 | -- names of its contributors may be used to endorse or promote products 375 | -- derived from this software without specific prior written permission. 376 | 377 | --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 378 | --ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 379 | --WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 380 | --DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 381 | --DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 382 | --(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 383 | --LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 384 | --ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 385 | --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 386 | --SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 387 | -------------------------------------------------------------------------------- /parse_info.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flippant/parse/acb7e49513e87e578e359c2674110b8124241a6d/parse_info.xlsx -------------------------------------------------------------------------------- /report.lua: -------------------------------------------------------------------------------- 1 | --[[ TO DO 2 | 3 | -- Specific handling of acc/racc to be more user-friendly 4 | -- Specific handling of da/ta/qa/ua/sa/ea/oa? 5 | -- Handle WS acc and JA acc 6 | 7 | ]] 8 | 9 | function report_data(stat,ability,chatmode,chattarget) 10 | local valid_chatmodes = S{'s','p','t','l','l2','echo'} 11 | -- If user doesn't enter a stat, then correct arguments 12 | if not stat then 13 | stat = 'damage' 14 | elseif valid_chatmodes:contains(stat) then 15 | chattarget = ability 16 | chatmode = stat 17 | ability = nil 18 | stat = 'damage' 19 | elseif valid_chatmodes:contains(ability) then 20 | chattarget = chatmode 21 | chatmode = ability 22 | ability = nil 23 | end 24 | if not valid_chatmodes[chatmode] then 25 | chatmode = nil 26 | end 27 | if chatmode == 't' then 28 | if chattarget then 29 | chat_prefix = chatmode..' '..chattarget 30 | else message("Chat target not found.") end 31 | else 32 | chat_prefix = chatmode 33 | end 34 | 35 | if S{'acc','accuracy','hitrate'}:contains(stat) then 36 | stat = 'melee' 37 | elseif S{'racc'}:contains(stat) then 38 | stat = 'ranged' 39 | elseif S{'evasion','eva'}:contains(stat) then 40 | stat = 'evade' 41 | end 42 | 43 | report_string = "" 44 | sorted_players = L{} 45 | sorted_players = get_sorted_players(stat,20) 46 | 47 | if stat == 'damage' then 48 | report_string = report_string .. '[Total damage] '..update_filters()..' | ' 49 | for player in sorted_players:it() do 50 | report_string = report_string .. (player..': '..get_player_stat_percent(stat,player)..'% ('..get_player_damage(player)..'), ') 51 | end 52 | elseif get_stat_type(stat)=='category' then 53 | report_string = report_string .. '[Reporting '..stat..' ' 54 | if ability then report_string = report_string .. '('..ability..') ' end 55 | report_string = report_string .. 'stats] '..update_filters()..' | ' 56 | player_spell_table = get_player_spell_table(stat) 57 | for player in sorted_players:it() do 58 | if not ability or (ability and player_spell_table[player][ability]) then 59 | report_string = report_string .. (player..': ') 60 | if not ability then 61 | report_string = report_string .. ('{Total} ') 62 | if (stat=='ws' or stat=='ja' or stat=='enfeeb') and get_player_stat_percent(stat,player) then 63 | report_string = report_string .. (get_player_stat_percent(stat,player) ..'% ') 64 | end 65 | if get_player_stat_avg(stat,player) then report_string = report_string .. ('~'..get_player_stat_avg(stat,player)..'avg ') end 66 | report_string = report_string .. ('('..get_player_stat_tally(stat,player)..'s) ') 67 | end 68 | for spell,spell_table in pairs(player_spell_table[player]) do 69 | if not ability then report_string = report_string .. ('['..spell..'] ') end 70 | if not ability or (ability and spell==ability) then 71 | if spell_table.damage then report_string = report_string .. ('~'..math.floor(spell_table.damage / spell_table.tally)..'avg ') end 72 | report_string = report_string .. ('('..spell_table.tally..'s) ') 73 | end 74 | end 75 | report_string = report_string .. (' | ') 76 | end 77 | end 78 | elseif get_stat_type(stat)=='multi' or stat=='multi' then 79 | report_string = report_string .. '[Reporting multihit stats] '..update_filters()..' | ' 80 | for player in sorted_players:it() do 81 | report_string = report_string .. (player..': ') 82 | report_string = report_string .. ('{Total} ') 83 | report_string = report_string .. ('~'..get_player_stat_avg(stat,player)..'avg ') 84 | for i=1,8,1 do 85 | if get_player_stat_tally(tostring(i),player) > 0 then 86 | report_string = report_string .. ('['..i..'-hit] ') 87 | if get_player_stat_percent(i,player) then report_string = report_string .. (''..get_player_stat_percent(tostring(i),player)..'% ') end 88 | report_string = report_string .. ('('..get_player_stat_tally(tostring(i),player)..'s)') 89 | report_string = report_string .. (', ') 90 | end 91 | end 92 | report_string = report_string .. (' | ') 93 | end 94 | elseif get_stat_type(stat) then 95 | report_string = report_string .. '[Reporting '..stat..' stats] '..update_filters()..' | ' 96 | for player in sorted_players:it() do 97 | report_string = report_string .. (player..': ') 98 | --report_string = report_string .. (get_player_stat_damage(stat,player)..' ') 99 | if get_player_stat_percent(stat,player) then report_string = report_string .. (''..get_player_stat_percent(stat,player)..'% ') end 100 | if get_player_stat_avg(stat,player) then report_string = report_string .. ('~'..get_player_stat_avg(stat,player)..'avg ') end 101 | report_string = report_string .. ('('..get_player_stat_tally(stat,player)..'s)') 102 | report_string = report_string .. (', ') 103 | end 104 | else 105 | message('That stat was not found. Reportable stats include:') 106 | message('damage, melee, multi, crit, miss, ranged, r_crit, r_miss, spike, sc, add, hit, block, evade, parry, intimidate, absorb, ws, ja, spell') 107 | return 108 | end 109 | 110 | --remove last two characters of report_string (extra symbol + space) 111 | report_string = report_string:slice(1,#report_string-2) 112 | 113 | line_cap = 90 114 | report_table = report_string:split('| ') 115 | report_table['n'] = nil 116 | 117 | for i,line in pairs(report_table) do 118 | if #line <= line_cap then 119 | if chat_prefix then windower.send_command('input /'..chat_prefix..' '..line) coroutine.sleep(1.5) 120 | else message(line) end 121 | else 122 | line_table = prepare_string(line,line_cap) 123 | line_table['n'] = nil 124 | for i,subline in pairs(line_table) do 125 | if chat_prefix then windower.send_command('input /'..chat_prefix..' '..subline) coroutine.sleep(1.5) 126 | else message(subline) end 127 | end 128 | end 129 | end 130 | end 131 | 132 | 133 | -- Takes string and returns table of strings 134 | function prepare_string(str,cap) 135 | str_table = str:split(' ') 136 | str_table['n'] = nil 137 | new_string = "" 138 | new_table = L{} 139 | 140 | for i,word in pairs(str_table) do 141 | new_string = new_string .. word .. ' ' 142 | if #new_string > cap then 143 | new_table:append(new_string) 144 | new_string = "" 145 | end 146 | end 147 | 148 | if new_string ~= "" then new_table:append(new_string) end 149 | 150 | return new_table 151 | end 152 | 153 | 154 | --Copyright (c) 2013~2016, F.R 155 | --All rights reserved. 156 | 157 | --Redistribution and use in source and binary forms, with or without 158 | --modification, are permitted provided that the following conditions are met: 159 | 160 | -- * Redistributions of source code must retain the above copyright 161 | -- notice, this list of conditions and the following disclaimer. 162 | -- * Redistributions in binary form must reproduce the above copyright 163 | -- notice, this list of conditions and the following disclaimer in the 164 | -- documentation and/or other materials provided with the distribution. 165 | -- * Neither the name of nor the 166 | -- names of its contributors may be used to endorse or promote products 167 | -- derived from this software without specific prior written permission. 168 | 169 | --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 170 | --ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 171 | --WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 172 | --DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 173 | --DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 174 | --(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 175 | --LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 176 | --ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 177 | --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 178 | --SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 179 | -------------------------------------------------------------------------------- /retrieval.lua: -------------------------------------------------------------------------------- 1 | --[[ TO DO 2 | 3 | -- Implement player filtering for reporting function 4 | -- Implement temporary mob_filters variable for reporting function 5 | 6 | ]] 7 | 8 | percent_table = { 9 | intimidate = S{"hit","block","anticipate","parry","evade"}, 10 | evade = S{"hit","block","anticipate","parry"}, 11 | parry = S{"nonparry"}, 12 | anticipate = S{"hit","block"}, 13 | block = S{"nonblock"}, 14 | absorb = S{"hit","block"}, 15 | retrate = S{"nonret"}, 16 | 17 | melee = S{"miss","+crit"}, 18 | crit = S{"melee"}, 19 | 20 | ranged = S{"r_miss","+r_crit"}, 21 | r_crit = S{"ranged"}, 22 | 23 | ws = S{"ws_miss"}, 24 | ja = S{"ja_miss"}, 25 | 26 | ['1'] = S{'2','3','4','5','6','7','8'}, 27 | ['2'] = S{'1','3','4','5','6','7','8'}, 28 | ['3'] = S{'1','2','4','5','6','7','8'}, 29 | ['4'] = S{'1','2','3','5','6','7','8'}, 30 | ['5'] = S{'1','2','3','4','6','7','8'}, 31 | ['6'] = S{'1','2','3','4','5','7','8'}, 32 | ['7'] = S{'1','2','3','4','5','6','8'}, 33 | ['8'] = S{'1','2','3','4','5','6','7'}, 34 | } 35 | 36 | -- Returns a table of players 37 | function get_players() 38 | local player_table = L{} 39 | 40 | for mob,players in pairs(database) do 41 | for player,__ in pairs(players) do 42 | if not player_table:contains(player) then 43 | player_table:append(player) 44 | end 45 | end 46 | end 47 | 48 | return player_table 49 | end 50 | 51 | -- Returns a table of monsters 52 | function get_mobs() 53 | local mob_table = L{} 54 | 55 | for mob,players in pairs(database) do 56 | if not mob_table:contains(mob) then 57 | mob_table:append(mob) 58 | end 59 | end 60 | 61 | return mob_table 62 | end 63 | 64 | -- Returns a list of players, sorted by a particular stat or stat type, and limited to a number (or to 20, if no number provided) 65 | function get_sorted_players(sort_value,limit) 66 | local player_table = get_players() 67 | if not get_players() then 68 | return nil 69 | end 70 | 71 | if not limit then 72 | limit = 20 73 | end 74 | 75 | if S{'multi','1','2','3','4','5','6','7','8'}:contains(sort_value) then 76 | sort_value = 'melee' 77 | end 78 | 79 | local sorted_player_table = L{} 80 | 81 | for i=1,limit,+1 do 82 | player_name = nil 83 | top_result = 0 84 | for __,player in pairs(player_table) do 85 | if sort_value == 'damage' then -- sort by total damage 86 | if get_player_damage(player) > top_result and not sorted_player_table:contains(player) then 87 | top_result = get_player_damage(player) 88 | player_name = player 89 | end 90 | elseif sort_value == 'defense' then -- sort by total parry/hit/evades/blocks 91 | player_hits_received = get_player_stat_tally('parry',player) + get_player_stat_tally('hit',player) + get_player_stat_tally('evade',player) + get_player_stat_tally('block',player) 92 | if player_hits_received > top_result and not sorted_player_table:contains(player) then 93 | top_result = player_hits_received 94 | player_name = player 95 | end 96 | elseif S{'ws','ja','spell','mb'}:contains(sort_value) and get_player_stat_avg(sort_value,player) then -- sort by avg 97 | if get_player_stat_avg(sort_value,player) > top_result and not sorted_player_table:contains(player) then 98 | top_result = get_player_stat_avg(sort_value,player) 99 | player_name = player 100 | end 101 | elseif S{'hit','miss','nonblock','nonparry','r_miss','ws_miss','ja_miss','enfeeb','enfeeb_miss'}:contains(sort_value) then -- sort by tally 102 | if get_player_stat_tally(sort_value,player) > top_result and not sorted_player_table:contains(player) then 103 | top_result = get_player_stat_tally(sort_value,player) 104 | player_name = player 105 | end 106 | elseif (S{'melee','ranged','crit','r_crit'}:contains(sort_value) or get_stat_type(sort_value)=="defense") and get_player_stat_percent(sort_value,player) then -- sort by percent 107 | if get_player_stat_percent(sort_value,player) > top_result and not sorted_player_table:contains(player) then 108 | top_result = get_player_stat_percent(sort_value,player) 109 | player_name = player 110 | end 111 | elseif S{'sc','add','spike'}:contains(sort_value) then --sort by damage 112 | if get_player_stat_damage(sort_value,player) > top_result and not sorted_player_table:contains(player) then 113 | top_result = get_player_stat_damage(sort_value,player) 114 | player_name = player 115 | end 116 | end 117 | end 118 | if player_name then sorted_player_table:append(player_name) end 119 | end 120 | 121 | 122 | return sorted_player_table 123 | end 124 | 125 | --takes table and collapses WS, JA, and spells 126 | function collapse_categories(t) 127 | for key,value in pairs(t) do 128 | if get_stat_type(key)=='category' then -- ws, ja, spell 129 | spells = value 130 | t[key].tally = 0 131 | t[key].damage = 0 132 | for spell,data in pairs(value) do 133 | if type(data)=='table' then 134 | t[key].tally = data.tally 135 | t[key].damage = data.damage 136 | t[key][spell] = nil 137 | end 138 | end 139 | return 140 | elseif type(value)=='table' then -- go deeper 141 | collapse_categories(value) 142 | else 143 | return -- hit a dead end, go back 144 | end 145 | end 146 | 147 | return t 148 | end 149 | 150 | function collapse_mobs(s_type,mob_filters) 151 | local player_table = nil 152 | 153 | for mob,players in pairs(copy(database)) do 154 | if not player_table then 155 | player_table = {} 156 | end 157 | if check_filters('mob',mob) then 158 | for player,player_data in pairs(players) do 159 | if check_filters('player',player) then 160 | if not player_table[player] then 161 | player_table[player] = player_data 162 | else 163 | merge_tables(player_table[player],player_data) 164 | end 165 | end 166 | end 167 | end 168 | end 169 | 170 | if player_table then 171 | collapse_categories(player_table) 172 | end 173 | 174 | return player_table 175 | end 176 | 177 | function get_player_spell_table(spell_type,mob_filters) 178 | local player_table = nil 179 | 180 | for mob,players in pairs(database) do 181 | if not player_table then 182 | player_table = {} 183 | end 184 | if check_filters('mob',mob) then 185 | for player,mob_player_table in pairs(players) do 186 | if check_filters('player',player) then 187 | if not player_table[player] then 188 | player_table[player] = {} 189 | end 190 | if mob_player_table['category'] and mob_player_table['category'][spell_type] then 191 | for spell,spell_table in pairs(mob_player_table['category'][spell_type]) do 192 | if not player_table[player][spell] then 193 | player_table[player][spell] = {} 194 | end 195 | for datum,value in pairs(spell_table) do 196 | if not player_table[player][spell][datum] then 197 | player_table[player][spell][datum] = 0 198 | end 199 | player_table[player][spell][datum] = player_table[player][spell][datum] + value 200 | end 201 | end 202 | end 203 | end 204 | end 205 | end 206 | end 207 | 208 | return player_table 209 | end 210 | 211 | function get_player_stat_tally(stat,plyr,mob_filters) 212 | if type(stat)=='number' then stat=tostring(stat) end 213 | local tally = 0 214 | for mob,mob_table in pairs(database) do 215 | if check_filters('mob',mob) then 216 | if database[mob][plyr] and database[mob][plyr][get_stat_type(stat)] and database[mob][plyr][get_stat_type(stat)][stat] then 217 | if database[mob][plyr][get_stat_type(stat)][stat].tally then 218 | tally = tally + database[mob][plyr][get_stat_type(stat)][stat].tally 219 | elseif get_stat_type(stat)=="category" then 220 | for spell,spell_table in pairs (database[mob][plyr][get_stat_type(stat)][stat]) do 221 | if spell_table.tally then 222 | tally = tally + spell_table.tally 223 | end 224 | end 225 | end 226 | end 227 | end 228 | end 229 | return tally 230 | end 231 | 232 | function get_player_stat_damage(stat,plyr,mob_filters) 233 | if type(stat)=='number' then stat=tostring(stat) end 234 | local damage = 0 235 | for mob,mob_table in pairs(database) do 236 | if (mob_filters and mob==mob_filters) or (not mob_filters and check_filters('mob',mob)) then 237 | if database[mob][plyr] and database[mob][plyr][get_stat_type(stat)] and database[mob][plyr][get_stat_type(stat)][stat] then 238 | if database[mob][plyr][get_stat_type(stat)][stat].damage then 239 | damage = damage + database[mob][plyr][get_stat_type(stat)][stat].damage 240 | elseif get_stat_type(stat)=="category" then 241 | for spell,spell_table in pairs (database[mob][plyr][get_stat_type(stat)][stat]) do 242 | if spell_table.damage then 243 | damage = damage + spell_table.damage 244 | end 245 | end 246 | end 247 | end 248 | end 249 | end 250 | return damage 251 | end 252 | 253 | function get_player_stat_avg(stat,plyr,mob_filters) 254 | if S{'ws_miss','ja_miss','enfeeb','enfeeb_miss'}:contains(stat) then return nil end 255 | if type(stat)=='number' then stat=tostring(stat) end 256 | local total,tally,result,digits = 0,0,0,0 257 | 258 | if stat=='multi' then 259 | digits = 2 260 | for i,__ in pairs(stat_types.multi) do 261 | total = total + (get_player_stat_tally(i,plyr,mob_filters) * tonumber(i)) 262 | tally = tally + get_player_stat_tally(i,plyr,mob_filters) 263 | end 264 | else 265 | digits = 0 266 | total = get_player_stat_damage(stat,plyr,mob_filters) 267 | tally = get_player_stat_tally(stat,plyr,mob_filters) 268 | end 269 | 270 | if tally == 0 then return nil end 271 | 272 | local shift = 10 ^ digits 273 | result = math.floor( (total / tally)*shift + 0.5 ) / shift 274 | 275 | return result 276 | end 277 | 278 | function get_player_stat_percent(stat,plyr,mob_filters) 279 | if type(stat)=='number' then stat=tostring(stat) end 280 | if stat=="damage" then 281 | dividend = get_player_damage(plyr,mob_filters) 282 | divisor = get_player_damage(nil,mob_filters) 283 | else 284 | if not percent_table[stat] then 285 | return nil 286 | end 287 | dividend = get_player_stat_tally(stat,plyr,mob_filters) 288 | divisor = get_player_stat_tally(stat,plyr,mob_filters) 289 | 290 | if percent_table[stat] then 291 | for v,__ in pairs(percent_table[stat]) do 292 | -- if string begins with + 293 | if type(v)=='string' and v:startswith('+') then 294 | dividend = dividend + get_player_stat_tally(string.sub(v,2),plyr,mob_filters) 295 | divisor = divisor + get_player_stat_tally(string.sub(v,2),plyr,mob_filters) 296 | else 297 | divisor = divisor + get_player_stat_tally(v,plyr,mob_filters) 298 | end 299 | end 300 | end 301 | end 302 | 303 | if dividend==0 or divisor==0 then 304 | return nil 305 | end 306 | 307 | digits = 4 308 | 309 | shift = 10 ^ digits 310 | result = math.floor( (dividend / divisor) *shift + 0.5 ) / shift 311 | 312 | return result * 100 313 | end 314 | 315 | function get_player_damage(plyr,mob_filters) 316 | local damage = 0 317 | 318 | for mob,players in pairs(database) do 319 | if (mob_filters and mob==mob_filters) or (not mob_filters and check_filters('mob',mob)) then 320 | for player,mob_player_table in pairs(players) do 321 | if not plyr or (plyr and player==plyr) then 322 | if mob_player_table.total_damage then 323 | damage = damage + mob_player_table.total_damage 324 | end 325 | end 326 | end 327 | end 328 | end 329 | 330 | return damage 331 | end 332 | 333 | -- For old versions of exports 334 | function find_total_damage(plyr,mnst) 335 | local damage = 0 336 | if database[mnst] and database[mnst][plyr] then 337 | for stat in damage_types:it() do 338 | damage = damage + get_player_stat_damage(stat,plyr,mnst) 339 | end 340 | end 341 | return damage 342 | end 343 | 344 | 345 | --Copyright (c) 2013~2016, F.R 346 | --All rights reserved. 347 | 348 | --Redistribution and use in source and binary forms, with or without 349 | --modification, are permitted provided that the following conditions are met: 350 | 351 | -- * Redistributions of source code must retain the above copyright 352 | -- notice, this list of conditions and the following disclaimer. 353 | -- * Redistributions in binary form must reproduce the above copyright 354 | -- notice, this list of conditions and the following disclaimer in the 355 | -- documentation and/or other materials provided with the distribution. 356 | -- * Neither the name of nor the 357 | -- names of its contributors may be used to endorse or promote products 358 | -- derived from this software without specific prior written permission. 359 | 360 | --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 361 | --ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 362 | --WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 363 | --DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 364 | --DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 365 | --(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 366 | --LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 367 | --ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 368 | --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 369 | --SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /utility.lua: -------------------------------------------------------------------------------- 1 | function message(message) 2 | windower.add_to_chat(messageColor,'PARSE: '..message) 3 | end 4 | 5 | function debug(message) 6 | if settings.debug then 7 | windower.add_to_chat(messageColor,'PARSE DEBUG: '..message) 8 | end 9 | end 10 | 11 | function merge_tables(t1,t2) 12 | for key,value in pairs(t2) do 13 | if not t1[key] then -- doesn't exist already 14 | --debug('key not found, making new') 15 | t1[key] = value 16 | else -- exists, need to merge data 17 | if type(value)=='number' then -- if a number, just add the data to the value 18 | t1[key] = t1[key] + value 19 | --debug('adding value to previous record') 20 | elseif type(value)=='table' then --if it's a table, we need to go deeper 21 | merge_tables(t1[key],value) 22 | end 23 | end 24 | end 25 | end 26 | 27 | function copy(obj, seen) 28 | if type(obj) ~= 'table' then return obj end 29 | if seen and seen[obj] then return seen[obj] end 30 | local s = seen or {} 31 | local res = setmetatable({}, getmetatable(obj)) 32 | s[obj] = res 33 | for k, v in pairs(obj) do res[copy(k, s)] = copy(v, s) end 34 | return res 35 | end --------------------------------------------------------------------------------