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