Class: Lich::GameBase::Game

Inherits:
Object
  • Object
show all
Defined in:
documented/games.rb

Overview

Main game class for managing game state and interactions This class handles the overall game logic and state management.

Direct Known Subclasses

DragonRealms::Game, Lich::Gemstone::Game

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

._bufferObject (readonly)

Returns the value of attribute _buffer.



214
215
216
# File 'documented/games.rb', line 214

def _buffer
  @_buffer
end

.bufferObject (readonly)

Returns the value of attribute buffer.



214
215
216
# File 'documented/games.rb', line 214

def buffer
  @buffer
end

.game_instanceObject (readonly)

Returns the value of attribute game_instance.



214
215
216
# File 'documented/games.rb', line 214

def game_instance
  @game_instance
end

.threadObject (readonly)

Returns the value of attribute thread.



214
215
216
# File 'documented/games.rb', line 214

def thread
  @thread
end

Class Method Details

._getsObject



370
371
372
# File 'documented/games.rb', line 370

def _gets
  @_buffer.gets
end

._puts(str) ⇒ Object



334
335
336
337
338
# File 'documented/games.rb', line 334

def _puts(str)
  @mutex.synchronize do
    @socket.puts(str)
  end
end

.closevoid

This method returns an undefined value.

Closes the socket connection

Examples:

Game.close


327
328
329
330
331
332
# File 'documented/games.rb', line 327

def close
  if @socket
    @socket.close rescue nil
    @thread.kill rescue nil
  end
end

.closed?Boolean

Checks if the socket is closed

Examples:

if Game.closed?
  puts 'Socket is closed'
end

Returns:

  • (Boolean)

    True if the socket is closed, false otherwise



319
320
321
# File 'documented/games.rb', line 319

def closed?
  @socket.nil? || @socket.closed?
end

.display_ruby_warningvoid

This method returns an undefined value.

Displays a warning if the Ruby version is below the recommended version

Examples:

Game.display_ruby_warning


523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'documented/games.rb', line 523

def display_ruby_warning
  ruby_warning = Terminal::Table.new
  ruby_warning.title = "Ruby Recommended Version Warning"
  ruby_warning.add_row(["Please update your Ruby installation."])
  ruby_warning.add_row(["You're currently running Ruby v#{Gem::Version.new(RUBY_VERSION)}!"])
  ruby_warning.add_row(["It's recommended to run Ruby v#{Gem::Version.new(RECOMMENDED_RUBY)} or higher!"])
  ruby_warning.add_row(["Future Lich5 releases will soon require this newer version."])
  ruby_warning.add_row([" "])
  ruby_warning.add_row(["Visit the following link for info on updating:"])

  # Use instance to get the appropriate documentation URL
  if @game_instance
    ruby_warning.add_row([@game_instance.get_documentation_url])
  else
    ruby_warning.add_row(["Unknown game type detected."])
    ruby_warning.add_row(["Unsure of proper documentation, please seek assistance via discord!"])
  end

  ruby_warning.to_s.split("\n").each do |row|
    Lich::Messaging.mono(Lich::Messaging.monsterbold(row))
  end
end

.getsString

Receives a string from the server

Examples:

response = Game.gets

Returns:

  • (String)

    The received string



366
367
368
# File 'documented/games.rb', line 366

def gets
  @buffer.gets
end

.handle_autostartvoid

This method returns an undefined value.

Handles the autostart process for the game

Examples:

Game.handle_autostart


504
505
506
507
508
509
510
511
512
513
514
515
516
517
# File 'documented/games.rb', line 504

def handle_autostart
  if defined?(LICH_VERSION) && defined?(Lich.core_updated_with_lich_version) &&
     Gem::Version.new(LICH_VERSION) > Gem::Version.new(Lich.core_updated_with_lich_version)
    Lich::Messaging.mono(Lich::Messaging.monsterbold("New installation or updated version of Lich5 detected!"))
    Lich::Messaging.mono(Lich::Messaging.monsterbold("Installing newest core scripts available to ensure you're up-to-date!"))
    Lich::Messaging.mono("")
    Lich::Util::Update.update_core_data_and_scripts
  end

  Script.start('autostart') if defined?(Script) && Script.respond_to?(:exists?) && Script.exists?('autostart')
  @autostarted = true

  display_ruby_warning if defined?(RECOMMENDED_RUBY) && Gem::Version.new(RUBY_VERSION) < Gem::Version.new(RECOMMENDED_RUBY)
