tmud-3.0.0/benchmark/
tmud-3.0.0/cmd/
tmud-3.0.0/cmd/objects/
tmud-3.0.0/cmd/tiny/
tmud-3.0.0/doc/SQLite/
tmud-3.0.0/doc/SQLite3/
tmud-3.0.0/doc/TernaryTrie/
tmud-3.0.0/farts/
tmud-3.0.0/lib/
tmud-3.0.0/lib/engine/
tmud-3.0.0/lib/farts/
tmud-3.0.0/logs/
#
# file::    account.rb
# author::  Jon A. Lambert
# version:: 2.9.0
# date::    03/15/2006
#
# This source code copyright (C) 2005, 2006 by Jon A. Lambert
# All rights reserved.
#
# Released under the terms of the TeensyMUD Public License
# See LICENSE file for additional information.
#
$:.unshift "lib" if !$:.include? "lib"

require 'utility/utility'
require 'utility/log'
require 'utility/publisher'
require 'core/root'

# The Account class handles connection login and passes them to
# character.
class Account < Root
  include Publisher
  logger 'DEBUG'
  property :color, :passwd, :characters
  attr_accessor :mode, :echo, :termsize, :terminal, :conn, :character

  # Create an Account connection.  This is a temporary object that handles
  # login for character and gets them connected.
  # [+conn+]   The session associated with this Account connection.
  # [+return+] A handle to the Account object.
  def initialize(conn)
    super("",nil)
    self.passwd = nil
    self.color = false
    self.characters = []
    @conn = conn                # Reference to network session (connection)
    @mode = :initialize
    @echo = false
    @termsize = nil
    @terminal = nil
    @checked = 3                # Login retry counter - on 0 disconnect
    @account = nil              # used only during sign-in process
    @character = nil            # reference to the currently played Character.
  end

  # Receives messages from a Connection being observed and handles login
  # state.
  #
  # [+msg+]      The message string
  #
  # This supports the following:
  # [:disconnected] - This symbol from the server informs us that the
  #                 Connection has disconnected.
  # [:initdone] - This symbol from the server indicates that the Connection
  #               is done setting up and done negotiating an initial state.
  #               It triggers us to start sending output and parsing input.
  # [:termsize] - This is sent everytime the terminal size changes (NAWS)
  # [String] - A String is assumed to be input from the Session and we
  #            send it to parse_messages.
  #
  def update(msg)
    case msg
    # Handle disconnection from server
    # Note that publishing a :quit event (see #disconnect) will return a
    #  :disconnected event when server has closed the connection.
    # Guest accounts and characters are deleted here.
    when :disconnected
      @conn = nil
      unsubscribe_all
      Engine.instance.db.makeswap(id)
      if @character
        world.connected_characters.delete(@character.id)
        world.connected_characters.each do |pid|
          add_event(@character.id,pid,:show,"#{name} has disconnected.")
        end
        Engine.instance.db.makeswap(@character.id)
        @character.account = nil
        if @character.name =~ /Guest/i
          world.all_characters.delete(@character.id)
          delete_object(@character.id)
        end
        @character = nil
      end
      if name =~ /Guest/i
        world.all_accounts.delete(id)
        delete_object(id)
      end
    # Issued when a NAWS event occurs
    # Currently this clears and resets the screen.  Ideally it should attempt
    # to redraw it.
    when :termsize
      @termsize = @conn.query(:termsize)
      if vtsupport?
        publish("[home #{@termsize[1]},1][clearline][cursave]" +
          "[home 1,1][scrreset][clear][scrreg 1,#{@termsize[1]-3}][currest]")
      end
    # Negotiation with client done.  Start talking to it.
    when :initdone
      @echo = @conn.query(:echo)
      @termsize = @conn.query(:termsize)
      @terminal = @conn.query(:terminal)
      if vtsupport?
        publish("[home #{@termsize[1]},1][clearline][cursave]" +
          "[home 1,1][scrreset][clear][scrreg 1,#{@termsize[1]-3}][currest]")
        sendmsg(LOGO)
      end
      sendmsg(BANNER)
      sendmsg(append_echo("login> "))
      @mode = :name
    # This is a message from our user
    when String
      parse_messages(msg)
    else
      log.error "Account#update unknown message - #{msg.inspect}"
    end
  rescue
    # We squash and print out all exceptions here.  There is no reason to
    # throw these back at the Connection.
    log.error $!
  end


  # Handles String messages from Connection - called by update.
  # This was refactored out of Account#update for length reasons.
  #
  # [+msg+]      The message string
  #
  # @mode tracks the state changes,  The Account is created with the
  # initial state of :initialize.  The following state transition
  # diagram illustrates the possible transitions.
  #
  # :intialize -> :name         Set when Account:update receives :initdone msg
  # :name      -> :password     Sets @login_name and finds @account
  #               :playing      Creates a new character if Guest account
  # :password  -> :newacct      Sets @login_passwd
  #            -> :menu         Good passwd, switches account, if account_system
  #                             option on goes to menu
  #            -> :playing      Good passwd, switches account, loads character
  #            -> :name         Bad passwd
  #            -> disconnect    Bad passwd, exceeds @check attempts
  #                             (see Account#disconnect)
  # :newacct   -> :menu           If account_system option on goes to menu
  #            -> :playing        Creates new character, adds account
  # :menu      -> parse_menu    Redirect message (see Account#parse_menu)
  # :playing   -> @character    Redirect message (see Character#parse)
  #
  def parse_messages(msg)
    case @mode
    when :initialize
      # ignore everything until negotiation done
    when :name
      publish("[clearline]") if vtsupport?
      @login_name = msg.proper_name
      if options['guest_accounts'] && @login_name =~ /Guest/i
        self.name = "Guest#{id}"
        @character = new_char
        put_object(self)
        world.all_accounts << id
        # make the account non-swappable so we dont lose connection
        Engine.instance.db.makenoswap(id)
        @conn.set(:color, color)
        welcome
        @mode = :playing
      elsif @login_name.empty?
        sendmsg(append_echo("login> "))
        @mode = :name
      else
        acctid = world.all_accounts.find {|a|
          @login_name == get_object(a).name
        }
        @account = get_object(acctid)
        sendmsg(append_echo("password> "))
        @conn.set(:hide, true)
        @mode = :password
      end
    when :password
      @login_passwd = msg
      @conn.set(:hide, false)
      if @account.nil?  # new account
        sendmsg(append_echo("Create new user?\n'Y/y' to create, Hit enter to retry login> "))
        @mode = :newacct
      else
        if @login_passwd.is_passwd?(@account.passwd)  # good login
          # deregister all observers here and on connection
          unsubscribe_all
          @conn.unsubscribe_all
          # reregister all observers to @account
          @conn.subscribe(@account.id)
          # make the account non-swappable so we dont lose connection
          Engine.instance.db.makenoswap(@account.id)
          @conn.set(:color, @account.color)
          switch_acct(@account)
          # Check if this account already logged in
          reconnect = false
          if @account.subscriber_count > 0
            @account.publish(:reconnecting)
            @account.unsubscribe_all
            reconnect = true
          end
          @account.subscribe(@conn)
          if options['account_system']
            @account.sendmsg(append_echo(login_menu))
            @account.mode = :menu
          else
            @character = get_object(@account.characters.first)
            # make the character non-swappable so we dont lose references
            Engine.instance.db.makenoswap(@character.id)
            world.connected_characters << @character.id
            @character.account = @account
            @account.character = @character
            welcome(reconnect)
            @account.mode = :playing
          end
        else  # bad login
          @checked -= 1
          sendmsg(append_echo("Sorry wrong password."))
          if @checked < 1
            disconnect
          else
            @mode = :name
            sendmsg(append_echo("login> "))
          end
        end
      end
    when :newacct
      if msg =~ /^y/i
        self.name = @login_name
        self.passwd = @login_passwd.encrypt
        put_object(self)
        # make the account non-swappable so we dont lose connection
        Engine.instance.db.makenoswap(id)
        world.all_accounts << id
        @conn.set(:color, color)
        if options['account_system']
          sendmsg(append_echo(login_menu))
          @mode = :menu
        else
          @character = new_char
          welcome
          @mode = :playing
        end
      else
        @mode = :name
        sendmsg(append_echo("login> "))
      end
    when :menu, :menucr, :menupl
      parse_menu(msg)
    when :playing
      @character.parse(msg)
    else
      log.error "Account#parse_messages unknown :mode - #{@mode.inspect}"
    end
  end

  # Handles message while in the login menu - called by parse_messages.
  # This was refactored out of Account#parse_messages for length reasons.
  #
  # [+msg+]      The message string
  #
  # @mode tracks the state changes,  This routine is entered by any @modes
  # staring with :menu.
  #
  # The following state transition diagram illustrates the possible transitions.
  #
  # :menu      -> :menucr       Create a character
  #            -> :menupl       Play a character
  # :menucr    -> :playing      Get character name, create character and play
  #
  def parse_menu(msg)
    case @mode
    when :menu
      case msg
      when /^1/i
        sendmsg(append_echo("Enter character name> "))
        @mode = :menucr
      when /^2/i
        if characters.size == 0
          sendmsg(append_echo(login_menu))
          @mode = :menu
        else
          sendmsg(append_echo(character_menu))
          @mode = :menupl
        end
      when /^Q/i
        disconnect
      else                        # Any other key
        sendmsg(append_echo(login_menu))
        @mode = :menu
      end
    when :menucr
      if msg.proper_name.empty?
        sendmsg(append_echo(login_menu))
        @mode = :menu
      else
        @character = new_char(msg.proper_name)
        @conn.set(:color, color)
        welcome
        @mode = :playing
      end
    when :menupl
      case msg
      when /(\d+)/
        if $1.to_i >= characters.size
          sendmsg(append_echo(character_menu))
        else
          @character = get_object(characters[$1.to_i])
          # make the character non-swappable so we dont lose references
          Engine.instance.db.makenoswap(@character.id)
          world.connected_characters << @character.id
          @character.account = self
          welcome
          @mode = :playing
        end
      else
        sendmsg(append_echo(login_menu))
        @mode = :menu
      end
    else
      log.error "Account#parse_menu unknown :mode - #{@mode.inspect}"
    end
  end

  # If echo hasn't been negotiated, we want to leave the cursor after
  # the message prompt, so we prepend linefeeds in front of messages.
  # This is hackish.
  def append_echo(msg)
    @echo ? msg : "\n" + msg
  end

  def sendmsg(msg)
    publish("[cursave][home #{@termsize[1]-3},1]") if vtsupport?
    publish(msg)
    publish("[currest]") if vtsupport?
    prompt
  end

  def prompt
    if vtsupport?
