reinforce 0.1.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.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ module Attributes
5
+ class Ability < Base
6
+ AUTOBUILDS = %w[
7
+ abilities/races/american/battlegroups/infantry/infantry_left_2a_medical_tent
8
+ ].freeze
9
+
10
+ SPAWNERS = %w[
11
+ armored_support_flame_p3_ak
12
+ armored_support_command_p4_ak
13
+ italian_combined_arms_bersaglieri_ak
14
+ italian_combined_arms_semovente_ak
15
+ italian_combined_arms_m13_40_ak
16
+ italian_infantry_double_l640_ak
17
+ italian_infantry_guastatori_ak
18
+ italian_infantry_cannone_da_105_ak
19
+ infiltration_left_1_vampire_ht_goliath_ak
20
+ british_air_and_sea_left_2a_centaur_cs_uk
21
+ british_air_and_sea_right_1_commandos_uk
22
+ british_air_and_sea_right_2a_pack_howitzer_team_uk
23
+ british_air_and_sea_right_2b_commando_lmg_team_uk
24
+ british_armored_right_2_crusader_aa_uk
25
+ british_armored_left_2_churchill
26
+ british_armored_left_3b_churchill_black_prince_uk
27
+ artillery_gurkhas_uk
28
+ artillery_4_2_inch_heavy_mortar_uk
29
+ artillery_bl_5_5_heavy_artillery_uk
30
+ australian_defense_archer_tank_destroyer_call_in_uk
31
+ australian_defense_australian_light_infantry_uk
32
+ australian_defense_2pdr_at_gun_uk
33
+ airborne_right_1a_pathfinders_us
34
+ airborne_right_1b_paradrop_hmg_us
35
+ airborne_right_2_paratrooper_us
36
+ airborne_right_3_paradrop_at_gun_us
37
+ armored_left_2b_recovery_vehicle_us
38
+ armored_right_2a_scott_us
39
+ armored_right_3_easy_8_task_force_us
40
+ special_operations_left_1a_m29_weasal_us
41
+ special_operations_left_1b_m29_weasal_with_pack_howitzer
42
+ special_operations_left_3_whizbang_us
43
+ special_operations_right_2_devils_brigade_us
44
+ infantry_left_1_rifleman_convert_to_ranger_us
45
+ infantry_right_1a_artillery_observers_us
46
+ infantry_right_2_105mm_howitzer_us
47
+ breakthrough_right_3a_assault_group_ger
48
+ breakthrough_left_1b_truck_2_5_ger
49
+ breakthrough_left_2b_panzer_iv_cmd_ger
50
+ breakthrough_left_3_tiger_ger
51
+ luftwaffe_right_2_fallschirmjagers_ger
52
+ luftwaffe_left_1b_fallschirmpioneers_ger
53
+ luftwaffe_left_2b_combat_group_ger
54
+ luftwaffe_left_2a_weapon_drop_ger
55
+ luftwaffe_left_3_88mm_at_gun_ger
56
+ mechanized_right_2a_stug_assault_group_ger
57
+ mechanized_left_2b_8_rad_ger
58
+ mechanized_right_3_panther_ger
59
+ mechanized_left_3a_wespe_ger
60
+ coastal_left_1_coastal_reserve_ger
61
+ coastal_artillery_officer_ger
62
+ coastal_obice_ger
63
+ halftrack_deployment_panzerjager_inf_1_ak
64
+ halftrack_deployment_assault_grenadier_1_ak
65
+ halftrack_deployment_at_gun_1_ak
66
+ halftrack_deployment_leig_1_ak
67
+ halftrack_deployment_piv_tank_hunter_group_ak
68
+ halftrack_deployment_stug_assault_group_ak
69
+ halftrack_deployment_panzer_iii_assault_group_ak
70
+ halftrack_deployment_tiger_ak
71
+ ].to_set
72
+
73
+ FILENAME = 'abilities.json'
74
+
75
+ attr_reader :builds
76
+
77
+ def initialize(path:, pbgid:, locstring:, icon_name:, builds:)
78
+ super(path:, pbgid:, locstring:, icon_name:)
79
+ @builds = builds
80
+ end
81
+
82
+ class << self
83
+ private
84
+
85
+ def parse(data)
86
+ parse_subtree(data, %w[abilities])
87
+ end
88
+
89
+ def parse_subtree(data, key)
90
+ data.flat_map do |k, tree|
91
+ new_key = key + [k]
92
+ if tree.key?('ability_bag')
93
+ parse_ability_bag(tree, new_key)
94
+ else
95
+ parse_subtree(tree, new_key)
96
+ end
97
+ end.compact
98
+ end
99
+
100
+ def parse_ability_bag(data, path)
101
+ locstring = data.dig('ability_bag', 'ui_info', 'screen_name', 'locstring', 'value')
102
+
103
+ return if locstring.nil? || locstring == '0'
104
+
105
+ icon_name = data.dig('ability_bag', 'ui_info', 'icon_name')
106
+ builds = data.dig('ability_bag', 'cursor_ghost_ebp', 'instance_reference')
107
+
108
+ new(locstring:,
109
+ icon_name:,
110
+ builds: builds == '' ? nil : builds,
111
+ path:,
112
+ pbgid: data['pbgid'])
113
+ end
114
+ end
115
+
116
+ def autobuild?
117
+ @path.include?('auto_build') || @path.include?('autobuild') || AUTOBUILDS.include?(path)
118
+ end
119
+
120
+ def production_building?
121
+ %r{ebps/races/.+/buildings/production/.+}.match?(@builds)
122
+ end
123
+
124
+ def spawner?
125
+ SPAWNERS.include?(@path.last)
126
+ end
127
+
128
+ def ==(other)
129
+ super && @builds == other.builds
130
+ end
131
+
132
+ def as_json(_options)
133
+ super.merge(builds:)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Reinforce
6
+ module Attributes
7
+ class Base
8
+ attr_reader :locstring, :icon_name
9
+
10
+ def initialize(path:, pbgid:, locstring:, icon_name:)
11
+ @path = path
12
+ @pbgid = pbgid
13
+ @locstring = locstring
14
+ @icon_name = icon_name
15
+ end
16
+
17
+ class << self
18
+ def load_from_file(path)
19
+ File.open(path) do |file|
20
+ data = JSON.parse(file.read)
21
+ parse(data)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def parse(_data)
28
+ raise NotImplementedError
29
+ end
30
+ end
31
+
32
+ def path
33
+ @path.join('/')
34
+ end
35
+
36
+ def pbgid
37
+ @pbgid.to_i
38
+ end
39
+
40
+ def ==(other)
41
+ return false unless other.is_a?(self.class)
42
+
43
+ path == other.path &&
44
+ pbgid == other.pbgid &&
45
+ locstring == other.locstring &&
46
+ icon_name == other.icon_name
47
+ end
48
+
49
+ def as_json(_options)
50
+ {
51
+ path: @path,
52
+ pbgid:,
53
+ locstring:,
54
+ icon_name:
55
+ }
56
+ end
57
+
58
+ def to_json(*options)
59
+ as_json(*options).to_json(*options)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ module Attributes
5
+ class Collection
6
+ LATEST_BUILD = -1
7
+
8
+ def initialize
9
+ @pbgid_keyed = {}
10
+ @path_keyed = {}
11
+ end
12
+
13
+ class << self
14
+ def generate_for(klass, pretty: false)
15
+ collection = load_from_data(klass)
16
+
17
+ collection.rehash_all
18
+
19
+ generation_path = File.join(Reinforce.root, 'generated', klass::FILENAME)
20
+ json = pretty ? JSON.pretty_generate(collection) : JSON.generate(collection)
21
+ File.write(generation_path, json)
22
+
23
+ true
24
+ end
25
+
26
+ def instance
27
+ return @instance if defined?(@instance)
28
+
29
+ @instance = new
30
+
31
+ [Ability, Entity, Squad, Upgrade].each do |klass|
32
+ generated_path = File.join(Reinforce.root, 'generated', klass::FILENAME)
33
+ File.open(generated_path) do |file|
34
+ data = JSON.parse(file.read)
35
+ data.each do |build, attributes|
36
+ @instance.populate(build, attributes.map { |a| klass.new(**a.transform_keys(&:to_sym)) }, rehash: false)
37
+ end
38
+ end
39
+ end
40
+
41
+ @instance.rehash_all
42
+ @instance
43
+ end
44
+
45
+ def load_for(klass)
46
+ collection = new
47
+ generated_path = File.join(Reinforce.root, 'generated', klass::FILENAME)
48
+ File.open(generated_path) do |file|
49
+ data = JSON.parse(file.read)
50
+ data.each do |build, attributes|
51
+ collection.populate(build, attributes.map { |a| klass.new(**a.transform_keys(&:to_sym)) }, rehash: false)
52
+ end
53
+ end
54
+
55
+ collection.rehash_all
56
+ collection
57
+ end
58
+
59
+ private
60
+
61
+ def load_from_data(klass)
62
+ collection = new
63
+ data_path = File.join(Reinforce.root, 'data')
64
+
65
+ Dir.chdir(data_path) do
66
+ Dir.glob('*').select { |f| File.directory?(f) }.each do |dir|
67
+ file_path = File.join(dir, klass::FILENAME)
68
+ next unless File.exist?(file_path)
69
+
70
+ data = klass.load_from_file(file_path)
71
+ collection.populate(dir, data, rehash: false)
72
+ end
73
+ end
74
+
75
+ collection
76
+ end
77
+ end
78
+
79
+ def get_by_pbgid(pbgid, build: LATEST_BUILD)
80
+ return nil if build.nil?
81
+
82
+ build = last_build if build == LATEST_BUILD
83
+ @pbgid_keyed.dig(build, pbgid) || get_by_pbgid(pbgid, build: previous_build_for(build))
84
+ end
85
+
86
+ def get_by_path(path, build: LATEST_BUILD)
87
+ return nil if build.nil?
88
+
89
+ build = last_build if build == LATEST_BUILD
90
+ @path_keyed.dig(build, path) || get_by_path(path, build: previous_build_for(build))
91
+ end
92
+
93
+ def populate(build, data, rehash: true)
94
+ populate_pbgid_hash(build, data)
95
+ populate_path_hash(build, data)
96
+
97
+ rehash_all if rehash
98
+ true
99
+ end
100
+
101
+ def rehash_all
102
+ rehash(@pbgid_keyed)
103
+ rehash(@path_keyed)
104
+ end
105
+
106
+ def as_json(_options)
107
+ @pbgid_keyed.transform_values(&:values)
108
+ end
109
+
110
+ def to_json(*options)
111
+ as_json(*options).to_json(*options)
112
+ end
113
+
114
+ private
115
+
116
+ def populate_pbgid_hash(build, data)
117
+ @pbgid_keyed[build.to_i] ||= {}
118
+ @pbgid_keyed[build.to_i] = @pbgid_keyed[build.to_i].merge(data.to_h { |o| [o.pbgid, o] })
119
+ end
120
+
121
+ def populate_path_hash(build, data)
122
+ @path_keyed[build.to_i] ||= {}
123
+ @path_keyed[build.to_i] = @path_keyed[build.to_i].merge(data.to_h { |o| [o.path, o] })
124
+ end
125
+
126
+ def first_build
127
+ @pbgid_keyed.keys.min
128
+ end
129
+
130
+ def last_build
131
+ @pbgid_keyed.keys.max
132
+ end
133
+
134
+ def previous_build_for(build)
135
+ @pbgid_keyed.keys.sort.reverse.find { |b| b < build }
136
+ end
137
+
138
+ def rehash(hash)
139
+ previous = nil
140
+
141
+ hash.keys.sort.each do |build|
142
+ next previous = build if previous.nil?
143
+
144
+ hash[build].each do |key, value|
145
+ hash[build].delete(key) if value == hash[previous][key]
146
+ end
147
+
148
+ hash[build] = hash[previous].merge(hash[build])
149
+
150
+ previous = build
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ module Attributes
5
+ class Entity < Base
6
+ FILENAME = 'ebps.json'
7
+
8
+ attr_reader :spawns, :upgrades
9
+
10
+ def initialize(path:, pbgid:, locstring:, icon_name:, spawns:, upgrades:)
11
+ super(path:, pbgid:, locstring:, icon_name:)
12
+ @spawns = spawns
13
+ @upgrades = upgrades
14
+ end
15
+
16
+ class << self
17
+ private
18
+
19
+ def parse(data)
20
+ parse_subtree(data, %w[ebps])
21
+ end
22
+
23
+ def parse_subtree(data, key)
24
+ data.flat_map do |k, tree|
25
+ new_key = key + [k]
26
+ if tree.key?('extensions')
27
+ parse_extensions(tree, new_key)
28
+ else
29
+ parse_subtree(tree, new_key)
30
+ end
31
+ end.compact
32
+ end
33
+
34
+ def parse_extensions(data, path)
35
+ locstring = parse_locstring(data)
36
+
37
+ return if locstring.nil? || locstring == '0'
38
+
39
+ new(locstring:,
40
+ icon_name: parse_icon_name(data),
41
+ path:,
42
+ spawns: parse_spawns(data),
43
+ upgrades: parse_upgrades(data),
44
+ pbgid: data['pbgid'])
45
+ end
46
+
47
+ def parse_locstring(data)
48
+ ext_data = data['extensions'].find { |ext| ext['exts'].key?('screen_name') }
49
+ ext_data&.dig('exts', 'screen_name', 'locstring', 'value')
50
+ end
51
+
52
+ def parse_icon_name(data)
53
+ ext_data = data['extensions'].find { |ext| ext['exts'].key?('screen_name') }
54
+ ext_data.dig('exts', 'icon_name')
55
+ end
56
+
57
+ def parse_spawns(data)
58
+ ext_spawns = data['extensions'].find { |ext| ext['exts'].key?('spawn_items') }
59
+ (ext_spawns&.dig('exts', 'spawn_items') || []).map do |item|
60
+ item.dig('spawn_item', 'squad', 'instance_reference')
61
+ end
62
+ end
63
+
64
+ def parse_upgrades(data)
65
+ ext_upgrades = data['extensions'].find { |ext| ext['exts'].key?('standard_upgrades') }
66
+ (ext_upgrades&.dig('exts', 'standard_upgrades') || []).map do |item|
67
+ item.dig('upgrade', 'instance_reference')
68
+ end
69
+ end
70
+ end
71
+
72
+ def produces?(path)
73
+ @spawns.include?(path) || @upgrades.include?(path)
74
+ end
75
+
76
+ def ==(other)
77
+ super && @spawns == other.spawns && @upgrades == other.upgrades
78
+ end
79
+
80
+ def as_json(_options)
81
+ super.merge(spawns:, upgrades:)
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ module Attributes
5
+ class Squad < Base
6
+ FILENAME = 'sbps.json'
7
+
8
+ class << self
9
+ private
10
+
11
+ def parse(data)
12
+ parse_subtree(data, %w[sbps])
13
+ end
14
+
15
+ def parse_subtree(data, key)
16
+ data.flat_map do |k, tree|
17
+ new_key = key + [k]
18
+ if tree.key?('extensions')
19
+ parse_extensions(tree, new_key)
20
+ else
21
+ parse_subtree(tree, new_key)
22
+ end
23
+ end.compact
24
+ end
25
+
26
+ def parse_extensions(data, path)
27
+ squad_data = squad_data_from(data)
28
+ locstring = squad_data&.dig('race_data', 'info', 'screen_name', 'locstring', 'value')
29
+
30
+ return if locstring.nil? || locstring == '0'
31
+
32
+ icon_name = squad_data.dig('race_data', 'info', 'icon_name')
33
+
34
+ new(locstring:,
35
+ icon_name:,
36
+ path:,
37
+ pbgid: data['pbgid'])
38
+ end
39
+
40
+ def squad_data_from(data)
41
+ race_ext = data['extensions'].find do |ext|
42
+ ext['squadexts'].key?('race_list')
43
+ end
44
+
45
+ race_ext&.dig('squadexts', 'race_list')&.find do |entry|
46
+ !entry.dig('race_data', 'info', 'screen_name').nil?
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ module Attributes
5
+ class Upgrade < Base
6
+ FILENAME = 'upgrade.json'
7
+
8
+ class << self
9
+ private
10
+
11
+ def parse(data)
12
+ parse_subtree(data, %w[upgrade])
13
+ end
14
+
15
+ def parse_subtree(data, key)
16
+ data.flat_map do |k, tree|
17
+ new_key = key + [k]
18
+ if tree.key?('upgrade_bag')
19
+ parse_upgrade_bag(tree, new_key)
20
+ else
21
+ parse_subtree(tree, new_key)
22
+ end
23
+ end.compact
24
+ end
25
+
26
+ def parse_upgrade_bag(data, path)
27
+ locstring = data.dig('upgrade_bag', 'ui_info', 'screen_name', 'locstring', 'value')
28
+
29
+ return if locstring.nil? || locstring == '0'
30
+
31
+ icon_name = data.dig('upgrade_bag', 'ui_info', 'icon_name')
32
+
33
+ new(locstring:,
34
+ icon_name:,
35
+ path:,
36
+ pbgid: data['pbgid'])
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ class Command
5
+ attr_reader :action_type, :tick, :pbgid, :source, :index, :details
6
+
7
+ # rubocop:disable Metrics/AbcSize
8
+ def initialize(command_hash, build_number)
9
+ @action_type = command_hash.keys.first
10
+ @tick = command_hash.values.first[:tick]
11
+ @pbgid = command_hash.values.first[:pbgid]
12
+ @source = command_hash.values.first[:source_identifier]
13
+ @index = command_hash.values.first[:queue_index]
14
+ @details = Attributes::Collection.instance.get_by_pbgid(@pbgid, build: build_number)
15
+ @cancelled = false
16
+ @suspect_from_tick = nil
17
+ end
18
+ # rubocop:enable Metrics/AbcSize
19
+
20
+ def cancelled?
21
+ @cancelled
22
+ end
23
+
24
+ def cancel
25
+ @cancelled = true
26
+ end
27
+
28
+ def suspect?
29
+ !@suspect_from_tick.nil?
30
+ end
31
+
32
+ def mark_suspect(tick)
33
+ @suspect_from_tick = tick
34
+ end
35
+
36
+ def mark_legit
37
+ @suspect_from_tick = nil
38
+ end
39
+
40
+ def suspect_since
41
+ @suspect_from_tick
42
+ end
43
+
44
+ def as_json(_options)
45
+ {
46
+ action: action_type,
47
+ tick:,
48
+ pbgid:,
49
+ locstring: @details.locstring,
50
+ icon_name: @details.icon_name
51
+ }
52
+ end
53
+
54
+ def to_json(*options)
55
+ as_json(*options).to_json(*options)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ class Factory
5
+ BUILDING_COMMANDS = %w[UseAbility].freeze
6
+ PRODUCTION_COMMANDS = %w[BuildSquad BuildGlobalUpgrade].freeze
7
+ BATTLEGROUP_COMMANDS = %w[SelectBattlegroup SelectBattlegroupAbility UseBattlegroupAbility].freeze
8
+ CANCEL_COMMANDS = %w[CancelConstruction CancelProduction].freeze
9
+ ALL_COMMANDS = BUILDING_COMMANDS + PRODUCTION_COMMANDS + BATTLEGROUP_COMMANDS + CANCEL_COMMANDS
10
+
11
+ def initialize(player, build_number)
12
+ @player = player.to_h
13
+ @build_number = build_number
14
+ @buildings = []
15
+ @productions = {}
16
+ @battlegroup = []
17
+ end
18
+
19
+ def build(with_cancellations: false)
20
+ commands.each { |c| classify_command(c) }
21
+ result = consolidate
22
+ result = rectify_suspects(result)
23
+ result = result.reject(&:cancelled?) unless with_cancellations
24
+ result
25
+ end
26
+
27
+ private
28
+
29
+ def commands
30
+ @commands ||= @player[:commands].filter { |c| ALL_COMMANDS.include?(c.keys.first) }
31
+ .map { |c| Command.new(c, @build_number) }
32
+ end
33
+
34
+ def classify_command(command)
35
+ if BUILDING_COMMANDS.include?(command.action_type)
36
+ classify_building_command(command)
37
+ elsif PRODUCTION_COMMANDS.include?(command.action_type)
38
+ classify_production_command(command)
39
+ elsif BATTLEGROUP_COMMANDS.include?(command.action_type)
40
+ classify_battlegroup_command(command)
41
+ elsif CANCEL_COMMANDS.include?(command.action_type)
42
+ process_cancellation(command)
43
+ end
44
+ end
45
+
46
+ def classify_building_command(command)
47
+ @buildings << command if command.details.autobuild?
48
+ end
49
+
50
+ def classify_production_command(command)
51
+ @productions[command.source] ||= []
52
+ @productions[command.source] << command
53
+ end
54
+
55
+ def classify_battlegroup_command(command)
56
+ if command.details.respond_to?(:autobuild?) && command.details.autobuild?
57
+ @buildings << command
58
+ else
59
+ @battlegroup << command
60
+ end
61
+ end
62
+
63
+ def process_cancellation(command)
64
+ if command.action_type == 'CancelConstruction'
65
+ @buildings.reject(&:suspect?).each { |building| building.mark_suspect(command.tick) }
66
+ else
67
+ @productions[command.source][command.index - 1].cancel
68
+ end
69
+ end
70
+
71
+ def consolidate
72
+ build = @buildings + @battlegroup + @productions.values.flatten
73
+ build.sort_by(&:tick)
74
+ end
75
+
76
+ # rubocop:disable Metrics/AbcSize
77
+ def rectify_suspects(commands)
78
+ commands.each_with_index do |command, idx|
79
+ next unless command.suspect?
80
+
81
+ building_details = Attributes::Collection.instance.get_by_path(command.details.builds, build: @build_number)
82
+ remaining = commands[(idx + 1)..]
83
+ relevant = remaining.take_while { |c| c.pbgid != command.pbgid }
84
+
85
+ used = relevant.any? do |c|
86
+ building_details.produces?(c.details.path)
87
+ end
88
+
89
+ command.mark_legit if used
90
+ end
91
+ end
92
+ # rubocop:enable Metrics/AbcSize
93
+ end
94
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reinforce
4
+ VERSION = '0.1.0'
5
+ end