1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

Creating UI mods with lua & xml

Discussion in 'Civ5 - Modding Tutorials & Reference' started by Onni, Nov 23, 2010.

  1. Onni

    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.jpg Game_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.jpg Game2_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.jpg Tuner_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.
     
  2. Onni

    Onni Chieftain

    Joined:
    Oct 9, 2010
    Messages:
    82
    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! ;)
     

    Attached Files:

  3. Onni

    Onni Chieftain

    Joined:
    Oct 9, 2010
    Messages:
    82
    Again reserved for later use...
     
  4. Deep_Blue

    Deep_Blue Knight

    Joined:
    Aug 2, 2005
    Messages:
    750
    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.
     
  5. Slowpoke

    Slowpoke The Mad Modder

    Joined:
    Sep 30, 2010
    Messages:
    1,321
    Ooooo this is just what I need! Thanks, I'll be looking through this.
     
  6. Zyxpsilon

    Zyxpsilon Running Spider

    Joined:
    Oct 29, 2009
    Messages:
    3,060
    Gender:
    Male
    Location:
    On Earth
    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?
     

    Attached Files:

  7. martijnaikema

    martijnaikema Warlord

    Joined:
    Oct 30, 2010
    Messages:
    111
    This was some quite usefull information. Especially the Contextptr info. I need to digest this information and do some experimenting. Thanx a lot!
     
  8. martijnaikema

    martijnaikema Warlord

    Joined:
    Oct 30, 2010
    Messages:
    111
    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?
     
  9. Onni

    Onni Chieftain

    Joined:
    Oct 9, 2010
    Messages:
    82
    Sorry for the late answers, but I was taking a little break from Civ modding.. :crazyeye:

    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).


    What do you mean with this virtual button? Do you mean that you would like to see FireTuner log within a game window?


    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?
     
  10. Deep_Blue

    Deep_Blue Knight

    Joined:
    Aug 2, 2005
    Messages:
    750
    Do you know how to change the order of displayed contents in lua, like bringing a box element forward. I know that elements are shown in order of their location in the xml file, is there a lua function to change this order?
     
  11. Civ Fuehrer

    Civ Fuehrer Eat, Sleep, Mod

    Joined:
    May 8, 2008
    Messages:
    1,227
    Location:
    CA, USA
    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.
     
  12. Moriboe

    Moriboe King

    Joined:
    Nov 30, 2010
    Messages:
    659
    Location:
    Belgium
    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...
     

Share This Page