Module: Lich::Common::Settings

Defined in:
documented/common/settings.rb

Overview

Provides configuration settings for the Lich framework.

This module manages settings with support for logging and path navigation.

See Also:

Defined Under Namespace

Classes: CircularReferenceError

Constant Summary collapse

LOG_LEVEL_NONE =

Logging Configuration

0
LOG_LEVEL_ERROR =
1
LOG_LEVEL_INFO =
2
LOG_LEVEL_DEBUG =
3
DEFAULT_SCOPE =
":".freeze
@@log_level =

Default: logging disabled

LOG_LEVEL_NONE
@@log_prefix =
"[SettingsModule]".freeze

Class Method Summary collapse

Class Method Details

.[](name) ⇒ Object



481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
# File 'documented/common/settings.rb', line 481

def self.[](name)
  scope_to_use = DEFAULT_SCOPE
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[]: name: #{name.inspect}, current_path: #{@path_navigator.path.inspect}, safe_nav: #{@safe_navigation_active}" })

  if @path_navigator.path.empty?
    data_for_scope = current_script_settings(scope_to_use)
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (top-level): data_for_scope (DUP) (object_id: #{data_for_scope.object_id}): #{data_for_scope.inspect}" })
    value = get_value_from_container(data_for_scope, name)
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (top-level): value for '#{name}': #{value.inspect}" })
    if value.nil? && !data_for_scope.is_a?(Array) && (!data_for_scope.is_a?(Hash) || !data_for_scope.key?(name))
      _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.[] (top-level): Key '#{name}' not found or value is nil. Activating safe_navigation." })
      @safe_navigation_active = true
    end
    return reset_path_and_return(wrap_value_if_container(value, scope_to_use, [name]))
  else
    current_target, _ = navigate_to_path(false, scope_to_use)
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (path-based): current_target: #{current_target.inspect}" })
    value = get_value_from_container(current_target, name)
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[] (path-based): value for '#{name}': #{value.inspect}" })
    new_path = @path_navigator.path + [name]
    if value.nil? && !current_target.is_a?(Array) && (!current_target.is_a?(Hash) || !current_target.key?(name))
      _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.[] (path-based): Key '#{name}' not found or value is nil in path. Activating safe_navigation." })
      @safe_navigation_active = true
    end
    return reset_path_and_return(wrap_value_if_container(value, scope_to_use, new_path))
  end
end

.[]=(name, value) ⇒ Object



509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# File 'documented/common/settings.rb', line 509

def self.[]=(name, value)
  scope_to_use = DEFAULT_SCOPE
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "Settings.[]=: name: #{name.inspect}, value: #{value.inspect}, current_path: #{@path_navigator.path.inspect}" })
  @safe_navigation_active = false # Reset safe navigation on assignment

  if @path_navigator.path.empty?
    set_script_settings(scope_to_use, name, value)
  else
    target, root_settings = navigate_to_path(true, scope_to_use)
    if target && (target.is_a?(Hash) || target.is_a?(Array))
      actual_value = value.is_a?(SettingsProxy) ? unwrap_proxies(value) : value
      target[name] = actual_value
      save_to_database(root_settings, scope_to_use)
      reset_path_and_return(value)
    else
      _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "Settings.[]=: Cannot assign to non-container or nil target at path #{@path_navigator.path.inspect}" })
      reset_path_and_return(nil)
    end
  end
end

._log(level, prefix, message_proc) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'documented/common/settings.rb', line 58

def self._log(level, prefix, message_proc)
  return unless Lich.respond_to?(:log)
  return unless level <= @@log_level

  level_str = case level
              when LOG_LEVEL_ERROR then "[ERROR]"
              when LOG_LEVEL_INFO  then "[INFO]"
              when LOG_LEVEL_DEBUG then "[DEBUG]"
              else "[UNKNOWN]"
              end

  begin
    message = message_proc.call
    Lich.log("#{prefix} #{level_str} #{message}")
  rescue => e
    Lich.log("#{prefix} [ERROR] Logging failed: #{e.message} - Original message proc: #{message_proc.source_location if message_proc.respond_to?(:source_location)}")
  end
end

._reattach_live!(proxy) ⇒ Boolean

Reattaches the live proxy to the current script context.

Parameters:

Returns:

  • (Boolean)

    true if reattachment was successful, false otherwise



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'documented/common/settings.rb', line 81

