[SDK]::Read/::Write Methods for Custom Classes

Voyhkah

Undead
Joined
Apr 25, 2009
Messages
1,444
Location
Earth
I recently created a new class (Technically a struct, since there's no CvClasses file, but not important) (Called CvBuilding, but that's not important either). How do I make the game load and save my new struct when the player loads and saves the game?
 
Hmm.... I am probably one of the few people who has added an info type to the game. And it has been four or more years since I did that... but I will do my best to remember.

A method exists in the SDK somewhere for saving (and opening) game files. It may be in the Game class or something general like that. If I remember correctly, it directly calls each important info's save method and each info handles its own output and input. So you will have to write a method for your info that handles both reading and writing the important information about the

There is also a general format for outputting specific data types (something like WriteBool(), WriteInt() etc...). Find one of the other info's saving/opening routines for an example.

See if you can find these. If you need some more help let me know and I see if I can have a look at my own info classes that saved stuff...
 
Hmm... Makes sense. I'll try it in the morning. By the way, what did you create?
 
First, create these methods to your CvBuilding class:
Code:
void CvBuilding::write(FDataStreamBase* pStream)
{
...
}

void CvBuilding::read(FDataStreamBase* pStream)
{
...
}

I assume an array of CvBuilding's is held by CvCity (If not - just make the appropriate changes to what I write here)
In that case, in CvCity::write, you should add somewhere (but not immediately at the start of the function), something like:

Code:
pStream->Write(numCvBuildings);
for (int iBuilding=0; iBuilding < numCvBuildings; ++i)
{
    cvBuildings[i]->write(pStream);
}

Of course, replace numCvBuildings and cvBuildings with your own variables.

And in the CvCity::read method, you should add - in the same place - this code:
Code:
pStream->Read(&numCvBuildings);
for (int iBuilding=0; iBuilding < numCvBuildings; ++i)
{
    cvBuildings[i]->read(pStream);
}

Note that the order is important here, since write() writes info items in a certain order to the saved game stream, and read() must read them in the same order.

You can then fill your CvBuilding::write and CvBuilding::read with actually reading and writing the data members of your class. There are many overloaded read()'s and write()'s in the stream, and they should cover most basic types.

One issue you should probably pay attention to is memory allocation and release, if your class has such.
 
I assume an array of CvBuilding's is held by CvCity

You are correct. m_ppBuildings is an array of pointers to CvBuilding objects.

In that case, in CvCity::write, you should add somewhere (but not immediately at the start of the function), something like:

Code:
pStream->Write(numCvBuildings);
for (int iBuilding=0; iBuilding < numCvBuildings; ++i)
{
    cvBuildings[i]->write(pStream);
}

numCvBuildings? What's that? There isn't a stated number of buildings. The array m_ppBuildings is the length of GC.getNumBuildingInfos(), and all the buildings that are not in the city have pointers of null. So I had to modifiy it a bit. Does this look god?

Code:
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
	{
		m_ppBuildings[iBuilding]->write(pStream);
	}

And in the CvCity::read method, you should add - in the same place - this code:
Code:
pStream->Read(&numCvBuildings);
for (int iBuilding=0; iBuilding < numCvBuildings; ++i)
{
    cvBuildings[i]->read(pStream);
}
Code:
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
	{
		m_ppBuildings[iBuilding]->read(pStream);
	}

Good?


Note that the order is important here, since write() writes info items in a certain order to the saved game stream, and read() must read them in the same order.

You can then fill your CvBuilding::write and CvBuilding::read with actually reading and writing the data members of your class. There are many overloaded read()'s and write()'s in the stream, and they should cover most basic types.

One issue you should probably pay attention to is memory allocation and release, if your class has such.

Got it
 
Good, except that you said m_ppBuildings[iBuilding] is NULL for many buildings.
So maybe something like that:

Write:
Code:
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
{
	if (m_ppBuildings[iBuilding])
	{
		pStream->write(true);
		m_ppBuildings[iBuilding]->write(pStream);
	}
	else
	{
		pStream->write(false);
	}
}


Read:
Code:
bool bBuildingExists;
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
{
	pStream->Read(&	bBuildingExists);

	if (bBuildingExists)
	{
		m_ppBuildings[iBuilding] = new CvBuilding();
		m_ppBuildings[iBuilding]->read(pStream);
	}
	else
	{
		SAFE_DELETE(m_ppBuildings[iBuilding]);
	}
}

