Bug Reports and Discussion

I am still unable to replicate. I tried several variations including your exact scenario (2 galleons, 10 flurries, 2 barbarian triremes) and never saw multiple defensive strikes on one target.

If you can provide a savegame (ver 2.7 please) with a repeatable occurrence of this issue I can try to debug it.

It happens with 2.7beta too. I figured out a culprit: The option settings. Dont think they are stored in save file. So save file would not help you? 'Quick Combat(Defense)' must be on OFF else it will not happen. I also have 'Show enemy moves' on ON, zoom to combat on OFF, stack attack on OFF - dont know if these matter.

If you still cannot reproduce it please post again. I will try to provide a save file and all my option settings (screenshots?) then.

I used 3 galleons & 15 flurries & 2 barb triremes now.
 
It happens with 2.7beta too. I figured out a culprit: 'Quick Combat(Defense)' must be on OFF else it will not happen

This is the issue. Looks like it's due to where Kael placed the FFH-specific code in updateCombat(). Think I might have a solution for it.

Thanks for providing the extra info!
 
Longshoremen can be cast with ships that do not have cargo if they pick 'Skeleton Crew' first. In similar fashion it can probably be cast by ships which have too much current cargo (if they have 'Skeleton Crew' active).

Suggested fix: Increase required cargo if 'Skeleton Crew' is active:

Code:
def reqCrewLongshoremen(caster):
	pPlot = caster.plot()
	if caster.isHasPromotion(gc.getInfoTypeForString('PROMOTION_LONGSHOREMEN')):
		return False
	req_cargo = 1
	if caster.isHasPromotion(gc.getInfoTypeForString('PROMOTION_SKELETON_CREW')):
		req_cargo = req_cargo + 1
	if caster.cargoSpaceAvailable(-1, gc.getInfoTypeForString('DOMAIN_LAND')) < req_cargo:
		return False

Tested and works for me.


Edit: There is a similar problem with the other spells that they allow ships to have more cargo than they should have: Picking Cargo spell + loading ship + picking other spell does allow the ship to exceed its cargo limits. Probably would be better to use something other than "cargoSpaceAvailable" as that does not consider current load (edit2: wrong it does, probably was some load/save issue between .py changes which made me believe that).

Edit2: After some testing:
Adding this to 'reqCrewBuccaneers' and 'reqCrewNormalCrew' should completely fix it (in combination with the fix for 'reqCrewLongshoremen'):
Code:
	if caster.isHasPromotion(gc.getInfoTypeForString('PROMOTION_SKELETON_CREW')) and caster.isFull():
		return False
 
General about withdrawal:

Withdrawal due to promotions works on offense and on defense - ok but differs from BTS AFAIK.
Withdrawal from iWithdrawlProbDefensive works only on defense - as intended obviously.
Withdrawal from <iWithdrawalProb>20</iWithdrawalProb> only works on offense.
Is this intentional? At least the displayed stats are misleading: The unit help does not distinct between iWithdrawalProb and withdrawal from promotions.
It just adds the 2 values and displays them. Which made sense in BTS (no defensive withdrawal based on looking into the respective cpp) but not in FFH2.
Example: Privateer with 3 promos has 70% withdraw on offense and 50% on defense. The help simply says 70%. I never even knew there was a difference before looking at this stuff in detail.

--

There is supposed to be a cap at 90%. The method to ensure the cap is questionable and does only work partially:
Code:
    if (promotionInfo.getWithdrawalChange() > 0)
    {
        if (promotionInfo.getWithdrawalChange() + m_pUnitInfo->getWithdrawalProbability() + getExtraWithdrawal() > GC.getDefineINT("MAX_WITHDRAWAL_PROBABILITY"))
        {
            return false;
        }
    }
Means to breach the cap:
a) Cast "fair winds" spell.
b) Mutation and getting 'Light' promotion.
c) Probably upgrading a unit (not tested).
d) The 10% from 'Advanced Tactics' are not considered (not tested).

