C/C++ Code Snippets / Discussion [EHM 2007]

Discuss all aspects of editing the data and databases in EHM here. Have a question about the EHM Editor, EHM Assistant, editing the .cfg files, hex editing the .dat or .db files? Want to tweak the EHM exe file to change league rules/structure, start date etc? This is the place!
Forum rules
This is the forum to discuss all aspects of editing the EHM data and tweaking the game.

Have a bug or feature request for the EHM Editor? Post them in the EHM Editor thread. Please start a new thread or post in another thread if you have a question about how to use the EHM Editor.

Given the large number of questions on similar topics, we ask that you start a new thread for a new question unless you can locate a similar question in an existing thread. This will hopefully ensure that similar questions do not get buried in large threads.

Useful links: EHM 1 Assistant (Download) | EHM 1 Editor (Download) | EHM 1 Editor Tutorials | Editing Rules & Structures Guide | Converting EHM 2004 / 2005 DBs to EHM 1 | Converting an EHM 2007 DB to EHM 1 | Extra_config.cfg | Import_config.cfg | Player Roles
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

C/C++ Code Snippets / Discussion [EHM 2007]

Post by archibalduk »

I thought I'd share some of the code/scripts that I've written for editing/modifying EHM and for exporting/importing data from/to the .dat database files. Please keep in mind I'm a complete novice when it comes to C/C++ and so the code I have written may not be the best way of writing things and may not be examples of perfect coding practice. However, my scripts do work and as I learn more about the language I hope I'll be able to improve what I've written so far.

PLEASE NOTE: This information is provided to assist you in editing and updating EHM. Please do not use the information in here to dump data from EHM for other purposes / games.

The purpose of posting this information here is to help any other users who might be interested in editing EHM or simply having a poke around in the .dat files. It also provides the opportunity for other users to share any scripts they have written. Maybe, if there are other users interested in using C/C++ to edit EHM, we can help each other improve our code and figure out anything we're struggling with.

To get started you'll need an IDE. This is something that allows you to write your code and to compile it. I have been using Microsoft Visual C++ 2010 Express which can be downloaded from here for free. An alternative to MSVC++ is Code::Blocks - this is another free IDE which I hear is also very good.

Before I share my first snippet, I thought I'd give some step by step instructions on how to create a C/C++ project in MSVC++ and how to paste the code I've written. Perhaps this might help any users who may be interested in trying out C/C++ but are unsure how to get started. These steps should only take about four or five minutes to carry out:


1) Load MSVC++. If you are using Windows Vista or 7 then it is best to run it is administrator because otherwise some of your code may not work (e.g. if you write a script to modify the data in the RAM to change the start date, it won't work unless you are running as admin. You can do this by right-clicking on the MSVC++ shortcut -> Properties -> Compatibility -> tick the box entitled 'Run this program as an administrator.'

2) In MSVC++, click on File -> New -> Project.

3) In the New Project window, select 'CLR Empty Project'. At the bottom of the window, give your project a name (I will call mine Dat Example). You will also see at the bottom of the window details of where your project will be saved (make a mental note of this).

4) Once you have clicked on OK to create your project, you will be presented with a blank window. From the toolbar at the top of the window, click on the Add New Item icon and select Add New Item from the menu:

Image

5) From the Add New Item window, select C++ File (.cpp) and give your .cpp file a name (you'll see the Name box at the bottom of the window). I'll call mine dat_example. Click on OK to create the file.

6) As we will be exporting the data from EHM, we will need to use the EHM database format/structure. This can be downloaded here. Open the downloaded ZIP file and extract the three .h files (database_flags.h, database_types.h and language_types.h) to the location of the .cpp file you created in step #5.

You can find your .cpp file by going to the location of your project (remember in step #3 I said make a mental note of this). For example, my project is stored here: D:\Documents\Visual Studio 2010\Projects\Dat Example\. Within this folder there is another folder of the same name - i.e. D:\Documents\Visual Studio 2010\Projects\Dat Example\[n]Dat Example\[/b]. This is where you will find your .cpp file and this is where you should place the three .h files.

Btw, .h files are C/C++ header files.

7) In MSVC++, click on the Add New Item icon and click on Add Existing Item. Select the three .h files. You can hold down the CTRL key to select all three files at once. After you click on OK you will see the three .h files listed in the Header Files section in the Solution Explorer (left hand side of the main window).

8) Your dat_example.cpp file should be open within the middle section of the window. If it is not then double click its name in the Solution Explorer (left hand side of the main window).

9) Copy and paste the code quoted below into your .cpp file (i.e. by pasting it into the middle section of your window.

10) The code below will convert index.dat into a csv file. You will need to place a copy of index.dat in the same folder as your .cpp file. Grab a copy of index.dat from any EHM database and put it in the correct location. As mentioned in step #6, on my computer, this would be: D:\Documents\Visual Studio 2010\Projects\Dat Example\[n]Dat Example\[/b].

11) Click on Debug -> Start Debugging or simply press F5 on your keyboard to load your script. Once the script has finished, it should say "Press ENTER to close this window." Press ENTER and your script will exit.

12) If you navigate to the location of your .cpp file, you will see a new file named index.csv has been created. Double-click on this file and it should open in Excel (or otherwise open it in Notepad). You will see that the contents of index.dat are now nicely formatted and readable. Hey presto!


By modifying the code below, you could open any of the .dat files in EHM and export all or just some of the data to csv. I have used index.dat for my example because it is a small file and therefore the simplest example.

Here's the code:

Code: Select all

#include <iostream>
#include <fstream>

#pragma pack(1)													// This is essential because the data in the .dat files are byte-swapped
#include "database_types.h"										// The three EHM database format .h files must be called after #pragma pack(1)
#include "database_flags.h"
#include "language_types.h"

using namespace std;											// As we are just using the standard library, we'll use std namespace for ease of reference

int main() {

	fstream dat_infile ("index.dat", ios::in | ios::binary);	// Open index.dat for reading in binary mode. We'll call it dat_infile.
	fstream csv_outfile ("index.csv", ios::out);				// Create a blank index.csv file in text mode. We'll call it csv_outfile.

	struct INDEX dat;											// Within the database_types.h file the structure of index.dat INDEX
																// We'll assign "dat" as the identifier for use with this structure

	// Calculate the length of index.dat and call is dat_filesize:
	dat_infile.seekg (0, ios::end);
	auto dat_filesize = dat_infile.tellg();
	dat_infile.seekg (0, ios::beg);

	if (dat_infile.is_open())									// If index.dat has been successfully opened then perform the following code within the {} braces
	{
		dat_infile.seekg(INDEX_IGNORE_DATA,ios::beg);			// DELETE THIS LINE IF YOU ARE OPENING ANY FILE OTHER THAN INDEX.DAT
																// The first 8 bytes of index.dat are blank and therefore we must skip over these. Hence the reason of the line above.

		// A loop to read the data stream into the buffer one record at a time and save it as a csv file
		while( dat_infile.tellg() < dat_filesize )
		{
			dat_infile.read ((char*)&dat, sizeof(INDEX));
			csv_outfile << dat.filename << "," << dat.file_id << "," << dat.table_sz << "," << dat.version << endl;		// Commas are added between each field as per csv file format. The final field of each record is terminated by a new line - i.e. 'endl'.
		}

		// Close the index.dat and index.csv files
		dat_infile.close();
		csv_outfile.close();
	}

	else														// If index.dat cannot be opened then do the following
	{
		cout << "Unable to open index.dat" << endl;
	}

	cout << "Press ENTER to close this window.";
	cin.get();													// Wait until the user has pressed ENTER before closing the window

	return(0);													// Exit the script / close the window.
}
It's not normally good practice to put quite so many comments in the code, but I've included lots here so that it is easy to understand what everything does. Comments are those parts with // in front of them. The forum struggles a little with the tab spaces I have added with all of the comments and also it doesn't have syntax highlighting (i.e. showing different types of code in different colours). However if you copy and paste it into MSVC++ or Code::Blocks it should look okay.

Here are a couple of notes:

#pragma pack(1)
You must include this at the top of your code. The data in EHM is byte-swapped (the bytes are stored in reverse order) and so you must include the above line in order to ensure that your script accounts for this. This line must be placed before you call your three .h header files.


if (dat_infile.is_open()) { }
This states what the script will do if index.dat (i.e. dat_infile as it is referred to in my script) is successfully opened. You can add an else { } structure afterwards if you want the script to do something if the file couldn't be opened. You'll see I've use the else { } structure to display a message saying that the file couldn't be opened. You don't have to include else { } if you don't want to, but it can help when debugging.


Looping through to the end of the file
In order to read each record at a time, I have use a loop like so:

Code: Select all

while( dat_infile.tellg() < dat_filesize )
dat_filesize is the name I have given to the value that stores the size of the index.dat file (in bytes). I calculated the file size a few lines above the loop like so:

Code: Select all

dat_infile.seekg (0, ios::end);
auto dat_filesize = dat_infile.tellg();
dat_infile.seekg (0, ios::beg);
seekg() places the "get pointer". The get pointer is kind of like the cursor when editing text. You can move the cursor to different places within the text file to read/edit/write from that point. seekg() tells the script from where in the file to start reading. ios::end places the get pointer at the end of the file. Once the pointer at the end of the file, tellg() states how many bytes into the file the seekg() pointer currently is. Thus if you place it at the end of the file then you'll know how many bytes the size of the file is. Once the script has done this, use ios:beg to place the pointer back to the beginning so that we can start reading the file.

The loop of while( dat_infile.tellg() < dat_filesize ) { } loops through the entirety of the file until it reaches the end. Some people use eof() to loop through the file but this doesn't work correctly because it will read the final record in the file twice (because the EOF flag isn't set until after it has reached the end of the file). My advice is to stick with my method of first calculating the file size and then using this in conjunction with tellg() in the loop.


dat_infile.seekg(INDEX_IGNORE_DATA,ios::beg);
Unlike all of the other .dat files, index.dat starts with eight bytes that are not used to store data. Therefore, when you read/write to index.dat you will need to skip over the first eight bytes. For ease of reference, I have added INDEX_IGNORE_DATA to the version of database_types.h that is stored on TBL. You could equally use the following instead of INDEX_IGNORE_DATA:

Code: Select all

dat_infile.seekg(8,ios::beg);
If you adapt the code for use with a different .dat file then you must remove this line.


dat_infile.read ((char*)&dat, sizeof(INDEX));
Within the loop mentioned above, we read through the data each record at a time. The sizeof(INDEX) tells the script how much data to read at once. sizeof(INDEX) returns the size (in bytes) of each record. If you wanted to know the size in bytes of the records in index.dat, you could display this on the screen by adding cout << sizeof(INDEX); somewhere within the code. You don't have to read the file to calculate the size - it is calculated based upon the structure info from the database_types.h file.


