Hacking Homebrew Question about editing switch playtime

  • Thread starter Thread starter funke_monke
  • Start date Start date
  • Views Views 5,283
  • Replies Replies 32
Will you plane to add remove function for titleID from playevent, if you will release it?
Do you mean from the playlog.v2.bin? I'm not sure it would do anything. I'm haven't tested but I'm assuming the system would just fill in whatever you delete from the PDM logs. I haven't taken a look at PlayEvent.dat beyond a quick glance at libnx's data structure for them.
 
This is indeed possible. But you'll have to modify my script.

I'll tell you everything you need to get you there:
- Get your prod.keys using lockpick rcm
- Use hekate to mount your nand through USB (read/write)
- Use nxnandmanager to mount SYSTEM (read/write) to your computer
- Go to SYSTEM/Save and copy out 80000000000000f0 (the file that has your play logs)
- Use hactoolnet to extract it: hactoolnet.exe -k prod.keys -t save 80000000000000f0 --outdir PlayTime

This will give you 2 files in the PlayTime directory:
BaseTimePoint.bin
PlayEvent.dat

PlayEvent.dat is what we are interested in. It's a hex encoded file where the first 4 bytes are the magic, the next 4 are the number of entries, then after that there are records (56 bytes per record). Then it has to be 0 padded out till the end of the file.

To read all your entries you can use this python (from that guys thesis):

Python:
#!/usr/bin/env python3
import struct
import sys
from datetime import datetime

def main():
    try:
        with open('PlayEvent.dat', 'rb') as fin:
            magic = struct.unpack('i', fin.read(4))[0]
            if magic != 0:
                print('Not a valid PlayEvent file', file=sys.stderr)
                sys.exit(1)

            number_of_entry = struct.unpack('I', fin.read(4))[0]

            for _ in range(number_of_entry):
                event_data = fin.read(0x1C)
                event_type = struct.unpack('B', fin.read(1))[0]
                fin.read(3)  # padding
                timestamp_user = struct.unpack('Q', fin.read(8))[0]
                timestamp_network = struct.unpack('Q', fin.read(8))[0]
                timestamp_steady = struct.unpack('Q', fin.read(8))[0]

                event_type_string = str(event_type)

                if event_type == 0:
                    title_id_part1 = struct.unpack('I', event_data[0x0:0x4])[0]
                    title_id_part2 = struct.unpack('I', event_data[0x4:0x8])[0]
                    applet_id = event_data[0xC]
                    storage_id = event_data[0xD]
                    log_policy = event_data[0xE]
                    event_subtype = event_data[0xF]
                    version = None

                    if applet_id == 1:
                        version = struct.unpack('I', event_data[0x8:0xC])[0]

                    print('%08x %s %d %s %08x%08x %s %d %d %d %d' % (
                        fin.tell() - 0x38,
                        "ET0",
                        event_type,
                        datetime.fromtimestamp(timestamp_user),
                        title_id_part1,
                        title_id_part2,
                        str(version) if version is not None else '',
                        applet_id,
                        storage_id,
                        log_policy,
                        event_subtype
                    ))

                elif event_type_string.startswith('1'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET1",
                        datetime.fromtimestamp(timestamp_user)
                    ))

                elif event_type_string.startswith('2'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET2",
                        datetime.fromtimestamp(timestamp_user)
                    ))

                elif event_type_string.startswith('3'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET3",
                        datetime.fromtimestamp(timestamp_user)
                    ))

                elif event_type_string.startswith('4'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET4",
                        datetime.fromtimestamp(timestamp_user)
                    ))

    except FileNotFoundError:
        print("PlayEvent.dat file not found.", file=sys.stderr)
    except Exception as e:
        print(f"An error occurred: {e}", file=sys.stderr)

if __name__ == "__main__":
    main()

Now all I wanted to do was delete out all my playtime after a certain day. It was a lot of app testing and stuff that I didn't want recorded:
Python:
#!/usr/bin/env python3
import struct
import sys
from datetime import datetime

CUTOFF_TIMESTAMP = int(datetime(2024, 1, 01).timestamp())
ENTRY_SIZE = 0x38  # 56 bytes
HEADER_SIZE = 8

def main():
    try:
        with open('PlayEvent.dat', 'rb') as fin:
            data = bytearray(fin.read())

        magic = struct.unpack_from('<I', data, 0)[0]
        if magic != 0:
            print("Not a valid PlayEvent file", file=sys.stderr)
            sys.exit(1)

        total_entries = struct.unpack_from('<I', data, 4)[0]
        print(f"Original total entries: {total_entries}")
        changed = 0
        valid_entries = 0

        for i in range(total_entries):
            offset = HEADER_SIZE + i * ENTRY_SIZE
            entry = data[offset:offset + ENTRY_SIZE]

            if len(entry) < ENTRY_SIZE:
                print(f"Incomplete entry at index {i}, skipping.")
                continue

            timestamp_user = struct.unpack_from('<Q', entry, 0x20)[0]

            if timestamp_user >= CUTOFF_TIMESTAMP:
                data[offset:offset + ENTRY_SIZE] = b'\x00' * ENTRY_SIZE
                changed += 1
            else:
                # Count as a valid (non-zero) entry
                valid_entries += 1

        # Update header with new number of valid entries
        struct.pack_into('<I', data, 4, valid_entries)

        with open('PlayEvent_filtered.dat', 'wb') as fout:
            fout.write(data)

        print(f"Filtered {changed} entries.")
        print(f"New valid entry count: {valid_entries}")
        print(f"Output file size: {len(data)} bytes (unchanged from original).")

    except FileNotFoundError:
        print("PlayEvent.dat not found.", file=sys.stderr)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)

