nmspec 1.5.0.pre → 1.5.0.pre2

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