Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
Talv committed Dec 13, 2018
0 parents commit ff50dd5
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
end_of_line = lf
charset = utf-8
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.pyc
*.egg-info
/__pycache__
/build
/dist
/out
.vscode/
s2repdump.spec
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SC2 Replay data dump

Small utility tool around [s2protocol](https://github.com/Blizzard/s2protocol) for processing `.SC2Replay` files. At the time being it exposes just two functionalities that might come useful for modders... and not only.

* Prints players list. including `PlayerHandle`
* Reconstruction of every `*.SC2Bank` file. Data comes from _NNet_ packets that is transmitted during preload process of online game.

## Installation

### Method 1 - Manual

**Requirements**

* Python2 (yes - **v2**, because _s2protocol_ is not compatible with py3...)

**Installation**

```sh
pip2 install https://github.com/Talv/sc2-repdump/archive/master.zip
```

### Method 2 - Bundled exe

Don't want to bother with installing Python environment? That's fine, you can download bundled version of this script, with single executable for Windows from [releases](https://github.com/Talv/sc2-repdump/releases).

## Usage

It's command line tool.

```
usage: s2repdump [-h] [--players] [--bank BANK] [--out OUT] [replay_file]
: Dump player handles:
--players [replay_file]
: Reconstruct players .SC2Bank files
--bank [player_slot] [replay_file]
positional arguments:
replay_file .SC2Replay file to load
optional arguments:
-h, --help show this help message and exit
--players print info about players
--bank BANK reconstruct player's SC2Bank files
--out OUT output directory
```

## Examples

**Dump list of the players**

It includes zero-indexed slotid, that should be used for `--bank` parameter.

```sh
$ s2repdump somerandomreplay.SC2Replay --players
{
"0": {
"handle": "2-S2-1-2642502",
"name": "Talv"
}
}
```

**Reconstruct `.SC2Bank` files of player at slot provided**

```sh
$ s2repdump somerandomreplay.SC2Replay --bank 0
Processing player "Talv"
Reconstructed "sefibecvprofile.SC2Bank"
Reconstructed "sefibeoptions.SC2Bank"
Reconstructed "sefiberewards.SC2Bank"
```
Empty file added s2repdump/__init__.py
Empty file.
178 changes: 178 additions & 0 deletions s2repdump/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#!/usr/bin/env python2

import sys
import os
import json
import argparse
import mpyq
from s2protocol import versions
import xml.etree.ElementTree as ET
from xml.dom import minidom


class SC2Bank(object):
def __init__(self, name):
self.name = name
self.sections = {}
self.sc_curr = None
self.key_curr = None

self.root = ET.Element('Bank')
self.root.set('version', '1')

def __str__(self):
return '%s (sections=%d)' % (self.name, len(self.sections))

def enter_section(self, name):
self.sc_curr = ET.Element('Section')
self.sc_curr.set('name', name)
self.root.append(self.sc_curr)

def enter_key(self, name, kind=None, value=None):
self.key_curr = ET.Element('Key')
self.key_curr.set('name', name)
self.sc_curr.append(self.key_curr)
if kind:
self.enter_value(kind, value)

def enter_value(self, kind, value):
el = ET.Element('Value')
if kind == 0:
attr = 'fixed'
elif kind == 1:
attr = 'flag'
elif kind == 2:
attr = 'int'
elif kind == 3:
attr = 'string'
# TODO: point
# TODO: unit
elif kind == 6:
attr = 'text'
elif kind == 7:
# skip - value will be in the next message
return
else:
raise Exception(
'unkown data kind: "%d". section = "%s" ; key = "%s" ; value = "%s"' %
(kind, self.sc_curr.get('name'), self.key_curr.get('name'), value)
)
el.set(attr, value.decode('utf8'))
self.key_curr.append(el)

def signature(self, signature):
el = ET.Element('Signature', {'value': ''.join('{:02X}'.format(x) for x in signature)})
self.root.append(el)

def write(self, cdir='./'):
fpath = os.path.abspath(cdir)
try:
os.makedirs(fpath)
except OSError:
if not os.path.isdir(fpath):
raise
filename = '%s.SC2Bank' % os.path.join(fpath, self.name)
btree = ET.ElementTree(self.root)
btree.write(filename, encoding='utf-8', xml_declaration=True)

# rewrite through minidom to beautify
pxml = minidom.parse(filename).toprettyxml(encoding='utf-8', indent=" " * 4, newl="\r\n")
with open(filename, 'wb') as f:
f.write(pxml)


def read_contents(archive, content):
contents = archive.read_file(content)
if not contents:
print('Error: Archive missing {}'.format(content))
sys.exit(1)
return contents


def reconstruct_banks(gameevents, player_id):
banks = []
for ev in gameevents:
if ev['_userid']['m_userId'] != player_id:
continue

if ev['_event'] == 'NNet.Game.SBankFileEvent':
banks.append(SC2Bank(ev['m_name']))
elif ev['_event'] == 'NNet.Game.SBankSectionEvent':
banks[-1].enter_section(ev['m_name'])
elif ev['_event'] == 'NNet.Game.SBankKeyEvent':
banks[-1].enter_key(ev['m_name'], ev['m_type'], ev['m_data'])
elif ev['_event'] == 'NNet.Game.SBankValueEvent':
banks[-1].enter_value(ev['m_type'], ev['m_data'])
elif ev['_event'] == 'NNet.Game.SBankSignatureEvent':
banks[-1].signature(ev['m_signature'])
else:
if ev['_gameloop'] > 0:
break
return banks


def read_players(details):
p_map = {}
for x in details['m_playerList']:
p_map[x['m_workingSetSlotId']] = {
'name': x['m_name'],
'handle': '%d-S2-%d-%d' % (x['m_toon']['m_region'], x['m_toon']['m_realm'], x['m_toon']['m_id']),
}
return p_map


def main():
parser = argparse.ArgumentParser(
prog='s2repdump',
description='''
: Dump player handles:
--players [replay_file]
: Reconstruct players .SC2Bank files
--bank [player_slot] [replay_file]
''',
formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument('replay_file', help='.SC2Replay file to load', nargs='?')
parser.add_argument('--players', help='print info about players', action='store_true')
parser.add_argument('--bank', help='reconstruct player\'s SC2Bank files', type=int)
parser.add_argument('--out', help='output directory', type=str, default='./out')
args = parser.parse_args()

if args.replay_file is None:
print(".SC2Replay file not specified")
sys.exit(1)

archive = mpyq.MPQArchive(args.replay_file)

# HEADER
contents = archive.header['user_data_header']['content']
header = versions.latest().decode_replay_header(contents)

# The header's baseBuild determines which protocol to use
baseBuild = header['m_version']['m_baseBuild']
try:
protocol = versions.build(baseBuild)
except Exception, e:
print('Unsupported base build: {0} ({1})'.format(baseBuild, str(e)))
protocol = versions.latest()
print('Attempting to use newest possible instead: %s' % protocol.__name__)

contents = read_contents(archive, 'replay.details')
details = protocol.decode_replay_details(contents)
p_map = read_players(details)

if args.players:
print(json.dumps(p_map, indent=True))

if args.bank is not None:
print('Processing player "%s"' % p_map[args.bank]['name'])
contents = read_contents(archive, 'replay.game.events')
banks = reconstruct_banks(protocol.decode_replay_game_events(contents), args.bank)
for b in banks:
print('Reconstructed "%s.SC2Bank"' % b.name)
b.write(args.out)


if __name__ == '__main__':
main()
19 changes: 19 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env python2

from distutils.core import setup

setup(
name='s2repdump',
version='0.1.0',
author='Talv',
url='https://github.com/Talv/s2repdump',
packages=['s2repdump'],
entry_points={
'console_scripts': [
's2repdump=s2repdump.main:main',
]
},
install_requires=[
's2protocol'
],
)

0 comments on commit ff50dd5

Please sign in to comment.