Module: Lich::Common::FrontendLauncher

Defined in:
documented/common/frontend_launcher.rb

Overview

Converts catalog launcher metadata into either a launch-file command template or a shell-free environment/argv process plan.

Defined Under Namespace

Classes: Error, SpawnPlan, UnavailableError, UnsupportedError

Constant Summary collapse

PROCESS_LOCAL_LAUNCH_FIELDS =

Launch data field names that are local to the Lich process and must be removed before the data is serialized for a native frontend via Simutronics' .sal contract.

See Also:

  • #native_session_data
%w[FRONTEND CUSTOMLAUNCHARGV].freeze
CONNECTION_PLACEHOLDER_PATTERN =

Regular expression matching Lich's connection-configuration placeholder tokens. These tokens are replaced with the actual connection values after the local proxy listener is opened.

Examples:

"%host%" matches when rendering a connection
"%port%" matches when rendering a connection
"%key%" matches when rendering a connection

See Also:

/(%host%|%port%|%key%)/.freeze

Class Method Summary collapse

Class Method Details

.command(frontend_id, platform_key: Frontend.platform_key, locator: FrontendLocator, simu_launcher: -> { Lich.get_simu_launcher }) ⇒ String+

Builds the platform command template for a registered frontend.

Parameters:

  • frontend_id (String, Symbol) —

    registered frontend identifier

  • platform_key (Symbol) (defaults to: Frontend.platform_key) —

    canonical host classification

  • locator (FrontendLocator) (defaults to: FrontendLocator) —

    injectable discovery API

  • simu_launcher (#call) (defaults to: -> { Lich.get_simu_launcher }) —

    injectable legacy launcher lookup

Returns:

  • (String, Array<String>) —

    legacy template or shell-free Windows argv

Raises:

  • (ArgumentError) —

    for an unknown frontend identifier

  • (UnsupportedError) —

    when the adapter has no platform command

  • (UnavailableError) —

    when a required executable/launcher is absent



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'documented/common/frontend_launcher.rb', line 83

def command(
  frontend_id,
  platform_key: Frontend.platform_key,
  locator: FrontendLocator,
  simu_launcher: -> { Lich.get_simu_launcher }
)
  definition = Frontend.definition_for(frontend_id)

  base_command = case definition.dig(:metadata, :launcher_adapter)
                 when :environment
                   raise UnsupportedError, "#{definition[:id]} requires a structured spawn plan"
                 when :avalon
                   avalon_command(definition, platform_key, locator)
                 when :simutronics
                   simu_launcher.call || raise(UnavailableError, 'Simutronics launcher was not found')
                 when :custom
                   definition.dig(:metadata, :launch_command)
                 else
                   raise UnsupportedError, "no launcher adapter for #{definition[:id]}"
                 end

  if base_command.to_s.strip.empty?
    raise UnavailableError, "#{Frontend.display_name(definition[:id])} has no launch command"
  end

  with_additional_arguments(base_command, definition, platform_key: platform_key)
end

.launchable?(frontend_id, locator: FrontendLocator, frontend: Frontend, refresh: false) ⇒ Boolean

Returns whether a frontend has enough machine-local configuration to launch. Custom adapters are backed by their persisted command; native adapters continue to use executable discovery.

Parameters:

  • frontend_id (String, Symbol) —

    registered frontend identifier

  • locator (FrontendLocator) (defaults to: FrontendLocator) —

    injectable discovery API

  • frontend (Frontend) (defaults to: Frontend) —

    injectable frontend catalog API

  • refresh (Boolean) (defaults to: false) —

    bypass cached native discovery when true

Returns:

  • (Boolean)


62
63
64
65
66
67
68
69
70
71
# File 'documented/common/frontend_launcher.rb', line 62

def launchable?(frontend_id, locator: FrontendLocator, frontend: Frontend, refresh: false)
  definition = frontend.definition_for(frontend_id)
  if definition.dig(:metadata, :launcher_adapter) == :custom
    return !definition.dig(:metadata, :launch_command).to_s.strip.empty?
  end

  locator.launchable?(definition[:id], refresh: refresh)
rescue ArgumentError
  false
end

.native_session_data(launch_data) ⇒ Array<String>

Removes Lich-only control fields before launch data is serialized for a native frontend. The selected frontend identity remains available to the current Lich process but is not part of Simutronics' .sal contract.

Parameters:

Returns:

  • (Array<String>) —

    detached native launch-data copy



138
139
140
141
142
143
# File 'documented/common/frontend_launcher.rb', line 138

def native_session_data(launch_data)
  Array(launch_data).reject do |line|
    key = line.to_s.split('=', 2).first.to_s.upcase
    PROCESS_LOCAL_LAUNCH_FIELDS.include?(key)
  end
end

.render_connection(command, host:, port:, key:) ⇒ String+

Resolves every connection placeholder after Lich opens the local listener used by a configured frontend.

Parameters:

Returns:



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'documented/common/frontend_launcher.rb', line 153

def render_connection(command, host:, port:, key:)
  if command.is_a?(Array)
    return command.map { |argument| render_connection(argument, host: host, port: port, key: key) }
  end

  {
    '%host%' => host,
    '%port%' => port,
    '%key%'  => key
  }.reduce(command.to_s) do |rendered, (placeholder, value)|
    rendered.gsub(placeholder, value.to_s)
  end
end

.saga_managed_login_plan(account:, character:, game_code:, platform_key: Frontend.platform_key, locator: FrontendLocator, refresh: true) ⇒ SpawnPlan

Builds a shell-free Saga launch plan that asks Saga to authenticate the named character and start its own Via-Lich session. Unlike spawn_plan, this contract does not create a Lich proxy listener or pass a game key; Saga owns authentication and Lich process startup.

Parameters:

  • account (String) —

    Saga account with a saved password

  • character (String) —

    character to launch

  • game_code (String) —

    Saga instance code (for example, GS3 or DR)

  • platform_key (Symbol) (defaults to: Frontend.platform_key) —

    canonical host classification

  • locator (FrontendLocator) (defaults to: FrontendLocator) —

    injectable discovery API

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

    refresh executable discovery before resolving

Returns:

Raises:

  • (ArgumentError) —

    for blank launch identifiers

  • (UnsupportedError) —

    when no Saga plan exists on the platform

  • (UnavailableError) —

    when Saga is not installed



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'documented/common/frontend_launcher.rb', line 240

def (
  account:,
  character:,
  game_code:,
  platform_key: Frontend.platform_key,
  locator: FrontendLocator,
  refresh: true
)
  identifiers = {
    account: ,
    character: character,
    game_code: game_code
  }
  identifiers.each do |name, value|
    raise ArgumentError, "#{name} must not be empty" if value.to_s.strip.empty?
  end

  definition = Frontend.definition_for('saga')
  unless definition.dig(:metadata, :launcher_adapter) == :environment
    raise UnsupportedError, 'no environment launcher for saga'
  end

  platform_key = Frontend.validate_platform_key!(platform_key)
  plan = definition.dig(:metadata, :launch_plans, platform_key)
  raise UnsupportedError, "no #{platform_key} launcher for saga" unless plan

  command, arguments = resolve_plan_launch(
    plan,
    definition,
    platform_key,
    locator,
    refresh: refresh
  )
  SpawnPlan.new(
    environment: {
      # Saga 0.8.5 implements this currently undocumented startup contract.
      'SAGA_AUTO_LOGIN'         => "#{character.to_s.strip}@#{game_code.to_s.strip.upcase}",
      'SAGA_AUTO_LOGIN_ACCOUNT' => .to_s.strip,
      'SAGA_AUTO_LOGIN_MODE'    => 'lich'
    },
    argv: [command, *arguments]
  )
end

.spawn_plan(frontend_id, host:, port:, key:, platform_key: Frontend.platform_key, locator: FrontendLocator, refresh: true) ⇒ SpawnPlan

Builds a shell-free process environment and argv for an environment launcher adapter. Catalog launch_plans are keyed by platform and contain :command, :arguments, and :environment. A command may be a literal executable or :resolved_executable for locator discovery; %host%, %port%, and %key% tokens are replaced in environment values. Connection secrets remain out of process arguments.

Parameters:

  • frontend_id (String, Symbol) —

    registered frontend identifier

  • host (String) —

    local proxy host

  • port (Integer, String) —

    local proxy port

  • key (String) —

    authenticated game connection key

  • platform_key (Symbol) (defaults to: Frontend.platform_key) —

    canonical host classification

  • locator (FrontendLocator) (defaults to: FrontendLocator) —

    injectable discovery API

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

    refresh executable discovery before resolving

Returns:

Raises:

  • (ArgumentError) —

    for blank connection values

  • (UnsupportedError) —

    when no plan exists on the platform



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'documented/common/frontend_launcher.rb', line 184

def spawn_plan(
  frontend_id,
  host:,
  port:,
  key:,
  platform_key: Frontend.platform_key,
  locator: FrontendLocator,
  refresh: true
)
  definition = Frontend.definition_for(frontend_id)
  unless definition.dig(:metadata, :launcher_adapter) == :environment
    raise UnsupportedError, "no environment launcher for #{definition[:id]}"
  end

  replacements = { '%host%' => host, '%port%' => port, '%key%' => key }
  replacements.each do |token, value|
    raise ArgumentError, "#{token.delete('%')} must not be empty" if value.to_s.empty?
  end

  platform_key = Frontend.validate_platform_key!(platform_key)
  plan = definition.dig(:metadata, :launch_plans, platform_key)
  unless plan
    raise UnsupportedError, "no #{platform_key} launcher for #{definition[:id]}"
  end

  environment = plan.fetch(:environment).transform_values do |value|
    replacements.reduce(value.to_s) { |resolved, (token, replacement)| resolved.gsub(token, replacement.to_s) }
  end
  command, arguments = resolve_plan_launch(
    plan,
    definition,
    platform_key,
    locator,
    refresh: refresh
  )
  SpawnPlan.new(
    environment: environment,
    argv: [command, *arguments]
  )
end

.with_additional_arguments(base_command, frontend, platform_key: Frontend.platform_key) ⇒ String+

Appends user-configured arguments to a command-template launcher. Arguments are escaped independently while Lich's connection placeholders remain available for the launch handoff to replace.

Parameters:

  • base_command (String, Array<String>) —

    command template or literal argv

  • frontend (String, Symbol, Hash) —

    frontend id or definition

  • platform_key (Symbol) (defaults to: Frontend.platform_key) —

    host command-line convention

Returns:

  • (String, Array<String>) —

    command template or shell-free argv



119
120
121
122
123
124
125
126
127
128
129
# File 'documented/common/frontend_launcher.rb', line 119

def with_additional_arguments(base_command, frontend, platform_key: Frontend.platform_key)
  definition = frontend.is_a?(Hash) ? frontend : Frontend.definition_for(frontend)
  arguments = Array(definition.dig(:metadata, :additional_arguments))
  return [*base_command, *arguments] if base_command.is_a?(Array)
  return base_command if arguments.empty?
  return [*WindowsCommandLine.split(base_command), *arguments] if platform_key == :windows

  escaped_arguments = arguments.map { |argument| escape_argument(argument) }
  argument_separator = definition.dig(:metadata, :launcher_adapter) == :avalon ? ' --args ' : ' '
  "#{base_command}#{argument_separator}#{escaped_arguments.join(' ')}"
end