I assume this is how you create a building? (m_ppBuildings[iBuilding] = new CvBuilding();)
And also make sure to set all pointers to NULL when you first create the array, or you might get crashes.
 
Good, except that you said m_ppBuildings[iBuilding] is NULL for many buildings.
So maybe something like that:

Write:
Code:
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
{
	if (m_ppBuildings[iBuilding])
	{
		pStream->write(true);
		m_ppBuildings[iBuilding]->write(pStream);
	}
	else
	{
		pStream->write(false);
	}
}


Read:
Code:
bool bBuildingExists;
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
{
	pStream->Read(&	bBuildingExists);

	if (bBuildingExists)
	{
		m_ppBuildings[iBuilding] = new CvBuilding();
		m_ppBuildings[iBuilding]->read(pStream);
	}
	else
	{
		SAFE_DELETE(m_ppBuildings[iBuilding]);
	}
}

I assume this is how you create a building? (m_ppBuildings[iBuilding] = new CvBuilding();)
And also make sure to set all pointers to NULL when you first create the array, or you might get crashes.

Aaaaahhhh!!! I just had the EUREKA! moment. I didn't really understand the read/writing before, now I do. It's all about order! I knew order was important, but it's more than that. The names of the variables themselves ARE NOT WRITTEN! Order determines which value goes to which variable!

Anyway, technically, you create a CvBuilding like this:

Code:
m_ppBuildings[iBuilding] = new CvBuilding((BuildingTypes)iBuilding, this);

,but that's not important.

One of the things I'm wondering about is how to write the pointers. Each CvBuilding has a pointer to the city it's in as an instance variable. How could this be saved/loaded? Any ideas?
 
One of the things I'm wondering about is how to write the pointers. Each CvBuilding has a pointer to the city it's in as an instance variable. How could this be saved/loaded? Any ideas?

It doesn't need to write the pointer. Just change the read code I posted to:

Code:
bool bBuildingExists;
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
{
	pStream->Read(&	bBuildingExists);

	if (bBuildingExists)
	{
		m_ppBuildings[iBuilding] = new CvBuilding([B](BuildingTypes)iBuilding, this[/B]);
		m_ppBuildings[iBuilding]->read(pStream);
	}
	else
	{
		SAFE_DELETE(m_ppBuildings[iBuilding]);
	}
}
 
It doesn't need to write the pointer. Just change the read code I posted to:

Code:
bool bBuildingExists;
for (int iBuilding = 0; iBuilding = GC.getNumBuildingInfos(); iBuilding++)
{
	pStream->Read(&	bBuildingExists);

	if (bBuildingExists)
	{
		m_ppBuildings[iBuilding] = new CvBuilding([B](BuildingTypes)iBuilding, this[/B]);
		m_ppBuildings[iBuilding]->read(pStream);
	}
	else
	{
		SAFE_DELETE(m_ppBuildings[iBuilding]);
	}
}

Ah. Looking back, I don't see how I missed that. :blush:

While I'm at it, maybe I should look at CvVillage. I assume the read/write meathods should be called from CvPlot. Like so:

Code:
void CvVillage::read(FDataStreamBase* pStream)
{
	pStream->Read(&m_iX);
	pStream->Read(&m_iY);
	pStream->Read(NUM_YIELD_TYPES, m_piYields);

	//We don't need to read/write worked plots. CvPlot takes care of that//
}

void CvVillage::write(FDataStreamBase* pStream)
{
	pStream->Write(m_iX);
	pStream->Write(m_iY);
	pStream->Write(m_piYields);

	//We don't need to read/write worked plots. CvPlot takes care of that//
}

As you can see from my comments, CvPlot will take care of read/writing working with the m_pWorkingVillage pointer. But how do I read/write a pointer without creating a new object on read?
 
Assuming there can only be one CvVillage per plot (does it come in addition to improvements?), you can have the CvPlot hold a CvVillage (not a pointer), and the CvVillage itself can contain a boolean member of whether the village exists.

And you might want to have the village point to the plot, and then you don't need to hold the x,y coordinates in the village.
 