end

.handle_thread_error(error) ⇒ Object



694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
# File 'documented/games.rb', line 694

def handle_thread_error(error)
  Lich.log "error: server_thread: #{error}\n\t#{error.backtrace.join("\n\t")}"
  $stdout.puts "error: server_thread: #{error}\n\t#{error.backtrace.slice(0..10).join("\n\t")}"
  sleep 0.2

  # Determine if we should retry
  case error
  when Errno::ETIMEDOUT, Errno::EWOULDBLOCK, IO::TimeoutError
    # Timeout errors are potentially recoverable if we haven't seen too many
    Lich.log "Timeout error detected - may attempt retry"
    return true
  when Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
    # Connection errors are fatal
    Lich.log "Fatal connection error - will not retry"
    return false
  else
    # Check if socket/client are closed or if it's a known fatal error
    if $_CLIENT_.closed? || @socket.closed?
      Lich.log "Client or socket closed - will not retry"
      return false
    elsif error.to_s =~ /invalid argument|A connection attempt failed|An existing connection was forcibly closed|An established connection was aborted by the software in your host machine./i
      Lich.log "Fatal error pattern detected - will not retry"
      return false
    else
      Lich.log "Unknown error - will attempt retry"
      return true
    end
  end
end

.handle_xml_error(server_string, error) ⇒ void

This method returns an undefined value.

Handles XML errors during processing

Examples:

Game.handle_xml_error(raw_string, error)

Parameters:

  • server_string (String)

    The server string that caused the error

  • error (StandardError)

    The error that occurred



617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
# File 'documented/games.rb', line 617

def handle_xml_error(server_string, error)
  # Ignoring certain XML errors
  unless error.to_s =~ /invalid byte sequence/
    # Handle specific XML errors
    if server_string =~ /<settingsInfo .*?space not found /
      Lich.log "Invalid settingsInfo XML tags detected: #{server_string.inspect}"
      server_string.sub!('space not found', '')
      Lich.log "Invalid settingsInfo XML tags fixed to: #{server_string.inspect}"
      return process_xml_data(server_string) # Return to retry with fixed string
    end

    $stdout.puts "error: server_thread: #{error}\n\t#{error.backtrace.join("\n\t")}"
    Lich.log "Invalid XML detected - please report this: #{server_string.inspect}"
    Lich.log "error: server_thread: #{error}\n\t#{error.backtrace.join("\n\t")}"
  end
end

.initialize_buffersvoid

This method returns an undefined value.

Initializes the buffers for the game Sets up necessary buffers for socket communication and game state.

Examples:

Game.initialize_buffers


221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'documented/games.rb', line 221

def initialize_buffers
  @socket = nil
  @mutex = Mutex.new
  @last_recv = nil
  @thread = nil
  @buffer = Lich::Common::SharedBuffer.new
  @_buffer = Lich::Common::SharedBuffer.new
  @_buffer.max_size = 1000
  @autostarted = false
  @cli_scripts = false
  @infomon_loaded = false
  @room_number_after_ready = false
  @last_id_shown_room_window = 0
  @game_instance = nil
end

.log_error(message, error) ⇒ Object (protected)



726
727
728
# File 'documented/games.rb', line 726

def log_error(message, error)
  Lich.log "#{message}: #{error}\n\t#{error.backtrace.join("\n\t")}"
end

.open(host, port) ⇒ TCPSocket

Opens a socket connection to the specified host and port

Examples:

socket = Game.open('localhost', 1234)

Parameters:

  • host (String)

    The hostname or IP address of the server

  • port (Integer)

    The port number to connect to

Returns:

  • (TCPSocket)

    The opened socket

Raises:

  • (StandardError)

    If there is an error during socket configuration



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'documented/games.rb', line 253

