WhysMod Template VI

Joined
Oct 20, 2007
Messages
504
Location
Spokane, WA
WhysMod Template

under_construction.png

To be released sometime in 2025.

The true beauty of modding is that you never have to know everything at once. No matter how new and confusing something might appear, there is always a next step. One need only focus on the specific question in front of them and then explore what is possible. Within every modder is an eternal desire to experiment and discover. And with each little answer comes a small wonder that leads to whatever comes next. Join me, so that we might explore together. I have assembled these tools for you.
 
Last edited:
Table of Contents


Features
🔽 Save & Share
🔽 Inspect & Copy
🔽 Output Controls
🔽 Exception Handling

Tutorials
🔽 For Dummies
🔽 New Mod
🔽 Basic Template
🔽 Advanced Template

Pinky & The Brain
🔽 Custom Interface
🔽 Custom Texture
🔽 Custom Audio
🔽 Custom Load Screen

🔽 For Smarties

🔽 About The Author
🔽 Technical
🔽 Troubleshooting
 
Last edited:
Inspect & Copy

Spoiler :

The WhysMod Template allows users to easily inspect and copy complex tables, at any specified depth, and labels the resulting output to reveal additional information. It can handle shared and cyclic references, as well as metatables.

ToString() & Copy():
Code:
aTable = {"depth 1", {"depth 2", {"depth 3"}}};

print(ToString(aTable));    -- unlimited depth.
aCopy = Copy(aTable, true); -- unlimited depth.

ToString() Output:
Code:
array (table): 000000006C0DCBB0
	{ [ 1 ] = "depth 1"
	, [ 2 ] = array (table): 000000006C0DCE30
		{ [ 1 ] = "depth 2"
		, [ 2 ] = array (table): 000000006C0DD7E0
			{ [ 1 ] = "depth 3"
			}
		}
	}

By default, Copy() prints a description of how the data was copied.

Copy() Output:
Code:
<deep> array (table): 000000006C0DCBB0
	{ [ 1 ] = "depth 1"
	, [ 2 ] = <deep> array (table): 000000006C0DCE30
		{ [ 1 ] = "depth 2"
		, [ 2 ] = <deep> array (table): 000000006C0DD7E0
			{ [ 1 ] = "depth 3"
			}
		}
	}

Depth of 1:
Code:
print(ToString(aTable, 1));
local aCopy = Copy(aTable, 1);

When the given depth is less than the actual depth of the table, ToString() truncates the output.

ToString() Output:
Code:
array (table): 000000006C0DCBB0
	{ [ 1 ] = "depth 1"
	, [ 2 ] = array (table): 000000006C0DCE30
	}

However, Copy() does not truncate the copied data. Rather, any tables at the given depth are shallow copied, thus any tables beyond that are retained as references within the copied data.

Copy() Output:
Code:
<deep> array (table): 000000006C0DCBB0
	{ [ 1 ] = "depth 1"
	, [ 2 ] = <shallow> array (table): 000000006C0DCE30
		{ [ 1 ] = "depth 2"
		, [ 2 ] = <ref> array (table): 000000006C0DD7E0
			{ [ 1 ] = "depth 3"
			}
		}
	}

Shared & Cyclic:
Code:
local aShared = {"shared_table"};
local hTable =
	{ [ "key" ] = "value"
	, [ {"table_key", aShared} ] = {"table_value", aShared}
	};
table.insert(hTable, hTable); -- cyclic reference.

print(ToString(hTable));       -- unlimited depth.
local hCopy = Copy(hTable, 2); -- depth of 2.

ToString() Output:
Code:
<shared:2> hash (table): 0000000068604B70
	{ [ 1 ] = <cyclic:1> hash (table): 0000000068604B70
	, [ "key" ] = "value"
	, [ array (table): 0000000068604BC0
		{ [ 1 ] = "table_key"
		, [ 2 ] = <shared:2> array (table): 0000000068605390
			{ [ 1 ] = "shared_table"
			}
		}
	] = array (table): 0000000068604D50
		{ [ 1 ] = "table_value"
		, [ 2 ] = <shared:2> array (table): 0000000068605390
			{ [ 1 ] = "shared_table"
			}
		}
	}

Copy() Output:
Code:
<deep> <shared:2> hash (table): 0000000068604B70
	{ [ 1 ] = <copy> <cyclic:1> hash (table): 0000000068604B70
	, [ "key" ] = "value"
	, [ <deep> array (table): 0000000068604BC0
		{ [ 1 ] = "table_key"
		, [ 2 ] = <shallow> <shared:2> array (table): 0000000068605390
			{ [ 1 ] = "shared_table"
			}
		}
	] = <deep> array (table): 0000000068604D50
		{ [ 1 ] = "table_value"
		, [ 2 ] = <copy> <shared:2> array (table): 0000000068605390
		}
	}

For both ToString() and Copy(), initial key and value depths can be handled separately.

Separate Depths:
Code:
local hCopy = Copy(hTable, 2, 0); -- key depth 2, value depth 0.

Copy() issues a warning when a shared reference is both within and beyond the copy depth, resulting in a split reference.

Split Reference:
Code:
[Warning] Copied data contains split references.
<deep> <shared:2> hash (table): 0000000068604B70
	{ [ 1 ] = <ref> <cyclic:1> hash (table): 0000000068604B70
	, [ "key" ] = "value"
	, [ <deep> array (table): 0000000068604BC0
		{ [ 1 ] = "table_key"
		, [ 2 ] = <shallow> <shared:2> array (table): 0000000068605390
			{ [ 1 ] = "shared_table"
			}
		}
	] = <ref> array (table): 0000000068604D50
		{ [ 1 ] = "table_value"
		, [ 2 ] = <shared:2> array (table): 0000000068605390
		}
	}

ToString() and Copy() can not only handle metatables, but also infinitely nested metatables. ToString() does not describe the metatables by default, thus a meta depth must be given. The nest depth can also be specified. The meta depth and the nest depth are both given as named parameters.

Metatables:
Code:
  local aMeta1 = {"metatable"};
  local aMeta2 = {"metatable_of_metatable"};
  local hTable = {["key"] = "value"};
  setmetatable(aMeta1, aMeta2);
  setmetatable(hTable, aMeta1);

  print(ToString(hTable, nil, nil, {["metaDepth"] = true}));
  local hCopy = Copy(hTable, nil, nil, nil, {["metaDepth"] = true, ["nestDepth"] = 0});

ToString() Output:
Code:
hash (table): 000000017B9BB160
	{ [ "key" ] = "value"
	}
	<meta> array (table): 000000017B9BB570
		{ [ 1 ] = "metatable"
		}
		<meta:1> array (table): 000000017B9BA8A0
			{ [ 1 ] = "metatable_of_metatable"
			}

Copy() Output:
Code:
<shallow> hash (table): 000000017B9BB160
	{ [ "key" ] = "value"
	}
	<deep> <meta> array (table): 000000017B9BB570
		{ [ 1 ] = "metatable"
		}
		<ref> <meta:1> array (table): 000000017B9BA8A0
			{ [ 1 ] = "metatable_of_metatable"
			}

ToString() and Copy() have additional named parameters for manipulating both the data and the output. For example, both functions use a default iterator to control the order of table elements and ensure a predictable output. The default iterator can be prevented, or a substitute iterator can be provided. To learn more, look in the “ToString” and “Copy” tutorials that exist as part of the WhysMod support files. They can be output to the console for a detailed description of their full functionality.

🔼 Top
⏫ Contents
 
Last edited:
New Mod

Spoiler :

Welcome to Civ 6 modding!

The first thing you’ll need is the Sid Meier’s Civilization VI Development Tools.

You can find it in the Steam library by searching for “Civilization VI”. You will see two search results under the TOOLS header. One is labeled “Tools” and the other “Assets”, though you may need to widen the search results window to tell them apart. Install and launch the tools only. The assets will not be used in this portion of the tutorial.

Launching the tools will open a menu with “ModBuddy” at the top of the list. This is your code editor that you will use to write your mod. Lower down in the list is “FireTuner”. It is primarily used as a text output screen that provides technical information while the program runs. You’ll need it for monitoring your mod during testing. It can also be launched from ModBuddy.

Click the ModBuddy button to get started.

Spoiler Animation :
NewMod_ani_01.gif

You can either create a new project from the menu bar at the top of the window, by selecting File → New → Project, or by clicking New Mod in the content window. A new window will open with a list of available templates to choose from. Select Empty Mod from the list. Below the list is the “Name” field, with a default name of “EmptyMod1”. Rename it to “MyMod_VI” and click OK. You will have the opportunity to create a mod with your own unique name later. For now, use this name for demonstration purposes. A window will open with a “Title” field at the top, and a default title of “My Custom Mod”. Rename the title to “MyMod_VI” and click Finish.

On the left side of your ModBuddy window is an interface with the header “Solution Explorer”. If you’ve closed it by accident, you can reopen it from the menu bar by selecting View → Solution Explorer. It lists all of your project files, as well as any other files you may have opened. Your mod automatically includes a file named MyMod_VI.Art.xml. It is unnecessary for this portion of the tutorial but will become important later. Ignore it for now.

Spoiler Animation :
NewMod_ani_02.gif

Next, click the link below to download the WhysMod_VI.zip file and save it to your desktop. Double-click the zip file to open a folder view of its contents. Drag the WhysMod_VI folder to your desktop, then delete the zip file.

Dropbox: WhysMod_VI.zip

In the Solution Explorer, right-click on MyMod_VI and select Add → New Folder from the menu. Name the folder “WhysMod_VI”, then right-click on the folder and select Add → Existing Item. A file selection window will open. Navigate to your desktop and double-click the WhysMod_VI folder to open it. Select all of the files it contains and click Add. Now delete the WhysMod_VI folder from your desktop.

Spoiler Animation :
NewMod_ani_03.gif

While these files are now a part of your project, they are not currently a part of your mod when your mod is loaded in game. In the Solution Explorer, right-click on MyMod_VI again and select Properties from the menu. The project properties window gives you access to a variety of vital configuration settings. On the left side is a list of menu options. Click In-Game Actions at the bottom and then click the Add Action button on the right. An “UpdateDatabase” action will be added to the list box by default, but this isn’t the action you need. You may want to full screen the window at this point if you haven’t already. Click on the action in the list box to select it, and a new interface will appear on the right. Near the top is the “Type” field, which is a dropdown list of available actions. Click the down-arrow to the right of it and select ImportFiles from the list. Below it are four sections that open and close when their circular arrow buttons are clicked. The bottom section is labeled “Files”, which is open by default and should be the only section open. Now click the Add button to the right of it to designate which project files are imported when your mod is loaded. A new window will open with a “File” field at the top. It is a dropdown list of available project files. Click the down-arrow to the right of it, select Whys_VI.lua from the list, then click OK. Repeat these steps to also add SaveUtils_VI.lua and WhysMod_VI.lua. It does not matter in what order they are added.

Spoiler Animation :
NewMod_ani_04.gif

The remaining files require a different action. Click the Add Action button again, then select the new action and change the type to AddUserInterfaces. Click the Add button in the Files section and add GCO_ModInGame.xml. Only add the “xml” file. The “lua” file by the same name loads automatically when the xml file is loaded. Now click the circular arrow button to open the Custom Properties section above. Click the Add button to the right and a new window will open with a “Name” field at the top and a “Value” field below it. In the name field type “Context” and in the value field type “InGame”. Then click the circular arrow again to close the Custom Properties.

Spoiler Animation :
NewMod_ani_05.gif

These support files provide the full functionality of the WhysMod Template, allowing you to use all of its features to easily build a more robust and interoperable mod. However, your mod isn’t currently doing anything. For that, you will need a gameplay-script, which runs your custom code when your mod is loaded. In the Solution Explorer, right-click MyMod_VI and select Add → New Item from the menu. A new window will open with a list of available templates to choose from. Select Lua Script from the list. Below the list is the “Name” field, with a default name of “LuaScript1.lua”. Rename it to “MyMod_VI.lua” and click Add.

Now return to the project properties window. If you accidentally closed it, right-click on MyMod_VI in the Solution Explorer and select Properties again, then click In-Game Actions if it’s not already selected. Add a new action and change the type to AddGameplayScripts. In the Files section, click Add, select MyMod_VI.lua from the dropdown list, and click OK. Then close the project properties window.

Spoiler Animation :
NewMod_ani_06.gif

At the bottom of your custom MyMod_VI.lua file, copy and paste the following code.

Spoiler Code :
Code:
--==================================================
-- WhysMod_VI.lua -- Pro-Solve XTreme for Mod
--==================================================
-- requires include: none.
-- includes file: Whys_VI.lua, SaveUtils_VI.lua.

-- unique name, alphanumeric & underscore only, no digit first, no lua keywords.
WHYSMOD__MOD_NAME = "<undefined_mod_name>";

-- allows mods to easily evaluate your version when your mod name is unchanged.
WHYSMOD__MOD_ITERATION = 1; -- positive-integer only.

-- you and your team only.
WHYSMOD__MOD_AUTHOR = "<undefined_mod_author>";

