Thalassicus
Bytes and Nibblers
This is just a reference for anyone trying to figure out how to do classes in Lua, link Lua with XML and SQL, or create function jump tables. It's some code I put together while learning Lua. I started with existing algorithms as a basis while figuring out how to do these topics, since I'm just getting my feet wet and anything more complex would probably be over my head. Some parts are based on work by Afforess (an immensely useful StringUtils library for serializing tables) and killmeplease (a great Emigration Mod that helps with the ignore-happiness problem in the vanilla game). If either author would prefer me to use different algorithms as a basis though, I will try and find something.
First I created a queue class to track average happiness, stored in savegame files. This comes in several parts.
The class has all the standard queue methods: new, push, pop, size. I did add something nonstandard to the push method: automated size management, useful for this particular application since the upper bound is fixed and I never pop manually. I also created an average() method which isn't a typical part of queues, but again was useful here. Then comes main, load, and save methods using instantiated objects of this queue.
The next part is where I tied in an XML file with Lua. The second half of the XML file is actually a jump table: a list of function names the program calls depending on input (C++ programmers can think of this as a function pointer table). The reason for doing a jump table is the function names can be stored externally. This increases modding flexibility, the user doesn't have to recompile the Lua to change things, and it allows the Lua code to have a very simple loop structure. Following the XML are two methods I created using this external data.
Anyway, hopefully this is helpful for someone's modding efforts out there so you don't have to figure out things from scratch. The code is all tested and works properly, you can download it below.
First I created a queue class to track average happiness, stored in savegame files. This comes in several parts.
The class has all the standard queue methods: new, push, pop, size. I did add something nonstandard to the push method: automated size management, useful for this particular application since the upper bound is fixed and I never pop manually. I also created an average() method which isn't a typical part of queues, but again was useful here. Then comes main, load, and save methods using instantiated objects of this queue.
Spoiler :
PHP:
--
--
-- Queue class
--
Queue = { }
function Queue:new(o)
o = o or {};
setmetatable(o, self);
self.__index = self;
return o;
end
function Queue:push( value )
table.insert(self, value);
if (self:size() >= QUEUE_CAPACITY) then
self:pop();
end
end
function Queue:pop( )
if self:size() == 0 then
return nil;
end
local val = self[self.first];
table.remove(self, 1);
return val;
end
function Queue:size( )
return #self;
end
function Queue:average()
local sum = 0;
for id,val in pairs(self) do
--printDebug(self[i]);
sum = sum + val;
end
--printDebug("sum = " .. sum);
--printDebug("self:size() = " .. self:size());
return math.floor(sum / self:size());
end
--
-- Globals
--
DebugMode = true;
QUEUE_CAPACITY = GameInfo.Emigration["HappinessAverageTurns"].Value * GameInfo.GameSpeeds[PreGame.GetGameSpeed()].GrowthPercent / 100;
iProbCityEmigration = {};
AverageHappiness = {};
HappinessTable = nil;
--
-- Main
--
-- Based off the Emigration Mod.
function doEmigration()
-- Thank you to Afforess for making his code open source. This save/load procedure is based off his work.
if not HappinessTable then
doGameInitialization();
end
updateHappinessInfo();
-- for each major civ
for iPlayerID,pPlayer in pairs(Players) do
if isValidPlayer(pPlayer) and not pPlayer:IsMinorCiv() then
printDebug("=== " .. getPlayerName(pPlayer) .. " ===");
local iAverageHappiness = HappinessTable[iPlayerID]:average();
printDebug("iAverageHappiness = " .. iAverageHappiness);
-- if unhappy
local bShouldEmigrate = (pPlayer:GetExcessHappiness() < 0 and iAverageHappiness < 0);
if pPlayer:GetNumCities() == 1 then
for pCity in pPlayer:Cities() do
if (pCity:IsCapital()) and (pCity:GetPopulation() == 1) then
bShouldEmigrate = false;
end
end
end
local iNumEmigrants = math.floor(-iAverageHappiness / GameInfo.Emigration["NumEmigrantsDenominator"].Value) + 1;
-- do emigration
if bShouldEmigrate then
doEmigratePlayer(pPlayer, iNumEmigrants);
end
end
end
saveGameData();
end
--
-- Utility Functions
--
-- Thank you to Afforess for providing his code open source. This save/load procedure is based off the Revolutions Mod.
function doGameInitialization()
HappinessTable = {};
for iPlayerID,pPlayer in pairs(Players) do
if (isValidPlayer(pPlayer)) then
local SaveData = pPlayer:GetScriptData();
--printDebug("Attempting to Load Save Data");
if SaveData == "" then
HappinessTable[iPlayerID] = Queue:new();
else
--printDebug("Loading Save Data");
--printDebug(SaveData);
SaveData = stringToTable(SaveData, QUEUE_CAPACITY, 1, {","}, 1);
HappinessTable[iPlayerID] = Queue:new(SaveData);
end
end
end
end
------------------------------------------------------------------------------
function saveGameData()
for iPlayerID,pPlayer in pairs(Players) do
if isValidPlayer(pPlayer) then
--printDebug("Attempting to Save Data");
local tbl = tableToString(HappinessTable[iPlayerID], QUEUE_CAPACITY, 1, {","}, 1);
--printDebug("Save Data Created");
--printDebug(tbl);
pPlayer:SetScriptData(tbl);
end
end
end
The next part is where I tied in an XML file with Lua. The second half of the XML file is actually a jump table: a list of function names the program calls depending on input (C++ programmers can think of this as a function pointer table). The reason for doing a jump table is the function names can be stored externally. This increases modding flexibility, the user doesn't have to recompile the Lua to change things, and it allows the Lua code to have a very simple loop structure. Following the XML are two methods I created using this external data.
Spoiler :
PHP:
<GameData>
<!-- Table definition -->
<Table name="Emigration">
<Column name="Type" type="text"/>
<Column name="Value" type="variant" default="0"/>
</Table>
<Table name="EmigrationWeights">
<Column name="Type" type="text"/>
<Column name="IsCityStatus" type="boolean" default="false"/>
<Column name="Value" type="variant" default="0"/>
</Table>
<!-- Table data -->
<Emigration>
<Row>
<Type>HappinessAverageTurns</Type>
<Value>10</Value>
</Row>
<Row>
<Type>MaxPasses</Type>
<Value>3</Value>
</Row>
<Row>
<Type>NumEmigrantsDenominator</Type>
<Value>10</Value>
</Row>
</Emigration>
<EmigrationWeights>
<Row>
<Type>EmigratedOnceAlready</Type>
<Value>0.5</Value>
</Row>
<Row>
<Type>IsRazing</Type>
<IsCityStatus>true</IsCityStatus>
<Value>0.0</Value>
</Row>
<Row>
<Type>IsResistance</Type>
<IsCityStatus>true</IsCityStatus>
<Value>1.0</Value>
</Row>
<Row>
<Type>IsPuppet</Type>
<IsCityStatus>true</IsCityStatus>
<Value>2.0</Value>
</Row>
<Row>
<Type>IsOccupied</Type>
<IsCityStatus>true</IsCityStatus>
<Value>1.5</Value>
</Row>
<Row>
<Type>IsBlockaded</Type>
<IsCityStatus>true</IsCityStatus>
<Value>0.5</Value>
</Row>
<Row>
<Type>IsCapital</Type>
<IsCityStatus>true</IsCityStatus>
<Value>0.5</Value>
</Row>
<Row>
<Type>GetGarrisonedUnit</Type>
<IsCityStatus>true</IsCityStatus>
<Value>0.5</Value>
</Row>
</EmigrationWeights>
</GameData>
Spoiler :
PHP:
function updateEmigrations(pPlayer)
-- Based off the Emigration Mod.
local cityWeight = {};
local cityProb = {};
local totalCulture = 0;
local totalWeight = 0;
local cityID = 0;
-- figure out weight
for city in pPlayer:Cities() do
cityID = city:GetID();
cityWeight[cityID] = 1;
-- retrieve function and weight from jump table stored with XML
for row in GameInfo.EmigrationWeights() do
if row.IsCityStatus and city[row.Type](city) then
cityWeight[cityID] = cityWeight[cityID] * row.Value;
end;
end
-- factor in culture
cityWeight[cityID] = cityWeight[cityID] / city:GetJONSCultureThreshold();
totalWeight = totalWeight + cityWeight[cityID];
cityID = cityID + 1;
end
-- probability = contribution to total weight
for city in pPlayer:Cities() do
iProbCityEmigration[city:GetID()] = 100 * cityWeight[city:GetID()] / totalWeight;
end
end
------------------------------------------------------------------------------
function shouldCityEmigrate(passNum, city)
-- Based off the Emigration Mod.
local p = Map.Rand(100, "City emigration probability - Lua")
printDebug(" " .. iProbCityEmigration[city:GetID()] * 2 ^ (passNum - 1) .. " >= " .. p .. "?");
if ((iProbCityEmigration[city:GetID()] * 2 ^ (passNum - 1)) >= p) and (city:GetPopulation() > 1) then
-- less likely to emigrate from a city multiple times
iProbCityEmigration[city:GetID()] = iProbCityEmigration[city:GetID()] * GameInfo.EmigrationWeights["EmigratedOnceAlready"].Value;
return true;
end;
return false;
end
Anyway, hopefully this is helpful for someone's modding efforts out there so you don't have to figure out things from scratch. The code is all tested and works properly, you can download it below.