csv_outfile << dat.filename << "," << dat.file_id << "," << dat.table_sz << "," << dat.version << endl;
This line saves each record to your csv_outfile (i.e. index.csv). You'll see that I've added commas between each field followed by a new line (endl) at the end of each record. This is so that the data is saved in csv format. You can do whatever you want with this line. For example, you could add spaces, make it display over multiple lines, export just some of the fields, change the order of the fields, etc etc.

The dat. part of each field name refers to the identifier that we gave the INDEX struct earlier on in the code (struct INDEX dat;). The bit after .dat refers to the field names. You will find the field names in database_types.h.


Adapting this code for other .dat files.
This is fairly straightforward to do:

  • Change the reference to index.dat in fstream dat_infile ("index.dat", ios::in | ios::binary); to whichever file you want to open. You can also change the reference index.csv to whatever you want to name your output csv file.
  • Change the reference to INDEX to the name of the struct which relates to the .dat file you want to open. You will find this in the database_types.h file (e.g. CLUBS is used for club.dat). There are two references in my script that you will need to change: struct INDEX dat; and dat_infile.read ((char*)&dat, sizeof(INDEX));.
  • Don't forget to remove the dat_infile.seekg(INDEX_IGNORE_DATA,ios::beg); line!
  • Change the csv_outfile << dat.filename << "," << dat.file_id << "," << dat.table_sz << "," << dat.version << endl; line according to the fields you want to export.


One final note: If you find that you are getting strange symbols being exported instead of numbers then try adding static_cast<int>() to the field names where you save the data to csv_outfile. E.g. csv_outfile << dat.filename << "," << static_cast<int>(dat.file_id) << "," << static_cast<int>(dat.table_sz) << "," << static_cast<int>(dat.version) << endl;


Good luck!
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

Following on from my fairly epic post above, here's a brief bit of info about how to interpret dates in EHM. Within the .dat files, dates are stored as follows:

Code: Select all

struct SI_DATE
{
	SHORT day;
	SHORT year;
	CBOOL leap_year;
};
In other words, the date is stored as:

1) The day of the year (e.g. 1st Feb is the 32nd day of the year);
2) The year itself;
3) Whether or not the year is a leap year (true or false). If the year is a leap year then it affects the day of the year because leap years are 366 days long.

To convert the data to an understandable date, declare the following at the top of your code:

Code: Select all

int monthtable[12]={31,28,31,30,31,30,31,31,30,31,30,31};
short monthdays = 0;
short month = 0;
short day = 0;
In the loop where you are reading data from a dat file, use the following code. I have used staff.dat / struct STAFF in the example below - you will need to adapt it for other .dat files / structs.

Code: Select all

if ( dat.StaffDateOfBirth.leap_year == 0 ) { monthtable[1] = 28; }
else  { monthtable[1] = 29; }

monthdays = 0;
month = 0;
day = 0;

while ( static_cast<short>(dat.StaffDateOfBirth.day) > monthdays ) { monthdays += monthtable[month++]; }
day = static_cast<short>(dat.StaffDateOfBirth.day) - monthdays + monthtable[month-1];
This will return month as the month and day as the day of the month. I have re-set monthdays, month and day as zero because I'm assuming this will be placed in a loop (you will need to reset it each time otherwise you may get weird results as data may carry over from previous records).

So you could then output the date like so:

Code: Select all

csv_outfile << day << "/" << month << "/" << static_cast<int>(dat.StaffDateOfBirth.year);
Next time, I'll try and find some time to post details on how to write your own script to modify the start date in EHM or any other aspect (such as league rules, structures, schedules, etc).

I do have a script for converting data from csv back to .dat but it's not as tidy as it could be. I'll share it in the future once it has been tidied up.
User avatar
Isles22
Junior League
Posts: 15
Joined: Tue Nov 22, 2011 6:10 pm

Re: C/C++ Code Snippets / Discussion

Post by Isles22 »

Hi arch,

I tried your above tutorial and it keeps giving me 100+ errors when I try to debug it.

Most of the errors seem to be related to the database_type.h file. Did you do any editing to yours?

These are the most common errors s get reports on:
database_types.h(76): error C2146: syntax error : missing ';' before identifier 'CONTINENTS_PTR'
database_types.h(76): error C4430: missing type specifier - int assumed. Note: C++ does not support default-int

It looks like for some reason C++ isn't recognizing LONG and DOUBLE etc. as data types.

I'm a beginner when it comes to programming this stuff. I did take programming courses in school but the was 15+ years ago. So most of the syntax I remember from then isn't the same now.

Thanks
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

Yes, I did some editing to my copy of the header files. The original copies posted by SI contained some typos and missing data. Understandably they had other more pressing things to do at the time! :D

I uploaded a corrected version of the files yesterday morning. If you replace the copies you have in your project folder with the updated copies from here then that should solve the errors.

The errors relating to LONG, CONTINENTS_PTR are because the typedef commands weren't written quite correctly IIRC in the original version. The aforementioned updated copy resolves this.

Let me know how you get along. :thup:
User avatar
Isles22
Junior League
Posts: 15
Joined: Tue Nov 22, 2011 6:10 pm

Re: C/C++ Code Snippets / Discussion

Post by Isles22 »

works just fine now, thanks
User avatar
Isles22
Junior League
Posts: 15
Joined: Tue Nov 22, 2011 6:10 pm

Re: C/C++ Code Snippets / Discussion

Post by Isles22 »

I added a little bit of code to put headers into the csv file.

Code: Select all

csv_outfile << "File_Name" << "," << "File_Id" << "," << "Table_Size" << "," << "Version" << endl;     // Add Headers to index.csv file
I know this isn't the most efficient (especially for files with many columns) but it works for this one.
I'll keep plugging away and see if I can get it to read the headings form the database file.
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

