• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

[Research] New ways of bulding Npcs

Yamaken

Pro OpenTibia Developer
Joined
Jul 27, 2013
Messages
534
Solutions
4
Reaction score
430
If you already worked with Npcs code you may already have the opinion that bulding a npc is a mess. KeywordHandler may be not powerful enough, talkState[cid]/npcHandler.topic[cid] is a ugly and cumbersome manner of handling topics. Another thing is the fact that some config are on the npc xml file and the rest is in the lua file. And we don't forget the code that glue xml and lua file.

So i'm doing a code research looking foward for a better manner of building Npcs. Its important to build global servers and even more important if someone is building a lot of Npcs for their own rpg server.

Basically we have a declarative manner of building the npcs. This small code almost represents the xml file, with keywords, messages(message_greet, message_walkaway, etc), shop/trade list, voices, etc. Its means the NPC is now only one lua file, istead of a lua file + xml file.

Code:
local npc = NPC({
    name = "Chondur",
   
    messages = {
        greet = "Be greeted, child. What do you want in an old shaman's hut?",
        farewell = "Good bye.",
        walkway = "Good bye!",
        sendTrade = "Well, I currently buy mysterious and enigmatic voodoo skulls and a few other things I might need for my rituals."
    },
   
    keywords = {
        ["rituals"]  = "Hm. I don't think you need another one of my counterspells to cross the barrier on Goroma.",
    },

    voices = {
        timeout = 16,
        chance = 100,
       
        "Selling general goods and paperware! Come to my shop!"
    },
   
    shop = {
        {id = 5669,        buy = 0,        sell = 4000,        name= "Mysterious Voodoo Skull"},
        {id = 5670,        buy = 0,        sell = 4000,        name= "Enigmatic Voodoo Skull"},
        {id = 9969,        buy = 0,        sell = 4000,        name= "black skull"},
        {id = 2798,        buy = 0,        sell = 500,            name= "blood herb"},
        {id = 9447,        buy = 0,        sell = 10000,        name= "blood goblet"},
    }
})

I believe using lua tables in a declarative manner is way better than using commands to setup keywords or messages:
Ugly examples:

Code:
--to setup messages
npcHandler:setMessage(MESSAGE_GREET, 'Hi! What is it, that d\'ye {want}?')
--to give a simple answer, a lot of code
keywordHandler:addKeyword({'ribbit'}, StdModule.say, {npcHandler = npcHandler, text = "Pyrale? That idiot transformed me into an human once. But my wife came and kissed me, so I'm a frog again."})

The npc variable that has been created by NPC function is just npcHandler. Of course, with a little hack in lua we can make npcHandler works like the Npc class in tfs 1.x which means we can just:

Code:
local npc = NPC({declarative data goes here})

npc:setLight()
npc:getName()
npc:getOutfit()
npc:setOutfit()
--and all functions that Npc and Creature class have
--npc handler functions:

npc:addModule()
npc:addFocus()
--etc

