ROM Hack WIP Backup and restore savedata (Pegaswitch)

  • Thread starter Deleted-442439
  • Start date
  • Views 2,852
  • Replies 7
  • Likes 4
D

Deleted-442439

Guest
OP
It is now possible to backup and and restore save data from the SD card via Pegaswitch. This is only possible on 1.0.0-3.0.0.

Backup script:
Code:
unction GetUsers() {
    return sc.getService("acc:u0", (acc) => {
        var numAccounts = sc.ipcMsg(0).sendTo(acc).assertOk().dataBuffer[0];
        var obuf = new Uint32Array(4*8);
        sc.ipcMsg(2).datau32().cDescriptor(obuf, obuf.length * 4, true).sendTo(acc).assertOk();
        var ids = [];
        for(var i = 0; i < numAccounts; i++) {
            ids[i] = [obuf[i*4+0], obuf[i*4+1], obuf[i*4+2], obuf[i*4+3]];
        }
        return ids;
    });
}

function GetProfile(id) {
    return sc.getService("acc:u0", (acc) => {
        return sc.withHandle(sc.ipcMsg(5).datau32(id[0], id[1], id[2], id[3]).sendTo(acc).assertOk().movedHandles[0], (iProfile) => {
            var userData = new Uint8Array(0x80);
            var profileBase = new Uint32Array(
                sc.ipcMsg(0).cDescriptor(userData).sendTo(iProfile).assertOk().data);
            var imageSize = sc.ipcMsg(10).sendTo(iProfile).assertOk().data[0];
            var image = new Uint8Array(imageSize);
            sc.ipcMsg(11).bDescriptor(image, image.byteLength, 0).sendTo(iProfile).assertOk();
            return {
                name: utils.u8a2nullstr(new Uint8Array(profileBase.buffer, 0x18, 10)),
                imageData: image
            };
        });
    });
}

function GetIApplicationManager(cb) {
    if(sc.hasService("ns:am")) {
        return sc.getService("ns:am", cb);
    } else {
        return sc.getService("ns:am2", (ns) => {
            return sc.ipcMsg(7996).sendTo(ns).assertOk().withHandles((r, m, c) => { // GetIApplicationManager
                return cb(m[0]);
            });
        });
    }
}

function GetTitleIds() {
    return GetIApplicationManager((iam) => {
        var buf = new Uint32Array(0x18*32); // 32 games max
        var count = sc.ipcMsg(0).datau32(0).bDescriptor(buf).sendTo(iam).assertOk().data[0];
        var tids = [];
        for(var i = 0; i < count; i++) {
            tids[i] = [buf[6*i+0], buf[6*i+1]];
        }
        return tids;
    });
}

sc.enableTurbo();
sc.escalateFilesystemAccess();
var date = new Date();
var prefix = "saves-" + date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() + "-" + date.getHours() + "-" + date.getMinutes();
var tids = GetTitleIds();

sc.getService("fsp-srv", (serv) => {
    GetUsers().forEach((userId) => {
        var profile = GetProfile(userId);
        utils.log("dump saves for " + profile.name);
        sc.ipcMsg(1).sendPid().data(0).sendTo(serv);
        
        tids.forEach((tid) => {
            utils.log("dump saves for " + profile.name + " game " + utils.paddr(tid));
            var msg = sc.ipcMsg(51).datau64(0x1, utils.pad64(tid), [userId[0], userId[1]], [userId[2], userId[3]], [0x0, 0x0], 0x1, 0x0, 0x0, 0x0).sendTo(serv);
            if(!msg.success) {
                utils.log("failed to mount save data for " + profile.name + " game " + utils.paddr(tid) + ": 0x" + msg.resultCode.toString(16));
                return;
            }
            sc.withHandle(msg.movedHandles[0], (h) => {
                var fs = new sc.IFileSystem(sc, h);
                try {
                    var curDir = fs.OpenDir("/").assertOk();
                    try {
                        curDir.DirDump(prefix + "/" + profile.name + "/" + utils.paddr(tid) + "/");
                    } finally {
                        curDir.Close();
                    }
                } finally {
                    fs.Close();
                }
                utils.log("finished dumping saves for " + profile.name + " game " + utils.paddr(tid));
            });
        });
        
        utils.log("finished dumping saves for " + profile.name);
    });
});

