Class: Lich::GameBase::Game

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

Overview

Base Game class with common functionality Base Game class with common functionality Provides methods and properties for game management

Examples:

Creating a game instance

game = Game.new

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.



235
236
237
# File 'documented/games.rb', line 235

def _buffer
  @_buffer
end

.bufferObject (readonly)

Returns the value of attribute buffer.



235
236
237
# File 'documented/games.rb', line 235

def buffer
  @buffer
end

.game_instanceObject (readonly)

Returns the value of attribute game_instance.



235
236
237
# File 'documented/games.rb', line 235

def game_instance
  @game_instance
end

.threadObject (readonly)

Returns the value of attribute thread.



235
236
237
# File 'documented/games.rb', line 235

def thread
  @thread
end

Class Method Details

._getsString

Retrieves a string from the internal buffer

Examples:

internal_response = Game._gets

Returns:

  • (String)

    The retrieved string



368
369
370
# File 'documented/games.rb', line 368

def _gets
  @_buffer.gets
end

._puts(str) ⇒ void

This method returns an undefined value.

Sends a string to the game server

Examples:

Game._puts("Hello, world!")

Parameters:

  • str (String)

    The string to send



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

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

.closevoid

This method returns an undefined value.

Closes the game connection

Examples:

Game.close


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

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

.closed?Boolean

Checks if the game connection is closed

Examples:

is_closed = Game.closed?

Returns:

  • (Boolean)

    True if the connection is closed, false otherwise



308
309
310
# File 'documented/games.rb', line 308

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

.display_ruby_warningvoid

This method returns an undefined value.

Displays a warning if the Ruby version is outdated

Examples:

Game.display_ruby_warning


472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
# File 'documented/games.rb', line 472

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 string from the game server

Examples:

response = Game.gets

Returns:

  • (String)

    The received string



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

def gets
  @buffer.gets
end

.handle_autostartvoid

This method returns an undefined value.

Handles the autostart process for the game

Examples:

Game.handle_autostart


453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'documented/games.rb', line 453

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

Handles errors that occur in the server thread

Examples:

should_retry = Game.handle_thread_error(error)

Parameters:

  • error (StandardError)

    The error that occurred

Returns:

  • (Boolean)

    True if the thread should retry, false otherwise



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

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
  # Cannot use retry here as it's not in a rescue block
  # Instead, we'll return a boolean indicating whether to retry
  return !($_CLIENT_.closed? || @socket.closed? || (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))
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



566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# File 'documented/games.rb', line 566

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

Examples:

Game.initialize_buffers


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'documented/games.rb', line 241

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)



669
670
671
# File 'documented/games.rb', line 669

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 game server

Examples:

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

Parameters:

  • host (String)

    The host of the game server

  • port (Integer)

    The port of the game server

Returns:

  • (TCPSocket)

    The socket connection to the server

Raises:

  • (StandardError)

    If there is an error opening the socket



273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'documented/games.rb', line 273

def open(host, port)
  @socket = TCPSocket.open(host, port)
  begin
    @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
  rescue StandardError => e
    log_error("Socket option error", e)
  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



588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
# File 'documented/games.rb', line 588

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

Processes room information from the server string

Examples:

processed_string = Game.process_room_information(alt_string)

Parameters:

  • alt_string (String)

    The room display string to process

Returns:

  • (String)

    The processed room display string



622
623
624
625
626
627
628
629
630
# File 'documented/games.rb', line 622

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 game

Examples:

Game.process_server_string(raw_string)

Parameters:

  • server_string (String)

    The server string to process



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
446
447
# File 'documented/games.rb', line 401

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



514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
# File 'documented/games.rb', line 514

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 game server

Examples:

Game.puts("Hello, world!")

Parameters:

  • str (String)

    The string to send



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

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

This method returns an undefined value.

Sends a string to the client

Examples:

Game.send_to_client(alt_string)

Parameters:

  • alt_string (String)

    The string to send to the client



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'documented/games.rb', line 637

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 (e.g., “GS”, “DR”)



262
263
264
# File 'documented/games.rb', line 262

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 if specified

Examples:

Game.start_cli_scripts


499
500
501
502
503
504
505
506
507
# File 'documented/games.rb', line 499

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 processing server messages

Examples:

Game.start_main_thread


376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'documented/games.rb', line 376

def start_main_thread
  @thread = Thread.new do
    begin
      while (server_string = @socket.gets)
        @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
      end
    rescue StandardError => e
      handle_thread_error(e)
    end
  end
  @thread.priority = 4
end

.start_wrap_threadvoid

This method returns an undefined value.

Starts a thread to wrap the game connection

Examples:

Game.start_wrap_thread


292
293
294
295
296
297
298
299
300
301
302
# File 'documented/games.rb', line 292

def start_wrap_thread
  @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