if __name__ == "__main__":
    main()

You will need to modify this to filter by the title ID instead of the date (trivial if you know python, but honestly, chatgpt could probably do it for you).

I find that 'home screen' counts as an event with a title id of all zeros. so any time you click home it records 2 events: closing your game, and 'opening' the home screen.

That said, you may need some manual intervention to make sure everything looks legit and un-tampered with. But I bet (as long as you don't want to go online) just deleting out your title would work fine.

After PlayEvent.dat is all modified, replace it in the PlayTime direcotry. You should only have 2 files in there.

Now, after that's all said and done you need to:
- Repack things: hactoolnet.exe -k prod.keys -t save --repack PlayTime --outfile 80000000000000f0 original_save_file
- replace the 80000000000000f0 file in SYSTEM/save
- Properly unmount everything
- Reboot your switch
 
I appreciate you condensing this all down in a single post. However, this process is just far too risky and involved for my taste, especially with Switch 2 around the corner. I would, however, like to hear the results of your testing. Did you get the results you wanted? Did the playtime counter go down in all the various locations (applete, sort by playtime, etc.)?
 
  • Like
Reactions: impeeza
This is indeed possible. But you'll have to modify my script.

I'll tell you everything you need to get you there:
- Get your prod.keys using lockpick rcm
- Use hekate to mount your nand through USB (read/write)
- Use nxnandmanager to mount SYSTEM (read/write) to your computer
- Go to SYSTEM/Save and copy out 80000000000000f0 (the file that has your play logs)
- Use hactoolnet to extract it: hactoolnet.exe -k prod.keys -t save 80000000000000f0 --outdir PlayTime

This will give you 2 files in the PlayTime directory:
BaseTimePoint.bin
PlayEvent.dat

PlayEvent.dat is what we are interested in. It's a hex encoded file where the first 4 bytes are the magic, the next 4 are the number of entries, then after that there are records (56 bytes per record). Then it has to be 0 padded out till the end of the file.

To read all your entries you can use this python (from that guys thesis):

Python:
#!/usr/bin/env python3
import struct
import sys
from datetime import datetime

def main():
    try:
        with open('PlayEvent.dat', 'rb') as fin:
            magic = struct.unpack('i', fin.read(4))[0]
            if magic != 0:
                print('Not a valid PlayEvent file', file=sys.stderr)
                sys.exit(1)

            number_of_entry = struct.unpack('I', fin.read(4))[0]

            for _ in range(number_of_entry):
                event_data = fin.read(0x1C)
                event_type = struct.unpack('B', fin.read(1))[0]
                fin.read(3)  # padding
                timestamp_user = struct.unpack('Q', fin.read(8))[0]
                timestamp_network = struct.unpack('Q', fin.read(8))[0]
                timestamp_steady = struct.unpack('Q', fin.read(8))[0]

                event_type_string = str(event_type)

                if event_type == 0:
                    title_id_part1 = struct.unpack('I', event_data[0x0:0x4])[0]
                    title_id_part2 = struct.unpack('I', event_data[0x4:0x8])[0]
                    applet_id = event_data[0xC]
                    storage_id = event_data[0xD]
                    log_policy = event_data[0xE]
                    event_subtype = event_data[0xF]
                    version = None

                    if applet_id == 1:
                        version = struct.unpack('I', event_data[0x8:0xC])[0]

                    print('%08x %s %d %s %08x%08x %s %d %d %d %d' % (
                        fin.tell() - 0x38,
                        "ET0",
                        event_type,
                        datetime.fromtimestamp(timestamp_user),
                        title_id_part1,
                        title_id_part2,
                        str(version) if version is not None else '',
                        applet_id,
                        storage_id,
                        log_policy,
                        event_subtype
                    ))

                elif event_type_string.startswith('1'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET1",
                        datetime.fromtimestamp(timestamp_user)
                    ))

                elif event_type_string.startswith('2'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET2",
                        datetime.fromtimestamp(timestamp_user)
                    ))

                elif event_type_string.startswith('3'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET3",
                        datetime.fromtimestamp(timestamp_user)
                    ))

                elif event_type_string.startswith('4'):
                    print('%08x %s %s' % (
                        fin.tell() - 0x38,
                        "ET4",
                        datetime.fromtimestamp(timestamp_user)
                    ))

    except FileNotFoundError:
        print("PlayEvent.dat file not found.", file=sys.stderr)
    except Exception as e:
        print(f"An error occurred: {e}", file=sys.stderr)

