Thanks to Semi-Nerdly for this article. Unfortunately the PHP-BB backup that he used to create the article was badly formatted and he didn’t find the time to reformat it, so here it is tidied up.
I created this server side mod (back in 2014) as a way to learn my way around the SWGEmu scripting code base. It’s basically a potentially useful “Hello World” project that acts as a tutorial.
To make this tutorial I referenced the guides in the SWGEmu Development Discussion forum and chatted with folks on the #opendev irc channel. Thank you muchly to all who helped!
This is a server side mod that was created using the screenplay logic. It places an NPC into the world and lets players buy an item with credits through a standard conversation. It consists of 3 files.
To use it, extract it and copy the files into your test server’s corresponding directories (folders). Then open up the README file for a guide on the very simple process of registering the three files with the server. Doing this manually is part of the learning experience!
The screenplay logic file is thoroughly commented, as I like to do. However, here is the full and detailed tutorial!
The Files Required
Adding a character or object to the world using a screenplay is very easy. It can be done within a single text file and only a few lines of code. Giving an NPC a conversation they can have with a player is a little more involved, but once you understand where things go and what they do, it’s not too difficult to create interactive custom characters.
A custom character consists of three files, 1. A Character Template that describes what model you’re using and what attributes it has.
2. A Screenplay that loads the character (and any associated decor, etc) and handles what happens to the character when the player interacts with it.
3. A Conversation Template that contains all the screens (or “stages”/”steps”) that are possible when talking with the character.
When you step back and look at it as a whole, this structure makes sense, because it keeps things neat and tidy while also easy to understand. For a huge project such as SWGEMu, it’s important to keep the code as organized as possible. This is why I decided to place these tutorial items where they are supposed to actually go in a production server, rather than creating a bunch of one-off “test” folder. It’s better to get used to using the actual file structure.
Note: When creating Lua scripts for SWGEmu, I use the Scite programming text editor and the system file manager in Linux. I search for word values in the scripts using grep in a command line window (grep -R “search-term” *). For me this is an easy and fast workflow.
Here are the locations where the files for this project reside,
Character Template workspace/Core3/MMOCoreORB/bin/scripts/mobile/quest/naboo/merch_crazy_larry.lua
Screenplay workspace/Core3/MMOCoreORB/bin/scripts/screenplays/tasks/naboo/merch_crazy_larry.lua
Conversation Template workspace/Core3/MMOCoreORB/bin/scripts/mobile/conversations/tasks/merch_crazy_larry_conv.lua
You can name your files anything you like. I named my character Crazy Larry and prefaced the file names with merch, in case I go on later to make more of these NPC merchants. I think they could be a really fun way to include a lot of the content that SOE put in the TRE files, but never got around to putting into the game for players to use. Lots of fun can be had in these conversations! Crazy Larry is actually pretty tame…
OK, let’s start with creating a custom character.
Creating a New Character Template
“The easiest way to create a new character template is to copy an existing one and change the names/values to what you’d like. This saves a lot of typing and reduces the likelihood that you’ll make a typo or forget something. However, for this turorial go ahead and navigate to
workspace/Core3/MMOCoreORB/bin/scripts/mobile/quest/naboo/
on your server and create a new file called merch_crazy_larry.lua
Copy and paste code for Crazy Larry into that file,
merch_crazy_larry = Creature:new { objectName = "@mob/creature_names:commoner", customName = "Crazy Larry", socialGroup = "townsperson", pvpFaction = "townsperson", faction = "townsperson", level = 1, chanceHit = 0.25, damageMin = 50, damageMax = 55, baseXp = 113, baseHAM = 180, baseHAMmax = 220, armor = 0, resists = {0,0,0,0,0,0,0,-1,-1}, meatType = "", meatAmount = 0, hideType = "", hideAmount = 0, boneType = "", boneAmount = 0, milk = 0, tamingChance = 0, ferocity = 0, pvpBitmask = NONE, creatureBitmask = NONE, optionsBitmask = 264, --for conversation diet = HERBIVORE, templates ={"object/mobile/dressed_criminal_thug_human_male_01.iff"}, lootGroups = {}, weapons = {}, conversationTemplate = "crazylarry_template", attacks = { } } CreatureTemplates:addCreatureTemplate(merch_crazy_larry, "merch_crazy_larry")
The important aspects of this file, for our purpose of creating a character that stands in one place and talks to the player when they start a conversation, are as follows:
merch_crazy_larry = Creature:new {
This defines the name of your new creature and is used with call that creates the character template for use by the engine, which is the last line here,
CreatureTemplates:addCreatureTemplate(merch_crazy_larry, “merch_crazy_larry”)
Note how merch_crazy_larry is spelled exactly the same in all three spots.
objectName = “@mob/creature_names:commoner”,
This will apply the default name of a commoner NPC above your character’s head, as pulled from the TRE files. Not very handy for a custom character that you would like to name appropriately.
customName = “Crazy Larry”,
Thankfully, this tag will over-ride the default name with what ever name you place inside the quotation marks!
socialGroup = “townsperson”, pvpFaction = “townsperson”, faction = “townsperson”,
This allows any player who hasn’t killed a whole lot of towns people to interact with the character. You can use this to limit who can interact with the character. Give a quick check if you’re duplicating an existing template to make a new one, just to make sure it’s what you want.
optionsBitmask = 264, –for conversation
I read on the SWGEmu wiki that this is required for conversation, but it seemed to work with other values too. But, there it is as suggested.
templates = {“object/mobile/dressed_criminal_thug_human_male_01.iff“},
This is the physical appearance of the character that you would like to use. You can look in workspace/Core3/MMOCoreORB/bin/scripts/object/mobile/ and open any of the Lua files to find the .iff file used by the character model you’d like – it’s on the last line of the file. There are hundreds of pre-defined characters to use, all based upon characters that exist in the TRE files. Creating an absolutely new custom character is outside of the scope of this tutorial (and my knowledge base). Pick one that’s close to what you’d like. And that’s it, your new character is ready to use anywhere in the game, as many times as you’d like!”
Creating the Conversation Template
“Logically to me, the next thing we should create is the Conversation Template. Why? Well, this is where you’ll layout everything that your character says to the player as well as all the possible responses the player can make. There’s no point in coding logic, before you’ve figured out how the conversation will play out! Go ahead and navigate on your server to workspace/Core3/MMOCoreORB/bin/scripts/mobile/conversations/tasks/ and create a new file called merch_crazy_larry_conv.lua
Copy and paste the following conversation code in to the file,
crazylarry_template = ConvoTemplate:new { initialScreen = "first_screen", templateType = "Lua", luaClassHandler = "crazylarry_convo_handler", screens = {} } crazylarry_first_screen = ConvoScreen:new { id = "first_screen", leftDialog = "", customDialogText = "Welcome to Crazy Larry’s Luxury Landspeeders! Would you like to buy a vehicle?", stopConversation = "false", options = { {"Speederbike – 10,000", "speederbike"}, {"No thank you.", "deny_quest"}, } } crazylarry_template:addScreen(crazylarry_first_screen); crazylarry_accept_quest = ConvoScreen:new { id = "speederbike", leftDialog = "", customDialogText = "Enjoy that Speederbike!", stopConversation = "true", options = { } } crazylarry_template:addScreen(crazylarry_accept_quest); crazylarry_deny_quest = ConvoScreen:new { id = "deny_quest", leftDialog = "", customDialogText = "Well, ya’ll have a nice day. Ya hear!", stopConversation = "true", options = { } } crazylarry_template:addScreen(crazylarry_deny_quest); crazylarry_insufficient_funds = ConvoScreen:new { id = "insufficient_funds", leftDialog = "", customDialogText = "Sorry, but you don’t have enough credits with you to purchase that. Head on over to the bank. I’ll be here when ya get back!", stopConversation = "true", options = { } } crazylarry_template:addScreen(crazylarry_insufficient_funds); crazylarry_insufficient_space = ConvoScreen:new { id = "insufficient_space", leftDialog = "", customDialogText = "Sorry, but you don’t have enough space in your inventory to accept the item. Please make some space and try again.", stopConversation = "true", options = { } } crazylarry_template:addScreen(crazylarry_insufficient_space); addConversationTemplate("crazylarry_template", crazylarry_template);
This defines conversation, sets the type conversation it is, and gives it a class name so it can be actioned upon by a screenplay. The first part of the name given to the luaClassHandler must match the first part of the name of the conversation itself, as you can see there.
initialScreen tells the engine where to start the conversation when a player engages the character.
crazylarry_first_screen = ConvoScreen:new { id = "first_screen", leftDialog = "", customDialogText = "Welcome to Crazy Larry’s Luxury Landspeeders! Would you like to buy a vehicle?", stopConversation = "false", options = { {"Speederbike – 10,000", "speederbike"}, {"No thank you.", "deny_quest"}, } } crazylarry_template:addScreen(crazylarry_first_screen);
This is the basic template that is used for all conversation interactions. The rest of the ones in this file are the same layout, just with different names and values. The things to keep in mind here are that the first line defines the name of this conversation screen and the last line registers that name for use by the engine, so the names all have to match up. It’s pretty easy to forget to change them all when copying and pasting an existing screen to use a template for a new screen, so double check them.
id = “first_screen”, This is the unique name for this screen. It will be used in your logic to play this part of the conversation. leftDialog = “”, This is used to pull existing dialog from a datatable in the TRE files. We’re not using it as we are creating a completely new character from scratch.
customDialogText = “Welcome to Crazy Larry’s Luxury Landspeeders! Would you like to buy a vehicle?”, And here is the custom dialog that will be displayed on the left of the NPC when he speaks.
stopConversation = “false”, When this is “false”, the conversation window stays open and allows the player to select an option. If there aren’t any options defined, the player will still see the “End Conversation” option. When this option is set to “true”, the conversation closes and anything the NPC says in this screen is simply printed to the spatial chat box. Setting this “true” is pretty much a way to say “good bye”.
options = { {"Speederbike – 10,000", "speederbike"}, {"No thank you.", "deny_quest"}, }
These are the options (on the right of the NPC) that the player can choose to reply to what the NPC has said. In this case, there are two options, one per line. The first set of quotes is the text that the player will read, while second set of quotes is the unique name given to option the player can choose. That unique name (also called a variable) will be used in your screenplay to take actions based upon the player’s choices. It’s a good rule of thumb to name these variables something that clearly or obviously describes their function.
addConversationTemplate(“crazylarry_template”, crazylarry_template);
This is the final line of the file that registers the conversation so that the engine can use it.
Alrighty, that covers how to make a conversation template and what all the parts mean, for our purposes. I would suggest that if you are going about creating a brand new conversation you should create a flow chart with all the dialog and links before you start writing any of the above code. It’s easy to mess things up if you’re not really sure what order you’d like things to work in.
Bringing it to life with a Screenplay
“This is where all the action takes place!Go ahead and navigate on your server to workspace/Core3/MMOCoreORB/bin/scripts/screenplays/tasks/naboo/ and create a new file named merch_crazy_larry.lua
Copy and paste the following code into the file,
CrazyLarry = ScreenPlay:new { numberOfActs = 1, questString = "crazy_larry_task", states = {}, } registerScreenPlay("CrazyLarry", true) function CrazyLarry:start() -- Spawn our character into the world, setting pLarry a pointer variable we can use to check or change his state. local pLarry = spawnMobile("naboo", "merch_crazy_larry", 1, -4881, 6.0, 4150, 35, 0 ) end crazylarry_convo_handler = Object:new { tstring = "myconversation" } function crazylarry_convo_handler:getNextConversationScreen(conversationTemplate, conversingPlayer, selectedOption) -- Assign the player to variable creature for use inside this function. local creature = LuaCreatureObject(conversingPlayer) -- Get the last conversation to determine whether or not we’re on the first screen local convosession = creature:getConversationSession() lastConversation = nil local conversation = LuaConversationTemplate(conversationTemplate) local nextConversationScreen -- If there is a conversation open, do stuff with it if ( conversation ~= nil ) then -- check to see if we have a next screen if ( convosession ~= nil ) then local session = LuaConversationSession(convosession) if ( session ~= nil ) then lastConversationScreen = session:getLastConversationScreen() end end -- Last conversation was nil, so get the first screen if ( lastConversationScreen == nil ) then nextConversationScreen = conversation:getInitialScreen() else -- Start playing the rest of the conversation based on user input local luaLastConversationScreen = LuaConversationScreen(lastConversationScreen) -- Set variable to track what option the player picked and get the option picked local optionLink = luaLastConversationScreen:getOptionLink(selectedOption) nextConversationScreen = conversation:getScreen(optionLink) -- Get some information about the player. local credits = creature:getCashCredits() local pInventory = creature:getSlottedObject("inventory") local inventory = LuaSceneObject(pInventory) -- Take action when the player makes a purchase. --if (inventory:hasFullContainerObjects() == true) then -- removed, does not work if (SceneObject(pInventory):isContainerFullRecursive()) then -- Bail if the player doesn’t have enough space in their inventory. -- Plays a chat box message from the NPC as well as a system message. nextConversationScreen = conversation:getScreen("insufficient_space") creature:sendSystemMessage("You do not have enough inventory space") elseif (optionLink == "speederbike" and credits < 10000) then -- Bail if the player doesn’t have enough cash on hand. -- Plays a chat box message from the NPC as well as a system message. nextConversationScreen = conversation:getScreen("insufficient_funds") creature:sendSystemMessage("You have insufficient funds") elseif (optionLink == "speederbike" and credits >= 10000) then -- Take 10,000 credits from the player’s cash on hand and give player a speederbike. creature:subtractCashCredits(10000) local pItem = giveItem(pInventory, "object/tangible/deed/vehicle_deed/speederbike_deed.iff", -1) end end end -- end of the conversation logic. return nextConversationScreen end function crazylarry_convo_handler:runScreenHandlers(conversationTemplate, conversingPlayer, conversingNPC, selectedOption, conversationScreen) -- Plays the screens of the conversation. return conversationScreen end
As you can see, I have put comments throughout the code to help explain the logic flow. Getting the syntax and commands right took me several hours of researching the existing SWGEmu scripting, as well reading the various tutorials on the forums, reading the wiki documentation, and chatting with some of the developers on irc. The over all architecture that allows this screenplay to function correctly in the engine is based upon [url=http://www.swgemu.com/forums/showthread.php?t=61881]Elpete’s example quest tutorial[/url]. While I am pretty confident that I have correctly noted how the architecture works, keep in mind that three days ago I didn’t know how any of it worked. But, it does work very well! The important things to note are:
- How a variable is set to track what screen of the conversation the player is on.
- How a pointer (named creature) is set to the player, so that we can query things like how many credits they have and if they have enough free inventory.
- How the logic block that takes action based upon the player’s choice first checks to see if they have enough inventory to receive the item and then if they have enough money to buy it. There’s no sense running the rest of the logic if they don’t have enough space for the item and we certainly don’t want to give them the item if they don’t have enough credits to buy it.
- local pItem = giveItem(pInventory, “object/tangible/deed/vehicle_deed/speederbike_deed.iff”, -1)
- This is able to place the item into the player’s inventory (rather than somewhere else), because we earlier defined pInventory as creature:getSlottedObject(“inventory”), where creature is the player. This is kind of an important consideration, as failing to set this correctly could, for instance, place the object into the inventory of every player lol… So, do be careful when defining these things!
Registering your files and making it go!
“The last step you’ll want to take is registering your new files, so that the server (engine) knows to load them. This is a simple task of adding a line of code to three text files. Here they are,
workspace/Core3/MMOCoreORB/bin/scripts/screenplays/screenplays.lua Add to the bottom: includeFile(“tasks/naboo/merch_crazy_larry.lua”) workspace/Core3/MMOCoreORB/bin/scripts/mobile/conversations.lua Add to the bottom: includeFile(“conversations/tasks/merch_crazy_larry_conv.lua”) workspace/Core3/MMOCoreORB/bin/scripts/mobile/quest/serverobjects.lua Add to the bottom of the Naboo section: includeFile(“quest/naboo/merch_crazy_larry.lua”)
Easy as pie. Mmmm…. pie… Now all you need to do is restart your server and head to the square in front of the Theed starport to test out our new character! Buy a speeder or three. Crazy Larry has a crazy family to feed too, ya know.—————–
While this is a very simple conversation, there is virtually no limit to what you can do with the logic and dialog. It’s really up to your imagination and logic building skills. Want to make an NPC vendor that sells different things based upon the time of day, day of the week, month of the year? That’s possible! Want to make it more story based, with lots of creative dialog? Go for it! Want to have the items sold be different based upon other quests the player’s completed, or based upon their factions, etc? Yup, you can do that too. It’s all a matter of learning the correct syntax and forming the logic to make it happen. The basic framework is there to use and enjoy.Take care and thanks for reading.