There are weird problems due to the questionable method:
a) Have to wait for 'fair winds' to turn off before picking withdrawal promotion.
b) Can mutate/upgrade after picking withdrawal promotion but not before.
c) It can also be advantageous to have less withdrawal (as having more may make it impossible to pick the next withdrawal promotion).
d) The method only considers offensive withdrawal but the promotions function for defensive withdrawal also. Which means defensive withdrawal is unfairly restricted due to having offensive withdrawal.

Any reason for not changing the code so that the maximum is actually ensured and all the weirdness due to the questionable code goes away?
Like - in combination with removing the questionable code: (not tested, cannot compile DLL)
Code:
int CvUnit::withdrawalProbability() const
{
	if (getDomainType() == DOMAIN_LAND && plot()->isWater())
	{
		return 0;
	}

//FfH: Added by Kael 04/06/2009
    if (getImmobileTimer() > 0)
    {
        return 0;
    }
//FfH: End Add

	// Advanced Tactics - all units have an inherent 10% withdrawal chance
	int iWithDrawalBonus = 0;
	if (GC.getGameINLINE().isOption(GAMEOPTION_ADVANCED_TACTICS))
	{
		if ((getDuration() == 0) && !m_pUnitInfo->isObject())
		{
			iWithDrawalBonus += 10;
		}
	}

[B]	const int prior_code_withdraw = std::max(0, (m_pUnitInfo->getWithdrawalProbability() + getExtraWithdrawal() + iWithDrawalBonus));

	// possibly too slow to read this here like that - may have to 'cache' the constant somewhere
	// or simply use static but I do not know if that is acceptable/workable in civ 4 code
	const int max_withdraw = GC.getDefineINT("MAX_WITHDRAWAL_PROBABILITY")); 

	return std::min(max_withdraw, prior_code_withdraw);[/B]
}

And maybe also getWithdrawlProbDefensive() but AFAIK only Loki can and does (intentionally) exceed the cap and I do not know if the current behaviour (see question above) is intended.
 
See:
http://forums.civfanatics.com/showpost.php?p=14050433&postcount=729
https://bitbucket.org/Tholal/more-naval-ai/issues/65/armageddon-can-hit-player-twice

I have looked into the .cpp (etc) code and done some testing. Not 100% sure I got everything right.

The culprit appears to be the way global events are handled. Armageddon Blight is not affected because iGlobal is not set. Presumably (not tested) Hellfire & Wrath are also affected. Maybe some other events too.

What appears to be happening is the following:

1) Human player (only happens to human player but not during autoplay as that does not count as "human" in cpp terms) increases AC to 100.

2) At the end of the humans turn the game will generate a Popup in "void CvPlayer::trigger(const EventTriggeredData& kData)".

3) Game switches to first/next AI player and ends up in the same function. But the code is different as instead it calls "applyEvent(eEvent, kData.getID());"

4) In applyEvent setTriggerFired is called. There the misworking happens (but not yet):