Yeah, I add headers to mine too (Access doesn't like it if I try to import CSV without a header).

If you wanted, you could put all of that text within one set of speech marks:

Code: Select all

csv_outfile << "File_Name,File_Id,Table_Size,Version" << endl;
If you can read the headings straight from the struct then I'd love to know. It's a pain when you load up one of the larger .dat files (e.g. Club.dat) because of the sheer number of fields. :brix:
User avatar
Isles22
Junior League
Posts: 15
Joined: Tue Nov 22, 2011 6:10 pm

Re: C/C++ Code Snippets / Discussion

Post by Isles22 »

If there was a text file with a list of all the fields separated by a commas, could that be read in at the start of the csv file?

as an example if there was a file struct_index.txt with
filename,file_id,table_sz,version
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

Yes you could - assuming you want to export every field in that particular dat file (you can just export a few if you want). Although, if you're going to list them like that then you may as well just paste it into your code like we already have.

Here is an example of how you could open your index.csv file. You could adapt this example to open a csv file containing a list of field headers.

Code: Select all

fstream csv_index ("index.csv", ios::in)
The you could use the same while loop I posted above to loop through the file. Within the loop you would do this:

Code: Select all

getline(csv_index, str_filename, ',');
getline(csv_index, str_file_id, ',');
getline(csv_index, str_table_sz, ',');
getline(csv_index, str_version);
The above would convert each portion of comma separated text as a string. As three of the fields in the file are long integers then you would need to convert the strings to long (or int - they're the same) within the same loop:

Code: Select all

int_file_id = atoi(str_file_id.c_str());
int_table_sz = atoi(str_table_sz.c_str());
int_version = atoi(str_version.c_str());
You would need to declare str_filename, str_file_id, etc as strings at the top of your code along with the long integers:

Code: Select all

string str_filename;
string str_file_id;
string str_table_sz;
string str_version;

long int_file_id;
long int_table_sz;
long int_version;
You would also need to include the following at the very top of your code:

Code: Select all

#include <string>
#include <sstream>
The key things in this example are getline() and atoi (ascii to integer).

When I have more time, I'll post the full code and will show how using the above you can re-write this data to the .dat file.
User avatar
Isles22
Junior League
Posts: 15
Joined: Tue Nov 22, 2011 6:10 pm

Re: C/C++ Code Snippets / Discussion

Post by Isles22 »

Is using the ios | binary function going to create problems when opening file that have text in them?

Such as the Club.dat file.
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

Isles22 wrote:Is using the ios | binary function going to create problems when opening file that have text in them?

Such as the Club.dat file.
ios::binary means that the file will be opened in binary mode. Binary mode is where the file is stored in hexadecimal/binary format (such as the .dat files in the database). If you omit the ios::binary command then it will open the file in text mode - this should only be used for things like txt or csv files. It refers to the way the file is encoded - is it plain text like a txt file or is it hexadecimal/binary like a dat file.

So for all of the .dat files you should open them using the ios::binary command. It will work fine with any of the data fields (including the text fields). If you look in my first example with index.dat, you will see that dat.filename is a text field and it works fine when exporting to csv. :)

This helpful (and simple) article can probably explain it better than I can: http://www.cplusplus.com/doc/tutorial/files/
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

Here is a very good introduction to C++: http://www.cplusplus.com/files/tutorial.pdf

http://www.cplusplus.com/ is a very useful site for information about the different libraries and functions in C++ and the link above is almost like a short introductory book to the language.
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

I've uploaded here a tool that updates index.dat. The index.dat file keeps track of the number of records stored in each of the .dat files in the EHM database.

If you want to write a script that adds or removes records to one of the .dat files then you'll need to update index.dat. In case anybody is interested in writing something that adds/remove records, I thought I'd share the source code of my Index.dat Updater.

An important note before I share the code: I found that the index.dat file fills the blank bytes in the filename field with certain characters (if you open index.dat in a hex editor you'll see what I mean). I tried generating my own index.dat but EHM doesn't like it (I get some database error) - it seems the certain characters have to be present in order for it to work. So rather than generate the index.dat file from scratch, my script simply updates the table_sz field (this is the field that keeps track of the number of records in each .dat file). The script updates that particular field by skipping through index.dat to the location of each table_sz field in each record.

Here's the code:

Code: Select all

#include <iostream>
#include <fstream>
#include <string>

#pragma pack(1)
#include "database_flags.h"
#include "database_types.h"
#include "language_types.h"

using namespace std;

const string IndexedFile[] = { "club.dat", "nat_club.dat", "colour.dat", "continent.dat", "nation.dat", "officials.dat", "stadium.dat", "staff.dat", "nonplayer.dat", "player.dat", "staff_comp.dat", "city.dat", "club_comp.dat", "nation_comp.dat", "first_names.dat", "second_names.dat", "staff_history.dat", "staff_comp_history.dat", "club_comp_history.dat", "nation_comp_history.dat", "affiliations.dat", "retired_numbers.dat", "states_provinces.dat", "injuries.dat", "staff_preferences.dat", "currencies.dat", "club_records.dat", "club_histories.dat", "drafts.dat", "drafted_players.dat", "player_rights.dat", "stage_names.dat", "staff_languages.dat", "player_info.dat", "staff_info.dat" };
long Size[34];

long datSize(string datFileName) {
	fstream datFile (datFileName, ios::in | ios::binary);

	long FileLength = 0L;
	long Records = 0L;

	if (datFile.is_open()) {
		datFile.seekg (0, ios::end);
		FileLength = static_cast<long>(datFile.tellg());
		datFile.seekg (0, ios::beg);
	}

	else { cout << "ERROR: Unable to open " << datFileName << "." << endl; }

	return FileLength;
}

int CalcIndex() {
	cout << "1) Calculating file sizes..." << endl;

	Size[0] = datSize(IndexedFile[0]) / sizeof(CLUBS);
	Size[1] = datSize(IndexedFile[1]) / sizeof(CLUBS);
	Size[2] = datSize(IndexedFile[2]) / sizeof(COLOURS);
	Size[3] = datSize(IndexedFile[3]) / sizeof(CONTINENTS);
	Size[4] = datSize(IndexedFile[4]) / sizeof(NATIONS);
	Size[5] = datSize(IndexedFile[5]) / sizeof(OFFICIALS);
	Size[6] = datSize(IndexedFile[6]) / sizeof(ARENAS);
	Size[7] = datSize(IndexedFile[7]) / sizeof(STAFF);
	Size[8] = datSize(IndexedFile[8]) / sizeof(NON_PLAYERS);
	Size[9] = datSize(IndexedFile[9]) / sizeof(PLAYERS);
	Size[10] = datSize(IndexedFile[10]) / sizeof(STAFF_COMPS);
	Size[11] = datSize(IndexedFile[11]) / sizeof(CITIES);
	Size[12] = datSize(IndexedFile[12]) / sizeof(CLUB_COMPS);
	Size[13] = datSize(IndexedFile[13]) / sizeof(CLUB_COMPS);
	Size[14] = datSize(IndexedFile[14]) / sizeof(NAMES);
	Size[15] = datSize(IndexedFile[15]) / sizeof(NAMES);
	Size[16] = datSize(IndexedFile[16]) / sizeof(STAFF_HISTORIES);
	Size[17] = datSize(IndexedFile[17]) / sizeof(STAFF_COMP_HISTORIES);
	Size[18] = datSize(IndexedFile[18]) / sizeof(CLUB_COMP_HISTORIES);
	Size[19] = datSize(IndexedFile[19]) / sizeof(CLUB_COMP_HISTORIES);
	Size[20] = datSize(IndexedFile[20]) / sizeof(AFFILIATIONS);
	Size[21] = datSize(IndexedFile[21]) / sizeof(RETIRED_NUMBERS);
	Size[22] = datSize(IndexedFile[22]) / sizeof(STATES_PROVINCES);
	Size[23] = datSize(IndexedFile[23]) / sizeof(INJURIES);
	Size[24] = datSize(IndexedFile[24]) / sizeof(STAFF_PREFERENCES);
	Size[25] = datSize(IndexedFile[25]) / sizeof(CURRENCIES);
	Size[26] = datSize(IndexedFile[26]) / sizeof(DB_CLUB_RECORDS);
	Size[27] = datSize(IndexedFile[27]) / sizeof(CLUB_HISTORIES);
	Size[28] = datSize(IndexedFile[28]) / sizeof(DRAFTS);
	Size[29] = datSize(IndexedFile[29]) / sizeof(DRAFTED_PLAYERS);
	Size[30] = datSize(IndexedFile[30]) / sizeof(DB_PLAYER_RIGHTS);
	Size[31] = datSize(IndexedFile[31]) / sizeof(STAGE_NAMES);
	Size[32] = datSize(IndexedFile[32]) / sizeof(STAFF_LANGUAGES);
	Size[33] = datSize(IndexedFile[33]) / sizeof(DB_PLAYER_INFO);
	Size[34] = datSize(IndexedFile[34]) / sizeof(DB_STAFF_INFO);

	cout << "File size calculation complete." << endl << endl;

	return 0;
}

int WriteIndex() {
	cout << "2) Updating index.dat..." << endl;

	fstream datIndex ("index.dat", ios::in | ios::out | ios::binary);
	
	int pos = 63;

	if (datIndex.is_open()) {

		datIndex.seekp(pos, ios::beg);

		for (int i = 0; i <= 34; ++i) {
			datIndex.seekp(pos, ios::beg);

			datIndex.write(reinterpret_cast<char*>(&Size[i]), 4);

			pos = pos + 63;
		}

		datIndex.close();

		cout << "Index.dat successfully updated." << endl;
	}

	else { cout << "ERROR: Unable to open index.dat." << endl; }
	
	return 0;
}

int main() {
	cout << "http://www.ehmtheblueline.com/" << endl << endl
		 << "Index.dat Updater for Eastside Hockey Manager 2007" << endl
		 << "Version 1.0" << endl
		 << "By Archibalduk" << endl << endl
		 << "Please wait..." << endl << endl;

	CalcIndex();
	WriteIndex();

	cout << endl << "UPDATE COMPLETE." << endl << endl << "Press ENTER to close this window." << endl;
	cin.get();
	return 0;
}
You'll see in the main() function that there are two functions called:


1) CalcIndex()

This function goes through each of the .dat files and calculates the number of records that exist in each file. This can be done by calculating the size (in bytes) of one record in each file - you can do this by sizeof(<Struct Name>). For example, staff.dat uses the STAFF struct so you would use sizeof(STAFF) to find out the size of one record. The size of a record is fixed at that size. Now you know the size of one record, you can count the number of records by dividing the .dat file's file size by the size of the record. For example you would divide the size of staff.dat divided by sizeof(STAFF).

CalcIndex() calls the datSize() function for each .dat file. datSize() calculates the file size for each .dat file, returns it to CalcIndex() and divides it by the size of the relevant struct. The result is then stored in the Size[] array.


2) WriteIndex()

Once CalcIndex() has calculated the number of records in each .dat file and stored them in the Size[] array, the WriteIndex() function is called. This function writes the record numbers to index.dat.

This function skips to every 63rd byte in the index.dat file using a for loop (every 63rd byte is where the table_sz field is located for each record). You will see the pos integer increases by 63 each time in the loop and then seekp moves the write pointer (or 'put' pointer) to the next table_sz field (in a similar way to seekg in my first post). Once the write/put pointer has been moved to the correct place, the new record count is written to the file: datIndex.write(reinterpret_cast<char*>(&Size[ i ]), 4);

The write command works like this:

write( (char*)<POINTER TO DATA TO BE WRITTEN>, <SIZE OF DATA TO BE WRITTEN )

You can use reinterpret_cast<char*>() to point to the variable/array being written, you use the & symbol to point to the address rather than the value itself. As each member of the Size[] array is four bytes in size (because an integer is 4 bytes) this is why I put 4 as the second element of write(). Instead of 4, I probably could have written sizeof(Size[ i ]) instead.

Here's a little more information about write(): http://www.cplusplus.com/reference/iost ... eam/write/
speedy_gonzales
Junior League
Posts: 22
Joined: Sat Sep 11, 2010 11:49 am
Location: Germany

Re: C/C++ Code Snippets / Discussion

Post by speedy_gonzales »

G'day fellas,
Switching from Nino's thread to here...
Alrighty, what I try to do are two things:
first, I'd like to export the nameset used for the regens, modify it (i.e remove all stupid entries and add several more) and re-import this to the resp. dat files.
this should be fairly straightforward!

Then, second, I'd like to create a mass-editing solution, mainly to generate new players (for ex. to populate some junior leagues).
I thought of entering first all relevant data into csv file (name, DoB, attributes and so on) , and load this file into the resp. dat files.
This seems to be more consuming, since to my understanding I need to:
- update the first_names.dat, second_names.dat and staff.dat; and who knows what else :)...
- update index. dat
- pay attention to the ordering of some tables
- think about unicode translations
- maybe history

But it shouldn't be tooo tricky, just some try/error learning

Well, Archi, if you already got some coding in this direction, I'd be very happy if I could have a look at it :(). Otherwise I'm working towards these goals, and would of course be willing to share my results.
Cheers from rainy Sydney!
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

It's nice to see a fellow user interested in coding tools for EHM! =D>

I have some code I can share with you which imports a CSV file back into the database. I'm finding that CSV isn't useful when importing text back into the DAT files because if the text has a comma in it then it is treated as a field/column separator (delimiter). I'm finding that when dealing with text, it is better to use a tab delimited TXT file. Thankfully you can easily open, edit and save these files in Excel. Both can be very easily processed in C/C++ (you just use '/t' instead of ',' as a delimiter in your code).

If you want to update the namesets (i.e. first_names.dat and second_names.dat), there are some important things to keep in mind (apologies if I'm stating the obvious - I don't know how much you know about the structure of the DAT files):

1) Many different files point to the nameset DAT files; not only staff.dat but also DAT files relating to team records, staff competitions, officials, etc. So if you want to make changes to the namesets then you'll also need to update these files (i.e. updating the name ID pointers). To me this sounds like a big and complex task, but perhaps you know what you're doing more than I (I'm a real novice).

2) You won't be able to remove names if they're in use by any of the linked DAT files (e.g. staff.dat, etc). If you do then you'll muddle up all of the names used in game. There are a lot of duplicate names in the namesets and so you could consolidate these - so long as you update all pointers and you update the name count fields in the namesets.

I have code for an index.dat updater which I can share with you.

Generating new staff should be fairly straightforward. You'll need to add a new entry to staff.dat as well as all of the linked DAT files (e.g. player.dat, player_info.dat, staff_info.dat, etc). If you want to contract them to a team then you'll also need to update the squad size field in the clubs.dat file. You'll also need to update the name counts in the nameset DAT files.

I'm interested in a relatively similar project. However, rather than adding new players, I'm looking to update existing players using Excel and enabling these updates to be shared with any database (so long as the players exist in the database).

I'll find some time this weekend to post some code for you.
speedy_gonzales
Junior League
Posts: 22
Joined: Sat Sep 11, 2010 11:49 am
Location: Germany

Re: C/C++ Code Snippets / Discussion

Post by speedy_gonzales »

hehe, gotta take this to the next level, love this game soo much.

anyway... to 1).... yeah, I feared sth like that.
But in the end it's just about unravelling all the connections.
Now I set sth up that writes new names in first/second names and updates the other files. As said, some try and error should help building a proper code.
So, in the end, my goal is would be a kind of a light-weight text editor, where you enter your data in a text file (even better than excel, imho) and it gets transfered into all the dat files. Useful for mass-editing...
Not sure how complicated it's going to be, but tbh I do have some experience (although not gaming stuff). Let's see...

