diff options
author | chai <chaifix@163.com> | 2021-11-15 13:53:59 +0800 |
---|---|---|
committer | chai <chaifix@163.com> | 2021-11-15 13:53:59 +0800 |
commit | 942a030afd348ab2e02eac8054b43e3c3a72ea48 (patch) | |
tree | a13459f39a3d2f1b533fbd1b5ab523d7a621f673 /Data/BuiltIn/Libraries/lua-addons/addons/GearSwap/helper_functions.lua | |
parent | e307051a56a54c27f10438fd2025edf61d0dfeed (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.lua | 1255 |
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 |