Assuming there can only be one CvVillage per plot (does it come in addition to improvements?), you can have the CvPlot hold a CvVillage (not a pointer), and the CvVillage itself can contain a boolean member of whether the village exists.

The village is an improvement, just a special one.

And you might want to have the village point to the plot, and then you don't need to hold the x,y coordinates in the village.


Probably a good idea.
 
Ok, let's clear this up: A village is a special improvement that can work other plots. The code for the CvVillage is like this.

Code:
CvVillage::CvVillage(CvPlot* pPlot)
{
	m_pPlot = pPlot;
	pPlot->setVillageOnPlot(this);
	setPlotWorked(AI_getBestPlot());
	setPlotWorked(AI_getBestPlot(AI_getBestPlot()));
	m_piYields = new int[NUM_YIELD_TYPES];
	CvPlot** m_ppWorkingPlots = new CvPlot*[4];

}

bool CvVillage::isWorkingPlot(const CvPlot* pPlot) const
{
	if (pPlot->getWorkingVillage() == this)
	{
		return true;
	}
	return false;
}

void CvVillage::setPlotWorked(CvPlot* pPlot)
{
	pPlot->setWorkingVillage(this);
}

int CvVillage::getNumPlotsWorked() const
{
	int iNumPlotsWorked;
	CvPlot* pPlot;
	for (int iX = -1; iX < 2; iX++)
	{
		for (int iY = -1; iY < 2; iY++)
		{
			if (!((abs(iX == 1) && (abs(iY)))))
			{
				pPlot = GC.getMap().plot(getX() + iX, getY() + iY);
				if (isWorkingPlot(pPlot))
				{
					iNumPlotsWorked++;
				}
			}
		}
	}
	return iNumPlotsWorked;
}

CvPlot* CvVillage::AI_getBestPlot(CvPlot* pIngoreOne, CvPlot* pIgnoreTwo) const
{
	int iTotal[4];
	int iLoop = 0;
	int iHighest = 0;
	CvPlot* pPlotHighestYield;
	CvPlot* pPlot;
	for (int iX = -1; iX < 2; iX++)
	{
		for (int iY = -1; iY < 2; iY++)
		{
			if (!(abs(iX == 1) && (abs(iY) == 1)) && (!((iX == 0) && (iY == 0))))
			{
				pPlot = GC.getMap().plot(getX() + iX, getY() + iY);
				iTotal[iLoop] = (pPlot->getYield(YIELD_FOOD) + pPlot->getYield(YIELD_PRODUCTION) + pPlot->getYield(YIELD_COMMERCE));
				if (iTotal[iLoop] < iHighest)
				{
					iHighest = iTotal[iLoop];
					pPlotHighestYield = pPlot;
				}
				iLoop++;
			}
		}
	}
	return pPlotHighestYield;
}

int CvVillage::calculateVillageYield(YieldTypes eYieldType) const
{
	int iYield;
	CvPlot* pPlot;
	for (int iX = -1; iX < 2; iX++)
	{
		for (int iY = -1; iY < 2; iY++)
		{
			if (!((abs(iX == 1) && (abs(iY)))))
			{
				pPlot = GC.getMap().plot(getX() + iX, getY() + iY);
				if (isWorkingPlot(pPlot))
				{
					iYield += pPlot->getYield(eYieldType);
				}
			}
		}
	}
	iYield += getVillagePlot()->calculateYield(eYieldType, false, true);
	return iYield;
}

void CvVillage::setSelfYield(YieldTypes eYieldType)
{
	int iYield = calculateVillageYield(eYieldType);
	m_piYields[(int)eYieldType] = iYield;
	getVillagePlot()->updateYield();

}

CvPlot* CvVillage::getVillagePlot() const
{
	return GC.getMap().plot(getX(), getY());
}

int CvVillage::getX() const
{
	return getVillagePlot()->getX();
}

int CvVillage::getY() const
{
	return getVillagePlot()->getY();
}

void CvVillage::read(FDataStreamBase* pStream)
{
	int iX;
	int iY;
	pStream->Read(&iX);
	pStream->Read(&iY);
	m_pPlot = GC.getMap().plot(iX,iY);
	pStream->Read(NUM_YIELD_TYPES, m_piYields);

	//We don't need to read/write worked plots. CvPlot takes care of that//
}

