Creating UI mods with lua & xml

Onni

Chieftain
Joined
Oct 9, 2010
Messages
82
This tutorial is meant to help you to create a simple UI mod. You will learn how to create a simple button which will change it's caption once you press it. It will also give you tips on using lua-scripts, xml-elements, the Tuner and how to write not-corefile-dependant mods. Before reading this tutorial, I'd recommend first reading this excellent Modders Guide from Kael.

Changelog:
2010-11-25 Added chapter 10 about source code


1. You are trying to trick me into reading some kind of manual! No way, Jose!
Here's a quick list of steps how to create a simple mod with an UI element.
  1. Create a Lua-project in ModBuddy.
    ModBuddy_modinfo.jpg
  2. Add a lua- and xml-file with exactly the same name to that project.
  3. Add an InGameUIAddin for your lua-file. This will also automatically import the xml-file.
    ModBuddy_content.jpg
  4. Create an xml-element in your xml-file.
  5. All done! You don't need to write anything in lua-file for just creating an xml-element and showing it.
2. XML Pilgrim vs the world
There are two types of xml-elements used in Civ: <GameData> and <Context>. GameData-elements changes the game rules and has nothing to do with this tutorial. Context-elements on the other hand create all the windows and buttons you can see. Actually almost everything you can see in Civ is created with Context-elements. So when ever this tutorial refers to xml it's always about Context-elements.

3. Local warming a.k.a why my neighbors ice caps aren't melting
There are three different levels of environment scope in Civ's Lua-engine. If you fail to change a valua or call a function then it most likely because it doesn't exist in your current environment.

  • local = Any values declared with this are only available from the current lua-element and it's children.
  • thread = This is the default scope. You can change non-local-values from any file or function as long as they are in same thread. Normally a single mod creates a single thread.
  • global = Truly global environment is not available in Civ. There is a top level lua-environment "_G" (=Main State), but you can't access that from a mod. Otherwise all threads could be accessed through this (Main State -> Threads).
4. Would you like your beds be connected or separated for your mod, Sir?
If all your mods values and functions are within a single thread then you can eg. call a function from anywhere simply like this: MyFunction()
If your mods values and functions are spread in different threads then it's not that simple anymore. You can't call a function within a different thread with normal methods. You have to do that using LuaEvent's or ShareData.
I would recommend that you try to create a single thread for your mod if it's possible. If you have only one lua- and xml-file in your mod then that's easy:
  1. Use exactly the same name for both lua- and xml-file.
  2. Add an InGameUIAddin for your lua-file. This will automatically also import the xml-file.
    ModBuddy_content.jpg
  3. This should create a single thread that is called the same as your file name.
  4. If you are using my templates then the Tuner log should read something like this after the game has started:
    Tuner_startup.jpg
  5. Also in the game the button should look like this before and after it has been clicked:
    Game_buttons.jpgGame_buttons_pressed.jpg
5. How to do it in a single bed with multiple files
If you have multiple files in your mod then you can put them in a single thread using this method.
  1. Create main lua- and xml-files and use exactly the same name for both of them. Your thread will be named after them. You can write also your normal code in them, but their main purpose is to bring all the other files into this thread.
  2. Add an InGameUIAddin for your main lua-file. This will automatically also import the main xml-file.
  3. This should create a single thread that is called the same as your file name.
  4. If you would add a second InGameUIAddin then that would create a second thread and we don't want that.
  5. Now all your lua-files will be merged into this main lua-file using include command.
  6. You may have noticed that there is no default initialization function in Civ's lua-engine. You might need it to e.g. initialize your main window AFTER you have all function declared and ready. With this method it's easy to do that by inserting your own init-function after all the include commands. Here's an example main lua-file:
    Code:
    print("PracticeMod2.lua loaded")
    include("Extra1.lua")
    include("Extra2.lua")
    Init() --My initialization function
  7. Now we will import all the xml-files using the main xml-file. Here's an example main xml-file:
    Code:
    <Context>
    	<LuaContext FileName="PracticeMod2Button1" ID="PracticeMod2Button1" Hidden="0" />
    	<LuaContext FileName="PracticeMod2Button2" ID="PracticeMod2Button2" Hidden="0" />
    </Context>
  8. Warning: Do not create lua-files that are named like these xml-files. Otherwise also your lua-code will be divided into multiple threads.
  9. There are couple minor side-effects with this one. You will get an error message saying that the lua-file for this xml-file wasn't found. And also all these xml-files will be created within their own thread. I haven't found any other way to do this, but don't worry...it only looks bad.
  10. If you are using my templates then the Tuner log should read something like this after the game has started:
    Tuner2_startup.jpg
  11. Also in the game the buttons should look like this before and after they have been clicked:
    Game2_buttons.jpgGame2_buttons_pressed.jpg