Restore script:

Code:
/*
*   0. Make sure your savefiles are in the correct layout under sdcard:/Saves/YOUR_GAME_TID_HERE/
*   1. Boot game
*   2. Wait a  second or so (for the game tid to register but before the code mounts the savedata)
*   3. Home button to pause game
*   4. Run this script
*   5. Look at debug log output to see if it restored everything correctly and then commited
*/
 
var tid = '00000000000000000'; //Change Title ID here
var commitAfterEveryFile = false; //set this to true if you get strange errors after some amount of files written (BoTW large saves trigger this)
 
IFileSystem.prototype.OpenDir = function (dir, flags) {
    if (flags === undefined) { flags = 3; }
    var path = utils.str2ab(dir);
    var self = this;
    return this.sc.ipcMsg(9).datau64(flags).xDescriptor(path, path.byteLength, 0).sendTo(this.handle).asResult()
        .map((r) => new self.sc.IDirectory(self.sc, dir, r.movedHandles[0], self));
};
 
IFileSystem.prototype.CreateDir = function (dir) {
    var path = utils.str2ab(dir);
    return this.sc.ipcMsg(2).xDescriptor(path, path.byteLength, 0).sendTo(this.handle).asResult();
};
 
IFileSystem.prototype.DeleteDir = function (dir, recursive) {
    if (recursive === undefined) { recursive = false; }
    var path = utils.str2ab(dir);
    return this.sc.ipcMsg(recursive ? 4 : 3).xDescriptor(path, path.byteLength, 0).sendTo(this.handle).asResult();
};
 
IFileSystem.prototype.Commit = function () {
    return sc.ipcMsg(10).sendTo(this.handle).asResult();
};
 
IFile.prototype.SetSize = function (size) {
    return this.sc.ipcMsg(3).datau64(size).sendTo(this.handle).asResult();
};
 
utils.log("stage1, hijack fsppr and set perms");
sc.getFSPPR = function () {
    if (sc.closed_pr !== undefined) {
        return;
    }
    sc.enableTurbo();
    var i = 0;
    var FspPr_Handle = null;
    while (true) {
        sc.ipcMsg(2).setType(5).sendTo('pm:shell');
        var FspPr = sc.getService('fsp-pr');
        if(FspPr.isOk) {
            FspPr_Handle = FspPr.getValue();
            break;
        }
        i++;
    }
    utils.log('Got fsp-pr handle after ' + i + ' iterations: ');
    utils.log('fsp-pr handle: 0x' + FspPr_Handle.toString(16));
    sc.svcCloseHandle(FspPr_Handle).assertOk();
    sc.closed_pr = true;
};
 
sc.getFSPPR();
sc.ipcMsg(1).sendPid().data(0).sendTo('fsp-srv').assertOk();
var pid = sc.read4(sc.ipcBufAddr, 0xC >> 2);
utils.log('Got process PID: '+pid.toString(16));
 
var buf1_sz = 0x1C;
var buf2_sz = 0x2C;
var buf = sc.malloc(buf1_sz + buf2_sz);
var buf2 = utils.add2(buf, buf1_sz);
 
//buffer init
sc.write4(1, buf, 0x0>>2);
sc.write8([0xFFFFFFFF, 0xFFFFFFFF], buf, 0x4 >> 2); //This is the permissions value.
sc.write4(buf1_sz, buf, 0xC >> 2);
sc.write4(buf1_sz, buf, 0x14 >> 2);
 
sc.write4(1, buf2, 0x0 >> 2);
sc.write8([0xFFFFFFFF, 0xFFFFFFFF], buf2, 0x4 >> 2); //This is the permissions value -- actual perms = buf2_val & buf1_val
sc.write4(0xFFFFFFFF, buf2, 0x14 >> 2);
sc.write4(0xFFFFFFFF, buf2, 0x18 >> 2);
sc.write4(0xFFFFFFFF, buf2, 0x24 >> 2);
sc.write4(0xFFFFFFFF, buf2, 0x28 >> 2);
 
