Class: Lich::GameBase::Game
- Inherits:
-
Object
- Object
- Lich::GameBase::Game
- Defined in:
- documented/games.rb
Overview
Base Game class with common functionality
Direct Known Subclasses
Defined Under Namespace
Classes: ServerQueueOverflow
Constant Summary collapse
- READ_TIMEOUT_SECONDS =
Seconds to wait for readable game socket data before one read timeout.
100- MAX_CONSECUTIVE_READ_TIMEOUTS =
Consecutive read timeouts allowed before treating the game link as dead.
3- READ_TIMEOUT =
Sentinel returned when the game socket has no readable data before timeout.
Object.new.freeze
- SERVER_QUEUE_CAPACITY =
A full queue means the parser cannot preserve the game stream. Dropping records or blocking the socket reader would both make recovery unsafe.
4_096
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.
-
.reader_thread ⇒ Object
readonly
Returns the value of attribute reader_thread.
-
.server_queue ⇒ Object
readonly
Returns the value of attribute server_queue.
-
.thread ⇒ Object
readonly
Returns the value of attribute thread.
Class Method Summary collapse
-
._gets ⇒ String?
private
Retrieves the next line from the testing/debug buffer.
-
._puts(str) ⇒ true?
Writes a string to the game server socket.
-
.autostarted? ⇒ Boolean
private
Returns whether the autostart sequence has completed.
-
.check_stream_desync!(parse_errors) ⇒ Object
Promote truncation-class Ox parse errors to GameStreamDesyncError so a desynced stream still hits the log + reset recovery path instead of being silently absorbed (see the GameStreamDesyncError comment).
-
.close ⇒ void
private
Closes the game socket and kills the reader and parser threads.
-
.closed? ⇒ Boolean
private
Returns whether the game socket is closed or nil.
-
.connection_disruption_log_message(error) ⇒ String
private
Formats a connection disruption error for logging.
-
.display_ruby_warning ⇒ void
private
Displays a formatted terminal table warning if the current Ruby version is below the recommended minimum.
-
.enqueue_server_string(server_string, enqueued_monotonic_at) ⇒ void
private
Adds a server string to the processing queue with a monotonic timestamp.
-
.fix_invalid_settings_info(server_string) ⇒ Object
The server sends a malformed <settingsInfo ...
-
.gets ⇒ String?
private
Retrieves the next line from the main game buffer.
-
.handle_autostart ⇒ void
private
Executes the autostart sequence: checks for version updates, syncs script repos, and launches the autostart script.
-
.handle_thread_error(error) ⇒ Boolean
private
Evaluates a thread error to determine whether the server thread should retry.
-
.initialize_buffers ⇒ void
private
Initializes or resets all socket, queue, and buffer state.
-
.intentional_shutdown_close_error?(error) ⇒ Boolean
private
Detects whether an error is a normal consequence of orderly user shutdown.
- .log_error(message, error) ⇒ Object protected
-
.open(host, port) ⇒ TCPSocket
Opens the TCP connection to the game server and starts the socket's wrap and main reader threads.
-
.open_with_timeout(host, port, timeout = 30) ⇒ void
Connects to the game server on a background thread, enforcing a connect timeout that a bare Game.open cannot.
-
.prefix_origin_sentinel(string) ⇒ String
private
Prefixes each line of a string with the frontend origin sentinel.
-
.process_downstream_hooks(server_string) ⇒ void
private
Runs downstream hooks and sends the modified server string to connected clients.
-
.process_room_information(alt_string) ⇒ void
private
Detects and processes room-name style tags to trigger room display modifications.
-
.process_server_string(server_string) ⇒ void
private
Main entry point for processing a server string: validates game state, cleans the string, parses XML, triggers downstream hooks, and manages autostart.
-
.process_xml_data(server_string) ⇒ void
private
Parses the server string as XML using Ox in SAX mode and updates XMLData.
-
.puts(str) ⇒ void
private
Sends a command to the game server and logs it to the client buffer.
-
.read_server_string(read_timeout: READ_TIMEOUT_SECONDS) ⇒ String, ...
Reads one game-server line after an explicit readiness wait.
-
.recognized_connection_disruption?(error) ⇒ Boolean
private
Tests whether an error represents a known connection disruption.
-
.record_server_parser_timing(parse_time) ⇒ nil
private
Records performance metrics for XML parsing and processing.
-
.record_server_queue_dequeue(enqueued_monotonic_at = nil) ⇒ nil
private
Records metrics when a server string is removed from the queue.
-
.record_server_queue_enqueue ⇒ nil
private
Records metrics when a server string is added to the queue.
-
.record_server_reader_timing(hook_time:, enqueue_time:, process_time:) ⇒ nil
private
Records performance metrics for socket read and hook processing.
-
.record_shutdown_reason(reason, source:, detail: nil) ⇒ void
private
Records the shutdown reason with ShutdownCoordinator for orderly exit handling.
-
.remote_eof? ⇒ Boolean
Whether the game server closed its side of the socket.
-
.repair_malformed_attributes_and_reparse(server_string) ⇒ Object
Ox reports "no attribute value" for two repairable malformations that scatter a tag into junk attributes: the settingsInfo space-not-found server bug, and a same-quote inside a quoted value (Simu's dynamic dialogs, e.g. title='Tsetem's Items').
-
.reset_server_queue_stats! ⇒ nil
private
Resets all queue and parser performance statistics to initial state.
-
.send_to_client(alt_string) ⇒ void
private
Transmits a server string to all connected clients.
-
.server_queue_stats(reset: false) ⇒ Hash
private
Returns a snapshot of queue and parser performance metrics.
-
.set_game_instance(game_type) ⇒ void
private
Creates and assigns a game-specific instance based on the game type.
-
.settings_init_needed? ⇒ Boolean
private
Returns whether a new settings record needs to be initialized for the character.
-
.shutdown_log ⇒ Lich::Common::ShutdownLog
private
Returns the logger for shutdown-related messages.
-
.shutdown_reason_for_thread_exit(error) ⇒ Symbol
private
Maps an error to a shutdown reason symbol for shutdown coordination.
-
.start_cli_scripts ⇒ void
private
Launches scripts passed via the --start-scripts command-line argument.
-
.start_main_thread ⇒ void
private
Initializes the server queue and starts the socket reader and parser threads.
-
.start_server_processor_thread ⇒ void
private
Starts the background thread that dequeues server strings and processes them.
-
.start_socket_reader_thread ⇒ void
private
Starts the background thread that reads lines from the game socket and enqueues them.
-
.start_wrap_thread ⇒ void
private
Starts the wrap thread that issues an initial "look" command after login.
-
.total_read_timeout_seconds(timeout_count = MAX_CONSECUTIVE_READ_TIMEOUTS) ⇒ Integer
Total elapsed no-data seconds represented by count.
-
.unwrap_server_queue_item(item) ⇒ Array(String, Numeric, nil)
private
Extracts a server string and its monotonic timestamp from a queue item.
Class Attribute Details
._buffer ⇒ Object (readonly)
Returns the value of attribute _buffer.
447 448 449 |
# File 'documented/games.rb', line 447 def _buffer @_buffer end |
.buffer ⇒ Object (readonly)
Returns the value of attribute buffer.
447 448 449 |
# File 'documented/games.rb', line 447 def buffer @buffer end |
.game_instance ⇒ Object (readonly)
Returns the value of attribute game_instance.
447 448 449 |
# File 'documented/games.rb', line 447 def game_instance @game_instance end |
.reader_thread ⇒ Object (readonly)
Returns the value of attribute reader_thread.
447 448 449 |
# File 'documented/games.rb', line 447 def reader_thread @reader_thread end |
.server_queue ⇒ Object (readonly)
Returns the value of attribute server_queue.
447 448 449 |
# File 'documented/games.rb', line 447 def server_queue @server_queue end |
.thread ⇒ Object (readonly)
Returns the value of attribute thread.
447 448 449 |
# File 'documented/games.rb', line 447 def thread @thread end |
Class Method Details
._gets ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Retrieves the next line from the testing/debug buffer.
794 795 796 |
# File 'documented/games.rb', line 794 def _gets @_buffer.gets end |
._puts(str) ⇒ true?
Writes a string to the game server socket.
Thread-safe via mutex. Silently absorbs fatal connection errors so callers (scripts) are not killed by a broken server link.
747 748 749 750 751 752 753 754 755 |
# File 'documented/games.rb', line 747 def _puts(str) @mutex.synchronize do @socket.puts(str) end true rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED, IOError => e Lich.log "error: _puts: #{e}\n\t#{e.backtrace.first}" nil end |
.autostarted? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns whether the autostart sequence has completed.
453 454 455 |
# File 'documented/games.rb', line 453 def autostarted? @@autostarted end |
.check_stream_desync!(parse_errors) ⇒ Object
Promote truncation-class Ox parse errors to GameStreamDesyncError so a desynced stream still hits the log + reset recovery path instead of being silently absorbed (see the GameStreamDesyncError comment). parse_errors is XMLData's collected Ox error-callback messages for the fragment just parsed.
1260 1261 1262 1263 1264 1265 |
# File 'documented/games.rb', line 1260 def check_stream_desync!(parse_errors) desync = parse_errors.find do || STREAM_DESYNC_ERRORS.any? { |pattern| pattern.match?() } end raise GameStreamDesyncError, desync if desync end |
.close ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Closes the game socket and kills the reader and parser threads.
732 733 734 735 736 737 738 |
# File 'documented/games.rb', line 732 def close if @socket @socket.close rescue nil @reader_thread.kill rescue nil @thread.kill rescue nil end end |
.closed? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns whether the game socket is closed or nil.
628 629 630 |
# File 'documented/games.rb', line 628 def closed? @socket.nil? || @socket.closed? end |
.connection_disruption_log_message(error) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Formats a connection disruption error for logging.
1517 1518 1519 1520 1521 |
# File 'documented/games.rb', line 1517 def (error) return "GameStreamDesyncError: #{error..lines.first&.strip}" if error.is_a?(GameStreamDesyncError) "#{error.class}: #{error.}" end |
.display_ruby_warning ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Displays a formatted terminal table warning if the current Ruby version is below the recommended minimum.
Called from handle_autostart if a RECOMMENDED_RUBY version is defined and the running version is older.
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 |
# File 'documented/games.rb', line 1161 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 |
.enqueue_server_string(server_string, enqueued_monotonic_at) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Adds a server string to the processing queue with a monotonic timestamp.
831 832 833 834 835 836 |
# File 'documented/games.rb', line 831 def enqueue_server_string(server_string, enqueued_monotonic_at) @server_queue.push([server_string, enqueued_monotonic_at], true) record_server_queue_enqueue rescue ThreadError raise ServerQueueOverflow, "game parser queue exceeded #{SERVER_QUEUE_CAPACITY} records" end |
.fix_invalid_settings_info(server_string) ⇒ Object
The server sends a malformed <settingsInfo ... space not found .../> (an attribute with no '=') to characters that have never connected with the Wrayth/StormFront client. REXML raised on it (the rescue repaired it); Ox tolerates it and emits "no attribute value", so it is repaired from repair_malformed_attributes_and_reparse. @@settings_init_needed makes gameloader's PostLoad seed a valid client record (see settings_init_needed? and lib/common/gameloader.rb).
1297 1298 1299 1300 1301 1302 1303 1304 |
# File 'documented/games.rb', line 1297 def fix_invalid_settings_info(server_string) return unless 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 end |
.gets ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Retrieves the next line from the main game buffer.
786 787 788 |
# File 'documented/games.rb', line 786 def gets @buffer.gets end |
.handle_autostart ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Executes the autostart sequence: checks for version updates, syncs script repos, and launches the autostart script.
Runs once when the first
1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 |
# File 'documented/games.rb', line 1119 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) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Evaluates a thread error to determine whether the server thread should retry.
Logs recognized connection disruptions (timeouts, resets) at info level; logs other errors at error level with backtrace. Timeouts after max retries, connection errors, stream desync, and queue overflow are fatal (no retry). Unknown errors retry if the socket/client are still alive.
1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 |
# File 'documented/games.rb', line 1392 def handle_thread_error(error) if recognized_connection_disruption?(error) shutdown_log.info("server_thread: #{(error)}") shutdown_log.debug("server_thread backtrace: #{error.backtrace.join("\n\t")}") if error.backtrace else shutdown_log.error("server_thread: #{error}\n\t#{Array(error.backtrace).join("\n\t")}") end sleep 0.2 case error when Errno::ETIMEDOUT, Errno::EWOULDBLOCK, IO::TimeoutError # Timeout errors reach this outer handler only after the inner # reader loop has exhausted its consecutive-timeout threshold. shutdown_log.info("game timeout - will not retry") return false when Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED # Connection errors are fatal shutdown_log.info("connection error - will not retry") return false when GameStreamDesyncError shutdown_log.info("game stream desync detected - will not retry") return false when ServerQueueOverflow shutdown_log.info("game parser queue overflow - will not retry") return false else # Check if socket/client are closed or if it's a known fatal error if !$_CLIENT_.alive? || @socket.closed? shutdown_log.info("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 shutdown_log.info("fatal error pattern detected - will not retry") return false else shutdown_log.debug("unknown server thread error - will attempt retry") return true end end end |
.initialize_buffers ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Initializes or resets all socket, queue, and buffer state.
Called at startup and after reconnects to clear stale data.
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 |
# File 'documented/games.rb', line 483 def initialize_buffers @socket = nil @mutex = Mutex.new @last_recv = nil @thread = nil @reader_thread = nil @remote_eof = false @server_queue = SizedQueue.new(SERVER_QUEUE_CAPACITY) reset_server_queue_stats! @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 # strip_xml's multiline carry is a process-global; clear it here so a # fragment left open before a reconnect/session reset does not bleed # into the next session. $strip_xml_multiline = {} end |
.intentional_shutdown_close_error?(error) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Detects whether an error is a normal consequence of orderly user shutdown.
Returns true if the error is a stream-closed error (EBADF, "stream closed", etc.) and ShutdownCoordinator reports an orderly exit is in progress and the socket is closed.
1488 1489 1490 1491 1492 1493 1494 1495 |
# File 'documented/games.rb', line 1488 def intentional_shutdown_close_error?(error) return false unless defined?(Lich::Common::ShutdownCoordinator) return false unless Lich::Common::ShutdownCoordinator.orderly_user_exit? return false unless @socket&.closed? error.is_a?(Errno::EBADF) || error.to_s =~ /stream closed in another thread|closed stream|bad file descriptor/i end |
.log_error(message, error) ⇒ Object (protected)
1525 1526 1527 |
# File 'documented/games.rb', line 1525 def log_error(, error) Lich.log "#{}: #{error}\n\t#{error.backtrace.join("\n\t")}" end |
.open(host, port) ⇒ TCPSocket
Connection errors propagate to the caller. Use open_with_timeout to bound how long the connect may block.
Opens the TCP connection to the game server and starts the socket's wrap and main reader threads.
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 559 560 561 562 563 564 565 566 |
# File 'documented/games.rb', line 525 def open(host, port) @remote_eof = false @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: 30, # 30s idle before first keepalive; defensive against L3/L4 idle reapers (best-effort, see SocketConfigurator) 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 |
.open_with_timeout(host, port, timeout = 30) ⇒ void
This method returns an undefined value.
Connects to the game server on a background thread, enforcing a connect timeout that a bare open cannot. Surfaces a stuck or failed connect instead of letting startup proceed with a dead socket.
580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 |
# File 'documented/games.rb', line 580 def open_with_timeout(host, port, timeout = 30) connect_thread = Thread.new { # report_on_exception off: a failed open is surfaced by the join below # (which re-raises it), not by an auto-printed thread warning. Thread.current.report_on_exception = false self.open(host, port) } # join returns nil on timeout, the thread on success, and re-raises the # thread's exception on failure -- so a Game.open that errors (e.g. # connection refused) propagates to the caller's rescue instead of being # silently swallowed (the old `if connect_thread.status` could not tell a # thread that died with an exception, status nil, from a normal finish, # status false). if connect_thread.join(timeout).nil? connect_thread.kill rescue nil raise "error: timed out connecting to #{host}:#{port}" end end |
.prefix_origin_sentinel(string) ⇒ String
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Prefixes each line of a string with the frontend origin sentinel.
462 463 464 |
# File 'documented/games.rb', line 462 def prefix_origin_sentinel(string) string.gsub(/^.+$/) { |line| "#{Frontend::ORIGIN_SENTINEL}#{line}" } end |
.process_downstream_hooks(server_string) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Runs downstream hooks and sends the modified server string to connected clients.
Processes room information, applies frontend-specific conversions (genie/frostbite room number formatting, GSL translation), calls game-specific room display methods, and sends to all connected detachable clients or the main client.
1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 |
# File 'documented/games.rb', line 1315 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 # Handle prefix origin sentinel if FE supports it alt_string = prefix_origin_sentinel(alt_string) if Frontend.supports_sentinel? # Send to client send_to_client(alt_string) end end |
.process_room_information(alt_string) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Detects and processes room-name style tags to trigger room display modifications.
Sets @room_number_after_ready to true when a roomName style tag is detected, signaling that the next prompt should trigger game-instance room display processing.
1354 1355 1356 1357 1358 1359 1360 1361 1362 |
# File 'documented/games.rb', line 1354 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 is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Main entry point for processing a server string: validates game state, cleans the string, parses XML, triggers downstream hooks, and manages autostart.
1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 |
# File 'documented/games.rb', line 1067 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 is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Parses the server string as XML using Ox in SAX mode and updates XMLData.
Handles stream desync detection (truncated fragments), repairs malformed attributes (nested quotes, settingsInfo bugs), and splits the parsed output into lines for downstream processing. Calls game-specific processing and fires Script hooks.
1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 |
# File 'documented/games.rb', line 1210 def process_xml_data(server_string) begin # Ox is a permissive parser: it handles Simu's not-quite-XML stream # without the clean/retry dance REXML required (nested quotes, missing # 'd' end tags, etc. are tolerated rather than raised). XMLData itself # implements the Ox::Sax interface, so Ox parses straight into it. No # <root> wrapper needed: that was a REXML requirement (single root); Ox # handles multiple top-level elements and bare text directly. XMLData.sax_parse_errors.clear # convert_special: false keeps Ox in bytes-land: it never decodes a # numeric entity (e.g. ’) into UTF-8. The five standard XML # entities are decoded by XMLData#attr/#text instead. Values are left # in Ox's native (ASCII-8BIT) encoding -- REXML effectively produced # ASCII for this (high-byte-scrubbed) stream, so retagging to # Windows-1252 was a divergence and caused entity corruption. Ox.sax_parse(XMLData, server_string, convert_special: false, symbolize: false, skip: :skip_none) check_stream_desync!(XMLData.sax_parse_errors) repair_malformed_attributes_and_reparse(server_string) rescue GameStreamDesyncError => e # A truncated/desynced fragment. Ox never raises on malformed stream # content -- it reports via the error callback, and check_stream_desync! # promotes truncation-class errors to this exception. Log and reset # rather than killing the server thread. Lich.log "warning: stream desync (#{e.}); resetting XMLData: #{server_string.inspect}" XMLData.reset return end stripped_server = strip_xml(server_string, type: "main") # Process game-specific data using instance if @game_instance && Module.const_defined?(:GameLoader) @game_instance.process_game_specific_data(server_string, stripped_server) end # Process downstream XML Script.new_downstream_xml(server_string) if defined?(Script) # Process stripped server string 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 is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Sends a command to the game server and logs it to the client buffer.
Records the command in $CLIENTBUFFER, echoes to the user unless the script is silent, updates $LASTUPSTREAM, and forwards to the game via _puts.
765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 |
# File 'documented/games.rb', line 765 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 |
.read_server_string(read_timeout: READ_TIMEOUT_SECONDS) ⇒ String, ...
Reads one game-server line after an explicit readiness wait.
Ruby does not reliably surface SO_RCVTIMEO through TCPSocket#gets on every supported platform. Waiting with IO.select makes the reader's no-data timeout deterministic while preserving gets-based EOF handling.
1049 1050 1051 1052 1053 |
# File 'documented/games.rb', line 1049 def read_server_string(read_timeout: READ_TIMEOUT_SECONDS) return READ_TIMEOUT unless IO.select([@socket], nil, nil, read_timeout) @socket.gets end |
.recognized_connection_disruption?(error) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Tests whether an error represents a known connection disruption.
1502 1503 1504 1505 1506 1507 1508 1509 1510 |
# File 'documented/games.rb', line 1502 def recognized_connection_disruption?(error) error.is_a?(Errno::ETIMEDOUT) || error.is_a?(Errno::EWOULDBLOCK) || error.is_a?(IO::TimeoutError) || error.is_a?(Errno::ECONNRESET) || error.is_a?(Errno::EPIPE) || error.is_a?(Errno::ECONNABORTED) || error.is_a?(GameStreamDesyncError) end |
.record_server_parser_timing(parse_time) ⇒ nil
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Records performance metrics for XML parsing and processing.
889 890 891 892 893 894 |
# File 'documented/games.rb', line 889 def record_server_parser_timing(parse_time) @server_parser_last = parse_time @server_parser_total = @server_parser_total.to_f + parse_time.to_f @server_parser_max = parse_time if parse_time.to_f > @server_parser_max.to_f nil end |
.record_server_queue_dequeue(enqueued_monotonic_at = nil) ⇒ nil
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Records metrics when a server string is removed from the queue.
Increments dequeue count, updates queue depth, and calculates queue wait time if a monotonic timestamp was provided.
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 |
# File 'documented/games.rb', line 868 def record_server_queue_dequeue(enqueued_monotonic_at = nil) @server_queue_dequeued = @server_queue_dequeued.to_i + 1 depth = @server_queue&.length.to_i @server_queue_last_depth = depth @server_queue_last_dequeue_at = Time.now if enqueued_monotonic_at wait = Process.clock_gettime(Process::CLOCK_MONOTONIC) - enqueued_monotonic_at.to_f if wait >= 0.0 @server_queue_last_wait = wait @server_queue_total_wait = @server_queue_total_wait.to_f + wait @server_queue_max_wait = wait if wait > @server_queue_max_wait.to_f end end nil end |
.record_server_queue_enqueue ⇒ nil
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Records metrics when a server string is added to the queue.
Increments enqueue count, updates queue depth and max depth, and records the timestamp.
815 816 817 818 819 820 821 822 |
# File 'documented/games.rb', line 815 def record_server_queue_enqueue @server_queue_enqueued = @server_queue_enqueued.to_i + 1 depth = @server_queue&.length.to_i @server_queue_last_depth = depth @server_queue_max_depth = depth if depth > @server_queue_max_depth.to_i @server_queue_last_enqueue_at = Time.now nil end |
.record_server_reader_timing(hook_time:, enqueue_time:, process_time:) ⇒ nil
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Records performance metrics for socket read and hook processing.
845 846 847 848 849 850 851 852 853 854 855 856 857 858 |
# File 'documented/games.rb', line 845 def record_server_reader_timing(hook_time:, enqueue_time:, process_time:) @server_reader_hook_last = hook_time @server_reader_hook_total = @server_reader_hook_total.to_f + hook_time.to_f @server_reader_hook_max = hook_time if hook_time.to_f > @server_reader_hook_max.to_f @server_reader_enqueue_last = enqueue_time @server_reader_enqueue_total = @server_reader_enqueue_total.to_f + enqueue_time.to_f @server_reader_enqueue_max = enqueue_time if enqueue_time.to_f > @server_reader_enqueue_max.to_f @server_reader_process_last = process_time @server_reader_process_total = @server_reader_process_total.to_f + process_time.to_f @server_reader_process_max = process_time if process_time.to_f > @server_reader_process_max.to_f nil end |
.record_shutdown_reason(reason, source:, detail: nil) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Records the shutdown reason with ShutdownCoordinator for orderly exit handling.
1464 1465 1466 1467 1468 1469 1470 |
# File 'documented/games.rb', line 1464 def record_shutdown_reason(reason, source:, detail: nil) return unless defined?(Lich::Common::ShutdownCoordinator) Lich::Common::ShutdownCoordinator.request(reason: reason, source: source, detail: detail) rescue StandardError => e shutdown_log.warning("failed to record shutdown reason #{reason.inspect}: #{e.class}: #{e.}") end |
.remote_eof? ⇒ Boolean
Returns whether the game server closed its side of the socket.
633 634 635 |
# File 'documented/games.rb', line 633 def remote_eof? @remote_eof == true end |
.repair_malformed_attributes_and_reparse(server_string) ⇒ Object
Ox reports "no attribute value" for two repairable malformations that scatter a tag into junk attributes: the settingsInfo space-not-found server bug, and a same-quote inside a quoted value (Simu's dynamic dialogs, e.g. title='Tsetem's Items'). Both raised in REXML and were repaired in the rescue; Ox tolerates them, so drive the repair off its error report -- only the rare flagged line pays the cost. Apply the repairs; if any changed the line, drop the junk the first parse committed and parse once more, ignoring any errors on that pass so we never loop. Escaped '/" round-trip back to the literal char via XmlEntities.decode and the front-end's own entity decoding.
1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 |
# File 'documented/games.rb', line 1277 def repair_malformed_attributes_and_reparse(server_string) return unless XMLData.sax_parse_errors.any? { || NO_ATTRIBUTE_VALUE_ERROR.match?() } before = server_string.dup fix_invalid_settings_info(server_string) XMLCleaner.clean_nested_quotes(server_string) return if server_string == before # nothing to repair (e.g. a genuine valueless attribute) XMLData.reset XMLData.sax_parse_errors.clear Ox.sax_parse(XMLData, server_string, convert_special: false, symbolize: false, skip: :skip_none) end |
.reset_server_queue_stats! ⇒ nil
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resets all queue and parser performance statistics to initial state.
Called at startup and can be called during operation to clear accumulated metrics.
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 643 def reset_server_queue_stats! @server_queue_enqueued = 0 @server_queue_dequeued = 0 @server_queue_last_depth = @server_queue&.length.to_i @server_queue_max_depth = @server_queue_last_depth @server_queue_last_enqueue_at = nil @server_queue_last_dequeue_at = nil @server_queue_last_wait = nil @server_queue_max_wait = 0.0 @server_queue_total_wait = 0.0 @server_reader_hook_last = nil @server_reader_hook_max = 0.0 @server_reader_hook_total = 0.0 @server_reader_enqueue_last = nil @server_reader_enqueue_max = 0.0 @server_reader_enqueue_total = 0.0 @server_reader_process_last = nil @server_reader_process_max = 0.0 @server_reader_process_total = 0.0 @server_parser_last = nil @server_parser_max = 0.0 @server_parser_total = 0.0 nil end |
.send_to_client(alt_string) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Transmits a server string to all connected clients.
Sends to detachable clients if any are registered and alive, otherwise sends to the main $CLIENT connection.
1372 1373 1374 1375 1376 1377 1378 1379 1380 |
# File 'documented/games.rb', line 1372 def send_to_client(alt_string) detachable_clients = $_DETACHABLE_CLIENT_REGISTRY_&.snapshot || [] detachable_clients = [$_DETACHABLE_CLIENT_] if detachable_clients.empty? && $_DETACHABLE_CLIENT_ if !detachable_clients.empty? detachable_clients.each { |client| client.write(alt_string) if client.alive? } elsif $_CLIENT_ $_CLIENT_.write(alt_string) end end |
.server_queue_stats(reset: false) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns a snapshot of queue and parser performance metrics.
676 677 678 679 680 681 682 683 684 685 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 715 716 717 718 719 720 721 722 723 724 725 726 |
# File 'documented/games.rb', line 676 def server_queue_stats(reset: false) depth = @server_queue&.length.to_i dequeued = @server_queue_dequeued.to_i last_wait = @server_queue_last_wait max_wait = @server_queue_max_wait.to_f avg_wait = dequeued.positive? ? @server_queue_total_wait.to_f / dequeued : 0.0 enqueued = @server_queue_enqueued.to_i hook_last = @server_reader_hook_last hook_max = @server_reader_hook_max.to_f hook_avg = enqueued.positive? ? @server_reader_hook_total.to_f / enqueued : 0.0 enqueue_last = @server_reader_enqueue_last enqueue_max = @server_reader_enqueue_max.to_f enqueue_avg = enqueued.positive? ? @server_reader_enqueue_total.to_f / enqueued : 0.0 process_last = @server_reader_process_last process_max = @server_reader_process_max.to_f process_avg = enqueued.positive? ? @server_reader_process_total.to_f / enqueued : 0.0 parser_last = @server_parser_last parser_max = @server_parser_max.to_f parser_avg = dequeued.positive? ? @server_parser_total.to_f / dequeued : 0.0 stats = { depth: depth, last_depth: @server_queue_last_depth.to_i, max_depth: [@server_queue_max_depth.to_i, depth].max, enqueued: enqueued, dequeued: dequeued, last_wait: last_wait, max_wait: max_wait, avg_wait: avg_wait, last_wait_ms: last_wait ? (last_wait * 1000.0).round(3) : nil, max_wait_ms: (max_wait * 1000.0).round(3), avg_wait_ms: (avg_wait * 1000.0).round(3), reader_hook_last_ms: hook_last ? (hook_last * 1000.0).round(3) : nil, reader_hook_max_ms: (hook_max * 1000.0).round(3), reader_hook_avg_ms: (hook_avg * 1000.0).round(3), reader_enqueue_last_ms: enqueue_last ? (enqueue_last * 1000.0).round(3) : nil, reader_enqueue_max_ms: (enqueue_max * 1000.0).round(3), reader_enqueue_avg_ms: (enqueue_avg * 1000.0).round(3), reader_process_last_ms: process_last ? (process_last * 1000.0).round(3) : nil, reader_process_max_ms: (process_max * 1000.0).round(3), reader_process_avg_ms: (process_avg * 1000.0).round(3), parser_process_last_ms: parser_last ? (parser_last * 1000.0).round(3) : nil, parser_process_max_ms: (parser_max * 1000.0).round(3), parser_process_avg_ms: (parser_avg * 1000.0).round(3), last_enqueue_at: @server_queue_last_enqueue_at, last_dequeue_at: @server_queue_last_dequeue_at, reader_status: @reader_thread&.status, parser_status: @thread&.status } reset_server_queue_stats! if reset stats end |
.set_game_instance(game_type) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Creates and assigns a game-specific instance based on the game type.
512 513 514 |
# File 'documented/games.rb', line 512 def set_game_instance(game_type) @game_instance = GameInstanceFactory.create(game_type) end |
.settings_init_needed? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns whether a new settings record needs to be initialized for the character.
Set to true when the server sends a malformed settingsInfo tag (from first connection with a client that is not Wrayth/StormFront).
473 474 475 |
# File 'documented/games.rb', line 473 def settings_init_needed? @@settings_init_needed end |
.shutdown_log ⇒ Lich::Common::ShutdownLog
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns the logger for shutdown-related messages.
1476 1477 1478 |
# File 'documented/games.rb', line 1476 def shutdown_log Lich::Common::ShutdownLog end |
.shutdown_reason_for_thread_exit(error) ⇒ Symbol
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Maps an error to a shutdown reason symbol for shutdown coordination.
1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 |
# File 'documented/games.rb', line 1438 def shutdown_reason_for_thread_exit(error) case error when Errno::ETIMEDOUT, Errno::EWOULDBLOCK, IO::TimeoutError :game_timeout when Errno::ECONNRESET :connection_reset when Errno::EPIPE :connection_pipe when Errno::ECONNABORTED :connection_aborted when GameStreamDesyncError :game_stream_desync when ServerQueueOverflow :unrecoverable_game_thread_error else :unrecoverable_game_thread_error end end |
.start_cli_scripts ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Launches scripts passed via the --start-scripts command-line argument.
Parses comma-separated script names and calls Script.start on each. Sets @cli_scripts to true to prevent repeated launches. Logs the character login info.
1191 1192 1193 1194 1195 1196 1197 1198 1199 |
# File 'documented/games.rb', line 1191 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 is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Initializes the server queue and starts the socket reader and parser threads.
802 803 804 805 806 807 |
# File 'documented/games.rb', line 802 def start_main_thread @server_queue = SizedQueue.new(SERVER_QUEUE_CAPACITY) reset_server_queue_stats! start_socket_reader_thread start_server_processor_thread end |
.start_server_processor_thread ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Starts the background thread that dequeues server strings and processes them.
Pops items from server_queue, parses XML, and runs downstream hooks.
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 |
# File 'documented/games.rb', line 1018 def start_server_processor_thread @thread = Thread.new do begin loop do item = @server_queue.pop break if item.nil? server_string, enqueued_monotonic_at = unwrap_server_queue_item(item) record_server_queue_dequeue(enqueued_monotonic_at) parse_started = Process.clock_gettime(Process::CLOCK_MONOTONIC) process_server_string(server_string) record_server_parser_timing(Process.clock_gettime(Process::CLOCK_MONOTONIC) - parse_started) end rescue StandardError => e log_error("Error processing server string", e) record_shutdown_reason(:unrecoverable_game_thread_error, source: :game_parser, detail: e.class) @socket.close rescue nil end end @thread.name = 'game parser' if @thread.respond_to?(:name=) @thread.priority = 4 end |
.start_socket_reader_thread ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Starts the background thread that reads lines from the game socket and enqueues them.
Handles socket timeouts, connection errors, and stream desync detection. Records performance metrics and runs SocketReadHook callbacks on each line received.
916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 |
# File 'documented/games.rb', line 916 def start_socket_reader_thread @reader_thread = Thread.new do consecutive_timeouts = 0 max_consecutive_timeouts = MAX_CONSECUTIVE_READ_TIMEOUTS begin while true begin server_string = read_server_string if server_string.equal?(READ_TIMEOUT) raise IO::TimeoutError, "no game data for #{READ_TIMEOUT_SECONDS} seconds" end consecutive_timeouts = 0 # Break if socket closed (gets returns nil) if server_string.nil? @remote_eof = true record_shutdown_reason(:game_eof, source: :game_reader) break end reader_process_started = Process.clock_gettime(Process::CLOCK_MONOTONIC) received_at = Time.now monotonic_received_at = reader_process_started @last_recv = received_at @_buffer.update(server_string) if defined?(TESTING) && TESTING hook_started = Process.clock_gettime(Process::CLOCK_MONOTONIC) Lich::Common::SocketReadHook.run( server_string, received_at: received_at, monotonic_received_at: monotonic_received_at ) if defined?(Lich::Common::SocketReadHook) hook_finished = Process.clock_gettime(Process::CLOCK_MONOTONIC) enqueue_started = hook_finished enqueue_server_string(server_string, enqueue_started) enqueue_finished = Process.clock_gettime(Process::CLOCK_MONOTONIC) record_server_reader_timing( hook_time: hook_finished - hook_started, enqueue_time: enqueue_finished - enqueue_started, process_time: enqueue_finished - reader_process_started ) rescue Errno::ETIMEDOUT, Errno::EWOULDBLOCK, IO::TimeoutError consecutive_timeouts += 1 shutdown_log.info("socket read timeout #{consecutive_timeouts}/#{max_consecutive_timeouts} (no game data for #{READ_TIMEOUT_SECONDS}s)") if consecutive_timeouts >= max_consecutive_timeouts total_timeout = total_read_timeout_seconds(max_consecutive_timeouts) shutdown_log.warning("game connection timed out after #{max_consecutive_timeouts} consecutive read timeouts (#{total_timeout}s)") raise IO::TimeoutError, "no game data for #{total_timeout} seconds" end # Check if socket is still alive if @socket.closed? shutdown_log.info("game socket is closed; exiting server 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 shutdown_log.info("connection error: #{conn_error.class} - #{conn_error.}") raise conn_error end end rescue StandardError => e if intentional_shutdown_close_error?(e) shutdown_log.info("server thread exiting after orderly user shutdown") next end # 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_.alive? shutdown_log.debug("retrying server thread after error") consecutive_timeouts = 0 # Reset counter on retry sleep 1 # Brief pause before retry retry else reason = shutdown_reason_for_thread_exit(e) record_shutdown_reason(reason, source: :game_reader, detail: e.class) shutdown_log.info("server thread exiting due to #{reason}") end ensure @server_queue << nil if @server_queue && @thread&.alive? end end @reader_thread.name = 'game socket reader' if @reader_thread.respond_to?(:name=) @reader_thread.priority = 5 end |
.start_wrap_thread ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Starts the wrap thread that issues an initial "look" command after login.
Also performs database vacuum if due. Sends "look" unless autostart has already run (or 6 seconds have elapsed with no server activity).
606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 |
# File 'documented/games.rb', line 606 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 |
.total_read_timeout_seconds(timeout_count = MAX_CONSECUTIVE_READ_TIMEOUTS) ⇒ Integer
Returns total elapsed no-data seconds represented by count.
1057 1058 1059 |
# File 'documented/games.rb', line 1057 def total_read_timeout_seconds(timeout_count = MAX_CONSECUTIVE_READ_TIMEOUTS) READ_TIMEOUT_SECONDS * timeout_count end |
.unwrap_server_queue_item(item) ⇒ Array(String, Numeric, nil)
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Extracts a server string and its monotonic timestamp from a queue item.
901 902 903 904 905 906 907 |
# File 'documented/games.rb', line 901 def unwrap_server_queue_item(item) if item.is_a?(Array) && item.length == 2 && item[1].is_a?(Numeric) item else [item, nil] end end |