WhysMod Template VI

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

Pro-Solve XTreme for Mod

This tutorial explains how to:
  • Install the modding tools.
  • Create user interfaces.
  • Add custom textures.
  • Add custom audio.
  • Save any kind of data.
  • Make interoperable mods.
  • And more.
The true beauty of modding is that you never need 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.

The WhysMod Template simplifies and enhances the Civ Lua modding environment, provides lightweight debugging tools, and allows seamless interoperability of mods. It has both a basic and advanced implementation available. Full tutorials are provided to demonstrate integration of the WhysMod support files into a new mod. This includes two demonstrator mods – Pinky & The Brain – which demonstrate both the basic and advanced implementations, as well as mod interaction between the two. Additionally, they demonstrate how to add a custom interface, texture, audio, and replacement load screen, to your mod.

To learn more, explore the Table of Contents below. The blue arrows are clickable buttons for navigating this tutorial. If you have little or no experience with coding, or are simply unfamiliar with Lua, then consider looking in the For Dummies section first. Otherwise, for a quick start, go to the New Mod section.
 
Last edited:
Table of Contents


Features
🔽 Save & Share
🔽 Inspect & Copy
🔽 Extended Types
🔽 Output Controls
🔽 Function Tracing
🔽 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

Miscellaneous
🔽 About The Author
🔽 Technical
🔽 Troubleshooting
 
Last edited:
Save & Share

Spoiler :

The WhysMod Template allows users to easily save any data, and that data is then automatically restored across saved games. This includes Units, Players, Plots, Cities, and events, plus custom tables of any complexity. Saved tables can contain any data, including shared and cyclic references, and allows references for table keys, including custom tables. Not only are references restored, but any reference relationships are also maintained. The advanced features allow saving functions, metatables, objects, and even classes. All saved data is then accessible from all scripts of all mods.

Any Data:
Code:
SetSave("boolean", true);
SetSave("number", 2);
SetSave("string", "text");
SetSave("table", {["key"] = "value"});
SetSave("Unit", pUnit);
SetSave("Player", pPlayer);
SetSave("Plot", pPlot);
SetSave("City", pCity);
SetSave("event", eEvent);

<<... save & load game. ...>>

From Any Script:
Code:
-- from your own mod.
local sString = GetSave("string");
local hTable = GetSave("table");

-- from some other mod.
local sString = GetSave("string", "OtherMod");
local hTable = GetSave("table", "OtherMod");

References:
Code:
local aKey = {"key"};
local aValue = {"value"};
local hTable = {[aKey] = aValue};
SetSave("key", aKey);
SetSave("value", aValue);
SetSave("table", hTable);

<<... save & load game. ...>>

Relationships:
Code:
local aKey = GetSave("key");
local aValue = GetSave("value");
local hTable = GetSave("table");
local bEqual = (hTable[aKey] == aValue); -- true.

Shared & Cyclic:
Code:
local aShared = {"shared"};
local aArray = {aShared , aShared};
table.insert(aArray, aArray); -- add cyclic.
SetSave("array", aArray);

<<... save & load game. ...>>

Code:
local aArray = GetSave("array");
aArray = table.remove(aArray); -- remove cyclic.
local aShared = table.remove(aArray);

To save functions, they must first be registered, then they can be saved on and after the LoadGameViewStateDone event. The LoadGameViewStateDone event occurs prior to the LoadScreenClose event.

Function Saving:
Code:
function Demo()
	print("Hello World.");
end

RegisterFunc("Demo", Demo);

function On_LoadGameViewStateDone()
	SetSave("Demo", Demo, "__allowed"); -- allows functions.
end

-- occurs prior to LoadScreenClose event.
AddToEvent(Events.LoadGameViewStateDone, On_LoadGameViewStateDone);

Function Loading:
Code:
function On_LoadScreenClose()
	local fDemo = GetSave("Demo");
	fDemo();
end

-- occurs after LoadGameViewStateDone event.
AddToEvent(Events.LoadScreenClose, On_LoadScreenClose);

Output:
Code:
Hello World.

SetSave() has additional parameters not shown here. There are also additional functions for handling saved data, such as HasSave(), DltSave(), and RegisterClass(). To learn more, look in the "Save" tutorial that exists as part of the WhysMod support files. It can be output to the console for a detailed description of the full functionality.


🔼 Top
⏫ Contents
 
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.
local 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:
Extended Types
Spoiler :

The WhysMod Template allows users to easily identify both native and extended data types. In Lua, there are only 8 native types: nil, boolean, number, string, table, function, userdata, and thread. The WhysMod Template extends these types to include: integer, float, array, pack, hash, class, object, pointer, and event. However, Lua's built-in type() function can not recognize these extended types, thus additional functions have been provided.

Type_() & SubType():
Code:
print(Type_(vData));
print(SubType(vData));

In addition to the native types, Type_() allows users to detect a class, object, pointer, or event. Whereas SubType() allows users to differentiate an integer or float from a number, or an array, pack, or hash, from a table, or give the name of the class, object, or pointer. Classes and objects are an implementation of the WhysMod Template and can have any valid name. The pointers have been designed by Firaxis and include: Unit, Player, Plot, and City.

Type_() Outputs:
Code:
"nil", "boolean", "number", "string", "table", "function", "class", "object",
"userdata", "pointer", "event", "thread".

Note, unlike types, some subtypes can change when the value of the data changes. For example, a float can become an integer after a mathematical operation, or an array becomes a hash when given a string key.

SubType() Outputs:
Code:
"nil", "boolean", "integer", "float", "string", "array", "pack", "hash",
"function", class name (ie: "Exception"), object name (ie: "Exception"),
"userdata", "Unit", "Player", "Plot", "City", "event", "thread".

These functions can be used in combination to differentiate a class, object, or pointer with a shared name.

Differentiate Named:
Code:
if Type_(vData) == "class"  and SubType(vData) == "Exception" then …
if Type_(vData) == "object" and SubType(vData) == "Exception" then …

Additional data typing functions are available if needed, such as IsArray(), IsPack(), IsReference(), and IsException(). There are also associated utility functions, such as Pack() and UnPack(), which transform a sequence of values into a pack and back. To learn more, look in the "Type" tutorial that exists as part of the WhysMod support files. It can be output to the console for a detailed description of the full functionality.


🔼 Top
⏫ Contents
 
Last edited:
Output Controls
Spoiler :

The WhysMod Template allows users to easily control notice, warning, and error messages that are output to the console. Both for your own mod as well as any others. Notices and warnings can be disabled for a particular function, file, an entire script, or even an entire mod. Or a traceback can be requested along with any messages.

Notice() & Warning():
Code:
function Demo()
	Notice( "File.lua", "Demo", nil, "This is a notice." );
	Warning("File.lua", "Demo", nil, "This is a warning.");
end

Demo();

Demo() Output:
Code:
File.lua:Demo(): This is a notice.
File.lua:Demo(): [Warning] This is a warning.

Scripts can include multiple files, thus both a script name and a file name can be specified in the output settings. If no script name is given, then the setting applies to all scripts of the mod.