def self._reattach_live!(proxy)
  script_name = Script.current.name
  scope       = proxy.scope
  path        = proxy.path

  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "reattach_live!: scope=#{scope.inspect} path=#{path.inspect}" })

  @path_navigator.reset_path
  @path_navigator.set_path(path)
  live, _root = @path_navigator.navigate_to_path(script_name, true, scope)

  if live.nil?
    _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "reattach_live!: failed to resolve live target for path=#{path.inspect} scope=#{scope.inspect}" })
    return false
  end

  # Swap the proxy onto the live object via SettingsProxy API (encapsulated)
  # Centralizes invariants/logging within SettingsProxy itself.
  proxy.rebind_to_live!(live)
  true
end

.autoObject



616
617
618
619
620
# File 'documented/common/settings.rb', line 616

def self.auto
  Lich.deprecated('Settings.auto', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated)
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.auto called (legacy deprecated no-op)." })
  nil
end

.auto=(_val) ⇒ Object



611
612
613
614
# File 'documented/common/settings.rb', line 611

def self.auto=(_val)
  Lich.deprecated('Settings.auto=(val)', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated)
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.auto= called (legacy deprecated no-op)." })
end

.autoloadObject



622
623
624
625
626
# File 'documented/common/settings.rb', line 622

def self.autoload
  Lich.deprecated('Settings.autoload', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated)
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.autoload called (legacy deprecated no-op)." })
  nil
end

.clearObject



605
606
607
608
609
# File 'documented/common/settings.rb', line 605

def self.clear
  Lich.deprecated('Settings.clear', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated)
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.clear called (legacy deprecated no-op)." })
  nil
end

.container?(value) ⇒ Boolean

Checks if the given value is a container (Hash or Array).

Parameters:

  • value (Object)

    the value to check

Returns:

  • (Boolean)

    true if the value is a container, false otherwise



113
114
115
# File 'documented/common/settings.rb', line 113

def self.container?(value)
  value.is_a?(Hash) || value.is_a?(Array)
end

.current_script_settings(scope = DEFAULT_SCOPE, script_name: nil) ⇒ OpenStruct

Retrieves the current settings for the specified script and scope.

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    the scope for which to retrieve settings (default is DEFAULT_SCOPE)

  • script_name (String) (defaults to: nil)

    the name of the script (optional)

Returns:

  • (OpenStruct)

    the current settings for the specified script



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'documented/common/settings.rb', line 352

def self.current_script_settings(scope = DEFAULT_SCOPE, script_name: nil)
  # Use provided script_name or fall back to Script.current.name
  script_name = script_name || Script.current.name
  cache_key = "#{script_name || ""}::#{scope}" # Use an empty string if script_name is nil
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "current_script_settings: Request for scope: #{scope.inspect}, cache_key: #{cache_key}" })

  cached_data = @settings_cache[cache_key]
  if cached_data
    _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Cache hit for #{cache_key} (object_id: #{cached_data.object_id}). Returning DUP: #{cached_data.inspect}" })
    return cached_data.dup
  else
    _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Cache miss for #{cache_key}. Loading from DB." })
    settings = @db_adapter.get_settings(script_name, scope)
    _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Loaded from DB (object_id: #{settings.object_id}): #{settings.inspect}" })
    @settings_cache[cache_key] = settings
    _log(LOG_LEVEL_INFO, @@log_prefix, -> { "current_script_settings: Stored in cache (object_id: #{@settings_cache[cache_key].object_id}). Returning DUP." })
    return settings.dup
  end
end

.get_log_levelInteger

Retrieves the current logging level for the Settings module.

Returns:

  • (Integer)

    the current log level



54
55
56
# File 'documented/common/settings.rb', line 54

def self.get_log_level
  @@log_level
end

.get_scoped_setting(scope_string, key_name, script_name: nil) ⇒ Object

Retrieves a setting value for a specific scope and key.

Parameters:

  • scope_string (String)

    the scope to retrieve the setting from

  • key_name (String)

    the name of the setting to retrieve

  • script_name (String) (defaults to: nil)

    the name of the script (optional)

Returns:

  • (Object)

    the value of the setting, or nil if not found



536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# File 'documented/common/settings.rb', line 536

def self.get_scoped_setting(scope_string, key_name, script_name: nil)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "get_scoped_setting: scope: #{scope_string.inspect}, key: #{key_name.inspect}, script_name: #{script_name.inspect}" })
  data_for_scope = current_script_settings(scope_string, script_name: script_name)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "get_scoped_setting: data_for_scope (DUP) (object_id: #{data_for_scope.object_id}): #{data_for_scope.inspect}" })
  value = get_value_from_container(data_for_scope, key_name)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "get_scoped_setting: value for '#{key_name}': #{value.inspect}" })

  if value.nil? && key_name
    key_absent_in_hash = data_for_scope.is_a?(Hash) && !data_for_scope.key?(key_name)
    key_invalid_for_array = data_for_scope.is_a?(Array) && (!key_name.is_a?(Integer) || key_name < 0 || key_name >= data_for_scope.length)

    if key_absent_in_hash || key_invalid_for_array || (data_for_scope.nil? || (data_for_scope.is_a?(Hash) && data_for_scope.empty?))
      _log(Settings::LOG_LEVEL_INFO, @@log_prefix, -> { "get_scoped_setting: Key '#{key_name}' not found in scope '#{scope_string}'. Value will be nil, supporting '|| default' idiom." })
    end
  end
  wrap_value_if_container(value, scope_string, key_name ? [key_name] : [], script_name: script_name)
