tassadar 0.0.2

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.
@@ -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