Module: Lich::Common::Frontend

Defined in:
documented/common/front-end.rb

Constant Summary collapse

CLIENT_STRING =

─── Client String ───────────────────────────────────────── Default client string (Wrayth identity) sent during handshake

"/FE:WRAYTH /VERSION:1.0.1.28 /P:WIN_UNKNOWN /XML"
XML_FRONTENDS =

─── Backward-Compatible Constants ───────────────────────── These arrays are derived from the registry for backward compatibility. External code may still reference these constants directly.

frontends_with_capability(:xml).freeze
GSL_FRONTENDS =
frontends_with_capability(:gsl).freeze
STREAM_FRONTENDS =
frontends_with_capability(:streams).freeze
MONO_FRONTENDS =
frontends_with_capability(:mono).freeze

Class Method Summary collapse

Class Method Details

.cleanup_session_filevoid

This method returns an undefined value.

Cleans up (deletes) the current session file if it exists.



236
237
238
239
# File 'documented/common/front-end.rb', line 236

def self.cleanup_session_file
  return if @session_file.nil?
  File.delete(@session_file) if File.exist? @session_file
end

.clientObject



184
185
186
# File 'documented/common/front-end.rb', line 184

def self.client
  $frontend
end

.client=(value) ⇒ Object



188
189
190
# File 'documented/common/front-end.rb', line 188

def self.client=(value)
  $frontend = value
end

.create_session_file(name, host, port, display_session: true) ⇒ void

This method returns an undefined value.

Creates a session file for the specified frontend session.

Parameters:

  • name (String)

    the name of the session

  • host (String)

    the host for the session

  • port (Integer)

    the port for the session

  • display_session (Boolean) (defaults to: true)

    whether to display session information (default: true)



215
216
217
218
219
220
221
222
223
224
# File 'documented/common/front-end.rb', line 215

def self.create_session_file(name, host, port, display_session: true)
  return if name.nil?
  FileUtils.mkdir_p @tmp_session_dir
  @session_file = File.join(@tmp_session_dir, "%s.session" % name.downcase.capitalize)
  session_descriptor = { name: name, host: host, port: port }.to_json
  puts "writing session descriptor to %s\n%s" % [@session_file, session_descriptor] if display_session
  File.open(@session_file, "w") do |fd|
    fd << session_descriptor
  end
end

.detect_pidInteger?

Detects the frontend process ID (PID) based on the launch method.

Returns:

  • (Integer, nil)

    the detected PID or nil if not found



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'documented/common/front-end.rb', line 303

def self.detect_pid
  # Return existing PID if already set
  current_pid = self.pid
  return current_pid if current_pid && current_pid > 0

  # Try to detect based on launch method
  # This is a fallback for cases where init wasn't called
  parent_pid = Process.ppid
  resolved_pid = resolve_pid(parent_pid)

  if resolved_pid && resolved_pid > 0
    self.pid = resolved_pid
    Lich.log "Frontend PID detected (fallback): #{resolved_pid}" if defined?(Lich.log)
    resolved_pid
  else
    Lich.log "Failed to detect frontend PID" if defined?(Lich.log)
    nil
  end
end

.detect_platformSymbol

Detects the current platform (Windows, macOS, Linux, or unsupported).

Returns:

  • (Symbol)

    the detected platform symbol



358
359
360
361
362
363
364
365
# File 'documented/common/front-end.rb', line 358

def self.detect_platform
  case RUBY_PLATFORM
  when /mingw|mswin/ then :windows
  when /darwin/      then :macos
  when /linux/       then :linux
  else                    :unsupported
  end
end

.ensure_windows_modulesBoolean

Ensures that the necessary Windows modules are loaded.

Returns:

  • (Boolean)

    true if modules are loaded, false otherwise



572
573
574
575
576
577
578
# File 'documented/common/front-end.rb', line 572

def self.ensure_windows_modules
  # Check if modules exist - they should be defined at file load time
  if RUBY_PLATFORM =~ /mingw|mswin/
    return defined?(::Win32Enum) && defined?(::WinAPI)
  end
  false
end

