1
0
mirror of https://github.com/rwengine/openrw.git synced 2024-09-15 15:02:34 +02:00

Proof of concept loading save data from save files

This commit is contained in:
Daniel Evans 2015-06-14 02:44:51 +01:00
parent 5399948e05
commit cd7bb3af15
8 changed files with 409 additions and 19 deletions

View File

@ -10,6 +10,7 @@
class GameWorld;
class GameObject;
class ScriptMachine;
class PlayerController;
struct CutsceneData;
@ -97,6 +98,39 @@ struct BlipData
{ }
};
/**
* Data for garages
*/
struct GarageInfo
{
enum /*GarageType*/ {
GARAGE_MISSION = 1,
GARAGE_BOMBSHOP1 = 2,
GARAGE_BOMBSHOP2 = 3,
GARAGE_BOMBSHOP3 = 4,
GARAGE_RESPRAY = 5,
GARAGE_INVALID = 6,
GARAGE_SPECIFIC_CARS_ONLY = 7, /* See Opcode 0x21B */
GARAGE_COLLECTCARS1 = 8, /* See Opcode 0x03D4 */
GARAGE_COLLECTCARS2 = 9,
GARAGE_COLLECTCARS3 = 10, /* Unused */
GARAGE_OPENFOREXIT = 11,
GARAGE_INVALID2 = 12,
GARAGE_CRUSHER = 13, /* Unused */
GARAGE_MISSION_KEEPCAR = 14,
GARAGE_FOR_SCRIPT = 15,
GARAGE_HIDEOUT_ONE = 16, /* Portland */
GARAGE_HIDEOUT_TWO = 17, /* Staunton */
GARAGE_HIDEOUT_THREE = 18, /* Shoreside */
GARAGE_FOR_SCRIPT2 = 19,
GARAGE_OPENS_FOR_SPECIFIC_CAR = 20,
GARAGE_OPENS_ONCE = 21
};
glm::vec3 min;
glm::vec3 max;
int type;
};
/**
* Gameplay state object that holds persistent state, and references runtime
* world state.
@ -177,11 +211,18 @@ struct GameState
std::map<int, BlipData> radarBlips;
std::vector<GarageInfo> garages;
/**
* World to use for this state, this isn't saved, just used at runtime
*/
GameWorld* world;
/**
* Script Machine associated with this state if it exists.
*/
ScriptMachine* script;
GameState();
/**

View File

@ -17,6 +17,25 @@ class SaveGame
{
public:
/**
* Writes the entire game state to a file format that closely approximates
* the format used in GTA III
*/
static void writeGame(GameState& state, const std::string& file);
/**
* Loads an entire Game State from a file, using a format similar to the
* format used by GTA III.
*
* It assumes that the state already has a world and script that have been
* initalized to the same state as the game being loaded.
* @return status, false if failure occured.
*/
static bool loadGame(GameState& state, const std::string& file);
/**
* Writes the current game state out into a file suitable for loading later.
*/
static void writeState(GameState& state, const std::string& file);
static bool loadState(GameState& state, const std::string& file);

View File

@ -17,7 +17,7 @@
* Changing this will break saves.
*/
#define SCM_VARIABLE_SIZE 4
#define SCM_STACK_DEPTH 32
#define SCM_STACK_DEPTH 4
class GameState;

View File

@ -26,7 +26,8 @@ minute(0),
cameraNear(0.1f),
cameraFixed(false),
cameraTarget(0),
world(nullptr)
world(nullptr),
script(nullptr)
{
}

View File

