After 12 hours of playing and not even reaching 1942 I decided to look into why HoI3 runs so slow. One thing I noticed was everytime an hour blocked (froze on the hour before continuing) the message/event panel would spam a bunch of trade info, spy info etc. So it occured to me: the ai is what is eating up all the time making constant decisions about diplomacy and potentially other things.
It's not going to be rendering (obviously since it's fine when paused) and I really doubted combat or pathfinding would be slow (unless they just learned to code a week ago).
Soooo I dug into ./script/ and opened the ai_diplomacy.lua file to see what kind of shit was going on.
Single functions were being called to handle so many decisions with so many logical branches of code that it was no wonder speed was a pile of crap, just look at this function for a SINGLE trade offer.
This is after a check in the minister AI to see if it would like to propose a trade, it gets worse.
Lua is not fast when used in this way and especially when functions like these are being called nearly 200 times EVERY IN GAME HOUR (or every couple hours or per day, depends on how often they do it, you can tell when the hours stall). Potentially they're not even staggering the AI updates and simply doing it all at once, as opposed to spreading it across the month doing a few every day/hour.
Won't take my word for it? Edit the lua files to all return 0 so that no trade or spy or diplomacy logic is performed. The game runs through days at lightning speed.
Why don't I think this can be easily solved? Because they'd have to re-write this AI logic in C/C++ where it wouldn't even stumble to process this but as a result you'd lose the cool modable AI potential. Other alternatives include staggering the updates across different frames, but they may already be doing this. Lastly they could potentially write a system to process lua in chunks on threads then update lots of lua states at once; however this would require a significant change in the way the AI works and interfaces back with C in order to make it thread safe and work in a data processing In-Out way.
Sorry to ramble on but just wanted everyone to know my perspective on the performance problem they have. I don't really blame them here because it's clear what they wanted it just has drawbacks and will require some serious thinking to get around it. Hopefully people will understand this and not just mindlessly claim "IT'S A FLOP! HOW COULD THEY HAVE DONE SOMETHING THIS STUPID?".
It's not going to be rendering (obviously since it's fine when paused) and I really doubted combat or pathfinding would be slow (unless they just learned to code a week ago).
Soooo I dug into ./script/ and opened the ai_diplomacy.lua file to see what kind of shit was going on.
Single functions were being called to handle so many decisions with so many logical branches of code that it was no wonder speed was a pile of crap, just look at this function for a SINGLE trade offer.
Code:
function DiploScore_OfferTrade(ai, actor, recipient, observer, action)
if observer == actor then
return 100 -- handled from foreign minister
else -- evaluate offer of trade from actor
--Utils.LUA_DEBUGOUT("TRADE ========")
--Utils.LUA_DEBUGOUT("actor: " .. tostring( actor ))
--Utils.LUA_DEBUGOUT("recipient: " .. tostring( recipient ))
local score = 0
local actorCountry = actor:GetCountry()
local recipientCountry = recipient:GetCountry()
local route = action:GetRoute()
if ai:AlreadyTradingResourceOtherWay( action:GetRoute() ) then
return 0
end
--Utils.LUA_DEBUGOUT("from: " .. tostring( route:GetFrom() ))
--Utils.LUA_DEBUGOUT("to: " .. tostring( route:GetTo() ))
local MAX_GOODS = CGoodsPool._GC_NUMOF_-1
for goods = 0, MAX_GOODS do
local balance = recipientCountry:GetDailyBalance(goods):Get()
if goods == CGoodsPool._SUPPLIES_ then
balance = math.min( balance, recipientCountry:GetSupplyBalanceAverage():Get() )
end
local absBalance = math.abs(balance)
local inTrade = route:GetTradedFromOf(goods):Get()
local outTrade = route:GetTradedToOf(goods):Get()
if absBalance > 0.005 and (outTrade > 0.01 or inTrade > 0.01) then
--Utils.LUA_DEBUGOUT("____________goods : " .. GOODS_TO_STRING[goods] )
--Utils.LUA_DEBUGOUT("balance : " .. balance )
if inTrade > 0.001 and absBalance > 0.001 then
--Utils.LUA_DEBUGOUT("GetTradedToOf: " .. inTrade)
local tradeFactor = 0.0
if balance < 0.0 then
tradeFactor = math.min(inTrade / (-balance) * 2, 1.0)
score = score + math.min(tradeFactor, 1.0) * 100
--Utils.LUA_DEBUGOUT("tradefactor: " .. tradeFactor)
--Utils.LUA_DEBUGOUT("inTrade: " .. inTrade)
--Utils.LUA_DEBUGOUT("balance: (-)" .. balance)
else
if not (goods == CGoodsPool._MONEY_) then
tradeFactor = math.min(inTrade / balance, 1.0) * 0.5
score = score - math.min(tradeFactor * 2, 1.0) * 100
--Utils.LUA_DEBUGOUT("tradefactor: " .. tradeFactor)
--Utils.LUA_DEBUGOUT("inTrade: " .. inTrade)
--Utils.LUA_DEBUGOUT("balance: ()" .. balance)
end
end
--Utils.LUA_DEBUGOUT("tradefactor: " .. tradeFactor)
--Utils.LUA_DEBUGOUT("score: " .. score)
end
if outTrade > 0.001 then
--Utils.LUA_DEBUGOUT("GetTradedFromOf : " .. outTrade )
--local tradeFactor = 0
--if (balance > 0) then
-- tradeFactor = (absBalance - outTrade) / absBalance
--else
-- score = 0
--end
--score = score + tradeFactor * 100
if balance > 0.0 then
tradeFactor = math.min(outTrade / (balance), 1.0)
if tradeFactor > 0.95 then
if tradeFactor > 0.999999 then
tradeFactor = 0
else
tradeFactor = (0.06 - tradeFactor - 0.95)
end
score = score + math.min(tradeFactor, 1.0) * 100
elseif tradeFactor > 0.7 then
score = score + 85
elseif tradeFactor > 0.3 then
score = score + 75
elseif tradeFactor > 0.001 then
score = score + 60
end
--score = score + math.min(tradeFactor, 1.0) * 100
--Utils.LUA_DEBUGOUT("tradefactorz: " .. tradeFactor)
--Utils.LUA_DEBUGOUT("outTrade: " .. outTrade)
--Utils.LUA_DEBUGOUT("balance: (-)" .. balance)
else
--tradeFactor = math.min(outTrade / balance, 1.0) * 0.5
score = score - 1000 -- math.min(tradeFactor * 2, 1.0) * 100
--Utils.LUA_DEBUGOUT("tradefactor: " .. tradeFactor)
--Utils.LUA_DEBUGOUT("outTrade: " .. outTrade)
--Utils.LUA_DEBUGOUT("balance: (-)" .. balance)
end
--Utils.LUA_DEBUGOUT("tradefactor: " .. tradeFactor)
--Utils.LUA_DEBUGOUT("score: " .. score)
end
--Utils.LUA_DEBUGOUT("____________")
end
end
-- we need transports to trade
if actorCountry:NeedConvoyToTradeWith( recipient ) then
if route:GetConvoyResponsible() == recipient then
if recipientCountry:GetTransports() == 0 then
score = 0
end
end
end
--Utils.LUA_DEBUGOUT("score before strategy" .. score)
if score > 0.001 or score < -0.001 then
if score > 30 then
local rel = ai:GetRelation(recipient, actor)
local strategy = recipient:GetCountry():GetStrategy()
score = score - strategy:GetAntagonism(actor) / 15
score = score + strategy:GetFriendliness(actor) / 15
score = score - rel:GetThreat():Get() / 2
if rel:IsGuaranteed() then
score = score + 5
end
if rel:HasFriendlyAgreement() then
score = score + 10
end
if rel:AllowDebts() then
score = score + 5
end
if rel:IsFightingWarTogether() then
score = score + 15
end
--Utils.LUA_DEBUGOUT("GetAntagonism" .. tostring(strategy:GetAntagonism(actor)))
--Utils.LUA_DEBUGOUT("GetFriendliness" .. tostring(strategy:GetFriendliness(actor)))
--Utils.LUA_DEBUGOUT("GetThreat" .. tostring(rel:GetThreat():Get()))
--Utils.LUA_DEBUGOUT("score after country stuff" .. score)
local relation = rel:GetValue():GetTruncated()
score = score + relation / 5
end
--Utils.LUA_DEBUGOUT("score after country stuff" .. score)
end
--Utils.LUA_DEBUGOUT("===================\n")
return Utils.CallScoredCountryAI(recipient, 'DiploScore_OfferTrade', score, ai, actor, recipient, observer)
end
end
This is after a check in the minister AI to see if it would like to propose a trade, it gets worse.
Code:
function ProposeTrades(minister)
local SMALLEST_TRADE = 0.03
local ministerTag = minister:GetCountryTag()
local ministerCountry = minister:GetCountry()
local ai = minister:GetOwnerAI()
local strategy = ministerCountry:GetStrategy()
local bestScore = -10000
local bestAction = nil
-- TODO: SCALE WITH AMOUNT THEY HAVE IN STOCKPILE FOR "STABILITY"
-- see what we are low on and find someone who is hoarding it
local MAX_GOODS = CGoodsPool._GC_NUMOF_-1
local myMoney = ministerCountry:GetDailyBalance( CGoodsPool._MONEY_ ):Get()
for goods = 0, MAX_GOODS do
if not (goods == CGoodsPool._MONEY_) then
local balance = ministerCountry:GetDailyBalance( goods ):Get()
if balance < 0.01 and myMoney > 0.01 then
--Utils.LUA_DEBUGOUT( "I'm low on " .. goods .. tostring(ministerTag) )
for country in CCurrentGameState.GetCountries() do
local countryTag = country:GetCountryTag()
if country:Exists() and countryTag:IsReal() and
not (ministerCountry:HasDiplomatEnroute(countryTag)) and
not (countryTag == ministerTag) then
local rel = ministerCountry:GetRelation( countryTag )
local theirBalance = country:GetDailyBalance(goods):Get() * 0.6
if goods == CGoodsPool._SUPPLIES_ then
theirBalance = math.min( theirBalance, country:GetSupplyBalanceAverage():Get() * 0.6 )
end
if theirBalance > 0.01 then
local requested = math.min( theirBalance, -balance )
local action = CTradeAction( ministerTag, countryTag )
if action:IsValid() and action:IsSelectable() then
action:SetTrading( CFixedPoint(requested), goods )
local money = action:GetTrading( CGoodsPool._MONEY_, ministerTag ):Get()
local factor = 1.0
if money > myMoney then
factor = myMoney / money - 0.08
if factor > 0.01 then
local action2 = CTradeAction( ministerTag, countryTag )
local amount = requested * factor
if amount > SMALLEST_TRADE then -- remove small stuff
action2:SetTrading( CFixedPoint(amount), goods )
local score = 70 --balance
score = score - rel:GetThreat():Get() * CalculateAlignmentFactor(ai, ministerCountry, country)
score = score + (100 * amount / math.abs(balance))
score = score + math.floor(theirBalance / 10) -- big producer bonus = more stable
-- we need transports to trade
if ministerCountry:NeedConvoyToTradeWith( countryTag ) then
if action2:GetRoute():GetConvoyResponsible() == ministerTag then
if ministerCountry:GetTransports() == 0 then
score = 0
end
else
if countryTag:GetCountry():IsAtWar() then
score = score - 20
end
end
end
local acceptanceChance = action2:GetAIAcceptance()
acceptanceChance = acceptanceChance - minister:GetSpamPenalty(countryTag)
if ( factor > 0.01 and acceptanceChance > 50 ) then
if score > bestScore and action2:IsConvoyPossible()
and (not ai:AlreadyTradingResourceOtherWay( action2:GetRoute() ) )
then
bestAction = action2
bestScore = score
end
end
end -- if amount
end
elseif requested > SMALLEST_TRADE then
local score = 70 --balance
score = score - rel:GetThreat():Get() * CalculateAlignmentFactor(ai, ministerCountry, country)
score = score + (100 * requested / math.abs(balance))
score = score + math.floor(theirBalance / 10) -- big producer bonus = more stable
-- we need transports to trade
if ministerCountry:NeedConvoyToTradeWith( countryTag ) then
if action:GetRoute():GetConvoyResponsible() == ministerTag then
if ministerCountry:GetTransports() == 0 then
score = 0
end
else
if countryTag:GetCountry():IsAtWar() then
score = score - 20
end
end
end
local acceptanceChance = action:GetAIAcceptance()
acceptanceChance = acceptanceChance - minister:GetSpamPenalty(countryTag)
if ( factor > 0.01 and acceptanceChance > 50 ) then
if score > bestScore and action:IsConvoyPossible()
and (not ai:AlreadyTradingResourceOtherWay( action:GetRoute() ) )
then
bestAction = action
bestScore = score
end
end
end
end
end
end
end
end
end
end
if bestAction then
ai:PostAction( bestAction )
end
end
Lua is not fast when used in this way and especially when functions like these are being called nearly 200 times EVERY IN GAME HOUR (or every couple hours or per day, depends on how often they do it, you can tell when the hours stall). Potentially they're not even staggering the AI updates and simply doing it all at once, as opposed to spreading it across the month doing a few every day/hour.
Won't take my word for it? Edit the lua files to all return 0 so that no trade or spy or diplomacy logic is performed. The game runs through days at lightning speed.
Why don't I think this can be easily solved? Because they'd have to re-write this AI logic in C/C++ where it wouldn't even stumble to process this but as a result you'd lose the cool modable AI potential. Other alternatives include staggering the updates across different frames, but they may already be doing this. Lastly they could potentially write a system to process lua in chunks on threads then update lots of lua states at once; however this would require a significant change in the way the AI works and interfaces back with C in order to make it thread safe and work in a data processing In-Out way.
Sorry to ramble on but just wanted everyone to know my perspective on the performance problem they have. I don't really blame them here because it's clear what they wanted it just has drawbacks and will require some serious thinking to get around it. Hopefully people will understand this and not just mindlessly claim "IT'S A FLOP! HOW COULD THEY HAVE DONE SOMETHING THIS STUPID?".