=begin
      publish("[cursave][home #{@termsize[1]-2},1]" +
        "[color Yellow on Red]#{" "*@termsize[0]}[/color]" +
        "[home #{@termsize[1]-1},1][clearline][color Magenta](#{name})[#{@mode}][/color]" +
        "[currest][clearline]> ")
=end
      publish("[home #{@termsize[1]-2},1]" +
        "[color Yellow on Red]#{" "*@termsize[0]}[/color]" +
        "[home #{@termsize[1]-1},1][clearline][color Magenta](#{name})[#{@mode}][/color]" +
        "[home #{@termsize[1]},1][clearline]> ")
    else
#      publish("> ")
    end
  end

  def status_rept
    str = "Terminal: #{@terminal}\n"
    str << "Terminal size: #{@termsize[0]} X #{@termsize[1]}\n"
    str << "Colors toggled #{@color ? '[COLOR Magenta]ON[/COLOR]' : 'OFF' }\n"
    str << "Echo is #{@echo ? 'ON' : 'OFF' }\n"
    str << "ZMP is #{@conn.query(:zmp) ? 'ON' : 'OFF' }\n"
  end

  def toggle_color
    color ? self.color = false : self.color = true
    @conn.set(:color,color)
    "Colors toggled #{color ? '[COLOR Magenta]ON[/COLOR]' : 'OFF' }\n"
  end


  # Disconnects this account
  def disconnect(msg=nil)
    publish("[home 1,1][scrreset][clear]") if vtsupport?
    publish(msg + "\n") if msg
    publish("Bye!\n")
    publish(:quit)
    unsubscribe_all
  end

  def character_menu
    str = '[color Yellow]'
    characters.each_index do |i|
      str << "#{i}) #{get_object(characters[i]).name}\n"
    end
    str << "Pick a character>[/color] "
  end

  def login_menu
    "[color Yellow]1) Create a character\n2) Play\nQ) Quit\n>[/color] "
  end

  def vtsupport?
    @terminal =~ /^vt|xterm/
  end
private
  def new_char(nm=nil)
    if nm.nil?
      ch = Character.new(name,id)
    else
      ch = Character.new(nm,id)
    end
    self.characters << ch.id
    world.all_characters << ch.id
    ch.account = self
    get_object(options['home'] || 1).add_contents(ch.id)
    put_object(ch)
    Engine.instance.db.makenoswap(ch.id)
    world.connected_characters << ch.id
    ch
  end

  def switch_acct(acct)
    acct.conn = @conn
    acct.echo = @echo
    acct.termsize = @termsize
    acct.terminal = @terminal
    acct.character = @character
  end

  def welcome(reconnect=false)
    rstr = reconnect ? 'reconnected' : 'connected'
    @character.sendto(append_echo("Welcome #{@character.name}@#{@conn.query(:host)}!"))
    world.connected_characters.each do |pid|
      if pid != @character.id
        add_event(@character.id,pid,:show,"#{@character.name} has #{rstr}.")
      end
    end
    @character.parse('look')
  end

end