Module: Lich::Common::Frontend Private

Defined in:
documented/common/frontend.rb,
documented/common/frontend.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

The main Frontend module (continued from earlier namespace definition).

This reopening contains the runtime registry implementation, user configuration management, capability queries, and platform-specific window focus logic.

Constant Summary collapse

PLATFORM_KEYS =

Canonical platform keys used by frontend discovery and launch-plan metadata.

Returns:

  • (Array<Symbol>) —

    :darwin (macOS), :linux, :windows, or :unsupported

%i[darwin linux windows unsupported].freeze
CAPABILITY_VOCABULARY =

Valid capability symbols that frontends may declare.

  • :xml – frontend supports the XML stream protocol
  • :gsl – frontend supports GSL (Gemstone Scripting Language)
  • :streams – frontend supports streaming data (Avalon, Stormfront)
  • :mono – frontend supports monospace text rendering
  • :room_window – frontend can display the room window independently
  • :sentinel – frontend is the Sentinel client

Returns:

  • (Array<Symbol>)
%i[xml gsl streams mono room_window sentinel].freeze
BUILT_IN_DEFINITION_FILES =

Manifest of built-in frontend definitions.

Each tuple is [frontend_id, filename] where frontend_id is the stable catalog identifier and filename is the relative path (without .rb) in the frontend/ directory. Order is preserved for the GTK launcher UI and legacy capability constants.

Returns:

[
  %w[stormfront wrayth],
  %w[profanity profanity],
  %w[genie genie],
  %w[frostbite frostbite],
  %w[suks suks],
  %w[wizard wizard],
  %w[avalon avalon],
  %w[saga saga]
].freeze
BUILT_IN_FRONTEND_IDS =

Canonical identifiers for Lich's built-in frontend catalog.

These identifiers are reserved and cannot be overridden by user-defined frontends. User configuration may only supply launch-field overrides for built-ins, not replace identity or protocol capabilities.

Returns:

BUILT_IN_DEFINITION_FILES.map(&:first).freeze
ORIGIN_SENTINEL =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

"\x1f"
SAGA_LICH_LAUNCH_ENVIRONMENT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Environment variables for the Saga launcher on Linux.

Extracted from the built-in Saga definition at module load time. Set when launching Saga to ensure proper library paths and runtime configuration.

Returns:

  • (Hash, nil) —

    the launch_plans.linux.environment hash from the Saga definition, or nil if Saga is not in the catalog

definition_for(:saga)
.dig(:metadata, :launch_plans, :linux, :environment)
CLIENT_STRING =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

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

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

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

--- Backward-Compatible Constants ------------------------- These frozen arrays preserve the built-in capability membership exposed by older Lich versions. They intentionally remain stable when user-defined frontends are loaded. Runtime callers that need current custom frontend membership must use frontends_with_capability or supports_*? instead.

frontends_with_capability(:xml).freeze
GSL_FRONTENDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Cached list of frontends that support GSL (Gemstone Scripting Language).

This list is frozen at module load and does not update when user-defined frontends are loaded. Scripts requiring current custom frontend membership must use .frontends_with_capability(:gsl) instead.

Returns:

  • (Array<String>) —

    frontend identifiers and aliases with GSL capability

frontends_with_capability(:gsl).freeze
STREAM_FRONTENDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Cached list of frontends that support streaming data.

This list is frozen at module load and does not update when user-defined frontends are loaded. Scripts requiring current custom frontend membership must use .frontends_with_capability(:streams) instead.

Returns:

  • (Array<String>) —

    frontend identifiers and aliases with streams capability

frontends_with_capability(:streams).freeze
MONO_FRONTENDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Cached list of frontends that support monospace text rendering.

This list is frozen at module load and does not update when user-defined frontends are loaded. Scripts requiring current custom frontend membership must use .frontends_with_capability(:mono) instead.

Returns:

  • (Array<String>) —

    frontend identifiers and aliases with mono capability

