steam-condenser 1.1.0 → 1.2.0

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.
Files changed (62) hide show
  1. data/Gemfile.lock +30 -0
  2. data/LICENSE +1 -1
  3. data/README.md +4 -3
  4. data/lib/steam-condenser/version.rb +2 -2
  5. data/lib/steam/community/alien_swarm/alien_swarm_mission.rb +24 -22
  6. data/lib/steam/community/alien_swarm/alien_swarm_stats.rb +66 -65
  7. data/lib/steam/community/alien_swarm/alien_swarm_weapon.rb +6 -6
  8. data/lib/steam/community/app_news.rb +2 -2
  9. data/lib/steam/community/css/css_map.rb +4 -4
  10. data/lib/steam/community/css/css_stats.rb +43 -43
  11. data/lib/steam/community/css/css_weapon.rb +5 -5
  12. data/lib/steam/community/defense_grid/defense_grid_stats.rb +36 -35
  13. data/lib/steam/community/dods/dods_class.rb +14 -14
  14. data/lib/steam/community/dods/dods_stats.rb +5 -4
  15. data/lib/steam/community/dods/dods_weapon.rb +6 -6
  16. data/lib/steam/community/game_achievement.rb +38 -31
  17. data/lib/steam/community/game_inventory.rb +6 -6
  18. data/lib/steam/community/game_leaderboard.rb +34 -32
  19. data/lib/steam/community/game_leaderboard_entry.rb +6 -6
  20. data/lib/steam/community/game_stats.rb +39 -65
  21. data/lib/steam/community/game_weapon.rb +2 -2
  22. data/lib/steam/community/l4d/abstract_l4d_stats.rb +54 -49
  23. data/lib/steam/community/l4d/abstract_l4d_weapon.rb +7 -6
  24. data/lib/steam/community/l4d/l4d2_map.rb +10 -10
  25. data/lib/steam/community/l4d/l4d2_stats.rb +33 -33
  26. data/lib/steam/community/l4d/l4d2_weapon.rb +8 -7
  27. data/lib/steam/community/l4d/l4d_explosive.rb +5 -4
  28. data/lib/steam/community/l4d/l4d_map.rb +8 -7
  29. data/lib/steam/community/l4d/l4d_stats.rb +7 -7
  30. data/lib/steam/community/l4d/l4d_weapon.rb +5 -4
  31. data/lib/steam/community/portal2/portal2_stats.rb +3 -3
  32. data/lib/steam/community/steam_game.rb +106 -16
  33. data/lib/steam/community/steam_group.rb +51 -40
  34. data/lib/steam/community/steam_id.rb +119 -87
  35. data/lib/steam/community/tf2/tf2_class.rb +14 -14
  36. data/lib/steam/community/tf2/tf2_class_factory.rb +2 -2
  37. data/lib/steam/community/tf2/tf2_engineer.rb +5 -7
  38. data/lib/steam/community/tf2/tf2_golden_wrench.rb +1 -1
  39. data/lib/steam/community/tf2/tf2_medic.rb +4 -6
  40. data/lib/steam/community/tf2/tf2_sniper.rb +3 -5
  41. data/lib/steam/community/tf2/tf2_spy.rb +10 -6
  42. data/lib/steam/community/tf2/tf2_stats.rb +15 -7
  43. data/lib/steam/community/web_api.rb +15 -1
  44. data/lib/steam/community/xml_data.rb +17 -0
  45. data/lib/steam/servers/game_server.rb +4 -4
  46. data/lib/steam/servers/master_server.rb +2 -2
  47. data/lib/steam/servers/source_server.rb +0 -2
  48. data/lib/steam/sockets/goldsrc_socket.rb +2 -2
  49. data/lib/steam/steam_player.rb +2 -2
  50. data/steam-condenser.gemspec +3 -2
  51. data/test/helper.rb +10 -2
  52. data/test/steam/communtiy/test_steam_group.rb +4 -4
  53. data/test/steam/communtiy/test_steam_id.rb +28 -2
  54. data/test/steam/communtiy/test_web_api.rb +2 -2
  55. data/test/steam/packets/test_steam_packet.rb +37 -0
  56. data/test/steam/servers/test_game_server.rb +296 -308
  57. data/test/steam/servers/test_goldsrc_server.rb +59 -59
  58. data/test/steam/servers/test_master_server.rb +131 -131
  59. data/test/steam/servers/test_server.rb +72 -72
  60. data/test/steam/servers/test_source_server.rb +126 -140
  61. data/test/steam/sockets/test_master_server_socket.rb +1 -0
  62. metadata +39 -19