.frontends_with_capability(capability) ⇒ Array<String>

Returns a list of frontend names that support a specific capability.

Parameters:

  • capability (Symbol)

    the capability to check for

Returns:

  • (Array<String>)

    an array of frontend names that support the capability



103
104
105
# File 'documented/common/front-end.rb', line 103

def self.frontends_with_capability(capability)
  @registry.select { |_name, data| data[:capabilities].include?(capability.to_sym) }.keys
end

.has_capability?(frontend_name, capability) ⇒ Boolean

Checks if a frontend has a specific capability.

Parameters:

  • frontend_name (String)

    the name of the frontend

  • capability (Symbol)

    the capability to check

Returns:

  • (Boolean)

    true if the frontend has the capability, false otherwise



75
76
77
78
79
# File 'documented/common/front-end.rb', line 75

def self.has_capability?(frontend_name, capability)
  return false if frontend_name.nil?

  @registry[frontend_name.to_s.downcase][:capabilities].include?(capability.to_sym)
end

.init_from_parent(parent_pid) ⇒ Integer

Initializes the frontend from a parent process ID.

Parameters:

  • parent_pid (Integer)

    the parent process ID to initialize from

Returns:

  • (Integer)

    the resolved frontend PID



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/common/front-end.rb', line 262

def self.init_from_parent(parent_pid)
  Lich.log "=== Frontend.init_from_parent called ==="
  Lich.log "Parent process PID: #{parent_pid}"

  # Let's see what process this actually is on Windows
  if RUBY_PLATFORM =~ /mingw|mswin/
    begin
      require 'win32ole'
      wmi = WIN32OLE.connect('winmgmts://')
      rows = wmi.ExecQuery("SELECT Name, ProcessId FROM Win32_Process WHERE ProcessId=#{parent_pid}")
      row = rows.each.first rescue nil
      if row
        Lich.log "Parent process name: #{row.Name}"
      end
    rescue => e
      Lich.log "Could not get parent process name: #{e.message}"
    end
  end

  resolved_pid = resolve_pid(parent_pid)
  Lich.log "resolve_pid(#{parent_pid}) returned: #{resolved_pid}"

  self.pid = resolved_pid
  Lich.log "Frontend PID set to: #{self.pid}"

  resolved_pid
end

.metadata_for(frontend_name, key) ⇒ String?

Retrieves metadata for a registered frontend by key.

Parameters:

  • frontend_name (String)

    the name of the frontend

  • key (String)

    the key of the metadata to retrieve

Returns:

  • (String, nil)

    the metadata value or nil if not found



86
87
88
89
90
# File 'documented/common/front-end.rb', line 86

def self.(frontend_name, key)
  return nil if frontend_name.nil?

  @registry[frontend_name.to_s.downcase][:metadata][key]
end

.pidInteger?

Retrieves the current frontend process ID (PID).

Returns:

  • (Integer, nil)

    the current PID or nil if not set



245
246
247
# File 'documented/common/front-end.rb', line 245

def self.pid
  @pid_mutex.synchronize { @frontend_pid }
end

.pid=(value) ⇒ void

This method returns an undefined value.

Sets the frontend process ID (PID).

Parameters:

  • value (Integer)

    the PID to set



253
254
255
256
# File 'documented/common/front-end.rb', line 253

def self.pid=(value)
  value = value.to_i
  @pid_mutex.synchronize { @frontend_pid = value }
end

.refocusBoolean

Refocuses the frontend window based on the detected platform.

Returns:

  • (Boolean)

    true if refocus was successful, false otherwise



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'documented/common/front-end.rb', line 326

def self.refocus
  pid = self.pid
  return false unless pid && pid > 0

  case detect_platform
  when :windows
    refocus_windows(pid)
  when :macos
    refocus_macos(pid)
  when :linux
    refocus_linux(pid)
  else
    false
  end
end

.refocus_callbackProc

Returns a callback proc for refocusing the frontend window.

Returns:

  • (Proc)

    a proc that refocuses the window when called



345
346
347
348
349
350
351
352
353
# File 'documented/common/front-end.rb', line 345

