Skip to content

Conversation

@martinbutt
Copy link

Obviously this is a big change, but it's mostly moving around what was already there so I can use it as part of another project (PM3000). I didn't want to mess with your code too much, so I've kept things in the C-style where I could, without leaning too much into modern C++ (although I did end up using some C++17).

Here's an overview:

  1. Split out the shareable code into pm3.hh / pm3.cc
  2. Updated the formatting of player lists to match the tabular game format.
  3. In all this tinkering, I've discovered that aggression has a disproportionate weight on a players abilities in the game. If you have a team where all players have an aggression of 9, they are pretty much unbeatable despite the rest of their stats. I've added a function to level the aggression of all players in all teams to 5.
  4. Mapped the structure of the PREFS file and SAVES.dir file.
  5. Fixed some minor bugs, e.g. directors_confidence_current and directors_confidence_start being reversed.
  6. Added mapping for injuries/bans/etc.
  7. Added support for PM3 Deluxe.
  8. Changed the path that is passed in to be the game path and not the saves path.
  9. Added CMake build scripts.
  10. Added function to load the default gamedata.dat, clubdata.dat and playdata.dat.
  11. Added mapping for kits style and color (home, away1, away2).
  12. Added mapping for weekly league position

I know it's a lot, and if you want to ignore this, no worries, I'll just use my fork for PM3000.

Also, my IDE reformatted the code. If that's a blocker, let me know and I'll restore the original formatting as best I can.

Copy link
Owner

@eb4x eb4x left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR would have been better if there were a bunch of smaller "atomic" commits, that fixed one issue at a time. That way we could go through the reasoning behind the changes, and I'd see the thought-process a bit clearer.

The C++17 std::filesystem is probably a better option than what we have today with aprinft. I'm not sure about the PATH_SEPARATOR, I think/ works just as well? (Maybe not if you're passing in \ from the commandline? i.e. game_path="C:\dosbox\pm3" and then appending /SAVES to that)

The functions reading the default game, club, player, meta and prefs look really interesting, but aren't getting used anywhere? (Update, I see they get used from PM3000)

Anyway, cool of you to check in. If you like the feedback and having your code reviewed, then you're welcome to submit PRs and I'll gladly take a look.

But if you have a vision for this code and what it can become (which it kinda seems like you do), you have my full blessing to just pursue PM3000. Which is looking great btw!