@@ -25,13 +25,14 @@ class L4D2Weapon
25
25
 
26
26
  # Creates a new instance of a weapon based on the given XML data
27
27
  #
28
- # @param [REXML::Element] weapon_data The XML data of this weapon
29
- def initialize(weapon_data)
30
- super weapon_data
31
-
32
- @damage = weapon_data.elements['damage'].text.to_i
33
- @kill_percentage = weapon_data.elements['pctkills'].text.to_f * 0.01
34
- @weapon_group = weapon_data.attribute('group')
28
+ # @param [String] weapon_name The name of this weapon
29
+ # @param [Hash<String, Object>] weapon_data The XML data of this weapon
30
+ def initialize(weapon_name, weapon_data)
31
+ super
32
+
33
+ @damage = weapon_data['damage'].to_i
34
+ @kill_percentage = weapon_data['pctkills'].to_f * 0.01
35
+ @weapon_group = weapon_data['group']
35
36
  end
36
37
 
37
38
  end
@@ -15,12 +15,13 @@ class L4DExplosive
15
15
 
16
16
  # Creates a new instance of an explosivve based on the given XML data
17
17
  #
18
- # @param [REXML::Element] weapon_data The XML data of this explosive
19
- def initialize(weapon_data)
18
+ # @param [String] weapon_name The name of this weapon
19
+ # @param [Hash<String, Object>] weapon_data The XML data of this weapon
20
+ def initialize(weapon_name, weapon_data)
20
21
  super weapon_data
21
22
 
22
- @id = weapon_data.name
23
- @shots = weapon_data.elements['thrown'].text.to_i
23
+ @id = weapon_name
24
+ @shots = weapon_data['thrown'].to_i
24
25
  end
25
26
 
26
27
  # Returns the average number of killed zombies for one shot of this explosive
@@ -42,14 +42,15 @@ class L4DMap
42
42
  # Creates a new instance of a Left4Dead Survival map based on the given
43
43
  # XML data
44
44
  #
45
- # @param [REXML::Element] map_data The XML data for this map
46
- def initialize(map_data)
47
- @best_time = map_data.elements['besttimeseconds'].text.to_f
48
- @id = map_data.name
49
- @name = map_data.elements['name'].text
50
- @times_played = map_data.elements['timesplayed'].text.to_i
45
+ # @param [String] map_name The name of this map
46
+ # @param [Hash<String, Object>] map_data The XML data for this map
47
+ def initialize(map_name, map_data)
48
+ @best_time = map_data['besttimeseconds'].to_f
49
+ @id = map_name
50
+ @name = map_data['name']
51
+ @times_played = map_data['timesplayed'].to_i
51
52
 
52
- case map_data.elements['medal'].text
53
+ case map_data['medal']
53
54
  when 'gold'
54
55
  @medal = L4DMap::GOLD
55
56
  when 'silver'
@@ -35,8 +35,8 @@ class L4DStats < GameStats
35
35
  if @survival_stats.nil?
36
36
  super
37
37
  @survival_stats[:maps] = {}
38
- @xml_data.elements.each('stats/survival/maps/*') do |map_data|
39
- @survival_stats[:maps][map_data.name] = L4DMap.new(map_data)
38
+ @xml_data['stats']['survival']['maps'].each do |map_data|
39
+ @survival_stats[:maps][map_data[0]] = L4DMap.new *map_data
40
40
  end