end

.get_value_from_container(container, key) ⇒ Object



646
647
648
649
650
651
652
653
654
655
656
657
# File 'documented/common/settings.rb', line 646

def self.get_value_from_container(container, key)
  if container.is_a?(Hash)
    container[key]
  elsif container.is_a?(Array) && key.is_a?(Integer)
    container[key]
  elsif container.is_a?(Array) && !key.is_a?(Integer)
    _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "get_value_from_container: Attempted to access Array with non-Integer key: #{key.inspect}" })
    nil
  else
    nil
  end
end

.loadObject



589
590
591
592
# File 'documented/common/settings.rb', line 589

def self.load
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.load called (legacy, aliasing to refresh_data)." })
  refresh_data
end

.method_missing(method, *args, &block) ⇒ Object



628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'documented/common/settings.rb', line 628

def self.method_missing(method, *args, &block)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "method_missing: method: #{method}, args: #{args.inspect}, path: #{@path_navigator.path.inspect}" })
  if @safe_navigation_active && !@path_navigator.path.empty?
    if method.to_s.end_with?("=")
      _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "method_missing: Attempted assignment (#{method}) on a nil path due to safe navigation." })
      return reset_path_and_return(nil)
    end
    return reset_path_and_return(nil)
  end

  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "method_missing: Delegating to path_navigator: #{method}" })
  @path_navigator.send(method, *args, &block)
end

Navigates to the specified path within the current settings structure.

Parameters:

  • create_missing (Boolean) (defaults to: true)

    whether to create missing segments (default is true)

  • scope (String) (defaults to: DEFAULT_SCOPE)

    the scope for which to navigate (default is DEFAULT_SCOPE)

Returns:

  • (Array<Object>)

    an array containing the target object and the root for the scope



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'documented/common/settings.rb', line 422

def self.navigate_to_path(create_missing = true, scope = DEFAULT_SCOPE)
  root_for_scope = current_script_settings(scope)
  return [root_for_scope, root_for_scope] if @path_navigator.path.empty?

  target = root_for_scope
  @path_navigator.path.each do |key|
    if target.is_a?(Hash) && target.key?(key)
      target = target[key]
    elsif target.is_a?(Array) && key.is_a?(Integer) && key >= 0 && key < target.length
      target = target[key]
    elsif create_missing && (target.is_a?(Hash) || target.is_a?(Array))
      _log(LOG_LEVEL_INFO, @@log_prefix, -> { "navigate_to_path: Creating missing segment '#{key}' in DUPPED structure for scope #{scope.inspect}" })
      new_node = key.is_a?(Integer) ? [] : {}
      if target.is_a?(Hash)
        target[key] = new_node
      elsif target.is_a?(Array) && key.is_a?(Integer)
        target[key] = new_node
      end
      target = new_node
    else
      return [nil, root_for_scope]
    end
  end
  [target, root_for_scope]
end

.refresh_data(scope = DEFAULT_SCOPE) ⇒ OpenStruct

Refreshes the settings data for the specified scope by clearing the cache.

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    the scope to refresh (default is DEFAULT_SCOPE)

Returns:

  • (OpenStruct)

    the refreshed settings



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

