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