def self.refocus_callback
  proc {
    if defined?(GLib) && GLib.respond_to?(:Idle)
      GLib::Idle.add(50) { self.refocus; false }
    else
      self.refocus
    end
  }
end

.refocus_linux(pid) ⇒ Boolean

Refocuses a Linux window based on the given process ID (PID).

Parameters:

  • pid (Integer)

    the PID of the window to refocus

Returns:

  • (Boolean)

    true if refocus was successful, false otherwise



553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'documented/common/front-end.rb', line 553

def self.refocus_linux(pid)
  return false unless system('which xdotool > /dev/null 2>&1')

  _stdout, stderr, status = Open3.capture3('xdotool', 'search', '--pid', pid.to_s, 'windowactivate')

  if status.success?
    true
  else
    Lich.log "Error refocusing Linux: #{stderr}" if defined?(Lich.log)
    false
  end
rescue => e
  Lich.log "Error refocusing Linux: #{e}" if defined?(Lich.log)
  false
end

.refocus_macos(pid) ⇒ Boolean

Refocuses a macOS window based on the given process ID (PID).

Parameters:

  • pid (Integer)

    the PID of the window to refocus

Returns:

  • (Boolean)

    true if refocus was successful, false otherwise



532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'documented/common/front-end.rb', line 532

