nmspec 1.4.0 → 1.5.1.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,391 @@
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_big_endian(#{big_endian})"
95
+
96
+ code
97
+ end
98
+
99
+ def _close
100
+ code = []
101
+
102
+ code << '# calls .disconnect_from_host() on the underlying socket'
103
+ code << 'func _close():'
104
+ code << "\tsocket.disconnect_from_host()"
105
+ end
106
+
107
+ def _connected
108
+ code = []
109
+
110
+ code << '# returns true if the messenger is connected, false otherwise'
111
+ code << 'func _connected():'
112
+ code << "\treturn socket != null && socket.get_status() == StreamPeerTCP.STATUS_CONNECTED"
113
+
114
+ code
115
+ end
116
+
117
+ def _errored
118
+ code = []
119
+
120
+ code << '# returns true if the messenger has errored, false otherwise'
121
+ code << 'func _errored():'
122
+ code << "\treturn socket != null && socket.get_status() == StreamPeerTCP.STATUS_ERROR"
123
+
124
+ code
125
+ end
126
+
127
+ def _has_bytes
128
+ code = []
129
+
130
+ code << '# returns true if there are bytes to be read, false otherwise'
131
+ code << 'func _has_bytes():'
132
+ code << "\treturn _ready_bytes() > 0"
133
+
134
+ code
135
+ end
136
+
137
+ def _ready_bytes
138
+ code = []
139
+
140
+ code << '# returns the number of bytes ready for reading'
141
+ code << 'func _ready_bytes():'
142
+ code << "\treturn socket.get_available_bytes()"
143
+
144
+ code
145
+ end
146
+
147
+ def _bool_type
148
+ code = []
149
+
150
+ code << '###########################################'
151
+ code << '# boolean type'
152
+ code << "func r_bool():"
153
+ code << "\treturn socket.get_8() == 1"
154
+ code << ""
155
+ code << "func w_bool(bool_var):"
156
+ code << "\tmatch bool_var:"
157
+ code << "\t\ttrue:"
158
+ code << "\t\t\tsocket.put_u8(1)"
159
+ code << "\t\t_:"
160
+ code << "\t\t\tsocket.put_u8(0)"
161
+
162
+ code
163
+ end
164
+
165
+ def _str_types
166
+ code = []
167
+
168
+ code << '###########################################'
169
+ code << '# string types'
170
+ code << "func r_str():"
171
+ code << "\treturn socket.get_data(socket.get_u32())[1].get_string_from_ascii()"
172
+ code << ""
173
+ code << "func w_str(string):"
174
+ code << "\tsocket.put_u32(string.length())"
175
+ code << "\tsocket.put_data(string.to_ascii())"
176
+
177
+ code
178
+ end
179
+
180
+ def _list_types
181
+ code = []
182
+
183
+ code << '###########################################'
184
+ code << '# list types'
185
+
186
+ ::Nmspec::V1::BASE_TYPES
187
+ .each do |type|
188
+ # See https://www.rubydoc.info/stdlib/core/1.9.3/Array:pack
189
+ num_bits = case type
190
+ when 'float_list' then 32
191
+ when 'double_list' then 64
192
+ when 'i8_list','u8_list' then 8
193
+ when 'i16_list','u16_list' then 16
194
+ when 'i32_list','u32_list' then 32
195
+ when 'i64_list','u64_list' then 64
196
+ else
197
+ next
198
+ end
199
+
200
+ code << _type_list_reader_writer_methods(type, num_bits)
201
+ end
202
+
203
+ code << "func r_str_list():"
204
+ code << "\tvar n = socket.get_u32()"
205
+ code << "\tvar strings = []"
206
+ code << ""
207
+ code << "\tfor _i in range(n):"
208
+ code << "\t\tstrings.append(socket.get_data(socket.get_u32())[1].get_string_from_ascii())"
209
+ code << ""
210
+ code << "\treturn strings"
211
+ code << ""
212
+ code << "func w_str_list(strings):"
213
+ code << "\tvar n = strings.size()"
214
+ code << "\tsocket.put_u32(strings.size())"
215
+ code << ""
216
+ code << "\tfor i in range(n):"
217
+ code << "\t\tsocket.put_u32(strings[i].length())"
218
+ code << "\t\tsocket.w_str(strings[i])"
219
+
220
+ code
221
+ end
222
+
223
+ def _type_list_reader_writer_methods(type, num_bits)
224
+ code = []
225
+
226
+ put_type = type.start_with?('i') ? type[1..] : type
227
+ code << "func r_#{type}():"
228
+ code << "\tvar n = socket.get_u32()"
229
+ code << "\tvar arr = []"
230
+ code << ""
231
+ code << "\tfor _i in range(n):"
232
+ code << "\t\tarr.append(socket.get_#{num_bits}())"
233
+ code << ""
234
+ code << "\treturn arr"
235
+ code << ""
236
+ code << "func w_#{type}(#{type}):"
237
+ code << "\tvar n = #{type}.size()"
238
+ code << "\tsocket.put_u32(n)"
239
+ code << ""
240
+ code << "\tfor i in range(n):"
241
+ code << "\t\tsocket.put_#{put_type.split('_list').first}(#{type}[i])"
242
+ code << ""
243
+ code
244
+ end
245
+
246
+ ##
247
+ # builds all msg methods
248
+ def _protos_methods(protos=[], subtypes=[])
249
+ code = []
250
+
251
+ return code unless protos && protos&.length > 0
252
+
253
+ code << ''
254
+ code << '###########################################'
255
+ code << '# messages'
256
+
257
+ protos.each_with_index do |proto, proto_code|
258
+ # This figures out which identifiers mentioned in the msg
259
+ # definition must be passed in vs. declared within the method
260
+
261
+ code << ''
262
+ send_local_vars = []
263
+ recv_local_vars = []
264
+ send_passed_params, recv_passed_params = proto[:msgs]
265
+ .inject([[], []]) do |all_params, msg|
266
+ msg[:type] = _replace_reserved_word(msg[:type])
267
+ msg[:identifier] = _replace_reserved_word(msg[:identifier])
268
+ send_params, recv_params = all_params
269
+
270
+ mode = msg[:mode]
271
+ type = msg[:type]
272
+ identifier = msg[:identifier]
273
+
274
+ case mode
275
+ when :read
276
+ send_local_vars << [type, identifier]
277
+ recv_params << identifier unless recv_local_vars.map{|v| v.last}.include?(identifier)
278
+ when :write
279
+ recv_local_vars << [type, identifier]
280
+ send_params << identifier unless send_local_vars.map{|v| v.last}.include?(identifier)
281
+ else
282
+ raise "Unsupported mode: `#{mode}`"
283
+ end
284
+
285
+ [send_params.uniq, recv_params.uniq]
286
+ end
287
+
288
+ ##
289
+ # send
290
+ code << _proto_method('send', proto_code, proto, send_local_vars, send_passed_params, subtypes)
291
+ code << _proto_method('recv', proto_code, proto, recv_local_vars, recv_passed_params, subtypes)
292
+ end
293
+
294
+ if protos.length > 0
295
+ code << ''
296
+ code << "# This method is used when you're receiving protocol messages"
297
+ code << "# in an unknown order, and dispatching automatically."
298
+ code << "func recv_any():"
299
+ code << "\tmatch socket.get_u8():"
300
+
301
+ protos.each_with_index do |proto, proto_code|
302
+ code << "\t\t#{proto_code}:"
303
+ code << "\t\t\treturn [#{proto_code}, recv_#{proto[:name]}()]"
304
+ end
305
+ end
306
+
307
+ code
308
+ end
309
+
310
+ def _replace_reserved_word(word)
311
+ case word
312
+ when 'float' then 'flt'
313
+ when 'str' then 'string'
314
+ when 'floor' then 'flr'
315
+ when 'bool' then 'bool_var'
316
+ else
317
+ word
318
+ end
319
+ end
320
+
321
+ ##
322
+ # Builds a single protocol method
323
+ def _proto_method(kind, proto_code, proto, local_vars, passed_params, subtypes)
324
+ code = []
325
+
326
+ code << "# #{proto[:desc]}" if proto[:desc]
327
+ unless local_vars.empty?
328
+ code << '#'
329
+ code << '# returns: (type | local var name)'
330
+ code << '# ['
331
+ local_vars.uniq.each{|v| code << " # #{"#{v.first}".ljust(12)} | #{v.last}" }
332
+ code << '# ]'
333
+ end
334
+
335
+ code << "func #{kind}_#{proto[:name]}#{passed_params.length > 0 ? "(#{(passed_params.to_a).join(', ')})" : '()'}:"
336
+
337
+ msgs = proto[:msgs]
338
+ code << "\tsocket.put_u8(#{proto_code})" if kind.eql?('send')
339
+ msgs.each do |msg|
340
+ msg = kind.eql?('send') ? msg : _flip_mode(msg)
341
+ code << "\t#{_line_from_msg(msg, subtypes)}"
342
+ end
343
+
344
+ code << "\treturn [#{local_vars.map{|v| v.last }.uniq.join(', ')}]"
345
+
346
+ code
347
+ end
348
+
349
+ def _flip_mode(msg)
350
+ opposite_mode = msg[:mode] == :read ? :write : :read
351
+ { mode: opposite_mode, type: msg[:type], identifier: msg[:identifier] }
352
+ end
353
+
354
+ def _line_from_msg(msg, subtypes)
355
+ subtype = subtypes.detect{|st| st[:name] == msg[:type] }&.dig(:base_type)
356
+ mode = msg[:mode]
357
+ type = _replace_reserved_word(subtype || msg[:type])
358
+ identifier = msg[:identifier]
359
+
360
+ type = type.start_with?('i') ? type[1..] : type
361
+
362
+ case mode
363
+ when :read
364
+ case
365
+ when type.end_with?('_list')
366
+ "var #{identifier} = r_#{type}()"
367
+ when type.eql?('string')
368
+ "var #{identifier} = r_str()"
369
+ when type.eql?('bool')
370
+ "var #{identifier} = r_bool()"
371
+ else
372
+ "var #{identifier} = socket.get_#{type}()"
373
+ end
374
+ when :write
375
+ case
376
+ when type.end_with?('_list')
377
+ "w_#{type}(#{identifier})"
378
+ when type.eql?('string')
379
+ "w_str(#{identifier})"
380
+ when type.eql?('bool')
381
+ "w_bool(#{identifier})"
382
+ else
383
+ "socket.put_#{type}(#{identifier})"
384
+ end
385
+ else
386
+ raise "Unsupported message msg mode: `#{mode}`"
387
+ end
388
+ end
389
+ end
390
+ end
391
+ 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