@ -6,6 +6,7 @@
#include <objects/CharacterObject.hpp>
#include <script/ScriptMachine.hpp>
#include <script/SCMFile.hpp>
#include <ai/PlayerController.hpp>
#include <fstream>
#include <cereal/cereal.hpp>
@ -88,6 +89,16 @@ void serialize(Archive& archive,
s.display);
}
template<class Archive>
void serialize(Archive& archive,
GarageInfo& s)
{
archive(
s.min,
s.max,
s.type);
}
template<class Archive>
void serialize(Archive& archive,
GameState& s)
@ -313,3 +324,310 @@ bool SaveGame::loadObjects(GameWorld& world, const std::string& file)
return true;
}
// Original save game file data structures
typedef uint16_t BlockWord;
typedef uint32_t BlockDword;
typedef BlockDword BlockSize;
struct Block0Data {
uint16_t saveName[24];
BlockWord year;
BlockWord month;
BlockWord weekday;
BlockWord day;
BlockWord hour;
BlockWord minute;
BlockWord second;
BlockWord milliseconds;
BlockDword unknown;
BlockDword islandNumber;
glm::vec3 cameraPosition;
BlockDword gameMinuteMS;
BlockDword lastTick;
uint8_t gameHour;
uint8_t _align0[3];
uint8_t gameMinute;
uint8_t _align1[3];
BlockWord padMode;
uint8_t _align2[2];
BlockDword timeMS;
float timeScale;
float timeStep;
float timeStep_unclipped; // Unknown purpose
BlockDword frameCounter;
float timeStep2;
float framesPerUpdate;
float timeScale2;
BlockWord lastWeather;
uint8_t _align3[2];
BlockWord nextWeather;
uint8_t _align4[2];
BlockWord forcedWeather;
uint8_t _align5[2];
float weatherInterpolation;
uint8_t dateTime[24]; // Unused
BlockDword weatherType;
float cameraData;
float cameraData2;
};
struct Block0ContactInfo {
BlockDword missionFlag;
BlockDword baseBrief;
};
struct Block0BuildingSwap {
BlockDword type;
BlockDword handle;
BlockDword newModel;
BlockDword oldModel;
};
struct Block0InvisibilitySettings {
BlockDword type;
BlockDword handle;
};
struct Block0RunningScript {
uint32_t nextPointer;
uint32_t prevPointer;
char name[8];
BlockDword programCounter;
BlockDword stack[4];
BlockDword unknown0;
BlockDword unknown1;
BlockWord stackCounter;
BlockWord unknown2;
BlockDword scriptVariables[16];
BlockDword timerA;
BlockDword timerB;
uint8_t ifFlag;
uint8_t unknown3;
uint8_t unknown4;
uint8_t _align0;
BlockDword wakeTimer;
BlockWord ifNumber; // ?
uint8_t unknown[6];
};
struct Block0ScriptData {
BlockDword onMissionOffset;
Block0ContactInfo contactInfo[16];
uint8_t unknown[0x100];
BlockDword lastMissionPassedTime;
Block0BuildingSwap buildingSwap[25];
Block0InvisibilitySettings invisibilitySettings[20];
uint8_t scriptRunning;
uint8_t _align0[3];
BlockDword mainSize;
BlockDword largestMissionSize;
BlockWord missionCount;
uint8_t _align1[2];
};
struct StructWeaponSlot {
BlockDword weaponId;
BlockDword unknown0;
BlockDword inClip;
BlockDword totalBullets;
BlockDword unknown1;
BlockDword unknown2;
};
struct StructPed {
uint8_t unknown0_[52];
glm::vec3 position;
uint8_t unknown1[640];
float health;
float armour;
uint8_t unknown2[148];
StructWeaponSlot weapons[13];
uint8_t unknown3[348];
};
// NOTE commented members are read manually, due to alignment.
struct Block1PlayerPed {
//BlockDword unknown0;
//BlockWord unknown1;
alignas(uint8_t) BlockDword reference; // 0x0A
alignas(uint8_t) StructPed info; // 0x0C
BlockDword maxWantedLevel;
BlockDword maxChaosLevel;
uint8_t modelName[24];
uint8_t align[2];
};
void SaveGame::writeGame(GameState& state, const std::string& file)
{
std::FILE* saveFile = std::fopen(file.c_str(), "w");
// BLOCK 0 - Variables
Block0Data block0Data = {};
//strcpy(block0Data.saveName, "OpenRW Save Game");
block0Data.islandNumber = 1;
block0Data.cameraPosition = glm::vec3(0.f);
block0Data.gameMinuteMS = 1000;
block0Data.lastTick = 1;
block0Data.gameHour = 12;
block0Data.gameMinute = 13;
block0Data.padMode = 1;
block0Data.timeMS = 10000;
block0Data.timeScale = 1.0;
block0Data.timeStep = 1.0/60.f;
block0Data.timeStep_unclipped = block0Data.timeStep;
block0Data.frameCounter = 1000;
block0Data.timeStep2 = 1.0;
block0Data.framesPerUpdate = 1.0;
block0Data.timeScale2 = 1.0;
block0Data.lastWeather = 1;
block0Data.nextWeather = 1;
block0Data.weatherInterpolation = 1.0;
block0Data.weatherType = 1;
BlockSize block0Size = sizeof(block0Data); // TODO calculate correctly.
fwrite(&block0Size, sizeof(BlockSize), 1, saveFile);
fwrite(&block0Data, sizeof(block0Data), 1, saveFile);
// BLOCK 0 - 0 Script
const char header[4] = "SCR";
BlockSize block0ScriptSize = sizeof(Block0ScriptData);
BlockSize block0ScriptHeaderSize = block0ScriptSize + sizeof(char) * 4 + sizeof(BlockDword);
fwrite(header, sizeof(char), 4, saveFile);
fwrite(&block0ScriptHeaderSize, sizeof(BlockSize), 1, saveFile);
BlockDword scriptVariablesCount = state.script->getGlobalData().size();
fwrite(&scriptVariablesCount, sizeof(BlockDword), 1, saveFile);
fwrite(state.script->getGlobals(), sizeof(SCMByte), scriptVariablesCount, saveFile);
BlockDword scriptDataSize = 0x03C8;
fwrite(&scriptDataSize, sizeof(BlockDword), 1, saveFile);
Block0ScriptData block0ScriptData = {};
block0ScriptData.onMissionOffset = (BlockDword)((SCMByte*)state.scriptOnMissionFlag - state.script->getGlobals());
block0ScriptData.lastMissionPassedTime = 0;
block0ScriptData.scriptRunning = 1;
block0ScriptData.mainSize = state.script->getFile()->getMainSize();
block0ScriptData.largestMissionSize = state.script->getFile()->getLargestMissionSize();
block0ScriptData.missionCount = state.script->getFile()->getMissionOffsets().size();
fwrite(&block0ScriptData, sizeof(block0ScriptData), 1, saveFile);
BlockDword scriptCount = state.script->getThreads().size();
fwrite(&scriptCount, sizeof(BlockDword), 1, saveFile);
for(SCMThread& thread : state.script->getThreads())
{
Block0RunningScript script = {};
strcpy(script.name, thread.name);
script.programCounter = thread.programCounter;
for(int i = 0; i < SCM_STACK_DEPTH; i++) {
script.stack[i] = thread.calls[i];
}
script.stackCounter = thread.stackDepth;
for(int i = 0; i < 16; i++) {
script.scriptVariables[i] = *(((BlockDword*)thread.locals.data())+i);
}
script.timerA = *(BlockDword*)(thread.locals.data() + 16 * sizeof ( SCMByte ) * 4);
script.timerB = *(BlockDword*)(thread.locals.data() + 16 * sizeof ( SCMByte ) * 4);
script.ifFlag = thread.conditionResult;
script.wakeTimer = thread.wakeCounter;
script.ifNumber = thread.conditionCount;
fwrite(&script, sizeof(block0ScriptData), 1, saveFile);
}
}
const size_type DWORDSZ = sizeof(BlockDword);
BlockDword readDword(std::FILE* file)
{
BlockDword sz;
fread(&sz, sizeof(BlockDword), 1, file);
}
bool SaveGame::loadGame(GameState& state, const std::string& file)
{
std::FILE* loadFile = std::fopen(file.c_str(), "r");
// BLOCK 0
BlockDword blockSize;
fread(&blockSize, sizeof(BlockDword), 1, loadFile);
Block0Data block0Data;
fread(&block0Data, sizeof(block0Data), 1, loadFile);
BlockDword scriptBlockSize;
fread(&scriptBlockSize, sizeof(BlockDword), 1, loadFile);
char signature[4];
fread(signature, sizeof(char), 4, loadFile);
if( std::strncmp(signature, "SCR", 3) != 0 ) {
return false;
}
fread(&scriptBlockSize, sizeof(BlockDword), 1, loadFile);
BlockDword scriptVars;
fread(&scriptVars, sizeof(BlockDword), 1, loadFile);
SCMByte bytes[scriptVars];
fread(bytes, sizeof(SCMByte), scriptVars, loadFile);
BlockDword scriptDataBlockSize;
fread(&scriptDataBlockSize, sizeof(BlockDword), 1, loadFile);
if( scriptDataBlockSize != 0x03C8 ) {
return false;
}
Block0ScriptData scriptData;
fread(&scriptData, sizeof(Block0ScriptData), 1, loadFile);
BlockDword numScripts;
fread(&numScripts, DWORDSZ, 1, loadFile);
Block0RunningScript scripts[numScripts];
fread(scripts, sizeof(Block0RunningScript), numScripts, loadFile);
// BLOCK 1
BlockDword playerBlockSize = readDword(loadFile);
BlockDword playerInfoSize = readDword(loadFile);
BlockDword playerCount = readDword(loadFile);
Block1PlayerPed players[playerCount];
BlockDword unknownPlayerValue = readDword(loadFile);
BlockWord unknownPlayerValue2;
fread(&unknownPlayerValue2, sizeof(BlockWord), 1, loadFile);
fread(players, sizeof(Block1PlayerPed), playerCount, loadFile);
// Insert Game State.
state.hour = block0Data.gameHour;
state.minute = block0Data.gameMinute;
state.gameTime = block0Data.timeMS / 1000.f;
state.currentWeather = block0Data.nextWeather;
state.cameraPosition = block0Data.cameraPosition;
for(int v = 0; v < scriptVars; ++v) {
state.script->getGlobals()[v] = bytes[v];
}
state.scriptOnMissionFlag = (unsigned int*)state.script->getGlobals() + (size_t)scriptData.onMissionOffset;
auto& threads = state.script->getThreads();
for(int s = 0; s < numScripts; ++s) {
state.script->startThread(scripts[s].programCounter);
SCMThread& thread = threads.back();
// thread.baseAddress // ??
thread.conditionResult = scripts[s].ifFlag;
thread.conditionCount = scripts[s].ifNumber;
thread.stackDepth = scripts[s].stackCounter;
for(int i = 0; i < SCM_STACK_DEPTH; ++i) {
thread.calls[i] = scripts[s].stack[i];
}
thread.wakeCounter = scripts[s].wakeTimer;
}
if( playerCount > 0 ) {
auto player = state.world->createPlayer(players[0].info.position);
player->mHealth = players[0].info.health;
state.playerObject = player->getGameObjectID();
state.maxWantedLevel = players[0].maxWantedLevel;
}
return true;
}

