Module: Lich::Common::Settings

Defined in:
documented/common/settings.rb

Overview

Provides configuration settings for the Lich application. This module handles logging, settings management, and data persistence.

Examples:

Setting a log level

Settings.set_log_level(:info)

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

Retrieves a setting by name for the current script and scope.

Examples:

Getting a setting by name

value = Settings["my_setting"]

Parameters:

  • name (String)

    The name of the setting to retrieve

Returns:

  • (Object)

    The value of the setting, or nil if not found



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

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

Sets a value for a setting by name for the current script and scope.

Examples:

Setting a value by name

Settings["my_setting"] = "new_value"

Parameters:

  • name (String)

    The name of the setting to set

  • value (Object)

    The value to assign to the setting

Returns:

  • (nil)

    Always returns nil



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
# File 'documented/common/settings.rb', line 526

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



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'documented/common/settings.rb', line 62

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

Examples:

Reattaching a live proxy

Settings._reattach_live!(my_proxy)

Parameters:

Returns:

  • (Boolean)

    True if reattachment was successful, false otherwise



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'documented/common/settings.rb', line 86

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

.autonil

Retrieves the auto configuration (legacy deprecated no-op).

Examples:

Getting auto configuration

auto_value = Settings.auto

Returns:

  • (nil)

    Always returns nil



664
665
666
667
668
# File 'documented/common/settings.rb', line 664

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

Sets the auto configuration (legacy deprecated no-op).

Examples:

Setting auto configuration

Settings.auto = true

Parameters:

  • _val (Object)

    The value to set (not used)

Returns:

  • (nil)

    Always returns nil



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

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

.autoloadnil

Autoloads settings (legacy deprecated no-op).

Examples:

Autoloading settings

Settings.autoload

Returns:

  • (nil)

    Always returns nil



674
675
676
677
678
# File 'documented/common/settings.rb', line 674

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

.clearnil

Clears all settings (legacy deprecated no-op).

Examples:

Clearing settings

Settings.clear

Returns:

  • (nil)

    Always returns nil



644
645
646
647
648
# File 'documented/common/settings.rb', line 644

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

Examples:

Checking if a value is a container

Settings.container?(my_value)

Parameters:

  • value (Object)

    The value to check

Returns:

  • (Boolean)

    True if the value is a container, false otherwise



119
120
121
# File 'documented/common/settings.rb', line 119

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

.current_script_settings(scope = DEFAULT_SCOPE) ⇒ Object

Retrieves the current settings for the active script and scope.

Examples:

Getting current script settings

settings = Settings.current_script_settings

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    The scope for which to retrieve settings (default is ‘:’)

Returns:

  • (Object)

    The current settings for the specified scope



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

def self.current_script_settings(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_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



58
59
60
# File 'documented/common/settings.rb', line 58

def self.get_log_level
  @@log_level
end

.get_scoped_setting(scope_string, key_name) ⇒ Object

Retrieves a scoped setting by key name.

Examples:

Getting a scoped setting

value = Settings.get_scoped_setting("my_scope", "my_setting")

Parameters:

  • scope_string (String)

    The scope to retrieve the setting from

  • key_name (String)

    The key name of the setting to retrieve

Returns:

  • (Object)

    The value of the setting, or nil if not found



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

def self.get_scoped_setting(scope_string, key_name)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "get_scoped_setting: scope: #{scope_string.inspect}, key: #{key_name.inspect}" })
  data_for_scope = current_script_settings(scope_string)
  _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] : [])
end

.get_value_from_container(container, key) ⇒ Object

Retrieves a value from a container (Hash or Array) by key.

Examples:

Getting a value from a container

value = Settings.get_value_from_container(my_hash, "my_key")

Parameters:

  • container (Object)

    The container to retrieve the value from

  • key (Object)

    The key to look up in the container

Returns:

  • (Object)

    The value found, or nil if not found



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

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

Loads the current settings (legacy, aliasing to refresh_data).

Examples:

Loading settings

settings = Settings.load

Returns:

  • (Object)

    The refreshed settings



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

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

Handles missing methods for the Settings module.

Examples:

Handling a missing method

Settings.some_missing_method

Parameters:

  • method (Symbol)

    The name of the missing method

  • args (Array)

    The arguments passed to the missing method

  • block (Proc)

    The block passed to the missing method

Returns:

  • (Object)

    The result of the method call, or nil if safe navigation is active



687
688
689
690
691
692
693
694
695
696
697
698
699
# File 'documented/common/settings.rb', line 687

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

Examples:

Navigating to a path

target, root = Settings.navigate_to_path

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 ‘:’)

Returns:

  • (Array)

    An array containing the target and the root for the specified scope



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'documented/common/settings.rb', line 428

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

Refreshes the settings data for the current script and scope.

Examples:

Refreshing settings data

