Add code to consider an empty (zero'd out) save as an invalid checksum

The gen 2 saves contain 2 markers per section (main/backup) to serve as canary values to see whether an actual save is stored.
Because after all: checksum 0 is a valid checksum in some cases. So to distinguish between a valid and a corrupted save, these markers are used.

And now we use it too! This will be necessary to detect that there's no save file in case the save was wiped (or a reproduction cart is used)
This commit is contained in:
Philippe Symons 2024-09-11 12:25:05 +02:00
parent 33b4f9ce2f
commit de5381da81
4 changed files with 212 additions and 3 deletions

View File

@ -8,6 +8,7 @@ all:
$(MAKE) -C encodeText_gen2 $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C decodeSprite $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C addDistributionPoke $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C checkSave $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C gen2_gsball $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C gen2_addItem $(MAKECMDGOALS) $(MAKEOVERRIDES)
$(MAKE) -C gen2_removeItem $(MAKECMDGOALS) $(MAKEOVERRIDES)
@ -21,6 +22,7 @@ clean:
$(MAKE) -C encodeText_gen2 clean
$(MAKE) -C decodeSprite clean
$(MAKE) -C addDistributionPoke clean
$(MAKE) -C checkSave clean
$(MAKE) -C gen2_gsball clean
$(MAKE) -C gen2_addItem clean
$(MAKE) -C gen2_removeItem clean

View File

@ -0,0 +1,48 @@
# # Compiler flags
CXXFLAGS := -std=c++11 -fno-rtti -fno-exceptions -fno-unwind-tables -Wall -Wextra -I $(CURDIR) -I $(CURDIR)/../../include -g -Os
# Source files directory
SRC_DIR := .
# Build directory
BUILD_DIR := build
# Source files (add more as needed)
SRCS := $(shell find $(SRC_DIR) -type f -name '*.cpp')
# Object files
OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(BUILD_DIR)/%.o,$(SRCS))
LIBS := -lpokemegb
ifeq ($(PNG_SUPPORT),1)
LIBS += -lpng
endif
# Ensure necessary directories exist
# This function ensures the directory for the target exists
define make_directory
@mkdir -p $(dir $@)
endef
# Target executable
TARGET := ../../checkSave
# Phony targets
.PHONY: all clean
# Default target
all: $(TARGET)
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) $^ -o $@ $(LIBS) -L ../../
# Rule to compile source files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
$(make_directory)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Create the build directory if it doesn't exist
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# Clean rule
clean:
rm -rf $(BUILD_DIR) $(TARGET)

View File

@ -0,0 +1,73 @@
#include "gen1/Gen1GameReader.h"
#include "gen2/Gen2GameReader.h"
#include "RomReader.h"
#include "SaveManager.h"
#include "utils.h"
#include <cstdio>
#include <cstdint>
#include <cstdlib>
static void print_usage()
{
printf("Usage: checkSave <path/to/rom.gbc> <path/to/file.sav>\n");
}
int main(int argc, char** argv)
{
if(argc != 3)
{
print_usage();
return 1;
}
uint8_t* romBuffer;
uint8_t* savBuffer;
uint32_t romFileSize;
uint32_t savFileSize;
printf("rom: %s, save: %s\n", argv[1], argv[2]);
romBuffer = readFileIntoBuffer(argv[1], romFileSize);
if(!romBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[1]);
return 1;
}
savBuffer = readFileIntoBuffer(argv[2], savFileSize);
if(!savBuffer)
{
fprintf(stderr, "ERROR: Couldn't read file %s\n", argv[2]);
free(romBuffer);
romBuffer = nullptr;
return 1;
}
GameboyCartridgeHeader cartridgeHeader;
BufferBasedRomReader romReader(romBuffer, romFileSize);
BufferBasedSaveManager saveManager(savBuffer, savFileSize);
readGameboyCartridgeHeader(romReader, cartridgeHeader);
// check if we're dealing with gen 1
const Gen1GameType gen1Type = gen1_determineGameType(cartridgeHeader);
const Gen2GameType gen2Type = gen2_determineGameType(cartridgeHeader);
if (gen1Type != Gen1GameType::INVALID)
{
Gen1GameReader gameReader(romReader, saveManager, gen1Type);
printf("%s", (gameReader.isMainChecksumValid()) ? "Game save valid!\n" : "Game save NOT valid!\n");
}
else if (gen2Type != Gen2GameType::INVALID)
{
Gen2GameReader gameReader(romReader, saveManager, gen2Type);
printf("%s", (gameReader.isMainChecksumValid()) ? "Main save valid!\n" : "Main save NOT valid!\n");
printf("%s", (gameReader.isBackupChecksumValid()) ? "Backup save valid!\n" : "Backup save NOT valid!\n");
}
free(romBuffer);
romBuffer = 0;
free(savBuffer);
savBuffer = 0;
return 0;
}

View File