However, I look forward to some more of your snippets:)
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

As promised, here is the source code for my Index.dat Updater. As with the first snippet, I'm going to provide some explanation on how it works. For anybody reading this who is unsure how to use this code or how to add the required .h header files, read the first post in this thread because it explains how to get it all set up in Microsoft Visual C++.

Here it is:

Code: Select all

#include <iostream>
#include <fstream>
#include <string>

#pragma pack(1)
#include "database_flags.h"
#include "database_types.h"
#include "language_types.h"

using namespace std;

const string IndexedFile[] = { "club.dat", "nat_club.dat", "colour.dat", "continent.dat", "nation.dat", "officials.dat", "stadium.dat", "staff.dat", "nonplayer.dat", "player.dat", "staff_comp.dat", "city.dat", "club_comp.dat", "nation_comp.dat", "first_names.dat", "second_names.dat", "staff_history.dat", "staff_comp_history.dat", "club_comp_history.dat", "nation_comp_history.dat", "affiliations.dat", "retired_numbers.dat", "states_provinces.dat", "injuries.dat", "staff_preferences.dat", "currencies.dat", "club_records.dat", "club_histories.dat", "drafts.dat", "drafted_players.dat", "player_rights.dat", "stage_names.dat", "staff_languages.dat", "player_info.dat", "staff_info.dat" };
long Size[34];

long datSize(string datFileName) {
	fstream datFile (datFileName, ios::in | ios::binary);

	long FileLength = 0L;
	long Records = 0L;

	if (datFile.is_open()) {
		datFile.seekg (0, ios::end);
		FileLength = static_cast<long>(datFile.tellg());
		datFile.close();
	}

	else { cout << "ERROR: Unable to open " << datFileName << "." << endl; }

	return FileLength;
}

int CalcIndex() {
	cout << "1) Calculating file sizes..." << endl;

	Size[0] = datSize(IndexedFile[0]) / sizeof(CLUBS);
	Size[1] = datSize(IndexedFile[1]) / sizeof(CLUBS);
	Size[2] = datSize(IndexedFile[2]) / sizeof(COLOURS);
	Size[3] = datSize(IndexedFile[3]) / sizeof(CONTINENTS);
	Size[4] = datSize(IndexedFile[4]) / sizeof(NATIONS);
	Size[5] = datSize(IndexedFile[5]) / sizeof(OFFICIALS);
	Size[6] = datSize(IndexedFile[6]) / sizeof(ARENAS);
	Size[7] = datSize(IndexedFile[7]) / sizeof(STAFF);
	Size[8] = datSize(IndexedFile[8]) / sizeof(NON_PLAYERS);
	Size[9] = datSize(IndexedFile[9]) / sizeof(PLAYERS);
	Size[10] = datSize(IndexedFile[10]) / sizeof(STAFF_COMPS);
	Size[11] = datSize(IndexedFile[11]) / sizeof(CITIES);
	Size[12] = datSize(IndexedFile[12]) / sizeof(CLUB_COMPS);
	Size[13] = datSize(IndexedFile[13]) / sizeof(CLUB_COMPS);
	Size[14] = datSize(IndexedFile[14]) / sizeof(NAMES);
	Size[15] = datSize(IndexedFile[15]) / sizeof(NAMES);
	Size[16] = datSize(IndexedFile[16]) / sizeof(STAFF_HISTORIES);
	Size[17] = datSize(IndexedFile[17]) / sizeof(STAFF_COMP_HISTORIES);
	Size[18] = datSize(IndexedFile[18]) / sizeof(CLUB_COMP_HISTORIES);
	Size[19] = datSize(IndexedFile[19]) / sizeof(CLUB_COMP_HISTORIES);
	Size[20] = datSize(IndexedFile[20]) / sizeof(AFFILIATIONS);
	Size[21] = datSize(IndexedFile[21]) / sizeof(RETIRED_NUMBERS);
	Size[22] = datSize(IndexedFile[22]) / sizeof(STATES_PROVINCES);
	Size[23] = datSize(IndexedFile[23]) / sizeof(INJURIES);
	Size[24] = datSize(IndexedFile[24]) / sizeof(STAFF_PREFERENCES);
	Size[25] = datSize(IndexedFile[25]) / sizeof(CURRENCIES);
	Size[26] = datSize(IndexedFile[26]) / sizeof(DB_CLUB_RECORDS);
	Size[27] = datSize(IndexedFile[27]) / sizeof(CLUB_HISTORIES);
	Size[28] = datSize(IndexedFile[28]) / sizeof(DRAFTS);
	Size[29] = datSize(IndexedFile[29]) / sizeof(DRAFTED_PLAYERS);
	Size[30] = datSize(IndexedFile[30]) / sizeof(DB_PLAYER_RIGHTS);
	Size[31] = datSize(IndexedFile[31]) / sizeof(STAGE_NAMES);
	Size[32] = datSize(IndexedFile[32]) / sizeof(STAFF_LANGUAGES);
	Size[33] = datSize(IndexedFile[33]) / sizeof(DB_PLAYER_INFO);
	Size[34] = datSize(IndexedFile[34]) / sizeof(DB_STAFF_INFO);

	cout << "File size calculation complete." << endl << endl;

	return 0;
}

int WriteIndex() {
	cout << "2) Updating index.dat..." << endl;

	fstream datIndex ("index.dat", ios::in | ios::out | ios::binary);
	
	int pos = 63;

	if (datIndex.is_open()) {

		datIndex.seekp(pos, ios::beg);

		for (int i = 0; i <= 34; ++i) {
			datIndex.seekp(pos, ios::beg);

			datIndex.write(reinterpret_cast<char*>(&Size[i]), 4);

			pos = pos + 63;
		}

		datIndex.close();

		cout << "Index.dat successfully updated." << endl;
	}

	else { cout << "ERROR: Unable to open index.dat." << endl; }
	
	return 0;
}

int main() {
	cout << "http://www.ehmtheblueline.com/" << endl << endl
		 << "Index.dat Updater for Eastside Hockey Manager 2007" << endl
		 << "Version 1.0" << endl
		 << "By Archibalduk" << endl << endl
		 << "Please wait..." << endl << endl;

	CalcIndex();
	WriteIndex();

	cout << endl << "UPDATE COMPLETE." << endl << endl << "Press ENTER to close this window." << endl;
	cin.get();
	return 0;
}
Index.dat keeps track of the number of records stored in each of the dat files in the database. Therefore, if you alter the number of records in any of the dat files, you have to update index.dat file. Index.dat stores four fields of data per dat file:

Code: Select all

struct INDEX
{
CHAR filename[ STANDARD_TEXT_LENGTH ]; // Data filename
LONG file_id;	 // Data filename ID number
LONG table_sz;	 // Data file table size
LONG version;	 // Version information (not used)
};

Before I talk through the code, here are three important notes:

1) When experimenting, I tried to generate a new index.dat from scratch. However, this didn't work (EHM would crash) presumably because there are some unusual characters stored in each of the filename fields. These characters appear as blank text but can be seen if you look at index.dat in a hex editor. Therefore, I wrote the tool to update an existing index.dat file rather than generating a new one from scratch.

2) Another important thing to keep in mind is that the first eight bytes in index.dat are trash (but they have to be there). Therefore, when you read/write to index.dat you will need to skip over the first eight bytes. For ease of reference, I have added INDEX_IGNORE_DATA to the version of database_types.h that is hosted on TBL.

3) The table_sz field in the index.dat is the only field we need to update when updating index.dat. This field stores the number of records held in each of the .dat files.


How it works

In order to try and make things a little more readable, I put a list of the relevant .dat files in an array called IndexedFile[].

The first function called is CalcIndex(). Within this function, we go through each file listed in the IndexedFile[] array and send it to the datSize() function:

Code: Select all

Size[0] = datSize(IndexedFile[0]) / sizeof(CLUBS);
The datSize() function opens the dat file sent to it and navigates the get pointer to the end of the file:

Code: Select all

datFile.seekg (0, ios::end);
Once navigated to the end of the file, it uses the tellg() function to find out the location of the get pointer. For example, if the final byte at the end of the file is the 100th byte then it will return 100 as the value. So by navigating to the end of the file, you can calculate how large the file is in bytes:

Code: Select all

FileLength = static_cast<long>(datFile.tellg());
The datSize() function then returns the file size to the CalcIndex() function. So we're returned back to where we were:

Code: Select all

Size[0] = datSize(IndexedFile[0]) / sizeof(CLUBS);
Once we have the file size, it is divided by the size of the struct of the relevant dat file. The size of any record in the database is fixed according to the data types in the struct. For example, if you have a struct consisting of two LONGs (2x 4 bytes) and one CHAR (1x 1 byte) then the size of the struct / each record is 9 bytes (8+1 = 9).

So by dividing the size of a dat file by the size of the relevant struct then you will calculate how many records are stored in the dat file. This result is then allocated to the Size[] array.

Once the CalcIndex() function is finished, you then need to call the WriteIndex() function. This loops through each element of the Size[] array and writes it to the table_sz field of each record in index.dat. The pos variable, which controls where to write each element of Size[] in index.dat (using the put pointer), increases by 63 with each iteration of the loop:

Code: Select all

	for (int i = 0; i <= 34; ++i) {
			datIndex.seekp(pos, ios::beg);    // Place the put pointer in the correct location in index.dat

			datIndex.write(reinterpret_cast<char*>(&Size[i]), 4);    // Write the value in Size[]

			pos = pos + 63;    // Increment the pos variable by 63 ready for the next iteration of the loop
		}
To find out where in the index.dat each table_sz field is located, I calculated it by adding up the data types contained within the struct - and also by taking into account the 8 bytes of trash at the beginning of the index.dat file (which by happy coincidence means that the table_sz field is located at every 63rd byte). So this is why I increment by 63 for each iteration.

This table shows the size of the common data types:

[table][tr][th]Data Type[/th][th]Size (Bytes)[/th][/tr]
[tr][td]char[/td][td]1[/td][/tr]
[tr][td]bool[/td][td]1[/td][/tr]
[tr][td]short[/td][td]2[/td][/tr]
[tr][td]int[/td][td]4[/td][/tr]
[tr][td]long[/td][td]4[/td][/tr]
[tr][td]float[/td][td]4[/td][/tr]
[tr][td]double[/td][td]8[/td][/tr]
[tr][td]long double[/td][td]8[/td][/tr][/table]


Applying the above table to the struct of index, here is a table that shows how large each field of the struct is as well as the total size:

[table][tr][th]Field Name[/th][th]Type[/th][th]Size (Bytes)[/th][/tr]
[tr][td]filename[/td][td]Array of 51 CHARs[/td][td]51[/td][/tr]
[tr][td]file_id[/td][td]LONG[/td][td]4[/td][/tr]
[tr][td]table_sz[/td][td]LONG[/td][td]4[/td][/tr]
[tr][td]version[/td][td]LONG[/td][td]4[/td][/tr]
[tr][td][/td][td]TOTAL:[/td][td]63[/td][/tr][/table]