Because your xml-elements are now in different threads than your lua-files, you have to use different methods to access them.
Normally you would access an xml-element within your thread like this:
Code:
Controls.MyButton:SetHide(true)
But if you wanted to do the same thing with an element within PracticeMod2Button1 thread you would have to do it like this:
Code:
ContextPtr:LookUpControl("PracticeMod2Button1/MyButton"):SetHide(true)
Remember that you have to use those ID-tags in your xml-elements to be able to manipulate them at all through lua.

6. OMG! My Tuner is live! Or on fire?
Personally I could almost live without ModBuddy, but I would refuse to do any modding without Firaxis Live/FireTuner. This program is a Lua console that you can use to either keep track watch happening with all the lua stuff behind the game engine or run your own lua commands. Here's what you can do with it.
  • Log (Lua Console tab): If you execute a print("Hello world") command from your mod then this is the place where you can the see result. It also shows you all the lua errors that you may encounter. The first word in every row is the thread name from where that log message is coming from.
  • Lua console (Lua Console tab): At the bottom row you can see an editbox where you can execute lua commands. But before you try to do that you have to choose a thread which you want to direct that command. Just underneath the "Lua Console" tab there's a dropdown box. By default "Main State" is chosen. You can't really get anything done with this one, but more likely be greeted with an error message. Choose your mods thread from this (it's normally called the same as your mod or main file). Now all your commands will be executed as you would have written them into your actual code. Here are example logs about using wrong threads.
    Tuner_commands.jpgTuner_commands_error.jpg
  • Other thing you can use this console is to reload your lua-files without ever leaving the game. You can do that with include("YourFileName.lua") command. But be aware that there are some difficulties with this one. It doesn't replace your current file, but simply add that file a second time to your mod. Therefore it will overwrite any values or functions you had with the same name. But if you eg. had event registration in that file then you have now registered twice into that event (=your function is called twice). At the moment there is no known way to reload your xml-files.
  • Table browser (Table Browser tab): With this tool you can see all the tables within the selected thread. Confusion comes when you try to select your own mods thread from the dropdown. You can't find it! You have to add it here by right clicking on the right side (on the blue bar) of this dropdown. Choose "Edit panel" and then tick your mods checkbox on the opened list.

7. Waiter, there's a ContextPtr in my Tooltip!
ContextPtr object can be accessed from all threads. You can use it to either create new threads or manipulate xml-elements in any thread.
  • Creating new threads with ContextPtr
Code:
--This command creates a new thread. The functions argument is the name
--of the lua- and xml-file you wish to use.
--You don't normally need to use this command.
ContextPtr:LoadNewContext("FileNameWithoutTail")
  • Manipulate xml-elements with ContextPtr. ContextPtr points to the <Context> element in your xml-file. Here are few of it's commands explained.
Code:
--Will hide your context-element and everything in it.
ContextPtr:SetHide(true)
Code:
--Everytime when you hide or show this Context-element then this function is called.
ContextPtr:SetShowHideHandler(ShowHideFunction)
Code:
--Everytime when you press a key or move your mouse this function is called.
--Be careful with this one since it's called very often.
ContextPtr:SetInputHandler(InputHandlerFunction)
Code:
--This is a powerful command to refer to any element (not just Context) with ID
--within any thread. You could use it to manipulate your own sub-threads
--or even the elements in core files.
ContextPtr:LookUpControl("/ParentThreadName/SubThreadName/ElementId")

8. Who let the dogs out (into MY front lawn)
In Civ you can have multiple mods functioning at the same time so you should try to design your mods so that they will interface other mods as little as possible. Maybe the biggest question is when and how you should modify the core-files (eg. InGame.xls)? Core-files are the lua- and xml-files that come with the vanilla Civ. You can overwrite those core-files by simply adding a file with the same name into your mod. If possible you should avoid modifying them or at least do it the "right way".
  • Only if you need to attach your mods xml-element to an existing xml-element then you might need to also modify core-files.
  • If you need to modify core-xml-element then you don't necessary need to modify the actual core-file. Here's an example how to hide the science-per-turn column on top panel from your mod:
    Code:
    ContextPtr:LookUpControl("/InGame/TopPanel/SciencePerTurn"):SetHide(true)
  • It's always possible to overwrite a single core-element from your mod, but if you can you should try adding to that element instead. This enables other mods to also use that same element. Here's another example how to add a new column to top panel:
    Code:
    --In xml-file:
    <TextButton Hidden="0" Anchor="C,T" String="CUSTOM" ColorLayer0="255,255,200,255"  ID="CustomString"/>
    
    --In lua-file:
    Controls.CustomString:ChangeParent(ContextPtr:LookUpControl("/InGame/TopPanel/TopPanelInfoStack"))
    ContextPtr:LookUpControl("/InGame/TopPanel/TopPanelInfoStack"):ReprocessAnchoring()
  • Sometimes there are no perfect solutions, but just compromises. Then the best thing is to communicate with other mod authors that are using same elements. And most importantly: let the users know about it.
 
9. Mom! Where are my socks!?
I've attached templates from the mods that I used to create these examples. You can create a new project in ModBuddy using them as the template so that you can see for yourself how they work. To be able to see these templates you should save them (do not extract them, but just leave them as zip's) into this directory: "..\My Documents\Firaxis ModBuddy\Templates\ProjectTemplates\" (eg. ..\ProjectTemplates\PracticeMod.zip)