@ -12,6 +12,9 @@
#define EVENT_FLAGS_OFFSET_GOLDSILVER 0x261F
#define EVENT_FLAGS_OFFSET_CRYSTAL 0x2600
#define SAVE_CORRUPTION_CHECK1_EXPECTED_VALUE 99
#define SAVE_CORRUPTION_CHECK2_EXPECTED_VALUE 127
static const uint8_t crystalPicsBanks[] = {
CRYSTAL_BANK_PICS_1 + 0,
CRYSTAL_BANK_PICS_1 + 1,
@ -203,6 +206,80 @@ static void updateMainChecksum(ISaveManager &saveManager, bool isCrystal)
writeMainChecksum(saveManager, calculatedChecksum, isCrystal);
}
/**
* Gen 2 has 2 canary values for the main save to check whether SRAM actually contains a save
*/
static bool hasMainSave(ISaveManager& saveManager, bool isCrystal)
{
// check first canary value
uint8_t saveCorruptionCheckValue1;
uint8_t saveCorruptionCheckValue2;
// this value is at the same offset for Gold/Silver/Crystal
saveManager.seekToBankOffset(1, 0x8);
saveManager.readByte(saveCorruptionCheckValue1);
// check second canary
// for this one the offset differs in crystal
if(isCrystal)
{
saveManager.seekToBankOffset(1, 0xD0F);
}
else
{
saveManager.seekToBankOffset(1, 0xD6B);
}
saveManager.readByte(saveCorruptionCheckValue2);
// for there to be an actual save, the canary values need to be set to these exact values.
// Taken from the PRET pokecrystal and pokegold projects
// https://github.com/pret/pokecrystal/blob/symbols/pokecrystal.sym
// https://github.com/pret/pokegold/blob/symbols/pokegold.sym
return (saveCorruptionCheckValue1 == SAVE_CORRUPTION_CHECK1_EXPECTED_VALUE)
&& (saveCorruptionCheckValue2 == SAVE_CORRUPTION_CHECK2_EXPECTED_VALUE);
}
/**
* Gen 2 has 2 canary values for the main save to check whether SRAM actually contains a save
*/
static bool hasBackupSave(ISaveManager& saveManager, bool isCrystal)
{
// check first canary value
uint8_t saveCorruptionCheckValue1;
uint8_t saveCorruptionCheckValue2;
if(isCrystal)
{
saveManager.seekToBankOffset(0, 0x1208);
}
else
{
saveManager.seekToBankOffset(3, 0x1E38);
}
saveManager.readByte(saveCorruptionCheckValue1);
// check second canary
// for this one the offset differs in crystal
if(isCrystal)
{
saveManager.seekToBankOffset(0, 0x1F0F);
}
else
{
saveManager.seekToBankOffset(3, 0x1E6F);
}
saveManager.readByte(saveCorruptionCheckValue2);
// for there to be an actual save, the canary values need to be set to these exact values.
// Taken from the PRET pokecrystal and pokegold projects
// https://github.com/pret/pokecrystal/blob/symbols/pokecrystal.sym
// https://github.com/pret/pokegold/blob/symbols/pokegold.sym
return (saveCorruptionCheckValue1 == SAVE_CORRUPTION_CHECK1_EXPECTED_VALUE)
&& (saveCorruptionCheckValue2 == SAVE_CORRUPTION_CHECK2_EXPECTED_VALUE);
}
Gen2GameReader::Gen2GameReader(IRomReader &romReader, ISaveManager &saveManager, Gen2GameType gameType)
: romReader_(romReader), saveManager_(saveManager), spriteDecoder_(romReader), gameType_(gameType)
{
@ -592,10 +669,17 @@ uint8_t Gen2GameReader::getPokedexCounter(PokedexFlag dexFlag)
return result;
}
#include <cstdio>
bool Gen2GameReader::isMainChecksumValid()
{
const bool isCrystal = isGameCrystal();
if(!hasMainSave(saveManager_, isCrystal))
{
return false;
}
const uint16_t storedChecksum = readMainChecksum(saveManager_, isCrystal);
const uint16_t calculatedChecksum = calculateMainRegionChecksum(saveManager_, isCrystal);
@ -606,6 +690,11 @@ bool Gen2GameReader::isBackupChecksumValid()
{
const bool isCrystal = isGameCrystal();
if(!hasBackupSave(saveManager_, isCrystal))
{
return false;
}
const uint16_t storedChecksum = readBackupChecksum(saveManager_, isCrystal);
const uint16_t calculatedChecksum = calculateBackupRegionChecksum(saveManager_, isCrystal);
@ -692,8 +781,6 @@ bool Gen2GameReader::getEventFlag(uint16_t flagNumber)
return (byteVal & flag);
}
#include <cstdio>
void Gen2GameReader::setEventFlag(uint16_t flagNumber, bool enabled)
{
const uint16_t saveOffset = (isGameCrystal()) ? EVENT_FLAGS_OFFSET_CRYSTAL : EVENT_FLAGS_OFFSET_GOLDSILVER;
@ -713,7 +800,6 @@ void Gen2GameReader::setEventFlag(uint16_t flagNumber, bool enabled)
{
resultVal |= flag;
}
printf("%s: flagNumber %hu, enabled: %d, orig byte: 0x%02x, new byte 0x%02x\n", __FUNCTION__, flagNumber, enabled, byteVal, resultVal);
saveManager_.writeByte(resultVal);
}