Class: Lich::GameBase::Game
- Inherits:
-
Object
- Object
- Lich::GameBase::Game
- Defined in:
- documented/games.rb
Overview
Class representing the game.
This class manages the overall game state and interactions.
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) ⇒ void
Sends a string to the socket.
-
.autostarted? ⇒ Boolean
Checks if the game has been autostarted.
-
.close ⇒ void
Closes the socket and any associated threads.
-
.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
Retrieves a line from the buffer.
-
.handle_autostart ⇒ void
Handles the autostart process for the game.
- .handle_thread_error(error) ⇒ Object
- .handle_xml_error(server_string, error) ⇒ Object
- .initialize_buffers ⇒ Object
- .log_error(message, error) ⇒ Object protected
-
.open(host, port) ⇒ TCPSocket
Opens a 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 received from the server.
-
.puts(str) ⇒ void
Sends a formatted string to the client.
- .send_to_client(alt_string) ⇒ Object
-
.set_game_instance(game_type) ⇒ void
Sets the game instance based on the provided game type.
-
.settings_init_needed? ⇒ Boolean
Checks if settings initialization is needed.
-
.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 ⇒ void
Starts a thread to manage wrapping operations.
Class Attribute Details
._buffer ⇒ Object (readonly)
Returns the value of attribute _buffer.
207 208 209 |
# File 'documented/games.rb', line 207 def _buffer @_buffer end |
.buffer ⇒ Object (readonly)
Returns the value of attribute buffer.
207 208 209 |
# File 'documented/games.rb', line 207 def buffer @buffer end |
.game_instance ⇒ Object (readonly)
Returns the value of attribute game_instance.
207 208 209 |
# File 'documented/games.rb', line 207 def game_instance @game_instance end |
.thread ⇒ Object (readonly)
Returns the value of attribute thread.
207 208 209 |
# File 'documented/games.rb', line 207 def thread @thread end |
Class Method Details
._gets ⇒ Object
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.
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.
211 212 213 |
# File 'documented/games.rb', line 211 def autostarted? @@autostarted end |
.close ⇒ void
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.
312 313 314 |
# File 'documented/games.rb', line 312 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.
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 |
.gets ⇒ String
Retrieves a line from the buffer.
359 360 361 |
# File 'documented/games.rb', line 359 def gets @buffer.gets end |
.handle_autostart ⇒ void
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.}" 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_buffers ⇒ Object
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(, error) Lich.log "#{}: #{error}\n\t#{error.backtrace.join("\n\t")}" end |
.open(host, port) ⇒ TCPSocket
Opens a connection to the specified host and port.
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.
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.
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.
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.(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.
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.
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.
217 218 219 |
# File 'documented/games.rb', line 217 def settings_init_needed? @@settings_init_needed end |
.start_cli_scripts ⇒ void
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_thread ⇒ void
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.}" 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 ⇒ void
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.}" 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 |