SnowFlash383935 commited on
Commit
56194fd
·
verified ·
1 Parent(s): 820ccad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +226 -43
app.py CHANGED
@@ -2,84 +2,267 @@ import gradio as gr
2
  import bz2
3
  import bson
4
  import struct
 
5
 
6
- PARTICLE_FORMAT = "<iii ffff f ii iiiii I" # 14 полей = 56 байт
7
- FIELDS = [
8
- "type", "life", "ctype",
9
- "x", "y", "vx", "vy", "temp",
10
- "tmp3", "tmp4", "flags", "tmp", "tmp2", "dcolour"
11
- ]
12
- PARTICLE_SIZE = 128
13
-
14
- def parse_parts(binary_data):
15
- count = len(binary_data) // PARTICLE_SIZE
16
- particles = []
17
- for i in range(count):
18
- chunk = binary_data[i * PARTICLE_SIZE : i * PARTICLE_SIZE + 56]
19
- try:
20
- values = struct.unpack(PARTICLE_FORMAT, chunk)
21
- particle = dict(zip(FIELDS, values))
22
- particles.append(particle)
23
- except Exception as e:
24
- particles.append({"error": str(e)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  return particles
26
 
 
27
  def analyze_cps(file):
28
  if file is None:
29
- return "", "", "", ""
30
-
31
  try:
32
  with open(file.name, "rb") as f:
33
  raw_data = f.read()
34
-
35
  hex_orig = ' '.join(f'{b:02x}' for b in raw_data[:256])
36
  hex_original = f"Оригинал (первые 256 байт):\n{hex_orig}"
37
-
38
  if raw_data[:4] != b'OPS1':
39
- return hex_original, "[Ошибка] Неверная сигнатура файла.", "", ""
40
-
41
  compressed = raw_data[12:]
42
  try:
43
  decompressed = bz2.decompress(compressed)
44
  except Exception as e:
45
- return hex_original, f"[Ошибка bzip2]: {e}", "", ""
46
-
47
  hex_decomp = ' '.join(f'{b:02x}' for b in decompressed[:512])
48
  hex_decomp_view = f"Распакованные данные (первые 512 байт):\n{hex_decomp}"
49
-
50
  try:
51
  doc = bson.decode(decompressed)
52
  except Exception as e:
53
- return hex_original, hex_decomp_view, f"[Ошибка BSON]: {e}", ""
54
-
55
  # Парсим parts
56
- if 'parts' in doc:
57
  try:
58
- binary_data = bytes(doc['parts'])
59
- parsed_parts = parse_parts(binary_data)
60
- doc['parts'] = parsed_parts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  except Exception as e:
62
- doc['parts'] = f"[Ошибка парсинга parts]: {e}"
63
-
 
64
  return hex_original, hex_decomp_view, str(doc)
65
-
66
  except Exception as e:
67
- return f"[Ошибка]: {e}", "", "", ""
68
 
 
69
  with gr.Blocks() as demo:
70
- gr.Markdown("## Распаковка .cps с красивым parts")
 
71
  file_input = gr.File(label="Загрузите .cps файл", file_types=[".cps"])
72
-
73
  hex_orig_out = gr.Textbox(label="HEX оригинала", lines=5)
74
  hex_decomp_out = gr.Textbox(label="HEX распакованных данных", lines=10)
75
- bson_parsed = gr.Textbox(label="Распарсенный BSON с parts", lines=30)
76
-
77
  file_input.change(
78
  fn=analyze_cps,
79
  inputs=file_input,
80
  outputs=[hex_orig_out, hex_decomp_out, bson_parsed]
81
  )
82
-
83
  file_input.clear(
84
  fn=lambda: ("", "", ""),
85
  outputs=[hex_orig_out, hex_decomp_out, bson_parsed]
 
2
  import bz2
3
  import bson
4
  import struct
5
+ from typing import List, Dict, Any
6
 
7
+ # ---------- низкоуровневые хелперы ----------
8
+ def _u8(buf, off) -> int:
9
+ return buf[off]
10
+ def _u16(buf, off) -> int:
11
+ return struct.unpack_from('<H', buf, off)[0]
12
+ def _i8(buf, off) -> int:
13
+ return struct.unpack_from('<b', buf, off)[0]
14
+
15
+ # ---------- чтение partsPos ----------
16
+ def read_parts_pos(partsPos: bytes, bx: int, by: int) -> List[int]:
17
+ """Возвращает сколько частиц в каждой клетке (длина = bx*by)"""
18
+ if len(partsPos) != bx * by * 3:
19
+ raise ValueError(f'partsPos length mismatch: expected {bx*by*3}, got {len(partsPos)}')
20
+ cnt_map = []
21
+ for i in range(bx * by):
22
+ base = i * 3
23
+ cnt = (partsPos[base] << 16) | (partsPos[base + 1] << 8) | partsPos[base + 2]
24
+ cnt_map.append(cnt)
25
+ return cnt_map
26
+
27
+ # ---------- класс Particle ----------
28
+ class Particle:
29
+ __slots__ = ('type', 'x', 'y', 'temp', 'life', 'tmp', 'ctype',
30
+ 'dcolour', 'vx', 'vy', 'tmp2', 'tmp3', 'tmp4')
31
+
32
+ def __init__(self):
33
+ self.type: int = 0
34
+ self.x: float = 0.0
35
+ self.y: float = 0.0
36
+ self.temp: float = 0.0
37
+ self.life: int = 0
38
+ self.tmp: int = 0
39
+ self.ctype: int = 0
40
+ self.dcolour: int = 0
41
+ self.vx: float = 0.0
42
+ self.vy: float = 0.0
43
+ self.tmp2: int = 0
44
+ self.tmp3: int = 0
45
+ self.tmp4: int = 0
46
+
47
+ def to_dict(self) -> Dict[str, Any]:
48
+ return {field: getattr(self, field) for field in self.__slots__}
49
+
50
+ def __repr__(self):
51
+ return f'Particle(type={self.type} @{self.x:.1f},{self.y:.1f})'
52
+
53
+ # ---------- чтение parts ----------
54
+ def read_parts(partsData: bytes, partsPosCounts: List[int],
55
+ bx: int, by: int, cell_size: int = 4) -> List[Dict[str, Any]]:
56
+ """
57
+ partsData – байты из BSON['parts']
58
+ partsPosCounts – результат read_parts_pos
59
+ Возвращает список частиц как dict
60
+ """
61
+ px_per_cell = cell_size
62
+ particles: List[Dict[str, Any]] = []
63
+ off = 0
64
+ data_len = len(partsData)
65
+
66
+ for y in range(by):
67
+ for x in range(bx):
68
+ cnt = partsPosCounts[y * bx + x]
69
+ base_x, base_y = x * px_per_cell, y * px_per_cell
70
+
71
+ for _ in range(cnt):
72
+ if off >= data_len - 3:
73
+ raise ValueError('truncated partsData')
74
+
75
+ # 1. type (1 байт + возможно второй)
76
+ ty = _u8(partsData, off)
77
+ off += 1
78
+
79
+ # 2. field-descriptor 2 байта
80
+ fd = _u16(partsData, off)
81
+ off += 2
82
+
83
+ has_fd3 = bool(fd & 0x8000)
84
+ if has_fd3:
85
+ if off >= data_len:
86
+ raise ValueError('no fd3 byte')
87
+ fd3 = _u8(partsData, off)
88
+ off += 1
89
+ fd |= fd3 << 16
90
+
91
+ # если type > 255 – второй байт
92
+ if fd & (1 << 14):
93
+ ty |= _u8(partsData, off) << 8
94
+ off += 1
95
+
96
+ p = Particle()
97
+ p.type = ty
98
+ p.x = float(base_x + px_per_cell // 2)
99
+ p.y = float(base_y + px_per_cell // 2)
100
+
101
+ # temperature
102
+ if fd & 1: # 16-бит абсолютное
103
+ p.temp = float(_u16(partsData, off))
104
+ off += 2
105
+ else: # 1 байт относительно 294.15 K
106
+ delta = _i8(partsData, off)
107
+ off += 1
108
+ p.temp = 294.15 + delta
109
+
110
+ # life
111
+ if fd & (1 << 1):
112
+ life = _u8(partsData, off)
113
+ off += 1
114
+ if fd & (1 << 2):
115
+ life |= _u8(partsData, off) << 8
116
+ off += 1
117
+ p.life = life
118
+
119
+ # tmp
120
+ if fd & (1 << 3):
121
+ tmp = _u8(partsData, off)
122
+ off += 1
123
+ if fd & (1 << 4):
124
+ tmp |= _u8(partsData, off) << 8
125
+ off += 1
126
+ if fd & (1 << 12):
127
+ tmp |= _u8(partsData, off) << 24
128
+ off += 1
129
+ tmp |= _u8(partsData, off) << 16
130
+ off += 1
131
+ p.tmp = tmp
132
+
133
+ # ctype
134
+ if fd & (1 << 5):
135
+ ct = _u8(partsData, off)
136
+ off += 1
137
+ if fd & (1 << 9):
138
+ ct |= _u8(partsData, off) << 24
139
+ off += 1
140
+ ct |= _u8(partsData, off) << 16
141
+ off += 1
142
+ ct |= _u8(partsData, off) << 8
143
+ off += 1
144
+ p.ctype = ct
145
+
146
+ # dcolour
147
+ if fd & (1 << 6):
148
+ p.dcolour = (_u8(partsData, off) << 24) | \
149
+ (_u8(partsData, off + 1) << 16) | \
150
+ (_u8(partsData, off + 2) << 8) | \
151
+ _u8(partsData, off + 3)
152
+ off += 4
153
+
154
+ # vx, vy
155
+ if fd & (1 << 7):
156
+ p.vx = (_u8(partsData, off) - 127.0) / 16.0
157
+ off += 1
158
+ if fd & (1 << 8):
159
+ p.vy = (_u8(partsData, off) - 127.0) / 16.0
160
+ off += 1
161
+
162
+ # tmp2
163
+ if fd & (1 << 10):
164
+ t2 = _u8(partsData, off)
165
+ off += 1
166
+ if fd & (1 << 11):
167
+ t2 |= _u8(partsData, off) << 8
168
+ off += 1
169
+ p.tmp2 = t2
170
+
171
+ # tmp3/tmp4
172
+ if fd & (1 << 13):
173
+ p.tmp3 = _u16(partsData, off)
174
+ p.tmp4 = _u16(partsData, off + 2)
175
+ off += 4
176
+ if fd & (1 << 16): # high halves
177
+ p.tmp3 |= _u16(partsData, off) << 16
178
+ p.tmp4 |= _u16(partsData, off + 2) << 16
179
+ off += 4
180
+
181
+ particles.append(p.to_dict())
182
+
183
  return particles
184
 
185
+ # ---------- основная функция Gradio ----------
186
  def analyze_cps(file):
187
  if file is None:
188
+ return "", "", ""
189
+
190
  try:
191
  with open(file.name, "rb") as f:
192
  raw_data = f.read()
193
+
194
  hex_orig = ' '.join(f'{b:02x}' for b in raw_data[:256])
195
  hex_original = f"Оригинал (первые 256 байт):\n{hex_orig}"
196
+
197
  if raw_data[:4] != b'OPS1':
198
+ return hex_original, "[Ошибка] Неверная сигнатура файла.", ""
199
+
200
  compressed = raw_data[12:]
201
  try:
202
  decompressed = bz2.decompress(compressed)
203
  except Exception as e:
204
+ return hex_original, f"[Ошибка bzip2]: {e}", ""
205
+
206
  hex_decomp = ' '.join(f'{b:02x}' for b in decompressed[:512])
207
  hex_decomp_view = f"Распакованные данные (первые 512 байт):\n{hex_decomp}"
208
+
209
  try:
210
  doc = bson.decode(decompressed)
211
  except Exception as e:
212
+ return hex_original, hex_decomp_view, f"[Ошибка BSON]: {e}"
213
+
214
  # Парсим parts
215
+ if 'parts' in doc and 'partsPos' in doc and 'blockSize' in doc:
216
  try:
217
+ # Получаем размеры блока
218
+ block_size = doc['blockSize']
219
+ if isinstance(block_size, dict):
220
+ bx, by = block_size.get('x', 0), block_size.get('y', 0)
221
+ elif isinstance(block_size, (list, tuple)):
222
+ bx, by = block_size[0], block_size[1]
223
+ else:
224
+ bx = by = int(block_size)
225
+
226
+ if bx <= 0 or by <= 0:
227
+ raise ValueError(f"Invalid blockSize: {block_size}")
228
+
229
+ # Читаем partsPos и parts
230
+ parts_pos_raw = bytes(doc['partsPos'])
231
+ parts_raw = bytes(doc['parts'])
232
+
233
+ counts = read_parts_pos(parts_pos_raw, bx, by)
234
+ parsed_parts = read_parts(parts_raw, counts, bx, by)
235
+
236
+ # Заменяем бинарные данные на распарсенные
237
+ doc['parts'] = f"[{len(parsed_parts)} particles parsed]"
238
+ doc['partsPos'] = f"[{len(counts)} cells]"
239
+ doc['_particles'] = parsed_parts # Сохраняем отдельно
240
+
241
  except Exception as e:
242
+ doc['parts'] = f"[Ошибка парсинга]: {e}"
243
+ doc['partsPos'] = f"[Ошибка парсинга]: {e}"
244
+
245
  return hex_original, hex_decomp_view, str(doc)
246
+
247
  except Exception as e:
248
+ return f"[Ошибка]: {e}", "", ""
249
 
250
+ # ---------- Gradio интерфейс ----------
251
  with gr.Blocks() as demo:
252
+ gr.Markdown("## Распаковка .cps (The Powder Toy) с полным парсингом частиц")
253
+
254
  file_input = gr.File(label="Загрузите .cps файл", file_types=[".cps"])
255
+
256
  hex_orig_out = gr.Textbox(label="HEX оригинала", lines=5)
257
  hex_decomp_out = gr.Textbox(label="HEX распакованных данных", lines=10)
258
+ bson_parsed = gr.Textbox(label="Распарсенный BSON", lines=30)
259
+
260
  file_input.change(
261
  fn=analyze_cps,
262
  inputs=file_input,
263
  outputs=[hex_orig_out, hex_decomp_out, bson_parsed]
264
  )
265
+
266
  file_input.clear(
267
  fn=lambda: ("", "", ""),
268
  outputs=[hex_orig_out, hex_decomp_out, bson_parsed]