-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinteraction_lookup.lua
More file actions
459 lines (384 loc) · 17.6 KB
/
interaction_lookup.lua
File metadata and controls
459 lines (384 loc) · 17.6 KB
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
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
----------------------------
----- InteractionLookup class
----------------------------
require('scripts/globals/interaction/action_util')
-- The 'data' variable will contain nested tables used to lookup if a certain NPC/mob/event has any handlers connected to it that should be run.
-- The structure of it is as follows (also see illustration below):
-- * First level of tables has keys that are the zone in which the handler takes place.
-- * Second level of tables has keys that are either the name of the entity which has the handler, or special zone-wide handlers like 'onEventFinish'.
-- * Third level of tables for entities has keys that are the name of the handlers, like 'onTrigger', 'onTrade', etc.
-- For zone-wide handlers like 'onEventFinish', the third level keys are the ID of the thing that is being finished, in the case of an event it is the event ID.
--
-- Finally the actual handlers will be found in a list as the values of the third level tables.
-- These handlers have an associated "check" function, that is run to check if the player is elligible to have the corresponding handler functon performed or not.
--[[ Illustration of the structure of the data:
{
-- First level (zone ID)
[dsp.zone.SOME_ZONE] = {
-- Second level (entity name)
['Some_NPC'] = {
-- Third level (handler name)
['onTrigger'] = {
-- List of handlers in the form below
{ check = function (player) .. end, handler = function (player) .. end, container = .. },
}
},
-- Second level (zone-wide handler name)
['onEventFinish'] = {
-- Third level (specifier for handler, here the ID of the event)
[123] = {
-- List of handlers in the form below
{ check = function (player) .. end, handler = function (player) .. end, container = .. },
}
}
}
}
--]]
InteractionLookup = {
}
function InteractionLookup:new(original)
local obj = original or {}
setmetatable(obj, self)
self.__index = self
obj.data = obj.data or {}
obj.containers = obj.containers or {}
obj.zoneDefaults = obj.zoneDefaults or {}
return obj
end
------------------------------------------------
--- Add/Remove helpers
------------------------------------------------
local function addHandler(thirdLevel, thirdLevelKey, handler, metatable)
thirdLevel[thirdLevelKey] = thirdLevel[thirdLevelKey] or {}
table.insert(thirdLevel[thirdLevelKey], setmetatable({ handler = handler }, metatable))
end
-- Parse out handler information into a table for future lookup
-- Table structure is:
-- i.e. data[123]['Miene']['onTrigger'] =
-- { container = <container object>,
-- check = <check function for section>,
-- handler = <action definition or function to run>,
-- }
local function addHandlers(secondLevel, lookupSecondLevel, checkFunc, container)
-- Use base table that all the handlers will reuse, to avoid creating many
-- very similar objects in the lookup table
local baseHandlerTable = {}
if checkFunc then
baseHandlerTable.check = checkFunc
end
if container then
baseHandlerTable.container = container
end
local mt = { __index = baseHandlerTable }
-- Loop through the given second level table, and add them to lookup as needed
for secondLevelKey, thirdLevel in pairs(secondLevel) do
lookupSecondLevel[secondLevelKey] = lookupSecondLevel[secondLevelKey] or {}
-- If only given a function or an action definition as third level, that will default to be an onTrigger handler
local onTriggerHandler = type(thirdLevel) == 'function' and thirdLevel or actionUtil.parseActionDef(thirdLevel)
if onTriggerHandler then
addHandler(lookupSecondLevel[secondLevelKey], 'onTrigger', onTriggerHandler, mt)
else
-- Parse out the handlers for the container
for thirdLevelKey, handler in pairs(thirdLevel) do
local parsedHandler = type(handler) == 'function' and handler or actionUtil.parseActionDef(handler)
addHandler(lookupSecondLevel[secondLevelKey], thirdLevelKey, parsedHandler, mt)
end
end
end
end
local function removeHandlersMatching(secondLevel, lookupSecondLevel, condition)
for secondLevelKey, thirdLevel in pairs(secondLevel) do
if lookupSecondLevel[secondLevelKey] then
-- Take care of short-hand action definitions that are made into onTrigger handlers
local onTriggerHandler = type(thirdLevel) == 'function' and thirdLevel or actionUtil.parseActionDef(thirdLevel)
if onTriggerHandler then
thirdLevel = { ['onTrigger'] = 0 }
end
for thirdLevelKey, _ in pairs(thirdLevel) do
local entries = lookupSecondLevel[secondLevelKey][thirdLevelKey]
if entries then
for i = 1, #entries do
if entries[i] and condition(entries[i]) then
table.remove(entries, i)
end
end
end
end
end
end
end
------------------------------------------------
--- Setup
------------------------------------------------
-- Add default handlers for a given zone
function InteractionLookup:addDefaultHandlers(zoneId, handlerTable)
self.data[zoneId] = self.data[zoneId] or {}
if self.zoneDefaults[zoneId] then
self:removeDefaultHandlers(zoneId)
end
self.zoneDefaults[zoneId] = true
addHandlers(handlerTable, self.data[zoneId])
end
-- Remove default handlers for a given zone
function InteractionLookup:removeDefaultHandlers(zoneId)
if self.data[zoneId] then
removeHandlersMatching(self.data[zoneId], self.data[zoneId], function (entry) return entry.check == nil and entry.container == nil end)
end
self.zoneDefaults[zoneId] = false
end
-- Add the given containers to the lookup table
function InteractionLookup:addContainers(containers, zoneIds)
local validZoneTable = nil
if zoneIds ~= nil then
validZoneTable = {}
for i=1, #zoneIds do
validZoneTable[zoneIds[i]] = true
end
end
for _, container in ipairs(containers) do
self:addContainer(container, validZoneTable)
end
end
-- Add handlers from a container, if the handler is in a zone in the valid zone table
function InteractionLookup:addContainer(container, validZoneTable)
if self.containers[container.id] then
-- Container already added, need to remove it first to re-add.
printf("Can't add a container that is already a loaded. Need to remove it first: " .. container.id);
return
end
self.containers[container.id] = true
-- Add to lookup
for _, section in ipairs(container.sections) do
local checkFunc = section.check
for zoneId, secondLevel in pairs(section) do
if zoneId ~= "check" and (validZoneTable == nil or validZoneTable[zoneId]) then
self.data[zoneId] = self.data[zoneId] or {}
addHandlers(secondLevel, self.data[zoneId], checkFunc, container)
end
end
end
end
-- Remove handlers for a container
function InteractionLookup:removeContainer(container)
for _, section in ipairs(container.sections) do
for zoneid, secondLevel in pairs(section) do
if zoneid ~= "check" and self.data[zoneid] then
removeHandlersMatching(secondLevel, self.data[zoneid], function (entry) return entry.container == container end)
end
end
end
self.containers[container.id] = nil
end
------------------------------------------------
--- Execution
------------------------------------------------
-- Safely runs the handler with the given args
local function runHandler(handler, args)
if not handler or type(handler) == 'table' then
return handler
end
local ok, res = pcall(handler, unpack(args))
if ok then
return res
else
printf("Error running handler: %s", res)
end
end
-- Makes a table that returns the value of the provided getter function when it is given the key as an argument,
-- and it caches this result, such that the getter function is only called once per key.
local function makeTableCache(getterFunc)
local obj = {}
obj.cache = {}
local mt = {
__index = function(table, key)
if obj.cache[key] == nil then
obj.cache[key] = getterFunc(key)
end
return obj.cache[key]
end
}
setmetatable(obj, mt)
return obj
end
-- Makes a nested cache with the a handler container being the first level, and a variable name for the second level.
-- This is done to avoid fetching the same variables from char_vars multiple times during the same NPC interaction
local function makeContainerVarCache(player)
return makeTableCache(function (container)
return makeTableCache(function (varname)
return container:getVar(player, varname)
end)
end)
end
-- Use preprocessed lookup to run relevant handlers
local function runHandlersInData(data, player, secondLevelKey, thirdLevelKey, args)
if not data then
return { }
end
local secondLevelTable = data[player:getZoneID()]
if not secondLevelTable
or not secondLevelTable[secondLevelKey]
or not secondLevelTable[secondLevelKey][thirdLevelKey] then
return { }
end
local actions = { }
local varCache = makeTableCache(function (varname)
return player:getVar(varname)
end)
local containerVarCache = makeContainerVarCache(player)
for _, entry in ipairs(secondLevelTable[secondLevelKey][thirdLevelKey]) do
local checkArgs = { }
if entry.container then
if entry.container.getCheckArgs then
checkArgs = entry.container:getCheckArgs(player)
end
checkArgs[#checkArgs+1] = containerVarCache[entry.container]
checkArgs[#checkArgs+1] = varCache
end
local ok, res = true, true
if entry.check then
ok, res = pcall(entry.check, player, unpack(checkArgs))
end
if not ok then
printf("Error running check: %s", res)
elseif res then
local resultAction = runHandler(entry.handler, args)
if resultAction ~= nil then
table.insert(actions, resultAction)
end
end
end
return actions
end
-- Find the current highest priority actions
local function getHighestPriorityActions(data, player, secondLevelKey, thirdLevelKey, args)
local possibleActions = runHandlersInData(data, player, secondLevelKey, thirdLevelKey, args)
-- If the possible actions is a number, we should always return immediately since it's CS ID from an onZoneIn
if possibleActions and #possibleActions == 0 or type(possibleActions[1]) == 'number' then
return possibleActions, Action.Priority.ReplaceDefault
end
local maxPriority = Action.Priority.Minimum
local highestPriorityActions = { }
for _, action in ipairs(possibleActions) do
if action and action.id then
-- Use the updated the priority of the action if any, else use the default action priority
local updatedPriority = player:getLocalVar(actionUtil.getActionVarName(secondLevelKey, thirdLevelKey, action.id))
local priority = updatedPriority ~= 0 and updatedPriority or action.priority
if priority == nil then
if maxPriority == Action.Priority.Minimum then
table.insert(highestPriorityActions, action)
end
elseif priority == maxPriority then
table.insert(highestPriorityActions, action)
elseif priority > maxPriority then
maxPriority = priority
highestPriorityActions = { action }
end
end
end
return highestPriorityActions, maxPriority
end
-- Perform the next action of the given actions based on which have been performed before
local function performNextAction(player, containerId, handlerId, actions, targetId)
local actionToPerform = actions[1]
if containerId == 'onZoneIn' then -- onZoneIn returns CS IDs as numbers, so return the first one
return actionToPerform
end
-- Figure out which action to perform if there's multiple
local actionCount = #actions
if actionCount > 1 then
local nextIndex = 1
-- Keep track of which action we're at in a local variable
local actionCycleId = actionUtil.getActionVarName(containerId, handlerId)
local previousActionId = player:getLocalVar(actionCycleId)
if previousActionId > 0 then
for idx, currentAction in ipairs(actions) do
if currentAction.id == previousActionId then
nextIndex = idx + 1
end
end
end
if nextIndex > actionCount then
nextIndex = 1
end
actionToPerform = actions[nextIndex]
player:setLocalVar(actionCycleId, actionToPerform.id)
end
local didPerformAction = actionToPerform and actionToPerform:perform(player, targetId and GetNPCByID(targetId) or nil)
-- If the action has a secondary priorty, we set a local variable on the player,
-- such that the next time this action is evaluated, it will use that priority instead
if didPerformAction and actionToPerform.secondaryPriority then
player:setLocalVar(actionUtil.getActionVarName(containerId, handlerId, actionToPerform.id), actionToPerform.secondaryPriority)
end
return didPerformAction
end
------------------------------------------------
--- Handlers
------------------------------------------------
-- Main handler function that determines which action should be performed
local function onHandler(data, secondLevelKey, thirdLevelKey, args, fallbackHandler, defaultReturn, targetId)
local playerArg = args.playerArg or 1
local player = args[playerArg]
if not player then -- if no player object is present, we can't do anything in the handler system
return fallbackHandler(unpack(args))
end
local actions, priority = getHighestPriorityActions(data, player, secondLevelKey, thirdLevelKey, args)
local fallbackVar = actionUtil.getActionVarName(secondLevelKey, thirdLevelKey, "UseFallback")
-- Most handlers should run both the handler system and fallback if available,
-- except those that should only perform one action at a time, like onTrigger and onTrade
if fallbackHandler and thirdLevelKey ~= 'onTrigger' and thirdLevelKey ~= 'onTrade' then
local result = performNextAction(player, secondLevelKey, thirdLevelKey, actions, targetId) or defaultReturn
local fallbackResult = fallbackHandler(unpack(args))
return result or fallbackResult
end
-- Prioritize important actions from the handler system if applicable
if not fallbackHandler
or (#actions > 0 -- only prioritize if there's actually actions to do
and (secondLevelKey == 'onZoneIn' -- play onZoneIn cs if given
or priority > Action.Priority.Event -- prioritize this if event is important enough
or player:getLocalVar(fallbackVar) == 0) -- alternate between trying handler system and fallback handler
)
then
player:setLocalVar(fallbackVar, 1)
local result = performNextAction(player, secondLevelKey, thirdLevelKey, actions, targetId) or defaultReturn
return result
end
-- Else we try to fallback to Lua files for the entity/zone
player:setLocalVar(fallbackVar, 0)
player:resetGotMessage()
-- Fall back to side-loaded handler from other lua file
local result = fallbackHandler(unpack(args))
if player:isInEvent() or player:didGetMessage() -- Fallback handler triggered something
or (result == -1 and thirdLevelKey == 'onTrigger') -- Doors return -1 to open
or (result ~= nil and result ~= -1 and secondLevelKey == 'onZoneIn') -- onZoneIn returns a csid if any, else -1
then
return result
end
-- If old handler didn't do anything significant,
-- we try a lower priority action from handler system instead
result = performNextAction(player, secondLevelKey, thirdLevelKey, actions, targetId)
return result
end
function InteractionLookup:onTrigger(player, npc)
return onHandler(self.data, npc:getName(), 'onTrigger', { player, npc }, onTrigger, -1, npc:getID())
end
function InteractionLookup:onTrade(player, npc, trade)
return onHandler(self.data, npc:getName(), 'onTrade', { player, npc, trade }, onTrade, nil, npc:getID())
end
function InteractionLookup:onMobDeath(mob, player, isKiller, firstCall)
return onHandler(self.data, mob:getName(), 'onMobDeath', { mob, player, isKiller, firstCall, playerArg = 2 }, onMobDeath)
end
function InteractionLookup:onRegionEnter(player, region)
return onHandler(self.data, 'onRegionEnter', region:GetRegionID(), { player, region }, onRegionEnter)
end
function InteractionLookup:onRegionLeave(player, region)
return onHandler(self.data, 'onRegionLeave', region:GetRegionID(), { player, region }, onRegionLeave)
end
function InteractionLookup:onZoneIn(player, prevZone)
return onHandler(self.data, 'onZoneIn', 1, { player, prevZone }, onZoneIn)
end
function InteractionLookup:onEventFinish(player, csid, option, npc)
return onHandler(self.data, 'onEventFinish', csid, { player, csid, option, npc }, onEventFinish)
end
function InteractionLookup:onEventUpdate(player, csid, option, npc)
return onHandler(self.data, 'onEventUpdate', csid, { player, csid, option, npc }, onEventUpdate)
end