Remember to look into the top level folder in ModBuddy to see these templates:
ModBuddy_templates.jpg

10. The source is out there
Here is the source code for PracticeMod:
Spoiler :

PracticeMod.lua
Code:
print("PracticeMod.lua loaded")

--This function has to exist before it's assigned to RegisterCallback.
function MyButtonFunction(arg1, arg2)
	print("Argument 1 = " .. arg1 .. ", Argument 2 = " .. arg2)
	Controls.MyLabel:SetText("Button pressed (" .. arg1 .. ", " .. arg2 .. ")")
end

--Hides button
Controls.MyBackground:SetHide(true)
--Shows button
Controls.MyBackground:SetHide(false)
--Changes the buttons text
Controls.MyLabel:SetText("New button text")

--When you left click the button MyButtonFunction is called. MyButtonFunction has to be without () !
Controls.MyButton:RegisterCallback(Mouse.eLClick, MyButtonFunction )
--MyButtonFunction is called with these arguments (eg. MyButtonFunction(9, 99)).
--The arguments can only be numbers.
Controls.MyButton:SetVoid1(9)
Controls.MyButton:SetVoid2(99)
PracticeMod.xml
Code:
<?xml version="1.0" encoding="utf-8"?>
<Context>
	<!-- Color="red, green, blue, transparency" -->
	<!-- ConsumeMouse="1" so that we don't click anything behind the element -->
	<Box ID="MyBackground" Hidden="0" Anchor="C,C" Size="200,50" Offset="0,0" Color="0,0,0,150" ConsumeMouse="1">
		<Button ID="MyButton" Anchor="L,T" Size="200,50">
			<Label ID="MyLabel" Anchor="C,C" Font="TwCenMT18" String="Button text"/>
		</Button>
	</Box>