Code:
void CvPlayer::setTriggerFired(const EventTriggeredData& kTriggeredData, bool bOthers, bool bAnnounce)
{
	FAssert(kTriggeredData.m_eTrigger >= 0 && kTriggeredData.m_eTrigger < GC.getNumEventTriggerInfos());

	CvEventTriggerInfo& kTrigger = GC.getEventTriggerInfo(kTriggeredData.m_eTrigger);

	if (!isTriggerFired(kTriggeredData.m_eTrigger))
	{
		m_triggersFired.push_back(kTriggeredData.m_eTrigger);

		if (bOthers)
		{
			if (kTrigger.isGlobal())
			{
				for (int i = 0; i < MAX_CIV_PLAYERS; i++)
				{
					if (i != getID())
					{

//						GET_PLAYER((PlayerTypes)i).setTriggerFired(kTriggeredData, false, false);
                        if (GET_PLAYER((PlayerTypes)i).isAlive())
                        {
                            GET_PLAYER((PlayerTypes)i).setTriggerFired(kTriggeredData, false, false);
                        }

					}
				}
			}
			else if (kTrigger.isTeam())
			{
				for (int i = 0; i < MAX_CIV_PLAYERS; i++)
				{
					if (i != getID() && getTeam() == GET_PLAYER((PlayerTypes)i).getTeam())
					{
						GET_PLAYER((PlayerTypes)i).setTriggerFired(kTriggeredData, false, false);
					}
				}
			}
		}
	}

//FfH: Modified by Kael 09/25/2008
<snip comments>
    if (isAlive())
    {
		if (!CvString(kTrigger.getPythonCallback()).empty())
        {
            long lResult;

            CyArgsList argsList;
            argsList.add(gDLL->getPythonIFace()->makePythonObject(&kTriggeredData));
            argsList.add(getID());	// Player ID
            gDLL->getPythonIFace()->callFunction(PYRandomEventModule, kTrigger.getPythonCallback(), argsList.makeFunctionArgs(), &lResult);
        }
    }
    GC.getGameINLINE().setEventTriggered((EventTriggerTypes)kTriggeredData.m_eTrigger, true);
//FfH: End Modify
<snip more code>

After/during the turn of the first/next AI player this function is called for every player - including the human one. The Armageddon code is actually in the python callback. Due to "GC.getGameINLINE().setEventTriggered((EventTriggerTypes)kTriggeredData.m_eTrigger, true);" the event cannot happen again after that.

5) However: When the next turn for the human comes the popup created in 2) will be shown to the human player. After clicking 'continue' the game goes again into applyEvent (*) and then setTriggerFired. In setTriggerFired the upper part inside "if (!isTriggerFired(kTriggeredData.m_eTrigger))" is skipped but the python callback is called again.

The python callback code was added for FFH according to the comment. My guess is that simply moving it into the "if (!isTriggerFired(kTriggeredData.m_eTrigger))" block will fix the problem. But the code is very complicated, I do not understand all possibilities and I may be completely wrong.

(*) This is just a deduction based on gameflow observation as I did not find the code for it.

Edit: Proposed fix may prevent other events from firing more often than once. Not quite sure how the components in the code are all intended but I will give it another try:
Code:
void CvPlayer::setTriggerFired(const EventTriggeredData& kTriggeredData, bool bOthers, bool bAnnounce)
{
	FAssert(kTriggeredData.m_eTrigger >= 0 && kTriggeredData.m_eTrigger < GC.getNumEventTriggerInfos());

	CvEventTriggerInfo& kTrigger = GC.getEventTriggerInfo(kTriggeredData.m_eTrigger);

[B]	const int global_and_fired = kTrigger.isGlobal() && isTriggerFired(kTriggeredData.m_eTrigger);[/B]

	if (!isTriggerFired(kTriggeredData.m_eTrigger))
	{
		m_triggersFired.push_back(kTriggeredData.m_eTrigger);

		if (bOthers)
		{
			if (kTrigger.isGlobal())
			{
				for (int i = 0; i < MAX_CIV_PLAYERS; i++)
				{
					if (i != getID())
					{

//						GET_PLAYER((PlayerTypes)i).setTriggerFired(kTriggeredData, false, false);
                        if (GET_PLAYER((PlayerTypes)i).isAlive())
                        {
                            GET_PLAYER((PlayerTypes)i).setTriggerFired(kTriggeredData, false, false);
                        }

					}
				}
			}
			else if (kTrigger.isTeam())
			{
				for (int i = 0; i < MAX_CIV_PLAYERS; i++)
				{
					if (i != getID() && getTeam() == GET_PLAYER((PlayerTypes)i).getTeam())
					{
						GET_PLAYER((PlayerTypes)i).setTriggerFired(kTriggeredData, false, false);
					}
				}
			}
		}
	}

//FfH: Modified by Kael 09/25/2008
//	if (!isEmpty(kTrigger.getPythonCallback()))
//	{
//		long lResult;
//		CyArgsList argsList;
//		argsList.add(gDLL->getPythonIFace()->makePythonObject(&kTriggeredData));
//		gDLL->getPythonIFace()->callFunction(PYRandomEventModule, kTrigger.getPythonCallback(), argsList.makeFunctionArgs(), &lResult);
//	}
    if (isAlive()[B] && !global_and_fired[/B])
    {
		if (!CvString(kTrigger.getPythonCallback()).empty())
        {
            long lResult;

            CyArgsList argsList;
            argsList.add(gDLL->getPythonIFace()->makePythonObject(&kTriggeredData));
            argsList.add(getID());	// Player ID
            gDLL->getPythonIFace()->callFunction(PYRandomEventModule, kTrigger.getPythonCallback(), argsList.makeFunctionArgs(), &lResult);
        }
    }
    GC.getGameINLINE().setEventTriggered((EventTriggerTypes)kTriggeredData.m_eTrigger, true);
//FfH: End Modify
 
Thanks Sezren for digging into that. I gave it a try and first test looked good. Also just added your crew promotion code and it is working well so far. Haven't had a chance to look into the withdrawal stuff.
 
Thanks Sezren for digging into that. I gave it a try and first test looked good.

Ok good. Did you use the first version or the edited one? It should probably get more testing though - like a "beta 3". Most of the gameplay code is pretty simple and easy to understand. But these event code parts are not (easy to understand).
 
After revolution,Bannor splited into two factions,but they all called themself Bannor.When another leader asked me to attack the Bannor,I don't know which one he mentioned.

Is there any way to make the new formed faction have different name? Like New Bannor,Fallen Bannor,or Bannor No.2?

Or make AI always use leaders' names rather than factions' names in diplomatic window.
 
After revolution,Bannor splited into two factions,but they all called themself Bannor.When another leader asked me to attack the Bannor,I don't know which one he mentioned.

Is there any way to make the new formed faction have different name? Like New Bannor,Fallen Bannor,or Bannor No.2?

Or make AI always use leaders' names rather than factions' names in diplomatic window.

Cool idea:)
 
