znmud-0.0.1/benchmark/
znmud-0.0.1/cmd/
znmud-0.0.1/cmd/emotes/
znmud-0.0.1/cmd/objects/
znmud-0.0.1/cmd/tiny/
znmud-0.0.1/doc/
znmud-0.0.1/farts/
znmud-0.0.1/lib/
znmud-0.0.1/lib/combat/
znmud-0.0.1/lib/core/bodytypes/
znmud-0.0.1/lib/engine/
znmud-0.0.1/lib/farts/
znmud-0.0.1/logs/
#
# file::    account.rb
# author::  Jon A. Lambert
# version:: 2.9.0
# date::    03/15/2006
#
# Additional Contributor: Craig Smith
#
# 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 'network/protocol/mxpcodes'
require 'utility/utility'
require 'utility/log'
require 'utility/publisher'
require 'core/root'
require 'gettext'

# The Account class handles connection login and passes them to
# character.
class Account < Root
  include Publisher
  include GetText
  include MXPCodes
  bindtextdomain("core")
  logger 'DEBUG'
  property :color, :passwd, :characters, :gender, :occupation, :logged_out, :logged_in
  attr_accessor :mode, :echo, :termsize, :terminal, :conn, :character, :mxp

  # 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.logged_out = nil
    self.logged_in = nil
    self.passwd = nil
    self.color = false
    self.characters = []
    @conn = conn                # Reference to network session (connection)
    @mode = :initialize
    @echo = false
    @mxp = 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
	room = get_object(@character.location)
	if room
		room.delete_contents(@character.id)
	end
	@character.combatants.each do |oid|
		get_object(oid).delete_combatant(oid)
	end
	if @character.following
		p = get_object(@character.following)
		p.del_follower(@character.id)
	end
        world.connected_characters.delete(@character.id)
        world.connected_characters.each do |pid|
	  msg = Msg.new _("%{name} has disconnected.") % {:name => name}
	  msg.system = true
          add_event(@character.id,pid,:show,msg)
        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)
      @mxp = @conn.query(:mxp)
      if vtsupport?
        publish("[home #{@termsize[1]},1][clearline][cursave]" +
          "[home 1,1][scrreset][clear][scrreg 1,#{@termsize[1]-3}][currest]")
        sendmsg(LOGO)
      end
      if @mxp
	sendmsg(init_mxp_elements)
	sendmsg(mxptag("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
  #            -> :newuserpass  Request new user password if private system
  #            -> :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
  # :newuserpass -> :newacct	If matches new system password create new acct
  # :gender    -> :pick_occupation	Picked gender, goto occupation/class selection
  # :pick_occupation -> :playing Start the game
  # :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
      if msg.is_accountname?
	@login_name = msg
      else
	sendmsg(append_echo(_("Invalid Login Characters.  Try again.  Login> ")))
	return
      end
      publish("[clearline]") if vtsupport?
      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)
        @mode = :playing
        welcome
      elsif @login_name.empty?
        sendmsg(append_echo(_("login> ")))
        @mode = :name
      else
        acctid = world.all_accounts.find {|a|
          @login_name.casecmp(get_object(a).name) == 0
        }
        @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
	if options['accept_new_users']
		if options['newuser_password_required']
			sendmsg(append_echo(_("Private System:  Enter the system password> ")))
			@conn.set(:hide, true)
			@mode = :newuserpass
		else
	        	sendmsg(append_echo(_("Create new user?\n'Y/y' to create, Hit enter to retry login> ")))
		        @mode = :newacct
		end
	else
		sendmsg(_("Account not found.  New user creation currently disabled."))
        	@mode = :name
	        sendmsg(append_echo(_("login> ")))
	end
      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)
	    if not @character
		log.error("Account #{@account.name} had no character objects assigned!")
		@mode = :disconnected
		sendmsg(append_echo(_("Error: No characters associated with your account.")))
		return
	    end
            # make the character non-swappable so we dont lose references
            Engine.instance.db.makenoswap(@character.id)
            world.connected_characters << @character.id if not world.connected_characters.include? @character.id
            @character.account = @account
            @account.character = @character
            @account.mode = :playing
	    @character.mode = :playing
	    @character.position = :standing
	    # Builders, Admins and zombies start where they left off
	    if not world.can_build? @character.id and not @character.has_attribute? :zombie
		# Special case if the lastsavedlocation is the default starting spot
		# but the player is infected.  Put them somewhere else
		if @character.has_attribute? :infected and options['infected_home'] and @character.lastsavedlocation == options['home']
			@character.location = options['infected_home']
		else
			@character.location = @character.lastsavedlocation
		end
	    end
	    @character.reset
            welcome(reconnect)
          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)
	# Multiple accounts are currently disabled in ZNMud
        #if options['account_system']
        #  sendmsg(append_echo(login_menu))
        #  @mode = :menu
        #else
	@mode = :gender
	sendmsg(append_echo(_("Gender M/F> ")))
      else
        @mode = :name
        sendmsg(append_echo(_("login> ")))
      end
    when :newuserpass
        @conn.set(:hide, false)
	if msg == options['newuser_password_required']
	       	sendmsg(append_echo(_("Create new user?\n'Y/y' to create, Hit enter to retry login> ")))
	        @mode = :newacct
	else
		sendmsg(append_echo(_("Invalid System Password. login> ")))
		@mode = :name
	end
    when :gender
	case msg
	when /^M/i # Male
		self.gender = :male
	when /^F/i # Female
		self.gender = :female
	else
		if rand(1)
			self.gender = :male
		else
			self.gender = :female
		end
		sendmsg(append_echo(_("Gender set to %{gender}.\n") % {:gender => gender}))
	end
	if options['enable_occupations']
		pick_occupation
	else
		finalize_new_acct
	end
    when :pick_occupation
	if msg.size > 0
		if msg.to_i > 0
			idx = msg.to_i
			occupations = options['occupations']
			if idx < 0 or idx > occupations.size
				sendmsg(append_echo(_("menu option out of range")))
			else
				self.occupation = occupations[idx-1]
				finalize_new_acct
			end
		else
			sendmsg(append_echo(_("Type the number of the occupation you want.")))
		end
	else
		occupations = options['occupations']
		self.occupation = occupations[rand(occupations.size)]
		finalize_new_acct
        	sendmsg(append_echo(_("You occupation is/was: %{occupation}.\n") % {:occupation => occupation}))
	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.empty?
        sendmsg(append_echo(login_menu))
        @mode = :menu
      else
        @character = new_char(msg)
	@character.reset
        @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
	  @character.reset
          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

  # Finalizes the last thing for a new character
  # If this is the first person in the world then make them God
  def finalize_new_acct
          @character = new_char
          welcome
          @mode = :playing
          @character.mode = :playing
	  @character.position = :standing
	  if options['starting_cash']
		start_cash = options['starting_cash'] + rand(5)
	  else
		start_cash = 0
	  end
	  # Set occupation specific settings
	if options['enable_occupations']
	  job = world.occupations.find(occupation)
	  if job.size == 1
		j = job[0]
		start_cash += j['start_cash_bonus'] if j['start_cash_bonus']
		if j['start_items']
			j['start_items'].each do |oid|
				o = world.load_object(oid)
				@character.add_contents(o.id)
				o.location = @character.id
			end
		end
		if j['start_skills']
			j['start_skills'].each do |skill|
				@character.add_skill(skill)
			end
		end
	  else
		log.error "Too many (or no) occupations defined for #{occupation}"
	  end
	end
	  @character.set_stat(:cash, start_cash)
	  world.add_admin @character.id if world.all_characters.size == 1
  end

  # When occupations/classes are enabled pick one
  def pick_occupation
	occupations = options['occupations']
	if options['random_occupations_only']
		self.occupation = occupations[rand(occupations.size)]
		finalize_new_acct
        	sendmsg(append_echo(_("You occupation is/was: %{occupation}.\n") % {:occupation => occupation}))
	else
		cnt = 0
		msg = ""
		(1..occupations.size).each do |x|
			msg << "#{x}) #{occupations[x-1]}".ljust(20)
			cnt += 1
			if cnt > 3
				msg << "\n"
				cnt = 0
			end
		end
		msg << _("\nChoose an occupation [Enter for Random] >")
		@mode = :pick_occupation
		sendmsg(append_echo(msg))
	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 if vtsupport?
  end

  def prompt
    if mxp_initialized?
	msgprompt = mxptag("Prompt")
	msgprompt << mxptag("Hp") + @character.health.to_s +  mxptag("/Hp") + "/"
	msgprompt << mxptag("MaxHp") + @character.stats[:maxhp].to_s + mxptag("/MaxHp") + " "
	msgprompt << mxptag("Mv") + @character.stats[:mp].to_s + mxptag("/Mv") + "/"
	msgprompt << mxptag("MaxMv") + @character.stats[:maxmp].to_s + mxptag("/MaxMv")
	msgprompt << " >" + mxptag("/Prompt")
	publish(msgprompt)
    elsif 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
      msgprompt = "[home #{@termsize[1]-2},1]" +
        "[color Yellow on Red]#{" "*@termsize[0]}[/color]" 
	if @character
        	msgprompt << "[home #{@termsize[1]-1},1][clearline][color Magenta](#{name})[#{@character.position}][/color]" 
        	msgprompt << "[home #{@termsize[1]},1][clearline][color Green]#{@character.health}[/color]H [color Yellow]#{@character.stats[:mp]}[/color]V > "
	else
        	msgprompt << "[home #{@termsize[1]-1},1][clearline][color Magenta](#{name})[#{@mode}][/color]" 
        	msgprompt << "[home #{@termsize[1]},1][clearline] > "
	end
	publish(msgprompt)
    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"
    str << "MXP is #{@conn.query(:mxp) ? 'ON' : 'OFF' }\n"
  end

  def toggle_color
    color ? self.color = false : self.color = true
    @conn.set(:color,color)
    msg = _("Colors toggled ")
    if color
	msg << _("[COLOR Magenta]ON[/COLOR]")
    else
	msg << _("OFF")
    end
    msg+"\n"
  end


  # Disconnects this account
  def disconnect(msg=nil)
    publish("[home 1,1][scrreset][clear]") if vtsupport?
    publish(msg + "\n") if msg
    if @character
	if @character.group_leader
		leader = get_object(@character.group_leader)
		leader.group_members.delete @character.id if leader.group_members.include? @character.id
	end
	if @character.group_members.size > 0
		@character.group_members.each do |gid|
			msg = _("You are no longer in %{name}'s group.")
			add_event(id, gid, :show, msg)
			get_object(gid).group_leader =nil
		end
	end
    end
    publish("Bye!\n")
    publish(:quit)
    self.logged_out = Time.now
    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 mxp_initialized?
	@mxp and @character
  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
    acct.mxp = @mxp
  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
    if world.motd
	msg = mxptag("Motd") + "[color Blue]" + world.motd + "[/color]" + mxptag("/Motd")
	add_event(@character.id, @character.id, :show, msg)
    end
    if @character.newmail? > 0
	msg = "[color Red]" + _("You have %{cnt} new messages. type 'mail'." % {:cnt => @character.newmail?}) + "[/color]"
	add_event(@character.id, @character.id, :show, msg)
    end
    self.logged_in = Time.now
    self.logged_out = nil
    add_event(@character.id, @character.location, :arrive, @character.id)
  end

end