def self.refocus_macos(pid)
  return false unless system('which osascript > /dev/null 2>&1')

  script = %{tell application "System Events" to set frontmost of (first process whose unix id is #{pid}) to true}
  _stdout, stderr, status = Open3.capture3('osascript', '-e', script)

  if status.success?
    true
  else
    Lich.log "Error refocusing macOS: #{stderr}" if defined?(Lich.log)
    false
  end
rescue => e
  Lich.log "Error refocusing macOS: #{e}" if defined?(Lich.log)
  false
end

.refocus_windows(pid) ⇒ Boolean

Refocuses a Windows window based on the given process ID (PID).

Parameters:

  • pid (Integer)

    the PID of the window to refocus

Returns:

  • (Boolean)

    true if refocus was successful, false otherwise



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
519
520
521
522
523
524
525
526
# File 'documented/common/front-end.rb', line 490

def self.refocus_windows(pid)
  ensure_windows_modules

  hwnd_buf = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP)

  enum_cb = Fiddle::Closure::BlockCaller.new(
    Fiddle::TYPE_INT,
    [Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG]
  ) do |hwnd, _|
    next 1 if ::WinAPI.IsWindowVisible(hwnd).zero?

    pid_tmp = [0].pack('L')
    ::WinAPI.GetWindowThreadProcessId(hwnd, pid_tmp)
    win_pid = pid_tmp.unpack1('L')

    if win_pid == pid
      hwnd_buf[0, Fiddle::SIZEOF_VOIDP] = [hwnd].pack('L!')
      0  # stop enumeration
    else
      1  # continue enumeration
    end
  end

  ::WinAPI.EnumWindows(enum_cb, 0)
  hwnd = hwnd_buf[0, Fiddle::SIZEOF_VOIDP].unpack1('L!')

  if hwnd != 0
    ::WinAPI.SetForegroundWindow(hwnd)
    true
  else
    Lich.log "Frontend window for PID #{pid} not found" if defined?(Lich.log)
    false
  end
rescue => e
  Lich.log "Error refocusing Windows: #{e}" if defined?(Lich.log)
  false
end

.register(name, capabilities: [], metadata: {}) ⇒ void

This method returns an undefined value.

Registers a new frontend with the specified capabilities and metadata.

Parameters:

  • name (String)

    the name of the frontend to register

  • capabilities (Array<Symbol>) (defaults to: [])

    a list of capabilities for the frontend

  • metadata (Hash) (defaults to: {})

    additional metadata for the frontend



64
65
66
67
68
# File 'documented/common/front-end.rb', line 64

def self.register(name, capabilities: [], metadata: {})
  entry = @registry[name.to_s.downcase]
  entry[:capabilities].merge(capabilities.map(&:to_sym))
  entry[:metadata].merge!()
end

.registered_frontendsArray<String>

Returns a list of all registered frontend names.

Returns:

  • (Array<String>)

    an array of registered frontend names



95
96
97
# File 'documented/common/front-end.rb', line 95

def self.registered_frontends
  @registry.keys
end

.resolve_linux_pid(pid) ⇒ Integer

Resolves a Linux process ID (PID) to find the correct one based on window visibility.

Parameters:

  • pid (Integer)

    the PID to resolve

Returns:

  • (Integer)

    the resolved PID



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'documented/common/front-end.rb', line 461

def self.resolve_linux_pid(pid)
  return pid unless system('which xdotool > /dev/null 2>&1')

  p = pid
  16.times do
    # Check if this process has a window
    return p if system("xdotool search --pid #{p} >/dev/null 2>&1")

    # Walk up to parent process
    begin
      status = File.read("/proc/#{p}/status")
      parent = status[/PPid:\s+(\d+)/, 1].to_i
    rescue
      parent = 0
    end
    return pid if parent.zero? || parent == p
    p = parent
  end

  pid # fallback
rescue => e
  Lich.log "Error resolving Linux PID: #{e}" if defined?(Lich.log)
  pid
end

.resolve_pid(pid) ⇒ Integer

Resolves a process ID (PID) to find the correct one based on platform.

Parameters:

  • pid (Integer)

    the PID to resolve

Returns:

  • (Integer)

    the resolved PID



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'documented/common/front-end.rb', line 371

def self.resolve_pid(pid)
  pid = pid.to_i
  return pid if pid <= 0 # Return as-is if invalid

  # Use the FrontendPID resolver logic
  case detect_platform
  when :windows
    resolve_windows_pid(pid)
  when :linux
    resolve_linux_pid(pid)
  else
    # macOS/other: PID usually already owns the window
    pid
  end
end

.resolve_windows_pid(pid) ⇒ Integer

Resolves a Windows process ID (PID) to find the correct one based on window visibility.

Parameters:

  • pid (Integer)

    the PID to resolve

Returns:

  • (Integer)

    the resolved PID



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
# File 'documented/common/front-end.rb', line 391

def self.resolve_windows_pid(pid)
  Lich.log "=== resolve_windows_pid starting with PID: #{pid} ==="

  ensure_windows_modules
  require 'win32ole' rescue (return pid)

  begin
    wmi = WIN32OLE.connect('winmgmts://')
    p = pid

    16.times do
      # Get process name for debugging
      rows = wmi.ExecQuery("SELECT Name FROM Win32_Process WHERE ProcessId=#{p}")
      row = rows.each.first rescue nil
      process_name = row ? row.Name : "unknown"
      Lich.log "  Process name: #{process_name}"

      # Check if this process owns any visible window
      found = false
      cb = Fiddle::Closure::BlockCaller.new(
        Fiddle::TYPE_INT,
        [Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG]
      ) do |hwnd, _|
        next 1 if ::Win32Enum.IsWindowVisible(hwnd).zero?
        buf = [0].pack('L')
        ::Win32Enum.GetWindowThreadProcessId(hwnd, buf)
        if buf.unpack1('L') == p
          found = true
          Lich.log "  Found visible window for PID #{p}"
          0  # stop enumeration
        else
          1  # continue enumeration
        end
      end
      ::Win32Enum.EnumWindows(cb, 0)

      if found
        Lich.log "  Stopping at PID #{p} (#{process_name}) - has visible window"
        return p
      end

      # Walk up to parent process
      parent = windows_parent_pid(wmi, p)

      break if parent.nil? || parent.zero? || parent == p
      p = parent
    end
  rescue => e
    Lich.log "ERROR in resolve_windows_pid: #{e}"
  end

  Lich.log "Fallback: returning original PID #{pid}"
  pid
end

.send_handshake(version_string) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'documented/common/front-end.rb', line 192

def self.send_handshake(version_string)
  $_CLIENTBUFFER_.push(version_string.dup)
  Game._puts(version_string)
  2.times do
    sleep 0.3
    $_CLIENTBUFFER_.push("#{$cmd_prefix}\r\n")
    Game._puts($cmd_prefix)
  end
  ["#{$cmd_prefix}_injury 2",
   "#{$cmd_prefix}_flag Display Inventory Boxes 1",
   "#{$cmd_prefix}_flag Display Dialog Boxes 0"].each do |cmd|
    $_CLIENTBUFFER_.push(cmd)
    Game._puts(cmd)
  end
end

.session_file_locationString?

Returns the location of the current session file.

Returns:

  • (String, nil)

    the session file location or nil if not set



229
230
231
# File 'documented/common/front-end.rb', line 229

def self.session_file_location
  @session_file
end

.set_from_client(pid) ⇒ Integer

Sets the frontend PID from the client.

Parameters:

  • pid (Integer)

    the PID to set

Returns:

  • (Integer)

    the set PID



294
295
296
297
298
# File 'documented/common/front-end.rb', line 294

def self.set_from_client(pid)
  self.pid = pid
  Lich.log "Frontend PID set from client: #{pid}" if defined?(Lich.log)
  pid
end

.supports_gsl?(fe = $frontend) ⇒ Boolean

Checks if the specified frontend supports GSL capabilities.

Parameters:

  • fe (String) (defaults to: $frontend)

    the frontend to check (defaults to $frontend)

Returns:

  • (Boolean)

    true if the frontend supports GSL, false otherwise



156
157
158
# File 'documented/common/front-end.rb', line 156

def self.supports_gsl?(fe = $frontend)
  has_capability?(fe, :gsl)
end

.supports_mono?(fe = $frontend) ⇒ Boolean

Checks if the specified frontend supports mono capabilities.

Parameters:

  • fe (String) (defaults to: $frontend)

    the frontend to check (defaults to $frontend)

Returns:

  • (Boolean)

    true if the frontend supports mono, false otherwise



172
173
174
# File 'documented/common/front-end.rb', line 172

def self.supports_mono?(fe = $frontend)
  has_capability?(fe, :mono)
end

.supports_room_window?(fe = $frontend) ⇒ Boolean

Checks if the specified frontend supports room window capabilities.

Parameters:

  • fe (String) (defaults to: $frontend)

    the frontend to check (defaults to $frontend)

Returns:

  • (Boolean)

    true if the frontend supports room window, false otherwise



180
181
182
# File 'documented/common/front-end.rb', line 180

def self.supports_room_window?(fe = $frontend)
  has_capability?(fe, :room_window)
end

.supports_streams?(fe = $frontend) ⇒ Boolean

Checks if the specified frontend supports streams capabilities.

Parameters:

  • fe (String) (defaults to: $frontend)

    the frontend to check (defaults to $frontend)

Returns:

  • (Boolean)

    true if the frontend supports streams, false otherwise



164
165
166
# File 'documented/common/front-end.rb', line 164

def self.supports_streams?(fe = $frontend)
  has_capability?(fe, :streams)
end

.supports_xml?(fe = $frontend) ⇒ Boolean

Checks if the specified frontend supports XML capabilities.

Parameters:

  • fe (String) (defaults to: $frontend)

    the frontend to check (defaults to $frontend)

Returns:

  • (Boolean)

    true if the frontend supports XML, false otherwise



148
149
150
# File 'documented/common/front-end.rb', line 148

def self.supports_xml?(fe = $frontend)
  has_capability?(fe, :xml)
end

.windows_parent_pid(wmi, pid) ⇒ Integer

Retrieves the parent process ID for a given Windows process ID.

Parameters:

  • wmi (WIN32OLE)

    the WMI connection object

  • pid (Integer)

    the PID to check

Returns:

  • (Integer)

    the parent process ID or 0 if not found



451
452
453
454
455
# File 'documented/common/front-end.rb', line 451

def self.windows_parent_pid(wmi, pid)
  rows = wmi.ExecQuery("SELECT ParentProcessId FROM Win32_Process WHERE ProcessId=#{pid}")
  row = rows.each.first rescue nil
  row ? row.ParentProcessId.to_i : 0
end