And this is how the beginning of the file looks (based upon the table above and the 8 bytes of trash at the beginning of the file):

[table][tr][th]Location (Bytes)[/th][th]Record Number[/th][th]Field Name[/th][th]Size (Bytes)[/th][/tr]
[tr][td]0[/td][td]N/A[/td][td]TRASH[/td][td]8[/td][/tr]
[tr][td]8[/td][td]0[/td][td]filename[/td][td]51[/td][/tr]
[tr][td]59[/td][td]0[/td][td]file_id[/td][td]4[/td][/tr]
[tr][td]63[/td][td]0[/td][td]table_sz[/td][td]4[/td][/tr]
[tr][td]67[/td][td]0[/td][td]version[/td][td]4[/td][/tr]
[tr][td]71[/td][td]1[/td][td]filename[/td][td]51[/td][/tr]
[tr][td]122[/td][td]1[/td][td]file_id[/td][td]4[/td][/tr]
[tr][td]126[/td][td]1[/td][td]table_sz[/td][td]4[/td][/tr]
[tr][td]130[/td][td]1[/td][td]version[/td][td]4[/td][/tr]
[tr][td]134[/td][td]2[/td][td]filename[/td][td]51[/td][/tr]
[tr][td]185[/td][td]2[/td][td]file_id[/td][td]4[/td][/tr]
[tr][td]189[/td][td]2[/td][td]table_sz[/td][td]4[/td][/tr]
[tr][td]193[/td][td]2[/td][td]version[/td][td]4[/td][/tr]
[tr][td]197[/td][td]3[/td][td]filename[/td][td]51[/td][/tr]
[tr][td]248[/td][td]3[/td][td]file_id[/td][td]4[/td][/tr]
[tr][td]252[/td][td]3[/td][td]table_sz[/td][td]4[/td][/tr]
[tr][td]256[/td][td]3[/td][td]version[/td][td]4[/td][/tr][/table]

So you can see that the table_sz field is located at every 63rd byte (e.g. 63, 126, 189, 252, etc).

One final note: You could probably merge the CalcIndex() and WriteIndex() functions into one function which you could then add to your own code (along with the datSize() function) and call it when you need to update index.dat.

I'll also post some code relating to converting csv/txt to dat this weekend.
speedy_gonzales
Junior League
Posts: 22
Joined: Sat Sep 11, 2010 11:49 am
Location: Germany

Re: C/C++ Code Snippets / Discussion

Post by speedy_gonzales »

OK, I see..many thanks for the code. I'm playing with it right away.
I'm wondering how long it will take me to get the things done I want to do.. ah well, I just give it a try... eventually something has to come out of it:)
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

Just to say I haven't forgotten about posting some sample code regarding converting csv->dat; I just haven't been able to find the time quite yet. :nerd:
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion

Post by archibalduk »

As promised, here is an example of how you can export data from the database to csv and then reimport it following editing. This is the basic principal that all of my editors/updaters follow. The example below will export the retired_numbers.dat file to csv. You can then edit it using Excel and then reimport it using the same script. The example also updates the index.dat because this has to be done whenever you alter the number of records stored in any of the dat files.

Rather than post a huge tutorial, I have included lots of comments within the code itself. It looks a bit of a mess below, but if you copy and paste it into your IDE (e.g. MS Visual C++ Express 2010 or CodeBlocks) then it will become clearer because the different types of code/comments will appear in different colours (aka syntax highlighting).

Note that you will need to include database_types.h with the below. I explained in the very first post of this thread how to add this to your project within the context of MS Visual C++ Express. Then you can just copy and paste the below into your IDE and compile:

Code: Select all

#define _WIN32_WINNT _WIN32_WINNT_WINXP // Windows XP compatibility macro
#pragma pack(1)	// This is needed in order to ensure that the data is read and written to the database files in the correct fashion. Without this, you will get weird results.

// Include the various standard library templates which we'll need:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

// Include the database_types.h header file so that we can read the structs of the files in the database:
#include "database_types.h"

// Use the std namespace because it makes things easier to read for such a simple programme:
using namespace std;

// Function prototypes. By declaring your functions here, you can then put the full function below main(). It helps keeps things a bit tidier.
char DetectDelimiter();
string ASCII(string &, bool);
bool ExportData(const char &);
bool ImportData(const char &);
long IndexSize(char [], unsigned int);
bool IndexUpdate();
void ParseCSV(string[], const int &, const string &, const char &);

// Main programme routine. This is where the programme starts:
int main()
{
	// Before we start, let's detect what csv delimiter is used by the user's computer settings:
	const char delimiter = DetectDelimiter();

	cout << "Retired numbers importer / exporter example" << endl
		 << "By Archibalduk" << endl
		 << endl
		 << "http://www.ehmtheblueline.com" << endl
		 << endl
		 << "Choose from one of the following options:" << endl
		 << endl
		 << "E: Export data from retired_numbers.dat -> csv" << endl
		 << "I: Import data from retired_numbers.csv -> dat" << endl
		 << endl
		 << "Enter your selection: ";

	// Take a single character input from the user and store it in option_char:
	char option_char;
	cin >> option_char;

	// The toupper() function converts any given char to uppercase. This means that you can just check for a capital letter if you use this in your if statement.
	// In other words it means that you don't have to check for both lower and upper case characters individually - you can just check for uppercase which will cover both.
	// I.e. you can do if(toupper(option_char) == 'E') rather than if(option_char == 'e' || option_char == 'E') - it just makes things a little simpler.
	if(toupper(option_char) == 'E')
	{
		// This if statement calls/runs the ExportData() function and then checks whether true is returned.
		// You'll see that we pass the delimiter to the function. This is so we can access the delimiter char within the function (because the delimiter has local scope).
		if(ExportData(delimiter) == true)
			cout << endl << "Data successfully exported." << endl;
		else
			cout << endl << "An error was encountered." << endl;
	}
	else
	{
		// This if statement calls/runs the ImportData() function and then checks whether true is returned.
		// You'll see that we pass the delimiter to the function. This is so we can access the delimiter char within the function (because the delimiter has local scope).
		if(ImportData(delimiter) == true)
		{
			cout << "Data successfully imported." << endl;

			// If the retired numbers have been successfully added then the index.dat file needs to be updated:
			IndexUpdate();
		}
		else
			cout << endl << "An error was encountered." << endl << "Process aborted." << endl;
	}

	// We're all done, so let's tell the user to exit the programme:
	cout << endl << "Press ENTER to close this window.";
	
	// Wait for the user to press enter...
	cin.ignore();
	cin.get();
	// ...Then exit the programme
	return 0;
}

// Depending on your computer's settings, you will either use a comma or a semicolon as the CSV delimiter/separator.
// This function will detect the delimiter used on your system:
char DetectDelimiter()
{
	return (use_facet<numpunct<char>>(cout.getloc()).decimal_point() == '.') ? ',' : ';';
}

// Convert SI Font text to normal text. This ensures that we get the correct characters when we export the text to csv (which is ASCII).
// We also strip caron letters to normal letters as well as replacing any commas/semicolons with underscores to prevent it being confused as a csv delimiter. 
string ASCII(string &ehm_text, bool lower_case)
{
	// If the ehm_text text is empty (i.e. blank) then we can return to the previous function because there is no text to process:
	if(ehm_text.empty())
		return ehm_text;

	// The ehm_char array contains a list of ASCII codes to be replaced. The ascii_char array contains a list of the ASCII codes to replace them with.
	// For example, the ASCII code stored at ehm_char[4] will be replaced by the code stored at ascii_char[4], etc.
	const int ehm_char[] = { '\xA7', '\xBD', '\xBC', '\x8F', '\x90', '\xB1', '\xA0', '\x9F', '\x86', '\xBB', '\xB3', '\xBE', '\x87', '\x9A', '\x8A', '\x8E', '\x9E', ',', ';' };
	const int ascii_char[] = { '\x72', '\x53', '\x43', '\x6E', '\x65', '\x4E', '\x63', '\x7A', '\x45', '\x64', '\x52', '\x44', '\x55', '\x73', '\x53', '\x5A', '\x7A', '_', '_' };
	short char_size = sizeof(ehm_char)/sizeof(*ehm_char);

	// Check for SI Font characters
	for(short i = 0; i < char_size; ++i)
		replace(ehm_text.begin(), ehm_text.end(), ehm_char[i], ascii_char[i]);

	// Remove trademark symbol if present at the end of the string
	if (ehm_text[ehm_text.length()-1] == '\x99')
		ehm_text.erase(ehm_text.length()-1);

	// Convert the text to lower case (if the lower_case bool object = true):
	if(lower_case == true)
	{
		int(*pf)(int)=tolower;
		transform(ehm_text.begin(), ehm_text.end(), ehm_text.begin(), pf);
	}

	return ehm_text;
}

// By using a function of bool type, we can return true/false to indicate whether or not the function executed successfully. We can then evaluate this in our main() function when we call this.
bool ExportData(const char &delimiter)
{
	cout << endl << "Exporting data. Please wait..." << endl;

	// Open the retired_numbers.dat file and create a blank retired_numbers.csv file.
	// Note that we open the dat file in binary mode but we omit this for the csv file (because we want to output in text mode instead).
	fstream file_retired ("retired_numbers.dat", ios::in | ios::binary);
	fstream file_csv ("retired_numbers.csv", ios::out);

	// If one or both of the files were not successfully opened, we return false which will abandon the function and return to main():
	if(!file_retired.is_open() || !file_csv.is_open())
		return false;

	// Calculate the file size of the dat file:
	file_retired.seekg (0, ios::end);
	streamoff filesize = file_retired.tellg();
	file_retired.seekg (0, ios::beg);

	// Generate an object called sRetired which is of the RETIRED_NUMBERS struct type:
	RETIRED_NUMBERS sRetired;

	// We'll count the number of entries read from the dat file and written to the csv file:
	int record_count = 0;

	// Before looping through the entry, let's give the csv file a header row so that it is easier to understand when viewing in Excel, etc:
	file_csv << "Club ID" << delimiter << "Player Name" << delimiter << "Jersey Number" << endl;

	// Loop through each entry in the dat file until we reach the end of the file (the filesize number indicates the end of the file - i.e. it represents the final byte of the file):
	while(file_retired.tellg() < filesize)
	{
		// Read one entry from the file. Read it into the sRetired object:
		file_retired.read((char*)&sRetired, sizeof(RETIRED_NUMBERS));		

		// Write the entry to the csv file with commas/semicolons separating each field and a new line separating each entry.
		// Note that we are not exporting the RetiredNumberID because there is no use in editing this number. Instead, we can create new ID numbers when it comes to reimporting the data back into the database.
		// Note that we must use static_cast to convert any number chars to integers. If we don't do this, we'll have a character/letter rather than a number appear in the csv file.
		file_csv << sRetired.RetiredNumberClub << delimiter
				 // In order to use the ASCII function, we need to convert RetiredNumberPlayerName from char[] to a string. This can be done using a C-style cast: (string).
				 << ASCII((string)sRetired.RetiredNumberPlayerName, false) << delimiter
				 << static_cast<int>(sRetired.RetiredNumber) << endl;

		// Increment the record_count number by one:
		++record_count;
	}

	// Close the files now that we have finished with them:
	file_retired.close();
	file_csv.close();

	cout << record_count << " record(s) processed." << endl;

	return true;
}

