Class: Lich::GameBase::Game
- Inherits:
-
Object
- Object
- Lich::GameBase::Game
- 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
Class Attribute Summary collapse
-
._buffer ⇒ Object
readonly
Returns the value of attribute _buffer.
-
.buffer ⇒ Object
readonly
Returns the value of attribute buffer.
-
.game_instance ⇒ Object
readonly
Returns the value of attribute game_instance.
-
.thread ⇒ Object
readonly
Returns the value of attribute thread.
Class Method Summary collapse
- ._gets ⇒ Object
- ._puts(str) ⇒ Object
-
.close ⇒ void
Closes the socket connection.
-
.closed? ⇒ Boolean
Checks if the socket is closed.
-
.display_ruby_warning ⇒ void
Displays a warning if the Ruby version is below the recommended version.
-
.gets ⇒ String
Receives a string from the server.
-
.handle_autostart ⇒ void
Handles the autostart process for the game.
- .handle_thread_error(error) ⇒ Object
-
.handle_xml_error(server_string, error) ⇒ void
Handles XML errors during processing.
-
.initialize_buffers ⇒ void
Initializes the buffers for the game Sets up necessary buffers for socket communication and game state.
- .log_error(message, error) ⇒ Object protected
-
.open(host, port) ⇒ TCPSocket
Opens a socket connection to the specified host and port.
-
.process_downstream_hooks(server_string) ⇒ void
Processes downstream hooks for the server string.
- .process_room_information(alt_string) ⇒ Object
-
.process_server_string(server_string) ⇒ void
Processes the server string received from the socket.
-
.process_xml_data(server_string) ⇒ void
Processes XML data from the server string.
-
.puts(str) ⇒ void
Sends a string to the server.
- .send_to_client(alt_string) ⇒ Object
-
.set_game_instance(game_type) ⇒ void
Sets the game instance based on the game type.
-
.start_cli_scripts ⇒ void
Starts CLI scripts based on command line arguments.
-
.start_main_thread ⇒ void
Starts the main thread for handling server communication.
- .start_wrap_thread ⇒ Object
Class Attribute Details
._buffer ⇒ Object (readonly)
Returns the value of attribute _buffer.
214 215 216 |
# File 'documented/games.rb', line 214 def _buffer @_buffer end |
.buffer ⇒ Object (readonly)
Returns the value of attribute buffer.
214 215 216 |
# File 'documented/games.rb', line 214 def buffer @buffer end |
.game_instance ⇒ Object (readonly)
Returns the value of attribute game_instance.
214 215 216 |
# File 'documented/games.rb', line 214 def game_instance @game_instance end |
.thread ⇒ Object (readonly)
Returns the value of attribute thread.
214 215 216 |
# File 'documented/games.rb', line 214 def thread @thread end |
Class Method Details
._gets ⇒ Object
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 |
.close ⇒ void
This method returns an undefined value.
Closes the socket connection
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
319 320 321 |
# File 'documented/games.rb', line 319 def closed? @socket.nil? || @socket.closed? end |
.display_ruby_warning ⇒ void
This method returns an undefined value.
Displays a warning if the Ruby version is below the recommended version
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 |
.gets ⇒ String
Receives a string from the server
366 367 368 |
# File 'documented/games.rb', line 366 def gets @buffer.gets end |
.handle_autostart ⇒ void
This method returns an undefined value.
Handles the autostart process for the game
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
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_buffers ⇒ void
This method returns an undefined value.
Initializes the buffers for the game Sets up necessary buffers for socket communication and game state.
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(, error) Lich.log "#{}: #{error}\n\t#{error.backtrace.join("\n\t")}" end |
.open(host, port) ⇒ TCPSocket
Opens a socket connection to the specified host and port
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
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
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
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.(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
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
242 243 244 |
# File 'documented/games.rb', line 242 def set_game_instance(game_type) @game_instance = GameInstanceFactory.create(game_type) end |
.start_cli_scripts ⇒ void
This method returns an undefined value.
Starts CLI scripts based on command line arguments
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_thread ⇒ void
This method returns an undefined value.
Starts the main thread for handling server communication
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.}" 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_thread ⇒ Object
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.}" 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 |