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.
- checksums.yaml +4 -4
- data/LICENSE +19 -0
- data/README.md +301 -0
- data/lib/nmspec/gdscript.rb +392 -0
- data/lib/nmspec/parser.rb +84 -0
- data/lib/nmspec/ruby.rb +403 -0
- data/lib/nmspec/v1.rb +188 -0
- data/lib/nmspec/version.rb +1 -0
- metadata +10 -2
@@ -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
|