Module: Lich::Gemstone::Infomon

Defined in:
documented/gemstone/infomon.rb,
documented/gemstone/infomon/cli.rb,
documented/gemstone/infomon/cache.rb,
documented/gemstone/infomon/parser.rb,
documented/gemstone/infomon/xmlparser.rb

Overview

CLI commands for Infomon

This module contains command-line interface (CLI) commands for managing Infomon data.

Examples:

Using the Infomon CLI

Lich::Gemstone::Infomon.sync

Defined Under Namespace

Modules: Parser, XMLParser Classes: Cache

Constant Summary collapse

AllowedTypes =

Allowed types for values in Infomon This constant defines the types that can be stored in the Infomon database.

[Integer, String, NilClass, FalseClass, TrueClass]

Class Method Summary collapse

Class Method Details

._key(key) ⇒ String

Normalizes the key for storage

Parameters:

  • key (String)

    The key to normalize

Returns:

  • (String)

    The normalized key



157
158
159
160
161
# File 'documented/gemstone/infomon.rb', line 157

def self._key(key)
  key = key.to_s.downcase
  key.tr!(' ', '_').gsub!('_-_', '_').tr!('-', '_') if /\s|-/.match?(key)
  return key
end

._validate!(key, value) ⇒ Object

Validates the key and value before insertion

Parameters:

  • key (String)

    The key to validate

  • value (Object)

    The value to validate

Returns:

  • (Object)

    The validated value

Raises:

  • (RuntimeError)

    If the value is of an invalid type



180
181
182
183
# File 'documented/gemstone/infomon.rb', line 180

def self._validate!(key, value)
  return self._value(value) if AllowedTypes.include?(value.class)
  raise "infomon:insert(%s) was called with %s\nmust be %s\nvalue=%s" % [key, value.class, AllowedTypes.map(&:name).join("|"), value]
end

._value(val) ⇒ Object

Normalizes the value for storage

Parameters:

  • val (Object)

    The value to normalize

Returns:

  • (Object)

    The normalized value



166
167
168
169
170
# File 'documented/gemstone/infomon.rb', line 166

def self._value(val)
  return true if val.to_s == "true"
  return false if val.to_s == "false"
  return val
end

.cacheCache

Returns the cache object

Returns:

  • (Cache)

    The cache instance used by Infomon



39
40
41
# File 'documented/gemstone/infomon.rb', line 39

def self.cache
  @cache
end

.cache_loadvoid

This method returns an undefined value.

Loads the cache from the database



146
147
148
149
150
151
152
# File 'documented/gemstone/infomon.rb', line 146

def self.cache_load
  sleep(0.01) if XMLData.name.empty?
  dataset = Infomon.table
  h = dataset.map(:key).zip(dataset.map(:value)).to_h
  self.cache.merge!(h)
  @cache_loaded = true
end

.context!Object

Checks if the context is valid before accessing Infomon

Raises:

  • (RuntimeError)

    If XMLData.name is not loaded



91
92
93
94
95
# File 'documented/gemstone/infomon.rb', line 91

def self.context!
  return unless XMLData.name.empty? or XMLData.name.nil?
  puts Exception.new.backtrace
  fail "cannot access Infomon before XMLData.name is loaded"
end

.dbSequel::Database

Returns the database connection

Returns:

  • (Sequel::Database)

    The Sequel database connection instance



51
52
53
# File 'documented/gemstone/infomon.rb', line 51

def self.db
  @db
end

.db_refresh_needed?Boolean

Checks if a refresh of the Infomon database is needed.

This method determines if the database needs to be refreshed based on the last sync date and version of the Infomon data structure.

Examples:

Checking if a database refresh is needed

Lich::Gemstone::Infomon.db_refresh_needed?

Returns:

  • (Boolean)

    true if a refresh is needed, false otherwise



105
106
107
108
109
110
111
112
# File 'documented/gemstone/infomon/cli.rb', line 105

def self.db_refresh_needed?
  # Change date below to the last date of infomon.db structure change to allow for a forced reset of data.
  # Change Lich version below to also force a refresh of DB as well due to new API/methods used by infomon (introduction of CHE and account subscription status for example).
  return true if Infomon.get("infomon.last_sync_date").nil?
  return true if Infomon.get("infomon.last_sync_date") < Time.new(2025, 6, 26, 20, 0, 0).to_i
  return true if Gem::Version.new("5.12.2") > Gem::Version.new(Infomon.get("infomon.last_sync_version"))
  return false