Is there any way to make the new formed faction have different name? Like New Bannor,Fallen Bannor,or Bannor No.2?

You can turn on Dynamic Civ Names in the BUG options (under Scoreboard)

Or make AI always use leaders' names rather than factions' names in diplomatic window.

Diplomacy window is handled in the .exe unfortunately. Not sure that I can change that.
 
You can turn on Dynamic Civ Names in the BUG options (under Scoreboard)



Diplomacy window is handled in the .exe unfortunately. Not sure that I can change that.


OK,thank you. And Nice work guys.Looking forward the 27.3 version.
 
This is in MNAI 2.6.3:

2x use of undefined variable 'player':
Code:
def doOrderVsVeilTemple1(argsList):
	iEvent = argsList[0]
	kTriggeredData = argsList[1]
	pPlayer = gc.getPlayer(kTriggeredData.ePlayer)
	pCity = pPlayer.getCity(kTriggeredData.iCityId)
	pCity.changeOccupationTimer(1)
	iOrder = gc.getInfoTypeForString('RELIGION_THE_ORDER')
	iVeil = gc.getInfoTypeForString('RELIGION_THE_ASHEN_VEIL')
	pCity.setNumRealBuilding(gc.getInfoTypeForString('BUILDING_TEMPLE_OF_THE_VEIL'), 0)
	if pCity.isHolyCityByType(iVeil) == False:
		if gc.getGame().getSorenRandNum(100, "Order vs Veil Temple 1") < 50:
			pCity.setHasReligion(iVeil, False, False, False)
	(loopCity, iter) = [B]player[/B].firstCity(false)
	while(loopCity):
		if loopCity.isHasReligion(iOrder):
			loopCity.changeHappinessTimer(5)
		if loopCity.isHasReligion(iVeil):
			loopCity.changeHurryAngerTimer(5)
		(loopCity, iter) = [B]player[/B].nextCity(iter, false)

Edit: Not a bug. See post by 'platyping'. '-1' is superfluous.