-- all authors of all included files and any special thanks.
WHYSMOD__MOD_CREDIT = "<undefined_mod_credit>";

-- this script.
WHYSMOD__SCRIPT_FILE    = "<undefined_script_file>";
WHYSMOD__SCRIPT_VERSION = "<undefined_script_version>";
WHYSMOD__SCRIPT_AUTHOR  = "<undefined_script_author>";
WHYSMOD__SCRIPT_LICENSE = "<undefined_script_license>";

-- adds global class definition: CLASS__Object, CLASS__Exception, CLASS__Whys,
--     CLASS__Persistent, CLASS__SaveUtils, CLASS__WhysMod.
-- adds global class: g_ocScript.
-- adds global object: g_oWhys, g_oSave.
include("WhysMod_VI.lua");

-- tutorials, uncomment for output to console.
g_ocScript:Tutorialize_WhysMod();
--g_ocScript:Tutorialize_Save();
--g_ocScript:Tutorialize_Event();
--g_ocScript:Tutorialize_Type();
--g_ocScript:Tutorialize_ToString();
--g_ocScript:Tutorialize_Copy();
--g_ocScript:Tutorialize_Output();
--g_ocScript:Tutorialize_Trace();
--g_ocScript:Tutorialize_Throw();

-- adds global functions.
g_ocScript:globalize({""
  -- if global functions are not desired, comment out individual function names
  -- and call relevant class or object directly.

  -- see "WhysMod" tutorial (overview).
  , "GetMod", "HasMod", "GetScript", "HasScript", "Split"

  -- see "Save" tutorial.
  , "SetSave", "DltSave", "GetSave", "HasSave", "RegisterFunc", "RegisterClass"

  -- see "Event" tutorial.
  , "AddToEvent", "RmvFromEvent"

  -- see "Type" tutorial.
  , "Type_", "SubType", "IsArray", "IsPack", "FindValue", "Pack", "UnPack"
  , "IsReference", "IsPointer", "IsEvent", "IsClass", "IsObject", "IsException"

  -- see "ToString" and "Copy" tutorials.
  , "ToString", "Copy", "IsParam", "MapTree", "IsTreeMap"

  -- see "Output" and "Trace" tutorials.
  , "Error", "Notice", "Warning", "Trace", "Trace_R"
  , "SetOutput", "DltOutput", "GetOutput", "HasOutput", "SeeOutput", "WilOutput"
  , "IsToStringArg", "IsOutputProfile"

  -- see "Throw" tutorial.
  , "Throw", "Catch"
  });

-- adds global object on 'LoadGameViewStateDone' event: g_ocScript, g_oMod.
g_ocScript:Instantiate(VERSION, AUTHOR, LICENSE, WHYSMOD__MOD_ITERATION
    , WHYSMOD__MOD_AUTHOR, WHYSMOD__MOD_CREDIT);
Now press the ctrl + home keys to go to the top of the file, or just scroll to the top. There you will see the following statement.

WHYSMOD__MOD_NAME = "<undefined_mod_name>";

Change it to the following.

WHYSMOD__MOD_NAME = "MyMod_VI";

This will register your mod with the WhysMod class, which is a necessary step before accessing many of its features. The name must be unique to avoid conflict with any other mods that are also using the WhysMod Template. The mod name must conform to the same rules as a variable name. It can only include uppercase and lowercase letters, digits, and underscores. It can not contain spaces or other special characters. It can not begin with a digit and it can not be a Lua keyword: and, break, do, else, elseif, end, false, for, function, if, in, local, nil, not, or, repeat, return, then, true, until, while.

A few lines down you will see the following statement.

WHYSMOD__SCRIPT_FILE = "<undefined_script_file>";

Change it to the following and be sure to include the file extension.

WHYSMOD__SCRIPT_FILE = "MyMod_VI.lua";

This will register the current script for your mod. This is necessary because mods can be composed of multiple scripts.

Farther down, you will see a series of statements for outputting the tutorials to the FireTuner console. Only the first tutorial in the list is currently active and the rest have been commented out with preceding double-hyphens. Remove the double-hyphens from the following statements to activate the “Save” and “Event” tutorials now.

g_ocScript:Tutorialize_Save();
g_ocScript:Tutorialize_Event();

Continue to scroll down and you will see a list of global function names, available for use in your mod. These functions are globalized methods originating from multiple classes. If you don’t know what that means, then don’t worry about it and leave them the way they are. If you do know what that means, then you can comment out any global functions you don’t want and access the methods directly from the relevant class or object. You can inspect the WhysMod:globalize() method in the WhysMod_VI.lua file to determine which class or object to call.

Now scroll to the bottom of the file and add the following line of code.

Code:
print(“>>> Hello World! <<<”);

This will print the message to the FireTuner console when your mod is loaded in the game.

Click Build in the menu bar and select Build Solution. This is a necessary step that will save and compile your code, making it usable in game. You will need to rebuild your code each time you make changes to it, in order for those changes to take effect. While most changes only require returning to the main menu to start a new game, eventually you may implement changes that require restarting Civ, such as when adding new textures. That’s not something you need to worry about right now, but it’s important to be aware of it.

Spoiler Animation :
NewMod_ani_07.gif

Next, from the menu bar, select Tools → Launch FireTuner. This will open the FireTuner console, which will appear mostly empty. Now launch Civilization VI and click the Additional Content button in the main menu, then click Mods in the sub menu. Ensure your mod, and only your mod, is currently enabled. Return to the main menu and start a new single player game.

Spoiler Animation :
NewMod_ani_08.gif

Once the game is done loading, you will see a “Begin Game” button below your leader image. Click it and then switch back to FireTuner. It will contain a long list of text that was output to the console while the game was running. Scroll up and not far from the bottom you should see the following output.

MyMod_VI: >>> Hello World! <<<


✅ Congratulations! You’re a Civ6 modder.


Now click on the empty line just above “Hello World” to place the text-cursor at the bottom of the tutorials you activated earlier. Grab the vertical scroll bar on the right and slide it all the way to the top, then start scrolling down. You should see a text block with the header “WhysMod Template”. It contains the version ID along with some other information. Keep scrolling down and you should see another text block with the header “WhysMod – Basic Features”. This is the start of the tutorials. Hold the shift key and click on the empty line just above it. Once the tutorial output has been selected, copy it for paste.

Return to ModBuddy. In the Solution Explorer, right-click MyMod_VI –- the project name near the top, not the file name below it -– and select Add → New Item from the menu. Select Text File from the list and rename it “Reference.txt”, then click Add. Now paste the copied output to your Reference.txt file and save. Press ctrl + home or scroll to the top of the file. While this does not represent the entirety of the available tutorials, it does explain the features you’ll want to learn first.

Spoiler Animation :
NewMod_ani_09.gif

As a final step, return to your Civilization VI game. Go to the game menu and click Exit To Main Menu, then return to FireTuner and inspect the output. Near the bottom, you should see a text block with the header “Finalization”. It should contain a sequence of messages confirming that both MyMod_VI.lua and SaveUtils_VI.lua were finalized successfully. This ensures that your next game will load properly. If you ever encounter a fault message in the finalization sequence, then you may need to restart Civ after correcting your code.

Technically, you now have everything you need to start writing functions, add them to events, and continue building your mod. However, this portion of the tutorial is only intended as a proof-of-concept to familiarize you with the mod creation process. You are strongly encouraged to continue with the remainder of this tutorial and implement either the Basic or Advanced template provided. Doing so will maximize your mod’s interoperability with other mods and make your code easier for others to understand and build upon.

🔼 Top
⏫ Contents
 
Last edited:
Basic Template

Spoiler :

If you got here from the previous section, it is recommended that you continue using the same MyMod_VI project for demonstration purposes. Click anywhere in your MyMod_VI.lua file, press the ctrl + a keys to select all, then delete. Your file should now appear empty.

If you’ve done this portion of the tutorial before and are returning to it to build a custom mod, then you’ll need to add the WhysMod Template files to your project -- explained previously in the New Mod section -- before proceeding.

Let’s begin. First, select and copy the code below.

