[DLL] How to create your own logging function that creates a .csv in the Logs.

Putmalk

Deity
Joined
Sep 26, 2010
Messages
2,652
Location
New York
This tutorial assumes advanced knowledge of C++ and how to navigate the DLL. If you don't understand objects, pointers, and classes you probably won't understand this tutorial.

I'm also not an experienced tutorial writer, so let me know how this is.

If you're modifying the DLL, you may encounter unexplained issues where a function is returning zero (especially if you're multiplying values out using predefined functions and all that jazz :P). Either way, you may not get a compile error, but you'll get a bug in the game, which is just as frustrating.

I'm to help you change that. I'll be using my GetTechValue function as an example.

Here's a snippet of code that'll be useful for what I'm talking about.
Code:
int CvDealAI::GetTechValue(TechTypes eTech, bool bFromMe, PlayerTypes eOtherPlayer)
{
	int iItemValue = 0;
	CvTechEntry* pkTechInfo = GC.getTechInfo(eTech);

	//int iTechCost = GET_TEAM(GET_PLAYER(eOtherPlayer).getTeam()).GetTeamTechs()->GetResearchCost(eTech);
	//int iLeft = GET_TEAM(GET_PLAYER(eOtherPlayer).getTeam()).GetTeamTechs()->GetResearchLeft(eTech);
	//int iTurnsLeft = GET_PLAYER(eOtherPlayer).GetPlayerTechs()->GetResearchTurnsLeft(eTech, false);

	int iResearchCost, iResearchProgress, iResearchRate;

	if(bFromMe)
	{
		iResearchCost = GET_PLAYER(eOtherPlayer).GetPlayerTechs()->GetResearchCost(eTech);
		iResearchProgress = GET_TEAM(GET_PLAYER(eOtherPlayer).getTeam()).GetTeamTechs()->GetResearchProgress(eTech);
		iResearchRate = GET_PLAYER(eOtherPlayer).GetScienceTimes100();
	}
	else
	{
		iResearchCost = m_pPlayer->GetPlayerTechs()->GetResearchCost(eTech);
		iResearchProgress = GET_TEAM(m_pPlayer->getTeam()).GetTeamTechs()->GetResearchProgress(eTech);
		iResearchRate = m_pPlayer->GetScienceTimes100();
	}

	int iResearchLeft = std::max(0, (iResearchCost - iResearchProgress));
	
	int iTechEra = pkTechInfo->GetEra();
	
	if(iResearchRate == 0)
	{
		return INT_MAX;
	}
	iResearchLeft *= 10000;
	int iTurnsLeft = (iResearchLeft / iResearchRate);


	if(iTurnsLeft * iResearchRate < iResearchLeft)
	{
		++iTurnsLeft;
	}

	iTurnsLeft = std::max(1, iTurnsLeft);
	iTurnsLeft = (iTurnsLeft + 99) / 100;


	iItemValue = (iTurnsLeft * /*30*/ GC.getGame().getGameSpeedInfo().getTechCostPerTurnMultiplier()) * (pow((double) std::max(1, iTechEra), (double) /*0.7*/ GC.getTECH_COST_ERA_EXPONENT()));

Obviously, this is an incomplete code because there's a ton more that goes on below (plus iItemValue is ultimately returned).

Now, when I was testing this function, it kept returning 0. I had no idea where the error was or why it happened. To save myself time and frustration, I'll create a logging function to capture those variables and read them out in a nice, formatted spreadsheet.

You'll need to create a function prototype in the corresponding .h file. Mine will look like:
Code:
// Added by Putmalk - Version Five
// Logging
void LogTechValue(PlayerTypes eOtherPlayer, TechTypes eTech, int iTurnsLeft, int iItemValue);

And call the logging function in your function.
Code:
LogTechValue(eOtherPlayer, eTech, iTurnsLeft, iItemValue);

These variables are all defined in my function GetTechValue() before I call the logging function LogTechValue(), but I'll provide a brief overview real quick of what each variable is.

eOtherPlayer - this is a PlayerTypes, an enum that refers to the Player's ID. Basically, it differentiates between player 1 and player 2, for example. OtherPlayer is the player the AI is dealing with.
eTech - this is a TechTypes, an enum that refers to the tech ID. Agriculture is 0, Sailing is 1, and so on.
iTurnsLeft - Calculated integer that tells me how many turns left the player has to research the tech.
iItemValue - Calculated item value, what we're returning from the function.

Now, we define out logging function in the .cpp:

Here's what the whole thing will look like:
Spoiler :

Code:
// Added by Putmalk - Version Five
void CvDealAI::LogTechValue(PlayerTypes eOtherPlayer, TechTypes eTech, int iTurnsLeft, int iItemValue)
{
	if(GC.getLogging() && GC.getAILogging())
	{
		CvString strBaseString;
		CvString ePlayerName;
		CvString eOtherPlayerName;
		CvString eTechName;
		CvString strLogName;
		CvString strTemp;
		CvString strOutBuf;
                CvTechEntry* pkTechInfo = GC.getTechInfo(eTech);

		if(GC.getPlayerAndCityAILogSplit())
		{
			strLogName = "AI_TechDealValue_Log_" + ePlayerName + ".csv";
		}
		else
		{
			strLogName = "AI_TechDealValue_Log.csv";
		}

		FILogFile* pLog;
		pLog = LOGFILEMGR.GetLog(strLogName, FILogFile::kDontTimeStamp);

		// Get the leading info for this line
		strBaseString.Format("%03d, ", GC.getGame().getElapsedGameTurns());
						
		// Player Name
		ePlayerName = m_pPlayer->getCivilizationShortDescription();
		strBaseString += ePlayerName;

		// eOtherPlayer Name
		eOtherPlayerName = GET_PLAYER(eOtherPlayer).getCivilizationShortDescription();
		strBaseString += ", " + eOtherPlayerName;
		
		// Tech Name
		eTechName = pkTechInfo->GetDescription();
		strBaseString += ", " + eTechName;

		// Variables
		strOutBuf = strBaseString;

		strTemp.Format("iTurnsLeft: %d", iTurnsLeft);
		strOutBuf += ", " + strTemp;

		strTemp.Format("iItemValue: %d", iItemValue);
		strOutBuf += ", " + strTemp;
						
		pLog->Msg(strOutBuf);

		OutputDebugString("\n");
		OutputDebugString(strOutBuf);
		OutputDebugString("\n");
	}
}


This is line by line:
Code:
void CvDealAI::LogTechValue(PlayerTypes eOtherPlayer, TechTypes eTech, int iTurnsLeft, int iItemValue)
{
	if(GC.getLogging() && GC.getAILogging())
	{
Here I check to see if the play has enabled logging and AI logging in his config.ini. This is optional but you don't want your users, who don't have logging enabled, to see these spreadsheet, as they're purely for testing. Some of these spreadsheets contain AI information that would be easy to cheat off of.

Code:
CvString strBaseString;
CvString ePlayerName;
CvString eOtherPlayerName;
CvString eTechName;
CvString strLogName;
CvString strTemp;
CvString strOutBuf;
CvTechEntry* pkTechInfo = GC.getTechInfo(eTech);

if(GC.getPlayerAndCityAILogSplit())
{
	strLogName = "AI_TechDealValue_Log_" + ePlayerName + ".csv";
}
else
{
	strLogName = "AI_TechDealValue_Log.csv";
}
Here, I create a bunch of CvString variables. CvString is a custom Firaxis class that's perfect for logging. The amount of strings and their use will vary, but in general you'll want to have these strings declared:

strBaseString - These are the strings that will always be there, regardless of the situation. They're normally reserved for turn count, playername, and the other player name (if you need one).
strLogName - The name of your log that will appear in the Logs folder. Name this something you can identify easily and find.
strTemp - This string is constantly changed to format your final excel spreadsheet. This is constantly reused and replaced, so it's useful for defining the name of your variable and its actual value. More on that below.
strOutBuf - The final string that's printed out, this will be what's finally attached.

I've created a pointer pkTechInfo that's used to get the information of eTech, passed into the logging function. I can use this to get it's name, cost, or whatever else you need to get. Firaxis has defined several functions this class can use.

Finally, I did a check to see if the option to split Player and AI was enabled. If so, the logs are created with the player's name active, if not, then we use a generic .csv. Usually, you'll use the generic, and if you want, you don't have to have this if statement here.


Code:
FILogFile* pLog;
pLog = LOGFILEMGR.GetLog(strLogName, FILogFile::kDontTimeStamp);
GetLog is a function of Firaxis's. This line will open a file to whatever you named your .csv in the block of code above. kDontTimeStamp is an enum of class FILogFile. You don't need to worry about this, just copy this function call and you'll be able to write to your .csv fine.


Code:
// Get the leading info for this line
		strBaseString.Format("%03d, ", GC.getGame().getElapsedGameTurns());
						
		// Player Name
		ePlayerName = m_pPlayer->getCivilizationShortDescription();
		strBaseString += ePlayerName;

		// eOtherPlayer Name
		eOtherPlayerName = GET_PLAYER(eOtherPlayer).getCivilizationShortDescription();
		strBaseString += ", " + eOtherPlayerName;
		
		// Tech Name
		eTechName = pkTechInfo->GetDescription();
		strBaseString += ", " + eTechName;
We begin filling out our base string here.

Format() is the most useful function, we'll use it a lot in a bit. Format allows us to put variable integers in our string, and specify those variables in the same function. The %d (%03d here) operator will pull the variable's value for you. You'll specify the variables with other parameters in Format. The first %d will be replaced by the first variable, the second %d with the second variable, etc. So string.Format("Value: %d + %d = %d", iValue1, iValue2, iSum); would look like: "Value: 1 + 4 = 5" in the spreadsheet. This will only apply for a single cell.

It's a little easier when we don't have to use Format(), as in the case of ePlayerName, eOtherPlayerName, and eTechName. We don't have to use Format() because these values are already strings, thus no conversion is necessary. For these, we'll simply get the correct string (pulled from functions inside their predefined classes). In this case, we'll simply add those variables to the string.

NOTE: to separate cells, we use a "," in the string. This is why we have ", " when adding to strBaseString.

Just for reference, to make it easier:
GC.getGame().getElapsedGameTurns() - Returns the number of game turns, called from the getGame() function in GC object.
m_pPlayer - This was specifically defined in CvDealAI, but it appears elsewhere in the code too. It returns a CvPlayer object. It's almost equivalent to GET_PLAYER(GetPlayer()->GetID()), except this is a pointer and you must use -> instead of the dot operator.
GET_PLAYER(eOtherPlayer) - Returns a CvPlayer object to eOtherPlayer.
pkTechInfo - pointer to a CvTechEntry object.


Code:
// Variables
		strOutBuf = strBaseString;

		strTemp.Format("iTurnsLeft: %d", iTurnsLeft);
		strOutBuf += ", " + strTemp;

		strTemp.Format("iItemValue: %d", iItemValue);
		strOutBuf += ", " + strTemp;
						
		pLog->Msg(strOutBuf);

		OutputDebugString("\n");
		OutputDebugString(strOutBuf);
		OutputDebugString("\n");
Our last segment of code is, in my opinion, the easiest to understand here. We first make strOutBuf (remember, the string that we'll be putting into our spreadsheet) equal to the base string, then we'll start adding onto it via temporary strings.

For each variable, we'll use Format() again to get it's value and place it inside the string. If we don't include the %d, all we'll see is "iItemValue: ", which is useless to us, so we'll place that there. After we've added the variable value to the temp string, we'll add it to the outbuf and then replace it with the next variable.

Finally, we'll add the completed string (which, in my case would look like:
Code:
The Iroquois, Babylon, Guilds, iTurnsLeft: 6, iItemValue: 180
to pLog (our opened file) using the Msg function, passing only the string.

I'd be lying if I pretended to know what OutputDebugString() does, but it's not defined for a final release anyway, so you'll never see it.

The results of our labor, located in Documents/MyGames/Civ V/Logs/AI_TechDealValue_Log.csv:

PYSJUjTl.png
 
Back
Top Bottom