Another important thing is how we handle topics. Istead of using talkstate/npcHandler.topic, i did build a module that handles topics(i'm using the declarative style using tables again):

Code:
npc.topicHandler:addTopic({
    word = {"footballs", "football"},
    answer = "Do you want to buy a football for 111 gold?",

    topics = {
        {
            word = "yes",
            answer = "Here it is.",
           
            reset = true,
           
            condition = {
                topicCondition.haveMoney(111, "You don't have enough money."),
            },
           
            action = {
                topicAction.giveItem(2109, 1),
                topicAction.removeMoney(111)
            },
        },
       
        {
            word = "no",
            answer = "Oh, but it's fun to play!",
           
            reset = true
        }
    }
})

Topics and sub topics, with conditions to answer the topic and actions when the conditions are met. We can have all types of conditions(haveMoney, haveItem, storage, etc) and all types of actions(giveMoney, giveItem, storage, etc).

A complete working example is here:

Beatrice:

http://pastebin.com/GA8PZj4x
(the trade list is just a example, it has been taken from djins)

Chondur:
http://pastebin.com/EZwyM42g

And orts old style implementation(messy):

https://github.com/orts/server/blob/master/data/npc/Chondur.xml
https://github.com/orts/server/blob/master/data/npc/scripts/Chondur.lua

Hope you guys can critize it. Thanks.

Ps: I'm doing this topic not because i'm willing to sell or release this lib, i'm doing that because i think its very important we research about new ways of building things in OTs which give us power, clearness and of course save our time.
 
I re-built my NPCs but that was because I added tons of new features.

I haven't removed the old NPC Handler yet, but I plan to.
Nice to see someone else is making better NPCs with more options as well.
 
I re-built my NPCs but that was because I added tons of new features.

I haven't removed the old NPC Handler yet, but I plan to.
Nice to see someone else is making better NPCs with more options as well.
NpcHandler is good, i don't have plans to remove it as it works very good and i like how it has a support for modules using events and such. As the backend of those declarative tables i'm using voicesModule(from orts), shopModule and topicHandler to handle keywords which is a module too. I just parse the data from the main table and inject it in the modules, with this i hide all the code and complex stuff and let the npc scripter just deal with he wants to deal: the npc.

This new way of building npcs is not about options, its about doing it right.
Current official way of doings npcs is ugly and not clean. NpcHandler, keywordHandler are both good things but they were build for programmers, its why they look so ugly even for making the npc answer one keyword.

The objective is to make simple npcs with no time, clean, easier to maintain.
 
NpcHandler is good, i don't have plans to remove it as it works very good and i like how it has a support for modules using events and such. As the backend of those declarative tables i'm using voicesModule(from orts), shopModule and topicHandler to handle keywords which is a module too. I just parse the data from the main table and inject it in the modules, with this i hide all the code and complex stuff and let the npc scripter just deal with he wants to deal: the npc.

This new way of building npcs is not about options, its about doing it right.
Current official way of doings npcs is ugly and not clean. NpcHandler, keywordHandler are both good things but they were build for programmers, its why they look so ugly even for making the npc answer one keyword.

The objective is to make simple npcs with no time, clean, easier to maintain.

My NPCs are complex and take a lot of time to make, but also I am trying to make it clean.

Which is also why I don't release it because basically no one would be able to use it other than me.
Yours is way more practical, and deserves praise :)
 
Whi World NPC config.lua - Maybe gives something to think about.
Code:
--[[ npc chat config guide
    answer = {STR}              default answer to question
    setSV  = {[K]=V}            player:setStorageValue(K, V)
    DsetSV = {[K]=V}            player:setStorageValue(K, player:getStorageValue(K)+V)      --should be written as addSV, but whatever.. xD (its just would make more sense)
    anySVS = {[K]=V}            if any of these storage values match, player can ask the question.
    anySVF = {[K]=V}            if any of these storage values match, player can't ask the question.
    allSVS = {[K]=V}            if all of these storage values match, player can ask the question.
    questionSV = INT            these questions can be asked only once and it will change the value to 1.
    SVBigger = {[K]=V}          if player:getStorageValue(K) >= V then it passes.
    SVBiggerF = {[K]=V}         if player:getStorageValue(K) >= V then it fails.
    level = INT                 if player:getLevel() >= INT then it passes.
   
    moreItems = {{              player:getItemCount(itemID, itemAID, fluidType) >= count then it passes.
        itemID = INT,           itemID = item:getId()
        itemAID = INT,          itemAID = item:getActionId()
        count = INT,            count = item:getCount()
        type = INT,             type = item:getFluidType()
    }}
    moreItemsF = {{             player:getItemCount(itemID, itemAID, fluidType) >= count then it fails.
        itemID = INT,           itemID = item:getId()
        itemAID = INT,          itemAID = item:getActionId()
        count = INT,            count = item:getCount()
        type = INT,             type = item:getFluidType()
    }}
    removeItems = {{            removes all these items
        itemID = INT,           itemID = item:getId()
        itemAID = INT,          itemAID = item:getActionId()
        count = INT,            count = item:getCount()
        type = INT,             type = item:getFluidType()
    }}
    rewardItems = {{           
        itemID = INT or {}      itemID == item:getId()
                                if itemID is table then choose 1 id randomly from table.
                           
        count = INT             amount of items rewarded
                                DEFAULT == 1
        itemAID = INT           if used item has action ID
        type = INT              if used then sets item type
    }}
   
    rewardText = {STR}          player:sendTextMessage(ORANGE, "STR")
    addRep = {[STR] = INT}      addRep(player, INT, STR)
    rewardExp = INT             player:addExperience(INT)
    townF = INT                 if player:getTown() ~= INT then it passes.
    removeOnly1 = true          removes only of the items in removeItems table
    setVariable = {K = V}       V can be function || only player attribute passed as of right now.
    getBool = {[K] = BOOL}      K = variable
    setVariableComplete = {K = V}       V can be function || only player attribute passed as of right now.
    Afunction = STR             activates function
    teleport = {                teleports player to this position.
        x=INT,
        y=INT,
        z=INT,
    }  
    closeWindow = BOOL          true = closes window after chat
]]

npcSystem4 = {}