frontends_with_capability(:mono).freeze
SENTINEL_FRONTENDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Cached list of frontends that are the Sentinel client.

This list is frozen at module load and does not update when user-defined frontends are loaded. Scripts requiring current custom frontend membership must use .frontends_with_capability(:sentinel) instead.

Returns:

  • (Array<String>) —

    frontend identifiers and aliases with sentinel capability

frontends_with_capability(:sentinel).freeze

Class Method Summary collapse

Class Method Details

.built_in_frontends ⇒ Array<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.

Returns the stable identifiers owned by Lich's built-in catalog. User configuration may override only explicitly supported launch fields; it cannot replace built-in identity, protocol capabilities, or defaults.

Returns:



193
194
195
# File 'documented/common/frontend.rb', line 193

def self.built_in_frontends
  BUILT_IN_FRONTEND_IDS
end

.canonical_name(frontend_name) ⇒ 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.

Returns the stable catalog identifier for a frontend or alias. Unknown values are normalized but are not registered.

Parameters:

  • frontend_name (String, Symbol)

Returns:



329
330
331
332
333
334
# File 'documented/common/frontend.rb', line 329

def self.canonical_name(frontend_name)
  registry_synchronize do
    key = frontend_name.to_s.downcase
    @aliases.fetch(key, key)
  end
end

.capability_vocabulary ⇒ Array<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.

Returns the capabilities users may declare for custom frontends. Runtime-only implementation details are deliberately excluded.

Returns:

  • (Array<Symbol>)


185
186
187
# File 'documented/common/frontend.rb', line 185

def self.capability_vocabulary
  CAPABILITY_VOCABULARY
end

.cleanup_session_file ⇒ 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.

Deletes the session descriptor file created by .create_session_file.

Safe to call multiple times or when no session file exists.



741
742
743
744
# File 'documented/common/frontend.rb', line 741

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

.client ⇒ Object

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.

Accessor for the current frontend identity ($frontend global)



678
679
680
# File 'documented/common/frontend.rb', line 678

def self.client
  $frontend
end

.client=(value) ⇒ Object

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.

Setter for the current frontend identity



683
684
685
# File 'documented/common/frontend.rb', line 683

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

.create_session_file(name, host, port, display_session: true) ⇒ 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.

Writes a session descriptor file for detachable clients (e.g., Saga).

Creates the file at %TEMP%/simutronics/sessions/Name.session on Windows or /tmp/simutronics/sessions/Name.session on Unix, containing a JSON object with name, host, and port. Stores the path internally for later cleanup.

Examples:

Frontend.create_session_file("Dragonrealms", "dr.simutronics.net", 4901)

Parameters:

  • name (String, nil) —

    the session name (e.g., "Dragonrealms"); nil skips file creation

  • host (String) —

    the game server hostname

  • port (Integer) —

    the game server port

  • display_session (Boolean) (defaults to: true) —

    if true, prints the file path and contents to stdout



718
719
720
721
722
723
724
725
726
727
# File 'documented/common/frontend.rb', line 718

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

.definition_for(frontend_name) ⇒ 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 an immutable catalog definition for a registered frontend.

Accepted inputs are a non-empty String or Symbol naming an existing registry entry. Invalid or unknown identifiers raise ArgumentError. This method performs no discovery and persists nothing.

Parameters:

  • frontend_name (String, Symbol)

Returns:

Raises:

  • (ArgumentError) —

    if frontend_name is blank or unregistered



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

def self.definition_for(frontend_name)
  registry_synchronize do
    key = canonical_name(frontend_name)
    raise ArgumentError, 'frontend name must not be empty' if key.empty?
    raise ArgumentError, "unknown frontend: #{frontend_name}" unless @registry.key?(key)

    @definitions[key] ||= Lich::Util.deep_freeze(
      {
        id: key,
        capabilities: @registry.fetch(key)[:capabilities].to_a,
        metadata: deep_copy(@registry.fetch(key)[:metadata])
      }
    )
  end
