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