mirror of
https://github.com/rwengine/openrw.git
synced 2024-11-22 10:22:52 +01:00
Proof of concept loading save data from save files
This commit is contained in:
parent
5399948e05
commit
cd7bb3af15
@ -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();
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -26,7 +26,8 @@ minute(0),
|
||||
cameraNear(0.1f),
|
||||
cameraFixed(false),
|
||||
cameraTarget(0),
|
||||
world(nullptr)
|
||||
world(nullptr),
|
||||
script(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user