#
# 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