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 +4 -4
- data/CHANGELOG.md +13 -1
- data/README.md +8 -2
- data/lib/openra/cli.rb +2 -1
- data/lib/openra/cli/command_registry.rb +1 -0
- data/lib/openra/cli/commands/detect_production_macros.rb +130 -0
- data/lib/openra/cli/commands/formatters.rb +33 -0
- data/lib/openra/cli/commands/replay_data.rb +6 -12
- data/lib/openra/replays/replay.rb +4 -1
- data/lib/openra/version.rb +1 -1
- metadata +4 -3
- data/lib/openra/cli/commands/replay_data/formatters.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f8e899e24d7c1a64648be5f39fb249439f7ff008
|
4
|
+
data.tar.gz: 6efadd30042d44cfd54bcaecf9594b09d5aed992
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b15a3518af884311eac8fda0ea5323f30af197c1e2b4e00fdf86ae2103094d666ec2ba133a8c96ad7ad6e0f3af1fd27b3008aa5f539dbce03d9c13d9da63cf79
|
7
|
+
data.tar.gz: 1631bcefa11178ba60eb37d4a71019e4e4383b3e33290c29f2ccde99aae410e2e51b98bf9acce0282172f15105bf1405d711d14ebea9a2f487f7b03ed0b8f7d9
|
data/CHANGELOG.md
CHANGED
@@ -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...
|
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
|
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 /
|
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
|
data/lib/openra/cli.rb
CHANGED
@@ -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/
|
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'
|
@@ -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
|
-
|
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 =
|
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
|
-
|
102
|
+
data[:chat] << {
|
109
103
|
channel: 'server',
|
110
104
|
name: nil,
|
111
105
|
message: utf8(order.target)
|
112
106
|
}
|
113
107
|
when 'Chat'
|
114
|
-
|
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
|
-
|
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(
|
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
|
-
|
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)
|
data/lib/openra/version.rb
CHANGED
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
|
+
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-
|
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
|