-
Notifications
You must be signed in to change notification settings - Fork 4
/
sendtarget.lua
286 lines (233 loc) · 9.35 KB
/
sendtarget.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
_addon.name = 'SendTarget'
_addon.author = 'DiscipleOfEris'
_addon.version = '1.0.4'
_addon.commands = {'sendtarget', 'sta'}
require('tables')
require('strings')
require('logger')
packets = require('packets')
res = require('resources')
require('statics')
local spells = res.spells
local job_abilities = res.job_abilities
local weapon_skills = res.weapon_skills
local PACKET_TYPE = { ACTION = 0x01A }
local ACTION_CATEGORY = { MAGIC_CAST = 0x03, WEAPON_SKILL_USE = 0x07, JOB_ABILITY_USE = 0x09 }
local ACTION_PARAM = { MAGIC_INITIATION = 24931, MAGIC_FAILURE = 28787 }
local mirroring = false
local send_packets = true
local command_queue = T{}
windower.register_event('addon command', function(command, ...)
command = command and command:lower()
local args = T{...}
if not command then
-- display help
log('Commands should look like: //sta <char_name|@others|@all> input')
log('Can also use the !mirror, !packets, !capture <char_name|@others|@all> commands.')
log('In a macro, you should use "/con sta" rather than "//sta".')
elseif command == '!mirror' then
if not args[1] then mirroring = not mirroring
elseif args[1] == 'on' then mirroring = true
elseif args[1] == 'off' then mirroring = false
else
log('!mirror has valid arguments on/off/true/false. To toggle, pass no argument.')
return
end
if mirroring then log('Mirroring enabled. Will have all alts mimic this character.')
else log('Mirroring disabled.') end
elseif command == '!packets' then
if not args[1] then send_packets = not send_packets
elseif args[1] == 'on' then send_packets = true
elseif args[1] == 'off' then send_packets = false
else
log('!packets has valid arguments on/off/true/false. To toggle, pass no argument.')
return
end
if send_packets then log('Packet injection enabled. This is necessary unless GearSwap is active with a profile loaded.')
else log('Packet injection disabled. Do this for compatibility with GearSwap when you have an active profile.') end
elseif command == '!capture' then
if #args == 0 then
log('!capture requires a character name to send to. Usage: //sta !capture <char_name|@others|@all>.')
log('This will capture the next input and send it to char_name.')
return
end
command_queue:insert({char=args[1]:lower(), ts=os.time(), handled=false})
else
if #args == 0 then
log('You must provide some input to send. For example, //sta @all /ma \'Thunder IV\' <stnpc>')
return
end
local char = command
local input = args:concat(' ')
command_queue:insert({char=char, ts=os.time(), handled=false, input=input})
if input:sub(1,1) == '/' then input = 'input '..input end
windower.send_command(input)
end
end)
windower.register_event('ipc message', function(msg)
local args = T(msg:split(' '))
local character = args:remove(1)
local input = args:concat(' ')
local player = windower.ffxi.get_player()
if (player and character == player.name:lower()) or character == '@others' or character == '@all' or character == '@everyone' then
if send_packets and should_inject(input) then
handle_command(input)
else
if input:sub(1,1) == '/' then input = 'input '..input end
windower.send_command(input)
end
end
end)
-- State machine for subtarget capturing, involves 'outgoing text' and 'prerender' events.
-- This state machine is rather convoluted, but has been reliable so far in testing.
local selecting = false
local st_target = nil
local last_st_target = nil
local last_st_command = false
local st_capture = false
local unblocking = false
windower.register_event('outgoing text', function(_, modified, blocked, typed, a, b)
if #command_queue == 0 or not typed or modified:sub(1,1) ~= '/' or (command_queue[#command_queue].handled and not command_queue[#command_queue].st) then return end
if modified == unblocking then return end
local args = T(modified:split(' '))
if #args == 1 then return end
local t_arg = args:remove(#args)
local command = args:concat(' ')
if not st_actions:contains(args[1]) then return end
if st_targs:contains(t_arg) then
if selecting or st_target then
-- already selecting.
command_queue:remove(#command_queue)
return
end
selecting = 1
last_st_command = command
command_queue[#command_queue].handled = true
command_queue[#command_queue].st = true
return
elseif t_arg:sub(1,1) == '<' then
local target = windower.ffxi.get_mob_by_target(t_arg)
if not target then return end
local cmd = command_queue:remove(#command_queue)
return send_message(cmd.char, command..' '..target.id) and '' or false
end
if selecting and command == last_st_command and tonumber(t_arg) then
if not st_target then
-- begin
selecting = 2
elseif selecting == 2 then
-- end
selecting = 3
st_capture = modified
if not should_send_self(command_queue[1].char) then
if blocked then
windower.add_to_chat(0, 'SendTarget: Another addon is blocking commands; SendTarget must be loaded first. If you have GearSwap, put "lua reload GearSwap" after "lua load SendTarget" in your init.txt script.')
end
return ''
end
end
end
end)
windower.register_event('prerender', function()
st_target = windower.ffxi.get_mob_by_target('st')
if #command_queue and last_st_target and not st_target then
if selecting and selecting == 3 then
-- chosen
send_message(command_queue[1].char, st_capture)
else
-- cancelled
end
selecting = false
last_st_command = false
command_queue:remove(1)
elseif last_st_target and st_target and st_capture and #command_queue and command_queue[1].st then
-- unblocking
selecting = 2
windower.send_command('@input '..st_capture)
unblocking = st_capture
end
st_capture = false
if not last_st_target and st_target then
if selecting == 2 then
-- capturing
end
end
last_st_target = st_target
end)
-- Mirror spells and abilities to other alts.
windower.register_event('outgoing chunk', function(id, original, modified, injected, blocked)
if not mirroring or id ~= PACKET_TYPE.ACTION then return end
local packet = packets.parse('outgoing', modified)
if packet.Category == ACTION_CATEGORY.MAGIC_CAST then
local spell = spells[packet.Param]
windower.send_ipc_message('@others /ma "'..spell.en..'" '..packet.Target)
elseif packet.Category == ACTION_CATEGORY.WEAPON_SKILL_USE then
local ws = spells[packet.Param]
windower.send_ipc_message('@others /ws "'..spell.en..'" '..packet.Target)
elseif packet.Category == ACTION_CATEGORY.JOB_ABILITY_USE then
local ja = job_abilities[packet.Param]
windower.send_ipc_message('@others /ja "'..ja.en..'" '..packet.Target)
end
end)
-- returns true if this should NOT be sent to self as well (i.e. block it)
function send_message(character, msg)
local player = windower.ffxi.get_player()
if not player or character ~= player.name:lower() then
windower.send_ipc_message(character..' '..msg)
return character ~= '@all' and character ~= '@everyone'
end
end
function should_send_self(character)
local player = windower.ffxi.get_player()
if not player then return false end
if character == player.name:lower() or character == '@all' or character == '@everyone' then return true end
return false
end
function should_inject(input)
if not send_packets then return false end
local args = T(input:split(' '))
local prefix = args[1]
-- /bstpet has arguments that would be too complicated to inject, and only allows <me> as target anyway.
if prefix == '/bstpet' or not st_actions:contains(prefix) then return false end
local target = args[#args]
if tonumber(target) then return true end
return false
end
-- Assumes should_inject() was already true.
function handle_command(input)
local args = T(input:split(' '))
local prefix = args:remove(1)
local t_arg = tonumber(args:remove(#args))
local name = args:concat(' '):gsub('"', '')
if name:sub(1,1) == "'" then name = name:sub(2) end
if name:sub(#name) == "'" then name = name:sub(1,-2) end
local target = t_arg
local self = windower.ffxi.get_player()
if not self then return end
self = self.id
local self_only = S{'Self'}
if spell_prefixes:contains(prefix) then
local spell = spells:with('en', name)
if spell.targets:equals(self_only) then target = self end
inject_action_packet(ACTION_CATEGORY.MAGIC_CAST, spell, target)
elseif job_ability_prefixes:contains(prefix) then
local ja = job_abilities:with('en', name)
if ja.targets:equals(self_only) then target = self end
inject_action_packet(ACTION_CATEGORY.JOB_ABILITY_USE, ja, target)
elseif weapon_skill_prefixes:contains(prefix) then
local ws = weapon_skills:with('en', name)
if ws.targets:equals(self_only) then target = self end
inject_action_packet(ACTION_CATEGORY.WEAPON_SKILL_USE, ws, target)
end
-- TODO: /attack, /range, /check, /assist, /follow, emotes
end
function inject_action_packet(category, ability, target_id)
local target = windower.ffxi.get_mob_by_id(target_id)
local packet = packets.new('outgoing', PACKET_TYPE.ACTION, {
['Target']=target.id,
['Target Index']=target.index,
['Category']=category,
['Param']=ability.id
})
packets.inject(packet)
end