Code:
def spellRobGrave(caster):
	CyGame().changeGlobalCounter(1)
	pPlot = caster.plot()
	pPlot.setImprovementType(-1)
	pPlayer = gc.getPlayer(caster.getOwner())
	lList = ['LOW_GOLD', 'HIGH_GOLD', 'SPAWN_SKELETONS', 'SPAWN_SPECTRE']
	if pPlayer.canReceiveGoody(pPlot, gc.getInfoTypeForString('GOODY_GRAVE_TECH'), caster):
		lList = lList + ['TECH']
	sGoody = lList[CyGame().getSorenRandNum(len(lList), "Pick Goody")[B]-1[/B]]
	if sGoody == 'LOW_GOLD':
		pPlayer.receiveGoody(pPlot, gc.getInfoTypeForString('GOODY_GRAVE_LOW_GOLD'), caster)
	if sGoody == 'HIGH_GOLD':
		pPlayer.receiveGoody(pPlot, gc.getInfoTypeForString('GOODY_GRAVE_HIGH_GOLD'), caster)
	if sGoody == 'TECH':
		pPlayer.receiveGoody(pPlot, gc.getInfoTypeForString('GOODY_GRAVE_TECH'), caster)
	if sGoody == 'SPAWN_SKELETONS':
		pPlayer.receiveGoody(pPlot, gc.getInfoTypeForString('GOODY_GRAVE_SKELETONS'), caster)
	if sGoody == 'SPAWN_SPECTRE':
		pPlayer.receiveGoody(pPlot, gc.getInfoTypeForString('GOODY_GRAVE_SPECTRE'), caster)

IMO it should not be giving a tech anyway though as that is usually much more valuable than sanctify/raise skeleton.
 
Assuming TECH is added to the list.
len(lList) will be 5, where lList[0] will be LOW_GOLD and lList[4] will be TECH.

CyGame().getSorenRandNum(len(lList), "Pick Goody") will generate a random number from 0 to 4, which is already correct.
-1 will then reduce this to become a random number from -1 to 3.
lList[0 to 3] will point correctly to LOW_GOLD to SPECTRE.
lList[-1] will actually point to TECH because it is the first entry from the end.
Thus, -1 to 3 will still end up giving equal chance to pick one from the list.

It is not a bug, but definitely a redundant and pointless step.
 
Have had demon champions from hellfire appear on volcano.
Volcano code seems to turn plot into flatlands. Hellfire code checks for flatlands. Other spawn code may be affected as well. For example Hyborem startup code uses "isPeak" which probably returns 'False' (not tested). Likelihood of it happening for Hyborem is very small of course - unlike for hellfire.

--

It is not a bug, but definitely a redundant and pointless step.

Ok, thanks. Tested it again and actually got a tech from it. Edited post.
 
To simplify it further, lList should directly contain ["GOODY_GRAVE_LOW_GOLD", "GOODY_GRAVE_HIGH_GOLD" ... ]

pPlayer.receiveGoody(pPlot, gc.getInfoTypeForString(sGoody), caster)

Then there is no need of a chunk of if statements.
 
I just got an event saying that Beeri Bawl had agreed to allow my (Faeryl Viconia's) city of Essuria return to my control, asking if I would welcome the city back or have it become an independent Khazad empire.

I selected welcome, but then nothign happened.

I found this in PythonErr.log
Code:
Traceback (most recent call last):

  File "CvEventInterface", line 34, in applyEvent

  File "CvEventManager", line 254, in applyEvent

  File "Revolution", line 6181, in joinHumanHandler

KeyError: 1
ERR: Python function applyEvent failed, module CvEventInterface

Note that this is in my modmod with the 2.7beta2.

I did not realize until a few minutes ago that a third beta had been released.
 
I just got an event saying that Beeri Bawl had agreed to allow my (Faeryl Viconia's) city of Essuria return to my control, asking if I would welcome the city back or have it become an independent Khazad empire.

I selected welcome, but then nothign happened.

I found this in PythonErr.log


Thanks for the report! I'll take a look.
 
Is anyone else finding that the civilization switch in the "High to Low" challenge mode isn't activating?

(Just tried out the mod today. The Revolutions integration is excellent!)
 
Top Bottom