41
41
  end
42
42
 
@@ -54,14 +54,14 @@ class L4DStats < GameStats
54
54
 
55
55
  if @weapon_stats.nil?
56
56
  @weapon_stats = {}
57
- @xml_data.elements.each('stats/weapons/*') do |weapon_data|
58
- unless %w{molotov pipes}.include? weapon_data.name
59
- weapon = L4DWeapon.new(weapon_data)
57
+ @xml_data['stats']['weapons'].each do |weapon_data|
58
+ unless %w{molotov pipes}.include? weapon_data[0]
59
+ weapon = L4DWeapon.new *weapon_data
60
60
  else
61
- weapon = L4DExplosive.new(weapon_data)
61
+ weapon = L4DExplosive.new *weapon_data
62
62
  end
63
63
 
64
- @weapon_stats[weapon_data.name] = weapon
64
+ @weapon_stats[weapon_data[0]] = weapon
65
65
  end
66
66
  end
67
67
 
@@ -15,11 +15,12 @@ class L4DWeapon
15
15
 
16
16
  # Creates a new instance of a weapon based on the given XML data
17
17
  #
18
- # @param [REXML::Element] weapon_data The XML data for this weapon
19
- def initialize(weapon_data)
20
- super weapon_data
18
+ # @param [String] weapon_name The name of this weapon
19
+ # @param [Hash<String, Object>] weapon_data The XML data of this weapon
20
+ def initialize(weapon_name, weapon_data)
21
+ super
21
22
 
22
- @kill_percentage = weapon_data.elements['killpct'].text.to_f * 0.01
23
+ @kill_percentage = weapon_data['killpct'].to_f * 0.01
23
24
  end
24
25
 
25
26
  end
@@ -1,7 +1,7 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under
2
2
  # the terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2011, Sebastian Staudt
4
+ # Copyright (c) 2011-2012, Sebastian Staudt
5
5
 
6
6
  require 'steam/community/game_stats'
7
7
  require 'steam/community/portal2/portal2_inventory'
@@ -23,9 +23,9 @@ class Portal2Stats < GameStats
23
23
  # Returns the current Portal 2 inventory (a.k.a Robot Enrichment) of this
24
24
  # player
25
25
  #
26
- # @return [TF2Inventory] This player's Portal 2 inventory
26
+ # @return [Portal2Inventory] This player's Portal 2 inventory
27
27
  def inventory
28
- @inventory = Portal2Inventory.new(steam_id64) if @inventory.nil?
28
+ @inventory = Portal2Inventory.new(user.steam_id64) if @inventory.nil?
29
29
  @inventory
30
30
  end
31
31
 
@@ -1,23 +1,31 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under
2
2
  # the terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2011, Sebastian Staudt
4
+ # Copyright (c) 2011-2012, Sebastian Staudt
5
5
 
6
+ require 'steam/community/cacheable'
6
7
  require 'steam/community/game_leaderboard'
7
8
  require 'steam/community/game_stats'
9
+ require 'steam/community/web_api'
8
10
 
9
11
  # This class represents a game available on Steam
10
12
  #
11
13
  # @author Sebastian Staudt
12
14
  class SteamGame
13
15
 
14
- @@games = {}
16
+ include Cacheable
17
+ cacheable_with_ids :app_id
15
18
 
16
19
  # Returns the Steam application ID of this game
17
20
  #
18
21
  # @return [Fixnum] The Steam application ID of this game
19
22
  attr_reader :app_id
20
23
 
24
+ # Returns the URL for the icon image of this game
25
+ #
26
+ # @return [String] The URL for the game icon
27
+ attr_reader :icon_url
28
+
21
29
  # Returns the full name of this game
22
30
  #
23
31
  # @return [String] The full name of this game
@@ -28,14 +36,48 @@ class SteamGame
28
36
  # @return [String] The short name of this game
29
37
  attr_reader :short_name
30
38
 
