/
demo.py
executable file
·140 lines (117 loc) · 4.48 KB
/
demo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# -*- coding: utf-8 -*-
from stream import *
from data import *
import sys
import os
stream = None
def CLIENTDEMO_GetStreamPos():
global stream
try:
return stream.tell()
except:
return -1
def CLIENTDEMO_Read(fn, callback):
global stream
stream = open(fn, 'rb')
# read demo header, assume its always the same
class DemoError(Exception):
pass
if stream.read(4) != b'ZCLD':
raise DemoError('Expected ZCLD demo')
#if NETWORK_ReadUByte(stream) != CLD_DEMOLENGTH:
# raise DemoError('Invalid demo header: expected CLD_DEMOLENGTH')
# we can't do Zandronum behavior here.
# CLD_DEMOLENGTH is assumed to be legit, and off it values for other CLD_ enums are calculated.
# this is required to support multiple demo versions.
CLD_DEMOLENGTH = NETWORK_ReadUByte(stream)
CLD_DEMOVERSION = CLD_DEMOLENGTH+1
CLD_CVARS = CLD_DEMOLENGTH+2
CLD_USERINFO = CLD_DEMOLENGTH+3
CLD_BODYSTART = CLD_DEMOLENGTH+4
CLD_TICCMD = CLD_DEMOLENGTH+5
CLD_LOCALCOMMAND = CLD_DEMOLENGTH+6
CLD_DEMOEND = CLD_DEMOLENGTH+7
CLD_DEMOWADS = CLD_DEMOLENGTH+8
g_lDemoLength = NETWORK_ReadULong(stream)
DOTVERSIONSTR = None
bBodyStart = False
while not bBodyStart:
lCommand = NETWORK_ReadUByte(stream)
if lCommand == CLD_DEMOVERSION:
lDemoVersion = NETWORK_ReadShort(stream)
DOTVERSIONSTR = NETWORK_ReadString(stream)
print('Version %s demo' % DOTVERSIONSTR)
BUILD_ID = NETWORK_ReadUByte(stream)
rngseed = NETWORK_ReadULong(stream)
break
if DOTVERSIONSTR is None:
raise DemoError('No version found in the demo')
# now use DOTVERSIONSTR to determine further parser
# a parser is basically python module which receives stream and callback.
basever = DOTVERSIONSTR.split('-')[0]
# for example, 2.1.2
# query modules
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(os.path.dirname(os.path.abspath(__file__))+'/versions')
modules = os.listdir('versions')
mod_parse = None
mod_parse_header = None
for module in modules:
module = module.lower()
# if it doesn't end with .py, we don't need it
if module[-3:] != '.py':
continue
module = module[:-3]
if module == '__init__':
continue
# try importing this
try:
mod = __import__('versions.'+module, globals(), locals(), ['version', 'next_header', 'next_packet'], 0)
except ImportError:
continue
if not hasattr(mod, 'version') or not hasattr(mod, 'next_header') or not hasattr(mod, 'next_packet'):
continue
# module class should have function called version() which specifies applicable versions (list)
if basever in mod.version():
mod_parse = mod.next_packet
mod_parse_header = mod.next_header
break
if mod_parse is None:
raise DemoError('No suitable parser found for version %s (%s)' % (basever, DOTVERSIONSTR))
while not bBodyStart:
lCommand = NETWORK_ReadUByte(stream)
if lCommand == CLD_BODYSTART:
bBodyStart = True
else:
stream.seek(stream.tell()-1)
try:
pkt = mod_parse_header(stream)
if pkt is None:
raise DemoError('Unknown demo header %02X (%d)' % (lCommand, lCommand))
except:
print('Exception @ %X' % CLIENTDEMO_GetStreamPos())
raise
# run callback on packet?..
# yup
callback(Struct(**pkt))
while True:
try:
lCommand = NETWORK_ReadUByte(stream)
if lCommand == 255:
raise
except:
print('Demo ended.')
break
stream.seek(stream.tell()-1)
try:
pkt = mod_parse(stream)
if pkt is None:
raise DemoError('Unknown demo packet %02X (%d)' % (lCommand, lCommand))
except:
print('Exception @ %X' % CLIENTDEMO_GetStreamPos())
raise
# run callback on packet
callback(Struct(**pkt))
if pkt['name'] == 'CLD_DEMOEND':
print('Demo ended.')
break