nmspec 1.5.0.pre → 1.5.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,392 @@
1
+ require 'set'
2
+
3
+ # Nmspec code generator for ruby
4
+ module Nmspec
5
+ module GDScript
6
+ class << self
7
+ def gen(spec)
8
+ big_endian = spec.dig(:msgr, :bigendian)
9
+ nodelay = spec.dig(:msgr, :nodelay)
10
+
11
+ code = []
12
+ code << '##'
13
+ code << '# NOTE: this code is auto-generated from an nmspec file'
14
+
15
+ if spec.dig(:msgr, :desc)
16
+ code << '#'
17
+ code << "# #{spec.dig(:msgr, :desc)}"
18
+ end
19
+
20
+ code << 'extends Reference'
21
+ code << ''
22
+ code << "class_name #{_class_name_from_msgr_name(spec.dig(:msgr, :name))}"
23
+ code << ''
24
+
25
+ if (spec[:protos]&.length || 0) > 0
26
+ code << _opcode_mappings(spec[:protos])
27
+ code << ''
28
+ end
29
+
30
+ code << '###########################################'
31
+ code << '# setup'
32
+ code << 'var socket = null'
33
+ code << ''
34
+ code << _init(big_endian, nodelay)
35
+ code << ''
36
+ code << _connected
37
+ code << ''
38
+ code << _errored
39
+ code << ''
40
+ code << _has_bytes
41
+ code << ''
42
+ code << _ready_bytes
43
+ code << ''
44
+ code << _close
45
+ code << ''
46
+
47
+ code << _bool_type
48
+ code << ''
49
+ code << _str_types
50
+ code << ''
51
+ code << _list_types
52
+
53
+ subtypes = spec[:types].select{|t| !t[:base_type].nil? }
54
+ code << _protos_methods(spec[:protos], subtypes)
55
+
56
+ code.join("\n")
57
+ rescue => e
58
+ "Code generation failed due to unknown error: check spec validity\n cause: #{e.inspect}"
59
+ puts e.backtrace.join("\n ")
60
+ end
61
+
62
+ def _class_name_from_msgr_name(name)
63
+ name
64
+ .downcase
65
+ .gsub(/[\._\-]/, ' ')
66
+ .split(' ')
67
+ .map{|part| part.capitalize}
68
+ .join + 'Msgr'
69
+ end
70
+
71
+ def _opcode_mappings(protos)
72
+ code = []
73
+
74
+ code << 'const PROTO_TO_OP = {'
75
+ code += protos.map.with_index{|p, i| "\t'#{p[:name]}': #{i}," }
76
+ code << '}'
77
+
78
+ code << ''
79
+
80
+ code << 'const OP_TO_PROTO = {'
81
+ code += protos.map.with_index{|p, i| "\t#{i}: '#{p[:name]}'," }
82
+ code << '}'
83
+
84
+ code
85
+ end
86
+
87
+ def _init(big_endian, nodelay)
88
+ code = []
89
+
90
+ code << '# WARN: Messengers in GDScript assume big_endian byte order'
91
+ code << '# WARN: this means sockets that use little-endian will tend to lock up'
92
+ code << 'func _init(_socket):'
93
+ code << "\tsocket = _socket"
94
+ code << "\tsocket.set_no_delay(#{nodelay})"
95
+ code << "\tsocket.set_big_endian(#{big_endian})"
96
+
97
+ code
98
+ end
99
+
100
+ def _close
101
+ code = []
102
+
103
+ code << '# calls .disconnect_from_host() on the underlying socket'
104
+ code << 'func _close():'
105
+ code << "\tsocket.disconnect_from_host()"
106
+ end
107
+
108
+ def _connected
109
+ code = []
110
+
111
+ code << '# returns true if the messenger is connected, false otherwise'
112
+ code << 'func _connected():'
113
+ code << "\treturn socket != null && socket.get_status() == StreamPeerTCP.STATUS_CONNECTED"
114
+
115
+ code
116
+ end
117
+
118
+ def _errored
119
+ code = []
120
+
121
+ code << '# returns true if the messenger has errored, false otherwise'
122
+ code << 'func _errored():'
123
+ code << "\treturn socket != null && socket.get_status() == StreamPeerTCP.STATUS_ERROR"
124
+
125
+ code
126
+ end
127
+
128
+ def _has_bytes
129
+ code = []
130
+
131
+ code << '# returns true if there are bytes to be read, false otherwise'
132
+ code << 'func _has_bytes():'
133
+ code << "\treturn _ready_bytes() > 0"
134
+
135
+ code
136
+ end
137
+
138
+ def _ready_bytes
139
+ code = []
140
+
141
+ code << '# returns the number of bytes ready for reading'
142
+ code << 'func _ready_bytes():'
143
+ code << "\treturn socket.get_available_bytes()"
144
+
145
+ code
146
+ end
147
+
148
+ def _bool_type
149
+ code = []
150
+
151
+ code << '###########################################'
152
+ code << '# boolean type'
153
+ code << "func r_bool():"
154
+ code << "\treturn socket.get_8() == 1"
155
+ code << ""
156
+ code << "func w_bool(bool_var):"
157
+ code << "\tmatch bool_var:"
158
+ code << "\t\ttrue:"
159
+ code << "\t\t\tsocket.put_u8(1)"
160
+ code << "\t\t_:"
161
+ code << "\t\t\tsocket.put_u8(0)"
162
+
163
+ code
164
+ end
165
+
166
+ def _str_types
167
+ code = []
168
+
169
+ code << '###########################################'
170
+ code << '# string types'
171
+ code << "func r_str():"
172
+ code << "\treturn socket.get_data(socket.get_u32())[1].get_string_from_ascii()"
173
+ code << ""
174
+ code << "func w_str(string):"
175
+ code << "\tsocket.put_u32(string.length())"
176
+ code << "\tsocket.put_data(string.to_ascii())"
177
+
178
+ code
179
+ end
180
+
181
+ def _list_types
182
+ code = []
183
+
184
+ code << '###########################################'
185
+ code << '# list types'
186
+
187
+ ::Nmspec::V1::BASE_TYPES
188
+ .each do |type|
189
+ # See https://www.rubydoc.info/stdlib/core/1.9.3/Array:pack
190
+ num_bits = case type
191
+ when 'float_list' then 32
192
+ when 'double_list' then 64
193
+ when 'i8_list','u8_list' then 8
194
+ when 'i16_list','u16_list' then 16
195
+ when 'i32_list','u32_list' then 32
196
+ when 'i64_list','u64_list' then 64
197
+ else
198
+ next
199
+ end
200
+
201
+ code << _type_list_reader_writer_methods(type, num_bits)
202
+ end
203
+
204
+ code << "func r_str_list():"
205
+ code << "\tvar n = socket.get_u32()"
206
+ code << "\tvar strings = []"
207
+ code << ""
208
+ code << "\tfor _i in range(n):"
209
+ code << "\t\tstrings.append(socket.get_data(socket.get_u32())[1].get_string_from_ascii())"
210
+ code << ""
211
+ code << "\treturn strings"
212
+ code << ""
213
+ code << "func w_str_list(strings):"
214
+ code << "\tvar n = strings.size()"
215
+ code << "\tsocket.put_u32(strings.size())"
216
+ code << ""
217
+ code << "\tfor i in range(n):"
218
+ code << "\t\tsocket.put_u32(strings[i].length())"
219
+ code << "\t\tsocket.w_str(strings[i])"
220
+
221
+ code
222
+ end
223
+
224
+ def _type_list_reader_writer_methods(type, num_bits)
225
+ code = []
226
+
227
+ put_type = type.start_with?('i') ? type[1..] : type
228
+ code << "func r_#{type}():"
229
+ code << "\tvar n = socket.get_u32()"
230
+ code << "\tvar arr = []"
231
+ code << ""
232
+ code << "\tfor _i in range(n):"
233
+ code << "\t\tarr.append(socket.get_#{num_bits}())"
234
+ code << ""
235
+ code << "\treturn arr"
236
+ code << ""
237
+ code << "func w_#{type}(#{type}):"
238
+ code << "\tvar n = #{type}.size()"
239
+ code << "\tsocket.put_u32(n)"
240
+ code << ""
241
+ code << "\tfor i in range(n):"
242
+ code << "\t\tsocket.put_#{put_type.split('_list').first}(#{type}[i])"
243
+ code << ""
244
+ code
245
+ end
246
+
247
+ ##
248
+ # builds all msg methods
249
+ def _protos_methods(protos=[], subtypes=[])
250
+ code = []
251
+
252
+ return code unless protos && protos&.length > 0
253
+
254
+ code << ''
255
+ code << '###########################################'
256
+ code << '# messages'
257
+
258
+ protos.each_with_index do |proto, proto_code|
259
+ # This figures out which identifiers mentioned in the msg
260
+ # definition must be passed in vs. declared within the method
261
+
262
+ code << ''
263
+ send_local_vars = []
264
+ recv_local_vars = []
265
+ send_passed_params, recv_passed_params = proto[:msgs]
266
+ .inject([[], []]) do |all_params, msg|
267
+ msg[:type] = _replace_reserved_word(msg[:type])
268
+ msg[:identifier] = _replace_reserved_word(msg[:identifier])
269
+ send_params, recv_params = all_params
270
+
271
+ mode = msg[:mode]
272
+ type = msg[:type]
273
+ identifier = msg[:identifier]
274
+
275
+ case mode
276
+ when :read
277
+ send_local_vars << [type, identifier]
278
+ recv_params << identifier unless recv_local_vars.map{|v| v.last}.include?(identifier)
279
+ when :write
280
+ recv_local_vars << [type, identifier]
281
+ send_params << identifier unless send_local_vars.map{|v| v.last}.include?(identifier)
282
+ else
283
+ raise "Unsupported mode: `#{mode}`"
284
+ end
285
+
286
+ [send_params.uniq, recv_params.uniq]
287
+ end
288
+
289
+ ##
290
+ # send
291
+ code << _proto_method('send', proto_code, proto, send_local_vars, send_passed_params, subtypes)
292
+ code << _proto_method('recv', proto_code, proto, recv_local_vars, recv_passed_params, subtypes)
293
+ end
294
+
295
+ if protos.length > 0
296
+ code << ''
297
+ code << "# This method is used when you're receiving protocol messages"
298
+ code << "# in an unknown order, and dispatching automatically."
299
+ code << "func recv_any():"
300
+ code << "\tmatch socket.get_u8():"
301
+
302
+ protos.each_with_index do |proto, proto_code|
303
+ code << "\t\t#{proto_code}:"
304
+ code << "\t\t\treturn [#{proto_code}, recv_#{proto[:name]}()]"
305
+ end
306
+ end
307
+
308
+ code
309
+ end
310
+
311
+ def _replace_reserved_word(word)
312
+ case word
313
+ when 'float' then 'flt'
314
+ when 'str' then 'string'
315
+ when 'floor' then 'flr'
316
+ when 'bool' then 'bool_var'
317
+ else
318
+ word
319
+ end
320
+ end
321
+
322
+ ##
323
+ # Builds a single protocol method
324
+ def _proto_method(kind, proto_code, proto, local_vars, passed_params, subtypes)
325
+ code = []
326
+
327
+ code << "# #{proto[:desc]}" if proto[:desc]
328
+ unless local_vars.empty?
329
+ code << '#'
330
+ code << '# returns: (type | local var name)'
331
+ code << '# ['
332
+ local_vars.uniq.each{|v| code << " # #{"#{v.first}".ljust(12)} | #{v.last}" }
333
+ code << '# ]'
334
+ end
335
+
336
+ code << "func #{kind}_#{proto[:name]}#{passed_params.length > 0 ? "(#{(passed_params.to_a).join(', ')})" : '()'}:"
337
+
338
+ msgs = proto[:msgs]
339
+ code << "\tsocket.put_u8(#{proto_code})" if kind.eql?('send')
340
+ msgs.each do |msg|
341
+ msg = kind.eql?('send') ? msg : _flip_mode(msg)
342
+ code << "\t#{_line_from_msg(msg, subtypes)}"
343
+ end
344
+
345
+ code << "\treturn [#{local_vars.map{|v| v.last }.uniq.join(', ')}]"
346
+
347
+ code
348
+ end
349
+
350
+ def _flip_mode(msg)
351
+ opposite_mode = msg[:mode] == :read ? :write : :read
352
+ { mode: opposite_mode, type: msg[:type], identifier: msg[:identifier] }
353
+ end
354
+
355
+ def _line_from_msg(msg, subtypes)
356
+ subtype = subtypes.detect{|st| st[:name] == msg[:type] }&.dig(:base_type)
357
+ mode = msg[:mode]
358
+ type = _replace_reserved_word(subtype || msg[:type])
359
+ identifier = msg[:identifier]
360
+
361
+ type = type.start_with?('i') ? type[1..] : type
362
+
363
+ case mode
364
+ when :read
365
+ case
366
+ when type.end_with?('_list')
367
+ "var #{identifier} = r_#{type}()"
368
+ when type.eql?('string')
369
+ "var #{identifier} = r_str()"
370
+ when type.eql?('bool')
371
+ "var #{identifier} = r_bool()"
372
+ else
373
+ "var #{identifier} = socket.get_#{type}()"
374
+ end
375
+ when :write
376
+ case
377
+ when type.end_with?('_list')
378
+ "w_#{type}(#{identifier})"
379
+ when type.eql?('string')
380
+ "w_str(#{identifier})"
381
+ when type.eql?('bool')
382
+ "w_bool(#{identifier})"
383
+ else
384
+ "socket.put_#{type}(#{identifier})"
385
+ end
386
+ else
387
+ raise "Unsupported message msg mode: `#{mode}`"
388
+ end
389
+ end
390
+ end
391
+ end
392
+ end
@@ -0,0 +1,84 @@
1
+ require 'yaml'
2
+
3
+ module Nmspec
4
+ module Parser
5
+ class << self
6
+ BASE_TYPES = %w(
7
+ bool
8
+ i8 u8 i8_list u8_list
9
+ i16 u16 i16_list u16_list
10
+ i32 u32 i32_list u32_list
11
+ i64 u64 i64_list u64_list
12
+ float float_list
13
+ double double_list
14
+ str str_list
15
+ )
16
+
17
+ def parse(spec_hash)
18
+ spec_hash
19
+
20
+ {}.tap do |parsed|
21
+ parsed[:version] = spec_hash['version']
22
+ parsed[:msgr] = {
23
+ name: spec_hash.dig('msgr', 'name'),
24
+ desc: spec_hash.dig('msgr', 'desc'),
25
+ nodelay: spec_hash.dig('msgr', 'nodelay') || false,
26
+ bigendian: spec_hash.dig('msgr', 'bigendian').nil? ? true : spec_hash.dig('msgr', 'bigendian')
27
+ }
28
+
29
+ parsed[:types] = []
30
+ BASE_TYPES.each do |type|
31
+ parsed[:types] << {
32
+ name: type,
33
+ base_type: nil,
34
+ kind: _kind_of(type),
35
+ }
36
+ end
37
+
38
+ (spec_hash['types'] || []).each do |type_spec|
39
+ base_type, name = type_spec.split
40
+ parsed[:types] << {
41
+ name: name,
42
+ base_type: base_type,
43
+ kind: _kind_of(base_type),
44
+ }
45
+ end
46
+
47
+ parsed[:protos] = []
48
+ (spec_hash['protos'] || []).each do |proto|
49
+ msgs = (proto['msgs'] || [])
50
+ parsed[:protos] << {
51
+ name: proto['name'],
52
+ desc: proto['desc'],
53
+ msgs: msgs.map do |msg|
54
+ type, identifier = msg.split
55
+ {
56
+ mode: :write,
57
+ type: type,
58
+ identifier: identifier,
59
+ }
60
+ end
61
+ }
62
+ end
63
+ end
64
+ end
65
+
66
+ def _kind_of(type)
67
+ case type
68
+ when 'bool'
69
+ 'bool'
70
+ when /\A(float|double|[ui]\d{1,2})\Z/
71
+ 'numeric'
72
+ when /\A(float|double|[ui]\d{1,2})_list\Z/
73
+ 'numeric_list'
74
+ when 'str'
75
+ 'str'
76
+ when 'str_list'
77
+ 'str_list'
78
+ else
79
+ raise "Unknown kind of type: `#{type}`"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end