def open(host, port)
  @socket = TCPSocket.open(host, port)

  # Configure socket with error handling
  # More forgiving settings for Windows reliability under network stress
  begin
    SocketConfigurator.configure(@socket,
                                 keepalive: {
                                   enable: true,
                                   idle: 120,      # 2 minutes before first keepalive
                                   interval: 30    # 30 seconds between keepalive probes
                                 },
                                 linger: {
                                   enable: true,
                                   timeout: 5      # Wait 5 seconds for data to send on close
                                 },
                                 timeout: {
                                   recv: 30,       # 30 second receive timeout (increased from 10)
                                   send: 30        # 30 second send timeout (increased from 10)
                                 },
                                 buffer_size: {
                                   recv: 32768,    # 32KB receive buffer (reduced from 65536)
                                   send: 32768     # 32KB send buffer (reduced from 65536)
                                 },
                                 tcp_nodelay: true, # Disable Nagle's algorithm for low latency
                                 tcp_maxrt: 10)     # Windows: max 10 retransmissions before giving up

    Lich.log("Socket configured successfully for #{host}:#{port}") if ARGV.include?("--debug")
  rescue StandardError => e
    # Log the error but continue - socket may still work with default settings
    log_error("Socket configuration error (continuing with defaults)", e)
    Lich.log("WARNING: Socket running with default OS settings - may be less reliable under network stress")
  end

  @socket.sync = true

  start_wrap_thread
  start_main_thread

  @socket
end

.process_downstream_hooks(server_string) ⇒ void

This method returns an undefined value.

Processes downstream hooks for the server string

Examples:

Game.process_downstream_hooks(raw_string)

Parameters:

  • server_string (String)

    The server string to process



639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
# File 'documented/games.rb', line 639

def process_downstream_hooks(server_string)
  if (alt_string = DownstreamHook.run(server_string))
    process_room_information(alt_string)

    # Handle frontend-specific modifications
    if $frontend =~ /genie/i && alt_string =~ /^<streamWindow id='room' title='Room' subtitle=" - \[.*\] \((?:\d+|\*\*)\)"/
      alt_string.sub!(/] \((?:\d+|\*\*)\)/) { "]" }
    end

    if $frontend =~ /frostbite/i && alt_string =~ /^<streamWindow id='main' title='Story' subtitle=" - \[.*\] \((?:\d+|\*\*)\)"/
      alt_string.sub!(/] \((?:\d+|\*\*)\)/) { "]" }
    end

    # Handle room number display
    if @room_number_after_ready && alt_string =~ /<prompt /
      alt_string = @game_instance ? @game_instance.process_room_display(alt_string) : alt_string
      @room_number_after_ready = false
    end

    # Handle frontend-specific conversions
    if $frontend =~ /^(?:wizard|avalon)$/
      alt_string = sf_to_wiz(alt_string)
    end

    # Send to client
    send_to_client(alt_string)
  end
end

.process_room_information(alt_string) ⇒ Object



668
669
670
671
672
673
674
675
676
# File 'documented/games.rb', line 668

def process_room_information(alt_string)
  if alt_string =~ /^(<pushStream id="familiar" ifClosedStyle="watching"\/>)?(?:<resource picture="\d+"\/>|<popBold\/>)?<style id="roomName"\s+\/>/
    if (Lich.display_lichid == true || Lich.display_uid == true || Lich.hide_uid_flag == true)
      @game_instance ? @game_instance.modify_room_display(alt_string) : alt_string
    end
    @room_number_after_ready = true
    alt_string
  end
end

.process_server_string(server_string) ⇒ void

This method returns an undefined value.

Processes the server string received from the socket

Examples:

Game.process_server_string(raw_string)

Parameters:

  • server_string (String)

    The raw server string to process



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'documented/games.rb', line 452