end

.delete!(key) ⇒ void

This method returns an undefined value.

Deletes a key from the cache and database

Parameters:

  • key (String)

    The key to delete



279
280
281
282
283
# File 'documented/gemstone/infomon.rb', line 279

def self.delete!(key)
  key = self._key(key)
  self.cache.delete(key)
  self.queue << "DELETE FROM %s WHERE key = (%s);" % [self.db.literal(self.table_name), self.db.literal(key)]
end

.fileString

Returns the file path for the Infomon database

Returns:

  • (String)

    The path to the Infomon database file



45
46
47
# File 'documented/gemstone/infomon.rb', line 45

def self.file
  @file
end

.get(key) ⇒ Object

Retrieves a value from the cache or database

Examples:

Retrieving a value

value = Infomon.get("key")

Parameters:

  • key (String)

    The key to retrieve

Returns:

  • (Object)

    The value associated with the key

Raises:

  • (StandardError)

    If an error occurs during retrieval



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
# File 'documented/gemstone/infomon.rb', line 191

def self.get(key)
  self.cache_load if !@cache_loaded
  key = self._key(key)
  val = self.cache.get(key) {
    sleep 0.01 until self.queue.empty?
    begin
      self.mutex.synchronize do
        begin
          db_result = self.table[key: key]
          if db_result
            db_result[:value]
          else
            nil
          end
        rescue => exception
          pp(exception)
          nil
        end
      end
    rescue StandardError
      respond "--- Lich: error: Infomon.get(#{key}): #{$!}"
      Lich.log "error: Infomon.get(#{key}): #{$!}\n\t#{$!.backtrace.join("\n\t")}"
    end
  }
  return self._value(val)
end

.get_bool(key) ⇒ Boolean

Retrieves a boolean value from the cache or database

Parameters:

  • key (String)

    The key to retrieve

Returns:

  • (Boolean)

    The boolean value associated with the key



221
222
223
224
225
226
227
228
229
230
# File 'documented/gemstone/infomon.rb', line 221

def self.get_bool(key)
  value = Infomon.get(key)
  if value.is_a?(TrueClass) || value.is_a?(FalseClass)
    return value
  elsif value == 1
    return true
  else
    return false
  end
end

.get_updated_at(key) ⇒ Float?

Retrieves the updated_at timestamp for a key

Parameters:

  • key (String)

    The key to retrieve the timestamp for

Returns:

  • (Float, nil)

    The updated_at timestamp or nil if not found

Raises:

  • (StandardError)

    If an error occurs during retrieval



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'documented/gemstone/infomon.rb', line 236

def self.get_updated_at(key)
  key = self._key(key)
  begin
    self.mutex.synchronize do
      db_result = self.table[key: key]
      if db_result
        db_result[:updated_at]
      else
        nil
      end
    end
  rescue StandardError
    respond "--- Lich: error: Infomon.get_updated_at(#{key}): #{$!}"
    Lich.log "error: Infomon.get_updated_at(#{key}): #{$!}\n\t#{$!.backtrace.join("\n\t")}"
    nil
  end
end

.mutexMutex

Returns the mutex for thread safety

Returns:

  • (Mutex)

    The mutex used for synchronizing access



57
58
59
# File 'documented/gemstone/infomon.rb', line 57

def self.mutex
  @sql_mutex
end

.mutex_lockObject

Locks the mutex to ensure thread safety

Raises:

  • (StandardError)

    If an error occurs while locking



63
64
65
66
67
68
69
70
# File 'documented/gemstone/infomon.rb', line 63

def self.mutex_lock
  begin
    self.mutex.lock unless self.mutex.owned?
  rescue StandardError
    respond "--- Lich: error: Infomon.mutex_lock: #{$!}"
    Lich.log "error: Infomon.mutex_lock: #{$!}\n\t#{$!.backtrace.join("\n\t")}"
  end
end

.mutex_unlockObject

Unlocks the mutex to allow other threads access

Raises:

  • (StandardError)

    If an error occurs while unlocking



74
75
76
77
78
79
80
81
# File 'documented/gemstone/infomon.rb', line 74

