summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/scoreboard
diff options
context:
space:
mode:
authorchai <chaifix@163.com>2021-11-15 13:53:59 +0800
committerchai <chaifix@163.com>2021-11-15 13:53:59 +0800
commit942a030afd348ab2e02eac8054b43e3c3a72ea48 (patch)
treea13459f39a3d2f1b533fbd1b5ab523d7a621f673 /Data/BuiltIn/Libraries/lua-addons/addons/scoreboard
parente307051a56a54c27f10438fd2025edf61d0dfeed (diff)
*rename
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/scoreboard')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/damagedb.lua179
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/data/settings.xml25
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/display.lua443
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/dpsclock.lua96
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/mergedplayer.lua354
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/player.lua178
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/readme.txt120
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/scoreboard/scoreboard.lua555
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.
+]]