changeset 373:6deab6ad8be8

Add the ability to save a replay.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sun, 05 Aug 2012 16:37:26 +0200
parents 704bea2e4360
children 6a63fd3deb76
files eosd pytouhou/formats/t6rp.py pytouhou/game/game.py pytouhou/ui/gamerunner.py
diffstat 4 files changed, 122 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/eosd
+++ b/eosd
@@ -27,7 +27,7 @@ from pytouhou.ui.gamerunner import GameR
 from pytouhou.games.eosd import EoSDGame
 from pytouhou.game.game import GameOver
 from pytouhou.game.player import PlayerState
-from pytouhou.formats.t6rp import T6RP
+from pytouhou.formats.t6rp import T6RP, Level
 from pytouhou.utils.random import Random
 from pytouhou.vm.msgrunner import NextStage
 
@@ -62,7 +62,9 @@ class EoSDGameBossRush(EoSDGame):
 
 
 
-def main(path, data, stage_num, rank, character, replay, boss_rush, fps_limit, single_buffer, debug, fixed_pipeline):
+def main(path, data, stage_num, rank, character, replay, save_filename,
+         boss_rush, fps_limit, single_buffer, debug, fixed_pipeline):
+
     resource_loader = Loader(path)
 
     try:
@@ -89,6 +91,12 @@ def main(path, data, stage_num, rank, ch
         rank = replay.rank
         character = replay.character
 
+    save_keystates = None
+    if save_filename:
+        save_replay = T6RP()
+        save_replay.rank = rank
+        save_replay.character = character
+
     difficulty = 16
     default_power = [0, 64, 128, 128, 128, 128, 0][stage_num - 1]
     states = [PlayerState(character=character, power=default_power)]
@@ -110,12 +118,25 @@ def main(path, data, stage_num, rank, ch
                 previous_level = replay.levels[stage_num - 1]
                 states[0].score = previous_level.score
                 states[0].effective_score = previous_level.score
+            states[0].point_items = level.point_items
             states[0].power = level.power
             states[0].lives = level.lives
             states[0].bombs = level.bombs
             difficulty = level.difficulty
         else:
-            prng = None
+            prng = Random()
+
+        if save_filename:
+            if not replay:
+                save_replay.levels[stage_num - 1] = level = Level()
+                level.score = states[0].score
+                level.random_seed = prng.seed
+                level.point_items = states[0].points
+                level.power = states[0].power
+                level.lives = states[0].lives
+                level.bombs = states[0].bombs
+                level.difficulty = difficulty
+            save_keystates = []
 
         # Load stage data
         stage = resource_loader.get_stage('stage%d.std' % stage_num)
@@ -126,7 +147,7 @@ def main(path, data, stage_num, rank, ch
         background = Background(stage, background_anm_wrapper)
 
         # Main loop
-        runner.load_game(game, background, stage.bgms, replay)
+        runner.load_game(game, background, stage.bgms, replay, save_keystates)
         try:
             runner.start()
             break
@@ -139,6 +160,17 @@ def main(path, data, stage_num, rank, ch
         except GameOver:
             print('Game over')
             break
+        finally:
+            if save_filename:
+                last_key = -1
+                for time, key in enumerate(save_keystates):
+                    if key != last_key:
+                        level.keys.append((time, key, 0))
+                    last_key = key
+
+    if save_filename:
+        with open(save_filename, 'wb+') as file:
+            save_replay.write(file)
 
 
 pathsep = os.path.pathsep
@@ -157,6 +189,7 @@ parser.add_argument('-s', '--stage', met
 parser.add_argument('-r', '--rank', metavar='RANK', type=int, default=0, help='Rank, from 0 (Easy, default) to 3 (Lunatic).')
 parser.add_argument('-c', '--character', metavar='CHARACTER', type=int, default=0, help='Select the character to use, from 0 (ReimuA, default) to 3 (MarisaB).')
 parser.add_argument('--replay', metavar='REPLAY', help='Select a replay')
+parser.add_argument('--save-replay', metavar='REPLAY', help='Save the upcoming game into a replay file')
 parser.add_argument('-b', '--boss-rush', action='store_true', help='Fight only bosses')
 parser.add_argument('--single-buffer', action='store_true', help='Disable double buffering')
 parser.add_argument('--fps-limit', metavar='FPS', default=60, type=int, help='Set fps limit')
@@ -166,5 +199,5 @@ parser.add_argument('--fixed-pipeline', 
 args = parser.parse_args()
 
 main(args.path, tuple(args.data), args.stage, args.rank, args.character,
-     args.replay, args.boss_rush, args.fps_limit, args.single_buffer,
-     args.debug, args.fixed_pipeline)
+     args.replay, args.save_replay, args.boss_rush, args.fps_limit,
+     args.single_buffer, args.debug, args.fixed_pipeline)
--- a/pytouhou/formats/t6rp.py
+++ b/pytouhou/formats/t6rp.py
@@ -20,8 +20,9 @@ a game of EoSD. Since the EoSD engine is
 replay file is sufficient to unfold a full game.
 """
 
-from struct import unpack
+from struct import unpack, pack
 from io import BytesIO
+from time import strftime
 
 from pytouhou.utils.helpers import read_string, get_logger
 
@@ -47,7 +48,17 @@ class T6RP(object):
         self.version = 0x102
         self.character = 0
         self.rank = 0
+        self.unknown1 = 0
+        self.unknown2 = 0
         self.key = 0
+        self.unknown3 = 0
+        self.date = strftime('%d/%m/%y')
+        self.name = 'PyTouhou'
+        self.unknown4 = 0
+        self.score = 0
+        self.unknown5 = 0
+        self.slowdown = 0.
+        self.unknown6 = 0
         self.levels = [None] * 7
 
 
@@ -84,8 +95,9 @@ class T6RP(object):
         if verify:
             data = file.read()
             file.seek(15)
-            if checksum != (sum(ord(c) for c in data) + 0x3f000318 + replay.key) & 0xffffffff:
-                raise Exception #TODO
+            real_sum = (sum(ord(c) for c in data) + 0x3f000318 + replay.key) & 0xffffffff
+            if checksum != real_sum:
+                raise Exception('Checksum mismatch: %d ≠ %d.' % (checksum, real_sum))
 
         replay.unknown3, = unpack('<B', file.read(1))
         replay.date = file.read(9) #read_string(file, 9, 'ascii')
@@ -114,3 +126,64 @@ class T6RP(object):
                 level.keys.append((time, keys, unknown))
 
         return replay
+
+
+    def write(self, file, encrypt=True):
+        if encrypt:
+            encrypted_file = file
+            file = BytesIO()
+
+        file.write(b'T6RP')
+        file.write(pack('<HBB', self.version, self.character, self.rank))
+
+        checksum_offset = file.tell()
+        file.seek(4, 1) # For checksum
+        file.write(pack('<BBB', self.unknown1, self.unknown2, self.key))
+
+        file.write(pack('<B', self.unknown3))
+
+        #TODO: find a more elegant method.
+        n = 9 - len(self.date)
+        file.write(self.date)
+        file.write('\0' * n)
+        n = 9 - len(self.name)
+        file.write(self.name)
+        file.write('\0' * n)
+
+        file.write(pack('<HIIfI', self.unknown4, self.score, self.unknown5, self.slowdown, self.unknown6))
+
+        stages_offsets_offset = file.tell()
+        file.seek(7*4, 1) # Skip the stages offsets.
+
+        stages_offsets = []
+        for level in self.levels:
+            if not level:
+                stages_offsets.append(0)
+                continue
+
+            stages_offsets.append(file.tell())
+            file.write(pack('<IHHBbbBI', level.score, level.random_seed,
+                            level.point_items, level.power, level.lives,
+                            level.bombs, level.difficulty, level.unknown))
+
+            for time, keys, unknown in level.keys:
+                file.write(pack('<IHH', time, keys, unknown))
+
+            file.write(pack('<IHH', 9999999, 0, 0))
+
+        file.seek(stages_offsets_offset)
+        file.write(pack('<7I', *stages_offsets))
+
+        # Write checksum
+        file.seek(15)
+        data = file.read()
+        checksum = (sum(ord(c) for c in data) + 0x3f000318 + self.key) & 0xffffffff
+        file.seek(checksum_offset)
+        file.write(pack('<I', checksum))
+
+        # Encrypt
+        if encrypt:
+            file.seek(0)
+            encrypted_file.write(file.read(15))
+            encrypted_file.write(b''.join(chr((ord(c) + self.key + 7*i) & 0xff) for i, c in enumerate(file.read())))
+
--- a/pytouhou/game/game.py
+++ b/pytouhou/game/game.py
@@ -14,8 +14,6 @@
 
 from itertools import chain
 
-from pytouhou.utils.random import Random
-
 from pytouhou.vm.eclrunner import ECLMainRunner
 from pytouhou.vm.msgrunner import MSGRunner
 
@@ -72,7 +70,7 @@ class Game(object):
         self.msg_wait = False
         self.bonus_list = [0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0,
                            1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 2]
-        self.prng = prng or Random()
+        self.prng = prng
         self.frame = 0
 
         self.enm_anm_wrapper = resource_loader.get_anm_wrapper2(('stg%denm.anm' % stage,
--- a/pytouhou/ui/gamerunner.py
+++ b/pytouhou/ui/gamerunner.py
@@ -61,7 +61,7 @@ class GameRunner(pyglet.window.Window, G
         self.clock = pyglet.clock.get_default()
 
 
-    def load_game(self, game=None, background=None, bgms=None, replay=None):
+    def load_game(self, game=None, background=None, bgms=None, replay=None, save_keystates=None):
         GameRenderer.load_game(self, game, background)
         self.replay_level = None
         if not replay or not replay.levels[game.stage-1]:
@@ -75,6 +75,8 @@ class GameRunner(pyglet.window.Window, G
             game.players[0].state.bombs = self.replay_level.bombs
             game.difficulty = self.replay_level.difficulty
 
+        self.save_keystates = save_keystates
+
         game.music = MusicPlayer(game.resource_loader, bgms)
         game.music.play(0)
 
@@ -166,6 +168,9 @@ class GameRunner(pyglet.window.Window, G
                     else:
                         keystate = _keystate
 
+            if self.save_keystates is not None:
+                self.save_keystates.append(keystate)
+
             self.game.run_iter(keystate)