sc2 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/lib/sc2.rb +51 -0
- data/lib/sc2/magick/hasher.rb +45 -0
- data/lib/sc2/magick/parser.rb +42 -0
- data/lib/sc2/magick/reader.rb +60 -0
- data/lib/sc2/version.rb +3 -0
- data/lib/sc2/wrappers/archive_header.rb +18 -0
- data/lib/sc2/wrappers/attribute.rb +9 -0
- data/lib/sc2/wrappers/block_table_entry.rb +25 -0
- data/lib/sc2/wrappers/hash_table_entry.rb +11 -0
- data/lib/sc2/wrappers/init_data.rb +14 -0
- data/lib/sc2/wrappers/player.rb +34 -0
- data/lib/sc2/wrappers/user_header.rb +10 -0
- data/sc2.gemspec +25 -0
- metadata +122 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/sc2.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bindata'
|
3
|
+
require 'bzip2-ruby'
|
4
|
+
Dir['./wrappers/*.rb'].each { |f| require f }
|
5
|
+
Dir['./magick/*.rb'].each { |f| require f }
|
6
|
+
|
7
|
+
module Sc2
|
8
|
+
class Replay
|
9
|
+
def initialize rep_file
|
10
|
+
@reader = Reader.new rep_file
|
11
|
+
@replay = Hash[:details, Parser.parse(@reader.read_file('replay.details')),
|
12
|
+
:initData, InitData.read(@reader.read_file('replay.initData')),
|
13
|
+
:userData, Parser.parse(@reader.user_hdr.user_data),
|
14
|
+
:gameVersion, '']
|
15
|
+
@replay[:gameVersion] = { :major => @replay[:userData][1][1], :minor => @replay[:userData][1][2], :patch => @replay[:userData[1][3]], :build => @replay[:userData][1][4] }
|
16
|
+
@replay[:players] = p_players
|
17
|
+
end
|
18
|
+
|
19
|
+
def map
|
20
|
+
@replay[:details][1]
|
21
|
+
end
|
22
|
+
|
23
|
+
def winner
|
24
|
+
@replay[:players].each { |player| return player if player.win? }
|
25
|
+
end
|
26
|
+
|
27
|
+
def loser
|
28
|
+
@replay[:players].each { |player| return player if !player.win? }
|
29
|
+
end
|
30
|
+
|
31
|
+
def players
|
32
|
+
@replay[:players]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def attributes
|
37
|
+
return @attributes if defined? @attributes
|
38
|
+
data = @reader.read_file "replay.attributes.events"
|
39
|
+
data.slice! 0, (@replay[:gameVersion][:build] < 17326 ? 4 : 5)
|
40
|
+
@attributes = []
|
41
|
+
data.slice!(0, 4).unpack("V")[0].times { @attributes << Attribute.read(data.slice!(0, 13)) }
|
42
|
+
@attributes
|
43
|
+
end
|
44
|
+
|
45
|
+
def p_players
|
46
|
+
return @players if defined? @players
|
47
|
+
player_id = 0
|
48
|
+
@players = @replay[:details][0].map { |player| Player.new ({ :name => player[0], :outcome => player[8], :id => player_id += 1, :attrs => attributes.map { |attr| { :attr_id => attr[:id], :value => attr[:val] } if attr[:player] == player_id } }) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Sc2
|
2
|
+
class Hasher
|
3
|
+
def initialize
|
4
|
+
set_enc_tbl
|
5
|
+
end
|
6
|
+
|
7
|
+
def hash_for hash_type, s
|
8
|
+
hash_type = [:table_offset, :hash_a, :hash_b, :table].index hash_type
|
9
|
+
seed1, seed2 = 0x7FED7FED, 0xEEEEEEEE
|
10
|
+
s.upcase.each_byte do |c|
|
11
|
+
value = @encryption_table[(hash_type << 8) + c]
|
12
|
+
seed1 = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
|
13
|
+
seed2 = (c + seed1 + seed2 + (seed2 << 5) + 3) & 0xFFFFFFFF
|
14
|
+
end
|
15
|
+
seed1
|
16
|
+
end
|
17
|
+
|
18
|
+
def decrypt data, seed1
|
19
|
+
seed2 = 0xEEEEEEEE
|
20
|
+
data.unpack('V*').map do |value|
|
21
|
+
seed2 = (seed2 + @encryption_table[0x400 + (seed1 & 0xFF)]) & 0xFFFFFFFF
|
22
|
+
value = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
|
23
|
+
seed1 = (((~seed1 << 0x15) + 0x11111111) | (seed1 >> 0x0B)) & 0xFFFFFFFF
|
24
|
+
seed2 = (value + seed2 + (seed2 << 5) + 3) & 0xFFFFFFFF
|
25
|
+
value
|
26
|
+
end.pack('V*')
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def set_enc_tbl
|
31
|
+
seed = 0x00100001
|
32
|
+
@encryption_table = {}
|
33
|
+
(0..255).each do |i|
|
34
|
+
index = i
|
35
|
+
(0..4).each do |j|
|
36
|
+
seed = (seed * 125 + 3) % 0x2AAAAB
|
37
|
+
tmp1 = (seed & 0xFFFF) << 0x10
|
38
|
+
seed = (seed * 125 + 3) % 0x2AAAAB
|
39
|
+
tmp2 = (seed & 0xFFFF)
|
40
|
+
@encryption_table[i + j * 0x100] = (tmp1 | tmp2)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Sc2
|
2
|
+
class Parser
|
3
|
+
def self.parse data
|
4
|
+
self.parse_recur String.new data
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.parse_recur data
|
8
|
+
case data.slice!(0).bytes.next
|
9
|
+
when 2
|
10
|
+
data.slice! 0, vlf(data)
|
11
|
+
when 4
|
12
|
+
data.slice! 0, 2
|
13
|
+
(0...vlf(data)).map {|i| parse_recur data }
|
14
|
+
when 5
|
15
|
+
Hash.[]((0...vlf(data)).map do |i|
|
16
|
+
[vlf(data), parse_recur(data)]
|
17
|
+
end)
|
18
|
+
when 6
|
19
|
+
data.slice! 0
|
20
|
+
when 7
|
21
|
+
data.slice!(0, 4).unpack("V")[0]
|
22
|
+
when 9
|
23
|
+
vlf data
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.vlf data
|
30
|
+
ret, shift = 0, 0
|
31
|
+
loop do
|
32
|
+
char = data.slice!(0)
|
33
|
+
return nil unless char
|
34
|
+
byte = char.bytes.next
|
35
|
+
ret += (byte & 0x7F) << (7 * shift)
|
36
|
+
break if byte & 0x80 == 0
|
37
|
+
shift += 1
|
38
|
+
end
|
39
|
+
(ret >> 1) * ((ret & 0x1) == 0 ? 1 : -1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Sc2
|
2
|
+
class Reader
|
3
|
+
attr_reader :user_hdr
|
4
|
+
def initialize rep_file
|
5
|
+
@rep_file = rep_file
|
6
|
+
set_hdrs
|
7
|
+
set_tbls
|
8
|
+
end
|
9
|
+
|
10
|
+
def read_file file
|
11
|
+
hasher = Hasher.new
|
12
|
+
hash_a = hasher.hash_for :hash_a, file
|
13
|
+
hash_b = hasher.hash_for :hash_b, file
|
14
|
+
hash_entry = @hash_tbl.find { |h| [h.hash_a, h.hash_b] == [hash_a, hash_b] }
|
15
|
+
block_entry = @block_tbl[hash_entry.block_index]
|
16
|
+
@rep_file.seek @user_hdr.archive_header_offset + block_entry.block_offset
|
17
|
+
file_data = @rep_file.read block_entry.archived_size
|
18
|
+
if block_entry.single_unit?
|
19
|
+
file_data = Bzip2.uncompress file_data[1, file_data.length] if block_entry.compressed? && file_data.bytes.next == 16
|
20
|
+
return file_data
|
21
|
+
end
|
22
|
+
sector_size = 512 << @archive_hdr.sector_size_shift
|
23
|
+
sectors = block_entry.size / sector_size + 1
|
24
|
+
sectors += 1 if block_entry.has_checksums
|
25
|
+
positions = file_data[0, 4 * (sectors + 1)].unpack "V#{sectors + 1}"
|
26
|
+
sectors = []
|
27
|
+
positions.each_with_index do |pos, i|
|
28
|
+
break if i + 1 == positions.length
|
29
|
+
sector = file_data[pos, positions[i + 1] - pos]
|
30
|
+
sector = Bzip2.uncompress sector if block_entry.compressed? && block_entry.size > block_entry.archived_size && sector.bytes.next == 16
|
31
|
+
sectors << sector
|
32
|
+
end
|
33
|
+
sectors.join ''
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def set_hdrs
|
38
|
+
@user_hdr = UserHeader.read @rep_file
|
39
|
+
@rep_file.seek @user_hdr.archive_header_offset
|
40
|
+
@archive_hdr = ArchiveHeader.read @rep_file
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_tbls
|
44
|
+
@hash_tbl = read_tbl 'hash'
|
45
|
+
@block_tbl = read_tbl 'block'
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_tbl type
|
49
|
+
tbl_offset = @archive_hdr[type + '_table_offset']
|
50
|
+
@rep_file.seek @user_hdr.archive_header_offset + tbl_offset
|
51
|
+
tbl_entries = @archive_hdr[type + '_table_entries']
|
52
|
+
hasher = Hasher.new
|
53
|
+
key = hasher.hash_for :table, "(#{type} table)"
|
54
|
+
data = @rep_file.read tbl_entries * 16
|
55
|
+
data = hasher.decrypt data, key
|
56
|
+
layout = type == 'hash' ? HashTableEntry : BlockTableEntry
|
57
|
+
(0...tbl_entries).map { |i| layout.read(data[i * 16, 16]) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/sc2/version.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Sc2
|
2
|
+
class ArchiveHeader < BinData::Record
|
3
|
+
endian :little
|
4
|
+
string :archive_magic, :length => 4
|
5
|
+
int32 :header_size
|
6
|
+
int32 :archive_size
|
7
|
+
int16 :format_version
|
8
|
+
int8 :sector_size_shift
|
9
|
+
int8
|
10
|
+
int32 :hash_table_offset
|
11
|
+
int32 :block_table_offset
|
12
|
+
int32 :hash_table_entries
|
13
|
+
int32 :block_table_entries
|
14
|
+
int64 :extended_block_table_offset
|
15
|
+
int16 :hash_table_offset_high
|
16
|
+
int16 :block_table_offset_high
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Sc2
|
2
|
+
class BlockTableEntry < BinData::Record
|
3
|
+
endian :little
|
4
|
+
int32 :block_offset
|
5
|
+
int32 :archived_size
|
6
|
+
int32 :file_size
|
7
|
+
uint32 :flags
|
8
|
+
|
9
|
+
def file?
|
10
|
+
(flags & 0x80000000) != 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def compressed?
|
14
|
+
(flags & 0x00000200) != 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def encrypted?
|
18
|
+
(flags & 0x00010000) != 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def single_unit?
|
22
|
+
(flags & 0x01000000) != 0
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Sc2
|
2
|
+
class InitData < BinData::Record
|
3
|
+
uint8 :num_players
|
4
|
+
array :players, :initial_length => :num_players do
|
5
|
+
uint8 :player_name_length
|
6
|
+
string :player_name, :length => :player_name_length
|
7
|
+
skip :length => 5
|
8
|
+
end
|
9
|
+
string :unknown_24, :length => 24
|
10
|
+
uint8 :account_length
|
11
|
+
string :account, :length => :account_length
|
12
|
+
rest :rest
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Sc2
|
2
|
+
class Player
|
3
|
+
OUTCOMES = [:unknown, :win, :loss]
|
4
|
+
ATTRIBUTES = { :player_race => { "RAND" => :random, "Terr" => 'terran', "Prot" => 'protoss', "Zerg" => 'zerg' } }
|
5
|
+
def initialize params
|
6
|
+
@params = params
|
7
|
+
@params[:outcome] = OUTCOMES[@params[:outcome]]
|
8
|
+
@params[:attrs].compact!
|
9
|
+
@params[:attrs].each { |attr| attr[:value].reverse! }
|
10
|
+
end
|
11
|
+
|
12
|
+
def race
|
13
|
+
@params[:attrs].each do |attr|
|
14
|
+
return ATTRIBUTES[:player_race][attr[:value]] if attr[:attr_id] == 3001
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def id
|
19
|
+
@params[:id]
|
20
|
+
end
|
21
|
+
|
22
|
+
def name
|
23
|
+
@params[:name]
|
24
|
+
end
|
25
|
+
|
26
|
+
def win?
|
27
|
+
@params[:outcome] == :win
|
28
|
+
end
|
29
|
+
|
30
|
+
def loss?
|
31
|
+
@params[:outcome] == :loss
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/sc2.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "sc2"
|
6
|
+
s.version = "0.0.5"
|
7
|
+
s.authors = ["vasechika"]
|
8
|
+
s.email = ["second.pilot@gmail.com"]
|
9
|
+
s.homepage = "https://github.com/vasechika/sc2"
|
10
|
+
s.summary = "SC 2"
|
11
|
+
s.description = "Gem for parsing Starcraft 2 replays"
|
12
|
+
|
13
|
+
s.rubyforge_project = "sc2"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# specify any dependencies here; for example:
|
21
|
+
# s.add_runtime_dependency "rest-client"
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_dependency "bindata"
|
24
|
+
s.add_dependency "bzip2-ruby"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sc2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 21
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 5
|
10
|
+
version: 0.0.5
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- vasechika
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-10-16 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: bindata
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: bzip2-ruby
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
type: :runtime
|
61
|
+
version_requirements: *id003
|
62
|
+
description: Gem for parsing Starcraft 2 replays
|
63
|
+
email:
|
64
|
+
- second.pilot@gmail.com
|
65
|
+
executables: []
|
66
|
+
|
67
|
+
extensions: []
|
68
|
+
|
69
|
+
extra_rdoc_files: []
|
70
|
+
|
71
|
+
files:
|
72
|
+
- .gitignore
|
73
|
+
- Gemfile
|
74
|
+
- Rakefile
|
75
|
+
- lib/sc2.rb
|
76
|
+
- lib/sc2/magick/hasher.rb
|
77
|
+
- lib/sc2/magick/parser.rb
|
78
|
+
- lib/sc2/magick/reader.rb
|
79
|
+
- lib/sc2/version.rb
|
80
|
+
- lib/sc2/wrappers/archive_header.rb
|
81
|
+
- lib/sc2/wrappers/attribute.rb
|
82
|
+
- lib/sc2/wrappers/block_table_entry.rb
|
83
|
+
- lib/sc2/wrappers/hash_table_entry.rb
|
84
|
+
- lib/sc2/wrappers/init_data.rb
|
85
|
+
- lib/sc2/wrappers/player.rb
|
86
|
+
- lib/sc2/wrappers/user_header.rb
|
87
|
+
- sc2.gemspec
|
88
|
+
homepage: https://github.com/vasechika/sc2
|
89
|
+
licenses: []
|
90
|
+
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
hash: 3
|
102
|
+
segments:
|
103
|
+
- 0
|
104
|
+
version: "0"
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 3
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
version: "0"
|
114
|
+
requirements: []
|
115
|
+
|
116
|
+
rubyforge_project: sc2
|
117
|
+
rubygems_version: 1.8.11
|
118
|
+
signing_key:
|
119
|
+
specification_version: 3
|
120
|
+
summary: SC 2
|
121
|
+
test_files: []
|
122
|
+
|