tassadar 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --format nested
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create 1.9.2@tassadar
@@ -0,0 +1,9 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.0.2
4
+
5
+ * Converts serialized strings ASCII-8BIT => UTF-8. This was the source of serious encoding problems in replay parsing.
6
+
7
+ ## 0.0.1
8
+
9
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in tassadar.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
7
+ gem 'watchr'
8
+ gem 'ruby-debug19'
9
+ gem 'rr'
10
+ gem 'rake'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011-2012 Agora Games
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # Tassadar
2
+
3
+ Starcraft 2 replay parser written in pure-Ruby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```
10
+ gem 'tassadar'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```
22
+ $ gem install tassadar
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Create a replay object:
28
+
29
+ ```ruby
30
+ replay = Tassadar::SC2::Replay.new(path_to_replay)
31
+ ```
32
+
33
+ All of the important information is in the game object:
34
+
35
+ ```ruby
36
+ replay.game
37
+ => #<Tassadar::SC2::Game:0x007f9e41e31408 @winner=#<Tassadar::SC2::Player:0x007f9e41e31728 @name="redgar", @id=2569192, @won=true, @color={:alpha=>255, :red=>0, :green=>66, :blue=>255}, @chosen_race="Zerg", @actual_race="Zerg", @handicap=100>, @time=2011-07-05 17:01:08 -0500, @map="Delta Quadrant">
38
+ ```
39
+
40
+ Or the player objects:
41
+
42
+ ```ruby
43
+ replay.players.first
44
+ => #<Tassadar::SC2::Player:0x007f9e41e31a48 @name="guitsaru", @id=1918894, @won=false, @color={:alpha=>255, :red=>180, :green=>20, :blue=>30}, @chosen_race="Terran", @actual_race="Terran", @handicap=100>
45
+ ```
46
+
47
+ ## Contributing
48
+
49
+ 1. Fork it
50
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
51
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
52
+ 4. Push to the branch (`git push origin my-new-feature`)
53
+ 5. Create new Pull Request
54
+
55
+ ## Copyright
56
+
57
+ Copyright (c) 2012 Agora Games. See LICENSE.txt for further details.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake'
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Open an irb session preloaded with this library"
7
+ task :console do
8
+ sh "bundle exec irb -rubygems -I lib -r tassadar.rb"
9
+ end
10
+
11
+ desc "Automatically run specs when files change."
12
+ task :"spec:watchr" do
13
+ sh "watchr spec/spec_watchr.rb"
14
+ end
15
+
16
+ RSpec::Core::RakeTask.new(:spec) do |spec|
17
+ spec.pattern = 'spec/**/*_spec.rb'
18
+ spec.rspec_opts = ['--backtrace']
19
+ # spec.ruby_opts = ['-w']
20
+ end
21
+
22
+ task :default => :spec
@@ -0,0 +1,7 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
+
3
+ require 'tassadar/mpq'
4
+ require 'tassadar/sc2/replay'
5
+
6
+ module Tassadar
7
+ end
@@ -0,0 +1,49 @@
1
+ require 'bindata'
2
+
3
+ require 'tassadar/mpq/archive_size'
4
+ require 'tassadar/mpq/archive_header'
5
+ require 'tassadar/mpq/sector'
6
+ require 'tassadar/mpq/file_data'
7
+ require 'tassadar/mpq/block_table'
8
+ require 'tassadar/mpq/hash_table'
9
+ require 'tassadar/mpq/block_encryptor'
10
+
11
+ module Tassadar
12
+ module MPQ
13
+ class MPQ < BinData::Record
14
+ endian :little
15
+
16
+ string :magic, :length => 3, :check_value => "MPQ"
17
+ int8 :magic_4, :check_value => 27
18
+ int32 :user_data_size
19
+ int32 :archive_header_offset
20
+ string :user_data, :read_length => :user_data_size
21
+
22
+ archive_header :archive_header, :adjust_offset => lambda { archive_header_offset }
23
+ encrypted_block_table :block_table, :entries => lambda { archive_header.block_table_entries },
24
+ :adjust_offset => lambda { archive_header_offset + archive_header.block_table_offset }
25
+ encrypted_hash_table :hash_table, :entries => lambda { archive_header.hash_table_entries },
26
+ :adjust_offset => lambda { archive_header_offset + archive_header.hash_table_offset },
27
+ :compressed => lambda { archive_header.block_table_offset != archive_header.hash_table_offset + archive_header.hash_table_entries * 16 }
28
+ file_data_array :file_data, :blocks => lambda { block_table.blocks },
29
+ :sector_size_shift => lambda { archive_header.sector_size_shift },
30
+ :archive_header_offset => :archive_header_offset
31
+
32
+ def files
33
+ @files ||= read_file('(listfile)').split
34
+ end
35
+
36
+ def read_file(filename)
37
+ get_hash_table_entry(filename).decompressed_data
38
+ end
39
+
40
+ def get_hash_table_entry(filename)
41
+ hash_a = BlockEncryptor.hash_string(filename, 0x100)
42
+ hash_b = BlockEncryptor.hash_string(filename, 0x200)
43
+
44
+ block = self.block_table.blocks[self.hash_table.hashes.select {|hash| hash.file_path_hash_a == hash_a && hash.file_path_hash_b == hash_b}.first.file_block_index]
45
+ self.file_data.select {|f| f.block_offset == block.block_offset}.first
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,31 @@
1
+ module Tassadar
2
+ module MPQ
3
+ class ArchiveHeader < BinData::Record
4
+ endian :little
5
+
6
+ string :magic, :length => 3, :check_value => "MPQ"
7
+ uint8 :magic_4, :check_value => 26
8
+
9
+ uint32 :header_size, :check_value => 44
10
+
11
+ # archive_size actually here, but not used and is computed from later data
12
+ skip :length => 4
13
+
14
+ uint16 :format_version
15
+ uint8 :sector_size_shift, :check_value => 3
16
+ skip :length => 1
17
+ uint32 :hash_table_offset
18
+ uint32 :block_table_offset
19
+ uint32 :hash_table_entries
20
+ uint32 :block_table_entries
21
+ uint64 :extended_block_table_offset
22
+ uint16 :hash_table_offset_high
23
+ uint16 :block_table_offset_high
24
+
25
+ archive_size :archive_size, :hash_table_offset => :hash_table_offset,
26
+ :hash_table_entries => :hash_table_entries,
27
+ :block_table_offset => :block_table_offset,
28
+ :block_table_entries => :block_table_entries
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module Tassadar
2
+ module MPQ
3
+ class ArchiveSize < BinData::Primitive
4
+ endian :little
5
+
6
+ def get
7
+ [eval_parameter(:hash_table_offset) + (eval_parameter(:hash_table_entries) * 16),
8
+ eval_parameter(:block_table_offset) + (eval_parameter(:block_table_entries) * 16)].max
9
+ end
10
+
11
+ def set(v)
12
+ [eval_parameter(:hash_table_offset) + (eval_parameter(:hash_table_entries) * 16),
13
+ eval_parameter(:block_table_offset) + (eval_parameter(:block_table_entries) * 16)].max
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,105 @@
1
+ module Tassadar
2
+ module MPQ
3
+ class BlockEncryptor
4
+ attr_accessor :key, :offset, :buffer, :size
5
+
6
+ def initialize(key, offset, buffer, size)
7
+ @key = key
8
+ @offset = offset
9
+ @buffer = buffer
10
+ @size = size
11
+ end
12
+
13
+ def self.hash_string(key, offset)
14
+ BlockEncryptor.new(key, offset, nil, nil).hash_string
15
+ end
16
+
17
+ def decrypt
18
+ seed1 = hash_string
19
+ seed2 = 0xEEEEEEEE
20
+ result = ""
21
+ size = @size
22
+
23
+ while size >= 4
24
+ seed2 += encryption_table[0x400 + (seed1 & 0xFF)]
25
+ seed2 &= 0xFFFFFFFF
26
+
27
+ value = buffer.readbytes(4).unpack("V").first
28
+ value = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
29
+
30
+ result += [value].pack("V")
31
+
32
+ seed1 = ((~seed1 << 0x15) + 0x11111111) | (seed1 >> 0x0B)
33
+ seed1 &= 0xFFFFFFFF
34
+ seed2 = value + seed2 + (seed2 << 5) + 3 & 0xFFFFFFFF
35
+ size = size - 4
36
+ end
37
+
38
+ result
39
+ end
40
+
41
+ def encrypt
42
+ seed1 = hash_string
43
+ seed2 = 0xEEEEEEEE
44
+ encrypted_block = []
45
+
46
+ while @size >= 4
47
+ seed2 += encryption_table[0x400 + (seed1 & 0xFF)]
48
+ seed2 &= 0xFFFFFFFF
49
+
50
+ value = buffer.readbytes(4).unpack("V").first
51
+ value = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
52
+
53
+ encrypted_block << value
54
+
55
+ seed = ((~seed1 << 0x15) + 0x11111111) | (seed1 >> 0x0B)
56
+ seed2 = value + seed2 + (seed2 << 5) + 3 & 0xFFFFFFFF
57
+
58
+ @size = @size - 4
59
+ end
60
+
61
+ encrypted_block.pack("V*")
62
+ end
63
+
64
+ def hash_string
65
+ seed1 = 0x7FED7FED
66
+ seed2 = 0xEEEEEEEE
67
+
68
+ @key.upcase.each_byte do |char|
69
+ value = encryption_table[@offset + char]
70
+ seed1 = (value ^ (seed1 + seed2)) & 0xFFFFFFFF
71
+ seed2 = char + seed1 + seed2 + (seed2 << 5) + 3 & 0xFFFFFFFF
72
+ end
73
+
74
+ seed1
75
+ end
76
+
77
+ private
78
+ def encryption_table
79
+ @encryption_table ||= begin
80
+ crypt_buf = []
81
+ seed = 0x00100001
82
+
83
+ 0.upto(0x100 - 1) do |index1|
84
+ index2 = index1
85
+
86
+ 0.upto(4) do |i|
87
+ seed = (seed * 125 + 3) % 0x2AAAAB
88
+ temp1 = (seed & 0xFFFF) << 0x10
89
+
90
+ seed = (seed * 125 + 3) % 0x2AAAAB
91
+ temp2 = (seed & 0xFFFF)
92
+
93
+ crypt_buf[index2] = (temp1 | temp2)
94
+
95
+ index2 = index2 + 0x100
96
+ end
97
+ end
98
+
99
+ crypt_buf
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,36 @@
1
+ module Tassadar
2
+ module MPQ
3
+ class BlockTable < BinData::Record
4
+ endian :little
5
+
6
+ array :blocks, :read_until => :eof do
7
+ uint32 :block_offset
8
+ uint32 :block_size
9
+ uint32 :file_size
10
+ uint32 :flags
11
+ end
12
+ end
13
+
14
+ class EncryptedBlockTable < BinData::BasePrimitive
15
+ mandatory_parameters :entries
16
+
17
+ def read_and_return_value(io)
18
+ value = BlockEncryptor.new("(block table)", 0x300, io, eval_parameter(:entries) * 16).decrypt
19
+ BlockTable.read(value)
20
+ end
21
+
22
+ def value_to_binary_string(value)
23
+ packed_string = ""
24
+ value.blocks.each do |block|
25
+ packed_string += [block.block_offset, block.block_size, block.file_size, block.flags].pack("VVVV")
26
+ end
27
+
28
+ BlockEncryptor.new("(block table)", 0x300, BinData::IO.new(packed_string), eval_parameter(:entries) * 16).encrypt
29
+ end
30
+
31
+ def sensible_default
32
+ [0,0,0,0].pack("VVVV")
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ module Tassadar
2
+ module MPQ
3
+ module CryptBuf
4
+ def self.[](index)
5
+ crypt_buf[index]
6
+ end
7
+
8
+ def self.crypt_buf
9
+ @@crypt_buff ||= crypt_buf!
10
+ end
11
+
12
+ def self.crypt_buf!
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,84 @@
1
+ require 'zlib'
2
+ require 'bzip2'
3
+
4
+ module Tassadar
5
+ module MPQ
6
+ class FileData < BinData::Record
7
+ MPQ_FILE_ENCRYPTED = 0x00010000
8
+ MPQ_FILE_EXISTS = 0x80000000
9
+ MPQ_SINGLE_UNIT = 0x01000000
10
+ MPQ_COMPRESSED = 0x00000200
11
+
12
+ attr_accessor :block_offset
13
+
14
+ endian :little
15
+
16
+ string :data, :read_length => lambda { block.block_size }
17
+
18
+ def decompressed_data
19
+ result = nil
20
+ block = eval_parameter(:block)
21
+
22
+ if (block.flags & MPQ_FILE_EXISTS) > 0
23
+ result = self.data
24
+ end
25
+
26
+ if (block.flags & MPQ_FILE_ENCRYPTED) > 0
27
+ raise NotImplementedError
28
+ end
29
+
30
+ if (block.flags & MPQ_SINGLE_UNIT) > 0
31
+ if block.flags & MPQ_COMPRESSED && block.file_size > block.block_size
32
+ result = decompress(self.data)
33
+ end
34
+ else
35
+ end
36
+
37
+ result
38
+ end
39
+
40
+ private
41
+ def decompress(data)
42
+ compression_type = data.bytes.first
43
+ case compression_type
44
+ when 0
45
+ data
46
+ when 2
47
+ Zlib::Deflate.deflate(data[1,data.size - 1])
48
+ when 16
49
+ Bzip2.uncompress(data[1,data.size - 1])
50
+ else
51
+ raise NotImplementedError
52
+ end
53
+ end
54
+ end
55
+
56
+ class FileDataArray < BinData::BasePrimitive
57
+ mandatory_parameters :blocks, :sector_size_shift
58
+
59
+ def read_and_return_value(io)
60
+ result = []
61
+ sector_size = 512 * (2 ** eval_parameter(:sector_size_shift))
62
+
63
+ eval_parameter(:blocks).each do |block|
64
+ num_sectors = block.flags & 0x01000000 ? 1 : (block["block_size"] / sector_size)
65
+ file = FileData.new(:adjust_offset => block.block_offset + eval_parameter(:archive_header_offset),
66
+ :block => block).read(io)
67
+ file.block_offset = block.block_offset
68
+
69
+ result << file
70
+ end
71
+
72
+ result
73
+ end
74
+
75
+ def value_to_binary_string(value)
76
+ value.pack("V*")
77
+ end
78
+
79
+ def sensible_default
80
+ ''
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,52 @@
1
+ module Tassadar
2
+ module MPQ
3
+ class FileTime < BinData::Record
4
+ endian :little
5
+
6
+ uint32 :low_date_time
7
+ uint32 :high_date_time
8
+ end
9
+
10
+ class Md5 < BinData::Record
11
+ endian :little
12
+
13
+ string :md5_hash, :length => 16
14
+ end
15
+
16
+ class HashTable < BinData::Record
17
+ endian :little
18
+
19
+ array :hashes, :read_until => :eof do
20
+ uint32 :file_path_hash_a
21
+ uint32 :file_path_hash_b
22
+ uint16 :language
23
+ uint8 :platform
24
+ skip :length => 1
25
+ uint32 :file_block_index
26
+ end
27
+ end
28
+
29
+ class EncryptedHashTable < BinData::BasePrimitive
30
+ mandatory_parameters :entries
31
+
32
+ def read_and_return_value(io)
33
+ value = BlockEncryptor.new("(hash table)", 0x300, io, eval_parameter(:entries) * 16).decrypt
34
+ HashTable.read(value)
35
+ end
36
+
37
+ def value_to_binary_string(value)
38
+ packed_string = ""
39
+ value.hashes.each do |hash|
40
+ packed_string += [hash.file_path_hash_a, hash.file_path_hash_b, hash.language, hash.platform, 0, hash.file_block_index].pack("VVvCCV")
41
+ end
42
+
43
+ BlockEncryptor.new("(hash table)", 0x300, value, eval_parameter(:entries) * 16).encrypt
44
+ end
45
+
46
+ def sensible_default
47
+ [0,0,0,0,0,0].pack("VVvCCV")
48
+ end
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,25 @@
1
+ module Tassadar
2
+ module MPQ
3
+ class Sector < BinData::Record
4
+ endian :little
5
+
6
+ uint8 :compression_mask
7
+ end
8
+
9
+ class SectorArray < BinData::BasePrimitive
10
+ mandatory_parameters :sector_offsets, :sector_size
11
+
12
+ def read_and_return_value(io)
13
+ result = []
14
+ file_data_offset = parent.offset
15
+
16
+ eval_parameter(:sector_offsets).each do |offset|
17
+ raise offset.inspect
18
+ result << Sector.new(:adjust_offset => offset + file_data_offset).read(io)
19
+ end
20
+
21
+ result
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # https://github.com/GraylinKim/sc2reader/wiki/replay.attributes.events
2
+
3
+ module Tassadar
4
+ module SC2
5
+ class Attribute < BinData::Record
6
+ endian :little
7
+
8
+ string :header, :read_length => 4, :check_value => "\xE7\x03\x00\x00"
9
+ uint32 :id
10
+ uint8 :player_number
11
+ reverse_string :attribute_value, :read_length => 4
12
+ end
13
+
14
+ class Attributes < BinData::Record
15
+ endian :little
16
+
17
+ skip :length => 5
18
+ uint32 :num_attributes
19
+
20
+ array :attributes, :type => :attribute, :initial_length => :num_attributes
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ module Tassadar
2
+ module SC2
3
+ class Details < BinData::Record
4
+ serialized_data :data
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ module Tassadar
2
+ module SC2
3
+ class Game
4
+ attr_accessor :map, :time, :winner, :speed, :type, :category
5
+
6
+ def initialize(replay)
7
+ @winner = replay.players.select {|p| p.won}.first
8
+ @time = convert_windows_to_ruby_date_time(replay.details.data[5], replay.details.data[6])
9
+ @map = replay.details.data[1]
10
+ @type = replay.attributes.attributes.select {|a| a.id == 2001}.first.attribute_value
11
+
12
+ speeds = {"Fasr" => "Faster", "Slow" => "Slow", "Fast" => "Fast", "Norm" => "Normal", "Slor" => "Slower"}
13
+ @speed = speeds[replay.attributes.attributes.select {|a| a.id == 3000}.first.attribute_value]
14
+
15
+ categories = {"Amm" => "Ladder", "Priv" => "Private", "Pub" => "Public"}
16
+ @category = categories[replay.attributes.attributes.select {|a| a.id == 3009}.first.attribute_value]
17
+ end
18
+
19
+ private
20
+ def convert_windows_to_ruby_date_time(time, zone)
21
+ unix_time = (time - 116444735995904000) / (10 ** 7)
22
+
23
+ @time = Time.at(unix_time)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ module Tassadar
2
+ module SC2
3
+ class Player
4
+ attr_accessor :name, :id, :won, :color, :chosen_race, :actual_race, :handicap
5
+
6
+ def initialize(details_hash, attributes)
7
+ @name = details_hash[0]
8
+ @id = details_hash[1][4]
9
+ @won = [false, true, false][details_hash[8]]
10
+ @color = {:alpha => details_hash[3][0], :red => details_hash[3][1], :green => details_hash[3][2], :blue => details_hash[3][3]}
11
+ races = {"Terr" => "Terran", "Prot" => "Protoss", "Zerg" => "Zerg", "RAND" => "Random"}
12
+ @chosen_race = races[attributes.select {|a| a.id == 0x0BB9 && a.player_number == details_hash[7] + 1}.first.attribute_value]
13
+ @actual_race = details_hash[2]
14
+ @handicap = details_hash[6]
15
+ end
16
+
17
+ def winner?
18
+ @won
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ require 'tassadar/sc2/reverse_string'
2
+ require 'tassadar/sc2/serialized_data'
3
+ require 'tassadar/sc2/attributes'
4
+ require 'tassadar/sc2/details'
5
+ require 'tassadar/sc2/player'
6
+ require 'tassadar/sc2/game'
7
+
8
+ module Tassadar
9
+ module SC2
10
+ class Replay
11
+ attr_accessor :mpq, :attributes, :details, :players, :game
12
+
13
+ def initialize(filename)
14
+ @mpq = MPQ::MPQ.read(File.read(filename))
15
+ @attributes = Attributes.read(@mpq.read_file("replay.attributes.events"))
16
+ @details = Details.read(@mpq.read_file("replay.details"))
17
+
18
+ @players = @details.data[0].map {|h| Player.new(h, @attributes.attributes)}
19
+ @game = Game.new(self)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module Tassadar
2
+ module SC2
3
+ class ReverseString < BinData::String
4
+ mandatory_parameters :read_length
5
+
6
+ def read_and_return_value(io)
7
+ super.reverse.gsub("\x00", '')
8
+ end
9
+
10
+ def value_to_binary_string(value)
11
+ clamp_to_length(val.reverse)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,86 @@
1
+ module Tassadar
2
+ module SC2
3
+ class SerializedData < BinData::BasePrimitive
4
+ def read_and_return_value(io)
5
+ key = io.readbytes(1).unpack("C").first
6
+
7
+ case key
8
+ when 2
9
+ read_byte_string(io)
10
+ when 4
11
+ read_array(io)
12
+ when 5
13
+ read_kvo(io)
14
+ when 6
15
+ read_small_int(io)
16
+ when 7
17
+ read_big_int(io)
18
+ when 9
19
+ read_vlf_int(io)
20
+ else
21
+ puts "No parser for key: #{key}"
22
+ end
23
+ end
24
+
25
+ def value_to_binary_string(value)
26
+ value.pack("V")
27
+ end
28
+
29
+ private
30
+ def read_byte_string(io)
31
+ num_bytes = io.readbytes(1).unpack("C").first >> 1
32
+
33
+ io.readbytes(num_bytes).unpack("A#{num_bytes}").first.force_encoding('UTF-8')
34
+ end
35
+
36
+ def read_small_int(io)
37
+ io.readbytes(1).unpack("C").first
38
+ end
39
+
40
+ def read_big_int(io)
41
+ io.readbytes(4).unpack("v").first
42
+ end
43
+
44
+ def read_vlf_int(io)
45
+ byte = io.readbytes(1).unpack("C").first
46
+ value = (byte & 0x7F)
47
+ shift = 1
48
+
49
+ while byte & 0x80 > 0
50
+ byte = io.readbytes(1).unpack("C").first
51
+ value += (byte & 0x7F) << (7 * shift)
52
+ shift += 1
53
+ end
54
+
55
+ (value & 1) == 1 ? -(value >> 1) : (value >> 1)
56
+ end
57
+
58
+ def read_array(io)
59
+ result = []
60
+ 2.times { io.readbytes(1).unpack("C").first }
61
+
62
+ num_elements = io.readbytes(1).unpack("C").first >> 1
63
+
64
+ num_elements.times do
65
+ result << read_and_return_value(io)
66
+ end
67
+
68
+ result
69
+ end
70
+
71
+ def read_kvo(io)
72
+ result = {}
73
+ num_pairs = io.readbytes(1).unpack("C").first >> 1
74
+
75
+ num_pairs.times do
76
+ key = io.readbytes(1).unpack("C").first >> 1
77
+ value = read_and_return_value(io)
78
+
79
+ result[key] = value
80
+ end
81
+
82
+ result
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ module Tassadar
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tassadar::MPQ::ArchiveHeader do
4
+ before(:each) do
5
+ @archive_header = Tassadar::MPQ::ArchiveHeader.read("MPQ\x1A,\x00\x00\x00P5\x00\x00\x01\x00\x03\x00\xB03\x00\x00\xB04\x00\x00\x10\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
6
+ end
7
+
8
+ describe "require a valid magic string" do
9
+ it "should accept a valid magic string" do
10
+ expect { Tassadar::MPQ::ArchiveHeader.read("MPQ\x1A,\x00\x00\x00P5\x00\x00\x01\x00\x03\x00\xB03\x00\x00\xB04\x00\x00\x10\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") }.to_not raise_error
11
+ end
12
+
13
+ it "should not accept an invalid magic string" do
14
+ expect { Tassadar::MPQ.ArchiveHeader.read("MPQ\x1A,\x00\x00\x00P5\x00\x00\x01\x00\x03\x00\xB03\x00\x00\xB04\x00\x00\x10\x00\x00\x00\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") }.to raise_error
15
+ end
16
+ end
17
+
18
+ it "should read the magic header" do
19
+ @archive_header.magic.should == "MPQ"
20
+ end
21
+
22
+ it "should read magic 4" do
23
+ @archive_header.magic_4.should == 26
24
+ end
25
+
26
+ it "should read the header size" do
27
+ @archive_header.header_size.should == 44
28
+ end
29
+
30
+ it "should read the archive size" do
31
+ @archive_header.archive_size.should == 13648
32
+ end
33
+
34
+ it "should read the format version" do
35
+ @archive_header.format_version.should == 1
36
+ end
37
+
38
+ it "should read the sector size shift" do
39
+ @archive_header.sector_size_shift.should == 3
40
+ end
41
+
42
+ it "should read the hash table offset" do
43
+ @archive_header.hash_table_offset.should == 13232
44
+ end
45
+
46
+ it "should read the block table offset" do
47
+ @archive_header.block_table_offset.should == 13488
48
+ end
49
+
50
+ it "should read the hash table entries" do
51
+ @archive_header.hash_table_entries.should > 0
52
+ @archive_header.hash_table_entries.should < 2 ** 20
53
+ Math.log2(@archive_header.hash_table_entries.to_i).floor.should == Math.log2(@archive_header.hash_table_entries.to_i).ceil
54
+ @archive_header.hash_table_entries.should == 16
55
+ end
56
+
57
+ it "should read the block table entries" do
58
+ @archive_header.block_table_entries.should == 10
59
+ end
60
+
61
+ it "should read the extended_block_table_offset" do
62
+ @archive_header.extended_block_table_offset.should == 0
63
+ end
64
+
65
+ it "should read the hash table offset high" do
66
+ @archive_header.hash_table_offset_high.should == 0
67
+ end
68
+
69
+ it "should read the block table offset high" do
70
+ @archive_header.block_table_offset_high.should == 0
71
+ end
72
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tassadar::MPQ::ArchiveHeader do
4
+ before(:each) do
5
+ @mpq = Tassadar::MPQ::MPQ.read(File.read("spec/replays/Delta\ Quadrant.SC2Replay"))
6
+ end
7
+
8
+ it "should have some blocks" do
9
+ @mpq.block_table.blocks.size.should == 10
10
+ end
11
+
12
+ it "should have a valid block table entry" do
13
+ block = @mpq.block_table.blocks.first
14
+ block.block_offset.should == 0x0000002C
15
+ block.block_size.should == 448
16
+ block.file_size.should == 448
17
+ block.flags.should == 0x81000200
18
+ end
19
+
20
+ it "should have another valid block table entry" do
21
+ block = @mpq.block_table.blocks[1]
22
+ block.block_offset.should == 0x000001EC
23
+ block.block_size.should == 652
24
+ block.file_size.should == 1216
25
+ block.flags.should == 0x81000200
26
+ end
27
+ end
28
+
29
+ # MPQ archive block table
30
+ # -----------------------------------
31
+ # Offset ArchSize RealSize Flags
32
+ # 0000002C 448 448 81000200
33
+ # 000001EC 652 1216 81000200
34
+ # 00000478 9984 19453 81000200
35
+ # 00002B78 113 149 81000200
36
+ # 00002BE9 96 96 81000200
37
+ # 00002C49 578 760 81000200
38
+ # 00002E8B 682 1112 81000200
39
+ # 00003135 254 581 81000200
40
+ # 00003233 120 164 81000200
41
+ # 000032AB 261 288 81000200
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tassadar::MPQ::MPQ do
4
+ before(:each) do
5
+ @mpq = Tassadar::MPQ::MPQ.read(File.read("spec/replays/Delta\ Quadrant.SC2Replay"))
6
+ end
7
+
8
+ it "should have a valid magic string" do
9
+ @mpq.magic.should == "MPQ"
10
+ end
11
+
12
+ it "should read the user data size" do
13
+ @mpq.user_data_size.should == 512
14
+ end
15
+
16
+ it "should have block_table entries" do
17
+ @mpq.block_table.blocks.size.should == 10
18
+ end
19
+
20
+ it "should have hash_table entries" do
21
+ @mpq.hash_table.hashes.size.should == 16
22
+ end
23
+
24
+ it "should have files" do
25
+ @mpq.file_data.size.should > 1
26
+ end
27
+
28
+ it "should have a list of files" do
29
+ @mpq.files.size.should == 8
30
+ @mpq.files.should include("replay.attributes.events")
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tassadar::SC2::Game do
4
+ before(:each) do
5
+ @replay = Tassadar::SC2::Replay.new("spec/replays/Delta\ Quadrant.SC2Replay")
6
+ end
7
+
8
+ it "should set the winner" do
9
+ @replay.game.winner.name.should == "redgar"
10
+ end
11
+
12
+ it "should set the map" do
13
+ @replay.game.map.should == "Delta Quadrant"
14
+ end
15
+
16
+ it "should set the time" do
17
+ @replay.game.time.should == Time.new(2011, 07, 05, 17, 01, 8, "-05:00")
18
+ end
19
+
20
+ it "should set the speed" do
21
+ @replay.game.speed.should == "Faster"
22
+ end
23
+
24
+ it "should set the game type" do
25
+ @replay.game.type.should == "1v1"
26
+ end
27
+
28
+ it "should set the category" do
29
+ @replay.game.category.should == "Ladder"
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+
4
+ describe Tassadar::SC2::Player do
5
+ context 'NA Sc2 Replay' do
6
+ before(:each) do
7
+ @replay = Tassadar::SC2::Replay.new("spec/replays/Delta\ Quadrant.SC2Replay")
8
+ @player = @replay.players.first
9
+ end
10
+
11
+ it "should set the name" do
12
+ @player.name.should == "guitsaru"
13
+ end
14
+
15
+ it "should set the id" do
16
+ @player.id.should == 1918894
17
+ end
18
+
19
+ it "should tell if the player won" do
20
+ @player.should_not be_winner
21
+ end
22
+
23
+ it "should have a color" do
24
+ @player.color.should == {:alpha => 255, :red => 180, :green => 20, :blue => 30}
25
+ end
26
+
27
+ it "should have a chosen race" do
28
+ @player.chosen_race.should == "Terran"
29
+ end
30
+
31
+ it "should have random as the chosen race if random" do
32
+ replay = Tassadar::SC2::Replay.new("spec/replays/random.sc2replay")
33
+ replay.players.last.chosen_race.should == "Random"
34
+ end
35
+
36
+ it "should have an actual race" do
37
+ @player.actual_race.should == "Terran"
38
+ end
39
+
40
+ it "should have a handicap" do
41
+ @player.handicap.should == 100
42
+ end
43
+ end
44
+
45
+ context 'EU SC2 Replay' do
46
+ let(:replay) { Tassadar::SC2::Replay.new('spec/replays/eu_replay.SC2Replay') }
47
+ subject { replay.players.last }
48
+
49
+ it 'encodes the name in UTF-8' do
50
+ subject.name.encoding.to_s.should == 'UTF-8'
51
+ subject.name.should == 'MǂStephano'
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+
6
+ require 'tassadar'
7
+
8
+ RSpec.configure do |config|
9
+ config.mock_with :rr
10
+ end
11
+
@@ -0,0 +1,77 @@
1
+ ENV["WATCHR"] = "1"
2
+ system 'clear'
3
+
4
+ $spec_cmd = "bundle exec rspec --tty --color --format nested -d"
5
+
6
+ def run(cmd)
7
+ puts(cmd)
8
+ if system("which growlnotify > /dev/null")
9
+ run_with_notifier :growl, cmd
10
+ elsif system("which notify-send > /dev/null")
11
+ run_with_notifier :libnotify, cmd
12
+ else
13
+ `#{cmd}`
14
+ end
15
+ end
16
+
17
+ def run_with_notifier(notifier, cmd)
18
+ pass = system(cmd)
19
+ if pass
20
+ image = File.join(File.dirname(__FILE__), 'support', 'rails_ok.png')
21
+ message = "Success!"
22
+ else
23
+ image = File.join(File.dirname(__FILE__), 'support', 'rails_fail.png')
24
+ message = "Failure!"
25
+ end
26
+
27
+ case notifier
28
+ when :growl
29
+ `growlnotify -n "StarLeagues Specs" -m "StarLeagues Specs" --image #{image} #{message}`
30
+ when :libnotify
31
+ `notify-send --icon #{image} "StarLeagues Specs" #{message}`
32
+ end
33
+ end
34
+
35
+ def run_spec(file)
36
+ system('clear')
37
+ result = run "#{$spec_cmd} #{file}"
38
+ result.split("\n").last rescue nil
39
+ puts result
40
+ end
41
+
42
+ def run_all_specs
43
+ system('clear')
44
+ result = run "#{$spec_cmd} spec/"
45
+ puts result
46
+ end
47
+
48
+ def related_specs(path)
49
+ Dir['spec/**/*.rb'].select { |file| file =~ /#{File.basename(path).split(".").first}_spec.rb/ }
50
+ end
51
+
52
+ watch('.*') { run_all_specs }
53
+
54
+ # Ctrl-\
55
+ Signal.trap 'QUIT' do
56
+ puts " --- Running all specs ---\n\n"
57
+ run_all_specs
58
+ end
59
+
60
+ @interrupted = false
61
+
62
+ # Ctrl-C
63
+ Signal.trap 'INT' do
64
+ if @interrupted then
65
+ @wants_to_quit = true
66
+ abort("\n")
67
+ else
68
+ puts "Interrupt a second time to quit"
69
+ @interrupted = true
70
+ Kernel.sleep 1.5
71
+ # raise Interrupt, nil # let the run loop catch it
72
+ run_all_specs
73
+ @interrupted = false
74
+ end
75
+ end
76
+
77
+ puts "Watching..."
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "tassadar/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "tassadar"
7
+ s.version = Tassadar::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Matt Pruitt", "Andrew Nordman"]
10
+ s.email = ["mpruitt@agoragames.com", "anordman@agoragames.com"]
11
+ s.homepage = "https://github.com/agoragames/tassadar"
12
+ s.summary = %q{Pure ruby MPQ and SC2 Replay parser}
13
+ s.description = %q{Pure ruby MPQ and SC2 Replay parser}
14
+
15
+ s.rubyforge_project = "tassadar"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency("bindata")
23
+ s.add_dependency("bzip2-ruby")
24
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tassadar
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matt Pruitt
9
+ - Andrew Nordman
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-04 00:00:00.000000000Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bindata
17
+ requirement: &70239924932500 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *70239924932500
26
+ - !ruby/object:Gem::Dependency
27
+ name: bzip2-ruby
28
+ requirement: &70239924854920 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *70239924854920
37
+ description: Pure ruby MPQ and SC2 Replay parser
38
+ email:
39
+ - mpruitt@agoragames.com
40
+ - anordman@agoragames.com
41
+ executables: []
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - .gitignore
46
+ - .rspec
47
+ - .rvmrc
48
+ - CHANGELOG.md
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - lib/tassadar.rb
54
+ - lib/tassadar/mpq.rb
55
+ - lib/tassadar/mpq/archive_header.rb
56
+ - lib/tassadar/mpq/archive_size.rb
57
+ - lib/tassadar/mpq/block_encryptor.rb
58
+ - lib/tassadar/mpq/block_table.rb
59
+ - lib/tassadar/mpq/crypt_buf.rb
60
+ - lib/tassadar/mpq/file_data.rb
61
+ - lib/tassadar/mpq/hash_table.rb
62
+ - lib/tassadar/mpq/sector.rb
63
+ - lib/tassadar/sc2/attributes.rb
64
+ - lib/tassadar/sc2/details.rb
65
+ - lib/tassadar/sc2/game.rb
66
+ - lib/tassadar/sc2/player.rb
67
+ - lib/tassadar/sc2/replay.rb
68
+ - lib/tassadar/sc2/reverse_string.rb
69
+ - lib/tassadar/sc2/serialized_data.rb
70
+ - lib/tassadar/version.rb
71
+ - spec/mpq/archive_header_spec.rb
72
+ - spec/mpq/block_table_spec.rb
73
+ - spec/mpq_spec.rb
74
+ - spec/replays/Delta Quadrant.SC2Replay
75
+ - spec/replays/eu_replay.SC2Replay
76
+ - spec/replays/random.sc2replay
77
+ - spec/sc2/game_spec.rb
78
+ - spec/sc2/player_spec.rb
79
+ - spec/spec_helper.rb
80
+ - spec/spec_watchr.rb
81
+ - tassadar.gemspec
82
+ homepage: https://github.com/agoragames/tassadar
83
+ licenses: []
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ segments:
95
+ - 0
96
+ hash: -1060130835485239362
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ segments:
104
+ - 0
105
+ hash: -1060130835485239362
106
+ requirements: []
107
+ rubyforge_project: tassadar
108
+ rubygems_version: 1.8.10
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: Pure ruby MPQ and SC2 Replay parser
112
+ test_files:
113
+ - spec/mpq/archive_header_spec.rb
114
+ - spec/mpq/block_table_spec.rb
115
+ - spec/mpq_spec.rb
116
+ - spec/replays/Delta Quadrant.SC2Replay
117
+ - spec/replays/eu_replay.SC2Replay
118
+ - spec/replays/random.sc2replay
119
+ - spec/sc2/game_spec.rb
120
+ - spec/sc2/player_spec.rb
121
+ - spec/spec_helper.rb
122
+ - spec/spec_watchr.rb