def self.refresh_data(scope = DEFAULT_SCOPE)
  script_name = Script.current.name
  cache_key = "#{script_name || ""}::#{scope}" # Use an empty string if script_name is nil
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "refresh_data: Deleting cache for scope: #{scope.inspect}, cache_key: #{cache_key}" })
  @settings_cache.delete(cache_key)
  current_script_settings(scope)
end

.reset_path_and_return(value) ⇒ Object



413
414
415
# File 'documented/common/settings.rb', line 413

def self.reset_path_and_return(value)
  @path_navigator.reset_path_and_return(value)
end

.respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


642
643
644
# File 'documented/common/settings.rb', line 642

def self.respond_to_missing?(method_name, include_private = false)
  @path_navigator.respond_to?(method_name, include_private) || super
end

.root_proxy_for(scope, script_name: Script.current.name) ⇒ SettingsProxy

Retrieves the root proxy for the specified scope and script name.

Parameters:

  • scope (String)

    the scope for which to retrieve the root proxy

  • script_name (String) (defaults to: Script.current.name)

    the name of the script (optional)

Returns:

Raises:

  • (ArgumentError)


153
154
155
156
157
158
159
160
161
# File 'documented/common/settings.rb', line 153

def self.root_proxy_for(scope, script_name: Script.current.name)
  raise ArgumentError, "scope must be a non-empty String" if scope.nil? || scope.to_s.strip.empty?

  script_name ||= ""
  cache_key = "#{script_name}::#{scope}"
  root = @settings_cache[cache_key] ||= @db_adapter.get_settings(script_name, scope)

  SettingsProxy.new(self, scope, [], root, script_name: script_name)
end

.saveObject



584
585
586
587
# File 'documented/common/settings.rb', line 584

def self.save
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.save called (legacy no-op)." })
  :noop
end

.save_allObject



599
600
601
602
603
# File 'documented/common/settings.rb', line 599

def self.save_all
  Lich.deprecated('Settings.save_all', 'not using, not applicable,', caller[0], fe_log: true) if Lich.respond_to?(:deprecated)
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.save_all called (legacy deprecated no-op)." })
  nil
end

.save_proxy_changes(proxy) ⇒ void

This method returns an undefined value.

Saves changes made to the specified proxy back to the database.

Parameters:



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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
224
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'documented/common/settings.rb', line 167