31
- # Creates a new or cached instance of the game specified by the given XML
32
- # data
39
+ # Checks if a game is up-to-date by reading information from a `steam.inf`
40
+ # file and comparing it using the Web API
41
+ #
42
+ # @param [String] path The file system path of the `steam.inf` file
43
+ # @return [Boolean] `true` if the game is up-to-date
44
+ def self.check_steam_inf(path)
45
+ steam_inf = File.read path
46
+ begin
47
+ app_id = steam_inf.match(/^\s*appID=(\d+)\s*$/im)[1].to_i
48
+ version = steam_inf.match(/^\s*PatchVersion=([\d\.]+)\s*$/im)[1].gsub('.', '').to_i
49
+ rescue
50
+ raise SteamCondenserError, "The steam.inf file at \"#{path}\" is invalid."
51
+ end
52
+ uptodate? app_id, version
53
+ end
54
+
55
+ # Creates a new instance of a game with the given data and caches it
56
+ #
57
+ # @param [Fixnum] app_id The application ID of the game
58
+ # @param [Hash<String, Object>] game_data The XML data of the game
59
+ def self.new(app_id, game_data = nil)
60
+ if cached? app_id
61
+ class_variable_get(:@@cache)[app_id]
62
+ else
63
+ game = SteamGame.allocate
64
+ game.send :initialize, app_id, game_data
65
+ game
66
+ end
67
+ end
68
+
69
+ # Returns whether the given version of the game with the given application ID
70
+ # is up-to-date
33
71
  #
34
- # @param [REXML::Element] game_data The XML data of the game
35
- # @see #initialize
36
- def self.new(game_data)
37
- app_id = game_data.elements['appID'].text.to_i
38
- @@games.key?(app_id) ? @@games[app_id] : super(app_id, game_data)
72
+ # @param [Fixnum] app_id The application ID of the game to check
73
+ # @param [Fixnum] version The version to check against the Web API
74
+ # @return [Boolean] `true` if the given version is up-to-date
75
+ def self.uptodate?(app_id, version)
76
+ params = { :appid => app_id, :version => version }
77
+ result = WebApi.json 'ISteamApps', 'UpToDateCheck', 1, params
78
+ result = MultiJson.load(result, { :symbolize_keys => true})[:response]
79
+ raise SteamCondenserError, result[:error] unless result[:success]
80
+ result[:up_to_date]
39
81
  end
40
82
 
41
83
  # Returns whether this game has statistics available
@@ -45,6 +87,15 @@ class SteamGame
45
87
  !@short_name.nil?
46
88
  end
47
89
 
90
+ # Returns a unique identifier for this game
91
+ #
92
+ # This is either the numeric application ID or the unique short name
93
+ #
94
+ # @return [Fixnum, String] The application ID or short name of the game
95
+ def id
96
+ @short_name == @app_id.to_s ? @app_id : @short_name
97
+ end
98
+
48
99
  # Returns the leaderboard for this game and the given leaderboard ID or name
49
100
  #
50
101
  # @param [Fixnum, String] id The ID or name of the leaderboard to return
@@ -60,6 +111,35 @@ class SteamGame
60
111
  GameLeaderboard.leaderboards @short_name
61
112
  end
62
113
 
114
+ # Returns the URL for the logo image of this game
115
+ #
116
+ # @return [String] The URL for the game logo
117
+ def logo_url
118
+ "http://media.steampowered.com/steamcommunity/public/images/apps/#@app_id/#@logo_hash.jpg"
119
+ end
120
+
121
+ # Returns the URL for the logo thumbnail image of this game
122
+ #
123
+ # @return [String] The URL for the game logo thumbnail
124
+ def logo_thumbnail_url
125
+ "http://media.steampowered.com/steamcommunity/public/images/apps/#@app_id/#@logo_hash_thumb.jpg"
126
+ end
127
+
128
+ # Returns the URL of this game's page in the Steam Store
129
+ #
130
+ # @return [String] This game's store page
131
+ def store_url
132
+ "http://store.steampowered.com/app/#@app_id"
133
+ end
134
+
135
+ # Returns whether the given version of this game is up-to-date
136
+ #
137
+ # @param [Fixnum] version The version to check against the Web API
138
+ # @return [Boolean] `true` if the given version is up-to-date
139
+ def uptodate?(version)
140
+ self.class.uptodate? @app_id, version
141
+ end
142
+
63
143
  # Creates a stats object for the given user and this game
