Class: Lich::GameBase::Game

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

Overview

Class representing the game.

This class manages the overall game state and interactions.

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.



207
208
209
# File 'documented/games.rb', line 207

def _buffer
  @_buffer
end

.bufferObject (readonly)

Returns the value of attribute buffer.



207
208
209
# File 'documented/games.rb', line 207

def buffer
  @buffer
end

.game_instanceObject (readonly)

Returns the value of attribute game_instance.



207
208
209
# File 'documented/games.rb', line 207

def game_instance
  @game_instance
end

.threadObject (readonly)

Returns the value of attribute thread.



207
208
209
# File 'documented/games.rb', line 207

def thread
  @thread
end

Class Method Details

._getsObject



363
364
365
# File 'documented/games.rb', line 363

def _gets
  @_buffer.gets
end

._puts(str) ⇒ void

This method returns an undefined value.

Sends a string to the socket.

Parameters:

  • str (String)

    the string to send.



328
329
330
331
332
333
334
335
# File 'documented/games.rb', line 328

def _puts(str)
  @mutex.synchronize do
    @socket.puts(str)
  end
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED, IOError => e
  Lich.log "error: _puts: #{e}\n\t#{e.backtrace.first}"
  nil
end

.autostarted?Boolean

Checks if the game has been autostarted.

Returns:

  • (Boolean)

    true if the game is autostarted, false otherwise.



211
212
213
# File 'documented/games.rb', line 211

def autostarted?
  @@autostarted
end

.closevoid

This method returns an undefined value.

Closes the socket and any associated threads.



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

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

.closed?Boolean

Checks if the socket is closed.

Returns:

  • (Boolean)

    true if the socket is closed, false otherwise.



312
313
314
# File 'documented/games.rb', line 312

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.



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

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

Retrieves a line from the buffer.

Returns:

  • (String)

    the retrieved line.



359
360
361
# File 'documented/games.rb', line 359

def gets
  @buffer.gets
end

.handle_autostartvoid

This method returns an undefined value.

Handles the autostart process for the game.



486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'documented/games.rb', line 486

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

  # Sync script repositories on login for both DR and GS.
  # MUST run in a background thread -- sync_all_repos makes HTTP calls
  # that block the game thread, preventing process_xml_data from setting
  # XMLData.name. If Vars is accessed before XMLData.name is set, it
  # loads/saves under scope "DR:" instead of "DR:CharName", overwriting
  # real data with an empty session.
  Thread.new do
    # Wait for XMLData.name to be populated by process_xml_data
    # before touching Vars. 200 x 50ms = 10s max wait.
    200.times do
      break if !XMLData.name.nil? && !XMLData.name.empty?

      sleep 0.05
    end
    Lich::Util::Update.sync_all_repos if !XMLData.name.nil? && !XMLData.name.empty?
  rescue StandardError => e
    Lich.log "repo_sync(login): #{e.class}: #{e.message}"
  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



686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
# File 'documented/games.rb', line 686

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) ⇒ Object



606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'documented/games.rb', line 606

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!(/\s\bspace not found\b\s/, " client='1.0.1.28' ")
      Lich.log "Invalid settingsInfo XML tags fixed to: #{server_string.inspect}"
      @@settings_init_needed = true
      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_buffersObject



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
  @@settings_init_needed = false
  @cli_scripts = false
  @room_number_after_ready = false
  @last_id_shown_room_window = 0
  @game_instance = nil
end

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



718
719
720
# File 'documented/games.rb', line 718

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

.open(host, port) ⇒ TCPSocket

Opens a connection to the specified host and port.

Parameters:

  • host (String)

    the hostname or IP address of the server.

  • port (Integer)

    the port number to connect to.

Returns:

  • (TCPSocket)

    the opened socket.



248
249
250
251
252
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
# File 'documented/games.rb', line 248

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.

Parameters:

  • server_string (String)

    the server string to process.



627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'documented/games.rb', line 627

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

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

    if Frontend.client.eql?('frostbite') && 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.supports_gsl?
      alt_string = sf_to_wiz(alt_string)
    end

    # Send to client
    send_to_client(alt_string)
  end
end

.process_room_information(alt_string) ⇒ Object



656
657
658
659
660
661
662
663
664
# File 'documented/games.rb', line 656

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.

Parameters:

  • server_string (String)

    the string received from the server.



441
442
443
444
445
446
447
448
449
450
451
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
# File 'documented/games.rb', line 441

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/gameloader'
      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)
    return if server_string.nil? # Buffering split component, wait for next line
  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 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 received from the server.

Parameters:

  • server_string (String)

    the XML data to process.



560
561
562
563
564
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
# File 'documented/games.rb', line 560

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 formatted string to the client.

Parameters:

  • str (String)

    the string to send.



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'documented/games.rb', line 340

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



666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
# File 'documented/games.rb', line 666

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
    begin
      $_CLIENT_.write(alt_string)
    rescue Errno::EPIPE, IOError => e
      Lich.log "error: client_thread: #{e}\n\t#{e.backtrace.join("\n\t")}"
    end
  end
end

.set_game_instance(game_type) ⇒ void

This method returns an undefined value.

Sets the game instance based on the provided game type.

Parameters:

  • game_type (String)

    the type of game to instantiate.



240
241
242
# File 'documented/games.rb', line 240

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

.settings_init_needed?Boolean

Checks if settings initialization is needed.

Returns:

  • (Boolean)

    true if settings initialization is required, false otherwise.



217
218
219
# File 'documented/games.rb', line 217

def settings_init_needed?
  @@settings_init_needed
end

.start_cli_scriptsvoid

This method returns an undefined value.

Starts CLI scripts based on command line arguments.



547
548
549
550
551
552
553
554
555
# File 'documented/games.rb', line 547

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.



369
370
371
372
373
374
375
376
377
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
# File 'documented/games.rb', line 369

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_threadvoid

This method returns an undefined value.

Starts a thread to manage wrapping operations.



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

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