</Context>

And the source code for PracticeMod2:
Spoiler :

PracticeMod2.lua (main lua-file)
Code:
print("PracticeMod2.lua loaded")

include("Extra1.lua")
include("Extra2.lua")
Init()
Extra1.lua
Code:
print("Extra1.lua loaded")

function Init()
	InitMainButton()
	InitButton1()
	InitButton2()
end

function InitMainButton()
	--Hides button
	Controls.MyBackground:SetHide(true)
	--Shows button
	Controls.MyBackground:SetHide(false)
	--Changes the buttons text
	Controls.MyLabel:SetText("New main button text")

	--When you left click the button MyButtonFunction is called. MyButtonFunction has to be without () !
	Controls.MyButton:RegisterCallback(Mouse.eLClick, MyButtonFunction )
	Controls.MyButton:RegisterCallback(Mouse.eLClick, MyButtonFunction2 )
	--MyButtonFunction is called with these arguments (eg. MyButtonFunction(9, 99)).
	--The arguments can only be numbers.
	Controls.MyButton:SetVoid1(9)
	Controls.MyButton:SetVoid2(99)
end

function InitButton1()
	--Manipulate the button in sub-thread: PracticeMod2Button1
	ContextPtr:LookUpControl("PracticeMod2Button1/MyLabel"):SetText("New button1 text")
	ContextPtr:LookUpControl("PracticeMod2Button1/MyButton"):RegisterCallback(Mouse.eLClick, MyButtonFunction )
	ContextPtr:LookUpControl("PracticeMod2Button1/MyButton"):SetVoid1(1)
	ContextPtr:LookUpControl("PracticeMod2Button1/MyButton"):SetVoid2(11)
end

function InitButton2()
	--Manipulate the button in sub-thread: PracticeMod2Button2
	ContextPtr:LookUpControl("PracticeMod2Button2/MyLabel"):SetText("New button2 text")
	ContextPtr:LookUpControl("PracticeMod2Button2/MyButton"):RegisterCallback(Mouse.eLClick, MyButtonFunction )
	ContextPtr:LookUpControl("PracticeMod2Button2/MyButton"):SetVoid1(2)
	ContextPtr:LookUpControl("PracticeMod2Button2/MyButton"):SetVoid2(22)
end
Extra2.lua
Code:
print("Extra2.lua loaded")

--This function has to exist before it's assigned to RegisterCallback.
function MyButtonFunction(arg1, arg2)
	print("Argument 1 = " .. arg1 .. ", Argument 2 = " .. arg2)
	local _text = "Button pressed (" .. arg1 .. ", " .. arg2 .. ")"
	if arg1 == 9 then
		Controls.MyLabel:SetText(_text)
	elseif arg1 == 1 then
		ContextPtr:LookUpControl("PracticeMod2Button1/MyLabel"):SetText(_text)
	elseif arg1 == 2 then
		ContextPtr:LookUpControl("PracticeMod2Button2/MyLabel"):SetText(_text)
	end
end

function MyButtonFunction2(arg1, arg2)
	print("XXXArgument 1 = " .. arg1 .. ", Argument 2 = " .. arg2)
end
PracticeMod2.xml (main xml-file)
Code:
<?xml version="1.0" encoding="utf-8"?>
<Context>
	<Box ID="MyBackground" Hidden="0" Anchor="C,C" Size="200,50" Offset="0,0" Color="0,0,0,150" ConsumeMouse="1">
		<Button ID="MyButton" Anchor="L,T" Size="200,50">
			<Label ID="MyLabel" Anchor="C,C" Font="TwCenMT18" String="Main button"/>
		</Button>
	</Box>
	<LuaContext FileName="PracticeMod2Button1" ID="PracticeMod2Button1" Hidden="0" />
	<LuaContext FileName="PracticeMod2Button2" ID="PracticeMod2Button2" Hidden="0" />