def process_server_string(server_string)
  $cmd_prefix = String.new if server_string =~ /^\034GSw/

  # Load game-specific modules if needed
  unless (XMLData.game.nil? || XMLData.game.empty?)
    unless Module.const_defined?(:GameLoader)
      require_relative 'common/game-loader'
      GameLoader.load!
    end
  end

  # Set instance if not already set
  if @game_instance.nil? && !XMLData.game.nil? && !XMLData.game.empty?
    set_game_instance(XMLData.game)
  end

  # Clean server string based on game type
  if @game_instance
    server_string = @game_instance.clean_serverstring(server_string)
  end

  # Debug output if needed
  pp server_string if defined?($deep_debug) && $deep_debug

  # Push to server buffer
  $_SERVERBUFFER_.push(server_string)

  # Handle autostart
  handle_autostart if !@autostarted && server_string =~ /<app char/

  # Handle infomon loading
  if !@infomon_loaded && (defined?(Infomon) || !$DRINFOMON_VERSION.nil?) && !XMLData.name.nil? && !XMLData.name.empty? && !XMLData.dialogs.empty?
    ExecScript.start("Infomon.redo!", { quiet: true, name: "infomon_reset" }) if XMLData.game !~ /^DR/ && Infomon.db_refresh_needed?
    @infomon_loaded = true
  end

  # Handle CLI scripts
  if !@cli_scripts && @autostarted && !XMLData.name.nil? && !XMLData.name.empty?
    start_cli_scripts
  end

  # Process XML data
  process_xml_data(server_string) unless server_string =~ /^<settings /

  # Run downstream hooks
  process_downstream_hooks(server_string)
end

.process_xml_data(server_string) ⇒ void

This method returns an undefined value.

Processes XML data from the server string

Examples:

Game.process_xml_data(raw_string)

Parameters:

  • server_string (String)

    The server string containing XML data



565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'documented/games.rb', line 565

def process_xml_data(server_string)
  begin
    # Check for valid XML
    REXML::Document.parse_stream("<root>#{server_string}</root>", XMLData)
  rescue => e
    case e.to_s
    # Missing attribute equal: <s> - in dynamic dialogs with a single apostrophe for possessive 'Tsetem's Items'
    when /nested single quotes|nested double quotes|Missing attribute equal: <[^>]+>|Invalid attribute name: <[^>]+>/
      original_server_string = server_string.dup
      server_string = XMLCleaner.clean_nested_quotes(server_string)
      if original_server_string != server_string
        retry
      else
        handle_xml_error(server_string, e)
        XMLData.reset
        return
      end
    when /invalid characters/
      server_string = XMLCleaner.fix_invalid_characters(server_string)
      retry
    when /Missing end tag for 'd'/
      server_string = XMLCleaner.fix_xml_tags(server_string)
      retry
    else
      handle_xml_error(server_string, e)
      XMLData.reset
      return
    end
  end

  # Process game-specific data using instance
  if @game_instance && Module.const_defined?(:GameLoader)
    @game_instance.process_game_specific_data(server_string)
  end

  # Process downstream XML
  Script.new_downstream_xml(server_string) if defined?(Script)

  # Process stripped server string
  stripped_server = strip_xml(server_string, type: 'main')
  stripped_server.split("\r\n").each do |line|
    @buffer.update(line) if defined?(TESTING) && TESTING
    Script.new_downstream(line) if defined?(Script) && !line.empty?
  end
end

.puts(str) ⇒ void

This method returns an undefined value.

Sends a string to the server

Examples:

Game.puts('Hello, server!')

Parameters:

  • str (String)

    The string to send



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'documented/games.rb', line 345

def puts(str)
  if Script.current&.file_name
    script_name = "#{Script.current.custom? ? 'custom/' : ''}#{Script.current&.name}"
  else
    script_name = Script.current&.name || '(unknown script)'
  end

  $_CLIENTBUFFER_.push "[#{script_name}]#{$SEND_CHARACTER}#{$cmd_prefix}#{str}\r\n"

  unless Script.current&.silent
    respond "[#{script_name}]#{$SEND_CHARACTER}#{str}\r\n"
  end

  _puts "#{$cmd_prefix}#{str}"
  $_LASTUPSTREAM_ = "[#{script_name}]#{$SEND_CHARACTER}#{str}"
end

.send_to_client(alt_string) ⇒ Object



678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
# File 'documented/games.rb', line 678

