summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/GearSwap/helper_functions.lua
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/GearSwap/helper_functions.lua
parente307051a56a54c27f10438fd2025edf61d0dfeed (diff)
*rename
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/GearSwap/helper_functions.lua')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/GearSwap/helper_functions.lua1255
1 files changed, 1255 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/GearSwap/helper_functions.lua b/Data/BuiltIn/Libraries/lua-addons/addons/GearSwap/helper_functions.lua
new file mode 100644
index 0000000..0034d60
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/GearSwap/helper_functions.lua
@@ -0,0 +1,1255 @@
+--Copyright (c) 2013~2016, Byrthnoth
+--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 <addon name> 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 <your name> 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.
+
+-----------------------------------------------------------------------------------
+--Name: string.lower()
+--Args:
+---- message (string): Message to be forced to lower case
+-----------------------------------------------------------------------------------
+--Returns:
+---- Lower case message (or not, if the language or message is invalid)
+-----------------------------------------------------------------------------------
+function string.lower(message)
+ if message and type(message) == 'string' and language == 'english' then
+ return __raw.lower(message)
+ elseif message and type(message) == 'string' then
+ return message:gsub('[A-Z]',function (letter) return string.char(letter:byte(1)+32) end)
+ else
+ return message
+ end
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: string.upper()
+--Args:
+---- message (string): Message to be forced to upper case
+-----------------------------------------------------------------------------------
+--Returns:
+---- Upper case message (or not, if the language or message is invalid)
+-----------------------------------------------------------------------------------
+function string.upper(message)
+ if message and type(message) == 'string' and language == 'english' then
+ return __raw.upper(message)
+ elseif message and type(message) == 'string' then
+ return message:gsub('[a-z]',function (letter) return string.char(letter:byte(1)-32) end)
+ else
+ return message
+ end
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: fieldsearch()
+--Args:
+---- message (string): Message to be searched
+-----------------------------------------------------------------------------------
+--Returns:
+---- Table of strings that contained {something}.
+---- Seems to be trying to exclude ${actor} and ${target}, but not.
+-----------------------------------------------------------------------------------
+function fieldsearch(message)
+ local fields = T{}
+ string.gsub(message,"{(.-)}", function(a) if a ~= '${actor}' and a ~= '${target}' then fields:append(a) end end)
+ return fields
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: strip()
+--Args:
+---- name (string): Name to be slugged
+-----------------------------------------------------------------------------------
+--Returns:
+---- string with a gsubbed version of name that converts numbers to Roman numerals
+-------- removes non-letter/numbers, and forces it to lower case.
+-----------------------------------------------------------------------------------
+function strip(name)
+ return name:gsub('4','iv'):gsub('9','ix'):gsub('0','p'):gsub('3','iii'):gsub('2','ii'):gsub('1','i'):gsub('8','viii'):gsub('7','vii'):gsub('6','vi'):gsub('5','v'):gsub('[^%a]',''):lower()
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: user_key_filter()
+--Args:
+---- val (key): potential key to be modified
+-----------------------------------------------------------------------------------
+--Returns:
+---- Filtered key
+-----------------------------------------------------------------------------------
+function user_key_filter(val)
+ return type(val) == 'string' and string.lower(val) or val
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: make_user_table()
+--Args:
+---- None
+-----------------------------------------------------------------------------------
+--Returns:
+---- Table with case-insensitive keys
+-----------------------------------------------------------------------------------
+function make_user_table()
+ return setmetatable({}, user_data_table)
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: unify_slots(g)
+-- Filters the provided gear table to only known slots, and then runs a map
+-- on the table to make sure all keys are the accepted versions for each.
+----Args:
+-- g - A dictionary table containing a gear set.
+-----------------------------------------------------------------------------------
+----Returns:
+-- A table simplified to only acceptable slots.
+-----------------------------------------------------------------------------------
+function unify_slots(g)
+ local g1 = table.key_filter(g, is_slot_key)
+ return table.key_map(g1, get_default_slot)
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: is_slot_key(k)
+-- Checks to see if key 'k' is known in the slot_map array, and that slot has not
+-- been disabled.
+----Args:
+-- k - A key to a gear slot in a gear table.
+-----------------------------------------------------------------------------------
+----Returns:
+-- True if the key is recognized in the slot_map table, and that slot is enabled;
+-- otherwise false.
+-----------------------------------------------------------------------------------
+function is_slot_key(k)
+ return slot_map[k]
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: make_empty_item_table(slot)
+-- Make an empty item table with slot = slot
+----Args:
+-- slot - The index of the item table
+-----------------------------------------------------------------------------------
+----Returns:
+-- A zero'd table with slot = slot
+-----------------------------------------------------------------------------------
+function make_empty_item_table(slot)
+ return {id=0,
+ count = 0,
+ bazaar = 0,
+ extdata = string.char(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0),
+ status = 0,
+ slot = slot}
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: make_inventory_table()
+-- Make a table of empty item tables
+----Args:
+-- none
+-----------------------------------------------------------------------------------
+----Returns:
+-- A table of 80 empty item tables indexed 1-80
+-----------------------------------------------------------------------------------
+function make_inventory_table()
+ local tab = {}
+ for i = 0,80 do
+ tab[i] = make_empty_item_table(i)
+ end
+ return tab
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: to_windower_api(str)
+-- Takes strings and converts them to resources table key format
+----Args:
+-- str - String to be converted to the windower API version
+-----------------------------------------------------------------------------------
+----Returns:
+-- a lower case string with ' ' replaced with '_'
+-----------------------------------------------------------------------------------
+function to_windower_api(str)
+ return __raw.lower(str:gsub(' ','_'))
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: to_windower_bag_api(str)
+-- Takes strings and converts them to resources table key format
+----Args:
+-- str - String to be converted to the windower bag API version
+-----------------------------------------------------------------------------------
+----Returns:
+-- a lower case string with ' ' replaced with ''
+-----------------------------------------------------------------------------------
+function to_windower_bag_api(str)
+ return __raw.lower(str:gsub(' ',''))
+end
+
+-----------------------------------------------------------------------------------
+----Name: to_bag_api(str)
+-- Takes strings and converts them to resources table key format
+----Args:
+-- str - String to be converted to the windower bag API version
+-----------------------------------------------------------------------------------
+----Returns:
+-- a lower case string with ' ' eliminated
+-----------------------------------------------------------------------------------
+function to_bag_api(str)
+ return __raw.lower(str:gsub(' ',''))
+end
+
+-----------------------------------------------------------------------------------
+----Name: to_windower_compact(str)
+-- Takes strings and converts them to a compact version of the resource table key
+----Args:
+-- str - String to be converted to the windower API version
+-----------------------------------------------------------------------------------
+----Returns:
+-- a lower case string with ' ' replaced with ''
+-----------------------------------------------------------------------------------
+function to_windower_compact(str)
+ return __raw.lower(str:gsub(' ',''))
+end
+
+-----------------------------------------------------------------------------------
+----Name: get_job_names()
+-- Returns the short and long form of the job name
+----Args:
+-- id - Job ID
+-----------------------------------------------------------------------------------
+----Returns:
+-- short and long form of the job name
+-----------------------------------------------------------------------------------
+function get_job_names(id)
+ if res.jobs[id] then
+ return res.jobs[id][language..'_short'], res.jobs[id][language]
+ else
+ return 'NONE', 'None'
+ end
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: update_job_names()
+-- Updates job names in the global player array
+----Args:
+-- none
+-----------------------------------------------------------------------------------
+----Returns:
+-- none
+-----------------------------------------------------------------------------------
+function update_job_names()
+ player.main_job,player.main_job_full = get_job_names(player.main_job_id)
+ player.sub_job, player.sub_job_full = get_job_names(player.sub_job_id)
+ player.job = player.main_job..'/'..player.sub_job
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: get_default_slot(k)
+-- Given a generally known slot key, return the default version of that key.
+----Args:
+-- k - A gear slot key.
+-----------------------------------------------------------------------------------
+----Returns:
+-- Returns the default slot key that matches the provided key.
+-----------------------------------------------------------------------------------
+function get_default_slot(k)
+ if slot_map[k] then
+ return toslotname(slot_map[k])
+ end
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: set_merge(baseSet, ...)
+-- Merges any additional gear sets (...) into the provided base set.
+-- Ensures that only valid slot keys/elements are used in the combined set.
+----Args:
+-- respect_disable - boolean indicating whether the disable_table should be respected.
+-- baseSet - The set that all the other sets are combined into. May be an empty set.
+-----------------------------------------------------------------------------------
+----Returns:
+-- Returns the modified base set, after all other sets have been merged into it.
+-----------------------------------------------------------------------------------
+function set_merge(respect_disable, baseSet, ...)
+ local combineSets = {...}
+
+ local canCombine = table.all(combineSets, function(t) return type(t) == 'table' end)
+ if not canCombine then
+ -- the code that called equip() or set_combine() is #3 on the stack from here
+ error("Trying to combine non-gear sets.", 3)
+ end
+
+ -- Take the list of tables we're given and cleans them up, so that they
+ -- only contain acceptable slot key entries.
+ local cleanSetsList = table.map(combineSets, unify_slots)
+
+ -- Combine the provided sets into combinedSet. If anything is blocked by having
+ -- the slot disabled, assign the item to the not_sent_out_equip table.
+ for _,set in pairs(cleanSetsList) do
+ for slot,item in pairs(set) do
+ if respect_disable and disable_table[slot_map[slot]] then
+ not_sent_out_equip[slot] = item
+ else
+ baseSet[slot] = item
+ end
+ end
+ end
+
+ return baseSet
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: parse_set_to_keys(str)
+-- Function to parse a string representation of a table into a list of keys that
+-- that can be used to select that table.
+----Args:
+-- str - Input can be a string, or a table of strings (which will be concatenated
+-- into a single string with spaces as intervals).
+--
+-- Example:
+-- Input: sets.precast.WS["Rudra's Storm"]['Ltng. Threnody'].Acc
+-- Output: [sets, precast, WS, Rudra's Storm, Ltng. Threnody, Acc]
+-----------------------------------------------------------------------------------
+----Returns:
+-- Returns a list of keys parsed from the provided input.
+-----------------------------------------------------------------------------------
+function parse_set_to_keys(str)
+ if type(str) == 'table' then
+ str = table.concat(str, ' ')
+ end
+
+ -- Parsing results get pushed into the result list.
+ local result = L{}
+
+ local remainder = str
+ local key
+ local stop
+ local sep = '.'
+ local count = 0
+
+ -- Loop as long as remainder hasn't been nil'd or reduced to 0 characters, but only to a maximum of 30 tries.
+ while remainder ~= "" and count < 30 do
+ -- Try aaa.bbb set names first
+ while sep == '.' do
+ _,_,key,sep,remainder = remainder:find("^([^%.%[]*)(%.?%[?)(.*)")
+ -- "key" is everything that is not . or [ 0 or more times.
+ -- "sep" is the next divider, which is necessarily . or [
+ -- "remainder" is everything after that
+ result:append(key)
+ end
+
+ -- Then try aaa['bbb'] set names.
+ -- Be sure to account for both single and double quote enclosures.
+ -- Ignore periods contained within quote strings.
+ while sep == '[' do
+ _,_,sep,remainder = remainder:find([=[^(%'?%"?)(.*)]=]) --' --block bad text highlighting
+ -- "sep" is the first ' or " found (or nil)
+ -- remainder is everything after that (or nil)
+ if sep == "'" then
+ _,_,key,stop,sep,remainder = remainder:find("^([^']+)('])(%.?%[?)(.*)")
+ elseif sep == '"' then
+ _,_,key,stop,sep,remainder = remainder:find('^([^"]+)("])(%.?%[?)(.*)')
+ end
+ if not sep or #sep == 0 then
+ -- If there is no single or double quote detected, attempt to treat the index as a number or boolean
+ local _,_,pot_key,pot_stop,pot_sep,pot_remainder = remainder:find('^([^%]]+)(])(%.?%[?)(.*)')
+ if tonumber(pot_key) then
+ key,stop,sep,remainder = tonumber(pot_key),pot_stop,pot_sep,pot_remainder
+ elseif pot_key == 'true' then
+ key,stop,sep,remainder = true,pot_stop,pot_sep,pot_remainder
+ elseif pot_key == 'false' then
+ key,stop,sep,remainder = false,pot_stop,pot_sep,pot_remainder
+ elseif pot_key and pot_key ~= "" then
+ key,stop,sep,remainder = pot_key,pot_stop,pot_sep,pot_remainder
+ end
+ end
+ result:append(key)
+ end
+
+ count = count +1
+ end
+
+ return result
+end
+
+
+-----------------------------------------------------------------------------------
+----Name: get_set_from_keys(keys)
+-- Function to take a list of keys select the set they point to, if possible.
+----Args:
+-- keys - A List of strings intended to be keys in progressively nested tables.
+-- The list is presumed to be based on the 'sets' table, and will start from that
+-- point if it is not explicitly provided in the key list.
+-----------------------------------------------------------------------------------
+----Returns:
+-- Returns the set if found, or nil if not.
+-----------------------------------------------------------------------------------
+function get_set_from_keys(keys)
+ local set = keys[1] == 'sets' and _G or sets
+ for key in (keys.it or it)(keys) do
+ if key == nil then
+ return nil
+ end
+ set = set[key]
+ if not set then
+ return nil
+ end
+ end
+
+ return set
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: initialize_arrow_offset(mob_table)
+--Desc: Returns the current target arrow offset.
+--Args:
+---- mob_table - Monster table of the target monster
+-----------------------------------------------------------------------------------
+--Returns:
+---- table - Keys x, y, and z with the respective current offsets from the target.
+-----------------------------------------------------------------------------------
+function initialize_arrow_offset(mob_table)
+ local backtab = {}
+ local arrow = windower.ffxi.get_info().target_arrow
+
+ if arrow.x == 0 and arrow.y == 0 and arrow.z == 0 then
+ return arrow
+ end
+
+ backtab.x = arrow.x-mob_table.x
+ backtab.y = arrow.y-mob_table.y
+ backtab.z = arrow.z-mob_table.z
+ return backtab
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: assemble_action_packet(target_id,target_index,category,spell_id)
+--Desc: Puts together an "action" packet (0x1A)
+--Args:
+---- target_id - The target's ID
+---- target_index - The target's index
+---- category - The action's category. (3 = MA, 7 = WS, 9 = JA, 16 = RA, 25 = MS)
+---- spell_ID - The current spell's ID
+-----------------------------------------------------------------------------------
+--Returns:
+---- string - An action packet. First four bytes are dummy bytes.
+-----------------------------------------------------------------------------------
+function assemble_action_packet(target_id,target_index,category,spell_id,arrow_offset)
+ local outstr = string.char(0x1A,0x08,0,0)
+ outstr = outstr..string.char( (target_id%256), math.floor(target_id/256)%256, math.floor( (target_id/65536)%256) , math.floor( (target_id/16777216)%256) )
+ outstr = outstr..string.char( (target_index%256), math.floor(target_index/256)%256)
+ outstr = outstr..string.char( (category%256), math.floor(category/256)%256)
+
+ if category == 16 then
+ spell_id = 0
+ end
+
+ outstr = outstr..string.char( (spell_id%256), math.floor(spell_id/256)%256)..string.char(0,0) .. 'fff':pack(arrow_offset.x,arrow_offset.z,arrow_offset.y)
+ return outstr
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: assemble_use_item_packet(target_id,target_index,item)
+--Desc: Puts together a "use item" packet (0x37)
+--Args:
+---- target_id - The target's ID
+---- target_index - The target's index
+---- item_id - The id for the current item
+-----------------------------------------------------------------------------------
+--Returns:
+---- string - A use item packet. First four bytes are dummy bytes.
+-----------------------------------------------------------------------------------
+function assemble_use_item_packet(target_id,target_index,item_id)
+ local outstr = string.char(0x37,0x0A,0,0)
+ outstr = outstr..string.char( (target_id%256), math.floor(target_id/256)%256, math.floor( (target_id/65536)%256) , math.floor( (target_id/16777216)%256) )
+ outstr = outstr..string.char(0,0,0,0)
+ outstr = outstr..string.char( (target_index%256), math.floor(target_index/256)%256)
+ inventory_index,bag_id = find_usable_item(item_id)
+ if inventory_index then
+ outstr = outstr..string.char(inventory_index%256)..string.char(0,bag_id,0,0,0)
+ else
+ msg.debugging('Proposed item: '..(res.items[item_id][language] or item_id)..' not found in inventory.')
+ return
+ end
+ return outstr
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: assemble_menu_item_packet(target_id,target_index,item)
+--Desc: Puts together a "menu item" packet (0x36)
+--Args:
+---- target_id - The target's ID
+---- target_index - The target's index
+---- item_id - The id for the current item
+-----------------------------------------------------------------------------------
+--Returns:
+---- string - A use item packet. First four bytes are dummy bytes.
+-----------------------------------------------------------------------------------
+function assemble_menu_item_packet(target_id,target_index,...)
+ local outstr = string.char(0x36,0x20,0,0)
+ -- Message is coming out too short by 12 characters
+
+ -- Target ID
+ outstr = outstr.."I":pack(target_id)
+ local item_ids,counts,count = {...},{},0
+ for i,v in pairs(item_ids) do
+ if res.items[v] then
+ counts[v] = (counts[v] or 0) + 1
+ count = count + 1
+ end
+ end
+
+ local unique_items = 0
+ for i,v in pairs(counts) do
+ outstr = outstr.."I":pack(v)
+ unique_items = unique_items + 1
+ end
+ if unique_items > 9 then
+ msg.debugging('Too many items ('..unique_items..') passed to the assemble_menu_item_packet function')
+ return
+ end
+ while #outstr < 0x30 do
+ outstr = outstr..string.char(0)
+ end
+
+ -- Inventory Index for the one unit
+
+ for i,v in pairs(counts) do
+ inventory_index = find_inventory_item(i)
+ if inventory_index then
+ outstr = outstr..string.char(inventory_index%256)
+ else
+ msg.debugging('Proposed item: '..(res.items[i][language] or i)..' not found in inventory.')
+ return
+ end
+ end
+ while #outstr < 0x3A do
+ outstr = outstr..string.char(0)
+ end
+ -- Target Index
+ outstr = outstr.."H":pack(target_index)
+ -- Only one item being traded
+ outstr = outstr..string.char(unique_items,0,0,0)
+ return outstr
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: find_inventory_item(item_id)
+--Desc: Finds a npc trade item in normal inventory. Assumes items array
+-- is accurate already.
+--Args:
+---- item_id - The resource line for the current item
+-----------------------------------------------------------------------------------
+--Returns:
+---- inventory_index - The item's use inventory index (if it exists)
+---- bag_id - The item's bag ID (if it exists)
+-----------------------------------------------------------------------------------
+function find_inventory_item(item_id)
+ for i,v in pairs(items.inventory) do
+ if type(v) == 'table' and v.id == item_id and v.status == 0 then
+ return i
+ end
+ end
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: find_usable_item(item_id,bool)
+--Desc: Finds a usable item in temporary or normal inventory. Assumes items array
+-- is accurate already.
+--Args:
+---- item_id - The resource line for the current item
+-----------------------------------------------------------------------------------
+--Returns:
+---- inventory_index - The item's use inventory index (if it exists)
+---- bag_id - The item's bag ID (if it exists)
+-----------------------------------------------------------------------------------
+function find_usable_item(item_id)
+ for _,bag in ipairs(usable_item_bags) do
+ for i,v in pairs(items[to_windower_bag_api(bag.en)]) do
+ if type(v) == 'table' and v.id == item_id and is_usable_item(v,bag.id) then
+ return i, bag.id
+ end
+ end
+ end
+end
+
+-----------------------------------------------------------------------------------
+--Name: is_usable_item(i_tab)
+--Desc: Determines whether the item table belongs to a usable item.
+--Args:
+---- i_tab - current item table
+---- bag_id - The item's bag ID
+-----------------------------------------------------------------------------------
+--Returns:
+---- true or false to indicate whether the item is usable
+-----------------------------------------------------------------------------------
+function is_usable_item(i_tab,bag_id)
+ local ext = extdata.decode(i_tab)
+ if ext.type == 'Enchanted Equipment' and ext.usable then
+ return i_tab.status == 5
+ elseif i_tab.status == 0 and bag_id < 4 then
+ return true
+ end
+ return false
+end
+
+-----------------------------------------------------------------------------------
+--Name: number_of_jps(jp_tab)
+--Desc: Gives the total number of job points spent on that job
+--Args:
+---- jp_tab - One table from windower.ffxi.get_player().job_points[job]
+-----------------------------------------------------------------------------------
+--Returns:
+---- The total number of job points spent on that job.
+-----------------------------------------------------------------------------------
+function number_of_jps(jp_tab)
+ local count = 0
+ for _,v in pairs(jp_tab) do
+ count = count + v*(v+1)
+ end
+ return count/2
+end
+
+-----------------------------------------------------------------------------------
+--Name: filter_pretarget(spell)
+--Desc: Determines whether the current player is capable of using the proposed action
+---- at pretarget.
+--Args:
+---- action - current action
+-----------------------------------------------------------------------------------
+--Returns:
+---- false to cancel further command processing and just return the command.
+-----------------------------------------------------------------------------------
+function filter_pretarget(action)
+ local category = outgoing_action_category_table[unify_prefix[action.prefix]]
+ local bool = true
+ local err
+ if world.in_mog_house then
+ msg.debugging("Unable to execute commands. Currently in a Mog House zone.")
+ return false
+ elseif category == 3 then
+ local available_spells = windower.ffxi.get_spells()
+ bool,err = check_spell(available_spells,action)
+ elseif category == 7 then
+ local available = windower.ffxi.get_abilities().weapon_skills
+ if not table.contains(available,action.id) then
+ bool,err = false,"Unable to execute command. You do not have access to that weapon skill."
+ end
+ elseif category == 9 then
+ local available = windower.ffxi.get_abilities().job_abilities
+ if not table.contains(available,action.id) then
+ bool,err = false,"Unable to execute command. You do not have access to that job ability."
+ end
+ elseif category == 25 and (not player.main_job_id == 23 or not windower.ffxi.get_mjob_data().species or
+ not res.monstrosity[windower.ffxi.get_mjob_data().species] or not res.monstrosity[windower.ffxi.get_mjob_data().species].tp_moves[action.id] or
+ not (res.monstrosity[windower.ffxi.get_mjob_data().species].tp_moves[action.id] <= player.main_job_level)) then
+ -- Monstrosity filtering
+ msg.debugging("Unable to execute command. You do not have access to that monsterskill ("..(res.monster_skills[action.id][language] or action.id)..")")
+ return false
+ end
+
+ if err then
+ msg.debugging(err)
+ end
+ return bool
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: check_spell(available_spells,spell)
+--Desc: Determines whether the current player is capable of using the proposed spell
+---- at precast.
+--Args:
+---- available_spells - current set of available spells
+---- spell - current spell table
+-----------------------------------------------------------------------------------
+--Returns:
+---- false if the spell is not currently accessible
+-----------------------------------------------------------------------------------
+function check_spell(available_spells,spell)
+ -- Filter for spells that you do not know. Exclude Impact / Dispelga.
+ local spell_jobs = copy_entry(res.spells[spell.id].levels)
+ if not available_spells[spell.id] and not (spell.id == 503 or spell.id == 417 or spell.id == 360) then
+ return false,"Unable to execute command. You do not know that spell ("..(res.spells[spell.id][language] or spell.id)..")"
+ -- Filter for spells that you know, but do not currently have access to
+ elseif (not spell_jobs[player.main_job_id] or not (spell_jobs[player.main_job_id] <= player.main_job_level or
+ (spell_jobs[player.main_job_id] >= 100 and number_of_jps(player.job_points[__raw.lower(res.jobs[player.main_job_id].ens)]) >= spell_jobs[player.main_job_id]) ) ) and
+ (not spell_jobs[player.sub_job_id] or not (spell_jobs[player.sub_job_id] <= player.sub_job_level)) and not (player.main_job_id == 23) then
+ return false,"Unable to execute command. You do not have access to that spell ("..(res.spells[spell.id][language] or spell.id)..")"
+ -- At this point, we know that it is technically castable by this job combination if the right conditions are met.
+ elseif player.main_job_id == 20 and ((addendum_white[spell.id] and not buffactive[401] and not buffactive[416]) or
+ (addendum_black[spell.id] and not buffactive[402] and not buffactive[416])) and
+ not (spell_jobs[player.sub_job_id] and spell_jobs[player.sub_job_id] <= player.sub_job_level) then
+ return false,"Unable to execute command. Addendum required for that spell ("..(res.spells[spell.id][language] or spell.id)..")"
+ elseif player.sub_job_id == 20 and ((addendum_white[spell.id] and not buffactive[401] and not buffactive[416]) or
+ (addendum_black[spell.id] and not buffactive[402] and not buffactive[416])) and
+ not (spell_jobs[player.main_job_id] and (spell_jobs[player.main_job_id] <= player.main_job_level or
+ (spell_jobs[player.main_job_id] >= 100 and number_of_jps(player.job_points[__raw.lower(res.jobs[player.main_job_id].ens)]) >= spell_jobs[player.main_job_id]) ) ) then
+ return false,"Unable to execute command. Addendum required for that spell ("..(res.spells[spell.id][language] or spell.id)..")"
+ elseif spell.type == 'BlueMagic' and not ((player.main_job_id == 16 and table.contains(windower.ffxi.get_mjob_data().spells,spell.id))
+ or unbridled_learning_set[spell.english]) and
+ not (player.sub_job_id == 16 and table.contains(windower.ffxi.get_sjob_data().spells,spell.id)) then
+ -- This code isn't hurting anything, but it doesn't need to be here either.
+ return false,"Unable to execute command. Blue magic must be set to cast that spell ("..(res.spells[spell.id][language] or spell.id)..")"
+ elseif spell.type == 'Ninjutsu' then
+ if player.main_job_id ~= 13 and player.sub_job_id ~= 13 then
+ return false,"Unable to make action packet. You do not have access to that spell ("..(spell[language] or spell.id)..")"
+ elseif not player.inventory[tool_map[spell.english][language]] and not (player.main_job_id == 13 and player.inventory[universal_tool_map[spell.english][language]]) then
+ return false,"Unable to make action packet. You do not have the proper tools."
+ end
+ end
+ return true
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: filter_precast(spell)
+--Desc: Determines whether the current player is capable of using the proposed spell
+---- at precast.
+--Args:
+---- spell - current spell table
+-----------------------------------------------------------------------------------
+--Returns:
+---- false to block the outgoing packet
+-----------------------------------------------------------------------------------
+function filter_precast(spell)
+ if not spell.target.id or not spell.target.index then
+ if debugging.general then msg.debugging('No target id or index') end
+ return false
+ end
+ return true
+end
+
+
+local cmd_reg = {}
+Command_Registry = {}
+
+function Command_Registry.new()
+ local new_instance = {_self={last_removed=os.clock()}}
+ local function remove_old_entries (t)
+ -- Removes old command registry entries.
+ for i,v in pairs(t) do
+ local lim = (type(v) == 'table' and (v.spell and v.spell.cast_time and v.spell.cast_time*1.1+2 or
+ v.spell and v.spell.prefix=='/pet' and 5 or
+ v.spell and v.spell.action_type and delay_map_to_action_type[v.spell.action_type] or
+ 3) + (v.pretarget_cast_delay or 0) + (v.precast_cast_delay or 0))
+ -- Sets it to normal casting time + 10% +1 for anything with a defined cast_time, or 1 if there is no defined cast time.
+ if tonumber(i) and os.time()-i >= lim then
+ cmd_reg.delete_entry(t,i)
+ end
+ end
+ return os.clock()
+ end
+
+ return setmetatable(new_instance, {__index = function(t, k)
+ if os.clock() - rawget(rawget(t,'_self'),'last_removed') > 0.04 then
+ rawset(rawget(t,'_self'),'last_removed', remove_old_entries(t))
+ end
+ if rawget(cmd_reg, k) ~= nil then
+ return rawget(cmd_reg,k)
+ else
+ return rawget(t,k)
+ end
+ end})
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: cmd_reg:new_entry(sp)
+--Desc: Makes a new entry in command_registry.
+--Args:
+---- sp - Resources line for the current spell
+-----------------------------------------------------------------------------------
+--Returns:
+---- ts - index for command_registry
+-----------------------------------------------------------------------------------
+function cmd_reg:new_entry(sp)
+ local ts = os.time()
+ while rawget(self,ts) do
+ ts = ts+0.001
+ end
+ rawset(self,ts,{pretarget_cast_delay=0, precast_cast_delay=0, cancel_spell=false, new_target=false, current_event='nascent', spell=sp, timestamp=ts,target_arrow={x=0,y=0,z=0}})
+ if debugging.command_registry then
+ msg.addon_msg('Creating a new command_registry entry: '..windower.to_shift_jis(tostring(ts)..' '..tostring(self[ts])))
+ end
+ return ts
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: cmd_reg:delete_entry(ts)
+--Desc: Makes a new entry in command_registry.
+--Args:
+---- ts - timestamp of the command registry entry to be deleted
+-----------------------------------------------------------------------------------
+--Returns:
+---- bool - true indicates a successful deletion
+-----------------------------------------------------------------------------------
+function cmd_reg:delete_entry(ts)
+ if rawget(self,ts) then
+ if debugging.command_registry then
+ msg.debugging('Deleting a command_registry entry: '..windower.to_shift_jis(tostring(ts)..' '..tostring(rawget(self,ts))))
+ end
+ rawset(self,ts,nil)
+ return true
+ elseif debugging.command_registry then
+ msg.debugging('Attempted to delete a command_registry entry that did not exist: '..windower.to_shift_jis(tostring(ts) ))
+ end
+ return false
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: cmd_reg:find_by_spell(value)
+--Desc: Returns the proper unified prefix, or "Monster" in the case of a monster action
+--Args:
+---- typ - 'spell', 'timestamp', or 'id'
+---- value - The spell, timestamp, or id
+---- Currently the ID and Timestamp options are unused.
+-----------------------------------------------------------------------------------
+--Returns:
+---- timestamp index of command_registry
+-----------------------------------------------------------------------------------
+function cmd_reg:find_by_spell(value)
+ -- Finds all entries of a given spell in the table.
+ -- Returns the one with the most recent timestamp.
+ -- Actions that do not have timestamps yet (have not hit midcast) are given lowest priority.
+ local potential_entries,current_time,winner,ts = {},os.time()
+ for i,v in pairs(self) do
+ if type(v) == 'table' and v.spell and v.spell.prefix == value.prefix and v.spell.name == value.name then
+ potential_entries[i] = v.timestamp or 0
+ elseif type(v) == 'table' and v.spell and v.spell.english == 'Double-Up' and value.type == 'CorsairRoll' then
+ -- Double Up ability uses will return action packets that match Corsair Rolls rather than Double Up
+ potential_entries[i] = v.timestamp or 0
+ end
+ end
+ for i,v in pairs(potential_entries) do
+ if not winner or (current_time - v < current_time - winner) then
+ winner = v
+ ts = i
+ end
+ end
+ return ts
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: cmd_reg:find_by_time()
+--Desc: Finds the most recent command_registry entry
+--Args:
+---- none
+-----------------------------------------------------------------------------------
+--Returns:
+---- ts,discovered entry
+-----------------------------------------------------------------------------------
+function cmd_reg:find_by_time(target_time)
+ local time_stamp,ts
+ target_time = target_time or os.time()
+
+ -- Iterate over command_registry looking for the spell with the closest timestamp.
+ -- Call aftercast with this spell's information (interrupted) if one is found.
+ for i,v in pairs(self) do
+ if not time_stamp or (type(v) == 'table' and v.timestamp and ((target_time - v.timestamp) < (target_time - time_stamp))) then
+ time_stamp = v.timestamp
+ ts = i
+ end
+ end
+ if time_stamp then
+ return ts,table.reassign({},self[ts])
+ end
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: cmd_reg:delete_by_id(id)
+--Desc: Deletes all command_registry entry based that match a given target ID.
+--Args:
+---- id - ID of the target
+-----------------------------------------------------------------------------------
+--Returns:
+---- ts,last_entry for the deleted entry
+-----------------------------------------------------------------------------------
+function cmd_reg:delete_by_id(id)
+ local ts,last_entry
+ for i,v in pairs(self) do
+ if v.spell and v.spell.target then
+ if v.spell.target.id == id then
+ last_entry = table.reassign({},self[i])
+ ts = i
+ self[i] = nil
+ end
+ end
+ end
+ return ts,last_entry
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: copy_entry(tab)
+--Desc: Copies a table into a new table while preserving its metatable.
+-- Designed for copying resources entries.
+--Args:
+---- tab - Resources table.
+-----------------------------------------------------------------------------------
+--Returns:
+---- ret - New table that has the same metatable and content as the original table.
+-----------------------------------------------------------------------------------
+function copy_entry(tab)
+ if not tab then return nil end
+ local ret = setmetatable(table.reassign({},tab),getmetatable(tab))
+ return ret
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: get_spell(act)
+--Desc: Takes an action table and returns a modified resource line
+--Args:
+---- act - action table in the same format as event_action
+-----------------------------------------------------------------------------------
+--Returns:
+---- spell - Resource line of the current spell
+-----------------------------------------------------------------------------------
+function get_spell(act)
+ local spell, abil_ID, effect_val
+ local msg_ID = act.targets[1].actions[1].message
+
+ if T{7,8,9}:contains(act.category) then
+ abil_ID = act.targets[1].actions[1].param
+ elseif T{3,4,5,6,11,13,14,15}:contains(act.category) then
+ abil_ID = act.param
+ effect_val = act.targets[1].actions[1].param
+ end
+
+ if act.category == 12 or act.category == 2 then
+ spell = copy_entry(resources_ranged_attack)
+ else
+ if not res.action_messages[msg_ID] or msg_ID == 31 then
+ if act.category == 4 or act.category == 8 then
+ spell = spell_complete(copy_entry(res.spells[abil_ID]))
+ if act.category == 4 and spell then spell.recast = act.recast end
+ elseif T{6,13,14,15}:contains(act.category) then
+ spell = spell_complete(copy_entry(res.job_abilities[abil_ID])) -- May have to correct for charmed pets some day, but I'm not sure there are any monsters with TP moves that give no message.
+ elseif T{3,7}:contains(act.category) then
+ spell = spell_complete(copy_entry(res.weapon_skills[abil_ID]))
+ elseif T{5,9}:contains(act.category) then
+ spell = copy_entry(res.items[abil_ID])
+ else
+ spell = {name=tostring(msg_ID)}
+ end
+
+ return spell
+ end
+
+
+ local fields = fieldsearch(res.action_messages[msg_ID].english) -- ENGLISH
+
+ if table.contains(fields,'spell') then
+ spell = copy_entry(res.spells[abil_ID])
+ if act.category == 4 then spell.recast = act.recast end
+ elseif table.contains(fields,'ability') then
+ spell = copy_entry(res.job_abilities[abil_ID])
+ elseif table.contains(fields,'weapon_skill') then
+ if abil_ID > 255 then -- WZ_RECOVER_ALL is used by chests in Limbus
+ spell = copy_entry(res.monster_abilities[abil_ID])
+ if not spell then
+ spell = {id=abil_ID,english='Special Attack'}
+ end
+ elseif abil_ID < 256 then
+ spell = copy_entry(res.weapon_skills[abil_ID])
+ end
+ elseif msg_ID == 303 then
+ spell = copy_entry(res.job_abilities[74]) -- Divine Seal
+ elseif msg_ID == 304 then
+ spell = copy_entry(res.job_abilities[75]) -- 'Elemental Seal'
+ elseif msg_ID == 305 then
+ spell = copy_entry(res.job_abilities[76]) -- 'Trick Attack'
+ elseif msg_ID == 311 or msg_ID == 311 then
+ spell = copy_entry(res.job_abilities[79]) -- 'Cover'
+ elseif msg_ID == 240 or msg_ID == 241 then
+ spell = copy_entry(res.job_abilities[43]) -- 'Hide'
+ elseif msg_ID == 244 then
+ spell = copy_entry(res.job_abilities[act.param]) -- Mug failures
+ elseif msg_ID == 328 then
+ spell = copy_entry(res.job_abilities[effect_val]) -- BPs that are out of range
+ end
+
+
+ if table.contains(fields,'item') then
+ if spell then
+ spell.item = copy_entry(res.items[effect_val])
+ else
+ spell = copy_entry(res.items[abil_ID])
+ end
+ else
+ spell = spell_complete(spell)
+ end
+ end
+
+ if spell then
+ spell.name = spell[language]
+ spell.interrupted = false
+ end
+
+ return spell
+end
+
+
+-----------------------------------------------------------------------------------
+--Name: spell_complete(rline)
+--Desc: Takes a resource line and modifies it so it includes aftercast cost and
+-- a few other values
+--Args:
+---- rline - resource line
+-----------------------------------------------------------------------------------
+--Returns:
+---- rline - modified resource line
+-----------------------------------------------------------------------------------
+function spell_complete(rline)
+ -- Hardcoded adjustments
+ if rline and rline.skill == 40 and buffactive.Pianissimo and rline.cast_time == 8 then
+ -- Pianissimo halves song casting time for buffs
+ rline.cast_time = 4
+ rline.targets.Party = true
+ end
+ if rline and rline.skill == 44 and buffactive.Entrust and string.find(rline.en,"Indi") then
+ -- Entrust allows Indi- spells to be cast on party members
+ rline.targets.Party = true
+ end
+
+ if rline == nil then
+ return {tpaftercast = player.tp, mpaftercast = player.mp, mppaftercast = player.mpp}
+ end
+ if not rline.mp_cost or rline.mp_cost == -1 then rline.mp_cost = 0 end
+ if not rline.tp_cost and rline.type == 'WeaponSkill' then
+ rline.tp_cost = player.tp
+ elseif not rline.tp_cost or rline.tp_cost == -1 then
+ rline.tp_cost = 0
+ end
+
+ if rline.skill and tonumber(rline.skill) then
+ rline.skill = res.skills[rline.skill][language]
+ end
+
+ if rline.element and tonumber(rline.element) then
+ rline.element = res.elements[rline.element][language]
+ end
+
+ if rline.tp_cost == 0 then rline.tpaftercast = player.tp else
+ rline.tpaftercast = player.tp - rline.tp_cost end
+
+ if rline.mp_cost == 0 then
+ rline.mpaftercast = player.mp
+ rline.mppaftercast = player.mpp
+ else
+ rline.mpaftercast = player.mp - rline.mp_cost
+ rline.mppaftercast = (player.mp - rline.mp_cost)/player.max_mp
+ end
+
+ return rline
+end
+
+-----------------------------------------------------------------------------------
+--Name: logit()
+--Args:
+---- logfile (file): File to be logged to
+---- str (string): String to be logged.
+-----------------------------------------------------------------------------------
+--Returns:
+---- none
+-----------------------------------------------------------------------------------
+function logit(str)
+ if debugging.logging then
+ if not logfile and windower.dir_exists('../addons/GearSwap/data/logs') then
+ logfile = io.open('../addons/GearSwap/data/logs/NormalLog'..tostring(os.clock())..'.log','w+')
+ logfile:write('GearSwap LOGGER HEADER\n')
+ end
+ logfile:write(str)
+ logfile:flush()
+ end
+end
+
+msg = {}
+
+-----------------------------------------------------------------------------------
+--Name: msg.add_to_chat(col,str)
+--Args:
+---- col (num): Color to print out in (0x1F,col)
+---- str (string): String to be printed.
+-----------------------------------------------------------------------------------
+--Returns:
+---- none
+-----------------------------------------------------------------------------------
+function msg.add_to_chat(col,str)
+ if str == '' then return end
+ if col == 1 then
+ windower.add_to_chat(1,str)
+ else
+ windower.add_to_chat(1,string.char(0x1F,col%256)..str..string.char(0x1E,0x01))
+ end
+end
+
+-----------------------------------------------------------------------------------
+--Name: msg.debugging(message)
+--Desc: Checks _settings.debug_mode and outputs the message if necessary
+--Args:
+---- message - The debug message
+-----------------------------------------------------------------------------------
+--Returns:
+---- none
+-----------------------------------------------------------------------------------
+function msg.debugging(message)
+ if _settings.debug_mode or debugging.general or debugging.command_registry then
+ msg.add_to_chat(8,"GearSwap (Debug Mode): "..windower.to_shift_jis(tostring(message)))
+ end
+end
+
+-----------------------------------------------------------------------------------
+--Name: msg.addon_msg(col,str)
+--Args:
+---- col (num): Color to print out in (0x1F,col)
+---- str (string): String to be printed.
+-----------------------------------------------------------------------------------
+--Returns:
+---- none
+-----------------------------------------------------------------------------------
+function msg.addon_msg(col,str)
+ msg.add_to_chat(col,'GearSwap: '..str)
+end
+
+-- Set up the priority list structure
+
+-----------------------------------------------------------------------------------
+--Name: prioritize()
+--Args:
+---- priority_list (table): Current list of slot priorities
+---- slot_id (number): Desired order of the piece of equipment
+---- priority (number): Name for the slot
+-----------------------------------------------------------------------------------
+--Returns:
+---- none
+-----------------------------------------------------------------------------------
+function prioritize(self,slot_id,priority)
+ if priority and tonumber(priority) then -- Check that priority is number
+ rawset(self,slot_id,priority)
+ return
+ elseif priority then
+ msg.addon_msg(123,'Invalid priority ('..tostring(priority)..') given')
+ end
+ rawset(self,slot_id,0)
+end
+
+
+local priority_list = {}
+
+Priorities = {}
+function Priorities.new()
+ local new_instance = {}
+ return setmetatable(new_instance, { __index = function(t, k) if rawget(t, k) ~= nil then return rawget(t,k) else return rawget(priority_list,k) end end,
+ __newindex=prioritize})
+end
+
+-----------------------------------------------------------------------------------
+--Name: priority_list:it()
+--Args:
+---- self (table): Current list of slot priorities
+-----------------------------------------------------------------------------------
+--Returns:
+---- slot_id : Number from 0~15
+-----------------------------------------------------------------------------------
+function priority_list:it()
+ return function ()
+ local maximum,slot_id = -math.huge
+ for i=0,15 do
+ if self[i] and (self[i] > maximum or (self[i] == maximum and self[i] == -math.huge)) then
+ maximum = self[i]
+ slot_id = i
+ end
+ end
+ if not slot_id then return end
+ self[slot_id] = nil
+ return slot_id,maximum
+ end
+end
+
+
+
+-----------------------------------------------------------------------------------
+--Name: toslotname(slot_id)
+--Args:
+---- slot_id: Number from 0-15 representing the slot
+-----------------------------------------------------------------------------------
+--Returns:
+---- slot name (string)
+-----------------------------------------------------------------------------------
+function toslotname(slot_id)
+ return rawget(default_slot_map,slot_id)
+end
+
+
+
+-----------------------------------------------------------------------------------
+--Name: toslotid(slot_name)
+--Args:
+---- slot_name: proposed slot name
+-----------------------------------------------------------------------------------
+--Returns:
+---- slot id (whole number from 0-15)
+-----------------------------------------------------------------------------------
+function toslotid(slot_name)
+ return slot_map[slot_name]
+end
+
+
+
+-----------------------------------------------------------------------------------
+--Name: windower.debug(...)
+--Args:
+---- ...: Anything, to be passed to the real windower.debug if the windower_debugging
+---- flag is set.
+-----------------------------------------------------------------------------------
+--Returns:
+---- Nothing
+-----------------------------------------------------------------------------------
+windower.__raw = {debug = windower.debug}
+windower.debug = function(...)
+ if debugging.windower_debug then __raw.debug(...) end
+end