def self.mutex_unlock
  begin
    self.mutex.unlock if self.mutex.owned?
  rescue StandardError
    respond "--- Lich: error: Infomon.mutex_unlock: #{$!}"
    Lich.log "error: Infomon.mutex_unlock: #{$!}\n\t#{$!.backtrace.join("\n\t")}"
  end
end

.queueQueue

Returns the SQL queue

Returns:

  • (Queue)

    The queue for SQL statements



85
86
87
# File 'documented/gemstone/infomon.rb', line 85

def self.queue
  @sql_queue
end

.redo!void

This method returns an undefined value.

Resets and repopulates the Infomon character table.

This method deletes the existing character table, recreates it, and then syncs the data again. This is a destructive operation.

Examples:

Resetting Infomon data

Lich::Gemstone::Infomon.redo!


66
67
68
69
70
71
72
# File 'documented/gemstone/infomon/cli.rb', line 66

def self.redo!
  # Destructive - deletes char table, recreates it, then repopulates it
  respond 'Infomon complete reset reqeusted.'
  Infomon.reset!
  Infomon.sync
  respond 'Infomon reset is now complete.'
end

.reset!void

This method returns an undefined value.

Resets the Infomon state by dropping the table and clearing the cache



106
107
108
109
110
111
112
# File 'documented/gemstone/infomon.rb', line 106

def self.reset!
  self.mutex_lock
  Infomon.db.drop_table?(self.table_name)
  self.cache.clear
  @cache_loaded = false
  Infomon.setup!
end

.set(key, value) ⇒ Symbol

Sets a key-value pair in the cache and database

Parameters:

  • key (String)

    The key to set

  • value (Object)

    The value to set

Returns:

  • (Symbol)

    :noop if the value is unchanged, otherwise performs the insert operation



267
268
269
270
271
272
273
274
# File 'documented/gemstone/infomon.rb', line 267

def self.set(key, value)
  key = self._key(key)
  value = self._validate!(key, value)
  return :noop if self.cache.get(key) == value
  self.cache.put(key, value)
  self.queue << "INSERT OR REPLACE INTO %s (`key`, `value`) VALUES (%s, %s)
on conflict(`key`) do update set value = excluded.value;" % [self.db.literal(self.table_name), self.db.literal(key), self.db.literal(value)]
end

.setup!Sequel::Dataset

Sets up the Infomon table if it does not exist

Returns:

  • (Sequel::Dataset)

    The dataset for the Infomon table



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'documented/gemstone/infomon.rb', line 122

def self.setup!
  self.mutex_lock

  # Check if table exists but missing updated_at column
  if @db.table_exists?(self.table_name)
    columns = @db.schema(self.table_name).map { |col| col[0] }
    unless columns.include?(:updated_at)
      self.mutex_unlock
      self.reset!
      return
    end
  end

  @db.create_table?(self.table_name) do
    text :key, primary_key: true
    any :value
    float :updated_at
  end
  self.mutex_unlock
  @_table = @db[self.table_name]
end

.show(full = false) ⇒ void

This method returns an undefined value.

Displays stored Infomon information for the current character.

This method can display either a full or filtered view of the stored data.

Examples:

Showing Infomon data

Lich::Gemstone::Infomon.show(true)

Parameters:

  • full (Boolean) (defaults to: false)

    Whether to display all data (default: false)



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'documented/gemstone/infomon/cli.rb', line 81

def self.show(full = false)
  response = []
  # display all stored db values
  respond "Displaying stored information for #{XMLData.name}"
  Infomon.table.map([:key, :value]).each { |k, v|
    response << "#{k} : #{v.inspect}\n"
  }
  unless full
    response.each { |_line|
      response.reject! do |line|
        line.match?(/\s:\s0$/)
      end
    }
  end
  respond response
end

.syncvoid

This method returns an undefined value.

CLI commands for Infomon Synchronizes Infomon data with the current character’s settings.

This method checks for active spells that may interfere with the sync process, and retrieves various character information to update Infomon.

Examples:

Syncing Infomon data

Lich::Gemstone::Infomon.sync

Raises:

  • (StandardError)

    if there is an issue during command execution.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'documented/gemstone/infomon/cli.rb', line 20