Spoiler Code :
Code:
--==============================================================================
local FILE    = "<undefined_script_file>";
local VERSION = "<undefined_script_version>";
local AUTHOR  = "<undefined_script_author>";
local LICENSE = "CC BY-NC-SA 4.0"; --[[
Creative Commons: This license requires that reusers give credit to the creator.
It allows reusers to distribute, remix, adapt, and build upon this file in any
medium or format, for noncommercial purposes only.  If others modify or adapt
this file, they must license the modified file under identical terms.  This
license does not apply to any included files.

--==================================================
-- File Contents
--==================================================
To navigate this file, line select a name below and do a find (ctrl+f).
To return here, do a find for triple-backtick (```).

include("WhysMod_VI.lua"
File Locals

function On_LoadGameViewStateDone(
function On_LoadScreenClose(

function Initialize_Custom(
function Initialize(
function Finalize(

--==================================================
-- How To Use
--==================================================
To use this file, add it to your mod project as an In-Game 'AddGameplayScript'
action.

--]]

-- file including.
--print("Including file '"..FILE.."' version "..VERSION
--    .." by "..AUTHOR.." -"..LICENSE);

--==================================================
-- WhysMod_VI.lua -- Pro-Solve XTreme for Mod
--==================================================
-- requires include: none.
-- includes file: Whys_VI.lua, SaveUtils_VI.lua.

-- unique name, alphanumeric & underscore only, no digit first, no lua keywords.
WHYSMOD__MOD_NAME = "<undefined_mod_name>";

-- allows mods to easily evaluate your version when your mod name is unchanged.
WHYSMOD__MOD_ITERATION = 1; -- positive-integer only.

-- you and your team only.
WHYSMOD__MOD_AUTHOR = "<undefined_mod_author>";

-- all authors of all included files and any special thanks.
WHYSMOD__MOD_CREDIT = "<undefined_mod_credit>";

-- this script.
WHYSMOD__SCRIPT_FILE    = FILE;
WHYSMOD__SCRIPT_VERSION = VERSION;
WHYSMOD__SCRIPT_AUTHOR  = AUTHOR;
WHYSMOD__SCRIPT_LICENSE = LICENSE;

-- adds global class definition: CLASS__Object, CLASS__Exception, CLASS__Whys,
--     CLASS__Persistent, CLASS__SaveUtils, CLASS__WhysMod.
-- adds global class: g_ocScript.
-- adds global object: g_oWhys, g_oSave.
include("WhysMod_VI.lua");

-- tutorials, uncomment for output to console.
g_ocScript:Tutorialize_WhysMod();
--g_ocScript:Tutorialize_Save();
--g_ocScript:Tutorialize_Event();
--g_ocScript:Tutorialize_Type();
--g_ocScript:Tutorialize_ToString();
--g_ocScript:Tutorialize_Copy();
--g_ocScript:Tutorialize_Output();
--g_ocScript:Tutorialize_Trace();
--g_ocScript:Tutorialize_Throw();

-- adds global functions.
g_ocScript:globalize({""
  -- if global functions are not desired, comment out individual function names
  -- and call relevant class or object directly.

  -- see "WhysMod" tutorial (overview).
  , "GetMod", "HasMod", "GetScript", "HasScript", "Split"

  -- see "Save" tutorial.
  , "SetSave", "DltSave", "GetSave", "HasSave", "RegisterFunc", "RegisterClass"

  -- see "Event" tutorial.
  , "AddToEvent", "RmvFromEvent"

  -- see "Type" tutorial.
  , "Type_", "SubType", "IsArray", "IsPack", "FindValue", "Pack", "UnPack"
  , "IsReference", "IsPointer", "IsEvent", "IsClass", "IsObject", "IsException"

  -- see "ToString" and "Copy" tutorials.
  , "ToString", "Copy", "IsParam", "MapTree", "IsTreeMap"

  -- see "Output" and "Trace" tutorials.
  , "Error", "Notice", "Warning", "Trace", "Trace_R"
  , "SetOutput", "DltOutput", "GetOutput", "HasOutput", "SeeOutput", "WilOutput"
  , "IsToStringArg", "IsOutputProfile"

  -- see "Throw" tutorial.
  , "Throw", "Catch"
  });

-- adds global object on 'LoadGameViewStateDone' event: g_ocScript, g_oMod.
g_ocScript:Instantiate(VERSION, AUTHOR, LICENSE, WHYSMOD__MOD_ITERATION
    , WHYSMOD__MOD_AUTHOR, WHYSMOD__MOD_CREDIT);

--==================================================
-- File Locals
--==================================================
-- none.

--==============================================================================
-- Global function.
--
-- Adds to events.  Runs on 'LoadGameViewStateDone' event.
--
-- @param  none.
--
-- @return  none-known.
--
function On_LoadGameViewStateDone()
  local FUNC = "On_LoadGameViewStateDone";
 
  -- trace call.
  Trace(FILE, FUNC);

  -- add to events.
  AddToEvent(Events.LoadScreenClose, On_LoadScreenClose);
end
--==============================================================================
-- Global function.
--
-- Runs on 'LoadScreenClose' event.
--
-- @param  none.
--
-- @return  none-known.
--
function On_LoadScreenClose()
  local FUNC = "On_LoadScreenClose";
 
  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  Notice(FILE, FUNC, nil, ">>> Hello World! <<<");
end
--==============================================================================
-- Global function, run-once.
--
-- Called by Initialize() for custom initialization.
--
-- @param  none.
--
-- @return  boolean.
--
-- @see Initialize().
--
function Initialize_Custom()
  local FUNC = "Initialize_Custom";

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local bError = false;

  --==================================================
  -- Custom Initialization
  --==================================================

  -- do-nothing.

  --==================================================

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Global function, run-once.
--
-- Most users will never need to edit this function and should leave it alone.
-- Use Initialize_Custom() for any custom initialization.
--
-- This function must be called before SaveUtils registration-complete.
--
-- Initializes this file.  Automatically runs on include.  Adds Finalize() to
-- finalization sequence.  Instantiates WhysMod class 'g_ocScript' into WhysMod
-- object on 'LoadGameViewStateDone' event.  When first script of mod to load,
-- instantiates WhysMod object 'g_oMod' on 'LoadGameViewStateDone' event.  Calls
-- Initialize_Custom() for custom initialization.
--
-- @param  none.
--
-- @return  boolean.
--
-- @see Finalize().
--
function Initialize()
  local FUNC = "Initialize";
 
  -- CLASS__WhysMod not found.
  if g_cClass == nil then
    error("\n( Uncaught Exception )--------------------------------"
        .."\n"..FILE..":"..FUNC.."(): Unable to initialize file '"..FILE
        .."', required class '"..g_cClass:getClass().."' not found.", 2);
  end

  -- registration-complete check.
  if g_ocScript:isRegComplete() then
    Throw(FILE, FUNC, nil, nil, "Invalid call, registration is complete.");
  end

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local bError = false;

  --print("Initializing custom file '"..FILE.."' for script '"
  --    ..sRegScript.."'...");

  -- adds to finalization sequence.
  g_ocScript:addFinalize(Finalize, sRegScript..":"..FILE..":Finalize");

  -- add to events on event.
  AddToEvent(Events.LoadGameViewStateDone, On_LoadGameViewStateDone);

  -- custom initialize.
  if not Initialize_Custom() then bError = true; end

  -- initialized.
  if bError then
    print("[Warning] Initialized custom file '"..FILE
        .."' for script '"..sRegScript.."' with errors.");
  else
    --print("Initialized custom file '"..FILE
    --    .."' for script '"..sRegScript.."' successfully.");
  end

  -- clear global functions.
  Initialize, Finalize, Initialize_Custom = nil, nil, nil;

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Global function.
--
-- Most users will never need to edit this function and should leave it alone.
--
-- Finalizes any custom data in this file as part of an ordered finalization
-- sequence, upon exit to main menu or restart.
--
-- @param  aExtend    array.
-- @param  bRelated   boolean.
-- @param  bFirstMod  boolean.
-- @param  sIndent    string.
--
-- @return  boolean.
--
-- @see  Initialize().
--
function Finalize(aExtend, bRelated, bFirstMod, sIndent)
  local FUNC = "Finalize";

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC, nil, aExtend, bRelated, bFirstMod, sIndent);

  -- proceed.
  local bError, fFinal = false;

  --print(sIndent.."Finalizing custom file '"..FILE.."' for script '"
  --    ..sRegScript.."'...");

  -- finalize extended.
  if aExtend ~= nil and #aExtend > 0 then
    fFinal = table.remove(aExtend);
    if not fFinal(aExtend, bRelated, bFirstMod, sIndent.."\t") then
      bError = true;
    end
  end

  --==================================================
  -- Custom Finalization
  --==================================================

  -- do-nothing.

  --==================================================

  -- finalized.
  if bError then
    print(sIndent.."[Warning] Finalized custom file '"..FILE
        .."' for script '"..sRegScript.."' with errors.");
  else
    --print(sIndent.."Finalized custom file '"..FILE
    --    .."' for script '"..sRegScript.."' successfully.");
  end

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Complete.
--

-- clear namespace.
-- none.

-- file included.
--print("Included file '"..FILE.."' version "..VERSION
--    .." by "..AUTHOR.." -"..LICENSE);

-- auto-init.
Initialize();
--==============================================================================

Paste the code to your MyMod_VI.lua file and then press the ctrl + home keys to go to the top. On line 2, you will see the following statement.

local FILE = "<undefined_script_file>";

Change this statement to the following.

local FILE = "MyMod_VI.lua";

On line 5, there is a Creative Commons license that should appeal to most modders, and a detailed explanation is given beneath it. Below is a short list of alternative Creative Commons licenses that you might prefer. If you wish to explore all of the possible options, then click the link below to use the license wizard. If you do change the license, be sure you keep the double-hyphen followed by the double-open-brackets at the end of line 5. It starts the comment block below it and your mod will not compile properly without it.

Spoiler Licenses :
Code:
-- more restrictive.
local LICENSE = "CC BY-NC-ND 4.0"; --[[
Creative Commons: This license requires that reusers give credit to the creator.
It allows reusers to copy and distribute this file in any medium or format in
unadapted form and for noncommercial purposes only.  This license does not apply
to any included files.

-- less restrictive.
local LICENSE = "CC BY-NC 4.0"; --[[
Creative Commons: This license requires that reusers give credit to the creator.
It allows reusers to distribute, remix, adapt, and build upon this file in any
medium or format, for noncommercial purposes only.  This license does not apply
to any included files.

-- least restrictive.
local LICENSE = "CC BY 4.0"; --[[
Creative Commons: This license requires that reusers give credit to the creator.
It allows reusers to distribute, remix, adapt, and build upon this file in any
medium or format, even for commercial purposes.  This license does not apply to
any included files.

-- no restrictions.
local LICENSE = "CC0 1.0 Universal"; --[[
Creative Commons: This license is dedicated to the public domain.  It allows
reusers to distribute, remix, adapt, and build upon the material in any medium
or format, even for commercial purposes.  This license does not apply to any
included files.


Next, scroll down to view the “File Contents” section. While your mod is still small, this only offers documentation of what is in the file and in what order. Thus it may be tempting to ignore it and not properly update it as the file contents change. But once your mod reaches significant size, it can become difficult to navigate and find what you’re looking for. When used properly, the File Contents provide a vital means of moving about your file with a quick and easy select and search. This method of navigation will be used throughout this tutorial.

Within the File Contents section, you will see the following text.

include("WhysMod_VI.lua"

Click the line number to the left of it to select the entire line. Now press the ctrl + f keys to do a find. A search box will open in the top right corner. At the top of the box is the search text input. At the bottom is a dropdown menu. Make sure the dropdown menu has the “Current Document” option selected, then click the “Find next” arrow button to the right of the search text. This will take you to that location in the file. In this case, it’s where the WhysMod_VI.lua file is included in your gameplay-script. Leave the search box open. You can click the find-next again to quickly return to the File Contents, then navigate to a new area.

Scroll up slightly and you will see the following statement.

WHYSMOD__MOD_NAME = "<undefined_mod_name>";

Change this statement to the following.

WHYSMOD__MOD_NAME = "MyMod_VI";

Scroll down and you will see a list of available tutorials. Uncomment any tutorials you wish to output to the console. Continue to scroll down and you will see a list of global function names, available for use in your mod. Adjacent comments identify the tutorials that explain each function.

Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Basic_Template_ani_01.gif

Click the find-next again to return to the File Contents. Within it, you will see the following text.

function Initialize(

Click the line number to the left of it, press the ctrl + f keys to do a find, and click the find-next arrow button. This will take you to your global Initialize() function, which provides a standardized means of initializing script files and ensures a proper finalization sequence upon game exit. Most modders will never need to change anything in this function and should leave it alone.

Now scroll up to the global Initialize_Custom() function above it. This function is called by Initialize() and provides an uncluttered space for adding any custom code. It is not necessary for this portion of the tutorial but will become important later.

Now scroll down past the Initialize() to your global Finalize() function. This function provides a standardized means of handling any cleanup for your custom file upon game exit. Again, most modders will never need to change anything in this function and should leave it alone.

Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Basic_Template_ani_02.gif

Click the find-next to return to the File Contents. Within it, you will see the following text.

function On_LoadGameViewStateDone(

Select it and do a find. This will take you to your global On_LoadGameViewStateDone() function. This function runs on the LoadGameViewStateDone event, which occurs prior to the LoadScreenClose event. The LoadScreenClose event occurs immediately upon clicking the Begin Game button. Most modders are unlikely to add functions to events that occur prior to LoadScreenClose, thus in the interests of greater mod interoperability, the On_LoadGameViewStateDone() function is a good place to add your custom functions to events. Currently, it is adding the On_LoadScreenClose() function to the LoadScreenClose event.

Continue to scroll down and you will see the global On_LoadScreenClose() function. Currently, it outputs the message “>>> Hello World! <<<” to the console. This is where you will put any custom code you want to run at the beginning of a game, before any player turns begin.

Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Basic_Template_ani_03.gif

Next, you’ll learn how to add a new custom function to another event that is frequently used by modders. Select and copy the code below.

Spoiler Code :
Code:
--==============================================================================
-- Global function.
--
-- Runs on 'PlayerTurnActivated' event.  Manages counter.  Starts at zero,
-- increments by 1 each turn, and outputs to console.  Works across saved games.
--
-- @param  iPlayer     nonnegative-integer.
-- @param  bFirstTime  boolean.
--
-- @return  none-known.
--
function On_PlayerTurnActivated(iPlayer, bFirstTime)
  local FUNC = "On_PlayerTurnActivated";
 
  -- trace call.
  Trace(FILE, FUNC, nil, iPlayer, bFirstTime);

  -- proceed.
  if iPlayer == 1 then
    if not HasSave("iCount") then SetSave("iCount", 0); end
    iCount = GetSave("iCount") +1;
    SetSave("iCount", iCount);
    print("\n=== Counter ===> "..tostring(iCount).."\n ");
  end
end

Now place your text-cursor at the beginning of the line directly below the “end” of the On_LoadScreenClose() function and paste. This adds the global On_PlayerTurnActivated() function to your file, to be added to the PlayerTurnActivated event in a moment.

Currently, this function is designed to increment a counter on each turn, and output the result to the console. Once the mod is running, it will demonstrate that the counter is saved and reloaded across saved games.

Partially select the new function name, from the beginning of the line to (and including) the first open parenthesis, and copy.

function On_PlayerTurnActivated(

Click the find-next to return to the File Contents and paste your new function name under the “On_LoadScreenClose“.

function On_LoadScreenClose(​
function On_PlayerTurnActivated(​

You’ve now updated the file contents to reflect the new function. Including the open parenthesis prevents any partial matches of functions with similar names.

Click the find-next again to return to the On_LoadGameViewStateDone() function. At the bottom of the function you will see the following statement.

AddToEvent(Events.LoadScreenClose, On_LoadScreenClose);

Select and copy the following code, then paste it below the statement.

Code:
AddToEvent(Events.PlayerTurnActivated, On_PlayerTurnActivated);

AddToEvent(Events.LoadScreenClose, On_LoadScreenClose);​
AddToEvent(Events.PlayerTurnActivated, On_PlayerTurnActivated);​

You’ve now added the On_PlayerTurnActivated() function to the PlayerTurnActivated event.

Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Basic_Template_ani_04.gif

The complete list of events in Civ6 is quite long and not fully covered in this tutorial. It would be impractical to include an empty event function for each one. However, below are four additional event functions many modders may want to have available.

Return to the File Contents and navigate to the On_PlayerTurnActivated() function you added earlier. Then select and copy the code below.

Spoiler Code :
Code:
--==============================================================================
-- Global function.
--
-- Runs on 'UnitSelectionChanged' event.
--
-- @param  iPlayer    integer.
-- @param  iUnit      integer.
-- @param  iHexI      integer.
-- @param  iHexJ      integer.
-- @param  iHexK      integer.
-- @param  bSelected  boolean.
-- @param  bEditable  boolean.
--
-- @return  none-known.
--
function On_UnitSelectionChanged(iPlayer, iUnit, iHexI, iHexJ, iHexK, bSelected
    , bEditable)
  local FUNC = "On_UnitSelectionChanged";
 
  -- trace call.
  Trace(FILE, FUNC, nil, iPlayer, iUnit, iHexI, iHexJ, iHexK, bSelected
      , bEditable);

  -- proceed.
  -- do-nothing.
end
--==============================================================================
-- Global function.
--
-- Runs on 'UnitMoved' event.
--
-- @param  iPlayer       integer.
-- @param  iUnit         integer.
-- @param  iX            integer.
-- @param  iY            integer.
-- @param  bVisible      boolean.
-- @param  iStateChange  integer.
--
-- @return  none-known.
--
function On_UnitMoved(iPlayer, iUnit, iX, iY, bVisible, iStateChange)
  local FUNC = "On_UnitMoved";

  -- trace call.
  Trace(FILE, FUNC, nil, iPlayer, iUnit, iX, iY, bVisible, iStateChange);

  -- proceed.
  -- do-nothing.
end
--==============================================================================
-- Global function.
--
-- Runs on 'CityInitialized' event.
--
-- @param  iPlayer  integer.
-- @param  iCity    integer.
-- @param  iX       integer.
-- @param  iY       integer.
--
-- @return  none-known.
--
function On_CityInitialized(iPlayer, iCity, iX, iY)
  local FUNC = "On_CityInitialized";
 
  -- trace call.
  Trace(FILE, FUNC, nil, iPlayer, iCity, iX, iY);

  -- proceed.
  -- do-nothing.
end
--==============================================================================
-- Global function.
--
-- Runs on 'CitySelectionChanged' event.
--
-- @param  iPlayer  integer.
-- @param  iCity    integer.
--
-- @return  none-known.
--
function On_CitySelectionChanged(iPlayer, iCity)
  local FUNC = "On_CitySelectionChanged";
 
  -- trace call.
  Trace(FILE, FUNC, nil, iPlayer, iCity);

  -- proceed.
  -- do-nothing.
end

Now place your text-cursor at the beginning of the line directly below the “end” of the On_PlayerTurnActivated() function and paste. This adds four new event functions to your file. Return to the File Contents, select and copy the code below, then update the contents to reflect the new functions.

Code:
function On_UnitSelectionChanged(
function On_UnitMoved(
function On_CityInitialized(
function On_CitySelectionChanged(

Navigate to On_LoadGameViewStateDone(). Select and copy the code below, then add the new functions to their respective events.

Code:
AddToEvent(Events.UnitSelectionChanged, On_UnitSelectionChanged);
AddToEvent(Events.UnitMoved, On_UnitMoved);
AddToEvent(Events.CityInitialized, On_CityInitialized);
AddToEvent(Events.CitySelectionChanged, On_CitySelectionChanged);

Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Basic_Template_ani_05.gif

Keep in mind, you can add functions of any name to events, and you can add more than one function per event. The “On_” + <event> function names are just a simple convention for ease of use, but it may become impractical as your mod increases in complexity.

Next, close the search box in the top right corner. If you ever need to return to the File Contents section, you can always do a find for triple-backtick. The backtick looks like a backwards apostrophe and the key is typically found to the left of the 1 key on your keyboard. Do a find for triple-backtick now and then close the search box. You should be at the File Contents.

Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Basic_Template_ani_06.gif

Your mod is now ready for testing. Click Build in the menu bar and select Build Solution. Launch FireTuner if it is not already open. If it is, then click the Clear Output button in the top right corner of the FireTuner window. This will remove any previous output to prevent confusion. Now launch Civilization VI, ensure your mod and only your mod is enabled, and then start a new game. Once the game is loaded, click Begin Game and return to FireTuner. You should see the following output at the bottom.

MyMod_VI: MyMod_VI.lua:On_LoadScreenClose(): >>> Hello World! <<<


✅ Congratulations! You’ve implemented the Basic Template.


Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Basic_Template_ani_07.gif

Next, take two turns in the game and you will see a counter output to the console.

MyMod_VI:
=== Counter ===> 1

MyMod_VI:
=== Counter ===> 2

This is the counter from your On_PlayerTurnActivated() function, which is saved and reloaded across saved games. Go to the game menu, save your game, and exit to the main menu.

Return to FireTuner and inspect the output. Near the bottom, you should see a text block with the header “Finalization”. It should contain a sequence of messages confirming that both MyMod_VI.lua and SaveUtils_VI.lua were finalized successfully. This ensures that your next game will load properly. If you ever encounter a fault message in the finalization sequence, then you may need to restart Civ after correcting your code.

Now return to Civ and load your saved game. Take a turn and return to FireTuner. You should see the following output at the bottom.

MyMod_VI:
=== Counter ===> 3

Spoiler Animation :
Basic_Template_ani_08.gif

As mentioned earlier, mods can be composed of multiple scripts. The WhysMod Template can and should be implemented in each one, allowing them to function as a team. The first script in your mod to load is treated as your centralized mod-script. Typically, this will be the gameplay-script file loaded by the "AddGameplayScript" action in your project properties. Any subsequent script files that implement the WhysMod Template are treated as related-scripts of your mod, which are then managed by the centralized mod-script. Civ6 loads any gameplay-scripts first, followed by any interface-scripts. When there is more than one script file of either category, the files are loaded in Lua-alphabetical order. Lua-alphabetical order separates the uppercase and lowercase letters, with the uppercase set coming first. Note, the underscore comes after the uppercase 'Z', as follows: A-Z _ a-z.

To implement the WhysMod Template in related-scripts of your mod, use the Basic Template as normal, with the following modification. After the File Contents where the WhysMod_VI.lua file is included in your script, you will see the following block of code.

Spoiler Code :
Code:
--==================================================
-- WhysMod_VI.lua -- Pro-Solve XTreme for Mod
--==================================================
-- requires include: none.
-- includes file: Whys_VI.lua, SaveUtils_VI.lua.

-- unique name, alphanumeric & underscore only, no digit first, no lua keywords.
WHYSMOD__MOD_NAME = "<undefined_mod_name>";

-- allows mods to easily evaluate your version when your mod name is unchanged.
WHYSMOD__MOD_ITERATION = 1; -- positive-integer only.

-- you and your team only.
WHYSMOD__MOD_AUTHOR = "<undefined_mod_author>";

-- all authors of all included files and any special thanks.
WHYSMOD__MOD_CREDIT = "<undefined_mod_credit>";

-- this script.
WHYSMOD__SCRIPT_FILE    = FILE;
WHYSMOD__SCRIPT_VERSION = VERSION;
WHYSMOD__SCRIPT_AUTHOR  = AUTHOR;
WHYSMOD__SCRIPT_LICENSE = LICENSE;

-- adds global class definition: CLASS__Object, CLASS__Exception, CLASS__Whys,
--     CLASS__Persistent, CLASS__SaveUtils, CLASS__WhysMod.
-- adds global class: g_ocScript.
-- adds global object: g_oWhys, g_oSave.
include("WhysMod_VI.lua");

-- tutorials, uncomment for output to console.
g_ocScript:Tutorialize_WhysMod();
--g_ocScript:Tutorialize_Save();
--g_ocScript:Tutorialize_Event();
--g_ocScript:Tutorialize_Type();
--g_ocScript:Tutorialize_ToString();
--g_ocScript:Tutorialize_Copy();
--g_ocScript:Tutorialize_Output();
--g_ocScript:Tutorialize_Trace();
--g_ocScript:Tutorialize_Throw();

-- adds global functions.
g_ocScript:globalize({""
  -- if global functions are not desired, comment out individual function names
  -- and call relevant class or object directly.

  -- see "WhysMod" tutorial (overview).
  , "GetMod", "HasMod", "GetScript", "HasScript", "Split"

  -- see "Save" tutorial.
  , "SetSave", "DltSave", "GetSave", "HasSave", "RegisterFunc", "RegisterClass"

  -- see "Event" tutorial.
  , "AddToEvent", "RmvFromEvent"

  -- see "Type" tutorial.
  , "Type_", "SubType", "IsArray", "IsPack", "FindValue", "Pack", "UnPack"
  , "IsReference", "IsPointer", "IsEvent", "IsClass", "IsObject", "IsException"

  -- see "ToString" and "Copy" tutorials.
  , "ToString", "Copy", "IsParam", "MapTree", "IsTreeMap"

  -- see "Output" and "Trace" tutorials.
  , "Error", "Notice", "Warning", "Trace", "Trace_R"
  , "SetOutput", "DltOutput", "GetOutput", "HasOutput", "SeeOutput", "WilOutput"
  , "IsToStringArg", "IsOutputProfile"

  -- see "Throw" tutorial.
  , "Throw", "Catch"
  });

-- adds global object on 'LoadGameViewStateDone' event: g_ocScript, g_oMod.
g_ocScript:Instantiate(VERSION, AUTHOR, LICENSE, WHYSMOD__MOD_ITERATION
    , WHYSMOD__MOD_AUTHOR, WHYSMOD__MOD_CREDIT);

Replace it with the following.

Spoiler Code :
Code:
--==================================================
-- WhysMod_VI.lua -- Pro-Solve XTreme for Mod
--==================================================
-- requires include: none.
-- includes file: Whys_VI.lua, SaveUtils_VI.lua.

-- previously-registered.
WHYSMOD__MOD_NAME = "<undefined_mod_name>";

-- this script.
WHYSMOD__SCRIPT_FILE    = FILE;
WHYSMOD__SCRIPT_VERSION = VERSION;
WHYSMOD__SCRIPT_AUTHOR  = AUTHOR;
WHYSMOD__SCRIPT_LICENSE = LICENSE;

-- adds global class definition: CLASS__Object, CLASS__Exception, CLASS__Whys,
--     CLASS__Persistent, CLASS__SaveUtils, CLASS__WhysMod.
-- adds global class: g_ocScript.
-- adds global object: g_oWhys, g_oSave.
include("WhysMod_VI.lua");

-- adds global functions.
g_ocScript:globalize(); -- same as previously-registered.

-- adds global object on 'LoadGameViewStateDone' event: g_ocScript, g_oMod.
g_ocScript:Instantiate(FILE, AUTHOR, LICENSE);

Replace "<undefined_mod_name>" with the name of your mod. The global functions enabled in your mod-script will carry over automatically, or you can supply a different list of function names you want available for that particular script. In the Pinky & The Brain section below, you will find an example of a multiscript mod that implements the Basic Template.

This concludes this portion of the tutorial. The Basic Template provides all the capabilities most modders will need, in an easy to understand format. But you are encouraged to explore the Advanced Template -- explained in the next section -- or return to it at a later date when you feel more confident. Either way, to learn about adding custom textures and sounds to your mod, or how to interact with other mods, look in the Pinky & The Brain section below.

🔼 Top
⏫ Contents
 
Last edited:
Custom Interface

Spoiler :

This section of the tutorial demonstrates how to add an In-Game user interface, as well as how to interact with other mods. The following sections then explain how to add a custom texture and custom audio to the interface.

Pinky & The Brain are two WhysMod demonstrators that work as a team when both are loaded. The Pinky mod uses the Basic Template, while the Brain mod uses the Advanced Template. Both are multiscript mods. As with all WhysMods, they can share, inspect, and copy each other’s saved data, enable or disable each other’s notices and warnings, and enable or disable each other’s tracing of function calls and returns. However, they are also designed to recognize one another and interact in a unique way. Such interoperability improves mod compatibility and greatly expands their potential.

First, open ModBuddy and start a new project. Name it “PinkyMod_VI” and add the WhysMod support files. Then create a new lua script and name it “PinkyMod_VI.lua”. Create an In-Game AddGameplayScripts action and add this file to it. Return to PinkyMod_VI.lua and replace the contents with the following code.

Spoiler Code :
Code:
--==============================================================================
local FILE    = "PinkyMod_VI.lua";
local VERSION = "vymdt.01.2024.10.31.1753";
local AUTHOR  = "Ryan F. Mercer";
local LICENSE = "CC BY-NC-SA 4.0"; --[[
Creative Commons: This license requires that reusers give credit to the creator.
It allows reusers to distribute, remix, adapt, and build upon this file in any
medium or format, for noncommercial purposes only.  If others modify or adapt
this file, they must license the modified file under identical terms.  This
license does not apply to any included files.

--==================================================
-- File Contents
--==================================================
To navigate this file, line select a name below and do a find (ctrl+f).
To return here, do a find for triple-backtick (```).

include("WhysMod_VI.lua"
File Locals

function IsOpenInterface(
function EnableCloseInterface(
function On_LoadGameViewStateDone(
function On_LoadScreenClose(
function On_PlayerTurnActivated(

function Initialize_Custom(
function Initialize(
function Finalize(

--==================================================
-- How To Use
--==================================================
To use this file, add it to your mod project as an In-Game 'AddGameplayScript'
action.  Also add file 'PinkyInterface_VI.xml' as an In-Game 'AddUserInterfaces'
action with custom property: name "Context", value "InGame".  File
'PinkyInterface_VI.lua' will load automatically when the XML file by the same
name is loaded.

--==================================================
-- Code Conventions
--==================================================
Intended data types are indicated with lowercase single-letter prefix and nil is
always an assumed possibility, intentional or not.  Some data types, such as
integer and array, are conceptual.  Multiple prefixes appear in order of greater
to lesser data type complexity (qepuxocfhkasnib).  Prefix 'i' (integer) and 'n'
(number) are mutually exclusive.  Prefix 'h' (hash) is mutually exclusive to
both 'a' (array) and 'k' (pack).  Prefix 'o' (object) and 'x' (Exception) are
mutually exclusive.  Prefix 'r' (reference) indicates and is mutually exclusive
to, all reference types (qepuxocfhka).  Prefix 'v' (variable) only appears alone
and indicates any type.  Prefix 't' (typeless) only appears alone and does not
specify type.
 
  bName  boolean
  iName  integer     (conceptual)
  nName  number      (float)
  sName  string
  aName  array       (conceptual)
  kName  pack        (conceptual)
  hName  hash        (table)
  fName  function
  cName  class       (conceptual)
  oName  object      (conceptual)
  xName  Exception   (object)
  uName  userdata
  pName  pointer     (conceptual, contains userdata)
  eName  event       (conceptual)
  qName  thread
  rName  reference   (compound type: qepuxocfhka)
  vName  variable    (any type)
  tName  typeless    (not type)

Global scope indicated with lowercase 'g_' prefix, ie: g_sName.
Class definitions indicated with uppercase 'CLASS__' prefix and assumed global,
ie: CLASS__Name.

Terms:

  unempty string       "..."
  unempty table        {...}
  positive integer     i > 0
  nonnegative integer  i >= 0
  nonpositive integer  i <= 0
  negative integer     i < 0
  nonzero integer      i < 0 or i > 0
  array                {[1] = *, [2] = *, [3] = *, [4] = *, [5] = *}
  pack                 {[2] = *, [4] = *, ["n"] = 5}
  hash                 {[*] = *}
  valid var name       alphanumeric & underscore, no digit first, no keywords.
  valid file name      no / \ : * ? < > | tab, spaces not recommended.

--]]

-- file including.
--print("Including file '"..FILE.."' version "..VERSION
--    .." by "..AUTHOR.." -"..LICENSE);

--==================================================
-- WhysMod_VI.lua -- Pro-Solve XTreme for Mod
--==================================================
-- requires include: none.
-- includes file: Whys_VI.lua, SaveUtils_VI.lua.

-- unique name, alphanumeric & underscore only, no digit first, no lua keywords.
WHYSMOD__MOD_NAME = "PinkyMod_VI";

-- allows mods to easily evaluate your version when your mod name is unchanged.
WHYSMOD__MOD_ITERATION = 1; -- positive-integer only.

-- you and your team only.
WHYSMOD__MOD_AUTHOR = "Ryan F. Mercer";

-- all authors of all included files and any special thanks.
WHYSMOD__MOD_CREDIT = "\n"
.."\tThomas C. Ruegger -- Creator of the Pinky & The Brain animated series.\n"
.."\tRobert F. Paulsen -- The voice of Pinky.";

-- this script.
WHYSMOD__SCRIPT_FILE    = FILE;
WHYSMOD__SCRIPT_VERSION = VERSION;
WHYSMOD__SCRIPT_AUTHOR  = AUTHOR;
WHYSMOD__SCRIPT_LICENSE = LICENSE;

-- adds global class definition: CLASS__Object, CLASS__Exception, CLASS__Whys,
--     CLASS__Persistent, CLASS__SaveUtils, CLASS__WhysMod.
-- adds global class: g_ocScript.
-- adds global object: g_oWhys, g_oSave.
include("WhysMod_VI.lua");

-- tutorials, uncomment for output to console.
--g_ocScript:Tutorialize_WhysMod();
--g_ocScript:Tutorialize_Save();
--g_ocScript:Tutorialize_Event();
--g_ocScript:Tutorialize_Type();
--g_ocScript:Tutorialize_ToString();
--g_ocScript:Tutorialize_Copy();
--g_ocScript:Tutorialize_Output();
--g_ocScript:Tutorialize_Trace();
--g_ocScript:Tutorialize_Throw();

-- adds global functions.
g_ocScript:globalize(true); -- all.

-- adds global object on 'LoadGameViewStateDone' event: g_ocScript, g_oMod.
g_ocScript:Instantiate(VERSION, AUTHOR, LICENSE, WHYSMOD__MOD_ITERATION
    , WHYSMOD__MOD_AUTHOR, WHYSMOD__MOD_CREDIT);

--==================================================
-- File Locals
--==================================================
-- none.

--==============================================================================
-- Global function.
--
-- Called by BrainInterface.  Checks if PinkyInterface is open and returns true
-- or false.
--
-- @param  none.
--
-- @return  boolean.
--
function IsOpenInterface()
  local FUNC = "IsOpenInterface";

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local fIsOpen = GetSave("PinkyInterface:IsOpen", nil, "__allowed");

  -- trace return.
  return Trace_R(FILE, FUNC, nil, fIsOpen());
end
--==============================================================================
-- Global function.
--
-- Called by BrainInterface.  Enables PinkyInterface close button.
--
-- @param  none.
--
-- @return  none.
--
function EnableCloseInterface()
  local FUNC = "EnableCloseInterface";

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local fEnableClose = GetSave("PinkyInterface:EnableClose", nil, "__allowed");
  fEnableClose();
end
--==============================================================================
-- Global function.
--
-- Adds to events.  Runs on 'LoadGameViewStateDone' event.
--
-- @param  none.
--
-- @return  none-known.
--
function On_LoadGameViewStateDone()
  local FUNC = "On_LoadGameViewStateDone";
 
  -- trace call.
  Trace(FILE, FUNC);

  -- add to events.
  AddToEvent(Events.LoadScreenClose, On_LoadScreenClose);
  AddToEvent(Events.PlayerTurnActivated, On_PlayerTurnActivated);

  -- share functions.
  SetSave("PinkyMod:IsOpenInterface", IsOpenInterface, "__allowed");
  SetSave("PinkyMod:EnableCloseInterface", EnableCloseInterface, "__allowed");
end
--==============================================================================
-- Global function.
--
-- Runs on 'LoadScreenClose' event.
--
-- @param  none.
--
-- @return  none-known.
--
function On_LoadScreenClose()
  local FUNC = "On_LoadScreenClose";
 
  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local fOpen = GetSave("PinkyInterface:Open", nil, "__allowed");
  fOpen();
end
--==============================================================================
-- Global function.
--
-- Runs on 'PlayerTurnActivated' event.
--
-- @param  iPlayer     nonnegative-integer.
-- @param  bFirstTime  boolean.
--
-- @return  none-known.
--
function On_PlayerTurnActivated(iPlayer, bFirstTime)
  local FUNC = "On_PlayerTurnActivated";
 
  -- trace call.
  Trace(FILE, FUNC, nil, iPlayer, bFirstTime);

  -- proceed.
  if iPlayer == 1 then
    -- do-nothing.
  end
end
--==============================================================================
-- Global function, run-once.
--
-- Called by Initialize() for custom initialization.
--
-- @param  none.
--
-- @return  boolean.
--
-- @see Initialize().
--
function Initialize_Custom()
  local FUNC = "Initialize_Custom";

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  bError = false;

  --==================================================
  -- Custom Initialization
  --==================================================

  -- allows saving functions on "__allowed" persistent on and after
  -- 'LoadGameViewStateDone' event.
  RegisterFunc("IsOpenInterface", IsOpenInterface);
  RegisterFunc("EnableCloseInterface", EnableCloseInterface);

  --==================================================

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Global function, run-once.
--
-- Most users will never need to edit this function and should leave it alone.
-- Use Initialize_Custom() for any custom initialization.
--
-- This function must be called before SaveUtils registration-complete.
--
-- Initializes this file.  Automatically runs on include.  Adds Finalize() to
-- finalization sequence.  Instantiates WhysMod class 'g_ocScript' into WhysMod
-- object on 'LoadGameViewStateDone' event.  When first script of mod to load,
-- instantiates WhysMod object 'g_oMod' on 'LoadGameViewStateDone' event.  Calls
-- Initialize_Custom() for custom initialization.
--
-- @param  none.
--
-- @return  boolean.
--
-- @see Finalize().
--
function Initialize()
  local FUNC = "Initialize";
 
  -- CLASS__WhysMod not found.
  if g_cClass == nil then
    error("\n( Uncaught Exception )--------------------------------"
        .."\n"..FILE..":"..FUNC.."(): Unable to initialize file '"..FILE
        .."', required class '"..g_cClass:getClass().."' not found.", 2);
  end

  -- registration-complete check.
  if g_ocScript:isRegComplete() then
    Throw(FILE, FUNC, nil, nil, "Invalid call, registration is complete.");
  end

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local bError = false;

  --print("Initializing custom file '"..FILE.."' for script '"
  --    ..sRegScript.."'...");

  -- adds to finalization sequence.
  g_ocScript:addFinalize(Finalize, sRegScript..":"..FILE..":Finalize");

  -- add to events on event.
  AddToEvent(Events.LoadGameViewStateDone, On_LoadGameViewStateDone);

  -- custom initialize.
  if not Initialize_Custom() then bError = true; end

  -- initialized.
  if bError then
    print("[Warning] Initialized custom file '"..FILE
        .."' for script '"..sRegScript.."' with errors.");
  else
    --print("Initialized custom file '"..FILE
    --    .."' for script '"..sRegScript.."' successfully.");
  end

  -- clear global functions.
  Initialize, Finalize, Initialize_Custom = nil, nil, nil;

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Global function.
--
-- Most users will never need to edit this function and should leave it alone.
--
-- Finalizes any custom data in this file as part of an ordered finalization
-- sequence, upon exit to main menu or restart.
--
-- @param  aExtend    array.
-- @param  bRelated   boolean.
-- @param  bFirstMod  boolean.
-- @param  sIndent    string.
--
-- @return  boolean.
--
-- @see  Initialize().
--
function Finalize(aExtend, bRelated, bFirstMod, sIndent)
  local FUNC = "Finalize";

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC, nil, aExtend, bRelated, bFirstMod, sIndent);

  -- proceed.
  local bError, fFinal = false;

  --print(sIndent.."Finalizing custom file '"..FILE.."' for script '"
  --    ..sRegScript.."'...");

  -- finalize extended.
  if aExtend ~= nil and #aExtend > 0 then
    fFinal = table.remove(aExtend);
    if not fFinal(aExtend, bRelated, bFirstMod, sIndent.."\t") then
      bError = true;
    end
  end

  --==================================================
  -- Custom Finalization
  --==================================================

  -- do-nothing.

  --==================================================

  -- finalized.
  if bError then
    print(sIndent.."[Warning] Finalized custom file '"..FILE
        .."' for script '"..sRegScript.."' with errors.");
  else
    --print(sIndent.."Finalized custom file '"..FILE
    --    .."' for script '"..sRegScript.."' successfully.");
  end

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Complete.
--

-- clear namespace.
-- none.

-- file included.
--print("Included file '"..FILE.."' version "..VERSION
--    .." by "..AUTHOR.." -"..LICENSE);

-- auto-init.
Initialize();
--==============================================================================

Next, create two additional files. For the first, create a lua script and name it “PinkyInterface_VI.lua”. Replace the contents with the following code.

Spoiler Code :
Code:
--==============================================================================
local FILE    = "PinkyInterface_VI.lua";
local VERSION = "vymdt.01.2024.10.31.1753";
local AUTHOR  = "Ryan F. Mercer";
local LICENSE = "CC BY-NC-SA 4.0"; --[[
Creative Commons: This license requires that reusers give credit to the creator.
It allows reusers to distribute, remix, adapt, and build upon this file in any
medium or format, for noncommercial purposes only.  If others modify or adapt
this file, they must license the modified file under identical terms.  This
license does not apply to any included files.

--==================================================
-- File Contents
--==================================================
To navigate this file, line select a name below and do a find (ctrl+f).
To return here, do a find for triple-backtick (```).

include("WhysMod_VI.lua"
File Locals

function Open(
function Close(
function On_Input(
function IsOpen(
function Speak(
function EnableClose(
function On_LoadGameViewStateDone(
function On_LoadScreenClose(

function Initialize_Custom(
function Initialize(
function Finalize(

--==================================================
-- How To Use
--==================================================
To use this file, add file 'PinkyMod_VI.lua' to your mod project as an In-Game
'AddGameplayScript' action.  Then add file 'PinkyInterface_VI.xml' as an In-Game
'AddUserInterfaces' action with custom property: name "Context", value "InGame".
This file will load automatically when the XML file by the same name is loaded.

--==================================================
-- Code Conventions
--==================================================
Intended data types are indicated with lowercase single-letter prefix and nil is
always an assumed possibility, intentional or not.  Some data types, such as
integer and array, are conceptual.  Multiple prefixes appear in order of greater
to lesser data type complexity (qepuxocfhkasnib).  Prefix 'i' (integer) and 'n'
(number) are mutually exclusive.  Prefix 'h' (hash) is mutually exclusive to
both 'a' (array) and 'k' (pack).  Prefix 'o' (object) and 'x' (Exception) are
mutually exclusive.  Prefix 'r' (reference) indicates and is mutually exclusive
to, all reference types (qepuxocfhka).  Prefix 'v' (variable) only appears alone
and indicates any type.  Prefix 't' (typeless) only appears alone and does not
specify type.
 
  bName  boolean
  iName  integer     (conceptual)
  nName  number      (float)
  sName  string
  aName  array       (conceptual)
  kName  pack        (conceptual)
  hName  hash        (table)
  fName  function
  cName  class       (conceptual)
  oName  object      (conceptual)
  xName  Exception   (object)
  uName  userdata
  pName  pointer     (conceptual, contains userdata)
  eName  event       (conceptual)
  qName  thread
  rName  reference   (compound type: qepuxocfhka)
  vName  variable    (any type)
  tName  typeless    (not type)

Global scope indicated with lowercase 'g_' prefix, ie: g_sName.
Class definitions indicated with uppercase 'CLASS__' prefix and assumed global,
ie: CLASS__Name.

Terms:

  unempty string       "..."
  unempty table        {...}
  positive integer     i > 0
  nonnegative integer  i >= 0
  nonpositive integer  i <= 0
  negative integer     i < 0
  nonzero integer      i < 0 or i > 0
  array                {[1] = *, [2] = *, [3] = *, [4] = *, [5] = *}
  pack                 {[2] = *, [4] = *, ["n"] = 5}
  hash                 {[*] = *}
  valid var name       alphanumeric & underscore, no digit first, no keywords.
  valid file name      no / \ : * ? < > | tab, spaces not recommended.

--]]

-- file including.
--print("Including file '"..FILE.."' version "..VERSION
--    .." by "..AUTHOR.." -"..LICENSE);

--==================================================
-- WhysMod_VI.lua -- Pro-Solve XTreme for Mod
--==================================================
-- requires include: none.
-- includes file: Whys_VI.lua, SaveUtils_VI.lua.

-- previously-registered.
WHYSMOD__MOD_NAME = "PinkyMod_VI";

-- this script.
WHYSMOD__SCRIPT_FILE    = FILE;
WHYSMOD__SCRIPT_VERSION = VERSION;
WHYSMOD__SCRIPT_AUTHOR  = AUTHOR;
WHYSMOD__SCRIPT_LICENSE = LICENSE;

-- adds global class definition: CLASS__Object, CLASS__Exception, CLASS__Whys,
--     CLASS__Persistent, CLASS__SaveUtils, CLASS__WhysMod.
-- adds global class: g_ocScript.
-- adds global object: g_oWhys, g_oSave.
include("WhysMod_VI.lua");

-- adds global functions.
g_ocScript:globalize(); -- same as previously-registered.

-- adds global object on 'LoadGameViewStateDone' event: g_ocScript, g_oMod.
g_ocScript:Instantiate(VERSION, AUTHOR, LICENSE);

--==================================================
-- File Locals
--==================================================
local bOpen = false;

--==============================================================================
-- Global function.
--
-- Opens Pinky portrait dialog.  Runs on 'LoadScreenClose' event.
--
-- @param  none.
--
-- @return  none.
--
function Open()
  local FUNC = "Open";

  -- trace call.
  Trace(FILE, FUNC);

  -- popup on 'Screens' to keep navigation on top.
  if not UIManager:IsInPopupQueue(ContextPtr) then
    local hParam =
      { ["RenderAtCurrentParent"] = true
      , ["InputAtCurrentParent"]  = true
      , ["AlwaysVisibleInQueue"]  = true
      };
    UIManager:QueuePopup(ContextPtr, PopupPriority.Current, hParam);
    ContextPtr:ChangeParent(ContextPtr:LookUpControl("/InGame/Screens"));
    bOpen = true;
    UI.PlaySound("UI_Screen_Open");
  end

  -- fade background to smokey.
  -- from Civ6_styles: FullScreenVignetteConsumer.
  local bHideConsumer = false;
  if HasMod("BrainMod_VI") then
    local oBrain = GetMod("instance", "BrainMod_VI");
    if oBrain:IsOpenInterface() then
      bHideConsumer = true;
    end
  end
  if bHideConsumer then
    Controls.Vignette:SetHide(true);
  else
    Controls.ScreenAnimIn:SetToBeginning();
    Controls.ScreenAnimIn:Play();
  end
end
--==============================================================================
-- Global function.
--
-- Closes both Pinky and Brain portrait dialogs.
--
-- @param  none.
--
-- @return  none.
--
function Close()
  local FUNC = "Close";

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  if UIManager:DequeuePopup(ContextPtr) then
    bOpen = false;
    UI.PlaySound("UI_Screen_Close");
    if HasMod("BrainMod_VI") then
      local oBrain = GetMod("instance", "BrainMod_VI");
      oBrain:CloseInterface();
    end
  end
end
--==============================================================================
-- Global function.
--
-- Handles input from keyboard and mouse.
--
-- @param  hInput  table.
--
-- @return  nil, boolean.
--
function On_Input(hInput)
  local FUNC = "On_Input";

  -- no Trace(), Trace_R(), due to continuous input from mouse.

  -- proceed.
  local tR = nil;
  if not ContextPtr:IsHidden() and hInput:GetMessageType() == KeyEvents.KeyUp
      and hInput:GetKey() == Keys.VK_ESCAPE then
    tR = true;
    Close();
  end

  return tR;
end
--==============================================================================
-- Global function.
--
-- Called by PinkyMod.  Checks if interface is open and returns true or false.
--
-- @param  none.
--
-- @return  boolean.
--
function IsOpen()
  local FUNC = "IsOpen";

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local tR = bOpen;

  -- trace return.
  return Trace_R(FILE, FUNC, nil, tR);
end
--==============================================================================
-- Global function.
--
-- Activates Pinky speech and enables Brain speak button.
--
-- @param  none.
--
-- @return  none.
--
function Speak()
  local FUNC = "Speak";

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  Controls.PinkyButton_Speak:SetDisabled(true);

  -- BrainMod not found.
  if not HasMod("BrainMod_VI") then
    Notice(FILE, FUNC, nil, ">>> Narf! <<<");
    UI.PlaySound("Pinky_Narf");
    Controls.PinkyButton_Speak:SetHide(true);
    Controls.PinkyButton_Close:SetHide(false);
  else

    -- interact.
    Notice(FILE, FUNC, nil
        , ">>> Gee Brain, what do you want to do tonight? <<<");
    UI.PlaySound("Pinky_Tonight");
    local oBrain = GetMod("instance", "BrainMod_VI");
    oBrain:EnableSpeak();
  end
end
--==============================================================================
-- Global function.
--
-- Called by PinkyMod.  Enables close button.
--
-- @param  none.
--
-- @return  none.
--
function EnableClose()
  local FUNC = "EnableClose";

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  Controls.PinkyButton_Speak:SetHide(true);
  Controls.PinkyButton_Close:SetHide(false);
end
--==============================================================================
-- Global function.
--
-- Adds to events and builds Pinky portrait dialog.  Runs on
-- 'LoadGameViewStateDone' event.
--
-- @param  none.
--
-- @return  none-known.
--
function On_LoadGameViewStateDone()
  local FUNC = "On_LoadGameViewStateDone";

  -- trace call.
  Trace(FILE, FUNC);

  -- add to events.
  AddToEvent(Events.LoadScreenClose, On_LoadScreenClose);
 
  -- share functions.
  SetSave("PinkyInterface:Open", Open, "__allowed");
  SetSave("PinkyInterface:IsOpen", IsOpen, "__allowed");
  SetSave("PinkyInterface:EnableClose", EnableClose, "__allowed");

  -- interface.
  ContextPtr:SetInputHandler(On_Input, true);
  Controls.PopupCloseButton:RegisterCallback(Mouse.eLClick, Close);
  Controls.PinkyButton_Close:RegisterCallback(Mouse.eLClick, Close);
  Controls.PinkyButton_Speak:RegisterCallback(Mouse.eLClick, Speak);
  Controls.PopupTitle:SetText("Pinky");

  -- adjust.
  if HasMod("BrainMod_VI") then
    Controls.PinkyContainer:SetOffsetVal(-150, -8);
  end
end
--==============================================================================
-- Global function.
--
-- Runs on 'LoadScreenClose' event.
--
-- @param  none.
--
-- @return  none-known.
--
function On_LoadScreenClose()
  local FUNC = "On_LoadScreenClose";

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  -- do-nothing.
end
--==============================================================================
-- Global function, run-once.
--
-- Called by Initialize() for custom initialization.
--
-- @param  none.
--
-- @return  boolean.
--
-- @see Initialize().
--
function Initialize_Custom()
  local FUNC = "Initialize_Custom";

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  bError = false;

  --==================================================
  -- Custom Initialization
  --==================================================

  -- allows saving functions on "__allowed" persistent on and after
  -- 'LoadGameViewStateDone' event.
  RegisterFunc("Open", Open);
  RegisterFunc("IsOpen", IsOpen);
  RegisterFunc("EnableClose", EnableClose);

  --==================================================

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Global function, run-once.
--
-- Most users will never need to edit this function and should leave it alone.
-- Use Initialize_Custom() for any custom initialization.
--
-- This function must be called before SaveUtils registration-complete.
--
-- Initializes this file.  Automatically runs on include.  Adds Finalize() to
-- finalization sequence.  Instantiates WhysMod class 'g_ocScript' into WhysMod
-- object on 'LoadGameViewStateDone' event.  When first script of mod to load,
-- instantiates WhysMod object 'g_oMod' on 'LoadGameViewStateDone' event.  Calls
-- Initialize_Custom() for custom initialization.
--
-- @param  none.
--
-- @return  boolean.
--
-- @see Finalize().
--
function Initialize()
  local FUNC = "Initialize";
 
  -- CLASS__WhysMod not found.
  if g_cClass == nil then
    error("\n( Uncaught Exception )--------------------------------"
        .."\n"..FILE..":"..FUNC.."(): Unable to initialize file '"..FILE
        .."', required class '"..g_cClass:getClass().."' not found.", 2);
  end

  -- registration-complete check.
  if g_ocScript:isRegComplete() then
    Throw(FILE, FUNC, nil, nil, "Invalid call, registration is complete.");
  end

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC);

  -- proceed.
  local bError = false;

  --print("Initializing custom file '"..FILE.."' for script '"
  --    ..sRegScript.."'...");

  -- adds to finalization sequence.
  g_ocScript:addFinalize(Finalize, sRegScript..":"..FILE..":Finalize");

  -- add to events on event.
  AddToEvent(Events.LoadGameViewStateDone, On_LoadGameViewStateDone);

  -- custom initialize.
  if not Initialize_Custom() then bError = true; end

  -- initialized.
  if bError then
    print("[Warning] Initialized custom file '"..FILE
        .."' for script '"..sRegScript.."' with errors.");
  else
    --print("Initialized custom file '"..FILE
    --    .."' for script '"..sRegScript.."' successfully.");
  end

  -- clear global functions.
  Initialize, Finalize, Initialize_Custom = nil, nil, nil;

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Global function.
--
-- Most users will never need to edit this function and should leave it alone.
--
-- Finalizes any custom data in this file as part of an ordered finalization
-- sequence, upon exit to main menu or restart.
--
-- @param  aExtend    array.
-- @param  bRelated   boolean.
-- @param  bFirstMod  boolean.
-- @param  sIndent    string.
--
-- @return  boolean.
--
-- @see  Initialize().
--
function Finalize(aExtend, bRelated, bFirstMod, sIndent)
  local FUNC = "Finalize";

  -- registered mod & script.
  local sRegMod    = g_ocScript:getRegMod();
  local sRegScript = g_ocScript:getRegScript();

  -- trace call.
  Trace(FILE, FUNC, nil, aExtend, bRelated, bFirstMod, sIndent);

  -- proceed.
  local bError, fFinal = false;

  --print(sIndent.."Finalizing custom file '"..FILE.."' for script '"
  --    ..sRegScript.."'...");

  -- finalize extended.
  if aExtend ~= nil and #aExtend > 0 then
    fFinal = table.remove(aExtend);
    if not fFinal(aExtend, bRelated, bFirstMod, sIndent.."\t") then
      bError = true;
    end
  end

  --==================================================
  -- Custom Finalization
  --==================================================

  -- do-nothing.

  --==================================================

  -- finalized.
  if bError then
    print(sIndent.."[Warning] Finalized custom file '"..FILE
        .."' for script '"..sRegScript.."' with errors.");
  else
    --print(sIndent.."Finalized custom file '"..FILE
    --    .."' for script '"..sRegScript.."' successfully.");
  end

  -- trace return.
  return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Complete.
--

-- clear namespace.
-- none.

-- file included.
--print("Included file '"..FILE.."' version "..VERSION
--    .." by "..AUTHOR.." -"..LICENSE);

-- auto-init.
Initialize();
--==============================================================================

For the second, select Database (Xml) and name it “PinkyInterface_VI.xml”. You should already have an In-Game AddUserInterfaces action that was added for the WhysMod Template. It contains the file GCO_ModInGame.xml. Now add PinkyInterface_VI.xml to the list. Only add the “xml” file. The “lua” file by the same name loads automatically when the xml file is loaded. Return to PinkyInterface_VI.xml and replace the contents with the following code.

Spoiler Code :
Code:
<?xml version="1.0" encoding="utf-8" ?>
<!--=========================================================================-->
<!DOCTYPE Context [
  <!-- Civ6: entities not implemented. -->
  <!ENTITY FILE    "PinkyInterface_VI.xml">
  <!ENTITY VERSION "vymdt.01.2024.10.23.0050">
  <!ENTITY AUTHOR  "Ryan F. Mercer">
  <!ENTITY LICENSE "CC BY-NC-SA 4.0">
]>
<Context Name="PinkyInterface">
  <!--=============================================-->
  <Container ID="Vignette" Style="FullScreenVignetteConsumer"/>
  <Container ID="PinkyContainer" Style="NotificationPopup" Size="276,363"
      Anchor="C,C" Offset="0,-8">

    <Image ID="PinkyPortrait" Size="274,324" Anchor="C,T" Offset="-1,37"
        Texture="Pinky_Portrait"/>
    <GridButton ID="PinkyButton_Speak" Style="MainButton" Size="180,41"
        Anchor="C,B" Offset="-1,-20" String="Speak"/>
    <GridButton ID="PinkyButton_Close" Style="ButtonRed" Size="180,41"
        Anchor="C,B" Offset="-1,-20" String="Close" Hidden="1"/>
  </Container>
  <!--=============================================-->
</Context>
<!--=========================================================================-->

Interface-scripts allow the player to interact with your mod through graphical controls that can respond to input from the keyboard and mouse. Available controls include menus, buttons, lists, and more. The interface xml file defines the layout of the controls, while the lua file by the same name defines their behavior. Think of the xml file as a control panel of buttons, knobs, and switches, and the lua file as the wiring behind it.

Go to your PinkyInterface_VI.xml file. Don’t worry about the entities at the top. They aren’t usable in Civ6 xml but still serve to document the version, author, and license. Within the body of the document are a couple of containers, and within one of the containers is an image and two buttons. Notice the close button is currently set to hidden.

Now go to your PinkyInterface_VI.lua file. This file implements the WhysMod Template, but does so as a related-script of your mod. This is because gameplay-scripts load before interface-scripts, thus your PinkyMod_VI.lua file is treated as your centralized mod-script while PinkyInterface_VI.lua is treated as a component of your mod.

Look at the File Contents and you will see this script contains functions for opening and closing the interface, as well as a function for handling input from the keyboard and mouse. Notice, it also contains a function for enabling the hidden close button seen in the xml.

Navigate to Initialize_Custom(). Under the “Custom Initialization” header you will see three statements that register three functions: Open(), IsOpen(), and EnableClose(). Once registered, functions can be saved, but not until on and after the LoadGameViewStateDone event. Once saved, functions can be called by other scripts and mods. Any functions that need to be saved should be registered in the Initialize_Custom().

Now navigate to On_LoadGameViewStateDone(). You will see three statements that save three functions: Open(), IsOpen(), and EnableClose(). Notice the third argument in each statement, which reads “__allowed”. This is necessary when saving functions, or anything containing functions. Look in the WhysMod “Save” tutorial to learn more.

As mentioned earlier, the interface lua file is like the wiring behind the controls. Here there are statements that decide which function handles the input and which functions are called when the buttons are clicked.

Now go to your PinkyMod_VI.lua file. Again, because it loads first, this is your centralized mod-script and all related-scripts are treated as components of your mod. Look in the file contents and you will see it contains the functions IsOpenInterface() and EnableCloseInterface(). These functions are registered in the Initialize_Custom() and then saved in the On_LoadGameViewStateDone(). This allows other scripts and mods to call them, and their purpose is to call the IsOpen() and EnableClose() functions in the PinkyInterface_VI.lua file.

These functions might seem unnecessary, because PinkyInterface_VI.lua is already sharing its IsOpen() and EnableClose(). But while it is possible for other mods to call these functions on the interface-script directly, there is utility in giving your mod-script authority over any interactions with its components. For example, at some point, you might decide to rename the interface or move the close button to a different script. If another mod was calling the interface directly, then that mod’s code must be updated or the call will fail. But if only your mod-script is ever called directly by other mods, then any internal changes to your mod components won’t impact those mods. This is but one simple example. As your mod increases in complexity, coordinating the behavior between various components can become challenging. Centralizing interactions through your mod-script can reduce confusion and enhance control. For this reason, PinkyInterface_VI.lua does not open the interface on its own. Instead, PinkyMod_VI.lua tells PinkyInterface_VI.lua when to open.

You might be asking yourself why you should care about other mods interacting with your own. The answer is rather simple. Those other mods could be yours.

Now compile the mod, launch FireTuner, and open Civ6. Ensure that PinkyMod_VI is enabled and that it is the only mod enabled. Then start a new game. As soon as the game begins, your interface should open in the center of the screen with a “Speak” button at the bottom.


✅ Congratulations! You’ve added a custom interface.


For now, the interface contents are empty and clicking Speak only outputs “>>> Narf! <<<” to the console. The Speak button is then replaced by the “Close” button. Click it or press the esc key to close the interface.

Continue to the next section to learn how to add a custom texture.

🔼 Top
⏫ Contents
 
Last edited:
Custom Texture

Spoiler :

This section of the tutorial is a continuation of the previous section and demonstrates how to add a custom texture to the interface. The following section then explains how to add custom audio.

The interface is already attempting to display a texture. It appears in the interface xml as the following.

<Image ID="PinkyPortrait" Size="274,324" Anchor="C,T" Offset="-1,37" Texture="Pinky_Portrait"/>

And the Speak button is already attempting to play audio. It appears in the interface lua as the following.

UI.PlaySound("Pinky_Narf");

But currently, those assets are not a part of your mod. Click the link below to download the Pinky_Raw_VI.zip file. Drag the Pinky_Raw_VI folder to your desktop, then delete the zip.

Dropbox: Pinky_Raw_VI.zip

In ModBuddy, add a folder to your project named “Raw” and add the contents of the Pinky_Raw_VI folder to it: Pinky_Narf.wav, Pinky_Portrait.png, Pinky_Tonight.wav. Then delete the folder from your desktop.

These are raw assets. In their current form, they are not usable in your mod. Adding custom textures and audio each require launching their own application for converting them into usable formats. For textures, you’ll need the Asset Editor.

From the ModBuddy menu bar, select Tools → Launch Asset Editor. Then from the Asset Editor menu bar, select File → New. A file-type selection window will open. Select Texture and click OK. A new tab will appear with a list of items and headers beneath it. Under the “Basic” header, click Class Name. This will place your text-cursor in the space to the right. Press the u key and select UserInterface from the dropdown list. Then click Class Name again for the change to take effect. Under the “Source” header, click Source File Path. This will place your text-cursor in the space to the right. To the far right is an ellipsis (triple-dot). Click it and a file-browser window will open. Navigate to your ModBuddy PinkyMod_VI project folder. For this author, the folder path is as follows, but your own path may differ.

buddha.png
  C:\Users\<user>\Documents\Firaxis ModBuddy\Civilization VI\PinkyMod_VI

Within your project folder, you will see a subfolder by the same name. Open the project subfolder and the Raw folder will be visible. Open it and select Pinky_Portrait.png, then click Open. From the menu bar, select File → Save.

Spoiler Animation :
Asset_Editor_ani_01.gif

This creates two files in your project’s Textures folder, but they are not currently visible in your ModBuddy project. Leave Asset Editor open and return to ModBuddy. Add an existing item to your project and navigate to your Textures folder, within your project subfolder. Open it and select the files Pinky_Portrait.dds and Pinky_Portrait.tex, then click Add. The folder and files will now appear in the Solution Explorer.

Return to the Asset Editor. From the menu bar, select File → New, then select XLP and click OK. A new tab will appear with a list of items beneath it. Click XLP Class. This will place your text-cursor in the space to the right. Press the u key and select UITexture from the dropdown list. Click the Entries tab below it and the small icons underneath will become enabled. Hover over them to find the “Add Existing” button and click it. An Asset Browser window will open. To make your texture easier to find, uncheck the “Shared” and “Civ6” checkboxes. Now select Pinky_Portrait from the list and click OK. From the menu bar, select File → Save. This will open the Save As file-browser. Navigate to your XLPs folder, within your project subfolder. Name the file Pinky.xlp and click Save.

Spoiler Animation :
Asset_Editor_ani_02.gif

Now close the Asset Editor and return to ModBuddy. Add an existing item to your project and navigate to your XLPs folder, then select Pinky.xlp and click Add. The folder and file will now appear in the Solution Explorer.

Add another existing item to your project and navigate to your ArtDefs folder. Open it, right-click inside it, then select New → Text Document from the context menu. Rename the file to Pinky.artdef, then select it and click Add. The folder and file will now appear in the Solution Explorer. Open it and paste the following code into the empty file.

Spoiler Code :
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<AssetObjects..ArtDefSet>
  <m_Version>
    <major>1</major>
    <minor>0</minor>
    <build>0</build>
    <revision>0</revision>
  </m_Version>
  <m_TemplateName text="PinkyBLPs"/>
  <m_RootCollections/>
  <m_BLPReferences>
    <Element>
      <xlpFile text="Pinky.xlp"/>
      <blpPackage text="PinkyMod_VI.blp"/>
      <xlpClass text="UITexture"/>
    </Element>
  </m_BLPReferences>
</AssetObjects..ArtDefSet>

This ArtDef file doesn’t really do anything. It serves as a necessary placeholder so that other elements of this process can function properly.

Create an In-Game UpdateArt action. In the Files section, click Add and select “(Mod Art Dependency File)” from the dropdown list, then click OK.

Add an existing item to your project and navigate to your project subfolder. A PinkyMod_VI.civ6proj file will be visible. At the top of your browser window, there is an address bar indicating your current folder path. On the far left side is a folder icon. Click it and the folder path will be converted into highlighted text. Copy it for paste and then click Cancel.

For the next step, you’ll need the ModArt Generator, developed by thecrazyscot. It’s a small utility that automates an otherwise tedious, nuanced, and error prone task. Click the link below to download the ModArt_Generator.zip file. Drag the ModArt_Generator folder to your desktop, then delete the zip.


Open the folder and double-click ModArt_Generator.exe. If Windows complains about an unrecognized app, click “More info” and then click “Run anyway”. A new window will open and it will request the folder path of your project subfolder. Paste the path you copied a moment ago and press the enter key. A small window will open with the words “Completed Successfully”, but it might be hidden behind the main window. This will make all of the necessary edits to the PinkyMod_VI.Art.xml file, with one exception. Click OK to close ModArt Generator, then open PinkyMod_VI.Art.xml. At both the top and bottom of the file, a double-colon appears with a red line under it. Change both of the double-colons to double-periods and save. This correction will remove the red underlines.

Compile your mod and close Civ6 if it is currently open. It needs to restart before the custom texture will take effect. Launch Civ6, ensure that PinkyMod_VI is enabled and is the only mod enabled, then start a new game. Now when the interface opens, it displays the image of Pinky.


✅ Congratulations! You’ve added a custom texture.


Continue to the next section to learn how to add custom audio.

🔼 Top
⏫ Contents
 
Last edited:
Custom Audio

Spoiler :

This section of the tutorial is a continuation of the previous section. It uses the raw assets already provided to demonstrate how to add custom audio to the interface.

Adding audio to Civ6 requires a specific legacy version of Wwise, developed by Audiokinetic. The link below is for the 64-bit Windows platform. Click it to download the Wwise_v2015.9.1_x64.zip file. Drag the Wwise_v2015.9.1_x64 folder to your desktop and open it. It contains three files: Authoring_Data.msi, Authoring_x64.msi, vc2013redist_x64.exe. Double-click each file to run it. If you already have the Visual C++ component installed, then a window will open with the options to “Repair” or “Uninstall”, in which case click Close. These files will install a free trial version of Wwise for noncommerical use only. Delete the folder from your desktop and put the zip file somewhere safe.


Launch Wwise and in the Project Launcher window, click New. The New Project window will open. Name it “Pinky_VI”, click Select None, then click OK. The License Manager window will open. Click Close. In the Wwise menu bar, select Project → Import Audio Files. The Audio File Importer window will open. Click Add Files and navigate to the Raw folder in your ModBuddy project. Select Pinky_Narf.wav and Pinky_Tonight.wav, then click Open. Near the bottom right corner of the window, click Import. In the Project Explorer, click the Audio tab. Under the “Actor-Mixer Hierarchy” node is the “Default Work Unit” node. Click the plus to the left of the node to expand it and you will see Pinky_Narf and Pinky_Tonight nested below it.

Spoiler Animation :
Wwise_ani_01.gif

In the Project Explorer, click the Events tab. Under the “Events” node, select Default Work Unit. This will enable small icons above it. Hover over them to find the “Create New Event” button and click it. A new event will appear highlighted below the Default Work Unit. Name it “Pinky_Narf” and press the enter key. Now click the new event to select it and the Event Editor panel will open on the right. Inside Event Actions, click Browse. The Project Explorer - Browse window will open. Under Actor-Mixer Hierarchy, click the plus to the left of the Default Work Unit and you will see Pinky_Narf nested below it. Select it and click OK. A “Play” action will appear in the list. Now repeat these steps to create another new event in the same Default Work Unit, and name it Pinky_Tonight. Browse again and another Play action will appear, but in its own list.

Spoiler Animation :
Wwise_ani_02.gif

In the Project Explorer, click the SoundBanks tab. Under the “SoundBanks” node, select the Default Work Unit. This will enable small icons above it. Hover over them to find the “Create New SoundBank” button and click it. A new soundbank will appear highlighted below the Default Work Unit. Name it “Pinky_Speech” and press enter. Now click the new soundbank to select it and the SoundBank Editor panel will open on the right. In the Project Explorer, click the Events tab. Now drag and drop both the Pinky_Narf and Pinky_Tonight events into the Hierarchy Inclusion list in the SoundBank Editor. This will add the events to the soundbank. In the Project Explorer, click the SoundBanks tab. Under the “SoundBanks” node, right-click the Default Work Unit and select Generate Soundbank(s) for current platform. The Generating SoundBanks - Completed window will open. Click Close. This creates new files in your Wwise project folder. From the menu bar, select Project → Save. Now close Wwise.

Spoiler Animation :
Wwise_ani_03.gif

Return to ModBuddy. Add a new folder to your project named “Platforms”. To that, add a new folder named “Windows”. And to that, add a new folder named “Audio”.

Platforms\Windows\Audio

Add an existing item to the Audio folder and navigate to your Wwise project folder. For this author, the folder path is as follows, but your own path may differ.

buddha.png
  C:\Users\<user>\Documents\WwiseProjects\Pinky_VI

Within the project folder, navigate to GeneratedSoundBanks\Windows and select the files Pinky_Speech.bnk, Pinky_Speech.txt, and SoundbanksInfo.xml, then click Add. In Solution Explorer, rename SoundbanksInfo.xml to “Pinky_Speech.xml”, then open it and remove the following lines.

Code:
		<SoundBank Id="" Language="SFX">
			<ShortName>Init</ShortName>
			<Path>Init.bnk</Path>
		</SoundBank>

In the Solution Explorer, add a new text file to the Audio folder and name it PinkyBanks.ini. Open it and paste the following code into the empty file.

Spoiler Code :
Code:
;
; Civilization VI
;
; Global: banks always loaded.
; Menu: banks loaded during main menu only.
; InGame: banks loaded during gameplay only, both 2D and 3D.
; 2D: banks loaded during 2D gameplay only.
; 3D: banks loaded during 3D gameplay only.
; FMV: banks loaded during full-motion video only.
;
; Retain a completely empty line under each list or header, including FMV.
;

[Global]

[Menu]

[InGame]
Pinky_Speech.bnk

[2D]

[3D]

[FMV]

Make certain that there is a completely empty line at the bottom of the file, or Civ6 will crash. You will need to add it due to this forum not displaying empty lines at the end of code blocks. Then create an In-Game UpdateAudio action and add the four audio files:

Platforms/Windows/Audio/Pinky_Speech.bnk,​
Platforms/Windows/Audio/Pinky_Speech.txt,​
Platforms/Windows/Audio/Pinky_Speech.xml,​
Platforms/Windows/Audio/PinkyBanks.ini.​

Compile your mod and close Civ6 if it is currently open. It needs to restart before the custom audio will take effect. Launch Civ6, ensure that PinkyMod_VI is enabled and is the only mod enabled. Also make sure your audio is not muted, then start a new game. Now when the Speak button is clicked, it plays the voice of Pinky.


✅ Congratulations! You’ve added custom audio.


This author would like to end this portion of the tutorial by thanking the people at Audiokinetic for their tireless commitment to the modding community. Their website contains additional resources that modders may find useful, for Civ6 as well as many other titles.


Visit their website and create a free account to explore the community. Once logged in, select Community → Creators Directory → Schools. In the search box, type “Wwise Modding”. Scroll down and in the search results you will see Wwise Modding Projects. Click it to see the available projects and in the list you will find Civilization VI Modding. It includes download and install instructions, and below that is a button to Request Access.

Click to join!

Once approved, this will allow you to build soundbanks with unlimited assets.

Spoiler Animation :
AudioKinetic_ani_02.gif

Continue to the next section to learn how to replace the Load Screen and interrupt the audio.

🔼 Top
⏫ Contents
 
Last edited:
Custom Load Screen

Spoiler :

This section of the tutorial is a continuation of the previous section and demonstrates how to replace the base game Load Screen, as well as how to interrupt audio before it has finished playing. This will complete the Pinky & The Brain demonstrator mods and allow them to perform as a team.

In ModBuddy, start a new project named “BrainMod_VI” and add the WhysMod support files. Click the link below to download the remaining project files and add them to your mod. The assets have already been converted into usable formats, but you still need to create the necessary In-Game actions.


Now compile the mod and clear the FireTuner output. In Civ, disable PinkyMod_VI and enable BrainMod_VI. Make sure it’s the only mod enabled, then start a new game. As soon as the game begins, an interface should open in the center of the screen with a “Speak” button at the bottom. The interface displays an image of Brain and when the Speak button is clicked, it plays Brian's voice.

Before loading both mods simultaneously, let’s add one additional element to BrainMod_VI. So far in this tutorial, you’ve only added new content that doesn’t normally exist in Civ. Whether it’s a lua file, xml, a texture, or audio, they’re all additions to the base game. However, much of the existing content can also be modified. It is one of the primary means of modding Civ and many mods require it. Fortunately, replacing existing lua and xml files is a remarkably straightforward process. You need only to include a file by the same name in your project.

To start, locate the base game files in your installed Civ folder. For this author, the folder path is as follows, but your own path may differ.

buddha.png
  C:\Program Files (x86)\Steam\steamapps\common\Sid Meier's Civilization VI\Base

Within it, an Assets folder will be visible, and within that are folders containing the lua and xml files that are available for replacement. You can explore them to learn more about how Civ is programmed, but it’s a lot of content, and at first sight it may appear intimidating. The best way to know what you’re looking for is by studying other mods that do something similar to what you want to do. For this portion of the tutorial, only the UI/FrontEnd/LoadScreen.lua file needs to be replaced.

In ModBuddy, add a new folder to your project and name it “UI”. This folder isn’t absolutely necessary, but it helps to keep your project organized. Select the new folder and add an existing item, then navigate to the LoadScreen.lua file in the base game assets. Select it to add a copy to your project.

This is a FrontEnd script rather than an In-Game script, which means it loads before you reach the Main Menu. Create a FrontEnd ImportFiles action and add your LoadScreen.lua file to the list. This replaces the base file with your own version, but currently it isn’t doing anything different. Open it and do a find for “Play_DawnOfMan_Speech”. This will take you to the following line of code.

UI.PlaySound("Play_DawnOfMan_Speech");

Replace it with the following.

Spoiler Code :
Code:
      -- find Pinky and Brain.
      local bHasPinky, bHasBrain;
      if Modding ~= nil then
        local a = Modding.GetInstalledMods();
        if a ~= nil then
          for i = #a, 1, -1 do
            if Locale.Lookup(a[i]["Name"]) == "PinkyMod_VI" then
              bHasPinky = a[i]["Enabled"];
            elseif Locale.Lookup(a[i]["Name"]) == "BrainMod_VI" then
              bHasBrain = a[i]["Enabled"];
            end
            if bHasPinky ~= nil and bHasBrain ~= nil then break; end
          end
        end
      end
      if bHasPinky and bHasBrain then
        UI.PlaySound("Pinky_And_Brain");
      else
        UI.PlaySound("Play_DawnOfMan_Speech");
      end

Compile your mod and make sure both PinkyMod_VI and BrainMod_VI are enabled, but don’t start a new game just yet. When enabling a mod with a FrontEnd script, or adding a FrontEnd script to an enabled mod after reaching the Main Menu, the FrontEnd scripts from the base game will have already loaded. Thus it is necessary to exit and relaunch Civ before the modified LoadScreen.lua file can take effect. Note, the reverse is also true when disabling a mod with a FrontEnd script.

Now relaunch Civ, make sure your sound is not muted, and start a new game. The load screen will play your custom audio. As soon as the game begins, both the Pinky and Brain interfaces will open and their Speak buttons will result in a unique interaction.


✅ Congratulations! You’ve added a custom load screen and implemented interaction between two mods.


If your game loads quickly enough, you might notice that the load screen music can overlap the speech of Pinky and Brain. Normally, the base game stops the Dawn of Man speech when the game begins, but currently your custom music keeps playing until the audio completes. This is also true when you click the Speak buttons too quickly, causing Pinky and Brain to speak over each other. Because Brain’s assets were converted for you, correcting this issue requires the Wwise project folder that was used to convert them. Click the link below to download the Brain_Wwise_VI.zip file.

Dropbox: Brain_Wwise_VI.zip

Open it and drag the Brain_VI folder into your Wwise projects folder. For this author, the folder path is as follows, but your own path may differ.

buddha.png
  C:\Users\<user>\Documents\WwiseProjects

Open Wwise and click the “Open Other” button in the Project Launcher, or if Wwise is already open, select Project → Open from the menu bar. Navigate to the Brain_VI folder you extracted to the Wwise projects folder. Open it and select the Brain_VI.wproj file. This will load the project that was used to convert the custom audio for Brain.

In the Project Explorer, click the Events tab if it is not already selected. Within the Events → Default Work Unit node, you will see the “Pinky_And_Brain” event. Right-click it, select Rename, and name it “Play_Pinky_And_Brain”. Then right-click the Default Work Unit above and select New Child → Stop → Stop. Name the new event “Stop_Pinky_And_Brain”, then select it to open the Event Editor on the right. Click Browse and select Actor-Mixer Hierarchy → Default Work Unit → Pinky_And_Brain. This will create a stop event that can be used to interrupt the play event, but first it needs to be added to the soundbank. In the Project Explorer, click the SoundBanks tab. Then select SoundBanks → Default Work Unit → Brain_Speech to open the SoundBank Editor on the right. Select “\Events\Default Work Unit\Pinky_And_Brain” in the list and click the Remove button. In the Project Explorer, click the Events tab, then drag both the Play event and the Stop event into the list.

Before saving your project, let’s make one more change. The Brain_Ponder.wav file is a little quiet. Return to the Audio tab and select Actor-Mixer Hierarchy → Default Work Unit → Brain_Ponder. This will open the Sound Property Editor on the right. Slide the “Voice” control up until it reaches volume 6.

Now save your project and return to the SoundBanks tab. Right-click the Default Work Unit and select Generate Soundbank(s) for current platform. In Windows, navigate to the Brain_VI\GeneratedSoundBanks\Windows folder in your Wwise projects folder. Copy the Brain_Speech.bnk, Brain_Speech.txt, and SoundbanksInfo.xml files to your Modbuddy BrainMod_VI\Platforms\Windows\Audio folder, and replace the existing files. Delete the Brain_Speech.xml file and rename SoundbanksInfo.xml to “Brain_Speech.xml”, then open it and remove the following lines. Don’t worry about the “ini” file. It’s fine the way it is.

Code:
        <SoundBank Id="" Language="SFX">
            <ShortName>Init</ShortName>
            <Path>Init.bnk</Path>
        </SoundBank>

Return to LoadScreen.lua and find the following line of code.

UI.PlaySound("Pinky_And_Brain");

Replace it with the following.

Code:
UI.PlaySound("Play_Pinky_And_Brain");

Then open BrainInterface_VI.lua and navigate to the “On_LoadScreenClose“ method. Add the following line of code at the bottom.

Code:
UI.PlaySound("Stop_Pinky_And_Brain");


✅ Congratulations! You’ve interrupted audio.


Ideally, the LoadScreen.lua replacement file would also implement the WhysMod Template, so that it too can function as a part of the team. Unfortunately, this is not currently an option. Attempting to implement the WhysMod Template in a FrontEnd script will cause significant issues, but this will likely be addressed in a subsequent build.

This concludes this portion of the tutorial and the Pinky & The Brain demonstrator mods. For practice, make the same changes to the other audio files in both mods, then add the necessary code to interrupt their speech when the other one is speaking. Also, you might have noticed that in addition to Play and Stop, Wwise includes Pause and Resume. These event actions can be implemented in the same way. Experiment and have fun!


🔼 Top
⏫ Contents
 
Last edited:
About The Author
Spoiler :

I am a Civ modding enthusiast with no special relationship with Firaxis, Audiokinetic, or any other associated entity. I began developing the WhysMod support files on about January 1st 2023. All of the programming is my own work and was built from scratch. I developed it to improve and enhance the Civ Lua modding environment and make it more user friendly. It was designed organically from first principles and is scalable to the user’s proficiency. I have made it freely available for noncommerical use in hopes of widespread adoption. This will lead to better mods, better modders, and a more active modding community.

Development of the WhysMod files required nearly 2 years to complete. It was made possible by my many years of prior programming experience. If you would like to show your support, I have a Venmo account and will give credit to all contributors. If you would prefer to remain anonymous or use an alternate pseudonym, then let me know in Venmo.

Just search for ”Ryan_F_Mercer” on Venmo.

Contributors:

Carmen Mercer – $1​

Special Thanks:

Sailor Cat – for their Icons and Leader Images tutorial, explaining how to use Asset Editor to add custom textures.​
thecrazyscot – for the ModArt Generator utility, automating an otherwise tedious, nuanced, and error-prone task.​
Ewok-Bacon-Bits – for their "quick rough" tutorial explaining how to use Wwise to add custom audio.​
FurionHuang – for their "Adding Music to Your MOD Civilization" tutorial explaining how to use Wwise to stop, pause, and resume audio.​

🔼 Top
⏫ Contents
 
Last edited:
Technical

Spoiler :

This author uses a version ID for their work. The ID is composed not only of the version number, but also the build date, and is preceded by a format indicator. The version number is always a whole number and only increments when a build is no longer backwards compatible with the previous build. Example below.

VersionID.png


Spoiler Change Log :
None.

Spoiler Deprecated :
None.

🔼 Top
⏫ Contents
 
Last edited:
Top Bottom