end

.definitions(gui_selectable: nil) ⇒ Array<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 immutable catalog definitions, optionally restricted to those intended for the graphical launcher.

Parameters:

  • gui_selectable (Boolean, nil) (defaults to: nil)

Returns:



391
392
393
394
395
396
397
398
399
400
# File 'documented/common/frontend.rb', line 391

def self.definitions(gui_selectable: nil)
  registry_synchronize do
    definitions = @registry.keys.map { |name| definition_for(name) }
    next definitions if gui_selectable.nil?

    definitions.select do |definition|
      definition.dig(:metadata, :gui_selectable) == gui_selectable
    end
  end
end

.detect_pid ⇒ Integer?

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.

Detect and store the frontend process ID Uses various methods depending on how Lich was launched

Returns:

  • (Integer, nil) —

    The detected PID or nil if detection fails



804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
# File 'documented/common/frontend.rb', line 804

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_platform ⇒ 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.

Detect the current platform

Returns:

  • (Symbol) —

    :windows, :macos, :linux, or :unsupported



856
857
858
859
# File 'documented/common/frontend.rb', line 856

def self.detect_platform
  key = platform_key
  key == :darwin ? :macos : key
end

.display_name(frontend_name) ⇒ 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.

Returns the catalog display name, with a stable fallback for legacy saved entries that predate the catalog.

Parameters:

  • frontend_name (String, Symbol)

Returns:



437
438
439
440
441
# File 'documented/common/frontend.rb', line 437

def self.display_name(frontend_name)
  definition_for(frontend_name).dig(:metadata, :display_name) || frontend_name.to_s.capitalize
rescue ArgumentError
  frontend_name.to_s.capitalize
end

.ensure_windows_modules ⇒ Object

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.

Ensure Windows modules are loaded (they're defined at top level)



1044
1045
1046
1047
1048
# File 'documented/common/frontend.rb', line 1044

def self.ensure_windows_modules
  return false unless native_windows_runtime?

  defined?(::Win32Enum) && defined?(::WinAPI)
end

.frontends_with_capability(capability) ⇒ Array<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.

Returns all frontends that have a specific capability.

Parameters:

  • capability (Symbol) —

    The capability to filter by

Returns:



453
454
455
456
457
458
459
460
461
# File 'documented/common/frontend.rb', line 453

def self.frontends_with_capability(capability)
  registry_synchronize do
    canonical = @registry.select { |_name, data| data[:capabilities].include?(capability.to_sym) }.keys
    aliases = @aliases.filter_map do |alias_name, name|
      alias_name if canonical.include?(name)
    end
    canonical + aliases
  end
end

.has_capability?(frontend_name, capability) ⇒ 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.

Checks if a frontend has a specific capability.

Parameters:

  • frontend_name (String) —

    The name of the frontend to check

  • capability (Symbol) —

    The capability to check for

Returns:

  • (Boolean)


340
341
342
343
344
345
346
347
# File 'documented/common/frontend.rb', line 340

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

  registry_synchronize do
    entry = @registry[canonical_name(frontend_name)]
    entry ? entry[:capabilities].include?(capability.to_sym) : false
  end
end

.init_from_parent(parent_pid) ⇒ Integer?

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.

Initialize PID from parent process (for Warlock)

Returns:

  • (Integer, nil) —

    The resolved frontend PID



764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
# File 'documented/common/frontend.rb', line 764

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 windows_platform?
    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 StandardError, LoadError => 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) ⇒ Object?

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 a metadata value for a given frontend.

Parameters:

  • frontend_name (String) —

    The name of the frontend

  • key (Symbol) —

    The metadata key to retrieve

Returns:

  • (Object, nil)


353
354
355
356
357
358
359
# File 'documented/common/frontend.rb', line 353

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

  definition_for(frontend_name).dig(:metadata, key)