sc.ipcMsg(256).data(0).sendTo('fsp-pr').assertOk().show();
sc.ipcMsg(1).data(pid).sendTo('fsp-pr').assertOk().show();
sc.ipcMsg(0).data(2, [pid,0], utils.parseAddr(tid), buf1_sz, buf2_sz, pid, pid, 0, 0, 0, 0, 0).aDescriptor(buf, buf1_sz).aDescriptor(buf2, buf2_sz).sendTo('fsp-pr').assertOk().show();
sc.free(buf);
sc.free(buf2);
 
foreachDirEntry = function(dir_obj, callback)
{
    var entryCount = utils.trunc32(dir_obj.GetEntryCount().assertOk());
    if (entryCount > 0 && callback !== undefined)
    {
        var entryBuf = new Uint32Array(0x310 * entryCount);
        dir_obj.GetEntries(entryBuf, entryCount).assertOk();
        for (var entryIdx = 0; entryIdx < entryCount; entryIdx++)
        {
            var fn = utils.u8a2str(new Uint8Array(entryBuf.buffer, 0x310 * entryIdx, 0x300));
            for (var strIdx=0; strIdx<fn.length; strIdx++)
            {
                if (fn.charCodeAt(strIdx) === 0)
                {
                    fn = fn.substring(0, strIdx);
                    break;
                }
            }
            var eType = entryBuf[(0x310 * entryIdx + 0x304) >> 2];         
            callback(fn, eType);
        }
    }
  
    return entryCount;
};
 
copyFiles = function(src_dir, dst_dir)
{
    var thefiles = [];
    foreachDirEntry(src_dir, function(fn, eType)
    {
        if (eType === 1) { thefiles.push(fn); }
    });
    //dont need handle to dst_dir anymore (and it will interfere with Commit ops)
    var dst_dir_fs = dst_dir.fs;
    var dst_dir_path = dst_dir.path;
    dst_dir.Close(); dst_dir = null;
 
    var size = [];
    var buf = null;
    for (var fileIdx=0; fileIdx<thefiles.length; fileIdx++)
    {
        var fn = thefiles[fileIdx];
        utils.log('[Opening existing file] ' + src_dir.path + fn);
        var f_src = src_dir.fs.OpenFile(src_dir.path + fn).assertOk();
        var f_dst = null;
        try
        {
            size = f_src.GetSize().assertOk();
            buf = f_src.Read(size).assertOk();
            f_src.Close(); f_src = null;
 
            utils.log('[Read file complete] ' + src_dir.path + fn + ' = ' + size + ' bytes');
            //try open existing file
            f_dst = dst_dir_fs.OpenFile(dst_dir_path + fn);
            if (f_dst.isOk) //file exists, must be deleted
            {
                f_dst = f_dst.assertOk();
                f_dst.Close(); f_dst = null;
 
                utils.log('[Deleting existing file] ' + dst_dir_path + fn);
                dst_dir_fs.DeleteFile(dst_dir_path + fn).assertOk();
            }
 
            utils.log('[Creating new file] ' + dst_dir_path + fn + ' with size ' + size + ' bytes');
            dst_dir_fs.CreateFile(dst_dir_path + fn, size).assertOk();
            utils.log('[Opening new file] ' + dst_dir_path + fn);         
            f_dst = dst_dir_fs.OpenFile(dst_dir_path + fn).assertOk();
            utils.log('[Open file OK] ' + dst_dir_path + fn); 
            f_dst.Write(0, buf, size).assertOk();
            utils.log('[Write file OK] ' + dst_dir_path + fn + ' -> ' + size + ' bytes');
            buf = null;
            f_dst.Close(); f_dst = null;
            utils.log('[Close file OK] ' + dst_dir_path + fn);
            if (commitAfterEveryFile)
            {
                utils.log('[Commiting to fs ' + dst_dir_fs.partition + ' handle ' + dst_dir_fs.handle + '] ' + dst_dir_path + fn);             
                dst_dir_fs.Commit().assertOk();
                utils.log('[Commit OK] ' + dst_dir_path + fn);
            }
        }
        finally
        {
            if (f_dst !== null && f_dst instanceof IFile) { f_dst.Close(); f_dst = null; }
            if (f_src !== null) { f_src.Close(); f_src = null; }
        }
    }
 
    return dst_dir;
};
 