// By using a function of bool type, we can return true/false to indicate whether or not the function executed successfully. We can then evaluate this in our main() function when we call this.
bool ImportData(const char &delimiter)
{
	cout << endl << "Importing data. Please wait..." << endl;

	// Open the retired_numbers.csv file and create a blank retired_numbers.dat file.
	// Note that we open the dat file in binary mode but we omit this for the csv file (because we want to input in text mode instead).
	fstream file_retired ("retired_numbers.dat", ios::in | ios::out | ios::binary);
	fstream file_csv ("retired_numbers.csv", ios::in);

	// If one or both of the files were not successfully opened, we return false which will abandon the function and return to main():
	if(!file_retired.is_open() || !file_csv.is_open())
		return false;

	// Calculate the file size of the csv file:
	file_csv.seekg (0, ios::end);
	streamoff filesize = file_csv.tellg();
	file_csv.seekg (0, ios::beg);

	// Generate an object called sRetired which is of the RETIRED_NUMBERS struct type:
	RETIRED_NUMBERS sRetired;

	// We'll count the number of entries read from the csv file and written to the dat file.
	// This will also be used to determine the RetiredNumberID for each record because the ID number for the first entry should be zero and increment by one for each successive entry.
	int record_count = 0;

	// We'll use this string buffer to temporarily store the line read in from the csv file:
	string buffer_line;

	// We'll use this string array to temporarily store the parsed/delimited string:
	string buffer_data[3];

	// Before we loop through each entry, we need to ignore the first row in the spreadsheet because it is just a header row.
	// Therefore, let's read the line into the buffer but we'll do nothing with it.
	getline(file_csv, buffer_line);

	// Loop through each entry in the dat file until we reach the end of the file (the filesize number indicates the end of the file - i.e. it represents the final byte of the file):
	while(file_csv.tellg() < filesize)
	{
		// Read one line from the csv file. Read it into the buffer_line variable.
		getline(file_csv, buffer_line);

		// Send the following data to the ParseCSV fuction:
		// buffer_data is the array where the ParseCSV function will store the parsed/delimited string.
		// The number of elements in the buffer_data array. This can be calculated using sizeof(array_name)/sizeof(*array_name).
		// buffer_line is the line read in from the csv file.
		// delimiter is the delimiter to use when separating/parsing the text.
		ParseCSV(buffer_data, sizeof(buffer_data)/sizeof(*buffer_data), buffer_line, delimiter);

		// Now that we have parsed the line of csv text (which has now been stored in the buffer_data array), we can prepare it to be added to the buffer_retired vector:
		// buffer_data[0] just stores the RetiredNumberID number read in from the csv file. We don't need this because we can use record_count to automatically generate the correct number. Therefore we'll just ignore this element.
	
		// Use the record_count number as the RetiredNumberID:
		sRetired.RetiredNumberID = record_count;

		// Assign buffer_data[0] to the RetiredNumberClub. Because buffer_data is a string array, atoi is used to convert the string to an integer:
		sRetired.RetiredNumberClub = atoi(buffer_data[0].c_str());

		// As buffer_data is a string array, strncpy_s is used to convert the string to a char[] array.
		// The original EHM database uses hex code 'CD' to represent blank bytes after the null terminator of the string. Therefore we can use memset to set the char[] array to 'CD' before copying the string from the buffer.
		memset(sRetired.RetiredNumberPlayerName,'\xCD', LONG_TEXT_LENGTH);
		strncpy_s(sRetired.RetiredNumberPlayerName, LONG_TEXT_LENGTH, buffer_data[1].c_str(), strlen(buffer_data[1].c_str()));

		// If you didn't want to replicate the 'CD' blank bytes then you could replace the above two lines of code with the following line. I don't think it matters which method you choose.
		// Note that the compiler might give a warning about using the secure version of strncpy, but it seems to be okay for our purposes:
		/* strncpy(sRetired.RetiredNumberPlayerName, buffer_data[1].c_str(), LONG_TEXT_LENGTH); */

		// Assign buffer_data[2] to the RetiredNumber. Because buffer_data is a string array, atoi is used to convert the string to an integer:
		sRetired.RetiredNumber = atoi(buffer_data[2].c_str());

		// Write the entry to the retired_numbers.dat file:
		file_retired.write((char*)&sRetired, sizeof(RETIRED_NUMBERS));

		// Increment the record_count number by one:
		++record_count;
	}

	// Close the files now that we have finished with them:
	file_retired.close();
	file_csv.close();

	cout << record_count << " record(s) processed." << endl;

	return true;
}

// Calculate the file size (in bytes) of a dat file and divide it by the size (in bytes) of its struct. This calculation determines how many records are stored within the file
long IndexSize(char filename[], unsigned int structure)
{
	fstream file (filename, ios::in | ios::binary);

	// Return -1 as the value if the dat file cannot be opened. We will use -1 to flag for an error.
	if(!file.is_open())
		return -1;

	file.seekg (0, ios::end);
	return static_cast<unsigned int>(file.tellg()) / structure;
}

// Update index.dat:
bool IndexUpdate()
{
	cout << endl << "Updating index.dat..." << endl;

	// Array to store index values for each database file
	long index[35];		

	// Calculate index values for each database file
	index[0] = IndexSize("club.dat", sizeof(CLUBS));
	index[1] = IndexSize("nat_club.dat", sizeof(CLUBS));
	index[2] = IndexSize("colour.dat", sizeof(COLOURS));
	index[3] = IndexSize("continent.dat", sizeof(CONTINENTS));
	index[4] = IndexSize("nation.dat", sizeof(NATIONS));
	index[5] = IndexSize("officials.dat", sizeof(OFFICIALS));
	index[6] = IndexSize("stadium.dat", sizeof(ARENAS));
	index[7] = IndexSize("staff.dat", sizeof(STAFF));
	index[8] = IndexSize("nonplayer.dat", sizeof(NON_PLAYERS));
	index[9] = IndexSize("player.dat", sizeof(PLAYERS));
	index[10] = IndexSize("staff_comp.dat", sizeof(STAFF_COMPS));
	index[11] = IndexSize("city.dat", sizeof(CITIES));
	index[12] = IndexSize("club_comp.dat", sizeof(CLUB_COMPS));
	index[13] = IndexSize("nation_comp.dat", sizeof(CLUB_COMPS));
	index[14] = IndexSize("first_names.dat", sizeof(NAMES));
	index[15] = IndexSize("second_names.dat", sizeof(NAMES));
	index[16] = IndexSize("staff_history.dat", sizeof(STAFF_HISTORIES));
	index[17] = IndexSize("staff_comp_history.dat", sizeof(STAFF_COMP_HISTORIES));
	index[18] = IndexSize("club_comp_history.dat", sizeof(CLUB_COMP_HISTORIES));
	index[19] = IndexSize("nation_comp_history.dat", sizeof(CLUB_COMP_HISTORIES));
	index[20] = IndexSize("affiliations.dat", sizeof(AFFILIATIONS));
	index[21] = IndexSize("retired_numbers.dat", sizeof(RETIRED_NUMBERS));
	index[22] = IndexSize("states_provinces.dat", sizeof(STATES_PROVINCES));
	index[23] = IndexSize("injuries.dat", sizeof(INJURIES));
	index[24] = IndexSize("staff_preferences.dat", sizeof(STAFF_PREFERENCES));
	index[25] = IndexSize("currencies.dat", sizeof(CURRENCIES));
	index[26] = IndexSize("club_records.dat", sizeof(DB_CLUB_RECORDS));
	index[27] = IndexSize("club_histories.dat", sizeof(CLUB_HISTORIES));
	index[28] = IndexSize("drafts.dat", sizeof(DRAFTS));
	index[29] = IndexSize("drafted_players.dat", sizeof(DRAFTED_PLAYERS));
	index[30] = IndexSize("player_rights.dat", sizeof(DB_PLAYER_RIGHTS));
	index[31] = IndexSize("stage_names.dat", sizeof(STAGE_NAMES));
	index[32] = IndexSize("staff_languages.dat", sizeof(STAFF_LANGUAGES));
	index[33] = IndexSize("player_info.dat", sizeof(DB_PLAYER_INFO));
	index[34] = IndexSize("staff_info.dat", sizeof(DB_STAFF_INFO));

	// Check for any errors returned from the IndexSize function (and, if so, abort before writing to index.dat)
	for(short i = 0; i < 35; ++i)
	{
		if(index[i] < 0)
		{
			cout << "ERROR: Unable to open all .dat files" << endl;
			return false;
		}
	}

	// Write to index.dat
	fstream file_index ("index.dat", ios::in | ios::out | ios::binary);

	if(!file_index.is_open())
	{
		cout << "ERROR: Unable to open index.dat" << endl;
		return false;
	}

	// File position marker
	streamoff pos = 63;

	for(short i = 0; i < 35; ++i)
	{
		file_index.seekp (pos, ios::beg);
		file_index.write ((char*)&index[i], sizeof(index[i]));
		pos += 63;
	}

	file_index.close();

	cout << "Index.dat successfully updated." << endl;

	return true;
}

// This function takes a line from the csv file and then separates it using delimiter. There are four variables you must pass to the function:
// buffer_string[] is an array in which we'll place the parsed data.
// max_element is the number of elements in the array. We need this number in order to figure out how long to loop through the line of csv text.
// csv_string is the line of csv text.
// delimiter is the delimiter to use when separating/parsing the text.
void ParseCSV(string buffer_string[], const int &max_element, const string &csv_string, const char &delimiter)
{
	// We have to use stringstream in order to use getline() to parse the text.
	stringstream csv_line(csv_string);

	// Parse each delimiter until the final delimiter
	for(int i = 0; i < (max_element-1); ++i)
		// This takes the next chunk of text and places it in the next element of the buffer_string[] array until the next delimiter is detected:
		getline(csv_line, buffer_string[i], delimiter);

	// After the final delimiter, get the remaining text from the line
	getline(csv_line, buffer_string[max_element-1]);

	// As this is a void function, we don't return any value:
	return;
}
Midas
Minor League
Posts: 238
Joined: Sun Nov 07, 2010 5:57 am