</Context>
PracticeMod2Button1.xml
Code:
<?xml version="1.0" encoding="utf-8"?>
<Context>
	<Box ID="MyBackground" Anchor="C,C" Size="200,50" Offset="0,-51" Color="0,0,0,150" ConsumeMouse="1">
		<Button ID="MyButton" Anchor="L,T" Size="200,50">
			<Label ID="MyLabel" Anchor="C,C" Font="TwCenMT18" String="Button1 button"/>
		</Button>
	</Box>
</Context>
PracticeMod2Button2.xml
Code:
<?xml version="1.0" encoding="utf-8"?>
<Context>
	<Box ID="MyBackground" Anchor="C,C" Size="200,50" Offset="0,51" Color="0,0,0,150" ConsumeMouse="1">
		<Button ID="MyButton" Anchor="L,T" Size="200,50">
			<Label ID="MyLabel" Anchor="C,C" Font="TwCenMT18" String="Button2 button"/>
		</Button>
	</Box>
</Context>

Seen you in the game! ;)
 

Attachments

  • PracticeMod.zip
    10.3 KB · Views: 312
  • PracticeMod2.zip
    11.9 KB · Views: 295
Ooooo this is just what I need! Thanks, I'll be looking through this.
 
Yeah, i agree... this whole fantastic stuff should be directly linked with the Wiki.

While i'm here. Onni, could you look into "adding" some virtual button for quick access to Fire-Tuner without having to alt-tab out of everything during runtime?
 

Attachments

  • firetuner_button(Onni).png
    firetuner_button(Onni).png
    8.2 KB · Views: 5,721
This was some quite usefull information. Especially the Contextptr info. I need to digest this information and do some experimenting. Thanx a lot!
 
You know if it is possible to change the order in which the different lua-parts are anchored in the toppanel? Like if I would like the Gold per Turn to display on the right, is that possible?
 
Sorry for the late answers, but I was taking a little break from Civ modding.. :crazyeye:

Do you know how to edit the UI for trade table in diplomacy windows?
I tried editing SimpleDiploTrade.xml but no change occurs in the game.
I think the correct xml might be DiploTrade.xml? All the lua-action in trade-window is handled in TradeLogic.lua (which doesn't have equal xml-file).


While i'm here. Onni, could you look into "adding" some virtual button for quick access to Fire-Tuner without having to alt-tab out of everything during runtime?
What do you mean with this virtual button? Do you mean that you would like to see FireTuner log within a game window?


You know if it is possible to change the order in which the different lua-parts are anchored in the toppanel? Like if I would like the Gold per Turn to display on the right, is that possible?
There is one interesting looking function in \asset\UI\FrontEnd\GameSetup\SelectGameSpeed.lua
Code:
ContextPtr:BuildInstanceForControlAtIndex( "ItemInstance", controlTable, Controls.Stack, i-1 );
Haven't tried it myself, but it could be the thing you are looking for? Otherwise I don't know any perfect solutions, but perhaps first using that ChangeParent function to detach the previous elements from that TopPanelInfoStack and then attaching them back there in your preferred order could accomplish it?
 
This is a great tut - I used it to start my UI mod a while ago which I have been taking a break from recently - but could I get a tut on how to do all this with dynamic buttons? Sure it's pretty handy for a static button format, but my mod is constantly updating with in-game progress and as random things through random events unlock, more buttons automatically spawn. I need to know how to get the Lua to understand which button was pressed since dynamic box instances all contain the same name / ID.
 
Great guide! I was getting lost in my erratic lua code; the info here made things clear for me! Why didn't I read this earlier...
 
Top Bottom