evaluateDirs = function(fs_obj, root_dir)
{
    var folders = [];
  
    var dir_obj = fs_obj.OpenDir(root_dir, 1); //directories only
    if (!dir_obj.isOk) { return folders; } //no dirs
    else { dir_obj = dir_obj.assertOk(); }
  
    folders.push(dir_obj.path); //this dir
    for (var evalIdx=0; evalIdx<folders.length; evalIdx++)
    {
        if (evalIdx !== 0)
        {
            dir_obj = fs_obj.OpenDir(folders[evalIdx], 1);
            if (!dir_obj.isOk) { throw new Error('Unable to open dir ' + folders[evalIdx] + ' for listing'); }         
            else { dir_obj = dir_obj.assertOk(); }
        }
 
        foreachDirEntry(dir_obj, function(fn) { folders.push(dir_obj.path + fn + '/'); });
        dir_obj.Close(); dir_obj = null;
    }
 
    return folders;
};
 
utils.log("stage2, open save data");
 
mountSaveData = function (userID, tid) {
    var self = this;
    return sc.ipcMsg(51).datau64(1, utils.parseAddr(tid), [userID[0], userID[1]], [userID[2], userID[3]], [0,0], 1, 0, 0, 0).sendTo('fsp-srv').asResult()
        .map((r) => new self.sc.IFileSystem(self.sc, r.movedHandles[0]));
};
 
mountSDCard = function () {
    var self = this;
    return sc.ipcMsg(18).sendTo('fsp-srv').asResult()
        .map((r) => new self.sc.IFileSystem(self.sc, r.movedHandles[0]));
};
 
utils.log("GetLastUserProfile");
var userID = sc.ipcMsg(4).sendTo('acc:u1').assertOk().data;
 
utils.log('MountSaveData');
var fs_sd = null;
var fs_data = mountSaveData(userID, tid).assertOk();
fs_data.partition = "SAVEDATA";
utils.log('SAVEDATA handle is ' + fs_data.handle);
 
try {
    utils.log("Mounting SD card");
    try { fs_sd = mountSDCard().assertOk(); }
    catch(e) { throw new Error("Failed to open SD card. Is it inserted?"); }
    fs_sd.partition = "SDCARD";
    utils.log('SDCARD handle is ' + fs_sd.handle);
 
    var sd_folder_root = '/Saves/'+tid+'/';
    utils.log('[Opening SD dir for listing] ' + sd_folder_root);
    var dirList = evaluateDirs(fs_sd, sd_folder_root);
    if (dirList.length < 1)
    {
        fs_sd.Close(); fs_sd = null;
        fs_data.Close(); fs_data = null;
        throw new Error("Unable to open SD saves dir " + sd_folder_root + ' or its empty');
    }
 
    utils.log('[Opening SAVEDATA dir for listing]');
    var save_root_dir = fs_data.OpenDir('/', 1).assertOk(); //directories only
    var saveEntries = [];
    foreachDirEntry(save_root_dir, function(fn) { saveEntries.push(save_root_dir.path + fn + '/'); });
    save_root_dir.Close(); save_root_dir = null;
    for (var saveDirIdx=0; saveDirIdx<saveEntries.length; saveDirIdx++)
    {
        var currSaveDir = saveEntries[saveDirIdx];
        utils.log('[Deleting SAVEDATA dir] ' + currSaveDir)
        fs_data.DeleteDir(currSaveDir, true).assertOk();
        utils.log('[SAVEDATA dir deleted] ' + currSaveDir)
    }
    save_root_dir = fs_data.OpenDir('/', 2).assertOk(); //files only
    saveEntries = [];
    foreachDirEntry(save_root_dir, function(fn) { saveEntries.push(save_root_dir.path + fn); });
    save_root_dir.Close(); save_root_dir = null;
    for (var saveFnIdx=0; saveFnIdx<saveEntries.length; saveFnIdx++)
    {
        var currSaveFile = saveEntries[saveFnIdx];
        utils.log('[Deleting SAVEDATA file] ' + currSaveFile)
        fs_data.DeleteFile(currSaveFile).assertOk();
        utils.log('[SAVEDATA file deleted] ' + currSaveFile)
    }
    saveEntries = [];
 
    for (var dirIndex = 0; dirIndex < dirList.length; dirIndex++)
    {
        var left_side = dirList[dirIndex];
        var right_side = '/' + dirList[dirIndex].substring(sd_folder_root.length);
 
        utils.log('[Opening folder on SD] '+ left_side);
        var dir_sd = fs_sd.OpenDir(left_side, 2).assertOk();
        utils.log('[Opening folder in savedata] '+ right_side);
        var dir_data = fs_data.OpenDir(right_side, 2);
        if (!dir_data.isOk)
        {
            utils.log('[Making folder in savedata] '+ right_side);
            fs_data.CreateDir(right_side).assertOk();
            utils.log('[Opening newly made folder] ' + right_side);           
            dir_data = fs_data.OpenDir(right_side, 2).assertOk();
            utils.log('[New folder opened OK] '+ right_side);
        }
        else
        {
            dir_data = dir_data.assertOk();
            utils.log('[Existing folder opened OK] '+ right_side);
        }
 
        try
        {
            dir_data = copyFiles(dir_sd, dir_data); //because we might change dst_dir inside
        }
        finally
        {
            if (dir_data !== null) { dir_data.Close(); dir_data = null; }
            if (dir_sd !== null) { dir_sd.Close(); dir_sd = null; }
        }
    }
 
    utils.log('[Commiting savedata changes]');
    fs_data.Commit().assertOk();
    utils.log('[Closing file systems]');
    fs_data.Close(); fs_data = null;
    fs_sd.Close(); fs_sd = null;
}
finally
{
    if (fs_data !== null) { fs_data.Close(); fs_data = null; }
    if (fs_sd !== null) { fs_sd.Close(); fs_sd = null; }
}
 