Re: C/C++ Coding Problems & Questions [EHM 2005]

Post by Midas »

This is an 07 question, but here's the code I mentioned before:

Code: Select all

#define _WIN32_WINNT _WIN32_WINNT_WINXP // Windows XP compatibility macro
#pragma pack(1)   // This is needed in order to ensure that the data is read and written to the database files in the correct fashion. Without this, you will get weird results.

// Include the various standard library templates which we'll need:
#include <algorithm>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

// Include the database_types.h header file so that we can read the structs of the files in the database:
#include "database_types.h"

// Use the std namespace because it makes things easier to read for such a simple programme:
using namespace std;

// Function prototypes. By declaring your functions here, you can then put the full function below main(). It helps keeps things a bit tidier.
char DetectDelimiter();
string ASCII(string &, bool);
bool ExportData(const char &);
bool ImportData(const char &);
long IndexSize(char [], unsigned int);
bool IndexUpdate();
void ParseCSV(string[], const int &, const string &, const char &);

// Main programme routine. This is where the programme starts:
int main()
{
   // Before we start, let's detect what csv delimiter is used by the user's computer settings:
   const char delimiter = DetectDelimiter();

   cout << "Cities exporter/importer" << endl
       << "By Archibalduk, Edited by Midas Panikkar" << endl
       << endl
       << "http://www.ehmtheblueline.com" << endl
       << endl
       << "Choose from one of the following options:" << endl
       << endl
       << "E: Export data from city.dat -> csv" << endl
       << "I: Import data from city.csv -> dat" << endl
       << endl
       << "Enter your selection: ";

   // Take a single character input from the user and store it in option_char:
   char option_char;
   cin >> option_char;

   // The toupper() function converts any given char to uppercase. This means that you can just check for a capital letter if you use this in your if statement.
   // In other words it means that you don't have to check for both lower and upper case characters individually - you can just check for uppercase which will cover both.
   // I.e. you can do if(toupper(option_char) == 'E') rather than if(option_char == 'e' || option_char == 'E') - it just makes things a little simpler.
   if(toupper(option_char) == 'E')
   {
      // This if statement calls/runs the ExportData() function and then checks whether true is returned.
      // You'll see that we pass the delimiter to the function. This is so we can access the delimiter char within the function (because the delimiter has local scope).
      if(ExportData(delimiter) == true)
         cout << endl << "Data successfully exported." << endl;
      else
         cout << endl << "An error was encountered." << endl;
   }
   else
   {
      // This if statement calls/runs the ImportData() function and then checks whether true is returned.
      // You'll see that we pass the delimiter to the function. This is so we can access the delimiter char within the function (because the delimiter has local scope).
      if(ImportData(delimiter) == true)
      {
         cout << "Data successfully imported." << endl;

         // If the retired numbers have been successfully added then the index.dat file needs to be updated:
         IndexUpdate();
      }
      else
         cout << endl << "An error was encountered." << endl << "Process aborted." << endl;
   }

   // We're all done, so let's tell the user to exit the programme:
   cout << endl << "Press ENTER to close this window.";
   
   // Wait for the user to press enter...
   cin.ignore();
   cin.get();
   // ...Then exit the programme
   return 0;
}

// Depending on your computer's settings, you will either use a comma or a semicolon as the CSV delimiter/separator.
// This function will detect the delimiter used on your system:
char DetectDelimiter()
{
   return (use_facet<numpunct<char>>(cout.getloc()).decimal_point() == '.') ? ',' : ';';
}

// Convert SI Font text to normal text. This ensures that we get the correct characters when we export the text to csv (which is ASCII).
// We also strip caron letters to normal letters as well as replacing any commas/semicolons with underscores to prevent it being confused as a csv delimiter. 
string ASCII(string &ehm_text, bool lower_case)
{
   // If the ehm_text text is empty (i.e. blank) then we can return to the previous function because there is no text to process:
   if(ehm_text.empty())
      return ehm_text;

   // The ehm_char array contains a list of ASCII codes to be replaced. The ascii_char array contains a list of the ASCII codes to replace them with.
   // For example, the ASCII code stored at ehm_char[4] will be replaced by the code stored at ascii_char[4], etc.
   const int ehm_char[] = { '\xA7', '\xBD', '\xBC', '\x8F', '\x90', '\xB1', '\xA0', '\x9F', '\x86', '\xBB', '\xB3', '\xBE', '\x87', '\x9A', '\x8A', '\x8E', '\x9E', ',', ';' };
   const int ascii_char[] = { '\x72', '\x53', '\x43', '\x6E', '\x65', '\x4E', '\x63', '\x7A', '\x45', '\x64', '\x52', '\x44', '\x55', '\x73', '\x53', '\x5A', '\x7A', '_', '_' };
   short char_size = sizeof(ehm_char)/sizeof(*ehm_char);

   // Check for SI Font characters
   for(short i = 0; i < char_size; ++i)
      replace(ehm_text.begin(), ehm_text.end(), ehm_char[i], ascii_char[i]);

   // Remove trademark symbol if present at the end of the string
   if (ehm_text[ehm_text.length()-1] == '\x99')
      ehm_text.erase(ehm_text.length()-1);

   // Convert the text to lower case (if the lower_case bool object = true):
   if(lower_case == true)
   {
      int(*pf)(int)=tolower;
      transform(ehm_text.begin(), ehm_text.end(), ehm_text.begin(), pf);
   }

   return ehm_text;
}

// By using a function of bool type, we can return true/false to indicate whether or not the function executed successfully. We can then evaluate this in our main() function when we call this.
bool ExportData(const char &delimiter)
{
   cout << endl << "Exporting data. Please wait..." << endl;

   // Open the CITIES.dat file and create a blank CITIES.csv file.
   // Note that we open the dat file in binary mode but we omit this for the csv file (because we want to output in text mode instead).
   fstream file_city ("city.dat", ios::in | ios::binary);
   fstream file_csv ("city.csv", ios::out);

   // If one or both of the files were not successfully opened, we return false which will abandon the function and return to main():
   if(!file_city.is_open() || !file_csv.is_open())
      return false;

   // Calculate the file size of the dat file:
   file_city.seekg (0, ios::end);
   streamoff filesize = file_city.tellg();
   file_city.seekg (0, ios::beg);

   // Generate an object called sCities which is of the CITIES struct type:
   CITIES sCities;

   // We'll count the number of entries read from the dat file and written to the csv file:
   int record_count = 0;

   // Before looping through the entry, let's give the csv file a header row so that it is easier to understand when viewing in Excel, etc:
   file_csv << "CityNation" << delimiter << "CityState" << delimiter << "CityName" << delimiter << "CityLatitude" << delimiter << "CityLongitude" << delimiter << "CityAttraction" << endl;

   // Loop through each entry in the dat file until we reach the end of the file (the filesize number indicates the end of the file - i.e. it represents the final byte of the file):
   while(file_city.tellg() < filesize)
   {
      // Read one entry from the file. Read it into the sCities object:
      file_city.read((char*)&sCities, sizeof(CITIES));      

      // Write the entry to the csv file with commas/semicolons separating each field and a new line separating each entry.
      // Note that we are not exporting the RetiredNumberID because there is no use in editing this number. Instead, we can create new ID numbers when it comes to reimporting the data back into the database.
      // Note that we must use static_cast to convert any number chars to integers. If we don't do this, we'll have a character/letter rather than a number appear in the csv file.
      file_csv << sCities.CityNation << delimiter << sCities.CityState << delimiter
             // In order to use the ASCII function, we need to convert RetiredNumberPlayerName from char[] to a string. This can be done using a C-style cast: (string).
             << ASCII((string)sCities.CityName, false) << delimiter
             << static_cast<int>(sCities.CityLatitude) << delimiter << static_cast<int>(sCities.CityLongitude) << delimiter << static_cast<int>(sCities.CityAttraction) << endl;

      // Increment the record_count number by one:
      ++record_count;
   }

   // Close the files now that we have finished with them:
   file_city.close();
   file_csv.close();

   cout << record_count << " record(s) processed." << endl;

   return true;
}

// By using a function of bool type, we can return true/false to indicate whether or not the function executed successfully. We can then evaluate this in our main() function when we call this.
bool ImportData(const char &delimiter)
{
   cout << endl << "Importing data. Please wait..." << endl;

   // Open the CITIES.csv file and create a blank CITIES.dat file.
   // Note that we open the dat file in binary mode but we omit this for the csv file (because we want to input in text mode instead).
   fstream file_city ("city.dat", ios::in | ios::out | ios::binary);
   fstream file_csv ("city.csv", ios::in);

   // If one or both of the files were not successfully opened, we return false which will abandon the function and return to main():
   if(!file_city.is_open() || !file_csv.is_open())
      return false;

   // Calculate the file size of the csv file:
   file_csv.seekg (0, ios::end);
   streamoff filesize = file_csv.tellg();
   file_csv.seekg (0, ios::beg);

   // Generate an object called sCities which is of the CITIES struct type:
   CITIES sCities;

   // We'll count the number of entries read from the csv file and written to the dat file.
   // This will also be used to determine the RetiredNumberID for each record because the ID number for the first entry should be zero and increment by one for each successive entry.
   int record_count = 0;

   // We'll use this string buffer to temporarily store the line read in from the csv file:
   string buffer_line;

   // We'll use this string array to temporarily store the parsed/delimited string:
   string buffer_data[4];

   // Before we loop through each entry, we need to ignore the first row in the spreadsheet because it is just a header row.
   // Therefore, let's read the line into the buffer but we'll do nothing with it.
   getline(file_csv, buffer_line);

   // Loop through each entry in the dat file until we reach the end of the file (the filesize number indicates the end of the file - i.e. it represents the final byte of the file):
   while(file_csv.tellg() < filesize)
   {
      // Read one line from the csv file. Read it into the buffer_line variable.
      getline(file_csv, buffer_line);

      // Send the following data to the ParseCSV fuction:
      // buffer_data is the array where the ParseCSV function will store the parsed/delimited string.
      // The number of elements in the buffer_data array. This can be calculated using sizeof(array_name)/sizeof(*array_name).
      // buffer_line is the line read in from the csv file.
      // delimiter is the delimiter to use when separating/parsing the text.
      ParseCSV(buffer_data, sizeof(buffer_data)/sizeof(*buffer_data), buffer_line, delimiter);

      // Now that we have parsed the line of csv text (which has now been stored in the buffer_data array), we can prepare it to be added to the buffer_retired vector:
      // buffer_data[0] just stores the RetiredNumberID number read in from the csv file. We don't need this because we can use record_count to automatically generate the correct number. Therefore we'll just ignore this element.
   
      // Use the record_count number as the RetiredNumberID:
      sCities.CityID = record_count;

      // Assign buffer_data[0] to the RetiredNumberClub. Because buffer_data is a string array, atoi is used to convert the string to an integer:
      sCities.CityNation = atoi(buffer_data[0].c_str());
	  sCities.CityState = atoi(buffer_data[1].c_str());
      // As buffer_data is a string array, strncpy_s is used to convert the string to a char[] array.
      // The original EHM database uses hex code 'CD' to represent blank bytes after the null terminator of the string. Therefore we can use memset to set the char[] array to 'CD' before copying the string from the buffer.
      memset(sCities.CityName,'\xCD', STANDARD_TEXT_LENGTH);
      strncpy_s(sCities.CityName, STANDARD_TEXT_LENGTH, buffer_data[2].c_str(), strlen(buffer_data[2].c_str()));

      // If you didn't want to replicate the 'CD' blank bytes then you could replace the above two lines of code with the following line. I don't think it matters which method you choose.
      // Note that the compiler might give a warning about using the secure version of strncpy, but it seems to be okay for our purposes:
      /* strncpy(sCities.RetiredNumberPlayerName, buffer_data[1].c_str(), LONG_TEXT_LENGTH); */

      // Assign buffer_data[2] to the RetiredNumber. Because buffer_data is a string array, atoi is used to convert the string to an integer:
	  sCities.CityLatitude = atoi(buffer_data[3].c_str());
	  sCities.CityLongitude = atoi(buffer_data[3].c_str());
	  sCities.CityAttraction = atoi(buffer_data[4].c_str());

      // Write the entry to the CITIES.dat file:
      file_city.write((char*)&sCities, sizeof(CITIES));

      // Increment the record_count number by one:
      ++record_count;
   }

   // Close the files now that we have finished with them:
   file_city.close();
   file_csv.close();

   cout << record_count << " record(s) processed." << endl;

   return true;
}