def self.save_proxy_changes(proxy)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Initiated for proxy.scope: #{proxy.scope.inspect}, proxy.path: #{proxy.path.inspect}, proxy.target_object_id: #{proxy.target.object_id}" })
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: proxy.target data: #{proxy.target.inspect}" })

  path        = proxy.path
  scope       = proxy.scope
  # Use proxy.script_name if available (for InstanceSettings), fall back to Script.current.name
  script_name = proxy.respond_to?(:script_name) && proxy.script_name ? proxy.script_name : Script.current.name
  cache_key   = "#{script_name || ""}::#{scope}"

  # Local helper to keep cache in sync with the just-persisted root
  sync_cache = lambda do |root_obj|
    cached = @settings_cache[cache_key]
    if cached && !cached.equal?(root_obj)
      if cached.is_a?(Hash) && root_obj.is_a?(Hash)
        cached.replace(root_obj)
      elsif cached.is_a?(Array) && root_obj.is_a?(Array)
        cached.clear
        cached.concat(root_obj)
      else
        @settings_cache[cache_key] = root_obj
      end
    elsif cached.nil?
      @settings_cache[cache_key] = root_obj
    end
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Cache synchronized post-save for #{cache_key}" })
  end

  # --- Refresh-before-save to prevent stale-cache overwrites ---
  # IMPORTANT: If proxy.target IS the cached object, we must NOT replace it
  # from the database, as that would wipe out the changes we're trying to save.
  # Only refresh when the proxy is detached or working with a different object.
  fresh_root = @db_adapter.get_settings(script_name, scope)

  cached = @settings_cache[cache_key]
  if cached
    # Skip refresh if proxy.target is the same object as cached - we'd wipe out our changes!
    if cached.equal?(proxy.target)
      _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Skipping refresh - proxy.target IS the cached object (object_id: #{cached.object_id})" })
      current_root_for_scope = cached
    elsif cached.is_a?(Hash) && fresh_root.is_a?(Hash)
      cached.replace(fresh_root)
      current_root_for_scope = cached
    elsif cached.is_a?(Array) && fresh_root.is_a?(Array)
      cached.clear
      cached.concat(fresh_root)
      current_root_for_scope = cached
    else
      @settings_cache[cache_key] = fresh_root
      current_root_for_scope = fresh_root
    end
    _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Cache state for #{cache_key} (object_id: #{current_root_for_scope.object_id}): #{current_root_for_scope.inspect}" })
  else
    @settings_cache[cache_key] = fresh_root
    current_root_for_scope = fresh_root
    _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Cache populated from DB for #{cache_key} (object_id: #{current_root_for_scope.object_id}): #{current_root_for_scope.inspect}" })
  end
  # -------------------------------------------------------------

  # EMPTY PATH → Save *current root* (not proxy.target). Also covers detached "view" proxies.
  if path.empty?
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Empty path; saving CURRENT ROOT for scope #{scope.inspect}" })

    unless current_root_for_scope.is_a?(Hash) || current_root_for_scope.is_a?(Array)
      _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Root not a container; initializing {} for scope #{scope.inspect}" })
      current_root_for_scope = {}
      @settings_cache[cache_key] = current_root_for_scope
    end

    if proxy.respond_to?(:detached?) && proxy.detached?
      _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Proxy is detached (view); persisting current root without copying view target." })
      save_to_database(current_root_for_scope, scope, script_name: script_name)
      sync_cache.call(current_root_for_scope)
      return nil
    end

    # Root identity drift: sync proxy.target into cached root if different objects (same-type containers).
    if !current_root_for_scope.equal?(proxy.target)
      if proxy.target.is_a?(Hash) && current_root_for_scope.is_a?(Hash)
        _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Root identity mismatch (cache #{current_root_for_scope.object_id} vs proxy #{proxy.target.object_id}); copying via Hash#replace" })
        current_root_for_scope.replace(proxy.target)
      elsif proxy.target.is_a?(Array) && current_root_for_scope.is_a?(Array)
        _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Root identity mismatch (Array); copying elements" })
        current_root_for_scope.clear
        current_root_for_scope.concat(proxy.target)
      else
        _log(LOG_LEVEL_WARN, @@log_prefix, -> { "save_proxy_changes: Root/target type mismatch; persisting current root only (root=#{current_root_for_scope.class}, target=#{proxy.target.class})." })
      end
    end

    save_to_database(current_root_for_scope, scope, script_name: script_name)
    sync_cache.call(current_root_for_scope)
    return nil
  end

  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: script_name: #{script_name.inspect}, cache_key: #{cache_key}" })

  # From here on, we’re saving into a nested path. Ensure root is a container.
  unless current_root_for_scope.is_a?(Hash) || current_root_for_scope.is_a?(Array)
    _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_proxy_changes: Root not a container; initializing {} for scope #{scope.inspect}" })
    current_root_for_scope = {}
    @settings_cache[cache_key] = current_root_for_scope
  end

  parent_path = path[0...-1]
  leaf_key    = path.last

  # Pre-navigation diagnostics
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> {
    "save_proxy_changes: Navigation preflight — parent_path=#{parent_path.inspect} (#{parent_path.map { |s| s.class }.inspect}), leaf_key=#{leaf_key.inspect} (#{leaf_key.class}), root_class=#{current_root_for_scope.class}"
  })

  # Navigate **within the freshly-loaded root** (do NOT re-fetch via PathNavigator here).
  begin
    parent = current_root_for_scope
    parent_path.each_with_index do |seg, idx|
      next_seg = parent_path[idx + 1]

      if parent.is_a?(Hash)
        unless parent.key?(seg)
          parent[seg] = next_seg.is_a?(Integer) ? [] : {}
          _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Created #{parent[seg].class} at #{parent_path[0..idx].inspect} for scope #{scope.inspect}" })
        end
        parent = parent[seg]
      elsif parent.is_a?(Array)
        unless seg.is_a?(Integer) && seg >= 0
          _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Non-integer or negative index #{seg.inspect} for Array at #{parent_path[0..idx].inspect} in scope #{scope.inspect}" })
          return nil
        end
        if seg >= parent.length
          (parent.length..seg).each { parent << nil }
          _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Extended Array to index #{seg} at #{parent_path[0..idx].inspect} for scope #{scope.inspect}" })
        end
        parent[seg] ||= (next_seg.is_a?(Integer) ? [] : {})
        parent = parent[seg]
      else
        _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Parent is not a container at #{parent_path[0..idx].inspect} (#{parent.class}) for scope #{scope.inspect}" })
        return nil
      end
    end
  rescue => e
    _log(LOG_LEVEL_ERROR, @@log_prefix, -> {
      "save_proxy_changes: Local navigation raised #{e.class}: #{e.message}. "\
      "scope=#{scope.inspect}, script_name=#{script_name.inspect}, cache_key=#{cache_key}, "\
      "parent_path=#{parent_path.inspect}, leaf_key=#{leaf_key.inspect}"
    })
    bt = (e.backtrace || [])[0, 5]
    _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Backtrace (top 5): #{bt.join(' | ')}" })
    return nil
  end

  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Navigated/created parent (object_id: #{parent.object_id}, class=#{parent.class}): #{parent.inspect}" })

  if parent.is_a?(Hash)
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Setting Hash key #{leaf_key.inspect} with proxy.target (object_id: #{proxy.target.object_id})" })
    parent[leaf_key] = proxy.target
  elsif parent.is_a?(Array) && leaf_key.is_a?(Integer)
    if leaf_key < 0
      _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_proxy_changes: Negative array index #{leaf_key} not supported at path #{path.inspect} in scope #{scope.inspect}" })
      return nil
    end
    if leaf_key >= parent.length
      (parent.length..leaf_key).each { parent << nil }
      _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Extended Array to index #{leaf_key} for parent at path #{parent_path.inspect}" })
    end
    parent[leaf_key] = proxy.target
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: Set Array index #{leaf_key} with proxy.target (object_id: #{proxy.target.object_id})" })
  else
    _log(LOG_LEVEL_ERROR, @@log_prefix, -> {
      "save_proxy_changes: Cannot set value at path #{path.inspect} in scope #{scope.inspect}; "\
      "parent_class=#{parent.class}, leaf_key_class=#{leaf_key.class}"
    })
    return nil
  end

  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_proxy_changes: root after update (object_id: #{current_root_for_scope.object_id}): #{current_root_for_scope.inspect}" })
  save_to_database(current_root_for_scope, scope, script_name: script_name)
  sync_cache.call(current_root_for_scope)
end

.save_to_database(data_to_save, scope = DEFAULT_SCOPE, script_name: nil) ⇒ void

This method returns an undefined value.

Saves the specified data to the database for the given script and scope.

Parameters:

  • data_to_save (Object)

    the data to save

  • scope (String) (defaults to: DEFAULT_SCOPE)

    the scope for which to save the data (default is DEFAULT_SCOPE)

  • script_name (String) (defaults to: nil)

    the name of the script (optional)



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'documented/common/settings.rb', line 378

def self.save_to_database(data_to_save, scope = DEFAULT_SCOPE, script_name: nil)
  # Use provided script_name or fall back to Script.current.name
  script_name = script_name || Script.current.name

  if script_name.nil? || script_name.empty?
    _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_to_database: Aborting save. script_name is nil or empty. Scope: #{scope.inspect}. Data will NOT be persisted." })
    return nil # Explicitly return nil
  end

  cache_key = "#{script_name}::#{scope}" # script_name is guaranteed to be non-nil/non-empty here
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_to_database: Saving for script: '#{script_name}', scope: #{scope.inspect}, cache_key: #{cache_key}" })
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_to_database: Data BEFORE unwrap_proxies (object_id: #{data_to_save.object_id}): #{data_to_save.inspect}" })

  unwrapped_settings = unwrap_proxies(data_to_save)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "save_to_database: Data AFTER unwrap_proxies (object_id: #{unwrapped_settings.object_id}): #{unwrapped_settings.inspect}" })

  @db_adapter.save_settings(script_name, unwrapped_settings, scope)
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_to_database: Data saved to DB for script '#{script_name}'." })

  @settings_cache[cache_key] = unwrapped_settings
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "save_to_database: Cache updated for #{cache_key} with saved data (object_id: #{@settings_cache[cache_key].object_id})." })
end