dofile('data/npc/NPC chat/tutorial npc.lua')
dofile('data/npc/NPC chat/task master.lua')
dofile('data/npc/NPC chat/tanner.lua')
dofile('data/npc/NPC chat/priest.lua')
dofile('data/npc/NPC chat/cook.lua')
dofile('data/npc/NPC chat/smith.lua')
dofile('data/npc/NPC chat/tonka.lua')
dofile('data/npc/NPC chat/peeter.lua')
dofile('data/npc/NPC chat/liam.lua')
 
Code:
voices = {
     timeout = 16,
     chance = 100,

     "Selling general goods and paperware! Come to my shop!"
},

The message (voice) is every string which the index is a number? So that if I add another string after "Selling[...]", it'll recognize it nonetheless or does it just recognize one voice?

And about topics. Does the way you're designing it right now accept topic dependence with a depth higher than one?
For example:
buy sword, yes. Depth 1, yes is a topic that depends on "buy sword".
mission, monsters, demon, begginer, edron, yes. Depth 5, yes depends on edron, that depends on begginer and so on.¹

If you have addressed this, could you share a example? Does the topics keep being clear and easy to script to anyone or does it start getting messy?

¹: Example of a mission npc with options to collect itens or kill monsters, with regulated mission difficulty and specific area to complete the mission.
 
Code:
voices = {
     timeout = 16,
     chance = 100,

     "Selling general goods and paperware! Come to my shop!"
},

The message (voice) is every string which the index is a number? So that if I add another string after "Selling[...]", it'll recognize it nonetheless or does it just recognize one voice?
Yeah, you just add more voices to the list. Its a trick with pairs and ipairs. ipairs will ignore the timeout and chance, only the list(in the case, the voices) will be used in the loop.

And about topics. Does the way you're designing it right now accept topic dependence with a depth higher than one?
For example:
buy sword, yes. Depth 1, yes is a topic that depends on "buy sword".
mission, monsters, demon, begginer, edron, yes. Depth 5, yes depends on edron, that depends on begginer and so on.¹

If you have addressed this, could you share a example? Does the topics keep being clear and easy to script to anyone or does it start getting messy?

¹: Example of a mission npc with options to collect itens or kill monsters, with regulated mission difficulty and specific area to complete the mission.
http://pastebin.com/EZwyM42g

Chondur gots "addons" -> "yes" -> "yes" deepth.
I think if we had something like Depth 5 it will be messy.
 
That first file (Chondur) is exactly how our npc system (The OX Server) looks lol.
The only thing you have to change is "sendTrade" to "sendtrade" and "shop" to "trade" and it would work on our OT.

The old system gets very messy, but it offers everything you need. Nobody is really using the actual npc sytem, everyone just create a CALLBACK_MESSAGE_DEFAULT callback.
The keywordhandler is amazing for example.
Wrapping the npc creation into lua is a good idea.
 
That first file (Chondur) is exactly how our npc system (The OX Server) looks lol.
The only thing you have to change is "sendTrade" to "sendtrade" and "shop" to "trade" and it would work on our OT.

The old system gets very messy, but it offers everything you need. Nobody is really using the actual npc sytem, everyone just create a CALLBACK_MESSAGE_DEFAULT callback.
The keywordhandler is amazing for example.
Wrapping the npc creation into lua is a good idea.
Yeah, its really alike. I think i have saw somewhere the OX system and then i have pay attention to the fact that the npc system really needed a remake plus i always have hate the way orts npcs were build. About the fact they are almost replaceable, i think i used the obvious names.

The old system is good but like i said before it took a programmer-style instead of a scripter-style. A lot of blames goes to people that given the first examples of how build a npc. People are using the actual npc system, they are just using it in a bad way.

KeyordHandler is limited in my opinion and too much verbose, its why i created the TopicHandler.
 
Update:

Zv72KbN.png


What you guys think about simple keywords being like this? Its similar to Cipsoft 7.7 Npc System without conditions and actions.

Another improvement i did was on voices. If the voice text is to be yell, just add a "#Y " in the begning of the voice text.
 
Official Tibia
Code:
Name = "Dove"
Sex = female
Race = 1
Outfit = (136,59-86-106-115)
Home = [32919,32075,7]
Radius = 1
GoStrength = 10