// Calculate the file size (in bytes) of a dat file and divide it by the size (in bytes) of its struct. This calculation determines how many records are stored within the file
long IndexSize(char filename[], unsigned int structure)
{
   fstream file (filename, ios::in | ios::binary);

   // Return -1 as the value if the dat file cannot be opened. We will use -1 to flag for an error.
   if(!file.is_open())
      return -1;

   file.seekg (0, ios::end);
   return static_cast<unsigned int>(file.tellg()) / structure;
}

// Update index.dat:
bool IndexUpdate()
{
   cout << endl << "Updating index.dat..." << endl;

   // Array to store index values for each database file
   long index[35];      

   // Calculate index values for each database file
   index[0] = IndexSize("club.dat", sizeof(CLUBS));
   index[1] = IndexSize("nat_club.dat", sizeof(CLUBS));
   index[2] = IndexSize("colour.dat", sizeof(COLOURS));
   index[3] = IndexSize("continent.dat", sizeof(CONTINENTS));
   index[4] = IndexSize("nation.dat", sizeof(NATIONS));
   index[5] = IndexSize("officials.dat", sizeof(OFFICIALS));
   index[6] = IndexSize("stadium.dat", sizeof(ARENAS));
   index[7] = IndexSize("staff.dat", sizeof(STAFF));
   index[8] = IndexSize("nonplayer.dat", sizeof(NON_PLAYERS));
   index[9] = IndexSize("player.dat", sizeof(PLAYERS));
   index[10] = IndexSize("staff_comp.dat", sizeof(STAFF_COMPS));
   index[11] = IndexSize("city.dat", sizeof(CITIES));
   index[12] = IndexSize("club_comp.dat", sizeof(CLUB_COMPS));
   index[13] = IndexSize("nation_comp.dat", sizeof(CLUB_COMPS));
   index[14] = IndexSize("first_names.dat", sizeof(NAMES));
   index[15] = IndexSize("second_names.dat", sizeof(NAMES));
   index[16] = IndexSize("staff_history.dat", sizeof(STAFF_HISTORIES));
   index[17] = IndexSize("staff_comp_history.dat", sizeof(STAFF_COMP_HISTORIES));
   index[18] = IndexSize("club_comp_history.dat", sizeof(CLUB_COMP_HISTORIES));
   index[19] = IndexSize("nation_comp_history.dat", sizeof(CLUB_COMP_HISTORIES));
   index[20] = IndexSize("affiliations.dat", sizeof(AFFILIATIONS));
   index[21] = IndexSize("CITIES.dat", sizeof(CITIES));
   index[22] = IndexSize("states_provinces.dat", sizeof(STATES_PROVINCES));
   index[23] = IndexSize("injuries.dat", sizeof(INJURIES));
   index[24] = IndexSize("staff_preferences.dat", sizeof(STAFF_PREFERENCES));
   index[25] = IndexSize("currencies.dat", sizeof(CURRENCIES));
   index[26] = IndexSize("club_records.dat", sizeof(DB_CLUB_RECORDS));
   index[27] = IndexSize("club_histories.dat", sizeof(CLUB_HISTORIES));
   index[28] = IndexSize("drafts.dat", sizeof(DRAFTS));
   index[29] = IndexSize("drafted_players.dat", sizeof(DRAFTED_PLAYERS));
   index[30] = IndexSize("player_rights.dat", sizeof(DB_PLAYER_RIGHTS));
   index[31] = IndexSize("stage_names.dat", sizeof(STAGE_NAMES));
   index[32] = IndexSize("staff_languages.dat", sizeof(STAFF_LANGUAGES));
   index[33] = IndexSize("player_info.dat", sizeof(DB_PLAYER_INFO));
   index[34] = IndexSize("staff_info.dat", sizeof(DB_STAFF_INFO));

   // Check for any errors returned from the IndexSize function (and, if so, abort before writing to index.dat)
   for(short i = 0; i < 35; ++i)
   {
      if(index[i] < 0)
      {
         cout << "ERROR: Unable to open all .dat files" << endl;
         return false;
      }
   }

   // Write to index.dat
   fstream file_index ("index.dat", ios::in | ios::out | ios::binary);

   if(!file_index.is_open())
   {
      cout << "ERROR: Unable to open index.dat" << endl;
      return false;
   }

   // File position marker
   streamoff pos = 63;

   for(short i = 0; i < 35; ++i)
   {
      file_index.seekp (pos, ios::beg);
      file_index.write ((char*)&index[i], sizeof(index[i]));
      pos += 63;
   }

   file_index.close();

   cout << "Index.dat successfully updated." << endl;

   return true;
}

// This function takes a line from the csv file and then separates it using delimiter. There are four variables you must pass to the function:
// buffer_string[] is an array in which we'll place the parsed data.
// max_element is the number of elements in the array. We need this number in order to figure out how long to loop through the line of csv text.
// csv_string is the line of csv text.
// delimiter is the delimiter to use when separating/parsing the text.
void ParseCSV(string buffer_string[], const int &max_element, const string &csv_string, const char &delimiter)
{
   // We have to use stringstream in order to use getline() to parse the text.
   stringstream csv_line(csv_string);

   // Parse each delimiter until the final delimiter
   for(int i = 0; i < (max_element-1); ++i)
      // This takes the next chunk of text and places it in the next element of the buffer_string[] array until the next delimiter is detected:
      getline(csv_line, buffer_string[i], delimiter);

   // After the final delimiter, get the remaining text from the line
   getline(csv_line, buffer_string[max_element-1]);

   // As this is a void function, we don't return any value:
   return;
}
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion [EHM 2007]

Post by archibalduk »

I haven't tested this, but I expect this is the problem:

Code: Select all

sCities.CityLatitude = atoi(buffer_data[3].c_str());
sCities.CityLongitude = atoi(buffer_data[3].c_str());
The atoi() function converts ASCII to integer (a whole number). The longitude and latitude fields are doubles/floats which are decimal numbers. Therefore you need to use atof() rather than atoi(). The atof() function coverts ASCII to float.

Btw, I've re-written your code using some generic functions I wrote for the Updater. It makes things a little easier to manage because a lot of the work is done for you. Let me know if you want me to post them for you.
NingDynasty
Junior League
Posts: 27
Joined: Wed Feb 08, 2012 10:36 pm

Re: C/C++ Code Snippets / Discussion [EHM 2007]

Post by NingDynasty »

Thanks Archie, it was real easy to compile a spreadsheet with all the players names and attributes, makes it easy to update with your EHM Updater having the base values all visible in one sheet.

As merger of two topics, does the players.dat file of a save game read out similarly? I was thinking of a utility that might randomize the PA/CA of a player right after retirement, something you could run once a year during your sim.
User avatar
archibalduk
TBL Admin Team
Posts: 20372
Joined: Tue Jul 06, 2004 8:44 pm
Custom Rank: Seaside + Fruit Juice Mode
Favourite Team: Guildford (EPL) / Invicta (NIHL)
Location: United Kingdom
Contact:

Re: C/C++ Code Snippets / Discussion [EHM 2007]

Post by archibalduk »

NingDynasty wrote:Thanks Archie, it was real easy to compile a spreadsheet with all the players names and attributes, makes it easy to update with your EHM Updater having the base values all visible in one sheet.

As merger of two topics, does the players.dat file of a save game read out similarly? I was thinking of a utility that might randomize the PA/CA of a player right after retirement, something you could run once a year during your sim.
Glad it's of some use. :-) I have written a bunch of generic functions which I can upload if you're serious about editing EHM DB files.

Unfortunately the saved game data files have a slightly different structure to the DB structure (i.e. additional fields). However I did make some progress with processing the saved game files; but then I got side-tracked by other things. My ultimate goal is similar to yours - to randomise CA/PAs for regens. It shouldn't be too hard either. My idea was to process all regens (going by their DOB) and randomly swap around the PAs between players of the same nationality. This way, the talent pool for each nation is maintained, but you can't do the usual regen-spotting. I was also thinking of setting up some templates for different types of players so as to address the issue where regens have weird combinations of attributes.

I'm happy to post some sample code to help you get started with editing the saved game structure if you're interested.
NingDynasty
Junior League
Posts: 27
Joined: Wed Feb 08, 2012 10:36 pm

Re: C/C++ Code Snippets / Discussion [EHM 2007]

Post by NingDynasty »

The saved game structure would be awesome for future versions of rosters (saved game distribution instead of pre-game) we could distribute a 2012 saved game with authentic stats to start with instead of all this custom date stuff. Of course it'd need some type of online editing and an army to get it done.

To quote my other post as far as regens go..
Pretty cool stuff, in a dream world as players retire we could run a utility that could adjust regens countries ca/pa's to reflect the IIHF ranking score... making player generation dynamic, therefore allowing you to build up a nation and it's hockey relevancy. Perhaps even making the national league of the country more important in the process.
Post Reply