Skip to content

Commit ac84e26

Browse files
authored
[Revnpcsys] Revisioned Npc System v1.1 (#4724)
- npc now correctly clears all npc id data when he is removed - farewell and greet messages can be individually set per talk state if not they fall back to default - fixed a bug if the npc disappeared while the player was talking to him, then the npc would not respond to the player again once he was spawned - better warning/error messages which show which npc it occured and which file it originates from - added shop callback function callback(npc, player, handler, items, afterDiscount) order: - discount (if there is any) -> callback - added NpcsHandler:player() this is basicly the equivalent to requirements but instead of requireing you can add/set stuff to the player (modules.lua) - implemented modules they're load after callbacks order: requirements (if returns true) -> callback (if returns true) -> modules - moved teleport/addItems/setStorageValue to modules - added new requirements - changed the requirements functions - I've included the error message directly inside the functions - removed :failureRespond & :getFailureRespond as they're redundant with requirement re work - changed modules outfit function it requires to insert 2 different outfits now because of 2 different player sexes ex: :outfit(id1, id2, addon) - a few linter/annotation fixes - if the player changed the talk state but still had the shop open and then tried to purchase something it thrown errors, this is now fixed
1 parent 553c79a commit ac84e26

File tree

12 files changed

+946
-324
lines changed

12 files changed

+946
-324
lines changed

data/cpplinter.lua

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ Podium = {}
322322
---@field setSkull fun(self: Creature, skullType: number)
323323
---@field getOutfit fun(self: Creature): Outfit
324324
---@field setOutfit fun(self: Creature, outfit: Outfit)
325-
---@field getCondition fun(self: Creature, conditionType: number): Condition
325+
---@field getCondition fun(self: Creature, conditionType: number, conditionId?: number, subId?: number): Condition
326326
---@field addCondition fun(self: Creature, condition: Condition)
327327
---@field removeCondition fun(self: Creature, conditionType: number)
328328
---@field hasCondition fun(self: Creature, conditionType: number): boolean
@@ -400,7 +400,7 @@ Creature = {}
400400
---@field setVocation fun(self: Player, vocationId: number)
401401
---@field getSex fun(self: Player): number
402402
---@field setSex fun(self: Player, sexId: number)
403-
---@field getTown fun(self: Player): number
403+
---@field getTown fun(self: Player): Town
404404
---@field setTown fun(self: Player, townId: number)
405405
---@field getGuild fun(self: Player): Guild
406406
---@field setGuild fun(self: Player, guild: Guild)
@@ -449,10 +449,10 @@ Creature = {}
449449
---@field hasBlessing fun(self: Player, blessingId: number): boolean
450450
---@field addBlessing fun(self: Player, blessingId: number)
451451
---@field removeBlessing fun(self: Player, blessingId: number)
452-
---@field canLearnSpell fun(self: Player, spellId: number): boolean
453-
---@field learnSpell fun(self: Player, spellId: number)
454-
---@field forgetSpell fun(self: Player, spellId: number)
455-
---@field hasLearnedSpell fun(self: Player, spellId: number): boolean
452+
---@field canLearnSpell fun(self: Player, name: string): boolean
453+
---@field learnSpell fun(self: Player, name: string)
454+
---@field forgetSpell fun(self: Player, name: string)
455+
---@field hasLearnedSpell fun(self: Player, name: string): boolean
456456
---@field sendTutorial fun(self: Player, tutorialId: number)
457457
---@field addMapMark fun(self: Player, position: Position, type: number, description?: string)
458458
---@field save fun(self: Player)
@@ -580,6 +580,13 @@ Npc = {}
580580
---@field onPlayerCloseChannel fun(self: NpcType, callback: function): boolean
581581
---@field onPlayerEndTrade fun(self: NpcType, callback: function): boolean
582582
---@field onThink fun(self: NpcType, callback: function): boolean
583+
---@field onSayCallback fun(self: NpcType, callback: function): boolean
584+
---@field onDisappearCallback fun(self: NpcType, callback: function): boolean
585+
---@field onAppearCallback fun(self: NpcType, callback: function): boolean
586+
---@field onMoveCallback fun(self: NpcType, callback: function): boolean
587+
---@field onPlayerCloseChannelCallback fun(self: NpcType, callback: function): boolean
588+
---@field onPlayerEndTradeCallback fun(self: NpcType, callback: function): boolean
589+
---@field onThinkCallback fun(self: NpcType, callback: function): boolean
583590
NpcType = {}
584591

585592
---@class Guild
@@ -1314,7 +1321,7 @@ function getDistanceTo(creature) end
13141321
function doNpcSetCreatureFocus(creature) end
13151322
function getNpcParameter(key) end
13161323
function openShopWindow(shopWindow) end
1317-
function closeShopWindow() end
1324+
function closeShopWindow(player) end
13181325
function doSellItem(item) end
13191326

13201327
storages = {}

data/lib/compat/compat.lua

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,6 +1674,28 @@ function table.maxn(t)
16741674
return max
16751675
end
16761676

1677+
function dump(t, indent, done)
1678+
done = done or {}
1679+
indent = indent or 0
1680+
1681+
done[t] = true
1682+
1683+
for key, value in pairs(t) do
1684+
print(string.rep("\t", indent))
1685+
1686+
if type(value) == "table" and not done[value] then
1687+
done[value] = true
1688+
print(key, ":\n")
1689+
1690+
dump(value, indent + 2, done)
1691+
done[value] = nil
1692+
else
1693+
print(key, "\t=\t", value, "\n")
1694+
end
1695+
end
1696+
end
1697+
1698+
16771699
ItemType.getDuration = ItemType.getDurationMin
16781700

16791701
function getFormattedWorldTime()

data/npc/lib/revnpcsys/events.lua

Lines changed: 73 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -73,18 +73,24 @@ if not NpcEvents then
7373
---@param npc Npc The NPC that disappeared.
7474
---@param creature Creature The creature (player) that the NPC disappeared from.
7575
function NpcEvents.onDisappear(npc, creature)
76-
if not creature:isPlayer() then
77-
return
78-
end
7976
local focus = NpcFocus(npc)
80-
if focus:isFocused(creature) then
81-
focus:removeFocus(creature)
82-
local talkQueue = NpcTalkQueue(npc)
77+
local talkQueue = NpcTalkQueue(npc)
78+
local voices = NpcVoices(npc)
79+
-- If the creature is a player and is focused on the NPC, the focus is removed and the talk state is reset
80+
if creature:isPlayer() and focus:isFocused(creature) then
8381
local handler = NpcsHandler(npc)
82+
focus:removeFocus(creature)
8483
talkQueue:clearQueue(creature)
8584
handler:setTalkState(handler, creature)
8685
handler:resetData(creature)
8786
end
87+
88+
-- Npc is being removed we clear all the data which is specific to it's npc id he holds to not leak memory
89+
if npc == creature then
90+
focus:clear()
91+
talkQueue:clear()
92+
voices:clear()
93+
end
8894
end
8995

9096
-- onThink function is called when an NPC thinks.
@@ -166,12 +172,25 @@ if not NpcEvents then
166172
focus:removeFocus(creature)
167173
closeShopWindow(creature)
168174
local msg = handler.farewellResponses[math.random(1, #handler.farewellResponses)]:replaceTags({playerName = creature:getName()})
175+
if handler:getTalkState(creature).farewellResponses then
176+
msg = handler:getTalkState(creature).farewellResponses[math.random(1, #handler:getTalkState(creature).farewellResponses)]:replaceTags({playerName = creature:getName()})
177+
end
169178
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
170179
handler:setTalkState(handler, creature)
171180
handler:resetData(creature)
172181
return
173182
end
174183
end
184+
-- incase the player is not focused but he has a talk state which was not resetted
185+
-- this can only happen if player talked with the npc and the npc somehow disappeared
186+
else
187+
if not handler:getTalkState(creature):isKeyword(message) then
188+
if handler ~= handler:getTalkState(creature) then
189+
handler:setTalkState(handler, creature)
190+
handler:getTalkState(creature):checkOnStorage(creature, handler)
191+
handler:resetData(creature)
192+
end
193+
end
175194
end
176195

177196
-- Checks if the NPC has a response for the given message
@@ -189,7 +208,10 @@ if not NpcEvents then
189208
if message == word then
190209
focus:addFocus(creature)
191210
doNpcSetCreatureFocus(creature:getId())
192-
local msg = handler:getTalkState(creature).greetResponses[math.random(1, #handler:getTalkState(creature).greetResponses)]:replaceTags({playerName = creature:getName()})
211+
local msg = handler.greetResponses[math.random(1, #handler.greetResponses)]:replaceTags({playerName = creature:getName()})
212+
if handler:getTalkState(creature).greetResponses then
213+
msg = handler:getTalkState(creature).greetResponses[math.random(1, #handler:getTalkState(creature).greetResponses)]:replaceTags({playerName = creature:getName()})
214+
end
193215
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
194216
greeted = true
195217
break
@@ -199,6 +221,8 @@ if not NpcEvents then
199221
return
200222
end
201223
end
224+
-- incase shop is open and the player swaps the talk state, then we need to close the shop
225+
closeShopWindow(creature)
202226
-- renewing the focus for the player
203227
focus:addFocus(creature)
204228

@@ -209,29 +233,16 @@ if not NpcEvents then
209233
handler:getTalkState(creature):checkOnStorage(creature, handler)
210234
end
211235
-- checking for requirements
212-
local ret, msg, reqType = handler:getTalkState(creature):requirements():init(creature)
236+
local ret, msg = handler:getTalkState(creature):requirements():init(creature)
213237
if not ret then
214-
if handler:getTalkState(creature):requirements():getFailureRespond(reqType) then
215-
msg = handler:getTalkState(creature):requirements():getFailureRespond(reqType):replaceTags({playerName = creature:getName()})
238+
if msg then
239+
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
216240
end
217-
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
218241
local _, start = next(handler.keywords)
219242
handler:setTalkState(start, creature)
220243
handler:getTalkState(creature):checkOnStorage(creature, handler)
221244
return
222245
end
223-
-- check if we want to release focus for this keyword
224-
if handler:getTalkState(creature).releaseFocus then
225-
if handler:getTalkState(creature):getResponse() then
226-
local msg = handler:getTalkState(creature):getResponse():replaceTags({playerName = creature:getName()})
227-
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
228-
end
229-
focus:removeFocus(creature)
230-
closeShopWindow(creature)
231-
handler:setTalkState(handler, creature)
232-
handler:resetData(creature)
233-
return
234-
end
235246
-- check if we have a callback for this talk state
236247
local messageSent = false
237248
if handler:getTalkState(creature).callback then
@@ -246,7 +257,8 @@ if not NpcEvents then
246257
handler:setTalkState(start, creature)
247258
handler:getTalkState(creature):checkOnStorage(creature, handler)
248259
if msg == "" then
249-
print("[Warning - NpcEvents.onSay] There is no failureResponse set for keyword: ".. message ..".\n".. debug.getinfo(2).source:match("@?(.*)"))
260+
print("[Warning - NpcEvents.onSay] Npc: ".. npc:getName() .." There is no failureResponse set for keyword: ".. message)
261+
print(debug.getinfo(handler:getTalkState(creature).callback).source:match("@?(.*)"))
250262
end
251263
return
252264
end
@@ -256,18 +268,11 @@ if not NpcEvents then
256268
end
257269
end
258270
-- checking for modules
259-
-- todo implement modules
260-
if handler:getTalkState(creature).teleportPosition then
261-
focus:removeFocus(creature)
262-
closeShopWindow(creature)
263-
local msg = handler:getTalkState(creature):getResponse():replaceTags({playerName = creature:getName()})
264-
selfSay(msg, creature)
265-
Position(creature:getPosition()):sendMagicEffect(handler:getTalkState(creature).teleportPosition.magicEffectFromPos)
266-
creature:teleportTo(handler:getTalkState(creature).teleportPosition.position)
267-
Position(creature:getPosition()):sendMagicEffect(handler:getTalkState(creature).teleportPosition.magicEffectToPos)
268-
handler:setTalkState(handler, creature)
269-
handler:resetData(creature)
270-
return
271+
if handler:getTalkState(creature).modules then
272+
if handler:getTalkState(creature).modules:init(npc, creature) == false then
273+
-- need to do that because of teleport
274+
return
275+
end
271276
end
272277
-- If the NPC has a shop for the message, it opens the shop window
273278
if handler:getTalkState(creature):getShop(message) then
@@ -284,53 +289,24 @@ if not NpcEvents then
284289
subtype = item.subtype == nil and nil or item.subtype
285290
})
286291
end
287-
npc:openShopWindow(creature, afterDiscount, shop.onBuy, shop.onSell)
288-
else
289-
npc:openShopWindow(creature, items, shop.onBuy, shop.onSell)
290-
end
291-
end
292-
-- If the Player gets a storage value set
293-
if handler:getTalkState(creature).setStorage then
294-
local storage = handler:getTalkState(creature).setStorage
295-
creature:setStorageValue(storage.key, storage.value)
296-
end
297-
-- If the Player gets items added (goes into the backpack by default but if there is no space/capacity it goes into inbox)
298-
if handler:getTalkState(creature).items then
299-
local weight = 0
300-
for _, item in pairs(handler:getTalkState(creature).items.items) do
301-
-- checking how much all items would weight
302-
weight = weight + ItemType(item.item):getWeight(item.count)
303-
end
304-
local backpack = creature:getSlotItem(CONST_SLOT_BACKPACK) and Container(creature:getSlotItem(CONST_SLOT_BACKPACK).uid) or nil
305-
-- checking if the player has enough capacity or has a backpack
306-
if creature:getFreeCapacity() < weight or not backpack then
307-
local containerId = handler:getTalkState(creature).items.container or ITEM_SHOPPING_BAG
308-
creature:sendInboxItems(handler:getTalkState(creature).items.items, containerId)
309-
local msg = "The items are to heavy for you to carry. I've sent them to your inbox."
310-
creature:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, msg)
311-
-- checking if the player has enough space in the backpack
312-
elseif backpack and backpack:getEmptySlots(true) < #handler:getTalkState(creature).items.items then
313-
local containerId = handler:getTalkState(creature).items.container or ITEM_SHOPPING_BAG
314-
creature:sendInboxItems(handler:getTalkState(creature).items.items, containerId)
315-
local msg = "You don't have enough space in your backpack. I've sent the items to your inbox."
316-
creature:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, msg)
317-
-- checking if we should add the items by default into inbox
318-
elseif handler:getTalkState(creature).items.inbox then
319-
local containerId = handler:getTalkState(creature).items.container or ITEM_SHOPPING_BAG
320-
creature:sendInboxItems(handler:getTalkState(creature).items.items, containerId)
321-
local msg = "Your items are waiting for you in your inbox."
322-
creature:sendTextMessage(MESSAGE_STATUS_CONSOLE_BLUE, msg)
292+
if shop.callback then
293+
afterDiscount = shop:callback(npc, creature, handler, items, afterDiscount)
294+
end
295+
if type(afterDiscount) == "table" then
296+
npc:openShopWindow(creature, afterDiscount, shop.onBuy, shop.onSell)
297+
else
298+
print("[Warning - NpcEvents.onSay] Callback for Npc: ".. npc:getName() .." with shop: ".. handler:getActiveShop(creature) .." did not return a table.")
299+
print(debug.getinfo(shop.callback).source:match("@?(.*)"))
300+
end
323301
else
324-
if handler:getTalkState(creature).items.container then
325-
local container = Game.createItem(handler:getTalkState(creature).items.container, 1)
326-
for _, item in pairs(handler:getTalkState(creature).items.items) do
327-
container:addItem(item.item, item.count)
328-
end
329-
container:moveTo(creature)
302+
if shop.callback then
303+
items = shop:callback(npc, creature, handler, items)
304+
end
305+
if type(items) == "table" then
306+
npc:openShopWindow(creature, items, shop.onBuy, shop.onSell)
330307
else
331-
for _, item in pairs(handler:getTalkState(creature).items.items) do
332-
creature:addItem(item.item, item.count)
333-
end
308+
print("[Warning - NpcEvents.onSay] Callback for Npc: ".. npc:getName() .." with shop: ".. handler:getActiveShop(creature) .." did not return a table.")
309+
print(debug.getinfo(shop.callback).source:match("@?(.*)"))
334310
end
335311
end
336312
end
@@ -341,6 +317,18 @@ if not NpcEvents then
341317
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
342318
end
343319
end
320+
-- check if we want to release focus for this keyword
321+
if handler:getTalkState(creature).releaseFocus then
322+
if handler:getTalkState(creature):getResponse() then
323+
local msg = handler:getTalkState(creature):getResponse():replaceTags({playerName = creature:getName()})
324+
talkQueue:addToQueue(creature, msg, TALK.defaultDelay)
325+
end
326+
focus:removeFocus(creature)
327+
closeShopWindow(creature)
328+
handler:setTalkState(handler, creature)
329+
handler:resetData(creature)
330+
return
331+
end
344332
-- if the NPC has reached the last keyword, it resets the talk state
345333
if next(handler:getTalkState(creature).keywords) == nil and not handler:getTalkState(creature).answer then
346334
local _, start = next(handler.keywords)
@@ -397,7 +385,8 @@ if not NpcEvents then
397385
handler:setTalkState(start, creature)
398386
handler:getTalkState(creature):checkOnStorage(creature, handler)
399387
if msg == "" then
400-
print("[Warning - NpcEvents.onSay] There is no failureResponse set for keyword: ".. message ..".\n".. debug.getinfo(2).source:match("@?(.*)"))
388+
print("[Warning - NpcEvents.onSay] Npc: ".. npc:getName() .." has no failureResponse set for keyword: ".. message ..".")
389+
print(debug.getinfo(handler:getTalkState(creature).callback).source:match("@?(.*)"))
401390
end
402391
handler:resetData(creature)
403392
return
@@ -407,7 +396,8 @@ if not NpcEvents then
407396
messageSent = true
408397
end
409398
else
410-
print("[Warning - NpcEvents.onSay] Npc: ".. npc:getName() .." has no callback set for onAnswer\n".. debug.getinfo(2).source:match("@?(.*)"))
399+
print("[Error - NpcEvents.onSay] Npc: ".. npc:getName() .." has no callback set for onAnswer")
400+
print(debug.getinfo(handler:getTalkState(creature).callback).source:match("@?(.*)"))
411401
local _, start = next(handler.keywords)
412402
handler:setTalkState(start, creature)
413403
handler:getTalkState(creature):checkOnStorage(creature, handler)

data/npc/lib/revnpcsys/focus.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- The focus duration and distance are defined in the FOCUS table in constants.lua.
99
1010
Functions:
11+
- NpcFocus:clear()
1112
- NpcFocus:addFocus(player)
1213
- NpcFocus:isFocused(player)
1314
- NpcFocus:removeFocus(player)
@@ -17,6 +18,7 @@
1718
---@class NpcFocus
1819
---@field focus table<number, number>
1920
---@field currentFocus Player
21+
---@field clear fun()
2022
---@field addFocus fun(player: Player)
2123
---@field isFocused fun(player: Player): boolean
2224
---@field removeFocus fun(player: Player)
@@ -42,6 +44,11 @@ if not NpcFocus then
4244
end
4345
})
4446

47+
-- Clears all NpcFocus data for an NPC.
48+
function NpcFocus:clear()
49+
self = nil
50+
end
51+
4552
-- Adds focus on a player for a certain duration.
4653
---@param player Player The player to add focus on.
4754
function NpcFocus:addFocus(player)

0 commit comments

Comments
 (0)