Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 69

Loading Game State from Binary Files

Recommended resources

Welcome back to the capstone project. In our last session, you built the saveGame function, a crucial first step toward making your game world persistent. You converted the game's complex, in-memory object graph into a flat sequence of bytes, cleverly solving the "pointer problem" by saving unique tags instead of volatile memory addresses.

This lesson completes the circle. We will now focus on deserialization: the process of reading your save.bin file and resurrecting the game state. You'll write the loadGame function, which reads the raw data and, more importantly, meticulously reconstructs the pointer-based relationships, restoring the world exactly as the player left it. This is where the careful design of your save file format pays off.

The Challenge of Rebuilding a World

Loading the game is more complex than simply reading data. Our primary challenge is to correctly restore the location pointers for every object. A naive fread of the entire object structure from the file would be disastrous, as it would write stale, meaningless memory addresses into your pointers.

The solution requires a two-pass loading strategy:

  1. First Pass: Data Ingestion. We read through the file and load the fundamental, non-pointer data for every object: its tag, description, and details. We also read the tag of its location (e.g., "livingRoom") and store it temporarily. At the end of this pass, all objects exist in memory as distinct entities, but they are not yet linked together.
  2. Second Pass: Pointer Relinking. With all objects now in memory, we can safely establish the relationships. We iterate through our newly created objects. For each object, we take its temporarily stored location tag and search our complete list of objects to find the one with the matching tag. Once found, we set the location pointer to the correct, current memory address of that object.

This process is analogous to assembling a model kit: first, you lay out all the individual pieces, and only then do you start gluing them together according to the instructions.


Why Not Just Read the Struct?

The core reason we cannot just fread an entire OBJECT struct is the presence of pointers like location, description, etc. A powerful comment on a Stack Overflow question about loading linked lists makes this point emphatically.

C - How to Read in a Linked List from a Binary File?

This resource explains why direct serialization of pointer-based structures is not feasible.

Please read the short but vital comment from user Petr Skocik, starting with "You can't save a generic linked list". This is the central constraint governing our entire save/load system.

This is precisely why our saveGame function wrote each attribute individually. Our loadGame function must do the reverse. The correct answer in that same Stack Overflow thread demonstrates a valid approach for a simple list: it reads each data value (int) one at a time and uses a function like add_tail to reconstruct the list node by node. Our two-pass approach is a more advanced version of this same principle, adapted for a more complex graph of objects.


A Professional Approach to Binary File Parsing

In professional settings, parsing binary files is a common task. Game save files, image formats, and network protocols all rely on it. A great reference for this comes from an ECE course on C programming that has students parse BMP image files.

HW16 – ECE 264 Advanced C Programming – Summer 2022

This document provides an excellent overview of robust binary file reading practices, which we will apply to our game.

Skim the section on the BMP file format. Notice how it's broken down into a header and data, just like our save file. The use of fread to pull the header into a struct is a standard technique. Focus on the description of the read_bmp function in the requirements table. It reads from an already-open file, returns a pointer to a heap-allocated structure, checks the file's integrity using the header, and handles errors. This is a model for our loadGame function. Find it under bmp.c by looking for this entry. Finally, read the brief section on Big-endian vs. little-endian. This concept (byte order) is a classic issue in binary file I/O. While your current machine (likely x86-64) is little-endian and will match the file it writes, being aware of endianness is what separates a hobbyist from a professional C programmer.


Your Implementation Task: The loadGame Function

Now it's time to build loadGame. You will create load.c and load.h and implement the two-pass strategy.

1. Create load.c and load.h

  • Add these new files to your project and update your Makefile to compile and link load.c.

  • In load.h, declare the loadGame function.

    // In load.h
    #ifndef LOAD_H
    #define LOAD_H
    
    #include <stdbool.h>
    
    bool loadGame(const char *filename);
    
    #endif
    

2. Implement the loadString Helper

  • In load.c, you'll need a helper function that's the inverse of saveString. It will read a length, allocate memory, and then read the string data.

    // In load.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include "object.h" // Your game's object definitions
    #include "save.h"   // For the SaveHeader/VersionNumber structs
    #include "load.h"
    
    // Helper to read a length-prefixed string from a file
    static char *loadString(FILE *fp) {
        unsigned int len = 0;
        char *str = NULL;
        
        // Read the length of the upcoming string
        if (fread(&len, sizeof(unsigned int), 1, fp) != 1) {
            return NULL; // Read error
        }
    
        if (len > 0) {
            // Allocate memory for the string (+1 for null terminator)
            str = malloc(len + 1);
            if (!str) return NULL; // Malloc failed
            
            // Read the string data
            if (fread(str, sizeof(char), len, fp) != len) {
                free(str);
                return NULL; // Read error
            }
            str[len] = '\0'; // Null-terminate the string
        }
        return str;
    }
    