Behaviour = {
ADDRESS,"hello$",! -> "Be greeted, noble %N."
ADDRESS,"hi$",!    -> *
ADDRESS,!          -> Idle
BUSY,"hello$",!    -> "Please wait a moment, %N.", Queue
BUSY,"hi$",!       -> *
BUSY,!             -> NOP
VANISH,!           -> "Come back soon, noble %N."

"bye"        -> "Come back soon, noble %N.", Idle
"farewell"   -> *

"kevin"                 -> "Mr. Postner is one of the most honorable men I know."
"postner"               -> *
"postmasters","guild"   -> "As long as everyone lives up to our standarts our guild will be fine."
"join"                  -> "We are always looking able recruits. Just speak to Mr.Postner in our headquarter." 
"headquarter"           -> "Its easy to be found. Its on the road from Thais to Kazordoon and Ab'dendriel."


"measurements",QuestValue(234)>0,QuestValue(238)<1  -> "Oh no! I knew that day would come! I am slightly above the allowed weight and if you can't supply me with some grapes to slim down I will get fired. Do you happen to have some grapes with you?",Type=3592, Amount=1,Topic=5

"grapes",QuestValue(234)>0,QuestValue(238)<1 -> "Do you happen to have some grapes with you?",Type=3592, Amount=1,Topic=5

Topic=5,"yes",Count(Type)>=Amount -> "Oh thank you! Thank you so much! So listen ... <whispers her measurements>", Delete(Type),SetQuestValue(234,QuestValue(234)+1),SetQuestValue(238,1)
Topic=5,"yes"                            -> "Don't tease me! You don't have any."
Topic=5                                   -> "Oh, no! I might loose my job."


"job"        -> "I am responsible for this post office. If you have questions about the mail system or the depots, just ask me."
"name"       -> "My name is Dove."
"dove"       -> "Yes, like the bird. <giggles>"
"time"       -> "Now it's %T."
#"mail"       -> "The Tibian mail system is unique! And everyone can use it. Do you want to know more about it?", Topic=1
#"depot"      -> "The depots are very easy to use. Just step in front of them and you will find your items in them. They are free for all Tibian citizens."
"king"       -> "Even the king can be reached by the mailsystem."
"tibianus"   -> *
"army"       -> "The soldiers get a lot of letters and parcels from Thais each week."
"ferumbras"  -> "Try to contact him by mail."
"general"    -> *
"sam"        -> "Ham? No thanks, I ate fish already."
"excalibug"  -> "If i find it in an undeliverable parcel, I will contact you."
"news"       -> "Well, there are rumours about the swampelves and the amazons, as usual."
"thais"      -> "All cities are covered by our mail system."
"carlin"     -> *
"swampelves" -> "They live somewhere in the swamp and usually stay out of our city. Only now and then some of them dare to interfere with us."
"amazon"     -> "These women are renegades from Carlin, and one of their hidden villages or hideouts might be in the swamp."

@"gen-post.ndb"

#"letter" -> Amount=1, Price=5,  "Do you want to buy a letter for %P gold?", Topic=2
#"parcel" -> Amount=1, Price=10, "Do you want to buy a parcel for %P gold?", Topic=3

#Topic=1,"yes" -> "The Tibia Mail System enables you to send and receive letters and parcels. You can buy them here if you want."
#Topic=1       -> "Is there anything else I can do for you?"

#Topic=2,"yes",CountMoney>=Price -> "Here it is. Don't forget to write the name of the receiver in the first line and the address in the second one before you put the letter in a mailbox.", DeleteMoney, Create(3505)
#Topic=2,"yes"                   -> "Oh, you have not enough gold to buy a letter."
#Topic=2                         -> "Ok."

#Topic=3,"yes",CountMoney>=Price -> "Here you are. Don't forget to write the name and the address of the receiver on the label. The label has to be in the parcel before you put the parcel in a mailbox.", DeleteMoney, Create(3503), Create(3507)
#Topic=3,"yes"                   -> "Oh, you have not enough gold to buy a parcel."
#Topic=3                         -> "Ok."
}
 
Official Tibia
Code:
Name = "Dove"
Sex = female
Race = 1
Outfit = (136,59-86-106-115)
Home = [32919,32075,7]
Radius = 1
GoStrength = 10

