textflight-client 1.0.1

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,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