def send_to_client(alt_string)
  if $_DETACHABLE_CLIENT_
    begin
      $_DETACHABLE_CLIENT_.write(alt_string)
    rescue
      $_DETACHABLE_CLIENT_.close rescue nil
      $_DETACHABLE_CLIENT_ = nil
      respond "--- Lich: error: client_thread: #{$!}"
      respond $!.backtrace.first
      Lich.log "error: client_thread: #{$!}\n\t#{$!.backtrace.join("\n\t")}"
    end
  else
    $_CLIENT_.write(alt_string)
  end
end

.set_game_instance(game_type) ⇒ void

This method returns an undefined value.

Sets the game instance based on the game type

Examples:

Game.set_game_instance('GS')

Parameters:

  • game_type (String)

    The type of game to set the instance for



242
243
244
# File 'documented/games.rb', line 242

def set_game_instance(game_type)
  @game_instance = GameInstanceFactory.create(game_type)
end

.start_cli_scriptsvoid

This method returns an undefined value.

Starts CLI scripts based on command line arguments

Examples:

Game.start_cli_scripts


550
551
552
553
554
555
556
557
558
# File 'documented/games.rb', line 550

def start_cli_scripts
  if (arg = ARGV.find { |a| a =~ /^\-\-start\-scripts=/ })
    arg.sub('--start-scripts=', '').split(',').each do |script_name|
      Script.start(script_name)
    end
  end
  @cli_scripts = true
  Lich.log("info: logged in as #{XMLData.game}:#{XMLData.name}")
end

.start_main_threadvoid

This method returns an undefined value.

Starts the main thread for handling server communication

Examples:

Game.start_main_thread


378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'documented/games.rb', line 378

def start_main_thread
  @thread = Thread.new do
    consecutive_timeouts = 0
    max_consecutive_timeouts = 3 # Allow 3 timeouts before giving up

    begin
      while true
        begin
          # Try to read from socket with timeout
          server_string = @socket.gets

          # Successfully received data - reset timeout counter
          consecutive_timeouts = 0

          # Break if socket closed (gets returns nil)
          break if server_string.nil?

          @last_recv = Time.now
          @_buffer.update(server_string) if defined?(TESTING) && TESTING

          begin
            process_server_string(server_string)
          rescue StandardError => e
            log_error("Error processing server string", e)
          end
        rescue Errno::ETIMEDOUT, Errno::EWOULDBLOCK, IO::TimeoutError => timeout_error
          # Socket read timed out - this is expected if server is quiet
          consecutive_timeouts += 1

          Lich.log "Socket read timeout #{consecutive_timeouts}/#{max_consecutive_timeouts} (no data for 30s)"

          if consecutive_timeouts >= max_consecutive_timeouts
            Lich.log "Too many consecutive timeouts, connection may be dead"
            raise timeout_error # Let the outer rescue handle it
          end

          # Check if socket is still alive
          if @socket.closed?
            Lich.log "Socket is closed, exiting thread"
            break
          end

          # Small sleep before retry
          sleep 0.1
          retry
        rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED => conn_error
          # Connection was reset/broken - these are fatal
          Lich.log "Connection error: #{conn_error.class} - #{conn_error.message}"
          raise conn_error
        end
      end
    rescue StandardError => e
      # Handle any other errors
      should_continue = handle_thread_error(e)

      # Only retry if handle_thread_error says it's safe and socket is still open
      if should_continue && !@socket.closed? && !$_CLIENT_.closed?
        Lich.log "Retrying server thread after error..."
        consecutive_timeouts = 0 # Reset counter on retry
        sleep 1 # Brief pause before retry
        retry
      else
        Lich.log "Server thread exiting due to unrecoverable error"
      end
    end
  end
  @thread.priority = 4
end

.start_wrap_threadObject



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'documented/games.rb', line 295

def start_wrap_thread
  begin
    Lich.db_vacuum_if_due!(months: 6)
  rescue => e
    Lich.log "db_maint(startup): #{e.class}: #{e.message}"
  end

  @wrap_thread = Thread.new do
    @last_recv = Time.now
    until @autostarted || (Time.now - @last_recv >= 6)
      break if @autostarted
      sleep 0.2
    end

    puts 'look' unless @autostarted
  end
end