def self.sync
  # since none of this information is 3rd party displayed, silence is golden.
  shroud_detected = false
  respond 'Infomon sync requested.'
  if Effects::Spells.active?(1212)
    respond 'ATTENTION:  SHROUD DETECTED - disabling Shroud of Deception to sync character\'s infomon setting'
    while Effects::Spells.active?(1212)
      dothistimeout('STOP 1212', 3, /^With a moment's concentration, you terminate the Shroud of Deception spell\.$|^Stop what\?$/)
      sleep(0.5)
    end
    shroud_detected = true
  end
  request = { 'info'               => /<a exist=.+#{XMLData.name}/,
              'skill'              => /<a exist=.+#{XMLData.name}/,
              'spell'              => %r{<output class="mono"/>},
              'experience'         => %r{<output class="mono"/>},
              'society'            => %r{<pushBold/>},
              'citizenship'        => /^You don't seem|^You currently have .+ in/,
              'armor list all'     => /<a exist=.+#{XMLData.name}/,
              'cman list all'      => /<a exist=.+#{XMLData.name}/,
              'feat list all'      => /<a exist=.+#{XMLData.name}/,
              'shield list all'    => /<a exist=.+#{XMLData.name}/,
              'weapon list all'    => /<a exist=.+#{XMLData.name}/,
              'ascension list all' => /<a exist=.+#{XMLData.name}/,
              'resource'           => /^Health: \d+\/(?:<pushBold\/>)?\d+(?:<popBold\/>)?\s+Mana: \d+\/(?:<pushBold\/>)?\d+(?:<popBold\/>)?\s+Stamina: \d+\/(?:<pushBold\/>)?\d+(?:<popBold\/>)?\s+Spirit: \d+\/(?:<pushBold\/>)?\d+/,
              'warcry'             => /^You have learned the following War Cries:|^You must be an active member of the Warrior Guild to use this skill/,
              'profile full'       => %r{<output class="mono"/>} }

  request.each do |command, start_capture|
    respond "Retrieving character #{command}." if $infomon_debug
    Lich::Util.issue_command(command.to_s, start_capture, /<prompt/, include_end: true, timeout: 5, silent: false, usexml: true, quiet: true)
    respond "Did #{command}." if $infomon_debug
  end
  respond 'Requested Infomon sync complete.'
  respond 'ATTENTION:  TEND TO YOUR SHROUD!' if shroud_detected
  Infomon.set('infomon.last_sync_date', Time.now.to_i)
  Infomon.set('infomon.last_sync_version', LICH_VERSION)
end

.tableSequel::Dataset

Returns the Infomon table

Returns:

  • (Sequel::Dataset)

    The dataset for the Infomon table



116
117
118
# File 'documented/gemstone/infomon.rb', line 116

def self.table
  @_table ||= self.setup!
end

.table_nameSymbol

Returns the table name based on the game and XMLData

Returns:

  • (Symbol)

    The table name for the Infomon data



99
100
101
102
# File 'documented/gemstone/infomon.rb', line 99

def self.table_name
  self.context!
  ("%s_%s" % [XMLData.game, XMLData.name]).to_sym
end

.upsert(*args) ⇒ void

This method returns an undefined value.

Inserts or replaces a record in the database

Parameters:

  • args (Array)

    The arguments for the insert operation



257
258
259
260
261
# File 'documented/gemstone/infomon.rb', line 257

def self.upsert(*args)
  self.table
      .insert_conflict(:replace)
      .insert(*args)
end

.upsert_batch(*blob) ⇒ void

This method returns an undefined value.

Inserts or replaces multiple records in the database

Parameters:

  • blob (Array)

    An array of key-value pairs to upsert



288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'documented/gemstone/infomon.rb', line 288

def self.upsert_batch(*blob)
  updated = (blob.first.map { |k, v| [self._key(k), self._validate!(k, v)] } - self.cache.to_a)
  return :noop if updated.empty?
  pairs = updated.map { |key, value|
    (value.is_a?(Integer) or value.is_a?(String)) or fail "upsert_batch only works with Integer or String types"
    # add the value to the cache
    self.cache.put(key, value)
    %[(%s, %s)] % [self.db.literal(key), self.db.literal(value)]
  }.join(", ")
  # queue sql statement to run async
  self.queue << "INSERT OR REPLACE INTO %s (`key`, `value`) VALUES %s
on conflict(`key`) do update set value = excluded.value;" % [self.db.literal(self.table_name), pairs]
end