Disable Notice & Enable Traceback:
Code:
-- for all scripts of your own mod.
SetOutput("notice", "state", false, nil, nil, "File.lua", "Demo");
SetOutput("warning", "traceback", true, nil, nil, "File.lua", "Demo");

-- for some other mod's script.
SetOutput("notice", "state", false, "OtherMod", "Script.lua", "File.lua", "Demo");
SetOutput("warning", "traceback", true, "OtherMod", "Script.lua", "File.lua", "Demo");

Demo() Output:
Code:
File.lua:Demo(): [Warning] This is a warning.
stack traceback:
	C:\...\File.lua:6: in function '(main chunk)'

Output settings are based on inheritance, beginning with the default value for the script. All files of the script inherit the default value, unless given their own value. All functions inherit their file's own or inherited value, unless given their own value. Thus disabling notices by file name only, disables any notices for all functions in that file, unless individual functions are enabled by name.

Output_01.png


Inherited Settings:
Code:
-- your own mod.
SetOutput("notice", "state", false, nil, "Script.lua", true);               -- disable notices for all files.
SetOutput("notice", "state", true,  nil, "Script.lua", "File.lua");         -- except this file.
SetOutput("notice", "state", false, nil, "Script.lua", "File.lua", "Demo"); -- but disable this function.

The order of the settings does not matter. They do not overwrite or reset each other.

Spoiler Animation :
Output_ani_01.gif


Reversed:
Output_ani_02.gif

Error messages can not be disabled, but a traceback can still be enabled. Also, Error() provides inline handling of return data for immediate return from a function.

Error Return:
Code:
function Demo()
	if ... then
		return Error("File.lua", "Demo", nil, false, "Returns false.");
	elseif ... then
		return Error("File.lua", "Demo", nil, {"table"}, "Returns table.");
	end
	return true;
end

local tCheck = Demo();

Both Warning() and Error() can be given a previously caught exception to be added to the output. The exception is given as a named parameter, and the exception output includes its own traceback.

Previous Exception:
Code:
Error("File.lua", "Demo", {["prev"] = xException}, nil, "This is an error.");

