openra 1.4.0 → 1.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b648bef069d1c0a709f510f889703f24d4a6d043
4
- data.tar.gz: efedb97d0a81eb63694cedc2f50672e82be71257
3
+ metadata.gz: f8e899e24d7c1a64648be5f39fb249439f7ff008
4
+ data.tar.gz: 6efadd30042d44cfd54bcaecf9594b09d5aed992
5
5
  SHA512:
6
- metadata.gz: 41a705ad37119d25f18d3c5387ffb25b0e36b80d8818e55d8e0c251fe051600de56ed14c21659d93599df2b61fe4483c97fe120f1a74c12701acbd0ca6713178
7
- data.tar.gz: bfe0e10cfdb0292540a3c6adf90d0873012fe5a843050845bf26348d9e2b8f60afbe2aa1a1d69f8b8be4b716cb58fac4088a944644bebf34a7ce2cefda20fca7
6
+ metadata.gz: b15a3518af884311eac8fda0ea5323f30af197c1e2b4e00fdf86ae2103094d666ec2ba133a8c96ad7ad6e0f3af1fd27b3008aa5f539dbce03d9c13d9da63cf79
7
+ data.tar.gz: 1631bcefa11178ba60eb37d4a71019e4e4383b3e33290c29f2ccde99aae410e2e51b98bf9acce0282172f15105bf1405d711d14ebea9a2f487f7b03ed0b8f7d9
@@ -1,10 +1,22 @@
1
1
  ## Unreleased
2
2
 