void CvVillage::write(FDataStreamBase* pStream)
{
	pStream->Write(getVillagePlot()->getX());
	pStream->Write(getVillagePlot()->getY());
	pStream->Write(m_piYields);

	//We don't need to read/write worked plots. CvPlot takes care of that//
}

A plot has two variables directly pretaining to villages: m_pVillageOnPlot and m_pWorkingVillage. m_pVillageOnPlot is the village that is located on the plot, usually NULL. m_pWorkingVillage is the village that is working the plot, often NULL.

So how do I read/write all this data?
 
Man, you really do have interesting ideas. :cool: And while I'm currently not really able to read any C++, your code looks very accessible indeed. I can't wait to get into SDK modding myself!
 
Let's start at the plot: You have 2 new members - m_pVillageOnPlot and m_pWorkingVillage.

A CvVillage can only be part of a single CvPlot, so you should write that village's data as part of the plot's data.
So in CvPlot::write:

Code:
if (m_pVillageOnPlot)
{
    pStream->Write(true);
    m_pVillageOnPlot->write(pStream);
}
else
{
    pStream->Write(false);
}

And to read it first read a boolean of whether the village exists, and if so - create a new CvVillage and read it. Don't forget to delete the existing one first (use SAFE_DELETE for that) - whether you create a new one or not.

Now, m_pWorkingVillage is trickier, since it might point to another plot's village, so you don't want to save the data again (you don't want to create a copy of the village), and you can't write pointers to the stream (because they will be meaningless after you load it).

In the rest of Civ's code it is done with ID's - each city/unit/whatever has an int ID. In your case, I suggest using the X,Y coordinates of the plot which village is working yours.

Now, since you might read a worked plot before the working plot, that means that you can't immediately point to the village. You can, however, point immediately to the plot since all plots are first allocated and only the read (see CvMap::read). So I suggest that each plot will keep a 'working village plot' of type CvPlot* instead of a 'working village'. You can still access the working village directly (m_pWorkingVillagePlot->getVillage(), or something), and it'll save you the need to make another pass over the plots.

It also means that you only need to save the plot's index, and not the two coordinates.

Then in your CvPlot::write():
Code:
if (m_pWorkingVillagePlot)
{
    int nWorkingVillagePlotIndex = GC.getMap().plotNumINLINE(m_pWorkingVillagePlot->getX_INLINE(), m_pWorkingVillagePlot->getY_INLINE());
    pStream->Write(nWorkingVillagePlotIndex );;
}
else
{
    pStream->Write(-1);
}

And in your CvPlot::read():
Code:
pStream->Read(&nWorkingVillagePlotIndex);
if (nWorkingVillagePlotIndex >= 0)
{
    m_pWorkingVillagePlot = GC.getMap().plotByIndexINLINE(nWorkingVillagePlotIndex);
}
else
{
    m_pWorkingVillagePlot = NULL;
}

As for the CvVillage code, I didn't go over everything there, but a few points:
In the constructor:
Code:
	CvPlot** m_ppWorkingPlots = new CvPlot*[4];
That defines a local variable and allocates memory into it. The pointer will disappear and create a memory leak when you leave the constructor, and more importantly - it will not assign anything to the class member. Remove the "CvPlot**" in the beginning.
BTW - why 4?

Code:
	setPlotWorked(AI_getBestPlot());
	setPlotWorked(AI_getBestPlot(AI_getBestPlot()));

Did you give default values to the parameters of AI_getBestPlot? Otherwise it will not compile.

---
You should really create a destructor for CvVillage in which you release all the memory you allocate in the class, otherwise you have a memory leak.

---
There's no need to read/write or even hold the x/y coordinates of the plot in CvVillage since you hold a pointer directly to the plot.

---
In CvVillage::write:
Code:
pStream->Write(m_piYields);

This should be:
Code:
pStream->Write([B]NUM_YIELD_TYPES[/B], m_piYields);

---

And can you please explain what the village is supposed to do?
 
Ok, a village is like a mini-city, in a way. Villages can work the 4 tiles directly bordering them, hence the '4', and transfer the yields o the tiles they work to themselves. They, in turn, can be worked by cities.
 
By the way, an recommendations on how to implement this?:help:
 
The part about how cities work villages.
 
It can't.
 
Back
Top Bottom