module Nmspec::GDScript

Public Class Methods

_bool_type() click to toggle source
# File lib/nmspec/gdscript.rb, line 148
def _bool_type
  code = []

  code << '###########################################'
  code << '# boolean type'
  code << "func r_bool():"
  code << "\treturn socket.get_8() == 1"
  code << ""
  code << "func w_bool(bool_var):"
  code << "\tmatch bool_var:"
  code << "\t\ttrue:"
  code << "\t\t\tsocket.put_u8(1)"
  code << "\t\t_:"
  code << "\t\t\tsocket.put_u8(0)"

  code
end
_class_name_from_msgr_name(name) click to toggle source
# File lib/nmspec/gdscript.rb, line 62
def _class_name_from_msgr_name(name)
  name
    .downcase
    .gsub(/[\._\-]/, ' ')
    .split(' ')
    .map{|part| part.capitalize}
    .join + 'Msgr'
end
_close() click to toggle source
# File lib/nmspec/gdscript.rb, line 100
def _close
  code = []

  code << '# calls .disconnect_from_host() on the underlying socket'
  code << 'func _close():'
  code << "\tsocket.disconnect_from_host()"
end
_connected() click to toggle source
# File lib/nmspec/gdscript.rb, line 108
def _connected
  code = []

  code << '# returns true if the messenger is connected, false otherwise'
  code << 'func _connected():'
  code << "\treturn socket != null && socket.get_status() == StreamPeerTCP.STATUS_CONNECTED"

  code
end
_errored() click to toggle source
# File lib/nmspec/gdscript.rb, line 118
def _errored
  code = []

  code << '# returns true if the messenger has errored, false otherwise'
  code << 'func _errored():'
  code << "\treturn socket != null && socket.get_status() == StreamPeerTCP.STATUS_ERROR"

  code
end
_flip_mode(msg) click to toggle source
# File lib/nmspec/gdscript.rb, line 350
def _flip_mode(msg)
  opposite_mode = msg[:mode] == :read ? :write : :read
  { mode: opposite_mode, type: msg[:type], identifier: msg[:identifier] }
end
_has_bytes() click to toggle source
# File lib/nmspec/gdscript.rb, line 128
def _has_bytes
  code = []

  code << '# returns true if there are bytes to be read, false otherwise'
  code << 'func _has_bytes():'
  code << "\treturn _ready_bytes() > 0"

  code
end
_init(big_endian, nodelay) click to toggle source
# File lib/nmspec/gdscript.rb, line 87
def _init(big_endian, nodelay)
  code = []

  code << '# WARN: Messengers in GDScript assume big_endian byte order'
  code << '# WARN: this means sockets that use little-endian will tend to lock up'
  code << 'func _init(_socket):'
  code << "\tsocket = _socket"
  code << "\tsocket.set_no_delay(#{nodelay})"
  code << "\tsocket.set_big_endian(#{big_endian})"

  code
end
_line_from_msg(msg, subtypes) click to toggle source
# File lib/nmspec/gdscript.rb, line 355
def _line_from_msg(msg, subtypes)
  subtype = subtypes.detect{|st| st[:name] == msg[:type] }&.dig(:base_type)
  mode = msg[:mode]
  type = _replace_reserved_word(subtype || msg[:type])
  identifier = msg[:identifier]

  type = type.start_with?('i') ? type[1..] : type

  case mode
  when :read
    case
    when type.end_with?('_list')
      "var #{identifier} = r_#{type}()"
    when type.eql?('string')
      "var #{identifier} = r_str()"
    when type.eql?('bool')
      "var #{identifier} = r_bool()"
    else
      "var #{identifier} = socket.get_#{type}()"
    end
  when :write
    case
    when type.end_with?('_list')
      "w_#{type}(#{identifier})"
    when type.eql?('string')
      "w_str(#{identifier})"
    when type.eql?('bool')
      "w_bool(#{identifier})"
    else
      "socket.put_#{type}(#{identifier})"
    end
  else
    raise "Unsupported message msg mode: `#{mode}`"
  end