rescue ArgumentError
  nil
end

.native_windows_runtime? ⇒ Boolean

Native user32 bindings require a Windows MRI ABI, not merely a Windows host.

Returns:

  • (Boolean)


62
63
64
# File 'documented/common/frontend.rb', line 62

def self.native_windows_runtime?
  OS.host_os.to_s.match?(/mingw|mswin/i)
end

.pid ⇒ Integer?

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.

Get the current frontend PID

Returns:

  • (Integer, nil) —

    The PID if set, nil otherwise



750
751
752
# File 'documented/common/frontend.rb', line 750

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

.pid=(value) ⇒ Integer

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.

Set the frontend PID

Parameters:

  • value (Integer) —

    The PID to store

Returns:

  • (Integer) —

    The stored PID



757
758
759
760
# File 'documented/common/frontend.rb', line 757

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

.platform_key ⇒ 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.

Returns the canonical platform key used by frontend discovery and launch-plan metadata.

Returns:

  • (Symbol) —

    :darwin, :windows, :linux, or :unsupported



406
407
408
409
410
411
412
# File 'documented/common/frontend.rb', line 406

def self.platform_key
  return :darwin if OS.mac?
  return :linux if OS.linux?
  return :windows if OS.windows?

  :unsupported
end

.player_id_tag(player_id) ⇒ Object

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.

Build the re-emit tag for a detachable client (e.g. Saga).

Lich consumes the game's one-time during its own login handshake, before a detachable client attaches, so the client never sees it. XMLData.player_id stores the id verbatim, so re-emitting reproduces exactly what a Direct login delivers.

Returns the tag string only when player_id is a bare numeric id (the form the game sends). Returns nil otherwise, so callers skip emitting an empty or malformed tag before login has populated the id.



670
671
672
673
674
675
# File 'documented/common/frontend.rb', line 670

def self.player_id_tag(player_id)
  id = player_id.to_s
  return nil unless id =~ /\A\d+\z/

  "<playerID id='#{id}'/>"
end

.refocus ⇒ 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.

Refocus the frontend window

Returns:

  • (Boolean) —

    true if successful, false otherwise



826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
# File 'documented/common/frontend.rb', line 826

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_callback ⇒ Proc

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.

Create a callback for GTK windows to refocus on click

Returns:

  • (Proc) —

    A proc that can be called to refocus the frontend



844
845
846
847
848
849
850
851
852
# File 'documented/common/frontend.rb', line 844

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

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.

Linux refocus implementation



1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
# File 'documented/common/frontend.rb', line 1027

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

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.

macOS refocus implementation



1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
# File 'documented/common/frontend.rb', line 1009

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

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.

Windows refocus implementation



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
# File 'documented/common/frontend.rb', line 970

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 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.

Registers a frontend with its capabilities and metadata.

Parameters:

  • name (Symbol, String) —

    The name of the frontend (e.g., :wrayth)

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

    A list of capabilities (e.g., [:xml, :streams])

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

    Additional data (e.g., { client_string: "..." })



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'documented/common/frontend.rb', line 162

def self.register(name, capabilities: [], metadata: {})
  registry_synchronize do
    key = name.to_s.downcase
    raise ArgumentError, 'frontend name must not be empty' if key.empty?

    aliases = Array([:aliases]).map { |alias_name| alias_name.to_s.downcase }
    settings_owned = Array(@user_frontend_ids)
    if settings_owned.include?(key) || aliases.any? { |alias_name| settings_owned.include?(alias_name) }
      raise ArgumentError, "frontend identifier is managed by user settings: #{key}"
    end

    entry = (@registry[key] ||= { capabilities: Set.new, metadata: {} })
    entry[:capabilities].merge(capabilities.map(&:to_sym))
    entry[:metadata].merge!(deep_copy())
    aliases.each { |alias_name| @aliases[alias_name] = key }
    @definitions.delete(key)
    nil
  end