View File

@ -373,15 +373,17 @@ void game_get_player(const ScriptArguments& args)
void game_create_garage(const ScriptArguments& args)
{
// http://www.gtamodding.com/index.php?title=Garage#GTA_III
glm::vec3 min(args[0].real, args[1].real, args[2].real);
glm::vec3 max(args[3].real, args[4].real, args[5].real);
/// @todo http://www.gtamodding.com/index.php?title=Garage#GTA_III
int garageType = args[6].integer;
args.getWorld()->state->garages.push_back({
min, max, garageType
});
// TODO actually store the garage information and return the handle
*args[7].globalInteger = 0;
args.getWorld()->logger->warning("SCM", "Garages Unimplemented! " + std::to_string(garageType));
*args[7].globalInteger = args.getWorld()->state->garages.size()-1;
}
void game_disable_ped_paths(const ScriptArguments& args)

View File

@ -1,6 +1,7 @@
#include <boost/test/unit_test.hpp>
#include <engine/GameState.hpp>
#include <engine/SaveGame.hpp>
#include <script/ScriptMachine.hpp>
#include <test_globals.hpp>
BOOST_AUTO_TEST_SUITE(SaveGameTests)
@ -44,6 +45,22 @@ BOOST_AUTO_TEST_CASE(test_write_state)
BOOST_CHECK_EQUAL( loaded.overrideNextStart, state.overrideNextStart );
BOOST_CHECK_EQUAL( loaded.hour, state.hour );
BOOST_CHECK_EQUAL( loaded.minute, state.minute );
// Check Garage data + garage vehicle restoration
}
BOOST_AUTO_TEST_CASE(test_load_game)
{
GameState state;
GameWorld world();
SCMOpcodes s;
auto file = Global::get().d->loadSCM("data/main.scm");
ScriptMachine machine(&state, file, &s);
state.world = Global::get().e;
state.script = &machine;
BOOST_REQUIRE( SaveGame::loadGame(state, "GTA3sf1.b") );
}
BOOST_AUTO_TEST_SUITE_END()

View File

@ -187,26 +187,17 @@ void RWGame::saveGame(const std::string& savename)
void RWGame::loadGame(const std::string& savename)
{
delete state->world;
//delete state;
delete state->script;
state = nullptr;
newGame();
startScript("data/main.scm");
if(! SaveGame::loadScript(*script, savename+".script") )
if(! SaveGame::loadGame(*state, "GTA3sf1.b") )
{
log.error("Game", "Failed to restore Script");
log.error("Game", "Failed to load game");
}
if(! SaveGame::loadState(*state, savename+".state") )
{
log.error("Game", "Failed to restore State");
}
if(! SaveGame::loadObjects(*world, savename+".world") )
{
log.error("Game", "Failed to restore World");
}
// TODO objects.
}
void RWGame::startScript(const std::string& name)
@ -246,6 +237,7 @@ void RWGame::startScript(const std::string& name)
log.info("Script", ss.str());
});
script->addBreakpoint(0);
state->script = script;
}
else {
log.error("Game", "Failed to load SCM: " + name);