end
_list_types() click to toggle source
# File lib/nmspec/gdscript.rb, line 181
def _list_types
  code = []

  code << '###########################################'
  code << '# list types'

  ::Nmspec::V1::BASE_TYPES
    .each do |type|
      # See https://www.rubydoc.info/stdlib/core/1.9.3/Array:pack
      num_bits =  case type
                  when 'float_list'           then 32
                  when 'double_list'          then 64
                  when 'i8_list','u8_list'    then 8
                  when 'i16_list','u16_list'  then 16
                  when 'i32_list','u32_list'  then 32
                  when 'i64_list','u64_list'  then 64
                  else
                    next
                  end

      code << _type_list_reader_writer_methods(type, num_bits)
    end

  code << "func r_str_list():"
  code << "\tvar n = socket.get_u32()"
  code << "\tvar strings = []"
  code << ""
  code << "\tfor _i in range(n):"
  code << "\t\tstrings.append(socket.get_data(socket.get_u32())[1].get_string_from_ascii())"
  code << ""
  code << "\treturn strings"
  code << ""
  code << "func w_str_list(strings):"
  code << "\tvar n = strings.size()"
  code << "\tsocket.put_u32(strings.size())"
  code << ""
  code << "\tfor i in range(n):"
  code << "\t\tsocket.put_u32(strings[i].length())"
  code << "\t\tsocket.w_str(strings[i])"

  code
end
_opcode_mappings(protos) click to toggle source
# File lib/nmspec/gdscript.rb, line 71
def _opcode_mappings(protos)
  code = []

  code << 'const PROTO_TO_OP = {'
  code += protos.map.with_index{|p, i| "\t'#{p[:name]}': #{i}," }
  code << '}'

  code << ''

  code << 'const OP_TO_PROTO = {'
  code += protos.map.with_index{|p, i| "\t#{i}: '#{p[:name]}'," }
  code << '}'

  code
end
_proto_method(kind, proto_code, proto, local_vars, passed_params, subtypes) click to toggle source

Builds a single protocol method

# File lib/nmspec/gdscript.rb, line 324
def _proto_method(kind, proto_code, proto, local_vars, passed_params, subtypes)
  code = []

  code << "# #{proto[:desc]}" if proto[:desc]
  unless local_vars.empty?
    code << '#'
    code << '# returns:  (type | local var name)'
    code << '# ['
    local_vars.uniq.each{|v| code << "  #    #{"#{v.first}".ljust(12)} | #{v.last}" }
    code << '# ]'
  end

  code << "func #{kind}_#{proto[:name]}#{passed_params.length > 0 ? "(#{(passed_params.to_a).join(', ')})" : '()'}:"

  msgs = proto[:msgs]
  code << "\tsocket.put_u8(#{proto_code})" if kind.eql?('send')
  msgs.each do |msg|
    msg = kind.eql?('send') ? msg : _flip_mode(msg)
    code << "\t#{_line_from_msg(msg, subtypes)}"
  end

  code << "\treturn [#{local_vars.map{|v| v.last }.uniq.join(', ')}]"

  code
end
_protos_methods(protos=[], subtypes=[]) click to toggle source

builds all msg methods

# File lib/nmspec/gdscript.rb, line 249
def _protos_methods(protos=[], subtypes=[])
  code = []

  return code unless protos && protos&.length > 0

  code << ''
  code << '###########################################'
  code << '# messages'

  protos.each_with_index do |proto, proto_code|
    # This figures out which identifiers mentioned in the msg
    # definition must be passed in vs. declared within the method

    code << ''
    send_local_vars = []
    recv_local_vars = []
    send_passed_params, recv_passed_params = proto[:msgs]
      .inject([[], []]) do |all_params, msg|
        msg[:type] = _replace_reserved_word(msg[:type])
        msg[:identifier] = _replace_reserved_word(msg[:identifier])
        send_params, recv_params = all_params

        mode = msg[:mode]
        type = msg[:type]
        identifier = msg[:identifier]

        case mode
        when :read
          send_local_vars << [type, identifier]
          recv_params << identifier unless recv_local_vars.map{|v| v.last}.include?(identifier)
        when :write
          recv_local_vars << [type, identifier]
          send_params << identifier unless send_local_vars.map{|v| v.last}.include?(identifier)
        else
          raise "Unsupported mode: `#{mode}`"
        end

        [send_params.uniq, recv_params.uniq]
      end

    ##
    # send
    code << _proto_method('send', proto_code, proto, send_local_vars, send_passed_params, subtypes)
    code << _proto_method('recv', proto_code, proto, recv_local_vars, recv_passed_params, subtypes)
  end

  if protos.length > 0
    code << ''
    code << "# This method is used when you're receiving protocol messages"
    code << "# in an unknown order, and dispatching automatically."
    code << "func recv_any():"
    code << "\tmatch socket.get_u8():"

    protos.each_with_index do |proto, proto_code|
      code << "\t\t#{proto_code}:"
      code << "\t\t\treturn [#{proto_code}, recv_#{proto[:name]}()]"
    end
  end

  code
end
_ready_bytes() click to toggle source
# File lib/nmspec/gdscript.rb, line 138
def _ready_bytes
  code = []

  code << '# returns the number of bytes ready for reading'
  code << 'func _ready_bytes():'
  code << "\treturn socket.get_available_bytes()"

  code
end
_replace_reserved_word(word) click to toggle source
# File lib/nmspec/gdscript.rb, line 311
def _replace_reserved_word(word)
  case word
  when 'float' then 'flt'
  when 'str'   then 'string'
  when 'floor' then 'flr'
  when 'bool'  then 'bool_var'
  else
    word
  end
end
_str_types() click to toggle source
# File lib/nmspec/gdscript.rb, line 166
def _str_types
  code = []

  code << '###########################################'
  code << '# string types'
  code << "func r_str():"
  code << "\treturn socket.get_data(socket.get_u32())[1].get_string_from_ascii()"
  code << ""
  code << "func w_str(string):"
  code << "\tsocket.put_u32(string.length())"
  code << "\tsocket.put_data(string.to_ascii())"

  code
end
_type_list_reader_writer_methods(type, num_bits) click to toggle source
# File lib/nmspec/gdscript.rb, line 224
def _type_list_reader_writer_methods(type, num_bits)
  code = []

  put_type = type.start_with?('i') ? type[1..] : type
  code << "func r_#{type}():"
  code << "\tvar n = socket.get_u32()"
  code << "\tvar arr = []"
  code << ""
  code << "\tfor _i in range(n):"
  code << "\t\tarr.append(socket.get_#{num_bits}())"
  code << ""
  code << "\treturn arr"
  code << ""
  code << "func w_#{type}(#{type}):"
  code << "\tvar n = #{type}.size()"
  code << "\tsocket.put_u32(n)"
  code << ""
  code << "\tfor i in range(n):"
  code << "\t\tsocket.put_#{put_type.split('_list').first}(#{type}[i])"
  code << ""
  code
end
gen(spec) click to toggle source
# File lib/nmspec/gdscript.rb, line 7
def gen(spec)
  big_endian = spec.dig(:msgr, :bigendian)
  nodelay = spec.dig(:msgr, :nodelay)

  code = []
  code << '##'
  code << '# NOTE: this code is auto-generated from an nmspec file'

  if spec.dig(:msgr, :desc)
    code << '#'
    code << "# #{spec.dig(:msgr, :desc)}"
  end

  code << 'extends Reference'
  code << ''
  code << "class_name #{_class_name_from_msgr_name(spec.dig(:msgr, :name))}"
  code << ''

  if (spec[:protos]&.length || 0) > 0
    code << _opcode_mappings(spec[:protos])
    code << ''
  end

  code << '###########################################'
  code << '# setup'
  code << 'var socket = null'
  code << ''
  code << _init(big_endian, nodelay)
  code << ''
  code << _connected
  code << ''
  code << _errored
  code << ''
  code << _has_bytes
  code << ''
  code << _ready_bytes
  code << ''
  code << _close
  code << ''

  code << _bool_type
  code << ''
  code << _str_types
  code << ''
  code << _list_types

  subtypes = spec[:types].select{|t| !t[:base_type].nil? }
  code << _protos_methods(spec[:protos], subtypes)

  code.join("\n")
rescue => e
  "Code generation failed due to unknown error: check spec validity\n  cause: #{e.inspect}"
  puts e.backtrace.join("\n  ")
end