.set_log_level(level) ⇒ void

This method returns an undefined value.

Sets the logging level for the Settings module.

Parameters:

  • level (Symbol)

    the desired log level, can be :none, :error, :info, or :debug



39
40
41
42
43
44
45
46
47
48
49
50
# File 'documented/common/settings.rb', line 39

def self.set_log_level(level)
  numeric_level = case level
                  when :none, LOG_LEVEL_NONE then LOG_LEVEL_NONE
                  when :error, LOG_LEVEL_ERROR then LOG_LEVEL_ERROR
                  when :info, LOG_LEVEL_INFO then LOG_LEVEL_INFO
                  when :debug, LOG_LEVEL_DEBUG then LOG_LEVEL_DEBUG
                  else
                    _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "Invalid log level specified: #{level.inspect}. Defaulting to NONE." })
                    LOG_LEVEL_NONE
                  end
  @@log_level = numeric_level
end

.set_script_settings(scope = DEFAULT_SCOPE, name, value, script_name: nil) ⇒ void

This method returns an undefined value.

Sets a specific setting for the current script and scope.

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    the scope in which to set the value (default is DEFAULT_SCOPE)

  • name (String)

    the name of the setting to set

  • value (Object)

    the value to assign to the setting

  • script_name (String) (defaults to: nil)

    the name of the script (optional)