64
144
  #
65
145
  # @param [String, Fixnum] steam_id The custom URL or the 64bit Steam ID of
@@ -75,18 +155,28 @@ class SteamGame
75
155
 
76
156
  # Creates a new instance of a game with the given data and caches it
77
157
  #
158
+ # @note The real constructor of `SteamGame` is {.new}
78
159
  # @param [Fixnum] app_id The application ID of the game
79
- # @param [REXML::Element] game_data The XML data of the game
160
+ # @param [Hash<String, Object>] game_data The XML data of the game
80
161
  def initialize(app_id, game_data)
81
- @app_id = app_id
82
- @name = game_data.elements['name'].text
83
- if game_data.elements['globalStatsLink'].nil?
84
- @short_name = nil
162
+ @app_id = app_id
163
+
164
+ if game_data.key? 'name'
165
+ @logo_hash = game_data['logo'].match(/\/#{app_id}\/([0-9a-f]+).jpg/)[1]
166
+ @name = game_data['name']
167
+
168
+ if game_data.key? 'globalStatsLink'
169
+ @short_name = game_data['globalStatsLink'].match(/http:\/\/steamcommunity.com\/stats\/([^?\/]+)\/achievements\//)[1].downcase
170
+ end
85
171
  else
86
- @short_name = game_data.elements['globalStatsLink'].text.match(/http:\/\/steamcommunity.com\/stats\/([^?\/]+)\/achievements\//)[1].downcase
172
+ @icon_url = game_data['gameIcon']
173
+ @logo_hash = game_data['gameLogo'].match(/\/#{app_id}\/([0-9a-f]+).jpg/)[1]
174
+ @name = game_data['gameName']
175
+ @short_name = game_data['gameFriendlyName'].downcase
176
+ @short_name = @app_id if @short_name == @app_id.to_s
87
177
  end
88
178
 
89
- @@games[@app_id] = self
179
+ super()
90
180
  end
91
181
 
92
182
  end
@@ -1,14 +1,12 @@
1
1
  # This code is free software; you can redistribute it and/or modify it under
2
2
  # the terms of the new BSD License.
3
3
  #
4
- # Copyright (c) 2008-2011, Sebastian Staudt
5
-
6
- require 'open-uri'
7
- require 'rexml/document'
4
+ # Copyright (c) 2008-2012, Sebastian Staudt
8
5
 
9
6
  require 'errors/steam_condenser_error'
10
7
  require 'steam/community/cacheable'
11
8
  require 'steam/community/steam_id'
9
+ require 'steam/community/xml_data'
12
10
 
13
11
  # The SteamGroup class represents a group in the Steam Community
14
12
  #
@@ -18,6 +16,8 @@ class SteamGroup
18
16
  include Cacheable
19
17
  cacheable_with_ids :custom_url, :group_id64
20
18
 
19
+ include XMLData
20
+
21
21
  # Returns the custom URL of this group
22
22
  #
23
23
  # The custom URL is a admin specified unique string that can be used instead
@@ -38,17 +38,14 @@ class SteamGroup
38
38
  # @param [Boolean] fetch if `true` the groups's data is loaded into the
39
39
  # object
40
40
  def initialize(id, fetch = true)
41
- begin
42
- if id.is_a? Numeric
43
- @group_id64 = id
44
- else
45
- @custom_url = id.downcase
46
- end
47
-
48
- super(fetch)
49
- rescue REXML::ParseException
50
- raise SteamCondenserError, 'Group could not be loaded.'
41
+ if id.is_a? Numeric
42
+ @group_id64 = id
43
+ else
44
+ @custom_url = id.downcase
51
45
  end
46
+ @members = []
47
+
48
+ super fetch
52
49
  end
53
50
 
54
51
  # Returns the base URL for this group's page
@@ -58,9 +55,9 @@ class SteamGroup
58
55
  # @return [String] The base URL for this group
59
56
  def base_url
60
57
  if @custom_url.nil?
61
- "http://steamcommunity.com/gid/#{@group_id64}"
58
+ "http://steamcommunity.com/gid/#@group_id64"
62
59
  else
63
- "http://steamcommunity.com/groups/#{@custom_url}"
60
+ "http://steamcommunity.com/groups/#@custom_url"
64
61
  end
65
62
  end
66
63
 
@@ -71,24 +68,14 @@ class SteamGroup
71
68
  #
72
69
  # @see Cacheable#fetch
73
70
  def fetch
74
- @members = []
75
- page = 0
71
+ if @member_count.nil? || @member_count == @members.size
72
+ page = 0
73
+ else
74
+ page = 1
75
+ end
76
76
 
77
77
  begin
78
- page += 1
79
- url = open("#{base_url}/memberslistxml?p=#{page}", {:proxy => true})
80
- member_data = REXML::Document.new(url.read).root
81
-
82
- begin
83
- @group_id64 = member_data.elements['groupID64'].text.to_i if page == 1
84
- total_pages = member_data.elements['totalPages'].text.to_i
85
-
86
- member_data.elements['members'].elements.each do |member|
87
- @members << SteamId.new(member.text.to_i, false)
88
- end
89
- rescue
90
- raise SteamCondenserError, 'XML data could not be parsed.'
91
- end
78
+ total_pages = fetch_page(page += 1)
92
79
  end while page < total_pages
93
80
 
94
81
  super
@@ -97,17 +84,17 @@ class SteamGroup
97
84
  # Returns the number of members this group has
98
85
  #
99
86
  # If the members have already been fetched the size of the member array is
100
- # returned. Otherwise the group size is separately fetched without needing
101
- # multiple requests for big groups.
87
+ # returned. Otherwise the the first page of the member listing is fetched and
88
+ # the member count and the first batch of members is stored.
102
89
  #
103
90
  # @return [Fixnum] The number of this group's members
104
91
  def member_count
105
- if @members.nil?
106
- url = open("#{base_url}/memberslistxml", {:proxy => true})
107
- REXML::Document.new(url.read).root.elements['memberCount'].text.to_i
108
- else
109
- @members.size
92
+ if @member_count.nil?
93
+ total_pages = fetch_page(1)
94
+ @fetch_time = Time.now if total_pages == 1
110
95
  end
96
+
97
+ @member_count
111
98
  end
112
99
 
113
100
  # Returns the members of this group
@@ -117,8 +104,32 @@ class SteamGroup
117
104
  # @return [Array<SteamId>] The Steam ID's of the members of this group
118
105
  # @see #fetch
119
106
  def members
120
- fetch if @members.nil? || @members[0].nil?
107
+ fetch if @members.size != @member_count
121
108
  @members
122
109
  end
123
110
 
111
+ private
112
+
113
+ # Fetches a specific page of the member listing of this group
114
+ #
115
+ # @param [Fixnum] page The member page to fetch
116
+ # @return [Fixnum] The total number of pages of this group's member listing
117
+ def fetch_page(page)
118
+ member_data = parse "#{base_url}/memberslistxml?p=#{page}"
119
+
120
+ begin
121
+ @group_id64 = member_data['groupID64'].to_i if page == 1
122
+ @member_count = member_data['memberCount'].to_i
123
+ total_pages = member_data['totalPages'].to_i
124
+
125
+ member_data['members']['steamID64'].each do |member|
126
+ @members << SteamId.new(member.to_i, false)
127
+ end
128
+ rescue
129
+ raise SteamCondenserError, 'XML data could not be parsed.', $!.backtrace
130
+ end
131
+
132
+ total_pages
133
+ end
134
+
124
135
  end