Behaviour = {
ADDRESS,"hello$",! -> "Be greeted, noble %N."
ADDRESS,"hi$",!    -> *
ADDRESS,!          -> Idle
BUSY,"hello$",!    -> "Please wait a moment, %N.", Queue
BUSY,"hi$",!       -> *
BUSY,!             -> NOP
VANISH,!           -> "Come back soon, noble %N."

"bye"        -> "Come back soon, noble %N.", Idle
"farewell"   -> *

"kevin"                 -> "Mr. Postner is one of the most honorable men I know."
"postner"               -> *
"postmasters","guild"   -> "As long as everyone lives up to our standarts our guild will be fine."
"join"                  -> "We are always looking able recruits. Just speak to Mr.Postner in our headquarter."
"headquarter"           -> "Its easy to be found. Its on the road from Thais to Kazordoon and Ab'dendriel."


"measurements",QuestValue(234)>0,QuestValue(238)<1  -> "Oh no! I knew that day would come! I am slightly above the allowed weight and if you can't supply me with some grapes to slim down I will get fired. Do you happen to have some grapes with you?",Type=3592, Amount=1,Topic=5

"grapes",QuestValue(234)>0,QuestValue(238)<1 -> "Do you happen to have some grapes with you?",Type=3592, Amount=1,Topic=5

Topic=5,"yes",Count(Type)>=Amount -> "Oh thank you! Thank you so much! So listen ... <whispers her measurements>", Delete(Type),SetQuestValue(234,QuestValue(234)+1),SetQuestValue(238,1)
Topic=5,"yes"                            -> "Don't tease me! You don't have any."
Topic=5                                   -> "Oh, no! I might loose my job."


"job"        -> "I am responsible for this post office. If you have questions about the mail system or the depots, just ask me."
"name"       -> "My name is Dove."
"dove"       -> "Yes, like the bird. <giggles>"
"time"       -> "Now it's %T."
#"mail"       -> "The Tibian mail system is unique! And everyone can use it. Do you want to know more about it?", Topic=1
#"depot"      -> "The depots are very easy to use. Just step in front of them and you will find your items in them. They are free for all Tibian citizens."
"king"       -> "Even the king can be reached by the mailsystem."
"tibianus"   -> *
"army"       -> "The soldiers get a lot of letters and parcels from Thais each week."
"ferumbras"  -> "Try to contact him by mail."
"general"    -> *
"sam"        -> "Ham? No thanks, I ate fish already."
"excalibug"  -> "If i find it in an undeliverable parcel, I will contact you."
"news"       -> "Well, there are rumours about the swampelves and the amazons, as usual."
"thais"      -> "All cities are covered by our mail system."
"carlin"     -> *
"swampelves" -> "They live somewhere in the swamp and usually stay out of our city. Only now and then some of them dare to interfere with us."
"amazon"     -> "These women are renegades from Carlin, and one of their hidden villages or hideouts might be in the swamp."

@"gen-post.ndb"

#"letter" -> Amount=1, Price=5,  "Do you want to buy a letter for %P gold?", Topic=2
#"parcel" -> Amount=1, Price=10, "Do you want to buy a parcel for %P gold?", Topic=3

#Topic=1,"yes" -> "The Tibia Mail System enables you to send and receive letters and parcels. You can buy them here if you want."
#Topic=1       -> "Is there anything else I can do for you?"

#Topic=2,"yes",CountMoney>=Price -> "Here it is. Don't forget to write the name of the receiver in the first line and the address in the second one before you put the letter in a mailbox.", DeleteMoney, Create(3505)
#Topic=2,"yes"                   -> "Oh, you have not enough gold to buy a letter."
#Topic=2                         -> "Ok."

#Topic=3,"yes",CountMoney>=Price -> "Here you are. Don't forget to write the name and the address of the receiver on the label. The label has to be in the parcel before you put the parcel in a mailbox.", DeleteMoney, Create(3503), Create(3507)
#Topic=3,"yes"                   -> "Oh, you have not enough gold to buy a parcel."
#Topic=3                         -> "Ok."
}
Yes, its good but its not in lua and it got plenty of limitations.
 
I know it's an old topic, but I hope you don't let this die out!
 
I know it's an old topic, but I hope you don't let this die out!
I have been using this system for like 2 years and its working good in production. I'm thinking about doing a pull request for tfs but its all depends if they are willing to accept it(otherwise i won't waste my time).
 
Maybe they are taking the transition in consideration. If there was a way to convert old NPCs to a new system then why would they deny it, one might ask.

Out of curiosity, how flexible can the NPCs be scripted? For example, can you change greeting/farewell words, use storage in the greeting part, use storage in shop (like djinn NPCs from Cipsoft) etc? Are they using same interfaces as the current one in TFS? Like onThink, onMove etc.
 
Maybe they are taking the transition in consideration. If there was a way to convert old NPCs to a new system then why would they deny it, one might ask.

Out of curiosity, how flexible can the NPCs be scripted? For example, can you change greeting/farewell words, use storage in the greeting part, use storage in shop (like djinn NPCs from Cipsoft) etc? Are they using same interfaces as the current one in TFS? Like onThink, onMove etc.
You can manipulate the trade list(or deny the trade) with callbacks, also the greet/walkway/farewell messages can be changed using callbacks.
 
Back
Top