455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'documented/common/settings.rb', line 455

def self.set_script_settings(scope = DEFAULT_SCOPE, name, value, script_name: nil)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: scope: #{scope.inspect}, name: #{name.inspect}, value: #{value.inspect}, script_name: #{script_name.inspect}, current_path: #{@path_navigator.path.inspect}" })
  unwrapped_value = unwrap_proxies(value)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: unwrapped_value: #{unwrapped_value.inspect}" })

  current_root = current_script_settings(scope, script_name: script_name)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: current_root (DUP) for scope #{scope.inspect} (object_id: #{current_root.object_id}): #{current_root.inspect}" })

  if @path_navigator.path.empty?
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: Path is empty. Setting '#{name}' on current_root." })
    current_root[name] = unwrapped_value
    save_to_database(current_root, scope, script_name: script_name)
  else
    if !@path_navigator.path.empty?
      _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "set_script_settings: WARNING: Called with non-empty path_navigator path: #{@path_navigator.path.inspect}. This is unusual for Char/GameSettings direct assignment." })
    end
    if current_root.is_a?(Hash)
      current_root[name] = unwrapped_value
      save_to_database(current_root, scope, script_name: script_name)
    else
      _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "set_script_settings: current_root for scope #{scope.inspect} is not a Hash. Cannot set key '#{name}'. Root class: #{current_root.class}" })
    end
  end
  reset_path_and_return(value)
end

.to_h(scope = DEFAULT_SCOPE) ⇒ Object

Added scope to match to_hash for consistency if used directly



594
595
596
597
# File 'documented/common/settings.rb', line 594

def self.to_h(scope = DEFAULT_SCOPE) # Added scope to match to_hash for consistency if used directly
  _log(LOG_LEVEL_INFO, @@log_prefix, -> { "Settings.to_h called (legacy, aliasing to to_hash)." })
  self.to_hash(scope)
end

.to_hash(scope = DEFAULT_SCOPE) ⇒ Hash

Converts the current settings for the specified scope to a Hash.

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    the scope to convert (default is DEFAULT_SCOPE)

Returns:

  • (Hash)

    the settings as a Hash



576
577
578
579
580
581
582
# File 'documented/common/settings.rb', line 576

def self.to_hash(scope = DEFAULT_SCOPE)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "to_hash: scope: #{scope.inspect}" })
  data = current_script_settings(scope)
  unwrapped_data = unwrap_proxies(data)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "to_hash: Returning unwrapped data (snapshot): #{unwrapped_data.inspect}" })
  return unwrapped_data
end

.wrap_value_if_container(value, scope, path_array, script_name: nil) ⇒ Object

Wraps a value in a proxy if it is a container (Hash or Array).

Parameters:

  • value (Object)

    the value to wrap

  • scope (String)

    the scope for the value

  • path_array (Array<String>)

    the path to the value

  • script_name (String) (defaults to: nil)

    the name of the script (optional)

Returns:

  • (Object)

    the wrapped value or the original value if not a container



561
562
563
564
565
566
567
568
569
570
# File 'documented/common/settings.rb', line 561

def self.wrap_value_if_container(value, scope, path_array, script_name: nil)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "wrap_value_if_container: value_class: #{value.class}, scope: #{scope.inspect}, path: #{path_array.inspect}, script_name: #{script_name.inspect}" })
  if container?(value)
    proxy = SettingsProxy.new(self, scope, path_array, value, script_name: script_name)
    _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "wrap_value_if_container: Wrapped in proxy: #{proxy.inspect}" })
    return proxy
  else
    return value
  end
end