pm3/pm3.cc Outdated
Comment on lines 426 to 427
char* append_trailing_slash(const char* path) {
char* appended_path = const_cast<char *>(path);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you taking in a const char *, and then casting it to a char *? const is there to prevent you from modifying/reassigning it. It's mostly a memo to yourself that "I do not intend to modify this."

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch there. That's an oversight on my part. At some point the const made sense further up the stack, but where it finished up, it no longer did.

pm3/pm3.cc Outdated
Comment on lines 434 to 435
appended_path[len] = PATH_SEPARATOR;
appended_path[len + 1] = '\0'; // Null-terminate the string
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see a malloc anywhere, so now you're writing beyond the buffer.

I think what you'd want to do something like,

size_t len = strlen(path);

if (path[len - 1] == PATH_SEPARATOR)
  return strdup(path);

char new_path = malloc(len + 2);
strcpy(new_path, path);
new_path[len] = PATH_SEPARATOR;
new_path[len + 1] = '\0';

return new_path;

But, now you have to track the returned value, and remember to free() it when it's not needed anymore. There are perhaps cleaner ways of doing this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good point. I've cleaned this up along with your other comments by moving over to use std::filessystem. I think the result is pretty clean.

pm3/pm3.cc Outdated
void load_binaries(int game_nr, const char *saves_path) {
char* full_path = append_trailing_slash(saves_path);

size_t gamexa_len = asprintf(&gamexa, "%sGAME%.1dA", full_path != nullptr ? full_path : "", game_nr);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need a corresponding call to free() for every asprintf as it allocates memory.

pm3/pm3.cc Outdated
fread(&gamea, sizeof (struct gamea), 1, fga);
fclose(fga);

size_t gamexb_len = asprintf(&gamexb, "%sGAME%.1dB", full_path != nullptr ? full_path : "", game_nr);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asprintf missing free

pm3/pm3.cc Outdated
fread(&gameb, sizeof (struct gameb), 1, fgb);
fclose(fgb);

size_t gamexc_len = asprintf(&gamexc, "%sGAME%.1dC", full_path != nullptr ? full_path : "", game_nr);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asprintf missing free

pm3/pm3.cc Outdated
fwrite(&saves, sizeof (struct saves), 1, fgs);
fclose(fgs);

size_t prefs_len = asprintf(&prefsx, "%sPREFS", full_path != nullptr ? full_path : "");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asprintf missing free

main.cc Outdated
Comment on lines 219 to 221
free(gamexa);
free(gamexb);
free(gamexc);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer relevant here.

Comment on lines +19 to +20
#define STANDARD_SAVES_PATH "SAVES"
#define DELUXE_SAVES_PATH "saves"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this really boils down to DOS was case insensitive, and would be better handled with a function that looks up the contents of a directory and case-insensitive looks for a entry, then uses the first result.

char *case_insensitive_path(const char *path, const char *name) {
  char *full_path = NULL;
  DIR *dirp = opendir(path);

  struct dirent *entry = NULL;
  while ((entry = readdir(dirp)) != NULL) {
    if (0 != strcasecmp(entry->d_name, name))
      continue;

    size_t res = asprintf(&full_path, "%s%c%s", path, PATH_SEPARATOR, entry->d_name);
    break;
  }

  closedir(dirp);
  return full_path;
}

Maybe the C++17 std::filesystem can do something similar?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename case sensitivity doesn't seem to have a straightforward solution with modern libraries. As there are only two cases "SAVES" and "saves", I could change get_saves_folder to:

const char* get_saves_folder(const char *game_path) {
    if (std::filesystem::exists(std::filesystem::path(game_path) / STANDARD_SAVES_PATH)) {
        return STANDARD_SAVES_PATH;
    } else if (std::filesystem::exists(std::filesystem::path(game_path) / DELUXE_SAVES_PATH)) {
        return DELUXE_SAVES_PATH;
    } else {
        fprintf(stderr, "Saves folder not found\n");
        exit(EXIT_FAILURE);
    }}
}

However, I'm using the get_pm3_game_type() function (which the current get_saves_folder relies on) in PM3000, so I'm tempted to leave it as is?

pm3/pm3.cc Outdated
gameb.club[new_club_idx].player_image = gameb.club[old_club_idx].player_image;

strncpy(gameb.club[new_club_idx].manager, gameb.club[old_club_idx].manager, 16);
strncpy(gameb.club[old_club_idx].manager, DEFAULT_MANAGER_NAME, 16);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By reading in the default values from game/player/club which you've already done, you can actually fix this to the original club manager instead of "J. Smith".

pm3/pm3.hh Outdated

#define HOME 0
#define AWAY 1
#define DEFAULT_MANAGER_NAME "J.Smith "
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment about J. Smith.

@eb4x eb4x mentioned this pull request Feb 11, 2025
@eb4x
Copy link
Owner

eb4x commented Feb 11, 2025

I've got a WIP splitting up your PR into individual commits at #4.

So, I really like these changes. I'm not sure about moving the pm3 files up a directory, but I like the idea of splitting them out into a header and a library, and just keept them at the root. (It just looks a bit weird when you've got pm3.git/pm3/pm3.{cc,h}, which seems like one too many pm3.) And so I'm keeping the dump/print functions in main.cc, as they're just there for figuring out the data structures, and don't really provide anything to the library per se.

The detection of standard vs deluxe seemed to me to be an issue of case sensitive filesystem, so I just wrote the case-insensitive search for the directory. If there's more to it, let me know?

I changed the load_default_* functions to return a pointer to a memory-allocated struct, as it probably doesn't make sense reading that into the game[abc] structs in pm3.cc if you're doing comparisons or what not? Then again, the extern structs might need some thought aswell?

I'll have a go at implementing the rest of the functions tomorrow and compiling it agains pm3k. Let me know what you think 😸

@martinbutt
Copy link
Author

Your code reviews are massively appreciated. The whole point of this for me is to sharpen my C++ and learn about reverse engineering. PM3000 has been a lot of fun. If you want to try that out and give feedback, it would also be very welcome. Eventually, I'd love to do go deeper into the .exe file and make mods directly to the game code, instead of just manipulating the save games. I would also like to finish decoding the gadgets.dat to extract the image assets.

Looks like we were working on this at the same time. I'll add some comments above.

@martinbutt
Copy link
Author

Just a heads up that if you are going to look at pm3k and have already pulled it, I've just pushed a refactor, so you'll want to pull down those changes first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants