RocketMUD-0.3/help/
RocketMUD-0.3/log/
RocketMUD-0.3/players/
#
# RocketMUD was written by Jon Lambert, 2006.
# It is based on SocketMUD(tm) written by Brian Graversen.
# This code is released to the public domain.
#

# This file contains the socket code, used for accepting
# new connections as well as reading and writing to
# sockets, and closing down unused sockets.
#

class SockDesc
  attr_accessor :control, :state
  attr_accessor :player, :events, :hostname, :inbuf, :outbuf
  attr_accessor :next_command, :bust_prompt


  def initialize sock
    @control = sock
    @state   =  :state_new_name   # Connection states symbols used
                                  #  state_new_name        = 0
                                  # :state_new_password    = 1
                                  # :state_verify_password = 2
                                  # :state_ask_password    = 3
                                  # :state_playing         = 4
                                  # :state_closed          = 5
    @player = nil
    @events = []
    @hostname = sock.peeraddr[2]
    @inbuf = ""
    @outbuf = ""
    @next_command = ""
    @bust_prompt = false
  end

  #
  # Text_to_socket()
  #
  # Sends text directly to the socket,
  # will compress the data if needed.
  def text_to_socket txt
    n = @control.send(txt, 0)
    # save unsent data for next call
    txt.slice!(0...n) # Only has an effect if txt parameter a reference.
    return true
  rescue Exception
    log_string "text_to_socket: Error"
    log_string $!.to_s
    return false
  end

  def next_cmd_from_buffer
    # if theres already a command ready, we return
    return if !@next_command.empty?

    # if there is nothing pending, then return
    return if @inbuf.empty?

    # check how long the next command is
    sz = 0
    while !@inbuf[sz].nil? && @inbuf[sz] != ?\n && @inbuf[sz] != ?\r
      sz += 1
    end

    # we only deal with real commands
    if @inbuf[sz].nil?
      return
    end

    telopt = 0
    # copy the next command into next_command
    sz.times do |i|
      if @inbuf[i].chr == IAC.chr
        telopt = 1
      elsif telopt == 1 && (@inbuf[i].chr == DO.chr || @inbuf[i].chr == DONT.chr)
        telopt = 2;
      elsif telopt == 2
        telopt = 0
      elsif @inbuf[i].chr =~ /[[:print:]]/
        @next_command << @inbuf[i].chr
      end
    end

    # skip forward to the next line
    while !@inbuf[sz].nil? && (@inbuf[sz] == ?\n || @inbuf[sz] == ?\r)
      @bust_prompt = true   # seems like a good place to check
      sz += 1
    end

    # move the context of inbuf down
    @inbuf.slice!(0..sz)
  end

  #
  # Close_socket()
  #
  # Will close one socket directly, freeing all
  # resources and making the socket availably on
  # the socket free_list.
  def close_socket reconnect
    return if @state == :state_closed

    # remove the socket from the polling list
    $fSet.delete @control

    if @state == :state_playing
      if reconnect
        text_to_socket "This connection has been taken over.\r\n"
      elsif @player
        @player.socket = nil
        log_string "Closing link to %s", @player.name
      end
    elsif @player
      @player.free_mobile
    end

    # dequeue all events for this socket
    @events.each do |ev|
      ev.dequeue_event
    end

    # set the closed state
    @state = :state_closed
  end

  def flush_output
    # nothing to send
    if @outbuf.size == 0 && !(@bust_prompt && @state == :state_playing)
      return true
    end

    # bust a prompt
    if @state == :state_playing && @bust_prompt
      text_to_buffer "\r\nRocketMud:> "
      @bust_prompt = false
    end

    #
    # Send the buffer, and return FALSE
    # if the write fails.
    if !(text_to_socket @outbuf)
      return false
    end

    # Success
    return true
  end

  #
  # Read_from_socket()
  #
  # Reads one line from the socket, storing it
  # in a buffer for later use. Will also close
  # the socket if it tries a buffer overflow.
  def read_from_socket
    # start reading from the socket
    input = @control.recv(MAX_BUFFER)
    if !input || input.empty?
      log_string "Read_from_socket: EOF"
      return false
    end
    @inbuf << input
    # check for buffer overflows, and drop connection in that case
    if @inbuf.size > MAX_BUFFER
      @inbuf = ''
      text_to_socket "\r\n!!!! Input Overflow !!!!\r\n"
      return false
    else
      return true
    end
  rescue Errno::EWOULDBLOCK
    return true
  rescue Exception
    log_string "Read_from_socket: Error"
    log_string $!.to_s
    return false
  end

  #
  # Text_to_buffer()
  #
  # Stores outbound text in a buffer, where it will
  # stay untill it is flushed in the gameloop.
  #
  # Will also parse ANSI colors and other tags.
  def text_to_buffer txt
    output = ""
    underline = bold = false
    last = -1

    if txt.size > MAX_BUFFER
      log_string "text_to_buffer: buffer overflow."
      return
    end

    # always start with a leading space
    if @outbuf.empty?
      @outbuf = "\r\n"
    end

    i = 0
    while i < txt.size
      if txt[i] == ?#
        i += 1
        # toggle underline on/off with #u
        if txt[i] == ?u
          i += 1
          if underline
            underline = false
            output << "\e[0"
            output << ';1' if bold
            if last != -1
              output << ';'
              output << $ansi_table[last][1]
            end
            output << 'm'
          else
            underline = true
            output << "\e[4m"
          end
        # parse ## to #
        elsif txt[i] == ?#
          i += 1
          output << '#'
        # #n should clear all tags
        elsif txt[i] == ?n
          i += 1
          if last != -1 || underline || bold
            underline = false
            bold = false
            output << "\e[0m"
          end
          last = -1
        # check for valid color tag and parse
        else
          validTag = false
          $ansi_table.size.times do |j|
            if txt[i] == $ansi_table[j][0]
              validTag = true
              # we only add the color sequence if it's needed
              if last != j
                cSequence = false
                # escape sequence
                output << "\e["
                # remember if a color change is needed
                cSequence = true if last == -1 || last / 2 != j / 2
                # handle font boldness
                if bold && $ansi_table[j][2] == false
                  output << '0'
                  bold = false
                  output << ";4" if underline
                  # changing from bold wipes the old color
                  output << ';'
                  cSequence = true
                elsif !bold && $ansi_table[j][2] == true
                  output << '1'
                  bold = true
                  output << ';' if cSequence
                end
                # add color sequence if needed
                output << $ansi_table[j][1] if cSequence
                output << 'm'
              end
              # remember the last color
              last = j
            end
          end
          # it wasn't a valid color tag
          if !validTag
            output << '#'
          else
            i += 1
          end
        end
      else
        output << txt[i].chr
        i += 1
      end
    end # while

    # and terminate it with the standard color
    if last != -1 || underline || bold
      output << "\e[0m"
    end

    # check to see if the socket can accept that much data
    if @outbuf.size + output.size > MAX_OUTPUT
      bug "Text_to_buffer: ouput overflow on %s.", @hostname
      return
    end

    # add data to buffer
    @outbuf << output
  end

  def handle_new_connections arg
    case @state
    when :state_new_name
      if !check_name(arg) # check for a legal name
        text_to_buffer "Sorry, that's not a legal name, please pick another.\r\nWhat is your name? "
        return
      end
      arg.capitalize!
      log_string "%s is trying to connect.", arg

      # Check for a new Player
      p_new = load_player arg
      if p_new.nil?
        p_new = Mobile.new
        # give the player it's name
        p_new.name = arg.dup
        # prepare for next step
        text_to_buffer "Please enter a new password: "
        @state = :state_new_password
      else # old player
        # prepare for next step
        text_to_buffer "What is your password? "
        @state = :state_ask_password
      end
      text_to_buffer DONT_ECHO
      # socket <-> player
      p_new.socket = self
      @player = p_new

    when :state_new_password
      if arg.size < 5 || arg.size > 12
        text_to_buffer "Between 5 and 12 chars please!\r\nPlease enter a new password: "
        return
      end
      @player.password = arg.crypt(@player.name)
      @player.password.size.times do |i|
        if @player.password[i].chr == '~'
          text_to_buffer "Illegal password!\r\nPlease enter a new password: "
          return
        end
      end
      text_to_buffer "Please verify the password: "
      @state = :state_verify_password

    when :state_verify_password
      if @player.password == arg.crypt(@player.name)
        text_to_buffer DO_ECHO
        # put him in the list
        $dmobile_list << @player
        log_string "New player: %s has entered the game.", @player.name
        # and into the game
        @state = :state_playing
        text_to_buffer $motd
        # initialize events on the player
        init_events_player @player
        # strip the idle event from this socket
        strip_event :event_socket_idle
      else
        @player.password = nil
        text_to_buffer "Password mismatch!\r\nPlease enter a new password: "
        @state = :state_new_password
      end

    when :state_ask_password
      text_to_buffer DO_ECHO
      if arg.crypt(@player.name) == @player.password
        if (p_new = check_reconnect(@player.name)) != nil
          # attach the new player
          @player.free_mobile
          @player = p_new
          p_new.socket = self

          log_string "%s has reconnected.", @player.name

          # and let him enter the game
          @state = :state_playing
          text_to_buffer "You take over a body already in use.\r\n"

          # strip the idle event from this socket
          strip_event :event_socket_idle
        elsif (p_new = load_player(@player.name)) == nil
          text_to_socket "ERROR: Your pfile is missing!\r\n"
          @player.free_mobile
          @player = nil
          close_socket false
          return
        else
          # attach the new player
          @player.free_mobile
          @player = p_new
          p_new.socket = self

          # put him in the active list
          $dmobile_list << p_new

          log_string "%s has entered the game.", @player.name

          # and let him enter the game
          @state = :state_playing
          text_to_buffer $motd

          # initialize events on the player
          init_events_player @player

          # strip the idle event from this socket
          strip_event :event_socket_idle
        end
      else
        text_to_socket "Bad password!\r\n"
        @player.free_mobile
        @player = nil
        close_socket false
      end
    else
      bug "Handle_new_connections: Bad state."
    end
  end


  def handle_cmd_input arg
    command = ""
    found_cmd = false
    dMob = @player
    return if dMob == nil

    one_arg! arg, command

    $tabCmd.each do |c|
      next if c.level > dMob.level

      if is_prefix command, c.cmd_name
        found_cmd = true
        dMob.send(c.cmd_funct, arg)
        break
      end
    end

    if !found_cmd
      dMob.text_to_mobile "No such command.\r\n"
    end
  rescue
    log_string "handle_cmd_input Error"
    log_string $!.to_s
  end

  # function   :: event_isset?
  # arguments  :: the type of event
  #
  # This function checks to see if a given type of event is enqueued/attached
  # to the socket, and if it is, it will return a pointer to this event.
  def event_isset? type
    @events.each do |event|
      if event.type == type
        return event
      end
    end
    return nil
  end

  # function   :: strip_event
  # arguments  :: the type of event
  #
  # This function will dequeue all events of a given type from the socket.
  def strip_event type
    @events.each do |event|
      if event.type == type
        event.dequeue_event
      end
    end
  end

  # function   :: add_event
  # arguments  :: the event and the delay
  #
  # This function attaches an event to a socket, and sets all the correct
  # values, and makes sure it is enqueued into the event queue.
  def add_event event, delay
    # check to see if the event has a type
    if event.type == :event_none
      bug "add_event: no type."
      return
    end

    # check to see of the event has a callback function
    if event.fun == nil
      bug "add_event: event type %d has no callback function.", event.type
      return
    end

    # set the correct variables for this event
    event.ownertype   = :event_owner_dsocket
    event.owner = self

    # attach the event to the sockets local list
    @events << event

    # attempt to enqueue the event
    if enqueue_event(event, delay) == false
      bug "add_event_socket: event type %d failed to be enqueued.", event.type
    end
  end


end