ROM Hack WIP Backup and restore savedata (Pegaswitch)

  • Thread starter Deleted-442439
  • Start date
  • Views 2,857
  • 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
  • light27 @ light27:
    call sps
  • Psionic Roshambo @ Psionic Roshambo:
    Hey mine helps me in the shower lol
  • Xdqwerty @ Xdqwerty:
    We went to a place in a mall where they let you play with some consoles for like an hour
  • Xdqwerty @ Xdqwerty:
    I choose the nintendo switch and I played smash ultimate and Mario wonder
  • Xdqwerty @ Xdqwerty:
    It was fun
    +1
  • Xdqwerty @ Xdqwerty:
    Also i'm back at my house
  • Psionic Roshambo @ Psionic Roshambo:
    Smash bros sounds like gay incest lol
    +1
  • BigOnYa @ BigOnYa:
    And Mario Wonder sounds like a Mario with old timers disease walking the streets lost.
    +1
  • K3Nv2 @ K3Nv2:
    Shove a gerbil through a tube
    +2
  • Psionic Roshambo @ Psionic Roshambo:
    Warp pipe Ken lol
    +1
  • K3Nv2 @ K3Nv2:
    Ring actually helped and I didn't have to cuss out the guy in arabic
  • BigOnYa @ BigOnYa:
    They help you delete some incriminating videos?
  • K3Nv2 @ K3Nv2:
    No you asked for proof that your wife was finally leaving
    +1
  • BigOnYa @ BigOnYa:
    Nuh I had her chipped, I always know where she is.
  • K3Nv2 @ K3Nv2:
    I found where it was
    +1
  • Psionic Roshambo @ Psionic Roshambo:
    If I could halucinAte this is what I imagine it would look like?
  • K3Nv2 @ K3Nv2:
    Featuring Taylor swift
  • BakerMan @ BakerMan:
    guys, pubg should've never blown up
  • BigOnYa @ BigOnYa:
    Wut do you mean? Pubg is alright, I play it with a few buddies online.
  • BakerMan @ BakerMan:
    the game itself is ok, but it's influence is the problem
  • K3Nv2 @ K3Nv2:
    Call of duty is a good Fortnite skin
  • BakerMan @ BakerMan:
    call of duty and fortnite, ex-fucking-actly
  • BakerMan @ BakerMan:
    without pubg popping off, fortnite wouldn't add battle royale, and call of duty wouldn't go even shittier than it did before because of blackout, warzone and dmz
    BakerMan @ BakerMan: without pubg popping off, fortnite wouldn't add battle royale, and call of duty wouldn't go even...