3
+ ### Added
4
+
5
+ * [detect-production-macros] A new command to detect production macros ([AMHOL](https://github.com/AMHOL))
6
+
7
+ ### Fixed
8
+
9
+ * [core] Fix error when sync info does not exactly precede the start game order ([AMHOL](https://github.com/AMHOL))
10
+
11
+ [Compare v1.4.0...HEAD](https://github.com/AMHOL/openra-ruby/compare/v1.4.0...HEAD)
12
+
13
+ ## v1.4.0
14
+
3
15
  ### Fixed
4
16
 
5
17
  * [replay-data] Output timestamps in ISO8601 (thanks to [dragunoff](https://github.com/dragunoff)) ([AMHOL](https://github.com/AMHOL))
6
18
 
7
- [Compare v1.3.0...HEAD](https://github.com/AMHOL/openra-ruby/compare/v1.3.0...HEAD)
19
+ [Compare v1.3.0...v1.4.0](https://github.com/AMHOL/openra-ruby/compare/v1.3.0...v1.4.0)
8
20
 
9
21
  ## v1.3.0
10
22
 
data/README.md CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  Unofficial gem for [OpenRA](https://openra.net); a free, open-source real-time strategy game engine for early Westwood games such as Command & Conquer: Red Alert written in C#
4
4
 
5
- Currently the only operation implemented by this gem is to parse and extract data from replay files, if this is not what you're looking for, please see the [Other tools](#other-tools) section, perhaps what you need has already been implemented elsewhere.
5
+ Currently this gem implements the following commands:
6
+
7
+ * replay-data - parse and extract data from replay files
8
+ * detect-production-macros - parse replay data and attempt to detect production macro usage by players
9
+
10
+ If this is not what you're looking for, please see the [Other tools](#other-tools) section, perhaps what you need has already been implemented elsewhere.
6
11
 
7
12
  ### Installation
8
13
  ```sh
@@ -19,7 +24,8 @@ gem update openra
19
24
  ### Example usage
20
25
 
21
26
  ```sh
22
- openra replay-data /payh/to/replay.orarep [--format json|pretty-json|yaml]
27
+ openra replay-data /path/to/replay.orarep [--format json|pretty-json|yaml]
28
+ openra detect-production-macros /path/to/replay.orarep [--format json|pretty-json|yaml]
23
29
  ```
24
30
 
25
31
  ### Other tools
@@ -4,7 +4,8 @@ require 'json'
4
4
  require 'hanami/cli'
5
5
  require 'openra/version'
6
6
  require 'openra/replays'
7
- require 'openra/cli/commands/replay_data/formatters'
7
+ require 'openra/cli/commands/formatters'
8
+ require 'openra/cli/commands/detect_production_macros'
8
9
  require 'openra/cli/commands/replay_data'
9
10
  require 'openra/cli/commands/version'
10
11
  require 'openra/cli/command_registry'
@@ -4,6 +4,7 @@ module Openra
4
4
  extend Hanami::CLI::Registry
5
5
 
6
6
  register 'replay-data', Commands::ReplayData
7
+ register 'detect-production-macros', Commands::DetectProductionMacros
7
8
  register 'version', Commands::Version
8
9
  end
9
10
  end
@@ -0,0 +1,130 @@
1
+ module Openra
2
+ class CLI
3
+ module Commands
4
+ class DetectProductionMacros < Hanami::CLI::Command
5
+ ARRAY_HASH = ->(hash, key) { hash[key] = [] }
6
+
7
+ desc 'Detect whether players are using production macros'
8
+
9
+ argument :replay, required: true, desc: 'Path of the replay file to read data from'
10
+ option :format, default: 'json', values: %w(json pretty-json yaml), desc: 'Output format'
11
+
12
+ def call(replay:, **options)
13
+ replay = Openra::Replays::Replay.new(replay)
14
+ commands = Hash.new(&ARRAY_HASH)
15
+
16
+ replay.orders.each do |order|
17
+ case order.command
18
+ when 'StartProduction'
19
+ commands[order.client_index.to_s] << {
20
+ target: order.target_string.to_s,
21
+ msec: order.frame * replay.frametime_multiplier
22
+ }
23
+ end
24
+ end
25
+
26
+ data = {
27
+ players: replay.players.select(&:is_human).map { |player|
28
+ player_commands = commands_with_delay(commands[player.index])
29
+ production_stats = production_stats(player_commands)
30
+ sequences = detect_sequences(player_commands)
31
+
32
+ {
33
+ index: player.index,
34
+ name: player.name,
35
+ team: player.team,
36
+ outcome: player.outcome,
37
+ production_stats: production_stats,
38
+ sequences: sequences,
39
+ suspected_macros: suspected_macros(production_stats, sequences)
40
+ }
41
+ }
42
+ }
43
+
44
+ puts FORMATTERS.fetch(options[:format]).call(data)
45
+ end
46
+
47
+ private
48
+
49
+ def commands_with_delay(commands)
50
+ commands.inject do |last_command, command|
51
+ command[:delay] = command[:msec] - last_command[:msec]
52
+ command
53
+ end
54
+
55
+ # Drop the first command, as we don't have a delay for that
56
+ commands.drop(1)
57
+ end
58
+
59
+ def detect_sequences(player_commands)
60
+ sequences = []
61
+ groups = player_commands.each_with_object(Hash.new(&ARRAY_HASH)).with_index do |(command, hash), index|
62
+ hash[command[:delay]] << command.merge(index: index)
63
+ end
64
+
65
+ groups.each_pair do |delay, commands|
66
+ sequence = []
67
+
68
+ commands.inject do |last_command, command|
69
+ if last_command[:index].next == command[:index]
70
+ sequence << command
71
+ elsif sequence.length > 1
72
+ sequences << {
73
+ delay: delay,
74
+ sequence: sequence.map { |c| c[:target] }
75
+ }
76
+ sequence = []
77
+ end
78
+
79
+ command
80
+ end
81
+ end
82
+
83
+ sequences = sequences.uniq.map do |sequence|
84
+ sequence.merge(
85
+ occurences: sequences.count { |current| current == sequence }
86
+ )
87
+ end
88
+
89
+ sequences.select { |sequence| sequence[:occurences] > 1 }.sort do |a, b|
90
+ b[:occurences] <=> a[:occurences]
91
+ end
92
+ end
93
+
94
+ def production_stats(player_commands)
95
+ order_delays = player_commands.map { |command| command[:delay] }
96
+
97
+ {
98
+ order_delay: {
99
+ min: order_delays.min,
100
+ max: order_delays.max,
101
+ mean: mean(order_delays),
102
+ median: median(order_delays)
103
+ }
104
+ }
105
+ end
106
+
107
+ def suspected_macros(production_stats, sequences)
108
+ suspicious_sequences = sequences.count do |sequence|
109
+ sequence[:delay] < 100 &&
110
+ sequence[:sequence].length > 3 &&
111
+ sequence[:sequence].uniq.length > 2
112
+ end
113
+
114
+ production_stats[:order_delay][:min] < 50 || suspicious_sequences > 0
115
+ end
116
+
117
+ def mean(ints)
118
+ ints.sum / ints.length
119
+ end
120
+
121
+ def median(ints)
122
+ ints = ints.sort
123
+ middle_index = (ints.length - 1) / 2.0
124
+
125
+ (ints[middle_index.floor] + ints[middle_index.ceil]) / 2
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,33 @@
1
+ module Openra
2
+ class CLI
3
+ module Commands
4
+ module Formatters
5
+ class JSON
6
+ def call(data)
7
+ ::JSON.dump(data)
8
+ end
9
+ end
10
+
11
+ class PrettyJSON
12
+ def call(data)
13
+ ::JSON.pretty_generate(data)
14
+ end
15
+ end
16
+
17
+ class YAML
18
+ def call(data)
19
+ ::YAML.dump(
20
+ Openra::Struct::Functions[:deep_stringify_keys].(data)
21
+ )
22
+ end
23
+ end
24
+ end
25
+
26
+ FORMATTERS = {
27
+ 'json' => Formatters::JSON.new,
28
+ 'pretty-json' => Formatters::PrettyJSON.new,
29
+ 'yaml' => Formatters::YAML.new,
30
+ }.freeze
31
+ end
32
+ end
33
+ end
@@ -2,12 +2,6 @@ module Openra
2
2
  class CLI
3
3
  module Commands
4
4
  class ReplayData < Hanami::CLI::Command
5
- FORMATTERS = {
6
- 'json' => Formatters::JSON.new,
7
- 'pretty-json' => Formatters::PrettyJSON.new,
8
- 'yaml' => Formatters::YAML.new,
9
- }.freeze
10
-
11
5
  desc 'Output replay data to stdout'
12
6
 
13
7
  argument :replay, required: true, desc: 'Path of the replay file to read data from'
@@ -16,7 +10,7 @@ module Openra
16
10
  def call(replay:, **options)
17
11
  replay = Openra::Replays::Replay.new(replay)
18
12
 
19
- replay_data = {
13
+ data = {
20
14
  mod: replay.metadata.mod,
21
15
  version: replay.metadata.version,
22
16
  server_name: utf8(replay.global_settings.server_name),
@@ -92,7 +86,7 @@ module Openra
92
86
  Openra::YAML.load(order.target)
93
87
  ).clients
94
88
  when 'PlaceBuilding'
95
- client_hash = replay_data[:clients].find do |candidate|
89
+ client_hash = data[:clients].find do |candidate|
96
90
  candidate[:index] == order.client_index.to_s
97
91
  end
98
92
 
@@ -105,19 +99,19 @@ module Openra
105
99
  }
106
100
  }
107
101
  when 'Message'
108
- replay_data[:chat] << {
102
+ data[:chat] << {
109
103
  channel: 'server',
110
104
  name: nil,
111
105
  message: utf8(order.target)
112
106
  }
113
107
  when 'Chat'
114
- replay_data[:chat] << {
108
+ data[:chat] << {
115
109
  channel: 'global',
116
110
  name: utf8(client.name),
117
111
  message: utf8(order.target)
118
112
  }
119
113
  when 'TeamChat'
120
- replay_data[:chat] << {
114
+ data[:chat] << {
121
115
  channel: client.team,
122
116
  name: utf8(client.name),
123
117
  message: utf8(order.target)
@@ -125,7 +119,7 @@ module Openra
125
119
  end
126
120
  end
127
121
 
128
- puts FORMATTERS.fetch(options[:format]).call(replay_data)
122
+ puts FORMATTERS.fetch(options[:format]).call(data)
129
123
  end
130
124
 
131
125
  private
@@ -56,7 +56,10 @@ module Openra
56
56
 
57
57
  start_game_order = orders.find { |order| order[:command] == 'StartGame' }
58
58
  start_game_index = orders.index(start_game_order)
59
- start_game_sync_order = orders[start_game_index.pred]
59
+ pre_game_orders = orders[0..start_game_index]
60
+ start_game_sync_order = pre_game_orders.reverse.find do |order|
61
+ order[:command] == 'SyncInfo'
62
+ end
60
63
 
61
64
  @sync_info = Openra::Struct::SyncInfo.new(
62
65
  Openra::YAML.load(start_game_sync_order.target)
@@ -1,3 +1,3 @@
1
1
  module Openra
2
- VERSION = '1.4.0'.freeze
2
+ VERSION = '1.5.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openra
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Holland
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-17 00:00:00.000000000 Z
11
+ date: 2018-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -133,8 +133,9 @@ files:
133
133
  - lib/openra.rb
134
134
  - lib/openra/cli.rb
135
135
  - lib/openra/cli/command_registry.rb
136
+ - lib/openra/cli/commands/detect_production_macros.rb
137
+ - lib/openra/cli/commands/formatters.rb
136
138
  - lib/openra/cli/commands/replay_data.rb
137
- - lib/openra/cli/commands/replay_data/formatters.rb
138
139
  - lib/openra/cli/commands/version.rb
139
140
  - lib/openra/replays.rb
140
141
  - lib/openra/replays/file.rb
@@ -1,29 +0,0 @@
1
- module Openra
2
- class CLI
3
- module Commands
4
- class ReplayData < Hanami::CLI::Command
5
- module Formatters
6
- class JSON
7
- def call(data)
8
- ::JSON.dump(data)
9
- end
10
- end
11
-
12
- class PrettyJSON
13
- def call(data)
14
- ::JSON.pretty_generate(data)
15
- end
16
- end
17
-
18
- class YAML
19
- def call(data)
20
- ::YAML.dump(
21
- Openra::Struct::Functions[:deep_stringify_keys].(data)
22
- )
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end