textflight-client 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,222 @@
1
+
2
+ module TFClient
3
+ module Models
4
+
5
+ class Scan < Response
6
+ LINE_IDENTIFIERS = [
7
+ "Owner",
8
+ "Operators",
9
+ "Outfit space",
10
+ "Shield charge",
11
+ "Outfits",
12
+ "Cargo"
13
+ ]
14
+
15
+ attr_reader :id, :name, :owner, :outfit_space, :shield_charge
16
+ attr_reader :outfits, :cargo
17
+
18
+ def initialize(lines:)
19
+ super(lines: lines)
20
+
21
+ ship_line = lines[0]
22
+ values_hash = ResponseParser.hash_from_line_values(line: ship_line)
23
+ @id = values_hash[:id].to_i
24
+ @name = values_hash[:name]
25
+
26
+ LINE_IDENTIFIERS.each do |line_id|
27
+
28
+ # Not sure what value this adds
29
+ next if line_id == "Operators"
30
+
31
+ var_name = ResponseParser.snake_case_sym_from_string(string: line_id)
32
+ class_name = ResponseParser.camel_case_from_string(string: line_id)
33
+ clazz = ResponseParser.model_class_from_string(string: class_name)
34
+
35
+ if clazz.nil?
36
+ raise "could not find class name: #{class_name} derived from #{line_id}"
37
+ end
38
+
39
+ line, _ = ResponseParser.line_and_index_for_beginning_with(lines: @lines,
40
+ string: line_id)
41
+
42
+ if ["Owner", "Outfit space", "Shield charge"].include?(line_id)
43
+ var = clazz.new(line: line)
44
+ elsif ["Outfits", "Cargo"].include?(line_id)
45
+ var = clazz.new(lines: @lines)
46
+ if var.is_a?(TFClient::Models::Outfits)
47
+ var.max_slots = @outfit_space.value
48
+ end
49
+ else
50
+ raise "Cannot find class initializer for: #{line_id}"
51
+ end
52
+
53
+ instance_variable_set("@#{var_name}", var)
54
+ end
55
+ end
56
+
57
+ def response
58
+ # TODO this is interesting only when you scan _other_ structures
59
+ # table = TTY::Table.new(header: [
60
+ # {value: @owner.translation, alignment: :center},
61
+ # {value: @outfit_space.translation, alignment: :center},
62
+ # {value: @shield_charge.translation, alignment: :center}
63
+ # ])
64
+ #
65
+ # table << [@owner.username,
66
+ # @outfit_space.value,
67
+ # @shield_charge.value]
68
+ #
69
+ # puts table.render(:ascii, padding: [0,1,0,1],
70
+ # width: Models::TABLE_WIDTH, resize: true) do |renderer|
71
+ # renderer.alignments= [:center, :center, :center]
72
+ # end
73
+
74
+ puts @outfits.to_s
75
+ puts @cargo.to_s
76
+ end
77
+ end
78
+
79
+ class Owner < Model
80
+ attr_reader :username
81
+
82
+ def initialize(line:)
83
+ super(line: line)
84
+ @username = @values_hash[:username]
85
+ end
86
+
87
+ def to_s
88
+ "#{@translation}: #{@username}"
89
+ end
90
+ end
91
+
92
+ class OutfitSpace < Model
93
+ attr_reader :value
94
+
95
+ def initialize(line:)
96
+ super(line: line)
97
+ @value = @values_hash[:space].to_i
98
+ end
99
+
100
+ def to_s
101
+ "#{@translation}: #{@value}"
102
+ end
103
+ end
104
+
105
+ class ShieldCharge < Model
106
+ attr_reader :value
107
+
108
+ def initialize(line:)
109
+ super(line: line)
110
+ @value = @values_hash[:charge].to_f
111
+ end
112
+
113
+ def to_s
114
+ "#{@translation}: #{@value}"
115
+ end
116
+ end
117
+
118
+ class Outfits < ModelWithItems
119
+
120
+ attr_accessor :max_slots
121
+
122
+ def initialize(lines:)
123
+ line, index = ResponseParser.line_and_index_for_beginning_with(lines: lines,
124
+ string: "Outfits")
125
+ super(line: line)
126
+
127
+ items = ResponseParser.collect_list_items(lines: lines, start_index: index + 1)
128
+ @items = items.map do |item|
129
+ line = item.strip
130
+
131
+ hash = ResponseParser.hash_from_line_values(line: line)
132
+
133
+ index = hash[:index].to_i
134
+ name = hash[:name]
135
+ mark = hash[:mark].to_i
136
+ setting = hash[:setting].to_i
137
+ { index: index, name: name, mark: mark, setting: setting}
138
+ end
139
+ @max_slots = 0
140
+ end
141
+
142
+ def to_s
143
+ table = TTY::Table.new(header: [
144
+ "#{@translation}: #{slots_used}/#{@max_slots} slots",
145
+ {value: "name", alignment: :center},
146
+ {value: "setting", alignment: :center},
147
+ {value: "index", alignment: :center}
148
+ ])
149
+
150
+ @items.each do |item|
151
+ table << [
152
+ "[#{item[:index]}]",
153
+ "#{item[:name]} (#{item[:mark].to_roman})",
154
+ item[:setting],
155
+ "[#{item[:index]}]"
156
+ ]
157
+ end
158
+
159
+ table.render(:ascii, Models::TABLE_OPTIONS) do |renderer|
160
+ renderer.alignments= [:right, :right, :center, :center]
161
+ end
162
+ end
163
+
164
+ def slots_used
165
+ @items.map { |hash| hash[:mark] }.sum
166
+ end
167
+ end
168
+
169
+ class Cargo < ModelWithItems
170
+
171
+ def initialize(lines:)
172
+ line, index = ResponseParser.line_and_index_for_beginning_with(lines: lines,
173
+ string: "Cargo")
174
+ super(line: line)
175
+
176
+ items = ResponseParser.collect_list_items(lines: lines, start_index: index + 1)
177
+ @items = items.map do |item|
178
+ line = item.strip
179
+
180
+ hash = ResponseParser.hash_from_line_values(line: line)
181
+
182
+ index = hash[:index].to_i
183
+ name = hash[:name]
184
+ count = hash[:count].to_i
185
+ # TODO: this must be the mark?
186
+ mark = hash[:extra].to_i
187
+ { index: index, name: name, count: count, mark: mark}
188
+ end
189
+ end
190
+
191
+ def to_s
192
+ table = TTY::Table.new(header: [
193
+ "Weight: #{weight}",
194
+ {value: "cargo", alignment: :center},
195
+ {value: "amount", alignment: :center},
196
+ {value: "index", alignment: :center}
197
+ ])
198
+
199
+ @items.each do |item|
200
+ name = item[:name]
201
+ mark = item[:mark].to_i
202
+ if mark && (mark != 0)
203
+ name = "#{name} (#{mark.to_roman})"
204
+ end
205
+ table << ["[#{item[:index]}]",
206
+ name,
207
+ item[:count],
208
+ "[#{item[:index]}]"]
209
+ end
210
+
211
+ table.render(:ascii, Models::TABLE_OPTIONS) do |renderer|
212
+ renderer.alignments= [:right, :right, :center, :center]
213
+ end
214
+ end
215
+
216
+ # TODO: only some items in the inventory contribute to weight
217
+ def weight
218
+ @items.map { |hash| hash[:count] }.sum
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,191 @@
1
+
2
+ module TFClient
3
+ module Models
4
+
5
+ class Status < Response
6
+
7
+ # returns 2 values
8
+ def self.status_from_lines(lines:, start_with:)
9
+ stripped = lines.map { |line| line.strip }
10
+ prefix = start_with.strip
11
+ line, _ = ResponseParser.line_and_index_for_beginning_with(lines: stripped,
12
+ string: prefix)
13
+ if !lines || !line.start_with?(prefix)
14
+ raise "expected line to be a status line for #{prefix} in #{lines}"
15
+ end
16
+
17
+ tokens = ResponseParser.tokenize_line(line: line)
18
+
19
+ status = tokens[0].split(": ").last
20
+
21
+ case status
22
+ when "Overheat in {remaining} seconds!"
23
+ status = "Overheating"
24
+ when "OVERHEATED"
25
+ status = "Overheated"
26
+ when "Ready to engage"
27
+ status = "Ready"
28
+ when "Charging ({charge}%)"
29
+ status = "Charging"
30
+ when "FAILED"
31
+ status = "Failed"
32
+ when "BROWNOUT"
33
+ status = "Brownout"
34
+ when "OVERLOADED"
35
+ status = "Overloaded"
36
+ when "Brownout in {remaining} seconds!"
37
+ status = "Overloaded"
38
+ when "Regenerating at {rate}/s ({shield}/{max})"
39
+ status = "Regenerating"
40
+ when "{progress}% ({interval} second interval)"
41
+ status = "Online"
42
+ end
43
+
44
+ translation = tokens[1]
45
+
46
+ return status, translation if tokens.size == 2
47
+
48
+ translation = ResponseParser.substitute_line_values(line: line)
49
+
50
+ return status, translation.strip
51
+ end
52
+
53
+ attr_reader :states
54
+ attr_reader :status_report
55
+ attr_reader :mass, :total_outfit_space, :used_outfit_space
56
+ attr_reader :heat, :max_heat, :heat_rate, :cooling_status
57
+ attr_reader :energy, :max_energy, :energy_rate, :power_status
58
+ attr_reader :antigravity_engine_status, :antigravity
59
+ attr_reader :mining_status, :mining_interval, :mining_power
60
+ attr_reader :engine_status, :warp_charge
61
+ attr_reader :shield_status, :shield_max, :shield, :shield_charge_rate
62
+ attr_reader :colonists, :colonists_status
63
+
64
+ def initialize(lines:)
65
+ super(lines: lines)
66
+
67
+ @states = {}
68
+
69
+ @status_report = Models::StatusReport.new(lines: lines)
70
+ @mass = @status_report.hash[:mass].to_i
71
+ @total_outfit_space = @status_report.hash[:total_outfit_space].to_i
72
+
73
+ outfit_space_line = lines.detect do |line|
74
+ line.strip.start_with?("Outfit space")
75
+ end
76
+
77
+ hash = ResponseParser.hash_from_line_values(line: outfit_space_line)
78
+ @used_outfit_space = @total_outfit_space - hash[:space].to_i
79
+
80
+ # Cooling
81
+ @states[:cooling], @cooling_status = Status.status_from_lines(
82
+ lines: lines,
83
+ start_with: "Cooling status")
84
+ @heat = @status_report.hash[:heat].to_i
85
+ @max_heat = @status_report.hash[:max_heat].to_i
86
+ @heat_rate = @status_report.hash[:heat_rate].to_f
87
+
88
+ # Energy / Power
89
+ @states[:power], @power_status = Status.status_from_lines(
90
+ lines: lines,
91
+ start_with: "Power status"
92
+ )
93
+
94
+ @energy = @status_report.hash[:energy].to_i
95
+ @max_energy = @status_report.hash[:max_energy].to_i
96
+ @energy_rate = @status_report.hash[:energy_rate].to_f
97
+
98
+ # Antigravity
99
+ antigravity_line = lines.detect do |line|
100
+ line.strip.start_with?("Antigravity engines")
101
+ end
102
+
103
+ if antigravity_line
104
+ @states[:antigravity], @antigravity_engine_status =
105
+ Status.status_from_lines(lines: lines, start_with: "Antigravity engines")
106
+ @antigravity = @status_report.hash[:antigravity].to_i
107
+ else
108
+ # Needs translation
109
+ @states[:antigravity] = "Offline"
110
+ @antigravity = "Antigravity engines: Offline"
111
+ end
112
+
113
+
114
+
115
+ # Mining
116
+ mining_progress_line = lines.detect do |line|
117
+ line.strip.start_with?("Mining progress")
118
+ end
119
+
120
+ if mining_progress_line
121
+ @states[:mining], @mining_status =
122
+ Status.status_from_lines(lines: lines,
123
+ start_with: "Mining progress")
124
+ hash = ResponseParser.hash_from_line_values(line: mining_progress_line)
125
+ @mining_interval = hash[:interval].to_f
126
+ @mining_power = @status_report.hash[:mining_power].to_f
127
+ else
128
+ @mining_interval = nil
129
+ @mining_power = nil
130
+ # TODO needs a translation
131
+ @states[:mining] = "Offline"
132
+ @mining_status = "Offline"
133
+ end
134
+
135
+ # Warp
136
+ warp_line = lines.detect do |line|
137
+ line.strip.start_with?("Warp engines")
138
+ end
139
+ if warp_line
140
+ @states[:warp], @engine_status =
141
+ Status.status_from_lines(lines: lines,
142
+ start_with: "Warp engines")
143
+ @warp_charge = @status_report.hash[:warp_charge].to_f
144
+ else
145
+ @states[:warp] = "Offline"
146
+ @warp_charge = 0.0
147
+ end
148
+
149
+ # Shield
150
+ shield_status_line = lines.detect do |line|
151
+ line.strip.start_with?("Shields")
152
+ end
153
+
154
+ if shield_status_line
155
+ @states[:shields], @shield_status =
156
+ Status.status_from_lines(lines: lines,
157
+ start_with: "Shields")
158
+ @shield_charge_rate = @status_report.hash[:shield_rate].to_f
159
+ @shield_max = @status_report.hash[:max_shield].to_f
160
+ @shield = @status_report.hash[:shield].to_f
161
+ else
162
+ # TODO Need translation
163
+ @shield_status = "Offline"
164
+ @states[:shields] = "Offline"
165
+ @shield_charge_rate = nil
166
+ @shield_max = @status_report.hash[:max_shield].to_f
167
+ @shield = 0
168
+ end
169
+
170
+ # Colonists
171
+ colonists_line = lines.detect do |line|
172
+ line.strip.start_with?("Colonists")
173
+ end
174
+
175
+ if colonists_line
176
+ # TODO Need translation
177
+ @states[:colonists] = "Crewed"
178
+ @colonists_status =
179
+ ResponseParser.substitute_line_values(line: colonists_line)
180
+ hash = ResponseParser.hash_from_line_values(line: colonists_line)
181
+ @colonists = hash[:crew].to_i
182
+ else
183
+ # TODO Need translation
184
+ @states[:colonists] = "Unmanned"
185
+ @colonists_status = "Unmanned"
186
+ @colonists = 0
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,26 @@
1
+
2
+ module TFClient
3
+ module Models
4
+
5
+ STATUS_BEGIN = "STATUSREPORT BEGIN".freeze
6
+ STATUS_END = "STATUSREPORT END".freeze
7
+
8
+ class StatusReport
9
+
10
+ attr_reader :hash
11
+
12
+ def initialize(lines:)
13
+ if lines[0] != STATUS_BEGIN
14
+ raise "Expected lines[0] to be == #{STATUS_BEGIN}, found: #{lines[0]}"
15
+ end
16
+
17
+ @hash = {}
18
+ lines.each do |line|
19
+ break if line == STATUS_END
20
+ tokens = line.strip.split(": ")
21
+ @hash[tokens[0].to_sym] = tokens[1]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,200 @@
1
+
2
+ module TFClient
3
+ class ResponseParser
4
+
5
+ FIELD_DELIMITER = "|".freeze
6
+ VARIABLE_REGEX = /(\{[a-z_]+\}+)/.freeze
7
+
8
+ def self.substitute_line_values(line:)
9
+ return line.chomp if !line[/\|/]
10
+ tokens = line.chomp.split("|")
11
+
12
+ translation = tokens[1]
13
+
14
+ matches = translation.scan(VARIABLE_REGEX)
15
+
16
+ return translation if matches.empty?
17
+
18
+ values = self.hash_from_line_values(line: line.chomp)
19
+
20
+ with_substitutes = translation.chomp
21
+
22
+ matches.each do |match|
23
+ key = match[0].sub("{", "").sub("}", "").to_sym
24
+ with_substitutes.gsub!(match[0], values[key])
25
+ end
26
+
27
+ with_substitutes
28
+ end
29
+
30
+ def self.substitute_values(lines:)
31
+ lines.map do |line|
32
+ self.substitute_line_values(line:line.chomp)
33
+ end
34
+ end
35
+
36
+ def self.tokenize_line(line:)
37
+ lines = line.split(FIELD_DELIMITER)
38
+ stripped = []
39
+ lines.each_with_index do |line, index|
40
+ if index == 0
41
+ stripped << line
42
+ else
43
+ stripped << line.strip
44
+ end
45
+ end
46
+ stripped
47
+ end
48
+
49
+ # returns two values
50
+ def self.line_and_index_for_beginning_with(lines:, string:)
51
+ lines.each_with_index do |line, index|
52
+ return line.chomp, index if line.start_with?(string)
53
+ end
54
+ return nil, -1
55
+ end
56
+
57
+ # Returns a hash of the key=value pairs found at the end of lines
58
+ def self.hash_from_line_values(line:)
59
+ tokens = self.tokenize_line(line: line)[2..-1]
60
+ hash = {}
61
+ tokens.each do |token|
62
+ key_value = token.split("=")
63
+ hash[key_value[0].to_sym] = key_value[1]
64
+ end
65
+ hash
66
+ end
67
+
68
+ def self.is_list_item?(line:)
69
+ if line && line.length != 0 && line.start_with?("\t")
70
+ true
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ def self.collect_list_items(lines:, start_index:)
77
+ items = []
78
+ index = start_index
79
+ loop do
80
+ line = lines[index]
81
+ if self.is_list_item?(line: line)
82
+ items << line.strip
83
+ index = index + 1
84
+ else
85
+ break
86
+ end
87
+ end
88
+ items
89
+ end
90
+
91
+ def self.label_and_translation(tokens:)
92
+ if tokens[0][/Claimed by/]
93
+ {label: "Claimed by", translation: tokens[1].split("'")[0].strip}
94
+ else
95
+ {label: tokens[0].split(":")[0], translation: tokens[1].split(":")[0] }
96
+ end
97
+ end
98
+
99
+ def self.camel_case_from_string(string:)
100
+ string.split(" ").map do |token|
101
+ token.capitalize
102
+ end.join("")
103
+ end
104
+
105
+ def self.snake_case_sym_from_string(string:)
106
+ string.split(" ").map do |token|
107
+ token.downcase
108
+ end.join("_").to_sym
109
+ end
110
+
111
+ def self.model_class_from_string(string:)
112
+ if !TFClient::Models.constants.include?(string.to_sym)
113
+ return nil
114
+ end
115
+
116
+ "TFClient::Models::#{string}".split("::").reduce(Object) do |obj, cls|
117
+ obj.const_get(cls)
118
+ end
119
+ end
120
+
121
+ attr_reader :command
122
+ attr_reader :textflight_command
123
+ attr_reader :response
124
+ attr_reader :lines
125
+
126
+ def initialize(command:, textflight_command:, response:)
127
+ @command = command
128
+ @textflight_command = textflight_command
129
+ @response = response
130
+ end
131
+
132
+ def parse
133
+ @lines = @response.lines(chomp: true).reject { |line| line.length == 0 }
134
+ case @textflight_command
135
+ when "nav"
136
+ parse_nav(command: @command)
137
+ when "scan"
138
+ parse_scan
139
+ when "status"
140
+ parse_status(command: @command)
141
+ else
142
+ if @response[/#{Models::STATUS_BEGIN}/]
143
+ @response = @lines[0].chomp
144
+ @lines = [@response]
145
+ end
146
+
147
+ puts ResponseParser.substitute_values(lines: @lines).join("\n")
148
+ end
149
+ end
150
+
151
+ def parse_nav(command:)
152
+ nav = TFClient::Models::Nav.new(lines: lines)
153
+ if command != "nav-for-prompt"
154
+ puts nav.response
155
+ end
156
+ nav
157
+ end
158
+
159
+ def parse_scan
160
+ scan = TFClient::Models::Scan.new(lines: lines)
161
+ puts scan.response
162
+ scan
163
+ end
164
+
165
+ def parse_status(command:)
166
+ if command == "status-for-prompt"
167
+ TFClient::Models::Status.new(lines: lines)
168
+ else
169
+ _, index_start =
170
+ ResponseParser.line_and_index_for_beginning_with(
171
+ lines: @lines,
172
+ string: Models::STATUS_BEGIN
173
+ )
174
+ if index_start == -1
175
+ puts ResponseParser.substitute_values(lines: @lines).join("\n")
176
+ end
177
+
178
+ _, index_end =
179
+ ResponseParser.line_and_index_for_beginning_with(
180
+ lines: @lines,
181
+ string: Models::STATUS_END
182
+ )
183
+
184
+ if index_start != 0
185
+ lines_before_status = @lines[0..index_start - 1]
186
+ puts ResponseParser.substitute_values(
187
+ lines: lines_before_status
188
+ ).join("\n")
189
+ else
190
+ lines_after_status = @lines[index_end + 1..-1]
191
+ puts ResponseParser.substitute_values(
192
+ lines: lines_after_status
193
+ ).join("\n")
194
+
195
+ Models::StatusReport.new(lines: @lines[index_start...index_end])
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end