end

.registered_frontends ⇒ Array<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.

Returns every recognized frontend name: canonical catalog identifiers followed by their accepted aliases.

Returns:



446
447
448
# File 'documented/common/frontend.rb', line 446

def self.registered_frontends
  registry_synchronize { @registry.keys + @aliases.keys }
end

.replace_user_configuration!(built_in_overrides:, custom_definitions:) ⇒ 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.

Replaces the complete process-local user configuration in one operation. Built-ins are restored from their sealed defaults before launch-only overrides are applied, and custom definitions from an earlier load are removed before the new set is registered.

FrontendSettings owns validation and persistence. This method is the narrow application seam that keeps catalog reloads deterministic.

Parameters:



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'documented/common/frontend.rb', line 225

def self.replace_user_configuration!(built_in_overrides:, custom_definitions:)
  registry_synchronize do
    raise 'built-in frontend catalog has not been sealed' unless @built_in_registry

    snapshot = {
      registry: deep_copy(@registry),
      aliases: deep_copy(@aliases),
      definitions: @definitions.dup,
      user_frontend_ids: Array(@user_frontend_ids).dup
    }

    begin
      replace_user_configuration_unlocked!(
        built_in_overrides: built_in_overrides,
        custom_definitions: custom_definitions
      )
    rescue StandardError
      @registry = snapshot[:registry]
      @aliases = snapshot[:aliases]
      @definitions = snapshot[:definitions]
      @user_frontend_ids = snapshot[:user_frontend_ids]
      raise
    end
  end
end

.resolve_linux_pid(pid) ⇒ Object

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.

Linux-specific PID resolution



944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
# File 'documented/common/frontend.rb', line 944

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

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.

Resolve PID by walking up process tree to find window owner

Parameters:

  • pid (Integer) —

    Starting process ID

Returns:

  • (Integer) —

    The resolved PID



864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
# File 'documented/common/frontend.rb', line 864

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

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.

Windows-specific PID resolution



881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
# File 'documented/common/frontend.rb', line 881

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

.seal_built_in_catalog! ⇒ 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.

Captures immutable built-in defaults after all built-ins are registered.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'documented/common/frontend.rb', line 309

def self.seal_built_in_catalog!
  registry_synchronize do
    @built_in_registry = BUILT_IN_FRONTEND_IDS.each_with_object({}) do |frontend_id, result|
      result[frontend_id] = deep_copy(@registry.fetch(frontend_id))
    end
    @built_in_aliases = @aliases.select do |_alias_name, frontend_id|
      BUILT_IN_FRONTEND_IDS.include?(frontend_id)
    end
    Lich::Util.deep_freeze(@built_in_registry)
    Lich::Util.deep_freeze(@built_in_aliases)
    @user_frontend_ids = []
    nil
  end
end

.send_handshake(version_string) ⇒ Object

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.

Send version string, ready signals, and setup commands to the game server. Used during login handshake for wizard/avalon/frostbite frontends.



689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'documented/common/frontend.rb', line 689

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_location ⇒ 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.

Returns the path to the most recently created session descriptor file.

Returns:

  • (String, nil) —

    the session file path, or nil if no session file has been created



732
733
734
# File 'documented/common/frontend.rb', line 732

def self.session_file_location
  @session_file
end

.set_from_client(pid) ⇒ Integer

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.

Set PID from a detachable frontend such as Profanity or Saga.

Parameters:

  • pid (Integer) —

    The PID sent by the client

Returns:

  • (Integer) —

    The stored PID



795
796
797
798
799
# File 'documented/common/frontend.rb', line 795

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

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.

Checks whether a frontend supports GSL (Gemstone Scripting Language).

Parameters:

  • fe (String, nil) (defaults to: $frontend) —

    the frontend name (defaults to the current $frontend global)