Error Output:
Code:
File.lua:Demo(): [ ERROR ] This is an error.
( Exception )--------------------------------
[ #1 ] File.lua:ThrowException(): This is an exception.
stack traceback:
	C:\...\File.lua:2: in function 'ThrowException'
	C:\...\File.lua:42: in function 'Demo'
--------------------------------

Notice(), Warning(), and Error() have additional parameters not shown here. There are also additional functions for the output settings, such as GetOutput(), HasOutput(), and WilOutput(). To learn more, look in the "Output" tutorial that exists as part of the WhysMod support files. It can be output to the console for a detailed description of the full functionality.


🔼 Top
⏫ Contents
 
Last edited:
Function Tracing
Spoiler :

The WhysMod Template allows users to easily trace function execution, for both calls and returns. Tracing can be enabled for an entire mod or script, a single file or function, or even individual parameters and returns. Both for your own mod as well as any others. Additionally, parameters and returns can be individually formatted with any of the ToString() arguments.

Trace() & Trace_R():
Code:
-- assumed enabled.
function Demo(pram1, pram2)
	Trace( "File.lua", "Demo", nil, pram1, pram2 );
	local rtrn1, rtrn2 = "three", {"four"};
	return Trace_R("File.lua", "Demo", nil, rtrn1, rtrn2);
end

local t1, t2 = Demo("one", 2);

Demo() Output:
Code:
File.lua:Demo([1]-> (3)"one", [2]-> 2);
File.lua:Demo->R([-1]-> (5)"three", [-2]-> array (table): 000000006C0DCBB0);

Trace() and Trace_R() use the same settings functions as Notice(), Warning(), and Error(), but can also include a parameter/return index argument. Parameters increment from 1 and returns decrement from -1. For formatting, the ToString() arguments must be given as a pack.

Enable Demo & Format Return:
Code:
-- for all scripts of your own mod.
SetOutput("trace", "state", true, nil, nil, "File.lua", "Demo");
SetOutput("trace", "tostring", Pack(1), nil, nil, "File.lua", "Demo", -2); -- second return, depth 1.

-- for some other mod's script.
SetOutput("trace", "state", true, "OtherMod", "Script.lua", "File.lua", "Demo");
SetOutput("trace", "tostring", Pack(1), "OtherMod", "Script.lua", "File.lua", "Demo", -2);

Demo() Output:
Code:
File.lua:Demo([1]-> (3)"one", [2]-> 2);
File.lua:Demo->R
	( [-1]-> (5)"three"
	, [-2]-> array (table): 000000006C0DCBB0
		{ [ 1 ] = "four"
		}
	);

The trace settings are based on inheritance and the parameter/return value comes last in the chain.

Trace_01.png


Inherited Settings:
Code:
-- your own mod.
SetOutput("trace", "state", true,  nil, "Script.lua", true);                  -- enable tracing for all files.
SetOutput("trace", "state", false, nil, "Script.lua", "File.lua");            -- except this file.
SetOutput("trace", "state", true,  nil, "Script.lua", "File.lua", "Demo");    -- but enable this function.
SetOutput("trace", "state", false, nil, "Script.lua", "File.lua", "Demo", 1); -- but not the first parameter.

The order of the settings does not matter. They do not overwrite or reset each other.

Spoiler Animation :
Trace_ani_01.gif


Reversed:
Trace_ani_02.gif

By default, the trace output is printed on a single line for each function call and return. If a parameter or return uses indented formatting, then the trace output is also indented. Additionally, the output is indented when the width exceeds the width threshold value. By default, the width threshold is set to 80, but it can also be adjusted. The set value applies to all scripts of all mods.

Width Threshold:
Code:
SetOutput("trace", "widthThreshold", 0); -- always indented.

Demo() Output:
Code:
File.lua:Demo
	( [1]-> (3)"one"
	, [2]-> 2
	);
File.lua:Demo->R
	( [-1]-> (5)"three"
	, [-2]-> array (table): 000000006C0DCBB0
		{ [ 1 ] = "four"
		}
	);

Trace() and Trace_R() have additional parameters not shown here. There are also additional functions for the output settings, such as GetOutput(), HasOutput(), and WilOutput(). To learn more, look in the "Trace" tutorial that exists as part of the WhysMod support files. It can be output to the console for a detailed description of the full functionality.


🔼 Top
⏫ Contents
 
Last edited:
Exception Handling
Spoiler :

The WhysMod Template allows users to easily throw and catch exceptions for object based exception handling. It can handle both user thrown and system thrown exceptions.

Throw():
Code:
function Demo()
	Throw("File.lua", "Demo", nil, nil, "This is an exception.");
end

Demo(); -- uncaught.

Output:
Code:
Runtime Error: C:\...\File.lua:5:
( Uncaught Exception )--------------------------------
[ #1 ] File.lua:Demo(): This is an exception.
stack traceback:
	C:\...\File.lua:2: in function 'Demo'
	C:\...\File.lua:5: in function '(main chunk)'
	[C]: in function '(anonymous)'

Catch():
Code:
function Demo()
	Throw("File.lua", "Demo", nil, nil, "This is an exception.");
end

local x = Catch(nil, Demo); -- caught.
print(x:GetMessage());

Output:
Code:
( Exception )--------------------------------
[ #1 ] File.lua:Demo(): This is an exception.
stack traceback:
	C:\...\File.lua:2: in function 'Demo'
	C:\...\File.lua:5: in function '(main chunk)'
--------------------------------

When the given function does not throw, Catch() returns nil for the exception, followed by any function returns.

Function Returns:
Code:
local x, t1, t2 ... = Catch(nil, Demo);

Throw() can include any data to be retrieved when caught. The included data is given as a named parameter.

Throw Data:
Code:
Throw("File.lua", "Demo", {["data"] = vData}, nil, "This is an exception");

Catch Data:
Code:
local x = Catch(nil, Demo);
local vData = x:GetData();

Catch() allows arguments to be passed to the given function. If the function uses implicit self, then the function container reference must be given first.

Arguments & Implicit Self
Code:
Catch(nil, Demo, arg1, arg2 ...);
Catch(nil, object.Demo, object, arg1, arg2 ...); -- uses implicit self.

Throw() and Catch() have additional parameters not shown here. For example, Throw() can be given a previous exception to be nested and rethrown as part of a chronological chain. And Catch() can group exceptions as they are caught. This can be useful when catching exceptions thrown from a loop before rethrowing the chain. To learn more, look in the "Throw" tutorial that exists as part of the WhysMod support files. It can be output to the console for a detailed description of the full functionality.


🔼 Top
⏫ Contents
 
Last edited:
For Dummies
Spoiler :

This section is not required to complete this tutorial. It is provided for improved comprehension of the material only.

Overview

Civ6 modding primarily consists of replacing or adding XML, SQL, and Lua files. The XML is mostly used to update Civ's database, allowing the modder to edit existing values, such as adjusting a unit's movement or cost. It can also be used to add completely new items, such as a unit or building, or remove existing items from the game. Optionally, SQL files can be used instead to perform the same tasks, but with greater power and flexibility. Any such database files are loaded first when you start a new game. Once loaded, the database is set and can not be changed.

The Lua files load next, and allow the modder to alter the game's logic or introduce new behavior. This is done by either tweaking functions in existing files, or by writing new functions that are then attached to events. When an event is triggered, such as when selecting a city or unit, or when a player's turn begins, any functions attached to that event are executed.

Additionally, a second set of XML and Lua files, known as interface files, load last, just before the "Begin Game" button appears on the load screen. These allow the modder to make changes to existing game interfaces, such as altering the layout of menus and buttons, or create entirely new interfaces for managing new behavior.

Multiple individuals have attempted to compile a list of all available events and built-in functions, however most are incomplete, and some items may only be available for specific Civ6 expansions.

➡️ Sukritact's Knowledge Base


Beginners

➡️ How to Code
➡️ Introduction to XML


Programming in Lua

➡️ Online Compiler
Spoiler Reference Guide :

Types and Values
➡️ Nil
➡️ Booleans
➡️ Numbers
➡️ Strings
➡️ Tables
➡️ Functions

Expressions
➡️ Arithmetic Operators
➡️ Relational Operators
➡️ Logical Operators
➡️ Concatenation
➡️ Precedence
➡️ Table Constructors

Statements
➡️ Assignment
➡️ Local Variables and Blocks

Control Structures
➡️ if then else
➡️ while
➡️ Numeric for
➡️ Generic for

➡️ break and return

Functions
➡️ Multiple Results
➡️ Named Arguments

More about Functions
➡️ Closures
➡️ Non-Global Functions

Terminology
Spoiler :

Multiple Assignment: more than one assignment in a single statement.
Code:
local s1, s2, s3 = "one", "two", "three";

Multiple Return: more than one value in a single return.
Code:
function Demo
	return "one", "two", "three";
end

local s1, s2, s3 = Demo();

Integer: number without a decimal.
Code:
local i1, i2, i3 = 1, 2, 3;

Float: number with a decimal.
Code:
local n1, n2, n3 = 1.1, 1.2, 1.3;

Argument: data passed to a function call.
Code:
Run("one", "two"); -- arguments one and two.

Parameter: data received by a function definition.
Code:
function Run(sOne, sTwo) ... end -- parameters one and two.

Method: function attached to a class or object.
Code:
local object = {};
function object.Example() ... end -- method definition.
object.Example();                 -- method call.

Implicit Self: method that uses the colon operator to pass the object reference.
Code:
local object = {["message"] = "Hello  Self."};

function object:Example()   -- colon instead of dot.
	print(self["message"]); -- automatic 'self' parameter.
end

object:Example(); -- colon provides 'self' argument to method.

Array: table with positive integer indexes only, starting from 1 with no gaps.
Code:
local aArray = {[1] = "one", [2] = "two", [3] = "three", [4] = "four", [5] = "five"};

Pack: table with string key "n" and positive integer indexes only, starting from 1 with gaps representing nil values, and "n" for the total number of values, indicating any trailing nils.
Code:
local kPack = {[2] = "two", [4] = "four", ["n"] = 5}; -- 1, 3, and 5 are nil.

Hash: table with any other assortment of keys.
Code:
local hHash = {[*] = "value"};

Up Value: local variable declared before and in the same scope as a function definition, and thus is available to that function.
Code:
local sUp = "value";

function UseUpValue()
	print(sUp);
end

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

From the ModBuddy menu bar, select Tools → Options, and an Options window will open with a list of menu items on the left. Double-click "Text Editor" in the list to reveal additional items below it. Then double-click "All Languages" to reveal additional items below that. On the right, check the box for "Line numbers". Then select "Scroll Bars" from the list on the left. Under the "Behavior" header on the right, click the "Use map mod for vertical scroll bar", then click OK. This will make your project files easier to navigate. You may also want to close the "Properties" interface on the right side of the ModBuddy window.

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.

⬇️ 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". Click OK, 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_cScr, 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_oMod, g_oScr, g_ocScript.
g_ocScript:Instantiate(WHYSMOD__SCRIPT_VERSION, WHYSMOD__SCRIPT_AUTHOR
		, WHYSMOD__SCRIPT_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, which is a necessary step before accessing many of the available 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! <<<\n ");

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

To use FireTuner, it must first be enabled. In Windows File Explorer, navigate to the AppOptions.txt file and double-click to open it. For this author, the file path is as follows, but your own path may differ.

Find your own path.
  C:\Users\<user>\AppData\Local\Firaxis Games\Sid Meier's Civilization VI\AppOptions.txt

⚠️ AppData is a hidden folder. If it is not visible, then from the File Explorer menu bar, select ViewShowHidden items.​

Within the file, search for "EnableTuner" and change the 0 to a 1, then save.

EnableTuner 1

Next, from the ModBuddy 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 support files to your project – explained previously in the New Mod section – before proceeding.

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

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

How To Use
include("WhysMod_VI.lua"
File Locals

function On_LoadGameViewStateDone(
function On_LoadScreenClose(

function Initialize_Custom(
function Initialize(
function Finalize(

--==================================================
-- How To Use
--==================================================
Tutorial: https://forums.civfanatics.com/threads/whysmod-template-vi.693004/

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_cScr, 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_oMod, g_oScr, g_ocScript.
g_ocScript:Instantiate(WHYSMOD__SCRIPT_VERSION, WHYSMOD__SCRIPT_AUTHOR
		, WHYSMOD__SCRIPT_LICENSE, WHYSMOD__MOD_ITERATION, WHYSMOD__MOD_AUTHOR
		, WHYSMOD__MOD_CREDIT);

--==================================================
-- File Locals (members)
--==================================================
-- 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! <<<\n ");
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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local bError = false;

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

	-- do-nothing.

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

	-- trace return.
	return g_oWhys: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_cScr == nil then
		error("\n( Uncaught Exception )--------------------------------"
				.."\n"..FILE..":"..FUNC.."(): Unable to initialize file '"..FILE
				.."', required class '"..g_cScr:getClass()
				.."' not found.", 2);
	end

	-- registration-complete check.
	if g_ocScript:isRegComplete() then
		g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local bError = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, "Initializing custom file '"..FILE.."' for script '"
	--		..sRegScript.."'...");

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

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

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

	-- initialized.
	if bError then
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, "Initialized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, "Initialized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

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

	-- trace return.
	return g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC, nil, aExtend, bRelated, bFirstMod, sIndent);

	-- proceed.
	local bError, fFinal = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, 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
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, sIndent.."Finalized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, sIndent.."Finalized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

	-- trace return.
	return g_oWhys: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 :

More Restrictive:
Code:
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:
Code:
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:
Code:
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:
Code:
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.
➡️ Creative Commons License Wizard

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 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 following code.

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
		local 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". 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);

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 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 following code.

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 following code, 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 following code, 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! <<<

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


✅ Congratulations! You’ve implemented the Basic Template.


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 Basic 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_cScr, 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_oMod, g_oScr, g_ocScript.
g_ocScript:Instantiate(WHYSMOD__SCRIPT_VERSION, WHYSMOD__SCRIPT_AUTHOR
		, WHYSMOD__SCRIPT_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_cScr, 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_oMod, g_oScr, g_ocScript.
g_ocScript:Instantiate(WHYSMOD__SCRIPT_VERSION, WHYSMOD__SCRIPT_AUTHOR
		, WHYSMOD__SCRIPT_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 Custom Interface 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 tutorial sections below, beginning with the Custom Interface.


🔼 Top
⏫ Contents
 
Last edited:
Advanced Template
Spoiler :

If you got here from the previous section, it is recommended that you continue using the same MyMod_VI project. Delete the contents of your MyMod_VI.lua file before proceeding. If you’ve done this before and are returning to it, then don't forget to add the WhysMod support files to your project.

Let’s begin. Paste the following code to your MyMod_VI.lua file and go to the top.

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

How To Use
include("WhysMod_VI.lua"
File Locals

CLASS__WhysMod:extend("<undefined_class>"
CLASS__<undefined_class>.hPublic:new(
CLASS__<undefined_class>.hPublic:finalize(
CLASS__<undefined_class>.hPublic:On_LoadGameViewStateDone(
CLASS__<undefined_class>.hPublic:On_LoadScreenClose(
CLASS__<undefined_class>.oProxy

function Initialize_Custom(
function Initialize(
function Finalize(

--==================================================
-- How To Use
--==================================================
Tutorial: https://forums.civfanatics.com/threads/whysmod-template-vi.693004/

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_cScr, 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"
	});

--==================================================
-- File Locals (members)
--==================================================
local m_hPriv, m_oProx, m_hMeta;

--==============================================================================
-- <undefined_class> Class
--==============================================================================
-- Extends WhysMod class for custom mod class.
--
m_hPriv = CLASS__WhysMod:extend("<undefined_class>", nil, true); -- final.
CLASS__<undefined_class> = m_hPriv;
CLASS__<undefined_class>.hSource["file"]    = FILE;
CLASS__<undefined_class>.hSource["version"] = VERSION;
CLASS__<undefined_class>.hSource["author"]  = AUTHOR;
CLASS__<undefined_class>.hSource["license"] = LICENSE;
--==============================================================================
-- Public constructor, singleton, static-only, run-once, override.
--
-- Returns new <undefined_class> object.
--
-- @param  none.
--
-- @return  <undefined_class> object.
--
-- @see  CLASS__WhysMod.
--
CLASS__<undefined_class>:override("new");
function CLASS__<undefined_class>.hPublic:new()
	local FILE, FUNC, this, priv = g_oWhys:id(self, "new");

	-- singleton check.
	if priv.bInstantiated == true then
		Throw(FILE, FUNC, {["class"] = priv.bClass}, nil
				, "Invalid call to constructor"
				..", singleton already instantiated.");
	end

	-- trace call.
	Trace(FILE, FUNC, {["class"] = priv.bClass});

	-- finally().
	local finally = function(sText)
		Throw(FILE, FUNC, {["class"] = priv.bClass, ["level"] = 1}, nil, sText);
	end

	-- create object & extract.
	local oProx = priv.hParent["WhysMod"]:new();
	local hMeta = getmetatable(oProx);
	local hPriv = hMeta.hPrivate;

	-- private fields.
	-- inherited only.

	-- private methods.
	-- no overwrites.

	-- public fields.
	-- inherited only.

	-- public methods.
	-- no overwrites.

	-- metamethods.
	-- no overwrites.

	-- security.
	setmetatable(oProx, hMeta); -- set metamethods first.

	return Trace_R(FILE, FUNC, {["class"] = priv.bClass}, oProx);
end
--==============================================================================
-- Public destructor, run-once, override.
--
-- Finalizes <undefined_class> object.
--
-- @param  aExtend    array.
-- @param  bRelated   boolean.
-- @param  bFirstMod  boolean.
-- @param  sIndent    string.
--
-- @return  boolean.
--
CLASS__<undefined_class>:override("finalize");
function CLASS__<undefined_class>.hPublic:finalize(
		aExtend, bRelated, bFirstMod, sIndent)
	local FILE, FUNC, this, priv = g_oWhys:id(self, "finalize");

	-- static check.
	if priv.bClass then
		Throw(FILE, FUNC, {["class"] = priv.bClass}, nil
				, "Invalid call to class, method not static.");
	end

	-- finalized check.
	if priv.bFinalized then
		Throw(FILE, FUNC, nil, nil
				, "Invalid call to instance, already finalized.");
	end

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

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

	-- proceed.
	local bError, b, x, t = false, true;

	-- delete.
	-- none.

	-- finalize parent.
	t, b = Catch(x, priv.hParent["WhysMod"].finalize, priv.hParent["WhysMod"]
			, aExtend, bRelated, bFirstMod, sIndent);
	if t ~= nil then
		x = t;
	elseif b == false then
		bError = true;
	end

	-- rethrow.
	if x ~= nil then
		Throw(FILE, FUNC, nil, x
			, "Problem encountered while finalizing "..this:getClass()
			.." object.");
	end

	-- trace return.
	return Trace_R(FILE, FUNC, nil, not bError);
end
--==============================================================================
-- Public method, static.
--
-- Executes on 'LoadGameViewStateDone' event.  Adds to events.
--
-- @param  none.
--
-- @return  none-known.
--
function CLASS__<undefined_class>.hPublic:On_LoadGameViewStateDone()
	local FILE, FUNC, this, priv = g_oWhys:id(self, "On_LoadGameViewStateDone");

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

	-- add to events.
	AddToEvent(Events.LoadScreenClose
			, g_ocScript.On_LoadScreenClose, g_ocScript);
end
--==============================================================================
-- Public method.
--
-- Executes on 'LoadScreenClose' event.
--
-- @param  none.
--
-- @return  none-known.
--
function CLASS__<undefined_class>.hPublic:On_LoadScreenClose()
	local FILE, FUNC, this, priv = g_oWhys:id(self, "On_LoadScreenClose");

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

	-- proceed.
	Notice(FILE, FUNC, nil, ">>> Hello World! <<<\n ");
end
--==============================================================================
-- Proxy & Source.
--
m_oProx, m_hMeta = {}, {};
m_hMeta.bProxy     = true;
m_hMeta.hPrivate   = CLASS__<undefined_class>;
m_hMeta.__index    = CLASS__<undefined_class>.hPublic;
m_hMeta.__newindex = CLASS__<undefined_class>.__newindex;
m_hMeta.__tostring = CLASS__<undefined_class>.__tostring;
m_hMeta.__pairs    = CLASS__<undefined_class>.__pairs;
setmetatable(m_oProx, m_hMeta); -- set metamethods first.
CLASS__<undefined_class>.oProxy = m_oProx;
CLASS__<undefined_class>        = m_oProx;
m_hPriv:buildSource();
--==============================================================================
-- END Class
--==============================================================================
--==============================================================================
-- 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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local bError = false;

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

	-- do-nothing.

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

	-- trace return.
	return g_oWhys: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_cScr == nil then
		error("\n( Uncaught Exception )--------------------------------"
				.."\n"..FILE..":"..FUNC.."(): Unable to initialize file '"..FILE
				.."', required class '"..g_cScr:getClass()
				.."' not found.", 2);
	end

	-- registration-complete check.
	if g_ocScript:isRegComplete() then
		g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local bError = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, "Initializing custom file '"..FILE.."' for script '"
	--		..sRegScript.."'...");

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

	-- instantiate, share, and globalize singleton WhysMod object:
  --     g_oMod, g_oScr, g_ocScript.
	g_ocScript:Instantiate(WHYSMOD__SCRIPT_VERSION, WHYSMOD__SCRIPT_AUTHOR
			, WHYSMOD__SCRIPT_LICENSE, WHYSMOD__MOD_ITERATION
			, WHYSMOD__MOD_AUTHOR, WHYSMOD__MOD_CREDIT
			, CLASS__<undefined_class>);

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

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

	-- initialized.
	if bError then
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, "Initialized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, "Initialized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

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

	-- trace return.
	return g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC, nil, aExtend, bRelated, bFirstMod, sIndent);

	-- proceed.
	local bError, fFinal = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, 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
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, sIndent.."Finalized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, sIndent.."Finalized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

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

-- clear namespace.
m_hPriv, m_oProx, m_hMeta = nil, nil, nil;

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

-- auto-init.
g_ocScript:RegisterExtend(CLASS__<undefined_class>);
Initialize();
--==============================================================================

Replace "<undefined_script_file>" with "MyMod_VI.lua".

Keep the current license or replace it with one you like better. 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. Otherwise, your mod will not compile properly.

Spoiler Licenses :

More Restrictive:
Code:
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:
Code:
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:
Code:
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:
Code:
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.
➡️ Creative Commons License Wizard

Next, scroll down to the "File Contents". Once your mod reaches significant size, it can become difficult to navigate. 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, you will see the following text.

include("WhysMod_VI.lua"

Click the line number to select the line, then do a find. Make sure the search box has "Current Document" selected, then click the "Find next" arrow button. This will take you to 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.

Scroll up slightly and replace "<undefined_mod_name>" with "MyMod_VI".

Scroll down and 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.

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

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

CLASS__WhysMod:extend("<undefined_class>"

Click the line number, do a find, and click find-next. This will take you to the top of your custom mod class. Select the "<undefined_class>" label, then do a find. In the search box, there is a down arrow to the left of the search-text input. Click it to open the replacement-text input, which will appear directly beneath the search-text input. In the replacement-text input, type "MyMod". To the right of the input are two buttons. The one on the left is the "Replace next" button and the one on the right is the "Replace all" button. Click the replace-all and a results window will open. It should say, "31 occurrence(s) replaced." Click OK.

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

Close the search box and scroll down slightly. You will see the following statement.

function CLASS__MyMod.hPublic:new()

This function is a public method of your custom mod class, and it defines how an object of your class is created. When called, it returns a new object instance. On the preceding line is the following statement.

CLASS__MyMod:override("new");

Your custom mod class extends the WhysMod class, which means it inherits all of its fields and methods. The WhysMod class already contains a new() method, thus your custom class is replacing it. Calling the override() first, allows the original method to remain available to your class. This allows an extended class to call overridden parent methods.

Continue to scroll down and you will see the following statement.

local oProx = priv.hParent["WhysMod"]:new();

Your custom new() begins by calling the overridden parent new(). This allows the original new() to do most of the work. Your custom new() can then make edits and additions to the object’s data. Below this are comment headers for the object’s fields and methods. It might appear empty, but that is only because the parent new() has already added all of the inherited data, as well as any custom methods defined by your class. How to add your own custom fields will be explained in a moment.

Continue to scroll down and you will see the finalize() method. It is preceded by a call to override(), allowing the parent finalize() to remain available to your class. This method ensures your object’s data is properly disposed of during cleanup. Currently, it calls the parent finalize() but has nothing of its own to delete. Even advanced modders are unlikely to ever change anything in this method.

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

If you ever need to return to the File Contents, you can always do a find for triple-backtick (```). Do a find for triple-backtick now, then close the search box and scroll down slightly. Within the File Contents, you will see the following text.

function Initialize(

Click the line number, do a find, and click find-next. 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. Even advanced modders are unlikely to ever change anything in this function.

Now scroll up to the global Initialize_Custom() function. This function 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 to the global Finalize() function. This function provides a standardized means of handling any cleanup for your custom file upon game exit. Again, even advanced modders are unlikely to ever change anything in this function.

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

CLASS__MyMod.hPublic:On_LoadGameViewStateDone(

Select it and do a find. This will take you to your On_LoadGameViewStateDone() method. This method 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() method is a good place to add your custom methods to events. Currently, it is adding the On_LoadScreenClose() method to the LoadScreenClose event.

Continue to scroll down and you will see the On_LoadScreenClose() method. 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.
Advanced_Template_ani_04.gif

Next, you’ll learn how to add a new custom method to another event that is frequently used by modders. Paste the following code directly under the On_LoadScreenClose() method.

Spoiler Code :
Code:
--==============================================================================
-- Public method.
--
-- 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 CLASS__<undefined_class>.hPublic:On_PlayerTurnActivated(
		iPlayer, bFirstTime)
	local FILE, FUNC, this, priv = g_oWhys:id(self, "On_PlayerTurnActivated");

	-- trace call.
	Trace(FILE, FUNC, nil, iPlayer, bFirstTime);

	-- proceed.
	if iPlayer == 1 then
		priv.iCount = priv.iCount +1;
		print("\n=== Counter ===> "..tostring(priv.iCount).."\n ");
	end
end

This adds the On_PlayerTurnActivated() method 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. The method begins with the following statement.

function CLASS__<undefined_class>.hPublic:On_PlayerTurnActivated(

Select the "<undefined_class>" label and replace it with "MyMod". Then partially select the new method name, from the beginning of "CLASS__" to (and including) the open parenthesis, and copy.

CLASS__MyMod.hPublic:On_PlayerTurnActivated(

Return to the File Contents and paste your new method name under the "On_LoadScreenClose". Including the open parenthesis prevents any partial matches of functions with similar names.

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

AddToEvent(Events.LoadScreenClose
, g_ocScript.On_LoadScreenClose, g_ocScript);

Paste the following code below the statement.

Code:
	AddToEvent(Events.PlayerTurnActivated
			, g_ocScript.On_PlayerTurnActivated, g_ocScript);

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

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

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

Return to the File Contents and navigate to the On_PlayerTurnActivated() method you added earlier. Paste the following code directly below the On_PlayerTurnActivated() method.

Spoiler Code :
Code:
--==============================================================================
-- Public method.
--
-- Executes 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 CLASS__<undefined_class>.hPublic:On_UnitSelectionChanged(
		iPlayer, iUnit, iHexI, iHexJ, iHexK, bSelected, bEditable)
	local FILE, FUNC, this, priv = g_oWhys:id(self, "On_UnitSelectionChanged");

	-- trace call.
	Trace(FILE, FUNC, nil, iPlayer, iUnit, iHexI, iHexJ, iHexK, bSelected
			, bEditable);

	-- proceed.
	-- do-nothing.
end
--==============================================================================
-- Public method.
--
-- Executes 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 CLASS__<undefined_class>.hPublic:On_UnitMoved(
		iPlayer, iUnit, iX, iY, bVisible, iStateChange)
	local FILE, FUNC, this, priv = g_oWhys:id(self, "On_UnitMoved");

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

	-- proceed.
	-- do-nothing.
end
--==============================================================================
-- Public method.
--
-- Executes on 'CityInitialized' event.
--
-- @param  iPlayer  integer.
-- @param  iCity    integer.
-- @param  iX       integer.
-- @param  iY       integer.
--
-- @return  none-known.
--
function CLASS__<undefined_class>.hPublic:On_CityInitialized(
		iPlayer, iCity, iX, iY)
	local FILE, FUNC, this, priv = g_oWhys:id(self, "On_CityInitialized");

	-- trace call.
	Trace(FILE, FUNC, nil, iPlayer, iCity, iX, iY);

	-- proceed.
	-- do-nothing.
end
--==============================================================================
-- Public method.
--
-- Executes on 'CitySelectionChanged' event.
--
-- @param  iPlayer  integer.
-- @param  iCity    integer.
--
-- @return  none-known.
--
function CLASS__<undefined_class>.hPublic:On_CitySelectionChanged(
		iPlayer, iCity)
	local FILE, FUNC, this, priv = g_oWhys:id(self, "On_CitySelectionChanged");

	-- trace call.
	Trace(FILE, FUNC, nil, iPlayer, iCity);

	-- proceed.
	-- do-nothing.
end

This adds four new event methods to your file. Return to the File Contents and paste the following code to reflect the new methods.

Code:
CLASS__<undefined_class>.hPublic:On_UnitSelectionChanged(
CLASS__<undefined_class>.hPublic:On_UnitMoved(
CLASS__<undefined_class>.hPublic:On_CityInitialized(
CLASS__<undefined_class>.hPublic:On_CitySelectionChanged(

Select any one of the "<undefined_class>" labels, then do a find. In the replacement-text input, type "MyMod". Click the replace-all and a results window will open. It should say, "8 occurrence(s) replaced." Click OK.

Navigate to CLASS__MyMod.hPublic:On_LoadGameViewStateDone(). Paste the following code to add the new functions to their respective events.

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

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

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

Navigate to CLASS__MyMod.hPublic:new(). Scroll down and you will see the following statement.

local oProx = priv.hParent["WhysMod"]:new();

As mentioned earlier, this calls the constructor of the parent class, which automatically adds all of the fields and methods from the parent class to your extended class object. It also adds all of the custom methods you’ve defined in your class, but any custom fields must still be added. Below this statement are the comment headers: private fields, private methods, public fields, public methods. Under each is the comment "-- inherited only" or "-- no overwrites.". This indicates no changes to the inherited data or custom methods.

Under the "private fields" header, replace "-- inherited only." with the following code.

Code:
hPriv.iCount = 0;

This adds a private field to your object instance. All private and public fields of your custom mod instance are automatically saved and restored across saved games. However, for any fields that are assigned functions, or data containing functions, those functions must be registered or they will not be saved.

Because this is a private field, only your class methods have access to it. Adding a public method that returns the private data allows you to then control how and under what circumstances that data is given.

Scroll down to the bottom of your finalize() method and paste the following code directly under it.

Spoiler Code :
Code:
--==============================================================================
-- Public method.
--
-- Returns private count.
--
-- @param  none.
--
-- @return  nonnegative-integer.
--
function CLASS__<undefined_class>.hPublic:GetCount()
	local FILE, FUNC, this, priv = g_oWhys:id(self, "GetCount");

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

	-- proceed.
	local tR = priv.iCount;

	-- trace return.
	return Trace_R(FILE, FUNC, nil, tR);
end

This adds the public GetCount() method to your custom class. The method begins with the following statement.

function CLASS__<undefined_class>.hPublic:GetCount()

Select the "<undefined_class>" label and replace it with "MyMod". Then select from the beginning of "CLASS__" to the open parenthesis, and copy.

CLASS__MyMod.hPublic:GetCount(

Return to the File Contents and add your new method name to it.

Spoiler Animation :
Do not rely on the line numbers shown in the editor.
Advanced_Template_ani_07.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:MyMod:On_LoadScreenClose(): >>> Hello World! <<<

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


✅ Congratulations! You’ve implemented the Advanced Template.


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() method, 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 :
Advanced_Template_ani_09.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 Advanced Template in related-scripts of your mod, use the Advanced 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_cScr, 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"
	});

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_cScr, g_ocScript.
-- adds global object: g_oWhys, g_oSave.
include("WhysMod_VI.lua");

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

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 Custom Load Screen section below, you will find an example of a multiscript mod that implements the Advanced Template.

This concludes this portion of the tutorial. The Advanced Template takes full advantage of the object based environment that is created by the WhysMod support files, allowing advanced modders to utilize inheritance, override methods, and implement polymorphism. To learn about adding custom textures and sounds to your mod, or how to interact with other mods, look in the Pinky & The Brain tutorial sections below, beginning with the Custom Interface.


🔼 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.02.2025.10.29.2235";
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 (```).

How To Use
Code Conventions

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
--==================================================
Tutorial: https://forums.civfanatics.com/threads/whysmod-template-vi.693004/

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 prefix 'g_'.  Member (file-local) scope prefix 'm_'.
Local-constant prefix 'c_'.  Global-constant prefix 'gc_'.  Member-constant
prefix 'mc_'.  Class-definition prefix 'CLASS__', assumed global.

Terms:

  unempty-string             "..."
  unempty-table              {...}
  positive-integer           i > 0
  nonnegative-integer        i >= 0
	negatable-integer          i >= -1
  nonpositive-integer        i <= 0
  negative-integer           i < 0
  nonzero-integer            i < 0 or i > 0
	nonzero-negatable-integer  i = -1 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_cScr, 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_oMod, g_oScr, g_ocScript.
g_ocScript:Instantiate(WHYSMOD__SCRIPT_VERSION, WHYSMOD__SCRIPT_AUTHOR
		, WHYSMOD__SCRIPT_LICENSE, WHYSMOD__MOD_ITERATION, WHYSMOD__MOD_AUTHOR
		, WHYSMOD__MOD_CREDIT);

--==================================================
-- File Locals (members)
--==================================================
-- 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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local bError = false;

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

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

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

	-- trace return.
	return g_oWhys: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_cScr == nil then
		error("\n( Uncaught Exception )--------------------------------"
				.."\n"..FILE..":"..FUNC.."(): Unable to initialize file '"..FILE
				.."', required class '"..g_cScr:getClass()
				.."' not found.", 2);
	end

	-- registration-complete check.
	if g_ocScript:isRegComplete() then
		g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local bError = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, "Initializing custom file '"..FILE.."' for script '"
	--		..sRegScript.."'...");

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

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

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

	-- initialized.
	if bError then
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, "Initialized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, "Initialized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

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

	-- trace return.
	return g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC, nil, aExtend, bRelated, bFirstMod, sIndent);

	-- proceed.
	local bError, fFinal = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, 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
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, sIndent.."Finalized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, sIndent.."Finalized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

	-- trace return.
	return g_oWhys: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.02.2025.10.29.2235";
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 (```).

How To Use
Code Conventions

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
--==================================================
Tutorial: https://forums.civfanatics.com/threads/whysmod-template-vi.693004/

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 prefix 'g_'.  Member (file-local) scope prefix 'm_'.
Local-constant prefix 'c_'.  Global-constant prefix 'gc_'.  Member-constant
prefix 'mc_'.  Class-definition prefix 'CLASS__', assumed global.

Terms:

  unempty-string             "..."
  unempty-table              {...}
  positive-integer           i > 0
  nonnegative-integer        i >= 0
	negatable-integer          i >= -1
  nonpositive-integer        i <= 0
  negative-integer           i < 0
  nonzero-integer            i < 0 or i > 0
	nonzero-negatable-integer  i = -1 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_cScr, 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_oMod, g_oScr, g_ocScript.
g_ocScript:Instantiate(WHYSMOD__SCRIPT_VERSION, WHYSMOD__SCRIPT_AUTHOR
		, WHYSMOD__SCRIPT_LICENSE);

--==================================================
-- File Locals (members)
--==================================================
local m_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"));
		m_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");
		bHideConsumer = oBrain:IsOpenInterface();
	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
		m_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 = m_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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local 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 g_oWhys: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_cScr == nil then
		error("\n( Uncaught Exception )--------------------------------"
				.."\n"..FILE..":"..FUNC.."(): Unable to initialize file '"..FILE
				.."', required class '"..g_cScr:getClass()
				.."' not found.", 2);
	end

	-- registration-complete check.
	if g_ocScript:isRegComplete() then
		g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC);

	-- proceed.
	local bError = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, "Initializing custom file '"..FILE.."' for script '"
	--		..sRegScript.."'...");

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

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

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

	-- initialized.
	if bError then
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, "Initialized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, "Initialized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

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

	-- trace return.
	return g_oWhys: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.
	g_oWhys:Trace(FILE, FUNC, nil, aExtend, bRelated, bFirstMod, sIndent);

	-- proceed.
	local bError, fFinal = false;

	--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
	--		, 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
		g_oWhys:Warning(FILE, FUNC, {["id"] = false, ["force"] = true}
				, sIndent.."Finalized custom file '"..FILE
				.."' for script '"..sRegScript.."' with errors.");
	else
		--g_oWhys:Notice(FILE, FUNC, {["id"] = false, ["force"] = true}
		--		, sIndent.."Finalized custom file '"..FILE
		--		.."' for script '"..sRegScript.."' successfully.");
	end

	-- trace return.
	return g_oWhys: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 support files. 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.2025.10.29.2235">
	<!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.

⬇️ 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.

Compile your project and then close ModBuddy. Also close Civ6 if it is open. Go to your Steam library, search for "Civilization VI", and install the Sid Meier’s Civilization VI Development Assets.

Launch ModBuddy and reopen your PinkyMod_VI project. From the 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.

Find your own path.
  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" (if present) 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.

⚠️ Before proceeding: ensure file name extensions are visible in Windows. Open File Explorer, then from the menu bar, select ViewShowFile name extensions.​

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. Windows may ask if you are sure you want to change the file extension. Click Yes, then select the file 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" ?>
<!--=========================================================================-->
<!DOCTYPE AssetObjects..ArtDefSet [
	<!-- Civ6: entities not implemented. -->
	<!ENTITY FILE    "Pinky.artdef">
	<!ENTITY VERSION "vymdt.01.2025.10.29.2235">
	<!ENTITY AUTHOR  "Ryan F. Mercer">
	<!ENTITY LICENSE "CC BY-NC-SA 4.0">
]>
<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. From the menu bar, select File → Save All to save your project.

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.

⬇️ ModArt_Generator.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 in ModBuddy and click anywhere within the file. 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 FireTuner and 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.

⬇️ Wwise_v2015.9.1_x64.zip

From the Windows start menu, launch Wwise. If it does not appear in the start menu, then from Windows File Explorer, navigate to the Wwise.exe file and double-click to run it. For this author, the file path is as follows, but your own path may differ.

Find your own path.
  C:\Program Files (x86)\Audiokinetic\Wwise v2015.1.9 build 5624\Authoring\x64\Release\Bin\Wwise.exe

Accept the license agreement, then 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 if it is not already selected. 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 interface will open on the right. Inside Event Actions, click Browse. The Project Explorer - Browser 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

From the menu bar, select Layouts → SoundBank. This will open the SoundBank Editor interface on the bottom right. 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. This will enable the "Hierarchy Inclusion" list in the SoundBank Editor. 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.

Find your own path.
  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 may 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.

➡️ audiokinetic.com

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 delete the BrainMod_VI.Art.xml file. Add the WhysMod support files, then 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. Be sure to include the UpdateArt action for the Mod Art Dependency File, as well as the UpdateAudio action for the four audio files.

⬇️ Brain_Cooked_VI.zip

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.

Find your own path.
  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's imported 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

From the menu bar, select File → Save All. Then 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 been imported. 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.

⬇️ 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.

Find your own path.
  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. From the menu bar, select Layouts → SoundBank. This will open the SoundBank Editor interface on the bottom right. In the Project Explorer, click the SoundBanks tab. Then select SoundBanks → Default Work Unit → Brain_Speech. In the Hierarchy Inclusion list on the right, select "\Events\Default Work Unit\Pinky_And_Brain" and click the Remove button in the bottom right corner. 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. From the menu bar, select Layouts → Interactive Music. This will open the Sound Property Editor on the right. Return to the Audio tab and select Actor-Mixer Hierarchy → Default Work Unit → Brain_Ponder. 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 in ModBuddy 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 of the method.

Code:
UI.PlaySound("Stop_Pinky_And_Brain");


✅ Congratulations! You’ve interrupted audio.


Ideally, replacement files should also implement the WhysMod Template, so that they too can function as a part of the team. Unfortunately, this is not possible for FrontEnd scripts. Attempting to implement the WhysMod Template in LoadScreen.lua, or any other FrontEnd script, will fail. But it is possible to include some of the same functionality by including the Whys_VI.lua file directly, at the top of the script, then adding it to the FrontEnd ImportFiles action. This provides many of the same global functions, but without access to saved data, other scripts, or other mods. Note, while the function names may be the same, the function parameters may be different. Whys_VI.lua includes its own tutorials for a detailed description of the full functionality.

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

-- adds global class: CLASS__Object, CLASS__Exception, CLASS__Whys.
-- adds global object: g_oWhys.
include("Whys_VI.lua"); -- auto-init.

-- tutorials, uncomment for output to console.
g_oWhys:Tutorialize_Whys();
--g_oWhys:Tutorialize_Type();
--g_oWhys:Tutorialize_ToString();
--g_oWhys:Tutorialize_Copy();
--g_oWhys:Tutorialize_Output();
--g_oWhys:Tutorialize_Trace();
--g_oWhys:Tutorialize_Throw();

-- adds global functions.
g_oWhys:globalize({""
	-- if global functions are not desired, comment out individual function
	-- names and call Whys object directly, ie: g_oWhys:ToString().

	-- see "Whys" tutorial (overview).
	, "Split"

	-- 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"
	});

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:
For Smarties
Spoiler :

The WhysMod Template allows users to easily create their own classes and objects, which can contain both public and private data. Custom classes can be final or extendable and can contain static methods. Methods of a parent class can be overridden and the original methods can still be called from within the extended class. These features allow custom objects to utilize inheritance and implement polymorphic behavior.

A class is first created as a table of private fields and methods. Contained within it is a table of public fields and methods. The private table is then placed within a metatable and the metatable is assigned to a proxy table. The public fields and methods are then accessible from the proxy table. This provides data security and allows for object orientated design.

Spoiler Code :
Code:
-- extend Object class.
local m_hPriv = CLASS__Object:extend("MyClass");
CLASS__MyClass = m_hPriv;

-- define source information.
CLASS__MyClass.hSource["file"]    = "<undefined_file>";
CLASS__MyClass.hSource["version"] = "<undefined_version>";
CLASS__MyClass.hSource["author"]  = "<undefined_author>";
CLASS__MyClass.hSource["license"] = "<undefined_license>";

-- add private & public methods.
CLASS__MyClass:RunPrivate() ... end
CLASS__MyClass.hPublic:RunPublic() ... end

-- create proxy & metatable.
local m_oProx, m_hMeta = {}, {};
m_hMeta.bProxy     = true;
m_hMeta.hPrivate   = CLASS__MyClass;
m_hMeta.__index    = CLASS__MyClass.hPublic;
m_hMeta.__newindex = CLASS__MyClass.__newindex;
m_hMeta.__tostring = CLASS__MyClass.__tostring;
m_hMeta.__pairs    = CLASS__MyClass.__pairs;
setmetatable(m_oProx, m_hMeta); -- set metamethods first.
CLASS__MyClass.oProxy = m_oProx;
CLASS__MyClass        = m_oProx;
m_hPriv:buildSource();

All class methods must use implicit self and begin with a call to g_oWhys:id(). It requires the implicit self parameter and the name of the current function. It returns the name of the current file and method, plus a reference to the proxy table, named "this", and a reference to the private table, named "priv". The this reference provides access to all of the public fields and methods, while the priv reference provides access to all of the private fields and methods.

Code:
function CLASS__MyClass.hPublic:RunPublic()
	local FILE, FUNC, this, priv = g_oWhys:id(self, "RunPublic");
	return this.iCustom, priv.bCustom;
end

Parent methods are overridden by first calling CLASS__<custom_class_name>:override("<function_name>") before redefining them. This ensures the original method can still be called internally. The "new" method is the object constructor, and it must be overridden to set the object data for instances of the class.

By calling the parent new() from within the custom new(), all private and public methods, both parent and custom, are added to the object instance. However, custom fields must still be added.

Spoiler Code :
Code:
CLASS__MyClass:override("new");
function CLASS__MyClass.hPublic:new()
	local FILE, FUNC, this, priv = g_oWhys:id(self, "new");

	-- create object & extract.
	local oProx = priv.hParent["Object"]:new();
	local hMeta = getmetatable(oProx);
	local hPriv = hMeta.hPrivate;

	-- private fields.
	hPriv.bCustom = true;
 
	-- private methods.
	-- no overwrites.

	-- public fields.
	hPriv.hPublic.iCustom = 0;

	-- public methods.
	-- no overwrites.

	-- metamethods.
	-- no overwrites.

	-- security.
	setmetatable(oProx, hMeta); -- set metamethods first.

	return oProx;
end

A class can be made final so that no new classes can be extended from it.

Code:
local hPriv = CLASS__Object:extend("MyClass", nil, true); -- final.

Otherwise, it may be necessary to override the extend() method to define how extended classes inherit from your custom class.

By calling the parent extend() from within the custom extend(), all private and public methods, both parent and custom, are added to the new class. However, custom fields must still be added.

Spoiler Code :
Code:
CLASS__MyClass:override("extend");
function CLASS__MyClass.hPublic:extend(sName, hRef, bFinal)
	local FILE, FUNC, this, priv = g_oWhys:id(self, "extend");

	-- create class.
	local hRef = priv.hParent["Object"]:extend(sName, hRef, bFinal);

	-- private fields.
	-- inherited only.

	-- private methods.
	-- no overwrites.

	-- public fields.
	-- inherited only.

	-- public methods.
	-- no overwrites.

	return hRef;
end

This concludes the WhysMod Template tutorial. This author hopes you find it and the WhysMod support files useful.


✅ Congratulations! I look forward to your future mods. 🏁


🔼 Top
⏫ Contents
 
Last edited:
...reserved...
 
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 Template on about January 1st 2023. All of the programming is my own work and was built from scratch. I developed it to 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.

Special Thanks:
  • 👤 Gedemon – for sharing their discovery of the built in ExposedMembers global and LeaveGameComplete event, allowing data to be shared between all scripts, and ensure proper cleanup. Also for developing the essential restart button override solution, ensuring proper cleanup on restart.
  •    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.

Honestly, I'm pretty certain the name 'Alpha Pro-Solve XTreme for Men' could apply to any product?  I would be equally satisfied buying a breakfast cereal with that name as I would a car, hat, razor, or toilet seat cover.


🔼 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 :
Date: ymd.2025.10.30
Version ID: vymdt.02.2025.10.29.2235
  • Changed global reference g_cClass to g_cScr (not BC).
  • Added global reference g_oScr.
  • Fixed typos and made minor changes to documentation.

🔼 Top
⏫ Contents
 
Last edited:
Back
Top Bottom