Ext Tutorial
Ext Tutorial
1 Anatomy of an Extension
An extension adds or replaces functionality on an existing ruleset.
Adding is like building an addition to your house. You need to figure out where the addition attaches,
how it's going to look, and what's going in it. You have to understand enough of your existing structure
(i.e. the ruleset) to figure out how to shoehorn this new, alien thing into what's already there.
Replacing is more like converting your extra bedroom to a bathroom. You have to really understand
how the house is built, how the room is built, what functions the room already has, what functions
you're going to replace, etc. It can be quite daunting given that if you override something improperly,
you might loose something you need!
For this demo, we're going to add functionality. In particular, we're going to add a new window, and
put some stuff inside it.
In order to get started, FG only needs a few things from us. First, an extension.xml file, second a
windowclass that will be the basis of our extension, and, of course, a place to hold our stuff
2 Directory Structure
For our basic extension, we're going to start with the MVP (minimal viable product). So let's start by
doing the following:
• Create a new directory under your fantasy grounds extensions directory that will hold your new
extension. In my case, my FG directory is “D:\Users\FantasyGrounds\extensions”, and I'm
adding a folder called “ext-tutorial”.
While we're creating folders, we might as well create some additional folders under our new folder.
These folders are not required. The main reason we want to create these folders is to make
management of our project easier as we get more complex. Really complex extensions can have many
folders, with many sub-folders. How you organize is up to you.
For our example, since I don't anticipate our tutorial will be monstrously large, I'm going to create
folders to hold XML files, Lua files, and graphic files. In effect, under the ext-tutorial folder, I'm going
to create the following folders:
• xml
• lua
• graphics
2 TutorialWindow.xml
No we start to get to some harder stuff. First, let's create a file called TutorialWindow.xml in our XML
sub-folder. Here is the basic XML that we'll start with:
Believe it or not, this is all that's needed in order to create a brand-spankin' new window in FG. You
can see the standard xml and root tags, and then we have the following:
• windowclass: This is the name of our newly created window
• frame: This is the name of the frame for our new window.
For this tutorial, I dug around in the coreRPG files and found a frame definition for “utilitybox”. A
frame definition provides the “frame” in which we add window classes in order to build up our
window. There are 40+ framedefs within the core packages, but most of them had names that didn't
sound like something we wanted to use, I took a guess at the frame we want, and we'll be able to see if
it was a good guess quite soon.
<windowclass name="TutorialWindow">
<frame>campaignlistwithtabs</frame>
<sheetdata>
<close_campaignlist />
</sheetdata>
</windowclass>
You'll notice we had to add sheetdata to our windowclass as well. That's because we want the delete
button and script to be part of our window. By placing it in the sheetdata section, we're telling FG that
this is part of what we're displaying and acting on. You can try it without being in sheetdata and you'll
see it has no effect. FG doesn't attach it the way we want so we can't use it. Sheetdata must be in
place.
Now, if you do the usual SRO (Save file, reload rulset, open window), you'll see we have a nice
window that we can close! Go ahead and test the close by clicking on the 'X', and then opening the
window again.
You'll notice that I've specified a file in the graphics sub-folder of our extension. I created that myself
using Gimp. This isn't a graphics tutorial, so you're on your own for that part. :-) Here is what I used:
One thing you'll see is that this graphic is oriented vertically, which makes sense because
that's what the CoreRPG ruleset is expecting. In CoreRPG and other rulesets, the graphic
for the title is vertical, and along the left edge of the window. If we try to use our new
extension in 5E, our title won't work since the title location is horizontal and along the top!
We'll cover some code later to detect which is which, and switch the graphic as needed.
(Chapter 3)
When you SRO again, we now have a nice window with a title and a close button.
<root>
<icon file="graphics/TutorialTitle.png" name="TutorialTitle" />
<windowclass name="TutorialWindow">
<frame>campaignlistwithtabs</frame>
<sheetdata>
<banner_campaign>
<icon>TutorialTitle</icon>
</banner_campaign>
<close_campaignlist />
<resize_campaignlistwithtabs />
</sheetdata>
</windowclass>
</root>
Voila! The resize icon is now visible in the lower right corner. Which is nice, but it still doesn't resize.
After digging into resize_campaignlistwithtabs, one thing we can see is that unlike close_campaignlist,
there isn't any lua script that makes the window resize. This is because resize is controlled by a special
tag that we need to add in our window class. Let's add the needed tags now.
<root>
<icon file="graphics/TutorialTitle.png" name="TutorialTitle" />
<windowclass name="TutorialWindow">
<frame>campaignlistwithtabs</frame>
<sizelimits>
<dynamic />
</sizelimits>
<sheetdata>
<banner_campaign>
<icon>TutorialTitle</icon>
</banner_campaign>
<close_campaignlist />
<resize_campaignlistwithtabs />
</sheetdata>
</windowclass>
</root>
As you can see, we needed to add the “sizelimits” tag so that we could add the “dynamic” tag
underneath it. This tells FG that we want the windowclass we've just created to be resizable. When we
SRO this, we now see the pointer change to a resize pointer when we hover over the sides, or the resize
icon.
<root>
<icon file="graphics/TutorialTitle.png" name="TutorialTitle" />
<windowclass name="TutorialWindow">
<frame>campaignlistwithtabs</frame>
<sizelimits>
<dynamic />
</sizelimits>
<script>
function onInit()
--[[ Self points to a windowinstance, in this case. ]]
Debug.console("self", self)
Debug.console("getclass", self.getClass())
Debug.console("getDatabaseNode", self.getDatabaseNode())
Debug.console("getFrame", self.getFrame())
end
</script>
<sheetdata>
<banner_campaign>
<icon>TutorialTitle</icon>
</banner_campaign>
<close_campaignlist />
<resize_campaignlistwithtabs />
</sheetdata>
</windowclass>
</root>
• Script Tag - The first thing you'll notice is that we've used XML tags to identify that we're
including some lua script. Later, we'll see different ways to use the script tag.
• OnInit() - Inside the script tag, we encounter our first bit of Lua. In this case, we're creating a
new function called “onInit”. The keyword “function” identifies to the lua processor that it's a
function, and the “()” tells it that it doesn't take any arguments. Functions are just convenient
ways to group logical blocks of code together to make everything cleaner, and also to allow us
to reuse the same function. If your confused here, go find an introduction to lua tutorial!
This new function is a special function. If we have an onInit function defined, then this
function is called by FG when the window instance is created, after the onInit functions of child
controls have been called, but before the window is first displayed. Let's not worry about child
controls just yet. The important thing to know is that this function will get called when we try
to open our window.
• Comments - The next line is a Lua comment. If you remember from earlier, XML comments
start with “<!--” and end with “-->”. Lua comments come in two forms:
◦ Wherever there is a “--” then lua assumes comment to the end of line
◦ Wherever there is a “--[[“ lua assumes everything is a comment until “]]”
Easy enough, but there is a catch! When doing Lua inline (like in our example), the “--”
comment doesn't work. This is because of the way the XML gets processed it ignores end-of-
line. You must use the block form in order to put comments in your Lua code.
In addition, once you're inside the script tag, you can't use XML comments anymore! You've
left that world and know you're in Lua land!
• Console Logging - The remaining lines output some information to the FG console. You'll find
the console to be your friend when you're tearing your hair out trying to figure out what is
supposed to do what. To open the console, type “/console” in the chat window. This is another
good string to drag and drop onto your command short cuts. You'll use it a ton.
Note that the console function (just like onInit, console is a function) has that “Debug.” prefix.
That's because the console function is part of the “Debug” package. In order for our bit of code
to call the console function, we have to tell FG where to find the function. That's why we have
that prefix.
For now, go ahead and SRO. Then, once the extension is reloaded, open the console and then open our
window. You should see several lines displayed corresponding to our statements. Next, we'll look at
what those statements mean.
...
<banner_campaign>
<script>
function onInit()
Debug.console(“self”, self)
end
</script>
<icon>TutorialTitle</icon>
</banner_campaign>
...
This time when we SRO, you'll notice we get this console line printed before our windowclass console
lines. This is because the onInit script for the child nodes runs before the onInit for the parent node.
That allows the children to complete their initiation tasks before the parents have to do the same. That
way, if the parent relies on the child to be ready, everything works out.
Of particular interest is that 'self' in this context refers to a windowcontrol, rather than a windowclass.
This makes sense since the contents of sheetdata is intended to be controls. Self doesn't have as many
attributes, either. Just x,y,w,h.
...
<banner_campaign>
<script>
function onInit()
Debug.console("self", self);
if(self.hasIcon() == true) then
local rset = User.getRulesetName();
Debug.console("rulesetName", rset);
if(rset == "5E") then
self.setIcon("TutorialTitle5E", true);
else
self.setIcon("TutorialTitle", true);
end
end
end
</script>
<icon>TutorialTitle</icon>
</banner_campaign>
...
Welcome to the big time! Now we have some real programmin' shiznit goin' on in the hiz'ouse! Here,
you can see we made use of our newly discovered methods; hasIcon(), setIcon() and getRulesetName().
In addition, we've added some 'conditional' statements. Conditional statements are particular kinds of
coding statements that make our programs able to make decisions. As you can see here, the first
statement, if(self.hasIcon() == true), is looking to see if self has an Icon. If it does, it will execute the
code underneath it (before it's matching 'end' statement). If it doesn't then it will skip to its matching
end and continue with the script from there. The next conditional statement is checking to see what the
ruleset name is, and then uses the setIcon method to set to one of those two icons.
If you SRO at this point, it isn't going to work right. We need to make a change to add the new icon,
“TutorialTitle5E”. First, of course you have to make your graphic, or find one (not a photoshop course,
remember?), and here is what I used:
Next, we could add another icon tag to our TutorialWindow.xml file, but it's already starting to get
bigger. So let's move the icon tag from TutorialWindow.xml and put it in extension.xml and add the
new one like so:
Now, if you SRO everything should look good. If you exit back to the launcher, and create a 5E
campaign with this extension, the title will now open in accordance with that ruleset!
-- ----------------------------------------------------------------------
-- setIconByRuleset - This function takes a windowcontrol for an
-- argument and, if that windowcontrol has an icon
-- it sets that icon to be one of two icons, based
-- on the ruleset.
-- ----------------------------------------------------------------------
function setIconByRuleset(wc)
if(wc.hasIcon() == false) then
Debug.console("setIcon - no icon", wc)
return
end
There are a couple of things to note. First, notice that since this is a lua script, and not and XML file, I
can use the lua '–' comments that I couldn't use before.
Next, note that the function takes an argument, wc. This is because by taking the code out of the XML,
it no longer has 'self' in scope. We'll have to pass the window control when we call this, which we will
do in just a minute.
I also changed the first conditional to look for cases where the windowcontrol doesn't have an icon.
This gets rid of some of the indention, and allows us to 'return' from this function immediately if there
is no icon.
You may have noticed the use of the word local, both in the script in the XML, and in our new
function. This is used to tell the Lua interpreter that we're creating a variable, in this case rset, but we
don't want it to be visible or even persist outside of the scope we've declared it in. This keeps our code
cleaner and smaller, and eliminates the risk of colliding with other variables with the same name.
Now that that's done, let's include this lua file in our extension.xml file:
<root release="3.0" version="3">
...
<base>
<icon file="graphics/TutorialTitle5E.png" name="TutorialTitle5E" />
<icon file="graphics/TutorialTitle.png" name="TutorialTitle" />
<script name="ExtTutorial" file="lua/ExtensionTutorial.lua" />
<includefile source="xml/TutorialWindow.xml" />
</base>
</root>
And lastly, modify our onInit function to call our new function:
<banner_campaign>
<script>
function onInit()
Debug.console("self", self)
ExtTutorial.setIconByRuleset(self)
end
</script>
<icon>TutorialTitle</icon>
</banner_campaign>
That's a Wrap!
Granted, it doesn't do much, but if you've never worked in Fantasy Grounds before, we covered a good
bit of ground fairly quickly. In part 2 of this tutorial, we're going to take our MVP, add some contents,
attach it to a database, and make it do something.
In particular, we're going to make it into a simple extension for creating and saving common chat
phrases, with their appropriate emotes. In other word, if you're fond of typing “/ooc scratches his head
and wonders aloud when dinner is”, then rather than take up a command slot for that, you'll be able to
type it in once, select “ooc”, and then save it. From that point forward, a single click will send your
message to the chat console.
Hmmm. That sounds pretty hard right now. Looks like I've got my work cut out for me...
Appendix A: The Final Result
If you managed to make it through all this, then you should have something fairly equivalent to this:
Text 1: extension.xml
<?xml version="1.0" encoding="iso-8859-1"?>
<!--
� Copyright Jeffery W Redding 2015+ except where explicitly stated otherwise.
Fantasy Grounds is Copyright � 2004-2013 SmiteWorks USA LLC.
Any use of material copyrighted by others is unintentional. Let me know, and I'll make sure credit is
given appropriately.
This material is provided freely, without warranty of any kind. This material is owned by me, so any use,
modification, or publication, whether electronic, printed, verbal or anything else must provide appropriate
attribution.
-->
Text 2: ExtensionTutorial.lua
-- -----------------------------------------------------------------------------------------------------------------
-- This is the main file for the Lua Scripts for ExtensionTutorial.
-- Copyright Jeffery W. Redding 2015+ except where explicitly stated otherwise.
-- Fantasy Grounds is Copyright © 2004-2015 SmiteWorks USA LLC.
-- Any use of material copyrighted by others is unintentional. Let me know, and I'll make
-- sure credit is given appropriately.
-- This material is provided freely, without warranty of any kind. This material is owned by me, so any use,
-- modification, or publication, whether electronic, printed, verbal or anything else must provide appropriate
-- attribution.
-- -----------------------------------------------------------------------------------------------------------------
-- -----------------------------------------------------------------------------------------------------------------
-- setIconByRuleset - This function takes a windowcontrol for an argument and, if that windowcontrol has an icon
-- it sets that icon to be one of two icons, based on the ruleset.
-- -----------------------------------------------------------------------------------------------------------------
function setIconByRuleset(wc)
if(wc.hasIcon() == false) then
Debug.console("setIcon - no icon", wc)
return
end
Any use of material copyrighted by others is unintentional. Let me know, and I'll make sure credit is
given appropriately.
This material is provided freely, without warranty of any kind. This material is owned by me, so any use,
modification, or publication, whether electronic, printed, verbal or anything else must provide appropriate
attribution.
-->
<root>
<!-- This is the main window for the tutorial. I'm using the framedef from RPGCore -->
<windowclass name="TutorialWindow">
<frame>campaignlistwithtabs</frame>
<script>
function onInit()
--[[ Self points to a windowinstance, in this case. ]]
Debug.console("self", self)
Debug.console("windowclass", self.getClass())
Debug.console("node", self.getDatabaseNode())
Debug.console("frame", self.getFrame())
end
</script>
<sizelimits>
<dynamic />
</sizelimits>
<sheetdata>
<banner_campaign>
<script>
function onInit()
--[[ Self points to a windowcontrol, in this case. ]]
Debug.console("self", self);
ExtTutorial.setIconByRuleset(self);
end;
</script>
<icon>TutorialTitle</icon>
</banner_campaign>
<resize_campaignlistwithtabs />
<close_campaignlist />
</sheetdata>
</windowclass>
</root>