Returns:

  • (Boolean) —

    true if the frontend has the :gsl capability



624
625
626
# File 'documented/common/frontend.rb', line 624

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

.supports_mono?(fe = $frontend) ⇒ 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.

Checks whether a frontend supports monospace text rendering.

Parameters:

  • fe (String, nil) (defaults to: $frontend) —

    the frontend name (defaults to the current $frontend global)

Returns:

  • (Boolean) —

    true if the frontend has the :mono capability



640
641
642
# File 'documented/common/frontend.rb', line 640

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

.supports_room_window?(fe = $frontend) ⇒ 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.

Checks whether a frontend can display the room window independently.

Parameters:

  • fe (String, nil) (defaults to: $frontend) —

    the frontend name (defaults to the current $frontend global)

Returns:

  • (Boolean) —

    true if the frontend has the :room_window capability



648
649
650
# File 'documented/common/frontend.rb', line 648

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

.supports_sentinel?(fe = $frontend) ⇒ 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.

Checks whether a frontend is the Sentinel client.

Parameters:

  • fe (String, nil) (defaults to: $frontend) —

    the frontend name (defaults to the current $frontend global)

Returns:

  • (Boolean) —

    true if the frontend has the :sentinel capability



656
657
658
# File 'documented/common/frontend.rb', line 656

def self.supports_sentinel?(fe = $frontend)
  has_capability?(fe, :sentinel)
end

.supports_streams?(fe = $frontend) ⇒ 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.

Checks whether a frontend supports streaming data.

Parameters:

  • fe (String, nil) (defaults to: $frontend) —

    the frontend name (defaults to the current $frontend global)

Returns:

  • (Boolean) —

    true if the frontend has the :streams capability



632
633
634
# File 'documented/common/frontend.rb', line 632

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

.supports_xml?(fe = $frontend) ⇒ 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.

Checks whether a frontend supports the XML stream protocol.

Examples:

Frontend.supports_xml?("wrayth") #=> true
Frontend.supports_xml? #=> true if current frontend supports XML

Parameters:

  • fe (String, nil) (defaults to: $frontend) —

    the frontend name (defaults to the current $frontend global)

Returns:

  • (Boolean) —

    true if the frontend has the :xml capability



616
617
618
# File 'documented/common/frontend.rb', line 616

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

.user_definition_id_available?(frontend_name) ⇒ 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 an identifier is available for a settings-owned custom frontend. An identifier already owned by the current settings document remains available so reloads can replace it without colliding with themselves. Registry entries and aliases owned elsewhere are reserved.

Parameters:

  • frontend_name (String, Symbol)

Returns:

  • (Boolean)


204
205
206
207
208
209
210
211
212
# File 'documented/common/frontend.rb', line 204

def self.user_definition_id_available?(frontend_name)
  registry_synchronize do
    key = frontend_name.to_s.downcase
    next false if key.empty?
    next true if Array(@user_frontend_ids).include?(key)

    !@registry.key?(key) && !@aliases.key?(key)
  end
end

.validate_platform_key!(key) ⇒ 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.

Validates a canonical platform key used by discovery and launch plans.

Parameters:

  • key (Symbol)

Returns:

  • (Symbol)

Raises:

  • (ArgumentError) —

    when key is not canonical



419
420
421
422
423
# File 'documented/common/frontend.rb', line 419

def self.validate_platform_key!(key)
  return key if PLATFORM_KEYS.include?(key)

  raise ArgumentError, "invalid platform key: #{key.inspect}"
end

.windows_parent_pid(wmi, pid) ⇒ Object

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.

Get parent process ID on Windows



937
938
939
940
941
# File 'documented/common/frontend.rb', line 937

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

.windows_platform? ⇒ 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 current host is classified as Windows.

Returns:

  • (Boolean)


428
429
430
# File 'documented/common/frontend.rb', line 428

def self.windows_platform?
  platform_key == :windows
end