diff options
author | chai <chaifix@163.com> | 2021-11-15 13:53:59 +0800 |
---|---|---|
committer | chai <chaifix@163.com> | 2021-11-15 13:53:59 +0800 |
commit | 942a030afd348ab2e02eac8054b43e3c3a72ea48 (patch) | |
tree | a13459f39a3d2f1b533fbd1b5ab523d7a621f673 /Data/BuiltIn/Libraries/lua-addons/addons/scoreboard | |
parent | e307051a56a54c27f10438fd2025edf61d0dfeed (diff) |
*rename
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/scoreboard')
8 files changed, 1950 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/damagedb.lua b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/damagedb.lua new file mode 100644 index 0000000..4b22b61 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/damagedb.lua @@ -0,0 +1,179 @@ +local Player = require 'player' +local MergedPlayer = require 'mergedplayer' + +local DamageDB = { + db = T{}, + filter = T{} +} + +--[[ +DamageDB.player_stat_fields = T{ + 'mmin', 'mmax', 'mavg', + 'rmin', 'rmax', 'ravg', + 'wsmin', 'wsmax', 'wsavg' +} +]] + +DamageDB.player_stat_fields = T{ + 'mavg', 'mrange', 'critavg', 'critrange', + 'ravg', 'rrange', 'rcritavg', 'rcritrange', + 'acc', 'racc', 'crit', 'rcrit', + 'wsavg', 'wsacc' +} + +function DamageDB:new (o) + o = o or {} + setmetatable(o, self) + self.__index = self + + return o +end + + +function DamageDB:iter() + local k, v + return function () + k, v = next(self.db, k) + while k and not self:_filter_contains_mob(k) do + k, v = next(self.db, k) + end + + if k then + return k, v + end + end +end + + +function DamageDB:get_filters() + return self.filter +end + + +function DamageDB:_filter_contains_mob(mob_name) + if self.filter:empty() then + return true + end + + for _, mob_pattern in ipairs(self.filter) do + if mob_name:lower():find(mob_pattern:lower()) then + return true + end + end + return false +end + + +function DamageDB:clear_filters() + self.filter = T{} +end + + +function DamageDB:add_filter(mob_pattern) + if mob_pattern then self.filter:append(mob_pattern) end +end + + +-- Returns the corresponding Player instance. Will create it if necessary. +function DamageDB:_get_player(mob, player_name) + if not self.db[mob] then + self.db[mob] = T{} + end + + if not self.db[mob][player_name] then + self.db[mob][player_name] = Player:new{name = player_name} + end + + return self.db[mob][player_name] +end + + +-- Returns a table {player1 = stat1, player2 = stat2...}. +-- For WS queries, the stat value is a sub-table of {ws1 = ws_stat1, ws2 = ws_stat2}. +function DamageDB:query_stat(stat, player_name) + local players = T{} + + if player_name and player_name:match('^[a-zA-Z]+$') then + player_name = player_name:lower():ucfirst() + end + + -- Gather a table mapping player names to all of the corresponding Player instances + for mob, mob_players in self:iter() do + for name, player in pairs(mob_players) do + if player_name and player_name == name or + not player_name and not player.is_sc then + if players[name] then + players[name]:append(player) + else + players[name] = T{player} + end + end + end + end + + -- Flatten player subtables into the merged stat we desire + for name, instances in pairs(players) do + local merged = MergedPlayer:new{players = instances} + players[name] = MergedPlayer[stat](merged) + end + + return players +end + + +function DamageDB:empty() + return self.db:empty() +end + + +function DamageDB:reset() + self.db = T{} +end + + +--[[ +The following player dispatchers all fetch the correct +instance of Player for a given mob and then dispatch the +method for data accmulation. +]]-- +function DamageDB:add_m_hit(m, p, d) self:_get_player(m, p):add_m_hit(d) end +function DamageDB:add_m_crit(m, p, d) self:_get_player(m, p):add_m_crit(d) end +function DamageDB:add_r_hit(m, p, d) self:_get_player(m, p):add_r_hit(d) end +function DamageDB:add_r_crit(m, p, d) self:_get_player(m, p):add_r_crit(d) end +function DamageDB:incr_misses(m, p) self:_get_player(m, p):incr_m_misses() end +function DamageDB:incr_r_misses(m, p) self:_get_player(m, p):incr_r_misses() end +function DamageDB:incr_ws_misses(m, p) self:_get_player(m, p):incr_ws_misses() end +function DamageDB:add_damage(m, p, d) self:_get_player(m, p):add_damage(d) end +function DamageDB:add_ws_damage(m, p, d, id) self:_get_player(m, p):add_ws_damage(id, d) end + + +return DamageDB + +--[[ +Copyright (c) 2013, Jerry Hebert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Scoreboard nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 JERRY HEBERT 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. +]] + diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/data/settings.xml b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/data/settings.xml new file mode 100644 index 0000000..2478be1 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/data/settings.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" ?> +<settings> + <!-- + This file controls the settings for the Scoreboard plugin. + Settings in the <global> section apply to all characters + + The available settings are: + posX - x coordinate for position + posY - y coordinate for position + numPlayers - The maximum number of players to display damage for + bgTransparency - Transparency level for the background. 0-255 range + --> + <global> + <posX>10</posX> + <posY>250</posY> + <bgTransparency>200</bgTransparency> + <numPlayers>8</numPlayers> + <UpdateFrequency>0.5</UpdateFrequency> + </global> + + <!-- + You may also override specific settings on a per-character basis here. + --> +</settings> + diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua new file mode 100644 index 0000000..b2582d6 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua @@ -0,0 +1,443 @@ +-- Display object +local texts = require('texts') + +local Display = { + visible = true, + settings = nil, + tb_name = 'scoreboard' +} + +local valid_fonts = T{ + 'fixedsys', + 'lucida console', + 'courier', + 'courier new', + 'ms mincho', + 'consolas', + 'dejavu sans mono' +} + +local valid_fields = T{ + 'name', + 'dps', + 'percent', + 'total', + 'mavg', + 'mrange', + 'critavg', + 'critrange', + 'ravg', + 'rrange', + 'rcritavg', + 'rcritrange', + 'acc', + 'racc', + 'crit', + 'rcrit', + 'wsavg' +} + + +function Display:set_position(posx, posy) + self.text:pos(posx, posy) +end + +function Display:new(settings, db) + local repr = setmetatable({db = db}, self) + self.settings = settings + self.__index = self + self.visible = settings.visible + + self.text = texts.new(settings.display, settings) + + if not valid_fonts:contains(self.settings.display.text.font:lower()) then + error('Invalid font specified: ' .. self.settings.display.text.font) + self.text:font(self.settings.display.text.font) + self.text:size(self.settings.display.text.fontsize) + else + self.text:font(self.settings.display.text.font, 'consolas', 'courier new', 'monospace') + self.text:size(self.settings.display.text.size) + end + + self:visibility(self.visible) + + return repr +end + + +function Display:visibility(v) + if v then + self.text:show() + else + self.text:hide() + end + + self.visible = v + self.settings.visible = v + self.settings:save() +end + + +function Display:report_filters() + local mob_str + local filters = self.db:get_filters() + + if filters:empty() then + mob_str = "Scoreboard filters: None (Displaying damage for all mobs)" + else + mob_str = "Scoreboard filters: " .. filters:concat(', ') + end + windower.add_to_chat(55, mob_str) + +end + + +-- Returns the string for the scoreboard header with updated info +-- about current mob filtering and whether or not time is currently +-- contributing to the DPS value. +function Display:build_scoreboard_header() + local mob_filter_str + local filters = self.db:get_filters() + + if filters:empty() then + mob_filter_str = 'All' + else + mob_filter_str = table.concat(filters, ', ') + end + + local labels + if self.db:empty() then + labels = '\n' + else + labels = '%32s%7s%9s\n':format('Tot', 'Pct', 'DPS') + end + + local dps_status + if dps_clock:is_active() then + dps_status = 'Active' + else + dps_status = 'Paused' + end + + local dps_clock_str = '' + if dps_clock:is_active() or dps_clock.clock > 1 then + dps_clock_str = ' (%s)':format(dps_clock:to_string()) + end + + local dps_chunk = 'DPS: %s%s':format(dps_status, dps_clock_str) + + return '%s%s\nMobs: %-9s\n%s':format(dps_chunk, ' ':rep(29 - dps_chunk:len()) .. '//sb help', mob_filter_str, labels) +end + + +-- Returns following two element pair: +-- 1) table of sorted 2-tuples containing {player, damage} +-- 2) integer containing the total damage done +function Display:get_sorted_player_damage() + -- In order to sort by damage, we have to first add it all up into a table + -- and then build a table of sortable 2-tuples and then finally we can sort... + local mob, players + local player_total_dmg = T{} + + if self.db:empty() then + return {}, 0 + end + + for mob, players in self.db:iter() do + -- If the filter isn't active, include all mobs + for player_name, player in pairs(players) do + if player_total_dmg[player_name] then + player_total_dmg[player_name] = player_total_dmg[player_name] + player.damage + else + player_total_dmg[player_name] = player.damage + end + end + end + + local sortable = T{} + local total_damage = 0 + for player, damage in pairs(player_total_dmg) do + total_damage = total_damage + damage + sortable:append({player, damage}) + end + + table.sort(sortable, function(a, b) + return a[2] > b[2] + end) + + return sortable, total_damage +end + + +-- Updates the main display with current filter/damage/dps status +function Display:update() + if not self.visible then + -- no need build a display while it's hidden + return + end + + if self.db:empty() then + self:reset() + return + end + local damage_table, total_damage + damage_table, total_damage = self:get_sorted_player_damage() + + local display_table = T{} + local player_lines = 0 + local alli_damage = 0 + for k, v in pairs(damage_table) do + if player_lines < self.settings.numplayers then + local dps + if dps_clock.clock == 0 then + dps = "N/A" + else + dps = '%.2f':format(math.round(v[2] / dps_clock.clock, 2)) + end + + local percent + if total_damage > 0 then + percent = '(%.1f%%)':format(100 * v[2] / total_damage) + else + percent = '(0%)' + end + display_table:append('%-25s%7d%8s %7s':format(v[1], v[2], percent, dps)) + end + alli_damage = alli_damage + v[2] -- gather this even for players not displayed + player_lines = player_lines + 1 + end + + if self.settings.showallidps and dps_clock.clock > 0 then + display_table:append('-':rep(17)) + display_table:append('Alli DPS: ' .. '%7.1f':format(alli_damage / dps_clock.clock)) + end + + self.text:text(self:build_scoreboard_header() .. table.concat(display_table, '\n')) +end + + +local function build_input_command(chatmode, tell_target) + local input_cmd = 'input ' + if chatmode then + input_cmd = input_cmd .. '/' .. chatmode .. ' ' + if tell_target then + input_cmd = input_cmd .. tell_target .. ' ' + end + end + + return input_cmd +end + +-- Takes a table of elements to be wrapped across multiple lines and returns +-- a table of strings, each of which fits within one FFXI line. +local function wrap_elements(elements, header, sep) + local max_line_length = 120 -- game constant + if not sep then + sep = ', ' + end + + local lines = T{} + local current_line = nil + local line_length + + local i = 1 + while i <= #elements do + if not current_line then + current_line = T{} + line_length = header:len() + lines:append(current_line) + end + + local new_line_length = line_length + elements[i]:len() + sep:len() + if new_line_length > max_line_length then + current_line = T{} + lines:append(current_line) + new_line_length = elements[i]:len() + sep:len() + end + + current_line:append(elements[i]) + line_length = new_line_length + i = i + 1 + end + + local baked_lines = lines:map(function (ls) return ls:concat(sep) end) + if header:len() > 0 and #baked_lines > 0 then + baked_lines[1] = header .. baked_lines[1] + end + + return baked_lines +end + + +local function slow_output(chatprefix, lines, limit) + -- this is funky but if we don't wait like this, the lines will spew too fast and error + windower.send_command(lines:map(function (l) return chatprefix .. l end):concat('; wait 1.2 ; ')) +end + + +function Display:report_summary (...) + local chatmode, tell_target = table.unpack({...}) + + local damage_table, total_damage + damage_table, total_damage = self:get_sorted_player_damage() + + local elements = T{} + for k, v in pairs(damage_table) do + elements:append('%s %d(%.1f%%)':format(v[1], v[2], 100 * v[2]/total_damage)) + end + + -- Send the report to the specified chatmode + slow_output(build_input_command(chatmode, tell_target), + wrap_elements(elements:slice(1, self.settings.numplayers), 'Dmg: '), self.settings.numplayers) +end + +-- This is a table of the line aggregators and related utilities +Display.stat_summaries = {} + + +Display.stat_summaries._format_title = function (msg) + local line_length = 40 + local msg_length = msg:len() + local border_len = math.floor(line_length / 2 - msg_length / 2) + + return ' ':rep(border_len) .. msg .. ' ':rep(border_len) + end + + +Display.stat_summaries['range'] = function (stats, filters, options) + + local lines = T{} + for name, pair in pairs(stats) do + lines:append('%-20s %d min %d max':format(name, pair[1], pair[2])) + end + + if #lines > 0 and options and options.name then + sb_output(Display.stat_summaries._format_title('-= '..options.name..' (' .. filters .. ') =-')) + sb_output(lines) + end + end + + +Display.stat_summaries['average'] = function (stats, filters, options) + + local lines = T{} + for name, pair in pairs(stats) do + if options and options.percent then + lines:append('%-20s %.2f%% (%d sample%s)':format(name, 100 * pair[1], pair[2], + pair[2] == 1 and '' or 's')) + else + lines:append('%-20s %d (%ds)':format(name, pair[1], pair[2])) + end + end + + if #lines > 0 and options and options.name then + sb_output(Display.stat_summaries._format_title('-= '..options.name..' (' .. filters .. ') =-')) + sb_output(lines) + end + end + + +-- This is a closure around a hash-based dispatcher. Some conveniences are +-- defined for the actual stat display functions. +Display.show_stat = function() + return function (self, stat, player_filter) + local stats = self.db:query_stat(stat, player_filter) + local filters = self.db:get_filters() + local filter_str + + if filters:empty() then + filter_str = 'All mobs' + else + filter_str = filters:concat(', ') + end + + Display.stat_summaries[Display.stat_summaries._all_stats[stat].category](stats, filter_str, Display.stat_summaries._all_stats[stat]) + end +end() + + +-- TODO: This needs to be factored somehow to take better advantage of similar +-- code already written for reporting and stat queries. +Display.stat_summaries._all_stats = T{ + ['acc'] = {percent=true, category="average", name='Accuracy'}, + ['racc'] = {percent=true, category="average", name='Ranged Accuracy'}, + ['crit'] = {percent=true, category="average", name='Melee Crit. Rate'}, + ['rcrit'] = {percent=true, category="average", name='Ranged Crit. Rate'}, + ['wsavg'] = {percent=false, category="average", name='WS Average'}, + ['wsacc'] = {percent=true, category="average", name='WS Accuracy'}, + ['mavg'] = {percent=false, category="average", name='Melee Non-Crit. Avg. Damage'}, + ['mrange'] = {percent=false, category="range", name='Melee Non-Crit. Range'}, + ['critavg'] = {percent=false, category="average", name='Melee Crit. Avg. Damage'}, + ['critrange'] = {percent=false, category="range", name='Melee Crit. Range'}, + ['ravg'] = {percent=false, category="average", name='Ranged Non-Crit. Avg. Damage'}, + ['rrange'] = {percent=false, category="range", name='Ranged Non-Crit. Range'}, + ['rcritavg'] = {percent=false, category="average", name='Ranged Crit. Avg. Damage'}, + ['rcritrange'] = {percent=false, category="range", name='Ranged Crit. Range'},} +function Display:report_stat(stat, args) + if Display.stat_summaries._all_stats:containskey(stat) then + local stats = self.db:query_stat(stat, args.player) + + local elements = T{} + local header = Display.stat_summaries._all_stats[stat].name .. ': ' + for name, stat_pair in pairs(stats) do + if stat_pair[2] > 0 then + if Display.stat_summaries._all_stats[stat].category == 'range' then + elements:append({stat_pair[1], ('%s %d~%d'):format(name, stat_pair[1], stat_pair[2])}) + elseif Display.stat_summaries._all_stats[stat].percent then + elements:append({stat_pair[1], ('%s %.2f%% (%ds)'):format(name, stat_pair[1] * 100, stat_pair[2])}) + else + elements:append({stat_pair[1], ('%s %d (%ds)'):format(name, stat_pair[1], stat_pair[2])}) + end + end + end + table.sort(elements, function(a, b) + return a[1] > b[1] + end) + + -- Send the report to the specified chatmode + local wrapped = wrap_elements(elements:slice(1, self.settings.numplayers):map(function (p) return p[2] end), header) + slow_output(build_input_command(args.chatmode, args.telltarget), wrapped, self.settings.numplayers) + end +end + + +function Display:reset() + -- the number of spaces here was counted to keep the table width + -- consistent even when there's no data being displayed + self.text:text(self:build_scoreboard_header() .. + 'Waiting for results...' .. + ' ':rep(17)) +end + + +return Display + +--[[ +Copyright © 2013-2014, Jerry Hebert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Scoreboard nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 JERRY HEBERT 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. +]] + + diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/dpsclock.lua b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/dpsclock.lua new file mode 100644 index 0000000..ba3a267 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/dpsclock.lua @@ -0,0 +1,96 @@ +-- Object to encapsulate DPS Clock functionality + +local DPSClock = { + clock = 0, + prev_time = 0, + active = false +} + +function DPSClock:new (o) + o = o or {} + setmetatable(o, self) + self.__index = self + + return o +end + + +function DPSClock:advance() + local now = os.time() + + if self.prev_time == 0 then + self.prev_time = now + end + + self.clock = self.clock + (now - self.prev_time) + self.prev_time = now + + self.active = true +end + + +function DPSClock:pause() + self.active = false + self.prev_time = 0 +end + + +function DPSClock:is_active() + return self.active +end + +function DPSClock:reset() + self.active = false + self.clock = 0 + self.prev_time = 0 +end + + +-- Convert integer seconds into a "HhMmSs" string +function DPSClock:to_string() + local seconds = self.clock + + local hours = math.floor(seconds / 3600) + seconds = seconds - hours * 3600 + + local minutes = math.floor(seconds / 60) + seconds = seconds - minutes * 60 + + local hours_str = hours > 0 and hours .. "h" or "" + local minutes_str = minutes > 0 and minutes .. "m" or "" + local seconds_str = seconds and seconds .. "s" or "" + + return hours_str .. minutes_str .. seconds_str +end + +return DPSClock + +--[[ +Copyright (c) 2013, Jerry Hebert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Scoreboard nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 JERRY HEBERT 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. +]] + + diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/mergedplayer.lua b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/mergedplayer.lua new file mode 100644 index 0000000..8346cfd --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/mergedplayer.lua @@ -0,0 +1,354 @@ +--[[ + The entire mergedplayer file exists to flatten individual stats in the db + into two numbers (per name). So normally the db is: + dps_db.dp[mob_name][player_name] = {stats} + Mergedplayer iterates over mob_name and returns a table that's just: + tab[player_name] = {CalculatedStatA,CalculatedStatB} +]] + +local MergedPlayer = {} + +function MergedPlayer:new (o) + o = o or {} + + assert(o.players and #o.players > 0, + "MergedPlayer constructor requires at least one Player instance.") + + setmetatable(o, self) + self.__index = self + + return o +end + +--[[ + 'wsmin', 'wsmax', 'wsavg' +]] + +function MergedPlayer:mavg() + local hits, hit_dmg = 0, 0 + + for _, p in ipairs(self.players) do + hits = hits + p.m_hits + hit_dmg = hit_dmg + p.m_hits*p.m_avg + end + + if hits > 0 then + return { hit_dmg / hits, hits} + else + return {0, 0} + end +end + + +function MergedPlayer:mrange() + local m_min, m_max = math.huge, 0 + + for _, p in ipairs(self.players) do + m_min = math.min(m_min, p.m_min) + m_max = math.max(m_max, p.m_max) + end + + return {m_min~=math.huge and m_min or m_max, m_max} +end + + +function MergedPlayer:critavg() + local crits, crit_dmg = 0, 0 + + for _, p in ipairs(self.players) do + crits = crits + p.m_crits + crit_dmg = crit_dmg + p.m_crits*p.m_crit_avg + end + + if crits > 0 then + return { crit_dmg / crits, crits} + else + return {0, 0} + end +end + + +function MergedPlayer:critrange() + local m_crit_min, m_crit_max = math.huge, 0 + + for _, p in ipairs(self.players) do + m_crit_min = math.min(m_crit_min, p.m_crit_min) + m_crit_max = math.max(m_crit_max, p.m_crit_max) + end + + return {m_crit_min~=math.huge and m_crit_min or m_crit_max, m_crit_max} +end + + +function MergedPlayer:ravg() + local r_hits, r_hit_dmg = 0, 0 + + for _, p in ipairs(self.players) do + r_hits = r_hits + p.r_hits + r_hit_dmg = r_hit_dmg + p.r_hits*p.r_avg + end + + if r_hits > 0 then + return { r_hit_dmg / r_hits, r_hits} + else + return {0, 0} + end +end + + +function MergedPlayer:rrange() + local r_min, r_max = math.huge, 0 + + for _, p in ipairs(self.players) do + r_min = math.min(r_min, p.r_min) + r_max = math.max(r_max, p.r_max) + end + + return {r_min~=math.huge and r_min or r_max, r_max} +end + + +function MergedPlayer:rcritavg() + local r_crits, r_crit_dmg = 0, 0 + + for _, p in ipairs(self.players) do + r_crits = r_crits + p.r_crits + r_crit_dmg = r_crit_dmg + p.r_crits*p.r_crit_avg + end + + if r_crits > 0 then + return { r_crit_dmg / r_crits, r_crits} + else + return {0, 0} + end +end + + +function MergedPlayer:rcritrange() + local r_crit_min, r_crit_max = math.huge, 0 + + for _, p in ipairs(self.players) do + r_crit_min = math.min(r_crit_min, p.r_crit_min) + r_crit_max = math.max(r_crit_max, p.r_crit_max) + end + + return {r_crit_min~=math.huge and r_crit_min or r_crit_max, r_crit_max} +end + + +function MergedPlayer:acc() + local hits, crits, misses = 0, 0, 0 + + for _, p in ipairs(self.players) do + hits = hits + p.m_hits + crits = crits + p.m_crits + misses = misses + p.m_misses + end + + local total = hits + crits + misses + if total > 0 then + return {(hits + crits) / total, total} + else + return {0, 0} + end +end + + +function MergedPlayer:racc() + local hits, crits, misses = 0, 0, 0 + + for _, p in ipairs(self.players) do + hits = hits + p.r_hits + crits = crits + p.r_crits + misses = misses + p.r_misses + end + + local total = hits + crits + misses + if total > 0 then + return {(hits + crits) / total, total} + else + return {0, 0} + end +end + + +function MergedPlayer:crit() + local hits, crits = 0, 0 + + for _, p in ipairs(self.players) do + hits = hits + p.m_hits + crits = crits + p.m_crits + end + + local total = hits + crits + if total > 0 then + return {crits / total, total} + else + return {0, 0} + end +end + + +function MergedPlayer:rcrit() + local hits, crits = 0, 0 + + for _, p in ipairs(self.players) do + hits = hits + p.r_hits + crits = crits + p.r_crits + end + + local total = hits + crits + if total > 0 then + return {crits / total, total} + else + return {0, 0} + end +end + + +function MergedPlayer:wsavg() + local wsdmg = 0 + local wscount = 0 + --[[ + for _, p in pairs(self.players) do + for _, dmgtable in pairs(p.ws) do + for _, dmg in pairs(dmgtable) do + wsdmg = wsdmg + dmg + wscount = wscount + 1 + end + end + end + ]] + + for _, p in pairs(self.players) do + for _, dmg in pairs(p.ws) do + wsdmg = wsdmg + dmg + wscount = wscount + 1 + end + end + + if wscount > 0 then + return {wsdmg / wscount, wscount} + else + return {0, 0} + end +end + +function MergedPlayer:wsacc() + local hits, misses = 0, 0 + + for _, p in ipairs(self.players) do + hits = hits + table.length(p.ws) + misses = misses + p.ws_misses + end + + local total = hits + misses + if total > 0 then + return {hits / total, total} + else + return {0, 0} + end +end + +-- Unused atm +function MergedPlayer:merge(other) + self.damage = self.damage + other.damage + + for ws_id, values in pairs(other.ws) do + if self.ws[ws_id] then + for _, value in ipairs(values) do + self.ws[ws_id]:append(value) + end + else + self.ws[ws_id] = table.copy(values) + end + end + + self.m_hits = self.m_hits + other.m_hits + self.m_misses = self.m_misses + other.m_misses + self.m_min = math.min(self.m_min, other.m_min) + self.m_max = math.max(self.m_max, other.m_max) + + local total_m_hits = self.m_hits + other.m_hits + if total_m_hits > 0 then + self.m_avg = self.m_avg * self.m_hits/total_m_hits + + other.m_avg * other.m_hits/total_m_hits + else + self.m_avg = 0 + end + + self.m_crits = self.m_crits + other.m_crits + self.m_crit_min = math.min(self.m_crit_min, other.m_crit_min) + self.m_crit_max = math.max(self.m_crit_max, other.m_crit_max) + + local total_m_crits = self.m_crits + other.m_crits + if total_m_crits > 0 then + self.m_crit_avg = self.m_crit_avg * self.m_crits / total_m_crits + + other.m_crit_avg * other.m_crits / total_m_crits + else + self.m_crit_avg = 0 + end + + self.r_hits = self.r_hits + other.r_hits + self.r_misses = self.r_misses + other.r_misses + self.r_min = math.min(self.r_min, other.r_min) + self.r_max = math.max(self.r_max, other.r_max) + + local total_r_hits = self.r_hits + other.r_hits + if total_r_hits > 0 then + self.r_avg = self.r_avg * self.r_hits/total_r_hits + + other.r_avg * other.r_hits/total_r_hits + else + self.r_avg = 0 + end + + self.r_crits = self.r_crits + other.r_crits + self.r_crit_min = math.min(self.r_crit_min, other.r_crit_min) + self.r_crit_max = math.max(self.r_crit_max, other.r_crit_max) + + local total_r_crits = self.r_crits + other.r_crits + if total_r_crits > 0 then + self.r_crit_avg = self.r_crit_avg * self.r_crits / total_r_crits + + other.r_crit_avg * other.r_crits / total_r_crits + else + self.r_crit_avg = 0 + end + + self.jobabils = self.jobabils + other.jobabils + self.spells = self.spells + other.spells +end + + + + +return MergedPlayer + +--[[ +Copyright (c) 2013, Jerry Hebert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Scoreboard nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 JERRY HEBERT 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. +]] + + diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/player.lua b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/player.lua new file mode 100644 index 0000000..18bcf94 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/player.lua @@ -0,0 +1,178 @@ +--[[ +Object to encapsulate Player battle data + +For each mob fought, a separate player instance will be stored. Therefore +there will be multiple Player instances for each actual player in the game. +This allows for easier mob filtering. +]] + +local Player = {} + +function Player:new (o) + o = o or {} + + assert(o.name, "Must pass a name to player constructor") + -- attrs should be defined in Player above but due to interpreter bug it's here for now + local attrs = { + clock = nil, -- specific DPS clock for this player + damage = 0, -- total damage done by this player + ws = T{}, -- table of all WS and their corresponding damage + ws_misses = 0, -- total ws misses + m_hits = 0, -- total melee hits + m_misses = 0, -- total melee misses + m_min = math.huge, -- minimum melee damage + m_max = 0, -- maximum melee damage + m_avg = 0, -- avg melee damage + m_crits = 0, -- total melee crits + m_crit_min = math.huge, -- minimum melee crit + m_crit_max = 0, -- maximum melee crit + m_crit_avg = 0, -- avg melee crit + r_hits = 0, -- total ranged hits + r_min = math.huge, -- minimum ranged damage + r_max = 0, -- maximum ranged damage + r_avg = 0, -- avg ranged damage + r_misses = 0, -- total ranged misses + r_crits = 0, -- total ranged crits + r_crit_min = math.huge, -- minimum ranged crit + r_crit_max = 0, -- maximum ranged crit + r_crit_avg = 0, -- avg ranged crit + jobabils = 0, -- total damage from JAs + spells = 0, -- total damage from spells + + parries = 0, -- total number of parries + blocks = 0, -- total number of blocks/guards + nonblocks = 0, -- total number of nonblocks + evades = 0, -- total number of evades + damage_taken = 0, -- total damage taken by this player + + } + attrs.name = o.name + o = attrs + if o.name:match('^Skillchain%(') then + o.is_sc = true + else + o.is_sc = false + end + + setmetatable(o, self) + self.__index = self + + return o +end + + +function Player:add_damage(damage) + self.damage = self.damage + damage +end + + +function Player:add_ws_damage(ws_name, damage) + --[[ + if not self.ws[ws_name] then + self.ws[ws_name] = L{} + end + + self.ws[ws_name]:append(damage) + ]] + self.ws:append(damage) + self.damage = self.damage + damage +end + + +function Player:add_m_hit(damage) + -- increment hits + self.m_hits = self.m_hits + 1 + + -- update min/max/avg melee values + self.m_min = math.min(self.m_min, damage) + self.m_max = math.max(self.m_max, damage) + self.m_avg = self.m_avg * (self.m_hits - 1)/self.m_hits + damage/self.m_hits + + -- accumulate damage + self.damage = self.damage + damage +end + + +function Player:add_m_crit(damage) + -- increment crits + self.m_crits = self.m_crits + 1 + + -- update min/max/avg melee values + self.m_crit_min = math.min(self.m_crit_min, damage) + self.m_crit_max = math.max(self.m_crit_max, damage) + self.m_crit_avg = self.m_crit_avg * (self.m_crits - 1)/self.m_crits + damage/self.m_crits + + -- accumulate damage + self.damage = self.damage + damage +end + +function Player:incr_m_misses() self.m_misses = self.m_misses + 1 end + +function Player:incr_ws_misses() self.ws_misses = self.ws_misses + 1 end + +function Player:add_r_hit(damage) + -- increment hits + self.r_hits = self.r_hits + 1 + + -- update min/max/avg melee values + self.r_min = math.min(self.r_min, damage) + self.r_max = math.max(self.r_max, damage) + self.r_avg = self.r_avg * (self.r_hits - 1)/self.r_hits + damage/self.r_hits + + -- accumulate damage + self.damage = self.damage + damage +end + + +function Player:add_r_crit(damage) + -- increment crits + self.r_crits = self.r_crits + 1 + + -- update min/max/avg melee values + self.r_crit_min = math.min(self.r_crit_min, damage) + self.r_crit_max = math.max(self.r_crit_max, damage) + self.r_crit_avg = self.r_crit_avg * (self.r_crits - 1)/self.r_crits + damage/self.r_crits + + -- accumulate damage + self.damage = self.damage + damage +end + + +function Player:incr_r_misses() self.r_misses = self.r_misses + 1 end + +-- Returns the name of this player +function Player:get_name() return self.name end + + + +return Player + +--[[ +Copyright (c) 2013, Jerry Hebert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Scoreboard nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 JERRY HEBERT 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. +]] + + diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/readme.txt b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/readme.txt new file mode 100644 index 0000000..1e5ff45 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/readme.txt @@ -0,0 +1,120 @@ +Author: Suji +Version: 1.09 +Addon to show alliance DPS and damage in real time. +Abbreviation: //sb + +This addon allows players to see their DPS live while fighting enemies. Party +and alliance member DPS is also dispalyed. In addition to DPS, each player's +total damage and their percent contribution is also displayed. + +Notable features: +* Live DPS +* You can still parse damage even if you enable chat filters. +* Ability to filter only the mobs you want to see damage for. +* 'Report' command for reporting damage back to where you like. + +DPS accumulation is active whenever anyone in your alliance is currently +in battle. + +All in-game commands are prefixed with "//sb" or "//scoreboard", for +example: "//sb help". + +Command list: +* HELP + Displays the help text + +* POS <x> <y> + Positions the scoreboard to the given coordinates + +* RESET + Resets all the data that's been tracked so far. + +* REPORT [<target>] + Reports the damage. With no argument, it will go to whatever you have + your current chatmode set to. You may also pass the standard FFXI chat + abbreviations as arguments. Support arguments are 's', 't', 'p', 'l'. + If you pass 't' (for tell), you must also pass a player name to send + the tell to. Examples: + //sb report Reports to current chatmode + //sb report l Reports to your linkshell + //sb report t suji Reports in tell to Suji + +* REPORTSTAT <stat> [<playerName>] [<target>] + RS <stat> [<playerName>] [<target>] + Reports the given stat. Supported stats are: + mavg, mrange, acc, ravg, rrange, racc, critavg, critrange, crit, + rcritavg, rcritrange, rcrit, wsavg, wsacc + + 'playerName' may be the name of a player if you wish to see only one player. + + For 'target', with no argument, it will go to whatever you have + your current chatmode set to. You may also pass the standard FFXI chat + abbreviations as arguments. Support arguments are 's', 't', 'p', 'l'. + If you pass 't' (for tell), you must also pass a player name to send + the tell to. + + Examples: + //sb reportstat acc -- Sends acc report your default chatmode + //sb rs crit -- Same as above + //sb rs crit p -- Explicitly to party + //sb rs acc tell suji -- Sends acc to Suji + //sb rs acc t suji -- Same as above + //sb rs acc tulia t suji -- Report accuracy for Tulia only and send it in tell to Suji + +* FILTER + This takes one of three sub-commands. + * FILTER SHOW + Shows the current mob filters. + +* FILTER ADD <mob1> <mob2> ... + Adds mob(s) to the filters. These can all be substrings. Legal Lua + patterns are also allowed. + + * FILTER CLEAR + Clears all mobs from the filter. + +* VISIBLE + Toggles the visibility of the scoreboard. Data will continue to + accumulate even while it is hidden. + +* STAT <statname> [<player>] + View specific parser stats. This will respect the current filter settings. + Valid stats are: acc, racc, crit, rcrit + Examples: + //sb stat acc Shows accuracy for everyone + //sb stat crit Flippant Only show crit rate for Flippant + +The settings file, located in addons/scoreboard/data/settings.xml, contains +additional configuration options: +* posX - x coordinate for position +* posY - y coordinate for position +* numPlayers - The maximum number of players to display damage for +* bgTransparency - Transparency level for the background. 0-255 range +* font - The font for the Scoreboard. This defaults to Courier but it + it may be changed to one of the following fonts: + Fixedsys, Lucida Console, Courier, Courier New, MS Mincho, + Consolas, Dejavu Sans Mono. +* fontsize - Size of Scoreboard's font +* sbcolor - Color of scoreboard's chat log output +* showallidps - Set to true to display the alliance DPS, false otherwise. +* resetfilters - Set to true if you want filters reset when you "//sb reset", false otherwise. +* showfellow - Set to true to display your adventuring fellow's DPS, false otherwise. + +Caveats: +* DPS is an approximation, although I tested it manually and found it to + be very accurate. Because DPS accumulation is based on the game's notion + of when you are in battle, if someone else engages before you, your DPS + will suffer. Try to engage fast to get a better approximation. + +* The methods used in here cause some discrepancies with the data reported + by KParser. In some cases, Scoreboard will report more damage, which + generally indicates that KParser is not including something (ie, Scoreboard + will be more accurate). However, there are cases where KParser is reporting + damage that Scoreboard is not, and I'm currently focused on resolving this + issue in particular. + +* This addon is still in development. Please report any issues or feedback to + to me (Suji on Phoenix) on FFXIAH or Guildwork. + +Thanks to Flippant for all of the helpful feedback and comments and to Zumi +for encouraging me to write this in the first place. diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/scoreboard.lua b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/scoreboard.lua new file mode 100644 index 0000000..893cefb --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/scoreboard.lua @@ -0,0 +1,555 @@ +-- Scoreboard addon for Windower4. See readme.md for a complete description. + +_addon.name = 'Scoreboard' +_addon.author = 'Suji' +_addon.version = '1.14' +_addon.commands = {'sb', 'scoreboard'} + +require('tables') +require('strings') +require('maths') +require('logger') +require('actions') +local file = require('files') +config = require('config') + +local Display = require('display') +local display +dps_clock = require('dpsclock'):new() -- global for now +dps_db = require('damagedb'):new() -- global for now + +------------------------------------------------------- + +-- Conventional settings layout +local default_settings = {} +default_settings.numplayers = 8 +default_settings.sbcolor = 204 +default_settings.showallidps = true +default_settings.resetfilters = true +default_settings.visible = true +default_settings.showfellow = true +default_settings.UpdateFrequency = 0.5 +default_settings.combinepets = true + +default_settings.display = {} +default_settings.display.pos = {} +default_settings.display.pos.x = 500 +default_settings.display.pos.y = 100 + +default_settings.display.bg = {} +default_settings.display.bg.alpha = 200 +default_settings.display.bg.red = 0 +default_settings.display.bg.green = 0 +default_settings.display.bg.blue = 0 + +default_settings.display.text = {} +default_settings.display.text.size = 10 +default_settings.display.text.font = 'Courier New' +default_settings.display.text.fonts = {} +default_settings.display.text.alpha = 255 +default_settings.display.text.red = 255 +default_settings.display.text.green = 255 +default_settings.display.text.blue = 255 + +settings = config.load(default_settings) + +-- Accepts msg as a string or a table +function sb_output(msg) + local prefix = 'SB: ' + local color = settings['sbcolor'] + + if type(msg) == 'table' then + for _, line in ipairs(msg) do + windower.add_to_chat(color, prefix .. line) + end + else + windower.add_to_chat(color, prefix .. msg) + end +end + +-- Handle addon args +windower.register_event('addon command', function() + local chatmodes = S{'s', 'l', 'l2', 'p', 't', 'say', 'linkshell', 'linkshell2', 'party', 'tell', 'echo'} + + return function(command, ...) + if command == 'e' then + assert(loadstring(table.concat({...}, ' ')))() + return + end + + command = (command or 'help'):lower() + local params = {...} + + if command == 'help' then + sb_output('Scoreboard v' .. _addon.version .. '. Author: Suji') + sb_output('sb help : Shows help message') + sb_output('sb pos <x> <y> : Positions the scoreboard') + sb_output('sb reset : Reset damage') + sb_output('sb report [<target>] : Reports damage. Can take standard chatmode target options.') + sb_output('sb reportstat <stat> [<player>] [<target>] : Reports the given stat. Can take standard chatmode target options. Ex: //sb rs acc p') + sb_output('Valid chatmode targets are: ' .. chatmodes:concat(', ')) + sb_output('sb filter show : Shows current filter settings') + sb_output('sb filter add <mob1> <mob2> ... : Add mob patterns to the filter (substrings ok)') + sb_output('sb filter clear : Clears mob filter') + sb_output('sb visible : Toggles scoreboard visibility') + sb_output('sb stat <stat> [<player>]: Shows specific damage stats. Respects filters. If player isn\'t specified, ' .. + 'stats for everyone are displayed. Valid stats are:') + sb_output(dps_db.player_stat_fields:tostring():stripchars('{}"')) + elseif command == 'pos' then + if params[2] then + local posx, posy = tonumber(params[1]), tonumber(params[2]) + settings.display.pos.x = posx + settings.display.pos.y = posy + config.save(settings) + display:set_position(posx, posy) + end + elseif command == 'set' then + if not params[2] then + return + end + + local setting = params[1] + if setting == 'combinepets' then + if params[2] == 'true' then + settings.combinepets = true + elseif params[2] == 'false' then + settings.combinepets = false + else + error("Invalid value for 'combinepets'. Must be true or false.") + return + end + settings:save() + sb_output("Setting 'combinepets' set to " .. tostring(settings.combinepets)) + elseif setting == 'numplayers' then + settings.numplayers = tonumber(params[2]) + settings:save() + display:update() + sb_output("Setting 'numplayers' set to " .. settings.numplayers) + elseif setting == 'bgtransparency' then + settings.display.bg.alpha = tonumber(params[2]) + settings:save() + display:update() + sb_output("Setting 'bgtransparency' set to " .. settings.display.bg.alpha) + elseif setting == 'font' then + settings.display.text.font = params[2] + settings:save() + display:update() + sb_output("Setting 'font' set to " .. settings.display.text.font) + elseif setting == 'sbcolor' then + settings.sbcolor = tonumber(params[2]) + settings:save() + sb_output("Setting 'sbcolor' set to " .. settings.sbcolor) + elseif setting == 'showallidps' then + if params[2] == 'true' then + settings.showallidps = true + elseif params[2] == 'false' then + settings.showallidps = false + else + error("Invalid value for 'showallidps'. Must be true or false.") + return + end + + settings:save() + sb_output("Setting 'showalldps' set to " .. tostring(settings.showallidps)) + elseif setting == 'resetfilters' then + if params[2] == 'true' then + settings.resetfilters = true + elseif params[2] == 'false' then + settings.resetfilters = false + else + error("Invalid value for 'resetfilters'. Must be true or false.") + return + end + + settings:save() + sb_output("Setting 'resetfilters' set to " .. tostring(settings.resetfilters)) + elseif setting == 'showfellow' then + if params[2] == 'true' then + settings.showfellow = true + elseif params[2] == 'false' then + settings.showfellow = false + else + error("Invalid value for 'showfellow'. Must be true or false.") + return + end + + settings:save() + sb_output("Setting 'showfellow' set to " .. tostring(settings.showfellow)) + end + elseif command == 'reset' then + reset() + elseif command == 'report' then + local arg = params[1] + local arg2 = params[2] + + if arg then + if chatmodes:contains(arg) then + if arg == 't' or arg == 'tell' then + if not arg2 then + -- should be a valid player name + error('Invalid argument for report t: Please include player target name.') + return + elseif not arg2:match('^[a-zA-Z]+$') then + error('Invalid argument for report t: ' .. arg2) + end + end + else + error('Invalid parameter passed to report: ' .. arg) + return + end + end + + display:report_summary(arg, arg2) + + elseif command == 'visible' then + display:update() + display:visibility(not settings.visible) + + elseif command == 'filter' then + local subcmd + if params[1] then + subcmd = params[1]:lower() + else + error('Invalid option to //sb filter. See //sb help') + return + end + + if subcmd == 'add' then + for i=2, #params do + dps_db:add_filter(params[i]) + end + display:update() + elseif subcmd == 'clear' then + dps_db:clear_filters() + display:update() + elseif subcmd == 'show' then + display:report_filters() + else + error('Invalid argument to //sb filter') + end + elseif command == 'stat' then + if not params[1] or not dps_db.player_stat_fields:contains(params[1]:lower()) then + error('Must pass a stat specifier to //sb stat. Valid arguments: ' .. + dps_db.player_stat_fields:tostring():stripchars('{}"')) + else + local stat = params[1]:lower() + local player = params[2] + display:show_stat(stat, player) + end + elseif command == 'reportstat' or command == 'rs' then + if not params[1] or not dps_db.player_stat_fields:contains(params[1]:lower()) then + error('Must pass a stat specifier to //sb reportstat. Valid arguments: ' .. + dps_db.player_stat_fields:tostring():stripchars('{}"')) + return + end + + local stat = params[1]:lower() + local arg2 = params[2] -- either a player name or a chatmode + local arg3 = params[3] -- can only be a chatmode + + -- The below logic is obviously bugged if there happens to be a player named "say", + -- "party", "linkshell" etc but I don't care enough to account for those people! + + if chatmodes:contains(arg2) then + -- Arg2 is a chatmode so we assume this is a 3-arg version (no player specified) + display:report_stat(stat, {chatmode = arg2, telltarget = arg3}) + else + -- Arg2 is not a chatmode, so we assume it's a player name and then see + -- if arg3 looks like an optional chatmode. + if arg2 and not arg2:match('^[a-zA-Z]+$') then + -- should be a valid player name + error('Invalid argument for reportstat t ' .. arg2) + return + end + + if arg3 and not chatmodes:contains(arg3) then + error('Invalid argument for reportstat t ' .. arg2 .. ', must be a valid chatmode.') + return + end + + display:report_stat(stat, {player = arg2, chatmode = arg3, telltarget = params[4]}) + end + elseif command == 'fields' then + error("Not implemented yet.") + return + elseif command == 'save' then + if params[1] then + if not params[1]:match('^[a-ZA-Z0-9_-,.:]+$') then + error("Invalid filename: " .. params[1]) + return + end + save(params[1]) + else + save() + end + else + error('Unrecognized command. See //sb help') + end + end +end()) + +local months = { + 'jan', 'feb', 'mar', 'apr', + 'may', 'jun', 'jul', 'aug', + 'sep', 'oct', 'nov', 'dec' +} + + +function save(filename) + if not filename then + local date = os.date("*t", os.time()) + filename = string.format("sb_%s-%d-%d-%d-%d.txt", + months[date.month], + date.day, + date.year, + date.hour, + date.min) + end + local parse = file.new('data/parses/' .. filename) + + if parse:exists() then + local dup_path = file.new(parse.path) + local dup = 0 + + while dup_path:exists() do + dup_path = file.new(parse.path .. '.' .. dup) + dup = dup + 1 + end + parse = dup_path + end + + parse:create() +end + + +-- Resets application state +function reset() + if settings.resetfilters then + dps_db:clear_filters() + end + display:reset() + dps_clock:reset() + dps_db:reset() +end + + +display = Display:new(settings, dps_db) + + +-- Keep updates flowing +local function update_dps_clock() + local player = windower.ffxi.get_player() + local pet + if player ~= nil then + local player_mob = windower.ffxi.get_mob_by_id(player.id) + if player_mob ~= nil then + local pet_index = player_mob.pet_index + if pet_index ~= nil then + pet = windower.ffxi.get_mob_by_index(pet_index) + end + end + end + if player and (player.in_combat or (pet ~= nil and pet.status == 1)) then + dps_clock:advance() + else + dps_clock:pause() + end + + display:update() +end + + +-- Returns all mob IDs for anyone in your alliance, including their pets. +function get_ally_mob_ids() + local allies = T{} + local party = windower.ffxi.get_party() + + for _, member in pairs(party) do + if type(member) == 'table' and member.mob then + allies:append(member.mob.id) + if member.mob.pet_index and member.mob.pet_index> 0 and windower.ffxi.get_mob_by_index(member.mob.pet_index) then + allies:append(windower.ffxi.get_mob_by_index(member.mob.pet_index).id) + end + end + end + + if settings.showfellow then + local fellow = windower.ffxi.get_mob_by_target("ft") + if fellow ~= nil then + allies:append(fellow.id) + end + end + + return allies +end + + +-- Returns true if is someone (or a pet of someone) in your alliance. +function mob_is_ally(mob_id) + -- get zone-local ids of all allies and their pets + return get_ally_mob_ids():contains(mob_id) +end + + +function action_handler(raw_actionpacket) + local actionpacket = ActionPacket.new(raw_actionpacket) + + local category = actionpacket:get_category_string() + + local player = windower.ffxi.get_player() + local pet + if player ~= nil then + local player_mob = windower.ffxi.get_mob_by_id(player.id) + if player_mob ~= nil then + local pet_index = player_mob.pet_index + if pet_index ~= nil then + pet = windower.ffxi.get_mob_by_index(pet_index) + end + end + end + if not player or not (windower.ffxi.get_player().in_combat or (pet ~= nil and pet.status == 1)) then + -- nothing to do + return + end + + for target in actionpacket:get_targets() do + for subactionpacket in target:get_actions() do + if (mob_is_ally(actionpacket.raw.actor_id) and not mob_is_ally(target.raw.id)) then + -- Ignore actions within the alliance, but parse all alliance-outwards or outwards-alliance packets. + local main = subactionpacket:get_basic_info() + local add = subactionpacket:get_add_effect() + local spike = subactionpacket:get_spike_effect() + if main.message_id == 1 then + dps_db:add_m_hit(target:get_name(), create_mob_name(actionpacket), main.param) + elseif main.message_id == 67 then + dps_db:add_m_crit(target:get_name(), create_mob_name(actionpacket), main.param) + elseif main.message_id == 15 or main.message_id == 63 then + dps_db:incr_misses(target:get_name(), create_mob_name(actionpacket)) + elseif main.message_id == 353 then + dps_db:add_r_crit(target:get_name(), create_mob_name(actionpacket), main.param) + elseif T{157, 352, 576, 577}:contains(main.message_id) then + dps_db:add_r_hit(target:get_name(), create_mob_name(actionpacket), main.param) + elseif main.message_id == 353 then + dps_db:add_r_crit(target:get_name(), create_mob_name(actionpacket), main.param) + elseif main.message_id == 354 then + dps_db:incr_r_misses(target:get_name(), create_mob_name(actionpacket)) + elseif main.message_id == 188 then + dps_db:incr_ws_misses(target:get_name(), create_mob_name(actionpacket)) + elseif main.resource and main.resource == 'weapon_skills' and main.conclusion then + dps_db:add_ws_damage(target:get_name(), create_mob_name(actionpacket), main.param, main.spell_id) + -- Siren's Hysteric Assault does HP drain and falls under message_id 802 + elseif main.message_id == 802 then + dps_db:add_damage(target:get_name(), create_mob_name(actionpacket), main.param) + elseif main.conclusion then + if main.conclusion.subject == 'target' and T(main.conclusion.objects):contains('HP') and main.param ~= 0 then + dps_db:add_damage(target:get_name(), create_mob_name(actionpacket), (main.conclusion.verb == 'gains' and -1 or 1)*main.param) + end + end + + if add and add.conclusion then + local actor_name = create_mob_name(actionpacket) + if T{196,223,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}:contains(add.message_id) then + actor_name = string.format("Skillchain(%s%s)", actor_name:sub(1, 3), + actor_name:len() > 3 and '.' or '') + end + if add.conclusion.subject == 'target' and T(add.conclusion.objects):contains('HP') and add.param ~= 0 then + dps_db:add_damage(target:get_name(), actor_name, (add.conclusion.verb == 'gains' and -1 or 1)*add.param) + end + end + if spike and spike.conclusion then + if spike.conclusion.subject == 'target' and T(spike.conclusion.objects):contains('HP') and spike.param ~= 0 then + dps_db:add_damage(target:get_name(), create_mob_name(actionpacket), (spike.conclusion.verb == 'gains' and -1 or 1)*spike.param) + end + end + elseif (mob_is_ally(target.raw.id) and not mob_is_ally(actionpacket.raw.actor_id)) then + local spike = subactionpacket:get_spike_effect() + if spike and spike.conclusion then + if spike.conclusion.subject == 'actor' and T(spike.conclusion.objects):contains('HP') and spike.param ~= 0 then + dps_db:add_damage(create_mob_name(actionpacket), target:get_name(), (spike.conclusion.verb == 'loses' and 1 or -1)*spike.param) + end + end + end + end + end +end + +ActionPacket.open_listener(action_handler) + + function find_pet_owner_name(actionpacket) + local pet = windower.ffxi.get_mob_by_id(actionpacket:get_id()) + local party = windower.ffxi.get_party() + + local name = nil + + for _, member in pairs(party) do + if type(member) == 'table' and member.mob then + if member.mob.pet_index and member.mob.pet_index> 0 and pet.index == member.mob.pet_index then + name = member.mob.name + break + end + end + end + return name, pet.name + end + + function create_mob_name(actionpacket) + local actor = actionpacket:get_actor_name() + local result = '' + local owner, pet = find_pet_owner_name(actionpacket) + if owner ~= nil then + if string.len(actor) > 8 then + result = string.sub(actor, 1, 7)..'.' + else + result = actor + end + if settings.combinepets then + result = '' + else + result = actor + end + if pet then + result = '('..owner..')'..' '..pet + end + else + return actor + end + return result + end + +config.register(settings, function(settings) + update_dps_clock:loop(settings.UpdateFrequency) + display:visibility(display.visible and windower.ffxi.get_info().logged_in) +end) + + +--[[ +Copyright � 2013-2014, Jerry Hebert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Scoreboard nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +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 JERRY HEBERT 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. +]] |