install_mission_ember.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. #!/usr/bin/env python3
  2. """Install the Ember DLL and all mission Lua files into a closed Linux/Proton game.
  3. Never launches/stops the game, and never modifies settings, saves or generated SDK.
  4. """
  5. import argparse
  6. import datetime
  7. import hashlib
  8. import json
  9. import os
  10. from pathlib import Path
  11. import shutil
  12. def closed():
  13. if not Path('/proc').is_dir():
  14. raise RuntimeError('This process guard requires Linux /proc; use a verified manual install elsewhere.')
  15. for process in Path('/proc').iterdir():
  16. if not process.name.isdecimal():
  17. continue
  18. try:
  19. name = (process / 'comm').read_text().strip().lower()
  20. except FileNotFoundError:
  21. continue
  22. if 'destiny' in name:
  23. raise RuntimeError('Game running; installation cancelled. The user must close it manually.')
  24. def sha(path):
  25. digest = hashlib.sha256()
  26. with path.open('rb') as stream:
  27. for block in iter(lambda: stream.read(1024 * 1024), b''):
  28. digest.update(block)
  29. return digest.hexdigest()
  30. def main():
  31. parser = argparse.ArgumentParser(description=__doc__)
  32. parser.add_argument('--game', type=Path, required=True, help='Game bin/x64 directory')
  33. parser.add_argument('--dll', type=Path, help='Override build/x64/Release/steam_api64.dll')
  34. parser.add_argument('--dry-run', action='store_true')
  35. args = parser.parse_args()
  36. root = Path(__file__).resolve().parents[1]
  37. game = args.game.expanduser().resolve()
  38. if not game.is_dir() or not (game / 'steam_api64.dll').is_file():
  39. raise RuntimeError('Target must be the existing game bin/x64 directory containing steam_api64.dll')
  40. dll = args.dll.expanduser().resolve() if args.dll else root / 'build/x64/Release/steam_api64.dll'
  41. pairs = [(dll, game / 'steam_api64.dll'),
  42. (root / 'scripts/mission_ember.lua', game / 'Sunrise/scripts/mission_ember.lua')]
  43. pairs += [(p, game / 'Sunrise/scripts/mission_ember' / p.name)
  44. for p in sorted((root / 'scripts/mission_ember').glob('*.lua'))]
  45. if len(pairs) != 18:
  46. raise RuntimeError('Expected one DLL and 17 mission Lua files; review the deployment list.')
  47. for source, _ in pairs:
  48. if not source.is_file():
  49. raise RuntimeError(f'Missing source: {source}')
  50. closed()
  51. manifest = {'created': datetime.datetime.now().astimezone().isoformat(), 'files': [
  52. {'path': str(dst.relative_to(game)), 'source': str(src), 'sha256': sha(src)}
  53. for src, dst in pairs]}
  54. if args.dry_run:
  55. print(json.dumps(manifest, indent=2))
  56. return
  57. backup = root / 'build' / ('ember-install-backup-' + datetime.datetime.now().strftime('%Y%m%d-%H%M%S-%f'))
  58. backup.mkdir(parents=True)
  59. for _, dst in pairs:
  60. if dst.exists():
  61. keep = backup / dst.relative_to(game)
  62. keep.parent.mkdir(parents=True, exist_ok=True)
  63. shutil.copy2(dst, keep)
  64. closed()
  65. for (src, dst), item in zip(pairs, manifest['files']):
  66. closed()
  67. dst.parent.mkdir(parents=True, exist_ok=True)
  68. temporary = dst.with_name(dst.name + '.ember-install-tmp')
  69. shutil.copy2(src, temporary)
  70. if sha(temporary) != item['sha256']:
  71. temporary.unlink()
  72. raise RuntimeError(f'Source changed during installation: {src}')
  73. os.replace(temporary, dst)
  74. if sha(dst) != item['sha256']:
  75. raise RuntimeError(f'Installed checksum mismatch: {dst}')
  76. manifest['backup'] = str(backup)
  77. (root / 'build/ember-installation.json').write_text(json.dumps(manifest, indent=2) + '\n')
  78. print(f'Installed and verified {len(pairs)} files. Backup: {backup}')
  79. print('Settings, saves and SDK untouched; game not launched.')
  80. if __name__ == '__main__':
  81. main()