Created
August 23, 2020 21:24
-
-
Save Ivaar/ab9829b348b2098365764e8adbc423f5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
A library to make the manipulation of the action packet easier. | |
The primary functionality provided here are iterators which allow for | |
easy traversal of the sub-tables within the packet. Example: | |
======================================================================================= | |
require('actions') | |
function event_action(act) | |
action = Action(act) -- constructor | |
-- print out all melee hits to the console | |
if actionpacket:get_category_string() == 'melee' then | |
for target in actionpacket:get_targets() do -- target iterator | |
for action in target:get_actions() do -- subaction iterator | |
if action.message == 1 then -- 1 is the code for messages | |
print(string.format("%s hit %s for %d damage", | |
actionpacket:get_actor_name(), target:get_name(), action.param)) | |
end | |
end | |
end | |
end | |
end | |
======================================================================================= | |
]] | |
_libs = _libs or {} | |
require('tables') | |
local table = _libs.tables | |
local res = require('resources') | |
_libs.actions = true | |
local category_strings = { | |
'melee', | |
'ranged_finish', | |
'weaponskill_finish', | |
'spell_finish', | |
'item_finish', | |
'job_ability', | |
'weaponskill_begin', | |
'casting_begin', | |
'item_begin', | |
'unknown', | |
'mob_tp_finish', | |
'ranged_begin', | |
'avatar_tp_finish', | |
'job_ability_unblinkable', | |
'job_ability_run' | |
} | |
-- ActionPacket operations | |
ActionPacket = {} | |
local actionpacket = {} | |
-- Constructor for Actions. | |
-- Usage: actionpacket = ActionPacket(raw_action) | |
function ActionPacket.new(a) | |
if a == nil then | |
return | |
end | |
local new_instance = {} | |
new_instance.raw = a | |
return setmetatable(new_instance, {__index = function(t, k) if rawget(t, k) ~= nil then return t[k] else return actionpacket[k] end end}) | |
end | |
local function act_to_string(original,act) | |
if type(act) ~= 'table' then return act end | |
function assemble_bit_packed(init,val,initial_length,final_length) | |
if not init then return init end | |
if type(val) == 'boolean' then | |
if val then val = 1 else val = 0 end | |
elseif type(val) ~= 'number' then | |
return false | |
end | |
local bits = initial_length%8 | |
local byte_length = math.ceil(final_length/8) | |
local out_val = 0 | |
if bits > 0 then | |
out_val = init:byte(#init) -- Initialize out_val to the remainder in the active byte. | |
init = init:sub(1,#init-1) -- Take off the active byte | |
end | |
out_val = out_val + val*2^bits -- left-shift val by the appropriate amount and add it to the remainder (now the lsb-s in val) | |
while out_val > 0 do | |
init = init..string.char(out_val%256) | |
out_val = math.floor(out_val/256) | |
end | |
while #init < byte_length do | |
init = init..string.char(0) | |
end | |
return init | |
end | |
local react = assemble_bit_packed(original:sub(1,4),act.size,32,40) | |
react = assemble_bit_packed(react,act.actor_id,40,72) | |
react = assemble_bit_packed(react,act.target_count,72,82) | |
react = assemble_bit_packed(react,act.category,82,86) | |
react = assemble_bit_packed(react,act.param,86,102) | |
react = assemble_bit_packed(react,act.unknown,102,118) | |
react = assemble_bit_packed(react,act.recast,118,150) | |
local offset = 150 | |
for i = 1,act.target_count do | |
react = assemble_bit_packed(react,act.targets[i].id,offset,offset+32) | |
react = assemble_bit_packed(react,act.targets[i].action_count,offset+32,offset+36) | |
offset = offset + 36 | |
for n = 1,act.targets[i].action_count do | |
react = assemble_bit_packed(react,act.targets[i].actions[n].reaction,offset,offset+5) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].animation,offset+5,offset+16) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].effect,offset+16,offset+21) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].stagger,offset+21,offset+27) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].param,offset+27,offset+44) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].message,offset+44,offset+54) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].unknown,offset+54,offset+85) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].has_add_effect,offset+85,offset+86) | |
offset = offset + 86 | |
if act.targets[i].actions[n].has_add_effect then | |
react = assemble_bit_packed(react,act.targets[i].actions[n].add_effect_animation,offset,offset+6) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].add_effect_effect,offset+6,offset+10) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].add_effect_param,offset+10,offset+27) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].add_effect_message,offset+27,offset+37) | |
offset = offset + 37 | |
end | |
react = assemble_bit_packed(react,act.targets[i].actions[n].has_spike_effect,offset,offset+1) | |
offset = offset + 1 | |
if act.targets[i].actions[n].has_spike_effect then | |
react = assemble_bit_packed(react,act.targets[i].actions[n].spike_effect_animation,offset,offset+6) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].spike_effect_effect,offset+6,offset+10) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].spike_effect_param,offset+10,offset+24) | |
react = assemble_bit_packed(react,act.targets[i].actions[n].spike_effect_message,offset+24,offset+34) | |
offset = offset + 34 | |
end | |
end | |
end | |
if react then | |
while #react < #original do | |
react = react..original:sub(#react+1,#react+1) | |
end | |
else | |
print('Action Library failure in '..(_addon.name or 'Unknown Addon')..': Invalid Act table returned.') | |
end | |
return react | |
end | |
-- Opens a listener event for the action packet at the incoming chunk level before modifications. | |
-- Passes in the documented act structures for the original and modified packets. | |
-- If a table is returned, the library will treat it as a modified act table and recompose the packet string from it. | |
-- If an invalid act table is passed, it will silently fail to be returned. | |
function ActionPacket.open_listener(funct) | |
if not funct or type(funct) ~= 'function' then return end | |
local id = windower.register_event('incoming chunk',function(id, org, modi, is_injected, is_blocked) | |
if id == 0x28 then | |
local act_org = windower.packets.parse_action(org) | |
act_org.size = org:byte(5) | |
local act_mod = windower.packets.parse_action(modi) | |
act_mod.size = modi:byte(5) | |
return act_to_string(org,funct(act_org,act_mod)) | |
end | |
end) | |
return id | |
end | |
function ActionPacket.close_listener(id) | |
if not id or type(id) ~= 'number' then return end | |
windower.unregister_event(id) | |
end | |
local actor_animation_twoCC = { | |
wh='White Magic', | |
bk='Black Magic', | |
bl='Blue Magic', | |
sm='Summoning Magic', | |
te='TP Move', | |
['k0']='Melee Attack', | |
['lg']='Ranged Attack', | |
} | |
function actionpacket:get_animation_string() | |
return actor_animation_twoCC[string.char(actor_animation_twoCC[self.raw['unknown']]%256,math.floor(actor_animation_twoCC[self.raw['unknown']]/256))] | |
end | |
function actionpacket:get_category_string() | |
return category_strings[self.raw['category']] | |
end | |
function actionpacket:get_spell() | |
local info = self:get_targets()():get_actions()():get_basic_info() | |
if rawget(info,'resource') and rawget(info,'spell_id') and rawget(rawget(res,rawget(info,'resource')),rawget(info,'spell_id')) then | |
local copied_line = {} | |
for i,v in pairs(rawget(rawget(res,rawget(info,'resource')),rawget(info,'spell_id'))) do | |
rawset(copied_line,i,v) | |
end | |
setmetatable(copied_line,getmetatable(res[rawget(info,'resource')][rawget(info,'spell_id')])) | |
return copied_line | |
end | |
end | |
-- Returns the name of this actor if there is one | |
function actionpacket:get_actor_name() | |
local mob = windower.ffxi.get_mob_by_id(self.raw['actor_id']) | |
if mob then | |
return mob['name'] | |
else | |
return nil | |
end | |
end | |
--Returns the id of the actor | |
function actionpacket:get_id() | |
return self.raw['actor_id'] | |
end | |
-- Returns an iterator for this actionpacket's targets | |
function actionpacket:get_targets() | |
local targets = self.raw['targets'] | |
local target_count = self.raw['target_count'] | |
local i = 0 | |
return function () | |
i = i + 1 | |
if i <= target_count then | |
return Target(self.raw['category'],self.raw['param'],targets[i]) | |
end | |
end | |
end | |
local target = {} | |
-- Constructor for target wrapper | |
function Target(category,top_level_param,t) | |
if t == nil then | |
return | |
end | |
local new_instance = {} | |
new_instance.raw = t | |
new_instance.category = category | |
new_instance.top_level_param = top_level_param | |
new_instance.id = t.id | |
return setmetatable(new_instance, {__index = function (t, k) if rawget(t, k) ~= nil then return t[k] else return target[k] end end}) | |
end | |
-- Returns an iterator for this target's actions | |
function target:get_actions() | |
local action_count = self.raw['action_count'] | |
local i = 0 | |
return function () | |
i = i + 1 | |
if i <= action_count then | |
return Action(self.category,self.top_level_param,self.raw['actions'][i]) | |
end | |
end | |
end | |
-- Returns the name of this target if there is one | |
function target:get_name() | |
local mob = windower.ffxi.get_mob_by_id(self.raw['id']) | |
if mob then | |
return mob['name'] | |
else | |
return nil | |
end | |
end | |
local reaction_strings = { | |
[1] = 'evade', | |
[2] = 'parry', | |
[4] = 'block/guard', | |
[8] = 'hit' | |
-- 12 = blocked? | |
} | |
local animation_strings = { | |
[0] = 'main hand', | |
[1] = 'off hand', | |
[2] = 'left kick', | |
[3] = 'right kick', | |
[4] = 'daken throw' | |
} | |
local effect_strings = { | |
[2] = 'critical hit' | |
} | |
local stagger_strings = { | |
} | |
local action = {} | |
function Action(category,top_level_param,t) | |
if category == nil or t == nil then | |
return | |
end | |
local new_instance = {} | |
new_instance.raw = t | |
new_instance.raw.category = category_strings[category] or category | |
new_instance.raw.top_level_param = top_level_param | |
return setmetatable(new_instance, {__index = function (t, k) if rawget(t, k) ~= nil then return t[k] else return action[k] or rawget(rawget(t,'raw'),k) end end}) | |
end | |
function action:get_basic_info() | |
local reaction = self:get_reaction_string() | |
local animation = self:get_animation_string() | |
local effect = self:get_effect_string() | |
local stagger = self:get_stagger_string() | |
local message_id = self:get_message_id() | |
local param, resource, spell_id, interruption, conclusion = self:get_spell() | |
return {reaction = reaction, animation = animation, effect=effect, message_id = message_id, | |
stagger = stagger, param = param, resource = resource, spell_id = spell_id, | |
interruption = interruption, conclusion = conclusion} | |
end | |
function action:get_reaction_string() | |
local reaction = rawget(rawget(self,'raw'),'reaction') | |
return rawget(reaction_strings,reaction) or reaction | |
end | |
function action:get_animation_string() | |
local animation = rawget(rawget(self,'raw'),'animation') | |
return rawget(animation_strings,animation) or animation | |
end | |
function action:get_effect_string() | |
local effect = rawget(rawget(self,'raw'),'effect') | |
return rawget(effect_strings,effect) or effect | |
end | |
function action:get_stagger_string() | |
local stagger = rawget(rawget(self,'raw'),'stagger') | |
return rawget(stagger_strings,stagger) or stagger | |
end | |
local cat_to_res_map = {['weaponskill_finish']='weapon_skills', ['spell_finish']='spells', | |
['item_finish']='items', ['job_ability']='job_abilities', ['weaponskill_begin']='weapon_skills', | |
['casting_begin']='spells', ['item_begin']='items', ['mob_tp_finish']='monster_abilities', | |
['avatar_tp_finish']='job_abilities', ['job_ability_unblinkable']='job_abilities', | |
['job_ability_run']='job_abilities'} | |
local begin_categories = {['weaponskill_begin']=true, ['casting_begin']=true, ['item_begin']=true, ['ranged_begin']=true} | |
local finish_categories = {['melee']=true,['ranged_finish']=true,['weaponskill_finish']=true, ['spell_finish']=true, ['item_finish']=true, | |
['job_ability']=true, ['mob_tp_finish']=true, ['avatar_tp_finish']=true, ['job_ability_unblinkable']=true, | |
['job_ability_run']=true} | |
local msg_id_to_conclusion_map = { | |
[26] = {subject="target", verb="gains", objects={"HP","MP"} }, | |
[31] = {subject="target", verb="loses", objects={"shadows"} }, | |
[112] = {subject="target", verb="count", objects={"doom"} }, | |
[120] = {subject="actor", verb="gains", objects={"Gil"} }, | |
[132] = {subject="target", verb="steals", objects={"HP"} }, | |
[133] = {subject="actor", verb="steals", objects={"Petra"} }, | |
[152] = {subject="actor", verb="gains", objects={"MP"} }, | |
[229] = {subject="target", verb="loses", objects={"HP"} }, | |
[231] = {subject="actor", verb="loses", objects={"effects"} }, | |
[453] = {subject="actor", verb="steals", objects={"status"} }, | |
[530] = {subject="target", verb="count", objects={"petrify"} }, -- Gradual Petrify | |
[557] = {subject="actor", verb="gains", objects={"Alexandrite"} }, -- Using a pouch | |
[560] = {subject="actor", verb="gains", objects={"FMs"} }, -- No Foot Rise | |
[572] = {subject="actor", verb="steals", objects={"ailments"} }, -- Sacrifice | |
[585] = {subject="actor", verb="has", objects={"enmity"} }, -- Libra with actor | |
[586] = {subject="target", verb="has", objects={"enmity"} }, -- Libra without actor | |
[674] = {subject="actor", verb="gains", objects={"items"} }, -- Scavenge | |
[730] = {subject="target", verb="has", objects={"TP"} }, | |
} | |
local expandable = {} | |
expandable[{1, 2, 67, 77, 110,157, | |
163,185,196,197,223,252, | |
264,265,288,289,290,291, | |
292,293,294,295,296,297, | |
298,299,300,301,302,317, | |
352,353,379,419,522,576, | |
577,648,650,732,767,768}] = {subject="target", verb="loses", objects={"HP"} } | |
expandable[{122,167,383}] = {subject="actor", verb="gains", objects={"HP"} } | |
expandable[{7, 24, 102,103,238,263, | |
306,318,357,367,373,382,384, | |
385,386,387,388,389,390,391, | |
392,393,394,395,396,397,398, | |
539,587,606,651,769,770}] = {subject="target", verb="gains", objects={"HP"} } | |
expandable[{25, 224,276,358,451,588}] = {subject="target", verb="gains", objects={"MP"} } | |
expandable[{161,187,227,274,281}] = {subject="actor", verb="steals", objects={"HP"} } | |
expandable[{165,226,454,652}] = {subject="actor", verb="steals", objects={"TP"} } | |
expandable[{162,225,228,275,366}] = {subject="actor", verb="steals", objects={"MP"} } | |
expandable[{362,363}] = {subject="target", verb="loses", objects={"TP"} } | |
expandable[{369,403,417}] = {subject="actor", verb="steals", objects={"attributes"} } | |
expandable[{370,404,642}] = {subject="actor", verb="steals", objects={"effects"} } | |
expandable[{400,570,571,589,607}] = {subject="target", verb="loses", objects={"ailments"} } | |
expandable[{401,405,644}] = {subject="target", verb="loses", objects={"effects"} } | |
expandable[{409,452,537}] = {subject="target", verb="gains", objects={"TP"} } | |
expandable[{519,520,521,591}] = {subject="target", verb="gains", objects={"daze"} } | |
expandable[{14, 535}] = {subject="actor", verb="loses", object={"shadows"} } | |
expandable[{603,608}] = {subject="target", verb="gains", objects={"TH"} } | |
expandable[{33, 44, 536,}] = {subject="actor", verb="loses", objects={"HP"} } | |
expandable[{166,374}] = {subject="actor", verb="loses", objects={"status"} } | |
expandable[{82, 127,141,160,164,186, | |
194,203,205,230,236,237,242, | |
243,266,267,268,269,270,271, | |
272,277,278,279,280,319,320, | |
321,375,412,645,754,755,804}] = {subject="target", verb="gains", objects={"status"} } | |
expandable[{64, 83, 123,159,168,204, | |
206,322,341,342,343,344,350, | |
378,531,647,805,806}] = {subject="target", verb="loses", objects={"status"} } | |
for ids,tab in pairs(expandable) do | |
for _,id in pairs(ids) do | |
msg_id_to_conclusion_map[id] = tab | |
end | |
end | |
local function msg_id_to_conclusion(msg_id) | |
return rawget(msg_id_to_conclusion_map,msg_id) or false | |
end | |
function action:get_spell() | |
local category = rawget(rawget(self,'raw'),'category') | |
-- It's far more accurate to filter by the resources line. | |
local function fieldsearch(message_id) | |
if not message_id or not res.action_messages[message_id] or not res.action_messages[message_id].en then return false end | |
local fields = {} | |
res.action_messages[message_id].en:gsub("${(.-)}", function(a) if a ~= "actor" and a ~= "target" and a ~= 'lb' then rawset(fields,a,true) end end) | |
return fields | |
end | |
local message_id = self:get_message_id() | |
local fields = fieldsearch(message_id) | |
local param = rawget(finish_categories, category) and rawget(rawget(self, 'raw'), 'param') | |
local spell_id = rawget(begin_categories, category) and rawget(rawget(self, 'raw'), 'param') or | |
rawget(finish_categories, category) and rawget(rawget(self, 'raw'), 'top_level_param') | |
local interruption = rawget(begin_categories, category) and rawget(rawget(self, 'raw'), 'top_level_param') == 28787 | |
if interruption == nil then interruption = false end | |
local conclusion = msg_id_to_conclusion(message_id) | |
local resource | |
if not fields or message_id == 31 then | |
-- If there is no message, assume the resources type based on the category. | |
if category == 'weaponskill_begin' and spell_id <= 256 then | |
resource = 'weapon_skills' | |
elseif category == 'weaponskill_begin' then | |
resource = 'monster_abilities' | |
else | |
resource = rawget(cat_to_res_map,category) or false | |
end | |
else | |
local msgID_to_res_map = { | |
[244] = 'job_abilities', -- Mug | |
[328] = 'job_abilities', -- BPs that are out of range | |
} | |
-- If there is a message, interpret the fields. | |
resource = msgID_to_res_map[message_id] or fields.spell and 'spells' or | |
fields.weapon_skill and spell_id <= 256 and 'weapon_skills' or | |
fields.weapon_skill and spell_id > 256 and 'monster_abilities' or | |
fields.ability and 'job_abilities' or | |
fields.item and 'items' or rawget(cat_to_res_map,category) | |
local msgID_to_spell_id_map = { | |
[240] = 43, -- Hide | |
[241] = 43, -- Hide failing | |
[303] = 74, -- Divine Seal | |
[304] = 75, -- Elemental Seal | |
[305] = 76, -- Trick Attack | |
[311] = 79, -- Cover | |
} | |
spell_id = msgID_to_spell_id_map[message_id] or spell_id | |
end | |
-- param will be a number or false | |
-- resource will be a string or false | |
-- spell_id will either be a number or false | |
-- interruption will be true or false | |
-- conclusion will either be a table or false | |
return param, resource, spell_id, interruption, conclusion | |
end | |
function action:get_message_id() | |
local message_id = rawget(rawget(self,'raw'),'message') | |
return message_id or 0 | |
end | |
---------------------------------------- Additional Effects ---------------------------------------- | |
local add_effect_animation_strings = {} | |
add_effect_animation_strings['melee'] = { | |
[1] = 'enfire', | |
[2] = 'enblizzard', | |
[3] = 'enaero', | |
[4] = 'enstone', | |
[5] = 'enthunder', | |
[6] = 'enwater', | |
[7] = 'enlight', | |
[8] = 'endark', | |
[12] = 'enblind', | |
[14] = 'enpetrify', | |
[21] = 'endrain', | |
[22] = 'enaspir', | |
[23] = 'enhaste', | |
} | |
add_effect_animation_strings['ranged_finish'] = add_effect_animation_strings['melee'] | |
add_effect_animation_strings['weaponskill_finish'] = { | |
[1] = 'light', | |
[2] = 'darkness', | |
[3] = 'gravitation', | |
[4] = 'fragmentation', | |
[5] = 'distortion', | |
[6] = 'fusion', | |
[7] = 'compression', | |
[8] = 'liquefaction', | |
[9] = 'induration', | |
[10] = 'reverberation', | |
[11] = 'transfixion', | |
[12] = 'scission', | |
[13] = 'detonation', | |
[14] = 'impaction', | |
[15] = 'radiance', | |
[16] = 'umbra', | |
} | |
add_effect_animation_strings['spell_finish'] = add_effect_animation_strings['weaponskill_finish'] | |
add_effect_animation_strings['mob_tp_finish'] = add_effect_animation_strings['weaponskill_finish'] | |
add_effect_animation_strings['avatar_tp_finish'] = add_effect_animation_strings['weaponskill_finish'] | |
local add_effect_effect_strings = {} | |
function action:get_add_effect() | |
if not rawget(rawget(self,'raw'),'has_add_effect') then return false end | |
local animation = self:get_add_effect_animation_string() | |
local effect = self:get_add_effect_effect_string() | |
local param = rawget(rawget(self,'raw'),'add_effect_param') | |
local message_id = rawget(rawget(self,'raw'),'add_effect_message') | |
local conclusion = msg_id_to_conclusion(message_id) | |
return {animation = animation, effect = effect, param = param, | |
message_id = message_id,conclusion = conclusion} | |
end | |
function action:get_add_effect_animation_string() | |
local add_effect_animation = rawget(rawget(self,'raw'),'add_effect_animation') | |
local add_eff_animation_tab = rawget(add_effect_animation_strings,rawget(rawget(self,'raw'),'category')) | |
return add_eff_animation_tab and rawget(add_eff_animation_tab,add_effect_animation) or add_effect_animation | |
end | |
function action:get_add_effect_effect_string() | |
local add_effect_effect = rawget(rawget(self,'raw'),'add_effect_effect') | |
return rawget(add_effect_effect_strings,add_effect_effect) or add_effect_effect | |
end | |
function action:get_add_effect_conclusion() | |
return msg_id_to_conclusion(rawget(rawget(self,'raw'),'add_effect_message')) | |
end | |
------------------------------------------- Spike Effects ------------------------------------------ | |
local spike_effect_animation_strings = { | |
[1] = 'blaze spikes', | |
[2] = 'ice spikes', | |
[3] = 'dread spikes', | |
[4] = 'water spikes', | |
[5] = 'shock spikes', | |
[6] = 'reprisal', | |
[7] = 'wind spikes', | |
[8] = 'stone spikes', | |
[63] = 'counter', | |
} | |
local spike_effect_effect_strings = { | |
} | |
function action:get_spike_effect() | |
if not rawget(rawget(self,'raw'),'has_spike_effect') then return false end | |
local effect = self:get_spike_effect_effect_string() | |
local animation = self:get_spike_effect_animation_string() | |
local param = rawget(rawget(self,'raw'),'spike_effect_param') | |
local message_id = rawget(rawget(self,'raw'),'spike_effect_message') | |
local conclusion = msg_id_to_conclusion(message_id) | |
return {animation = animation, effect = effect, param = param, | |
message_id = message_id,conclusion = conclusion} | |
end | |
function action:get_spike_effect_effect_string() | |
local spike_effect_effect = rawget(rawget(self,'raw'),'spike_effect_effect') | |
return rawget(spike_effect_effect_strings,spike_effect_effect) or spike_effect_effect | |
end | |
function action:get_spike_effect_animation_string() | |
local spike_effect_animation = rawget(rawget(self,'raw'),'spike_effect_animation') | |
return rawget(spike_effect_animation_strings,spike_effect_animation) or spike_effect_animation | |
end | |
function action:get_additional_effect_conclusion() | |
return msg_id_to_conclusion(rawget(rawget(self,'raw'),'spike_effect_message')) | |
end | |
--[[ | |
Copyright © 2013, Suji | |
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 actions 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 SUJI 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. | |
]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Copyright © 2019, Xathe | |
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 Debuffed 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 Xathe 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. | |
]] | |
_addon.name = 'Debuffed' | |
_addon.author = 'Xathe (Asura)' | |
_addon.version = '1.0.0.5' | |
_addon.commands = {'dbf','debuffed'} | |
config = require('config') | |
packets = require('packets') | |
res = require('resources') | |
texts = require('texts') | |
require('logger') | |
require('actions') | |
defaults = {} | |
defaults.interval = .1 | |
defaults.mode = 'blacklist' | |
defaults.timers = true | |
defaults.hide_below_zero = false | |
defaults.whitelist = S{} | |
defaults.blacklist = S{} | |
defaults.colors = {} | |
defaults.colors.player = {} | |
defaults.colors.player.red = 255 | |
defaults.colors.player.green = 255 | |
defaults.colors.player.blue = 255 | |
defaults.colors.others = {} | |
defaults.colors.others.red = 255 | |
defaults.colors.others.green = 255 | |
defaults.colors.others.blue = 0 | |
settings = config.load(defaults) | |
box = texts.new('${current_string}', settings) | |
box:show() | |
list_commands = T{ | |
w = 'whitelist', | |
wlist = 'whitelist', | |
white = 'whitelist', | |
whitelist = 'whitelist', | |
b = 'blacklist', | |
blist = 'blacklist', | |
black = 'blacklist', | |
blacklist = 'blacklist' | |
} | |
sort_commands = T{ | |
a = 'add', | |
add = 'add', | |
['+'] = 'add', | |
r = 'remove', | |
remove = 'remove', | |
['-'] = 'remove' | |
} | |
player_id = 0 | |
frame_time = 0 | |
debuffed_mobs = {} | |
function update_box() | |
local lines = L{} | |
local target = windower.ffxi.get_mob_by_target('t') | |
if target and target.valid_target and (target.claim_id ~= 0 or target.spawn_type == 16) then | |
local data = debuffed_mobs[target.id] | |
if data then | |
for effect, spell in pairs(data) do | |
local name = res[spell.resource][spell.id].name | |
local remains = math.max(0, spell.timer - os.clock()) | |
if settings.mode == 'whitelist' and settings.whitelist:contains(name) or settings.mode == 'blacklist' and not settings.blacklist:contains(name) then | |
if spell.step then | |
name = '%s Lv.%s':format(name, spell.step) | |
end | |
if settings.timers and remains > 0 then | |
lines:append('\\cs(%s)%s: %.0f\\cr':format(get_color(spell.actor), name, remains)) | |
elseif remains == 0 and settings.hide_below_zero then | |
debuffed_mobs[target.id][effect] = nil | |
else | |
lines:append('\\cs(%s)%s\\cr':format(get_color(spell.actor), name)) | |
end | |
end | |
end | |
end | |
end | |
if lines:length() == 0 then | |
box.current_string = '' | |
else | |
box.current_string = 'Debuffed [' .. target.name .. ']\n\n' .. lines:concat('\n') | |
end | |
end | |
function get_color(actor) | |
if actor == player_id then | |
return '%s,%s,%s':format(settings.colors.player.red, settings.colors.player.green, settings.colors.player.blue) | |
else | |
return '%s,%s,%s':format(settings.colors.others.red, settings.colors.others.green, settings.colors.others.blue) | |
end | |
end | |
function handle_overwrites(target, spell_id, resource, overwrite) | |
if not debuffed_mobs[target] then | |
return true | |
end | |
local new = res[resource][spell_id].overwrites or {} | |
for effect, spell in pairs(debuffed_mobs[target]) do | |
local old = res[spell.resource][spell.id].overwrites or {} | |
-- Check if the effect is already active | |
if spell_id == spell.id and not overwrite then | |
return spell.timer - os.clock() < 0 | |
end | |
-- Check if there isn't a higher priority debuff active | |
if table.length(old) > 0 then | |
for _,v in ipairs(old) do | |
if spell_id == v then | |
if overwrite then | |
debuffed_mobs[target][effect] = nil | |
else | |
return false | |
end | |
end | |
end | |
end | |
-- Check if a lower priority debuff is being overwritten | |
if table.length(new) > 0 then | |
for _,v in ipairs(new) do | |
if spell.id == v then | |
debuffed_mobs[target][effect] = nil | |
end | |
end | |
end | |
end | |
return true | |
end | |
function apply_debuff(target, effect, spell, actor, resource, overwrite) | |
-- Check overwrite conditions | |
if not handle_overwrites(target, spell, resource, overwrite) then | |
return | |
end | |
-- Create timer | |
debuffed_mobs[target] = debuffed_mobs[target] or {} | |
debuffed_mobs[target][effect] = {id=spell, timer=(os.clock() + (res[resource][spell].duration or 0)), actor=actor, resource=resource} | |
end | |
corsair_shots = { | |
[125] = {[235]=2}, -- Fire Shot | |
[126] = {[236]=2, [58]=1, [80]=1}, -- Ice Shot | |
[127] = {[237]=2}, -- Wind Shot | |
[128] = {[238]=2, [56]=1, [344]=1, [345]=1}, -- Earth Shot | |
[129] = {[239]=2}, -- Thunder Shot | |
[130] = {[240]=2, [220]=1, [221]=1}, -- Water Shot | |
[131] = {[23]=1, [24]=1, [25]=1}, -- Light Shot | |
[132] = {[230]=1, [231]=1, [232]=1, [254]=1, [276]=1, [347]=1, [348]=1}, -- Dark Shot | |
} | |
function handle_shot(target, id) | |
if not debuffed_mobs[target] then | |
return | |
end | |
local effects = corsair_shots[id] | |
for effect, spell in pairs(debuffed_mobs[target]) do | |
local max = effects[spell.id] | |
if max then | |
debuffed_mobs[target][effect].step = math.min(max, (spell.step or 0) + 1) | |
end | |
end | |
end | |
function handle_step(target, effect, id, actor, level) | |
debuffed_mobs[target] = debuffed_mobs[target] or {} | |
local clock = os.clock() | |
local tracked = debuffed_mobs[target][effect] | |
local dur = tracked and tracked.timer - clock or 0 | |
local ext = actor == player_id and ext_step_dur or 0 | |
dur = dur > 0 and math.min(dur+30+ext, 120) or 60+ext | |
debuffed_mobs[target][effect] = {id=id, step=level, timer=clock+dur, actor=actor, resource='job_abilities'} | |
end | |
categories = S{ | |
'job_ability', | |
'job_ability_unblinkable', | |
'spell_finish', | |
'weaponskill_finish', | |
'avatar_tp_finish', | |
} | |
function inc_action(raw) | |
local category = raw.category | |
if category ~= 3 and category ~= 4 and category ~= 6 and category ~= 13 and category ~= 14 then | |
return | |
end | |
if raw.param == 0 then | |
return | |
end | |
local act = ActionPacket.new(raw) | |
local actor = act:get_id() | |
for target in act:get_targets() do | |
for action in target:get_actions() do | |
local param, resource, id, _, conclusion = action:get_spell() | |
--[[ | |
if not resource then | |
return | |
end | |
]] | |
if not conclusion then | |
return | |
end | |
if resource == 'job_abilities' and res[resource][id].type =='CorsairShot' then | |
handle_shot(target.id, id) | |
end | |
local verb = conclusion.verb | |
local object = conclusion.objects[1] | |
local status = res[resource][id].status | |
if not status then | |
return | |
end | |
if verb == 'gains' and object == 'status' and param == status then | |
apply_debuff(target.id, status, id, actor, resource, true) | |
elseif verb == 'loses' and object == 'HP' then | |
apply_debuff(target.id, status, id, actor, resource, false) | |
elseif verb == 'gains' and object == 'daze' then | |
handle_step(target.id, status, id, actor, param) | |
end | |
end | |
end | |
end | |
ActionPacket.open_listener(inc_action) | |
function inc_action_message(arr) | |
-- Unit died | |
if S{6,20,113,406,605,646}:contains(arr.message_id) then | |
debuffed_mobs[arr.target_id] = nil | |
-- Debuff expired | |
elseif S{64,204,206,350,531}:contains(arr.message_id) then | |
if debuffed_mobs[arr.target_id] then | |
debuffed_mobs[arr.target_id][arr.param_1] = nil | |
windower.send_ipc_message('%s %s':format(arr.target_id, arr.param_1)) | |
end | |
end | |
end | |
windower.register_event('login','load','job change', function() | |
local player = windower.ffxi.get_player() or {} | |
ext_step_dur = 'DNC' == player.main_job and player.job_points.dnc.step_duration or 0 | |
player_id = player.id | |
end) | |
windower.register_event('logout','zone change', function() | |
debuffed_mobs = {} | |
end) | |
windower.register_event('ipc message', function(message) | |
message = message:split(' '):map(tonumber) | |
local target = message[1] | |
local effect = message[2] | |
if debuffed_mobs[target] then | |
debuffed_mobs[target][effect] = nil | |
end | |
end) | |
windower.register_event('incoming chunk', function(id, data) | |
if id == 0x029 then | |
local arr = {} | |
arr.target_id = data:unpack('I',0x09) | |
arr.param_1 = data:unpack('I',0x0D) | |
arr.message_id = data:unpack('H',0x19)%32768 | |
inc_action_message(arr) | |
end | |
end) | |
windower.register_event('prerender', function() | |
local curr = os.clock() | |
if curr > frame_time + settings.interval then | |
frame_time = curr | |
update_box() | |
end | |
end) | |
windower.register_event('addon command', function(command1, command2, ...) | |
local args = L{...} | |
command1 = command1 and command1:lower() or nil | |
command2 = command2 and command2:lower() or nil | |
local name = args:concat(' ') | |
if command1 == 'm' or command1 == 'mode' then | |
if settings.mode == 'blacklist' then | |
settings.mode = 'whitelist' | |
else | |
settings.mode = 'blacklist' | |
end | |
log('Changed to %s mode.':format(settings.mode)) | |
settings:save() | |
elseif command1 == 't' or command1 == 'timers' then | |
settings.timers = not settings.timers | |
log('Timer display %s.':format(settings.timers and 'enabled' or 'disabled')) | |
settings:save() | |
elseif command1 == 'i' or command1 == 'interval' then | |
settings.interval = tonumber(command2) or .1 | |
log('Refresh interval set to %s seconds.':format(settings.interval)) | |
settings:save() | |
elseif command1 == 'h' or command1 == 'hide' then | |
settings.hide_below_zero = not settings.hide_below_zero | |
log('Timers that reach 0 will be %s.':format(settings.hide_below_zero and 'hidden' or 'shown')) | |
settings:save() | |
elseif list_commands:containskey(command1) then | |
if sort_commands:containskey(command2) then | |
local spell = res.spells:with('name', windower.wc_match-{name}) | |
command1 = list_commands[command1] | |
command2 = sort_commands[command2] | |
if spell == nil then | |
error('No spells found that match: %s':format(name)) | |
elseif command2 == 'add' then | |
settings[command1]:add(spell.name) | |
log('Added spell to %s: %s':format(command1, spell.name)) | |
else | |
settings[command1]:remove(spell.name) | |
log('Removed spell from %s: %s':format(command1, spell.name)) | |
end | |
settings:save() | |
end | |
else | |
print('%s (v%s)':format(_addon.name, _addon.version)) | |
print(' \\cs(255,255,255)mode\\cr - Switches between blacklist and whitelist mode (default: blacklist)') | |
print(' \\cs(255,255,255)timers\\cr - Toggles display of debuff timers (default: true)') | |
print(' \\cs(255,255,255)interval <value>\\cr - Allows you to change the refresh interval (default: 0.1)') | |
print(' \\cs(255,255,255)blacklist|whitelist add|remove <name>\\cr - Adds or removes the spell <name> to the specified list') | |
end | |
end) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment