#106272 - sgeos - Tue Oct 17, 2006 12:36 pm
Many programs (open office, nethack, etc) have crash recovery systems. Even if the power cuts out, you don't lose your work. What is the best way to implement a crash recovery system on the PC? (tools) What would be the best way to implement a crash recovery system on something like the GBA? (power cuts out or pak is removed)
-Brendan
#106279 - tepples - Tue Oct 17, 2006 2:31 pm
Crash recovery can be accomplished either through journaling (write a list of transactions that can be used to reconstruct the current state, where each transaction represents a small change and can be stored small) or through periodically writing the whole document to another file. How to best implement crash recovery depends on the nature of your document's state, on the nature of the transactions, and on the nature of your storage medium. It will differ for a text editor, for an audio editor, for a simple game, or for a complex game. It will also differ for the PC, for the Game Boy Advance, and for the Nintendo DS.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.
#106309 - poslundc - Tue Oct 17, 2006 7:55 pm
I've implemented a virtual save system in the past. The basic principle is that if you have three save slots you instead use four (or more) physical save slots, and cycle through the slots when saving your game. You record both the virtual slot it is being saved into (so I could be saving into the 2nd slot virtually when I'm saving into the 5th physical slot), and a version number so you can keep track of which version is most recent.
When presenting the virtual slots to the user, you reference the physical slots with the highest version number.
The algorithm automatically recovers to the most recent version if it discovers corrupt data, since the maximum version number it can find for the virtual save slot it's looking for will simply be in a different physical slot.
There are some finer details with metadata you need to store in the physical slots... ie. is a file deleted, marked as corrupted, etc.
On a project that had much more data to save, I even created separate virtual filesystems for different resources that were being used by a single save game. So you could lose a custom drawing or a microphone sample from corruption without it affecting your main save-game at all.
Another good thing about a system like this is that it greatly extends the life of your backup media, since you are performing fewer writes to the same location.
Dan.
#106678 - sgeos - Sun Oct 22, 2006 2:09 pm
poslundc wrote: |
I've implemented a virtual save system in the past. The basic principle is that if you have three save slots you instead use four (or more) physical save slots, and cycle through the slots when saving your game.
...
When presenting the virtual slots to the user, you reference the physical slots with the highest version number. |
Interesting. I could see the player being confused when an archived save is corrupted and turns into a save for the current game- unless I misunderstood. (I beat the game with a party of level 7 characters and never want to delete that data.)
Mirroring can also be used to prevent corruption.
Quote: |
Another good thing about a system like this is that it greatly extends the life of your backup media, since you are performing fewer writes to the same location. |
I had not thought of that. It honestly sounds like an amazing system.
But... it's not really what I wanted to know... If I make it to the final room and the batteries run out, this usually means I need to fight my way all the way back to final room. I think it would be neat if I didn't have to do that. Worse yet, the power goes out when I'm making the last room. =)
Journaling or a periodic state dump (every 15 minutes) seems like the way to go. I guess I want a cyclinic/mirrored 5/15 minute autosave.
-Brendan
#106686 - tepples - Sun Oct 22, 2006 6:55 pm
sgeos wrote: |
If I make it to the final room and the batteries run out, this usually means I need to fight my way all the way back to final room. |
Animal Crossing: Wild World for Nintendo DS has a partial solution: warn the player when the system's battery light has turned red.
You could say make 8 save slots, and use 4 for the three virtual slots of the "save" function and 4 for real-time saves.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.
#106704 - poslundc - Sun Oct 22, 2006 9:04 pm
sgeos wrote: |
poslundc wrote: | I've implemented a virtual save system in the past. The basic principle is that if you have three save slots you instead use four (or more) physical save slots, and cycle through the slots when saving your game.
...
When presenting the virtual slots to the user, you reference the physical slots with the highest version number. |
Interesting. I could see the player being confused when an archived save is corrupted and turns into a save for the current game- unless I misunderstood. (I beat the game with a party of level 7 characters and never want to delete that data.) |
That's not how it works. If I have a 5 physical slots on my media and 3 virtual slots (save locations) in my game, then my physical slots might have the following configuration:
Code: |
========================
= Physical slot 1
= - Virtual slot: 2 * ACTIVE
= - Version number: 0
=========================
= Physical slot 2
= - Virtual slot: 1 * INACTIVE
= - Version number: 1
=========================
= Physical slot 3
= - Virtual slot: 3 * INACTIVE
= - Version number: 2
=========================
= Physical slot 4
= - Virtual slot: 1 * ACTIVE
= - Version number: 4
=========================
= Physical slot 5
= - Virtual slot: 3 * ACTIVE
= - Version number: 5
========================= |
In this example:
- Physical slot 1 is active because it has the highest version number of any slot that points to virtual slot 2
- Physical slot 2 is inactive because there is another physical slot that has the same virtual slot number as it, but has a higher version number
So if I were to display a list of save-game-slots to the player, I would take the data from physical slots 1, 4, and 5 and ignore the data in slots 2 and 3.
When any game is next saved, it will be recorded into physical slot 2, because that is the inactive slot with the lowest version number. (The version number of that next save record will be 6.)
Say we are trying to make a list of all of our valid save-slots (virtual slots) the player can save into. If one of the active slots - say physical slot 5 - is found to be corrupted, then the game reverts to making physical slot 3 an active slot, since that is the slot that has the next-most-recent version number that points to the virtual slot number we were trying to populate. The other virtual slots remain unaffected by this procedure, so you can't get the situation you describe where data for one slot becomes data for another slot.
As a side note, it would probably be a Nintendo lot-check violation to "silently" revert to older data; if corrupt data is found then it would have to be reported to the user, with a message that might say "your data has been restored to an earlier version". That corrupt data would then be marked as a normal empty slot so you don't get that message again. (As I said, there are a number of nuances to implementing the actual system.)
Quote: |
But... it's not really what I wanted to know... If I make it to the final room and the batteries run out, this usually means I need to fight my way all the way back to final room. I think it would be neat if I didn't have to do that. Worse yet, the power goes out when I'm making the last room. =)
Journaling or a periodic state dump (every 15 minutes) seems like the way to go. I guess I want a cyclinic/mirrored 5/15 minute autosave. |
You can base autosave on a number of things... you could save every time you enter a new room, for example. Give the user a "quick start" option as well as a "return to some central game location" option when selecting their file.
Dan.
#106706 - tepples - Sun Oct 22, 2006 9:09 pm
poslundc wrote: |
As a side note, it would probably be a Nintendo lot-check violation to "silently" revert to older data; if corrupt data is found then it would have to be reported to the user, with a message that might say "your data has been restored to an earlier version". That corrupt data would then be marked as a normal empty slot so you don't get that message again. (As I said, there are a number of nuances to implementing the actual system.) |
If this is true, then Nintendo violated its own lot check guidelines in Super Mario World and Mario Paint for Super NES. Those games would silently lose saves all the time.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.
#106708 - poslundc - Sun Oct 22, 2006 9:12 pm
tepples wrote: |
If this is true, then Nintendo violated its own lot check guidelines in Super Mario World and Mario Paint for Super NES. Those games would silently lose saves all the time. |
Nintendo has always followed its own unique set of inscrutable rules when it comes to their own first-party titles. :-P
That said, I imagine the lot check requirements have evolved considerably since the days of those two games.
Dan.
#106737 - sgeos - Mon Oct 23, 2006 3:52 am
poslundc wrote: |
If one of the active slots - say physical slot 5 - is found to be corrupted, then the game reverts to making physical slot 3 an active slot, since that is the slot that has the next-most-recent version number that points to the virtual slot number we were trying to populate. |
Got it. If physical slot 1 is corrupted, you end up with an empty slot.
tepples wrote: |
warn the player when the system's battery light has turned red. |
That is certainly a reasonable solution. The goal is to cater to the people that play their GBA/DS until the batteries actually run out. (This may not actually be a worth while goal.)
poslundc wrote: |
You can base autosave on a number of things... you could save every time you enter a new room, for example. |
Memory efficient- seed, room/entrance, player status. On the otherhand, it doesn't help if me if I spend an hour in the same room. At any rate, specific solutions seem readily available for specific problems.
poslundc wrote: |
tepples wrote: | If this is true, then Nintendo violated its own lot check guidelines in Super Mario World and Mario Paint for Super NES. Those games would silently lose saves all the time. |
Nintendo has always followed its own unique set of inscrutable rules when it comes to their own first-party titles. :-P |
That's the difference between we want this out the door and you want this out the door. If the standards are yours, you can fudge them a little without having to answer to anyone.
For what it's worth, if you really didn't want to fix this lot check violation, you might be able to return it to Mario Club as "Could not reproduce. Please provide specific instructions to reproduce this bug." Then hope that the psychic playtester(*) doesn't get around to doing just that. =P
For this particular "bug", that is probably the wrong course of action as it does result in an inferior product.
-Brendan
(*) The person who submits the bug reports that are humanly impossible to pull off. "Starting with the upper lefthand button, if you move to the right and press a different button every frame the game will lock up when you hit the exit button." =P
#108271 - sgeos - Tue Nov 07, 2006 9:32 am
I assume that a basic crash recovery system looks something like this. It is written in java and works superficially, although I have not gone crazy with a debugger. If you are using something like C you'd obviously replace serialization with a custom save scheme. On the GBA the file name would be replaced with an appropriate write location.
Code: |
import java.io.*;
public class CrashRecovery
implements ITicking, Serializable
{
// User Constant
private static final String DEFAULT_FILE_NAME = "recovery.ser";
private static final String DEFAULT_MIRROR_NAME = "recovery_mirror.ser";
private static final int DEFAULT_TIMEOUT = 60 * 60 * 5; // 60fps * 60s/m * 5m
// System Constant
private static final int DEFAULT_TICK = 1;
private static final int ONE_SECOND = 1000; // milliseconds
// State
private Serializable mHost;
private String mFileName;
private String mMirrorName;
private int mTimer;
private int mTimeout;
private int mVersion;
public CrashRecovery()
{
init(null, DEFAULT_FILE_NAME, DEFAULT_MIRROR_NAME);
}
public CrashRecovery(Serializable pHost, String pFileName, String pMirrorName)
{
init(pHost, pFileName, pMirrorName);
}
public CrashRecovery init(Serializable pHost, String pFileName, String pMirrorName)
{
setHost(pHost);
setFile(pFileName, pMirrorName);
resetTimer();
resetVersion();
return this;
}
public CrashRecovery setHost(Serializable pHost)
{
mHost = pHost;
return this;
}
public CrashRecovery setFile(String pFileName, String pMirrorName)
{
mFileName = pFileName;
mMirrorName = pMirrorName;
return this;
}
// tick every frame
public void tick()
{
tick(DEFAULT_TICK);
}
// ITicking supports multiticking
public void tick(int pTick)
{
mTimer += pTick;
if (mTimeout < mTimer)
{
resetTimer();
save();
}
}
public void setTimeout(int pTimeout)
{
mTimeout = pTimeout;
}
// because splitting timeout time and FPS is probably a good thing
public void setTimeout(int pSeconds, int pFps)
{
mTimeout = pSeconds * pFps;
}
public void resetTimer()
{
mTimer = 0;
}
private void resetVersion()
{
mVersion = 0;
}
public boolean save()
{
mVersion++;
resetTimer();
boolean success = saveState(mFileName);
success &= saveState(mMirrorName);
return success;
}
public Serializable load()
{
Serializable result;
SerializableShell data = loadShell(mFileName);
SerializableShell mirror = loadShell(mMirrorName);
// null is bad
if ((null == data) && (null == mirror))
{
result = null;
}
else if (null == data)
{
result = mirror.host;
}
else if (null == mirror)
{
result = data.host;
}
// lower version is more reliable
else if (mirror.version < data.version)
{
result = mirror.host;
}
else
{
result = data.host;
}
if (null != result)
{
setHost(result);
resetVersion();
save();
}
return mHost;
}
public boolean saveState(String pFilename)
{
boolean success;
try
{
SerializableShell data = new SerializableShell(mHost, mVersion);
FileOutputStream fos = new FileOutputStream(pFilename);
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(data);
out.close();
success = true;
}
catch (IOException e)
{
success = false;
}
return success;
}
public Serializable loadState(String pFilename)
{
SerializableShell data = loadShell(pFilename);
if (null == data)
{
return null;
}
return data.host;
}
private SerializableShell loadShell(String pFilename)
{
SerializableShell data = null;
try
{
FileInputStream fis = new FileInputStream(pFilename);
ObjectInputStream in = new ObjectInputStream(fis);
data = (SerializableShell)in.readObject();
in.close();
}
catch (Throwable e)
{
data = null;
}
return data;
}
public boolean deleteFiles()
{
boolean success = (new File(mFileName)).delete();
success &= (new File(mMirrorName)).delete();
return success;
}
class SerializableShell
implements Serializable
{
public Serializable host;
public int version;
public SerializableShell(Serializable pHost, int pVersion)
{
host = pHost;
version = pVersion;
}
}
} |
Minus stylistic issues, is there anything obviously stupid about this scheme, or is it more or less usable?
-Brendan