Foundforgood89

Member
Newcomer
Joined
Nov 19, 2017
Messages
24
Trophies
0
Age
34
XP
147
Country
United States
This is awesome! Is this something practical that a noob can do? Not sure how to run scripts on the switch but I have PegaSwitch running on 3.0.0. Sorry for the ignorance
 
D

Deleted-442439

Guest
OP
This is awesome! Is this something practical that a noob can do? Not sure how to run scripts on the switch but I have PegaSwitch running on 3.0.0. Sorry for the ignorance

Yep! All you need to do is run the above scripts from Pegaswitch. Save them in notepad and change the file to .js
 
Last edited by ,

Foundforgood89

Member
Newcomer
Joined
Nov 19, 2017
Messages
24
Trophies
0
Age
34
XP
147
Country
United States
Yep! All you need to do is run the above scripts from Pegaswitch. Save them in notepad and change the file to .js
Great. Just curious, is the backup script first line supposed to be "function GetUsers() {" rather than "unction GetUsers() {"?
And the first one (maybe 2) lines of the restore script are the only things that need changing, correct?
 
D

Deleted-442439

Guest
OP
Great. Just curious, is the backup script first line supposed to be "function GetUsers() {" rather than "unction GetUsers() {"?
And the first one (maybe 2) lines of the restore script are the only things that need changing, correct?

Ups! Yes the "f" is missing! And yes the restore script only needs a few tweaks depending on the title
 

_______

 
Member
Joined
May 13, 2016
Messages
515
Trophies
0
XP
834
Country
Japan
Notice that some save file have checksum (e.g. Splatoon use CRC in the header), so these scripts are only good if you wanted to backup saves. If you wanted to edit them, you might need some extra work.
 

Retr0id

Active Member
Newcomer
Joined
Apr 12, 2018
Messages
33
Trophies
0
Age
54
XP
441
Country
United Kingdom
Why are you posting other people's code without attribution?

The save backup script is already a part of PegaSwitch: github. com/reswitched/pegaswitch/blob/master/usefulscripts/BackupAllSaves.js

The save restore script is by Rajkosto: pastebin. com/0zAwHi4w
 
  • Like
Reactions: yahoo

Site & Scene News

Popular threads in this forum

General chit-chat
Help Users
  • No one is chatting at the moment.
    BakerMan @ BakerMan: this one +1