ssc.bot 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,283 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # This file is part of SSC.Bot.
7
+ # Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
8
+ #
9
+ # SSC.Bot is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU Lesser General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # SSC.Bot is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU Lesser General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU Lesser General Public License
20
+ # along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
21
+ #++
22
+
23
+
24
+ require 'attr_bool'
25
+ require 'set'
26
+
27
+ require 'ssc.bot/ssc_file'
28
+
29
+ require 'ssc.bot/chat_log/message'
30
+ require 'ssc.bot/chat_log/message_parsable'
31
+ require 'ssc.bot/chat_log/message_parser'
32
+ require 'ssc.bot/chat_log/messages'
33
+
34
+
35
+ module SSCBot
36
+ ###
37
+ # @author Jonathan Bradley Whited (@esotericpig)
38
+ # @since 0.1.0
39
+ ###
40
+ class ChatLog
41
+ include MessageParsable
42
+
43
+ attr_reader? :alive
44
+ attr_accessor :file_mode
45
+ attr_reader :file_opt
46
+ attr_accessor :filename
47
+ attr_accessor :idle_secs
48
+ attr_reader :observers
49
+ attr_reader :thread
50
+
51
+ def initialize(filename,file_mode: 'rt',file_opt: {},idle_secs: 0.250,**parser_kargs)
52
+ super()
53
+
54
+ @alive = false
55
+ @file_mode = file_mode
56
+ @file_opt = file_opt
57
+ @filename = filename
58
+ @idle_secs = idle_secs
59
+ @observers = {}
60
+ @parser = MessageParser.new(**parser_kargs)
61
+ @semaphore = Mutex.new()
62
+ @thread = nil
63
+ end
64
+
65
+ def add_observer(observer=nil,*funcs,type: :any,&block)
66
+ if observer.nil?() && block.nil?()
67
+ raise ArgumentError,'no observer'
68
+ end
69
+
70
+ check_type(type)
71
+
72
+ type_observers = fetch_observers(type: type)
73
+
74
+ if !observer.nil?()
75
+ funcs << :call if funcs.empty?()
76
+
77
+ type_observers << Observer.new(observer,*funcs)
78
+ end
79
+
80
+ if !block.nil?()
81
+ type_observers << Observer.new(block,:call)
82
+ end
83
+ end
84
+
85
+ def add_observers(*observers,type: :any,func: :call,&block)
86
+ if observers.empty?() && block.nil?()
87
+ raise ArgumentError,'no observer'
88
+ end
89
+
90
+ check_type(type)
91
+
92
+ type_observers = fetch_observers(type: type)
93
+
94
+ observers.each() do |observer|
95
+ type_observers << Observer.new(observer,func)
96
+ end
97
+
98
+ if !block.nil?()
99
+ type_observers << Observer.new(block,:call)
100
+ end
101
+ end
102
+
103
+ def check_type(type,nil_ok: false)
104
+ if type.nil?()
105
+ if !nil_ok
106
+ raise ArgumentError,"invalid type{#{type.inspect()}}"
107
+ end
108
+ else
109
+ if type != :any && !Message.valid_type?(type)
110
+ raise ArgumentError,"invalid type{#{type.inspect()}}"
111
+ end
112
+ end
113
+ end
114
+
115
+ def clear_content()
116
+ SSCFile.clear(@filename)
117
+ end
118
+
119
+ def count_observers(type: nil)
120
+ check_type(type,nil_ok: true)
121
+
122
+ count = 0
123
+
124
+ if type.nil?()
125
+ @observers.each_value() do |type_observers|
126
+ count += type_observers.length
127
+ end
128
+ else
129
+ type_observers = @observers[type]
130
+
131
+ if !type_observers.nil?()
132
+ count += type_observers.length
133
+ end
134
+ end
135
+
136
+ return count
137
+ end
138
+
139
+ def delete_observer(observer,type: nil)
140
+ delete_observers(observer,type: type)
141
+ end
142
+
143
+ def delete_observers(*observers,type: nil)
144
+ check_type(type,nil_ok: true)
145
+
146
+ if observers.empty?()
147
+ if type.nil?()
148
+ @observers.clear()
149
+ else
150
+ type_observers = @observers[type]
151
+
152
+ if !type_observers.nil?()
153
+ type_observers.clear()
154
+ end
155
+ end
156
+ else
157
+ observers = observers.to_set()
158
+
159
+ if type.nil?()
160
+ @observers.each_value() do |type_observers|
161
+ type_observers.delete_if() do |observer|
162
+ observers.include?(observer.object)
163
+ end
164
+ end
165
+ else
166
+ type_observers = @observers[type]
167
+
168
+ if !type_observers.nil?()
169
+ type_observers.delete_if() do |observer|
170
+ observers.include?(observer.object)
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def fetch_observers(type: :any)
178
+ check_type(type)
179
+
180
+ type_observers = @observers[type]
181
+
182
+ if type_observers.nil?()
183
+ type_observers = []
184
+ @observers[type] = type_observers
185
+ end
186
+
187
+ return type_observers
188
+ end
189
+
190
+ def notify_observers(message)
191
+ any_observers = @observers[:any]
192
+ type_observers = @observers[message.type]
193
+
194
+ if !any_observers.nil?()
195
+ any_observers.each() do |observer|
196
+ observer.notify(self,message)
197
+ end
198
+ end
199
+
200
+ if !type_observers.nil?()
201
+ type_observers.each() do |observer|
202
+ observer.notify(self,message)
203
+ end
204
+ end
205
+ end
206
+
207
+ def run(seek_to_end: true)
208
+ @semaphore.synchronize() do
209
+ return if @alive # Already running
210
+ end
211
+
212
+ stop() # Justin Case
213
+
214
+ @semaphore.synchronize() do
215
+ @alive = true
216
+
217
+ soft_touch() # Create the file if it doesn't exist
218
+
219
+ @thread = Thread.new() do
220
+ SSCFile.open(@filename,@file_mode,**@file_opt) do |fin|
221
+ fin.seek_to_end() if seek_to_end
222
+
223
+ while @alive
224
+ while !(line = fin.get_line()).nil?()
225
+ message = @parser.parse(line)
226
+
227
+ notify_observers(message)
228
+ end
229
+
230
+ sleep(@idle_secs)
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ def soft_touch()
238
+ SSCFile.soft_touch(@filename)
239
+ end
240
+
241
+ def stop(wait_secs=5)
242
+ @semaphore.synchronize() do
243
+ @alive = false
244
+
245
+ if !@thread.nil?()
246
+ if @thread.alive?()
247
+ # First, try to kill it gracefully (waiting X secs).
248
+ @thread.join(@idle_secs + wait_secs)
249
+
250
+ # Die!
251
+ @thread.kill() if @thread.alive?()
252
+ end
253
+
254
+ @thread = nil
255
+ end
256
+ end
257
+ end
258
+
259
+ ###
260
+ # @author Jonathan Bradley Whited (@esotericpig)
261
+ # @since 0.1.0
262
+ ###
263
+ class Observer
264
+ attr_reader :funcs
265
+ attr_reader :object
266
+
267
+ def initialize(object,*funcs)
268
+ super()
269
+
270
+ raise ArgumentError,'empty funcs' if funcs.empty?()
271
+
272
+ @funcs = funcs
273
+ @object = object
274
+ end
275
+
276
+ def notify(chatlog,message)
277
+ @funcs.each() do |func|
278
+ @object.__send__(func,chatlog,message)
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # This file is part of SSC.Bot.
7
+ # Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
8
+ #
9
+ # SSC.Bot is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU Lesser General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # SSC.Bot is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU Lesser General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU Lesser General Public License
20
+ # along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
21
+ #++
22
+
23
+
24
+ require 'set'
25
+
26
+
27
+ module SSCBot
28
+ class ChatLog
29
+ ###
30
+ # The base class of all parsed messages from a chat log file.
31
+ #
32
+ # @author Jonathan Bradley Whited (@esotericpig)
33
+ # @since 0.1.0
34
+ ###
35
+ class Message
36
+ # Valid types of messages.
37
+ #
38
+ # You can add your own custom type(s) that you parse manually:
39
+ # SSCBot::ChatLog::Message::TYPES.add(:custom)
40
+ TYPES = Set[
41
+ # In order of F1 Help box.
42
+ *%i{
43
+ pub team private remote freq chat
44
+ ?lines ?namelen ?ignore ?nopubchat ?obscene ?away ?log ?logbuffer
45
+ ?kill kill ?enter enter ?leave leave ?message ?messages ?chat
46
+ ?status ?scorereset ?team ?spec ?target ?time ?flags ?score ?crown
47
+ ?best ?buy
48
+ ?owner ?password ?usage ?userid ?find ?ping ?packetloss ?lag ?music
49
+ ?sound ?alarm ?sheep ?getnews
50
+ ?squadowner ?squad ?squadlist ?loadmacro ?savemacro
51
+ unknown
52
+ },
53
+ ]
54
+
55
+ # @param type [Symbol] the type to check if valid
56
+ # @return [Boolean] +true+ if +type+ is one of {TYPES}, else +false+
57
+ def self.valid_type?(type)
58
+ return TYPES.include?(type)
59
+ end
60
+
61
+ attr_reader :line # @return [String] the raw (unparsed) line from the file
62
+ attr_reader :type # @return [Symbol] what type of message this is; one of {TYPES}
63
+
64
+ # @param line [String] the raw (unparsed) line from the file
65
+ # @param type [Symbol] what type of message this is; must be one of {TYPES}
66
+ def initialize(line,type:)
67
+ type = type.to_sym()
68
+
69
+ raise ArgumentError,"invalid line{#{line.inspect()}}" if line.nil?()
70
+ raise ArgumentError,"invalid type{#{type.inspect()}}" if !self.class.valid_type?(type)
71
+
72
+ @line = line
73
+ @type = type
74
+ end
75
+
76
+ # A convenience method for comparing anything that responds to
77
+ # +:to_sym():+, like +String+.
78
+ #
79
+ # @param type [String,Symbol] the type to convert & compare against
80
+ # @return [Boolean] +true+ if this message is of type +type+, else +false+
81
+ def type?(type)
82
+ return @type == type.to_sym()
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # This file is part of SSC.Bot.
7
+ # Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
8
+ #
9
+ # SSC.Bot is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU Lesser General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # SSC.Bot is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU Lesser General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU Lesser General Public License
20
+ # along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
21
+ #++
22
+
23
+
24
+ require 'forwardable'
25
+
26
+ require 'ssc.bot/chat_log/message_parser'
27
+
28
+
29
+ module SSCBot
30
+ class ChatLog
31
+ ###
32
+ # @author Jonathan Bradley Whited (@esotericpig)
33
+ # @since 0.1.0
34
+ ###
35
+ module MessageParsable
36
+ extend Forwardable
37
+
38
+ MAX_NAMELEN = MessageParser::MAX_NAMELEN
39
+
40
+ def_delegators(:@parser,
41
+ :autoset_namelen?,:autoset_namelen=,
42
+ :check_history_count,:check_history_count=,
43
+ :commands,
44
+ :messages,
45
+ :namelen,:namelen=,
46
+ :regex_cache,
47
+ :store_history?,:store_history=,
48
+ :strict?,:strict=,
49
+
50
+ :parse,
51
+
52
+ :clear_history,
53
+ :reset_namelen,
54
+ :store_command,
55
+
56
+ :command?,
57
+ )
58
+
59
+ attr_reader :parser
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,562 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+ # frozen_string_literal: true
4
+
5
+ #--
6
+ # This file is part of SSC.Bot.
7
+ # Copyright (c) 2020 Jonathan Bradley Whited (@esotericpig)
8
+ #
9
+ # SSC.Bot is free software: you can redistribute it and/or modify
10
+ # it under the terms of the GNU Lesser General Public License as published by
11
+ # the Free Software Foundation, either version 3 of the License, or
12
+ # (at your option) any later version.
13
+ #
14
+ # SSC.Bot is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ # GNU Lesser General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU Lesser General Public License
20
+ # along with SSC.Bot. If not, see <https://www.gnu.org/licenses/>.
21
+ #++
22
+
23
+
24
+ require 'attr_bool'
25
+ require 'set'
26
+
27
+ require 'ssc.bot/error'
28
+ require 'ssc.bot/util'
29
+
30
+ require 'ssc.bot/chat_log/message'
31
+ require 'ssc.bot/chat_log/messages'
32
+
33
+
34
+ module SSCBot
35
+ class ChatLog
36
+ ###
37
+ # @author Jonathan Bradley Whited (@esotericpig)
38
+ # @since 0.1.0
39
+ ###
40
+ class MessageParser
41
+ MAX_NAMELEN = 24
42
+
43
+ attr_accessor? :autoset_namelen
44
+ attr_accessor :check_history_count
45
+ attr_reader :commands
46
+ attr_reader :messages
47
+ attr_accessor :namelen
48
+ attr_reader :regex_cache
49
+ attr_accessor? :store_history
50
+ attr_accessor? :strict
51
+
52
+ def initialize(autoset_namelen: true,check_history_count: 5,namelen: nil,store_history: true,strict: true)
53
+ super()
54
+
55
+ @autoset_namelen = autoset_namelen
56
+ @check_history_count = check_history_count
57
+ @commands = {}
58
+ @messages = []
59
+ @namelen = namelen
60
+ @regex_cache = {}
61
+ @store_history = store_history
62
+ @strict = strict
63
+ end
64
+
65
+ # The Ruby interpreter should cache the args' default values,
66
+ # so no reason to manually cache them unless a variable is involved inside.
67
+ #
68
+ # @example Default Format
69
+ # 'X Name> Message'
70
+ def match_player(line,type_name:,name_prefix: '',name_suffix: '> ',type_prefix: %r{..},use_namelen: true)
71
+ cached_regex = @regex_cache[type_name]
72
+
73
+ if cached_regex.nil?()
74
+ cached_regex = {}
75
+ @regex_cache[type_name] = cached_regex
76
+ end
77
+
78
+ if use_namelen && !@namelen.nil?()
79
+ regex = cached_regex[@namelen]
80
+
81
+ if regex.nil?()
82
+ name_prefix = Util.quote_str_or_regex(name_prefix)
83
+ name_suffix = Util.quote_str_or_regex(name_suffix)
84
+ type_prefix = Util.quote_str_or_regex(type_prefix)
85
+
86
+ # Be careful to not use spaces ' ', but to use '\\ ' (or '\s') instead
87
+ # because of the '/x' option.
88
+ regex = /
89
+ \A#{type_prefix}
90
+ #{name_prefix}(?<name>.{#{@namelen}})#{name_suffix}
91
+ (?<message>.*)\z
92
+ /x
93
+
94
+ cached_regex[@namelen] = regex
95
+ end
96
+ else
97
+ regex = cached_regex[:no_namelen]
98
+
99
+ if regex.nil?()
100
+ name_prefix = Util.quote_str_or_regex(name_prefix)
101
+ name_suffix = Util.quote_str_or_regex(name_suffix)
102
+ type_prefix = Util.quote_str_or_regex(type_prefix)
103
+
104
+ # Be careful to not use spaces ' ', but to use '\\ ' (or '\s') instead
105
+ # because of the '/x' option.
106
+ regex = /
107
+ \A#{type_prefix}
108
+ #{name_prefix}(?<name>.*?\S)#{name_suffix}
109
+ (?<message>.*)\z
110
+ /x
111
+
112
+ cached_regex[:no_namelen] = regex
113
+ end
114
+ end
115
+
116
+ return regex.match(line)
117
+ end
118
+
119
+ def parse(line)
120
+ if line.nil?()
121
+ if @strict
122
+ raise ArgumentError,"invalid line{#{line.inspect()}}"
123
+ else
124
+ line = ''
125
+ end
126
+ end
127
+
128
+ message = nil
129
+
130
+ if !line.empty?()
131
+ case line[0]
132
+ when 'C'
133
+ message = parse_chat(line)
134
+ when 'E'
135
+ message = parse_freq(line)
136
+ when 'P'
137
+ if (match = match_remote?(line))
138
+ message = parse_remote(line,match: match)
139
+ else
140
+ message = parse_private(line)
141
+ end
142
+ when 'T'
143
+ message = parse_team(line)
144
+ else
145
+ # Check this one first to prevent abuse from pubbers.
146
+ if (match = match_pub?(line))
147
+ message = parse_pub(line,match: match)
148
+ else
149
+ if (match = match_kill?(line))
150
+ message = parse_kill(line,match: match)
151
+ elsif (match = match_q_log?(line))
152
+ message = parse_q_log(line,match: match)
153
+ elsif (match = match_q_namelen?(line))
154
+ message = parse_q_namelen(line,match: match)
155
+ else
156
+ # These are last because too flexible.
157
+ if (match = match_q_find?(line))
158
+ message = parse_q_find(line,match: match)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ if message.nil?()
166
+ message = Message.new(line,type: :unknown)
167
+ end
168
+
169
+ if @store_history
170
+ @messages << message
171
+ end
172
+
173
+ return message
174
+ end
175
+
176
+ # @example Format
177
+ # # NOT affected by namelen.
178
+ # 'C 1:Name> Message'
179
+ def parse_chat(line)
180
+ match = match_player(line,type_name: :chat,name_prefix: %r{(?<channel>\d+)\:},use_namelen: false)
181
+ player = parse_player(line,type_name: :chat,match: match)
182
+
183
+ return nil if player.nil?()
184
+
185
+ channel = match[:channel].to_i()
186
+
187
+ return ChatMessage.new(line,channel: channel,name: player.name,message: player.message)
188
+ end
189
+
190
+ # @example Format
191
+ # 'E Name> Message'
192
+ def parse_freq(line)
193
+ player = parse_player(line,type_name: :freq)
194
+
195
+ return nil if player.nil?()
196
+
197
+ return FreqMessage.new(line,name: player.name,message: player.message)
198
+ end
199
+
200
+ # @example Format
201
+ # ' Killed.Name(100) killed by: Killer.Name'
202
+ def parse_kill(line,match:)
203
+ if match.nil?()
204
+ if @strict
205
+ raise ParseError,"invalid kill message{#{line}}"
206
+ else
207
+ return nil
208
+ end
209
+ end
210
+
211
+ killed = match[:killed]
212
+ bounty = match[:bounty].to_i()
213
+ killer = match[:killer]
214
+
215
+ return KillMessage.new(line,killed: killed,bounty: bounty,killer: killer)
216
+ end
217
+
218
+ # @example Default Format
219
+ # 'X Name> Message'
220
+ def parse_player(line,type_name:,match: :default)
221
+ if match.nil?()
222
+ if @strict
223
+ raise ParseError,"invalid #{type_name} message{#{line}}"
224
+ else
225
+ return nil
226
+ end
227
+ elsif match == :default
228
+ # Use type_name of :player (not passed in param) for regex_cache.
229
+ match = match_player(line,type_name: :player)
230
+ end
231
+
232
+ name = Util.u_lstrip(match[:name])
233
+ message = match[:message]
234
+
235
+ if name.empty?() || name.length > MAX_NAMELEN
236
+ if @strict
237
+ raise ParseError,"invalid player name for #{type_name} message{#{line}}"
238
+ else
239
+ return nil
240
+ end
241
+ end
242
+
243
+ return PlayerMessage.new(line,type: :unknown,name: name,message: message)
244
+ end
245
+
246
+ # @example Format
247
+ # 'P Name> Message'
248
+ def parse_private(line)
249
+ player = parse_player(line,type_name: :private)
250
+
251
+ return nil if player.nil?()
252
+
253
+ return PrivateMessage.new(line,name: player.name,message: player.message)
254
+ end
255
+
256
+ # @example Format
257
+ # ' Name> Message'
258
+ def parse_pub(line,match:)
259
+ player = parse_player(line,type_name: :pub,match: match)
260
+
261
+ return nil if player.nil?()
262
+
263
+ cmd = Util.u_strip(player.message).downcase()
264
+
265
+ if cmd.start_with?('?find')
266
+ store_command(:pub,%s{?find})
267
+ end
268
+
269
+ return PubMessage.new(line,name: player.name,message: player.message)
270
+ end
271
+
272
+ # @example Format
273
+ # ' Not online, last seen more than 10 days ago'
274
+ # ' Not online, last seen 9 days ago'
275
+ # ' Not online, last seen 18 hours ago'
276
+ # ' Not online, last seen 0 hours ago'
277
+ # ' Name - Public 0'
278
+ # ' TWCore - (Private arena)'
279
+ # ' Name is in SSCJ Devastation'
280
+ # ' Name is in SSCC Metal Gear CTF'
281
+ def parse_q_find(line,match:)
282
+ if match.nil?()
283
+ if @strict
284
+ raise ParseError,"invalid ?find message{#{line}}"
285
+ else
286
+ return nil
287
+ end
288
+ end
289
+
290
+ caps = match.named_captures
291
+ q_find = nil
292
+
293
+ if (days = caps['days'])
294
+ more = caps.key?('more')
295
+ days = days.to_i()
296
+
297
+ q_find = QFindMessage.new(line,find_type: :days,more: more,days: days)
298
+ elsif (hours = caps['hours'])
299
+ hours = hours.to_i()
300
+
301
+ q_find = QFindMessage.new(line,find_type: :hours,hours: hours)
302
+ elsif (player = caps['player'])
303
+ if (arena = caps['arena'])
304
+ private = (arena == '(Private arena)')
305
+
306
+ q_find = QFindMessage.new(line,find_type: :arena,player: player,arena: arena,private: private)
307
+ elsif (zone = caps['zone'])
308
+ q_find = QFindMessage.new(line,find_type: :zone,player: player,zone: zone)
309
+ end
310
+ end
311
+
312
+ if q_find.nil?() && @strict
313
+ raise ParseError,"invalid ?find message{#{line}}"
314
+ end
315
+
316
+ return q_find
317
+ end
318
+
319
+ # @example Format
320
+ # ' Log file open: session.log'
321
+ # ' Log file closed'
322
+ def parse_q_log(line,match:)
323
+ if match.nil?()
324
+ if @strict
325
+ raise ParseError,"invalid ?log message{#{line}}"
326
+ else
327
+ return nil
328
+ end
329
+ end
330
+
331
+ filename = match.named_captures['filename']
332
+ log_type = filename.nil?() ? :close : :open
333
+
334
+ return QLogMessage.new(line,log_type: log_type,filename: filename)
335
+ end
336
+
337
+ # @example Format
338
+ # ' Message Name Length: 24'
339
+ def parse_q_namelen(line,match:)
340
+ if match.nil?()
341
+ if @strict
342
+ raise ParseError,"invalid ?namelen message{#{line}}"
343
+ else
344
+ return nil
345
+ end
346
+ end
347
+
348
+ namelen = match[:namelen].to_i()
349
+
350
+ if namelen < 1
351
+ if @strict
352
+ raise ParseError,"invalid namelen for ?namelen message{#{line}}"
353
+ else
354
+ return nil
355
+ end
356
+ end
357
+
358
+ if @autoset_namelen
359
+ @namelen = namelen
360
+ end
361
+
362
+ return QNamelenMessage.new(line,namelen: namelen)
363
+ end
364
+
365
+ # @example Format
366
+ # # NOT affected by namelen.
367
+ # 'P :Self.Name:Message'
368
+ # 'P (Name)>Message'
369
+ def parse_remote(line,match:)
370
+ player = parse_player(line,type_name: 'remote private',match: match)
371
+
372
+ return nil if player.nil?()
373
+
374
+ own = (line[2] == ':')
375
+ squad = (player.name[0] == '#')
376
+
377
+ return RemoteMessage.new(line,
378
+ own: own,squad: squad,
379
+ name: player.name,message: player.message,
380
+ )
381
+ end
382
+
383
+ # @example Format
384
+ # 'T Name> Message'
385
+ def parse_team(line)
386
+ player = parse_player(line,type_name: :team)
387
+
388
+ return nil if player.nil?()
389
+
390
+ return TeamMessage.new(line,name: player.name,message: player.message)
391
+ end
392
+
393
+ def clear_history()
394
+ @messages.clear()
395
+ end
396
+
397
+ def reset_namelen()
398
+ @namelen = nil
399
+ end
400
+
401
+ def store_command(type,name)
402
+ type_hash = @commands[type]
403
+
404
+ if type_hash.nil?()
405
+ type_hash = {}
406
+ @commands[type] = type_hash
407
+ end
408
+
409
+ type_hash[name] = @messages.length # Index of when command was found/stored
410
+ end
411
+
412
+ def command?(type,name,delete: true)
413
+ return true if @check_history_count < 1
414
+
415
+ type_hash = @commands[type]
416
+
417
+ if !type_hash.nil?()
418
+ index = type_hash[name]
419
+
420
+ if !index.nil?() && (@messages.length - index) <= @check_history_count
421
+ type_hash.delete(name) if delete
422
+
423
+ return true
424
+ end
425
+ end
426
+
427
+ return false
428
+ end
429
+
430
+ # @example Format
431
+ # ' Killed.Name(100) killed by: Killer.Name'
432
+ def match_kill?(line)
433
+ return false if line.length < 19 # ' N(0) killed by: N'
434
+
435
+ return /\A (?<killed>.*?\S)\((?<bounty>\d+)\) killed by: (?<killer>.*?\S)\z/.match(line)
436
+ end
437
+
438
+ # @example Format
439
+ # ' Name> Message'
440
+ def match_pub?(line)
441
+ return false if line.length < 5 # ' N> '
442
+
443
+ match = match_player(line,type_name: :pub,type_prefix: ' ')
444
+
445
+ if !match.nil?()
446
+ name = Util.u_lstrip(match[:name])
447
+
448
+ if name.empty?() || name.length > MAX_NAMELEN
449
+ return false
450
+ end
451
+ end
452
+
453
+ return match
454
+ end
455
+
456
+ # @example Format
457
+ # ' Not online, last seen more than 10 days ago'
458
+ # ' Not online, last seen 9 days ago'
459
+ # ' Not online, last seen 18 hours ago'
460
+ # ' Not online, last seen 0 hours ago'
461
+ # ' Name - Public 0'
462
+ # ' TWCore - (Private arena)'
463
+ # ' Name is in SSCJ Devastation'
464
+ # ' Name is in SSCC Metal Gear CTF'
465
+ def match_q_find?(line)
466
+ return false if line.length < 7 # ' N - A'
467
+ return false unless command?(:pub,%s{?find})
468
+
469
+ if line.start_with?(' Not online, last seen ')
470
+ match = line.match(/(?<more>more) than (?<days>\d+) days ago\z/)
471
+
472
+ if match.nil?()
473
+ match = line.match(/(?<days>\d+) days? ago\z/)
474
+ end
475
+
476
+ if match.nil?()
477
+ match = line.match(/(?<hours>\d+) hours? ago\z/)
478
+ end
479
+
480
+ return match
481
+ else
482
+ match = line.match(/\A (?<player>.+) is in (?<zone>.+)\z/)
483
+
484
+ if match.nil?()
485
+ match = line.match(/\A (?<player>.+) - (?<arena>.+)\z/)
486
+ end
487
+
488
+ if match
489
+ caps = match.named_captures
490
+
491
+ player = caps['player']
492
+
493
+ if player.length > MAX_NAMELEN
494
+ return false
495
+ end
496
+
497
+ if caps.key?('arena')
498
+ area = caps['arena']
499
+ elsif caps.key?('zone')
500
+ area = caps['zone']
501
+ else
502
+ return false
503
+ end
504
+
505
+ [player[0],player[-1],area[0],area[-1]].each() do |c|
506
+ if c =~ /[[:space:]]/
507
+ return false
508
+ end
509
+ end
510
+
511
+ return match
512
+ end
513
+ end
514
+
515
+ return false
516
+ end
517
+
518
+ # @example Format
519
+ # ' Log file open: session.log'
520
+ # ' Log file closed'
521
+ def match_q_log?(line)
522
+ return false if line.length < 17
523
+
524
+ match = /\A Log file open: (?<filename>.+)\z/.match(line)
525
+
526
+ if match.nil?()
527
+ match = /\A Log file closed\z/.match(line)
528
+ end
529
+
530
+ return match
531
+ end
532
+
533
+ # @example Format
534
+ # ' Message Name Length: 24'
535
+ def match_q_namelen?(line)
536
+ return false if line.length < 24 # '...: 0'
537
+ return false if line[21] != ':'
538
+
539
+ return /\A Message Name Length: (?<namelen>\d+)\z/.match(line)
540
+ end
541
+
542
+ # @example Format
543
+ # # NOT affected by namelen.
544
+ # 'P :Self.Name:Message'
545
+ # 'P (Name)>Message'
546
+ def match_remote?(line)
547
+ return false if line.length < 5 # 'P :N:'
548
+
549
+ case line[2]
550
+ when ':'
551
+ return match_player(line,type_name: %s{remote.out},
552
+ name_prefix: ':',name_suffix: ':',use_namelen: false)
553
+ when '('
554
+ return match_player(line,type_name: %s{remote.in},
555
+ name_prefix: '(',name_suffix: ')>',use_namelen: false)
556
+ end
557
+
558
+ return false
559
+ end
560
+ end
561
+ end
562
+ end