if __name__ == "__main__":
    main()

Now all I wanted to do was delete out all my playtime after a certain day. It was a lot of app testing and stuff that I didn't want recorded:
Python:
#!/usr/bin/env python3
import struct
import sys
from datetime import datetime

CUTOFF_TIMESTAMP = int(datetime(2024, 1, 01).timestamp())
ENTRY_SIZE = 0x38  # 56 bytes
HEADER_SIZE = 8

def main():
    try:
        with open('PlayEvent.dat', 'rb') as fin:
            data = bytearray(fin.read())

        magic = struct.unpack_from('<I', data, 0)[0]
        if magic != 0:
            print("Not a valid PlayEvent file", file=sys.stderr)
            sys.exit(1)

        total_entries = struct.unpack_from('<I', data, 4)[0]
        print(f"Original total entries: {total_entries}")
        changed = 0
        valid_entries = 0

        for i in range(total_entries):
            offset = HEADER_SIZE + i * ENTRY_SIZE
            entry = data[offset:offset + ENTRY_SIZE]

            if len(entry) < ENTRY_SIZE:
                print(f"Incomplete entry at index {i}, skipping.")
                continue

            timestamp_user = struct.unpack_from('<Q', entry, 0x20)[0]

            if timestamp_user >= CUTOFF_TIMESTAMP:
                data[offset:offset + ENTRY_SIZE] = b'\x00' * ENTRY_SIZE
                changed += 1
            else:
                # Count as a valid (non-zero) entry
                valid_entries += 1

        # Update header with new number of valid entries
        struct.pack_into('<I', data, 4, valid_entries)

        with open('PlayEvent_filtered.dat', 'wb') as fout:
            fout.write(data)

        print(f"Filtered {changed} entries.")
        print(f"New valid entry count: {valid_entries}")
        print(f"Output file size: {len(data)} bytes (unchanged from original).")

    except FileNotFoundError:
        print("PlayEvent.dat not found.", file=sys.stderr)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)

if __name__ == "__main__":
    main()

You will need to modify this to filter by the title ID instead of the date (trivial if you know python, but honestly, chatgpt could probably do it for you).

I find that 'home screen' counts as an event with a title id of all zeros. so any time you click home it records 2 events: closing your game, and 'opening' the home screen.

That said, you may need some manual intervention to make sure everything looks legit and un-tampered with. But I bet (as long as you don't want to go online) just deleting out your title would work fine.

After PlayEvent.dat is all modified, replace it in the PlayTime direcotry. You should only have 2 files in there.

Now, after that's all said and done you need to:
- Repack things: hactoolnet.exe -k prod.keys -t save --repack PlayTime --outfile 80000000000000f0 original_save_file
- replace the 80000000000000f0 file in SYSTEM/save
- Properly unmount everything
- Reboot your switch
Do you have discord? I've tried extracting the prod.keys and the 80000000000000f0 file into hactoolnet.exe but I haven't got any results
 
Last edited by garlicuser254,
Do you have discord? I've tried extracting the prod.keys and the 80000000000000f0 file into hactoolnet.exe but I haven't got any results
You don't actually need to do it that way. JKSV has extra options that should allow you to get to it. You just need to pin down which service keeps the save data open and makes it inaccessible normally. I know Switchbrew has a list of them. I can make a video on how to pull it off later. I'm working on a cache system right now since 20.0+ made retrieving control data records slow as all hell.
 
You don't actually need to do it that way. JKSV has extra options that should allow you to get to it. You just need to pin down which service keeps the save data open and makes it inaccessible normally. I know Switchbrew has a list of them. I can make a video on how to pull it off later. I'm working on a cache system right now since 20.0+ made retrieving control data records slow as all hell.
Much gratitude to you
 
You don't actually need to do it that way. JKSV has extra options that should allow you to get to it. You just need to pin down which service keeps the save data open and makes it inaccessible normally. I know Switchbrew has a list of them. I can make a video on how to pull it off later. I'm working on a cache system right now since 20.0+ made retrieving control data records slow as all hell.
Can you tell me how to do it step by step?
 
Can you tell me how to do it step by step?
I'll set a reminder and do it later today. I'll upload a video or something.

Edit: Might need to wait... I can't seem to find which service to terminate. I could have sworn it was NS, but I guess not?
 
Last edited by JK_,
Rip. Closest thing I could find based on my own research was using NX Activity Log to export the json on my old v1 switch, replace the user id in the json to my new oled switch ID and match the usernames then import the json file on my oled switch.

But just as surmised, this edits only NX Activity Log and not the master log file contained within the switch.
 
Rip. Closest thing I could find based on my own research was using NX Activity Log to export the json on my old v1 switch, replace the user id in the json to my new oled switch ID and match the usernames then import the json file on my oled switch.

But just as surmised, this edits only NX Activity Log and not the master log file contained within the switch.
I wish I knew how to do this. I’ve been trying to edit the playtime on my modded switch as well.
 

Site & Scene News

Popular threads in this forum