Settings.refresh_data

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    The scope for which to refresh data (default is ‘:’)

Returns:

  • (Object)

    The refreshed settings for the specified scope



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

Resets the path navigator and returns the specified value.

Examples:

Resetting path and returning a value

result = Settings.reset_path_and_return(my_value)

Parameters:

  • value (Object)

    The value to return after resetting the path

Returns:

  • (Object)

    The value passed in



418
419
420
# File 'documented/common/settings.rb', line 418

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

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

Checks if the Settings module responds to a missing method.

Examples:

Checking for a missing method

exists = Settings.respond_to_missing?(:some_method)

Parameters:

  • method_name (Symbol)

    The name of the method to check

  • include_private (Boolean) (defaults to: false)

    Whether to include private methods in the check

Returns:

  • (Boolean)

    True if the method is handled, false otherwise



707
708
709
# File 'documented/common/settings.rb', line 707

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 given scope and script name.

Examples:

Getting the root proxy

proxy = Settings.root_proxy_for("my_scope")

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 if the scope is nil or empty



161
162
163
164
165
166
167
168
169
# File 'documented/common/settings.rb', line 161

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)
end

.saveSymbol

Saves the current settings (legacy no-op).

Examples:

Saving settings

result = Settings.save

Returns:

  • (Symbol)

    Always returns :noop



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

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

.save_allnil

Saves all settings (legacy deprecated no-op).

Examples:

Saving all settings

Settings.save_all

Returns:

  • (nil)

    Always returns nil



634
635
636
637
638
# File 'documented/common/settings.rb', line 634

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

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

Examples:

Saving changes to a proxy

Settings.save_proxy_changes(my_proxy)

Parameters:

Returns:

  • (nil)

    Always returns nil



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
346
# File 'documented/common/settings.rb', line 176

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
  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 ---
  fresh_root = @db_adapter.get_settings(script_name, scope)

  cached = @settings_cache[cache_key]
  if cached
    if 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 refreshed from DB 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)
      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)
    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)
  sync_cache.call(current_root_for_scope)
end

.save_to_database(data_to_save, scope = DEFAULT_SCOPE) ⇒ nil

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

Examples:

Saving data to the database

Settings.save_to_database(my_data)

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 ‘:’)

Returns:

  • (nil)

    Always returns nil



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

def self.save_to_database(data_to_save, scope = DEFAULT_SCOPE)
  script_name = Script.current.name

  if script_name.nil? || script_name.empty?
    _log(LOG_LEVEL_ERROR, @@log_prefix, -> { "save_to_database: Aborting save. Script.current.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) ⇒ Integer

Sets the logging level for the Settings module.

Examples:

Setting log level to debug

Settings.set_log_level(:debug)

Parameters:

  • level (Symbol)

    The log level to set (:none, :error, :info, :debug)

Returns:

  • (Integer)

    The numeric value of the log level set

Raises:

  • CircularReferenceError if an invalid log level is specified



43
44
45
46
47
48
49
50
51
52
53
54
# File 'documented/common/settings.rb', line 43

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

Sets a specific setting for the current script and scope.

Examples:

Setting a script setting

Settings.set_script_settings("my_scope", "my_setting", "my_value")

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    The scope for which to set the setting (default is ‘:’)

  • name (String)

    The name of the setting to set

  • value (Object)

    The value to assign to the setting

Returns:

  • (nil)

    Always returns nil



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

def self.set_script_settings(scope = DEFAULT_SCOPE, name, value)
  _log(LOG_LEVEL_DEBUG, @@log_prefix, -> { "set_script_settings: scope: #{scope.inspect}, name: #{name.inspect}, value: #{value.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)
  _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)
  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)
    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) ⇒ Hash

Converts the current settings to a hash representation (legacy, aliasing to to_hash).

Examples:

Converting settings to a hash

settings_hash = Settings.to_h

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    The scope to convert (default is ‘:’)

Returns:

  • (Hash)

    The settings as a hash



625
626
627
628
# File 'documented/common/settings.rb', line 625

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 to a hash representation.

Examples:

Converting settings to a hash

settings_hash = Settings.to_hash

Parameters:

  • scope (String) (defaults to: DEFAULT_SCOPE)

    The scope to convert (default is ‘:’)

Returns:

  • (Hash)

    The settings as a hash



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

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

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

Examples:

Wrapping a value if it is a container

wrapped_value = Settings.wrap_value_if_container(my_value, "my_scope", ["my_key"])

Parameters:

  • value (Object)

    The value to wrap

  • scope (String)

    The scope for which to wrap the value

  • path_array (Array)

    The path array for the value

Returns:

  • (Object)

    The wrapped value if it is a container, otherwise the original value



578
579
580
581
582
583
584
585
586
587
# File 'documented/common/settings.rb', line 578

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