{"id":43,"date":"2021-07-13T14:19:43","date_gmt":"2021-07-13T14:19:43","guid":{"rendered":"https:\/\/blogs.scummvm.org\/ayyg\/?p=43"},"modified":"2021-07-13T14:19:43","modified_gmt":"2021-07-13T14:19:43","slug":"week_n-saves","status":"publish","type":"post","link":"https:\/\/blogs.scummvm.org\/ayyg\/2021\/07\/13\/week_n-saves\/","title":{"rendered":"Week_n = Saves"},"content":{"rendered":"\n<p>Let&#8217;s jump right into the main focus of this week&#8217;s development: savefiles.<\/p>\n\n\n\n<p>The original game uses a FILE setup to read and write savefiles, which we will need to refactor as with everything else we&#8217;ve done so far. <\/p>\n\n\n\n<p>I will outline the process of how a save is written\/read:<\/p>\n\n\n\n<p>When we click &#8220;save&#8221; in the options menu, a event for <code>cmdOptionsSaveGame<\/code> is sent, which eventually leads to a call for <code>saveGameState<\/code>. Here is the GDB Backtrace:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#0  Saga2::saveGameState(short, char*)\n    (saveNo=32767, saveName=0x7fffffffa170 \"\\263\\212\\265A\")\n    at engines\/saga2\/loadsave.cpp:131\n#1  0x0000555555f34a1a in Saga2::cmdFileSave(Saga2::gEvent&amp;) (ev=...)\n    at engines\/saga2\/uidialog.cpp:1662\n#2  0x0000555555e130c5 in Saga2::gPanel::notify(Saga2::gEventType, int)\n    (this=0x60e00002fb40, type=Saga2::gEventNewValue, value=1)\n    at engines\/saga2\/panel.cpp:169\n#3  0x0000555555f464e0 in Saga2::gCompButton::pointerRelease(Saga2::gPanelMessage&amp;)\n    (this=0x60e00002fb40) at engines\/saga2\/button.cpp:553\n#4  0x0000555555e1baeb in Saga2::gToolBase::handleMouse(Common::Event&amp;, unsigned int)\n    (this=0x555557592e40 &lt;Saga2::G_BASE&gt;, event=..., time=71827)\n    at engines\/saga2\/panel.cpp:985\n#5  0x0000555555d843e8 in Saga2::processEventLoop(bool) (updateScreen=true)\n    at engines\/saga2\/main.cpp:304\n#6  0x0000555555d840b8 in Saga2::EventLoop(bool&amp;, bool)\n    (running=@0x7fffffffa5e0: false) at engines\/saga2\/main.cpp:270\n#7  0x0000555555f2ea42 in Saga2::FileDialog(short) (fileProcess=0)\n    at engines\/saga2\/uidialog.cpp:760\n#8  0x0000555555f351ca in Saga2::cmdOptionsSaveGame(Saga2::gEvent&amp;) (ev=...)\n    at engines\/saga2\/uidialog.cpp:1719\n#9  0x0000555555e130c5 in Saga2::gPanel::notify(Saga2::gEventType, int)\n    (this=0x60e00002e720, type=Saga2::gEventNewValue, value=1)\n    at engines\/saga2\/panel.cpp:169\n#10 0x0000555555f464e0 in Saga2::gCompButton::pointerRelease(Saga2::gPanelMessage&amp;)\n    (this=0x60e00002e720) at engines\/saga2\/button.cpp:553\n#11 0x0000555555e1baeb in Saga2::gToolBase::handleMouse(Common::Event&amp;, unsigned int)\n    (this=0x555557592e40 &lt;Saga2::G_BASE&gt;, event=..., time=58752)\n    at engines\/saga2\/panel.cpp:985\n#12 0x0000555555d843e8 in Saga2::processEventLoop(bool) (updateScreen=true)\n    at engines\/saga2\/main.cpp:304<\/code><\/pre>\n\n\n\n<p>I will showcase its contents:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void saveGameState( int16 saveNo, char *saveName )\n{\n\tpauseTimer();\n\n\ttry\n\t{\n\t\tSaveFileConstructor saveGame( saveNo, saveName );\n\n\t\tsaveGlobals( saveGame );\n\t\tsaveTimer( saveGame );\n...\n\t\tsavePaletteState( saveGame );\n\t\tsaveContainerNodes( saveGame );\n\t}\n\tcatch ( SaveFileWriteError writeError )\n\t{\n\t\t\t\/\/\tFor now this simply rethrows the exception.  Eventually,\n\t\t\t\/\/\tthis must notify the user that the game was not saved and\n\t\t\t\/\/\trecover gracefully.\n\t\t\/\/throw writeError;\n\t\tthrow SystemError(cpSavFileWrite,\"Writing saved game\");\n\t}\n\n\tresumeTimer();\n}<\/code><\/pre>\n\n\n\n<p>I&#8217;ve summarized it a bit to keep it simple, but the gist of it is that we create this <code>SaveFileConstructor<\/code> object and pass it through all of these methods, saving parts of the game into <code>saveGame<\/code>.<\/p>\n\n\n\n<p>This is how one of those methods look like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/-----------------------------------------------------------------------\n\/\/\tStore miscellaneous globals in a save file\n\nvoid saveGlobals( SaveFileConstructor &amp;saveGame )\n{\n\tGlobalsArchive\tarchive;\n\n\tarchive.objectIndex\t\t\t\t\t= objectIndex;\n\tarchive.actorIndex\t\t\t\t\t= actorIndex;\n\tarchive.brotherBandingEnabled\t\t= brotherBandingEnabled;\n\tarchive.centerActorIndicatorEnabled\t= centerActorIndicatorEnabled;\n\tarchive.interruptableMotionsPaused\t= interruptableMotionsPaused;\n\tarchive.objectStatesPaused\t\t\t= objectStatesPaused;\n\tarchive.actorStatesPaused\t\t\t= actorStatesPaused;\n\tarchive.actorTasksPaused\t\t\t= actorTasksPaused;\n\tarchive.combatBehaviorEnabled\t\t= combatBehaviorEnabled;\n\tarchive.backgroundSimulationPaused\t= backgroundSimulationPaused;\n\n\tsaveGame.writeChunk(\n\t\tMakeID( 'G', 'L', 'O', 'B' ),\n\t\t&amp;archive,\n\t\tsizeof( archive ) );\n}<\/code><\/pre>\n\n\n\n<p>We may as well take a look into this <code>writeChunk<\/code> method to see how exactly one writes into the file.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/----------------------------------------------------------------------\n\/\/\tCreate a new chunk and write the chunk data.\n\nbool SaveFileConstructor::writeChunk( ChunkID id, void *buf, int32 size )\n{\n\t\t\/\/\tDetermine if file position is at end of previous chunk\n\tif ( posInChunk &lt; chunkSize ) return FALSE;\n\n\tASSERT( posInChunk == chunkSize );\n\n\tSaveFileChunkInfo\tchunkHeader;\n\n\t\t\/\/\tInitialize the chunk header\n\tchunkHeader.id = id;\n\tchunkHeader.size = size;\n\n\t\t\/\/\tWrite the chunk header\n\tif ( fwrite( &amp;chunkHeader, sizeof( chunkHeader ), 1, fileHandle ) != 1 )\n\t\tthrow SaveFileWriteError( \"Error writing save game chunk header\" );\n\n\t\t\/\/\tWrite the chunk data\n\tif ( size &gt; 0 &amp;&amp; fwrite( buf, size, 1, fileHandle ) != 1 )\n\t\tthrow SaveFileWriteError( \"Error writing save game data\" );\n\n\t\t\/\/\tInitialize the chunk varibles to indicate the file position is\n\t\t\/\/\tat the end of the chunk\n\tchunkSize = posInChunk = size;\n\n\t\t\/\/\tReturn success\n\treturn TRUE;\n}<\/code><\/pre>\n\n\n\n<p>The important takeaway here is that we first write the &#8220;chunk&#8221; header, which consists of the id (the 4 bytes that serve as the identifier, in this case &#8220;GLOB&#8221;) and the size. After that comes the actual contents that we want to save, that we store in a <code>GlobalsArchive<\/code> here.<\/p>\n\n\n\n<p>So then, how do we refactor this for portability? The answer is to use streams. More specifically, for writing into savefiles, we use <code><a href=\"https:\/\/doxygen.scummvm.org\/dc\/d16\/class_common_1_1_out_save_file.html\" data-type=\"URL\" data-id=\"https:\/\/doxygen.scummvm.org\/dc\/d16\/class_common_1_1_out_save_file.html\">Common::OutSaveFile<\/a><\/code> and for reading we use <code><a href=\"https:\/\/doxygen.scummvm.org\/d6\/d4a\/group__common__savefile.html#gacecc33709ca5b50089b387cb6565fdc8\" data-type=\"URL\" data-id=\"https:\/\/doxygen.scummvm.org\/d6\/d4a\/group__common__savefile.html#gacecc33709ca5b50089b387cb6565fdc8\">Common::InSaveFile<\/a><\/code>. The latter is simply a <code>typedef<\/code> for <code>Common::SeekableReadStream<\/code>.<\/p>\n\n\n\n<p>You remember how we declared <code>saveGame<\/code> inside of <code>saveGameState<\/code>? We shall do the same thing here. We declare a <code>Common::OutSaveFile *out<\/code> pointer that we initialize using OSystem&#8217;s <code>SaveFileManager<\/code>.<sup>1<\/sup> After that, we send it as an argument to all of these methods. Obviously, that requires us to rewrite all of these individual methods.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Common::Error saveGameState(int16 saveNo, char *saveName) {\n\tpauseTimer();\n\n\tdebugC(1, kDebugSaveload, \"Saving game\");\n\n\tCommon::OutSaveFile *out = g_vm-&gt;getSaveFileManager()-&gt;openForSaving(getSaveFileName(saveNo), false);\n\tif (!out)\n\t\treturn Common::kCreatingFileFailed;\n\n\tSaveFileHeader header;\n\theader.gameID = gameID;\n\theader.saveName = saveName;\n\n\theader.write(out);\n\n\tsaveGlobals(out);\n\tsaveTimer(out);\n...\n\tsavePaletteState(out);\n\tsaveContainerNodes(out);\n\n\tout-&gt;finalize();\n\n\tdelete out;\n\n\tresumeTimer();\n\n\treturn Common::kNoError;\n}<\/code><\/pre>\n\n\n\n<p>And then finally, once we&#8217;re done writing all of the data, we call <code>out-&gt;finalize()<\/code> and delete the object.<\/p>\n\n\n\n<p>The code for <code>saveGlobals<\/code> becomes as follows, by the way:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void saveGlobals(Common::OutSaveFile *out) {\n\tout-&gt;write(\"GLOB\", 4);\n\tout-&gt;writeUint32LE(sizeof(GlobalsArchive));\n\n\tout-&gt;writeUint32LE(objectIndex);\n\tout-&gt;writeUint32LE(actorIndex);\n\tout-&gt;writeByte(brotherBandingEnabled);\n\tout-&gt;writeByte(centerActorIndicatorEnabled);\n\tout-&gt;writeByte(interruptableMotionsPaused);\n\tout-&gt;writeByte(objectStatesPaused);\n\tout-&gt;writeByte(actorStatesPaused);\n\tout-&gt;writeByte(actorTasksPaused);\n\tout-&gt;writeByte(combatBehaviorEnabled);\n\tout-&gt;writeByte(backgroundSimulationPaused);\n}<\/code><\/pre>\n\n\n\n<p>I&#8217;ve removed some debug message calls that aren&#8217;t of interest to us. As you can see, we write the chunk header first, and then write all of the individual members of <code>GlobalsArchive<\/code> according to their order within the struct. An important thing to note here is the <code>sizeof(GlobalsArchive)<\/code>. Due to padding shenanigans, the size of these structs may not be the same for all platforms. This obviously creates portability issues when you try to load saves from other machines. That is why we need to change each and every of one of these suspicious <code>sizeof<\/code> operations to a constant such as <code>kGlobalsArchiveSize<\/code>.<\/p>\n\n\n\n<p>As for loading, the process is very similar:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void loadSavedGameState(int16 saveNo) {\n\tuint32  loadFlags = 0;\n\n\tpauseTimer();\n\n\tCommon::InSaveFile *in = g_vm-&gt;getSaveFileManager()-&gt;openForLoading(getSaveFileName(saveNo));\n\tChunkID         id;\n\tint32           chunkSize;\n\tbool            notEOF;\n\n\tnotEOF = firstChunk(in, id, chunkSize);\n\twhile (notEOF) {\n\t\tswitch (id) {\n\t\tcase MKTAG('G', 'L', 'O', 'B'):\n\t\t\tloadGlobals(in);\n\t\t\tloadFlags |= loadGlobalsFlag;\n\t\t\tbreak;\n                ...\n                }\n                \n                notEOF = nextChunk(in, id, chunkSize);\n        }\n...\n}<\/code><\/pre>\n\n\n\n<p>I&#8217;ve omitted some code for simplicity. Here we construct a <code>Common::InSaveFile<\/code> similar to before. We load in each chunk&#8217;s header with <code>firstChunk<\/code> or <code>nextChunk<\/code><sup>2<\/sup>, and according to the ChunkID we load in the appropriate chunk.<br><code>loadGlobals<\/code> is like this, by the way:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>void loadGlobals(Common::InSaveFile *in) {\n\tobjectIndex = in-&gt;readUint32LE();\n\tactorIndex = in-&gt;readUint32LE();\n\tbrotherBandingEnabled = in-&gt;readByte();\n\tcenterActorIndicatorEnabled = in-&gt;readByte();\n\tinterruptableMotionsPaused = in-&gt;readByte();\n\tobjectStatesPaused = in-&gt;readByte();\n\tactorStatesPaused = in-&gt;readByte();\n\tactorTasksPaused = in-&gt;readByte();\n\tcombatBehaviorEnabled = in-&gt;readByte();\n\tbackgroundSimulationPaused = in-&gt;readByte();\n}<\/code><\/pre>\n\n\n\n<p>So, all in all, it&#8217;s not very hard work. As long as one pays attention to the sizes<sup>3<\/sup>, there is not much to think about. I personally cut down some time by streamlining with Vim macros, and I&#8217;m sure someone could write a script to automate much of this, but I didn&#8217;t go that far.<\/p>\n\n\n\n<p>Now, I&#8217;m showing all of the code commented out, but we proceeded in steps. We started with an <code>#if 0<\/code> block surrounding all of the save methods and took things out of it one by one. It took a while, but because of that I learned a new skill: Vim macros.<\/p>\n\n\n\n<p>An interesting thing to note here, is that because the code is mostly the same, it&#8217;s very easy to notice when it&#8217;s different. In the majority of the code we use methods such as <code>void *archive(...)<\/code> or <code>constructX(void**)<\/code>, but in <code>spellio.cpp<\/code> the save\/loading methods are named quite different:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ ------------------------------------------------------------------\n\/\/ serialize active spells\n\nvoid saveSpellState( SaveFileConstructor &amp;saveGame )\n{\n\tactiveSpells.save(saveGame);\n}\n\n\/\/ ------------------------------------------------------------------\n\/\/ read serialized active spells\n\nvoid loadSpellState( SaveFileReader &amp;saveGame )\n{\n\tactiveSpells.load(saveGame);\n}<\/code><\/pre>\n\n\n\n<p>This leads me to ponder that the spell system was written much later (or earlier) in development.<\/p>\n\n\n\n<hr class=\"wp-block-separator\" \/>\n\n\n\n<p>Now, this is all very fun, but even after doing all of that the work is not done. We still need to: Get rid of the original code; Replace sizeof by portable alternatives; Fix any crashes or bugs detected during playtest. As the saying goes, work never ends!<\/p>\n\n\n\n<hr class=\"wp-block-separator\" \/>\n\n\n\n<p>[1] <code>getSaveFileName(int16)<\/code> is a method that returns the filename according to the slot. For instance getSaveFileName(0) returns &#8220;000.SAV&#8221; as a Common::String.<\/p>\n\n\n\n<p>[2] The original had methods for seeking to the next chunk when the chunk is thought to not be completed. I removed those operations because they did not seem necessary. I believe this shouldn&#8217;t be a problem for saves created locally, but I wonder if it will have an effect when loading original saves. <em>I&#8217;m writing this here so that I don&#8217;t forget it one week later when I can&#8217;t get loading to work for mysterious reasons.<\/em><\/p>\n\n\n\n<p>[3] Extra points if the size is knowable. There were some instances of <code>enum SpellID<\/code> being saved into the archive. This is a problem because enums could have all kinds of size depending on the compiler. Will this have to be reverse engineered !?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Let&#8217;s jump right into the main focus of this week&#8217;s development: savefiles. The original game uses a FILE setup to read and write savefiles, which we will need to refactor as with everything else we&#8217;ve done so far. I will outline the process of how a save is written\/read: When we click &#8220;save&#8221; in the [&hellip;]<\/p>\n","protected":false},"author":7,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-43","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/posts\/43","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/users\/7"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/comments?post=43"}],"version-history":[{"count":2,"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/posts\/43\/revisions"}],"predecessor-version":[{"id":45,"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/posts\/43\/revisions\/45"}],"wp:attachment":[{"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/media?parent=43"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/categories?post=43"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.scummvm.org\/ayyg\/wp-json\/wp\/v2\/tags?post=43"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}