This function is fundamental. It encapsulates the process of reading your variable-length string data from the file.

3. Implement the loadGame Function

  • This is the core of the lesson. Follow the two-pass logic carefully.

    // In load.c, continued...
    
    // Helper to find an object by its tag
    static OBJECT *findObjectByTag(const char *tag, int numObjects) {
        if (!tag || *tag == '\0') return NULL;
        for (int i = 0; i < numObjects; i++) {
            if (objects[i].tag && strcmp(objects[i].tag, tag) == 0) {
                return &objects[i];
            }
        }
        return NULL;
    }
    
    bool loadGame(const char *filename) {
        FILE *fp = fopen(filename, "rb");
        if (!fp) {
            perror("fopen");
            return false;
        }
    
        // 1. Verify Header
        SaveHeader header;
        if (fread(&header, sizeof(SaveHeader), 1, fp) != 1) {
            fprintf(stderr, "Error: Could not read save file header.\n");
            fclose(fp);
            return false;
        }
        if (header.fourcc != 'ADVC' || header.ver.major != VERSION.major) {
            fprintf(stderr, "Error: Incompatible save file format or version.\n");
            fclose(fp);
            return false;
        }
        
        // 2. Read Object Count
        int numObjects = 0;
        if (fread(&numObjects, sizeof(int), 1, fp) != 1) {
            fprintf(stderr, "Error: Could not read object count.\n");
            fclose(fp);
            return false;
        }
    
        // We need a temporary array to hold the location tags for the 2nd pass
        char *location_tags[numObjects];
        
        // --- PASS 1: Data Ingestion ---
        for (int i = 0; i < numObjects; i++) {
            // Free any old strings if you are overwriting an existing game state
            free(objects[i].tag);
            free(objects[i].description);
            free(objects[i].details);
            
            objects[i].tag = loadString(fp);
            objects[i].description = loadString(fp);
            objects[i].details = loadString(fp);
            location_tags[i] = loadString(fp);
            
            if (objects[i].tag == NULL || objects[i].description == NULL) {
                fprintf(stderr, "Error loading object #%d\n", i);
                // Proper cleanup would be needed here in a real app
                fclose(fp);
                return false;
            }
        }
    
        // --- PASS 2: Pointer Relinking ---
        for (int i = 0; i < numObjects; i++) {
            objects[i].location = findObjectByTag(location_tags[i], numObjects);
            free(location_tags[i]); // Clean up the temporary tag
        }
    
        fclose(fp);
        printf("Game state loaded from %s.\n", filename);
        return true;
    }
    

4. Integrate with the Parser

  • Finally, add a load command to your game's command list.
    1. Include load.h in your parser file (e.g., main.c).
    2. Create the executeLoad function. Notice it also calls executeLook to show the player where they are after loading.
      // In main.c or wherever your command execution logic is
      #include "load.h"
      
      // Forward declaration if executeLook is defined later
      bool executeLook(void); 
      
      bool executeLoad(void) {
          if (loadGame("save.bin")) {
              // After loading, show the player their surroundings
              return executeLook(); 
          }
          return false;
      }
      
    3. Add it to your commands array:
      static const COMMAND commands[] = {
          /* ... your other commands ... */
          { "save",      executeSave },
          { "load",      executeLoad },
          { NULL,        NULL }
      };
      

Compile and run your program. Start a new game, move some items around, save the game, and then quit. Relaunch the program and immediately type load. If everything is correct, the look command should execute, and your inventory and the room's contents should be exactly as you left them.


Conclusion

You have successfully implemented deserialization, the counterpart to the serialization you built in the last lesson. You've demonstrated a deep understanding of C's memory model by navigating the complexities of pointer reconstruction, transforming a static file on disk back into a dynamic, living game world in memory.

Key Takeaways:

  • Deserialization is the process of reconstructing data structures from a stored format.
  • Loading pointer-based graphs requires a two-pass approach: first ingest all data, then relink the pointers once all potential targets exist in memory.
  • Robust loading includes header verification to prevent loading corrupt or incompatible save files.
  • Helper functions like loadString are essential for managing dynamic data and keeping the main loading logic clean.

In the next lesson, we will finalize the capstone project. We will integrate the save and load commands more formally and, most importantly, conduct a final round of debugging with GDB and memory checking with Valgrind to ensure your game is robust, correct, and free of memory errors.