mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Let the launcher's bug report export write the game's log and settings when the game has no save yet.
A game that stops before its first save is the one its player most wants to report, and Export Save refused it: "the game has no save yet". With Add Log and Settings on (the default) the launcher now writes NX2navi-log-<date>_<time>-bug-report.zip with the game's latest log, nx2navi.ini and a manifest that counts no save files, and says so on the Saves screen. Without a log there is still nothing to export, an export without the log and settings still needs a save, no account is made for the export, and an import refuses such a zip as one without a save. This is the first of the two faults pull request #364 names, written anew inside the existing export; its Export Diagnostics screen and its separate crash log are not taken. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -250,7 +250,9 @@ save. Saves (home, under Game Folder; `launcher/saves.py`) and the Saves rows of
|
||||
|
||||
**Export.** Export Save writes `NX2navi-save-<date>_<time>.zip` into the Downloads folder (Export To chooses another
|
||||
folder) and opens the folder with the zip selected, ready to drag into a bug report. With Add Log and Settings on (the
|
||||
default) it is `...-bug-report.zip` and also holds the game's latest log and its `nx2navi.ini`. In the game, Export
|
||||
default) it is `...-bug-report.zip` and also holds the game's latest log and its `nx2navi.ini`; while the game has no
|
||||
save yet (it stopped before its first one) the launcher then writes `NX2navi-log-<date>_<time>-bug-report.zip` with
|
||||
those two alone, which an import refuses as a zip without a save. In the game, Export
|
||||
save and Export save for a bug report write the same two zips into Downloads; the second holds the log of the run in
|
||||
progress. The zip:
|
||||
|
||||
|
||||
@@ -390,15 +390,49 @@ def _fresh_file(folder, stem, suffix):
|
||||
return p
|
||||
|
||||
|
||||
NO_SAVE = 'the game has no save yet: play until it saves, then export'
|
||||
|
||||
|
||||
def _export_log(game, dest, version, log):
|
||||
"""The bug report of a game without a save (it stopped before its first one): NX2navi-log-<date>_<time>-bug-report.zip
|
||||
with the game's latest log, nx2navi.ini and a manifest that counts no save files; import_zip refuses it as a zip
|
||||
without a save. SaveError when the game has written no log either: then there is nothing to report with."""
|
||||
extras = [f for f in (game / 'logs' / 'nx2navi.log', game / 'nx2navi.ini') if f.is_file()]
|
||||
if not extras or extras[0].name != 'nx2navi.log':
|
||||
raise SaveError(NO_SAVE)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
out = _fresh_file(dest, time.strftime('NX2navi-log-%Y-%m-%d_%H-%M-%S') + '-bug-report', '.zip')
|
||||
tmp = out.with_name(out.name + '.part')
|
||||
manifest = {'format': FORMAT, 'format_version': FORMAT_VERSION, 'game': GAME, 'title_id': _title_hex(),
|
||||
'game_version': GAME_VERSION, 'nx2navi': version,
|
||||
'exported': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'exported_by': 'launcher',
|
||||
'files': 0, 'bug_report': True}
|
||||
try:
|
||||
with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as z:
|
||||
z.writestr(MANIFEST, json.dumps(manifest, indent=1) + '\n')
|
||||
for f in extras:
|
||||
z.write(f, REPORT_DIR + f.name)
|
||||
os.replace(tmp, out)
|
||||
except BaseException:
|
||||
tmp.unlink(missing_ok=True)
|
||||
raise
|
||||
log(f'saves: the game has no save yet; exported {", ".join(f.name for f in extras)} to {out}')
|
||||
return {'path': out, 'files': 0, 'extras': [f.name for f in extras]}
|
||||
|
||||
|
||||
def export_save(game, dest=None, bug_report=False, version='', log=print):
|
||||
"""Write the save the game would load now into one zip in dest (default_export_dir when None), for a bug report
|
||||
or another player. bug_report adds the game's latest log and nx2navi.ini. Never a key file. Returns {'path',
|
||||
'files', 'extras'} (extras: the names of the bug report files that were there)."""
|
||||
or another player. bug_report adds the game's latest log and nx2navi.ini, and still writes those two when the game
|
||||
has no save yet (_export_log): a game that stops before its first save is the one its player wants to report.
|
||||
Never a key file. Returns {'path', 'files', 'extras'} (files 0: no save; extras: the names of the bug report files
|
||||
that were there)."""
|
||||
game = Path(game)
|
||||
if not (game / 'profile' / 'account.uid').is_file():
|
||||
raise SaveError('the game has no save yet: play until it saves, then export')
|
||||
account = account_dir(game)
|
||||
dest = Path(dest) if dest else default_export_dir()
|
||||
if not (game / 'profile' / 'account.uid').is_file(): # account_dir would make the account: not for an export
|
||||
if bug_report:
|
||||
return _export_log(game, dest, version, log)
|
||||
raise SaveError(NO_SAVE)
|
||||
account = account_dir(game)
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
stem = time.strftime('NX2navi-save-%Y-%m-%d_%H-%M-%S') + ('-bug-report' if bug_report else '')
|
||||
extras = [f for f in (game / 'logs' / 'nx2navi.log', game / 'nx2navi.ini') if bug_report and f.is_file()]
|
||||
@@ -406,7 +440,9 @@ def export_save(game, dest=None, bug_report=False, version='', log=print):
|
||||
# a running game may swap a commit in while its files are read (runtime/fs_host.c swap_in): read again
|
||||
current = _newest(game, account)
|
||||
if current is None or not _has_file(current):
|
||||
raise SaveError('the game has no save yet: play until it saves, then export')
|
||||
if bug_report:
|
||||
return _export_log(game, dest, version, log)
|
||||
raise SaveError(NO_SAVE)
|
||||
files = [rel for rel in _files(current, set()) if not _key_file(rel)]
|
||||
out = _fresh_file(dest, stem, '.zip')
|
||||
tmp = out.with_name(out.name + '.part')
|
||||
|
||||
@@ -1050,7 +1050,8 @@ class App:
|
||||
Row('value', 'Export Save', out.name or str(out), on=self.export_save, desc=export_desc),
|
||||
Row('toggle', 'Add Log and Settings', on=report, get=lambda: bool(self.settings.get('export_with_log')),
|
||||
desc=('On: the zip also holds the game\'s latest log (logs\\nx2navi.log) and its settings '
|
||||
'(nx2navi.ini), which show what happened and with which settings. Keep it on for a bug report.',
|
||||
'(nx2navi.ini), which show what happened and with which settings. Keep it on for a bug report: '
|
||||
'while the game has no save yet, Export Save then writes these two alone.',
|
||||
'Off: the save alone, to give to another player.')),
|
||||
Row('value', 'Export To', str(out), on=self.choose_export_dir,
|
||||
desc=(f'Export Save writes into {out}.', 'Select this row to choose another folder.')),
|
||||
@@ -1088,6 +1089,9 @@ class App:
|
||||
else:
|
||||
extra = ' with the game\'s log and settings' if r['extras'] else ''
|
||||
self.export_message = f'Exported {r["files"]} files{extra} to {r["path"]}.'
|
||||
if not r['files']: # a game that stopped before its first save: the report files alone
|
||||
self.export_message = (f'The game has no save yet: exported its log and settings alone to '
|
||||
f'{r["path"]}.')
|
||||
try:
|
||||
winsys.reveal(r['path'])
|
||||
except OSError:
|
||||
|
||||
@@ -2554,6 +2554,32 @@ class SaveShareTest(unittest.TestCase):
|
||||
saves.export_save(self.game, self.out, log=lambda m: None) # an account without files
|
||||
self.assertEqual(list(self.out.iterdir()), [])
|
||||
|
||||
def test_a_bug_report_without_a_save_holds_the_log_and_settings(self):
|
||||
"""A game that stops before its first save: the bug report export writes its log and settings alone."""
|
||||
with self.assertRaises(saves.SaveError):
|
||||
saves.export_save(self.game, self.out, bug_report=True, log=lambda m: None) # no log either: nothing
|
||||
self.assertFalse(self.out.exists() and list(self.out.iterdir()))
|
||||
self.files(self.game, {'logs/nx2navi.log': b'FATAL: it stopped', 'nx2navi.ini': b'[x]', 'prod.keys': b'secret'})
|
||||
with self.assertRaises(saves.SaveError):
|
||||
saves.export_save(self.game, self.out, log=lambda m: None) # not a bug report: a save or nothing
|
||||
for account in (False, True): # a game that never made its account, and an account without files
|
||||
if account:
|
||||
saves.account_dir(self.game).mkdir(parents=True)
|
||||
with self.subTest(account=account):
|
||||
r = saves.export_save(self.game, self.out, bug_report=True, version='0.7.1', log=lambda m: None)
|
||||
self.assertRegex(r['path'].name, r'^NX2navi-log-.*-bug-report( \(\d+\))?\.zip$') # (2): the same second
|
||||
self.assertEqual((r['files'], r['extras']), (0, ['nx2navi.log', 'nx2navi.ini']))
|
||||
with zipfile.ZipFile(r['path']) as z:
|
||||
self.assertEqual(sorted(z.namelist()), ['bug-report/nx2navi.ini', 'bug-report/nx2navi.log',
|
||||
'manifest.json'])
|
||||
self.assertEqual(z.read('bug-report/nx2navi.log'), b'FATAL: it stopped')
|
||||
m = json.loads(z.read('manifest.json'))
|
||||
self.assertEqual((m['format'], m['files'], m['bug_report'], m['nx2navi']),
|
||||
(saves.FORMAT, 0, True, '0.7.1'))
|
||||
with self.assertRaises(saves.SaveError): # nobody's save: an import says so and changes nothing
|
||||
saves.import_zip(self.game_with_save(self.tmp / f'other{account}'), r['path'], log=lambda m: None)
|
||||
self.assertFalse(list(self.out.glob('*.part')))
|
||||
|
||||
def test_a_shared_save_replaces_the_save_and_keeps_a_backup(self):
|
||||
other = self.game_with_save(self.tmp / 'other')
|
||||
shared = saves.export_save(other, self.out, bug_report=True, version='0.6.1', log=lambda m: None)['path']
|
||||
|
||||
Reference in New Issue
Block a user