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

This module handles information monitoring for the game.

Examples:

Including the Infomon module

include Lich::Gemstone::Infomon

Defined Under Namespace

Modules: Parser, XMLParser Classes: Cache

Constant Summary collapse

AllowedTypes =

Allowed types for values in Infomon

[Integer, String, NilClass, FalseClass, TrueClass]

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

._key(key) ⇒ String

Normalizes the key for storage

Parameters:

  • key (String, Symbol)

    The key to normalize

Returns:

  • (String)

    The normalized key



145
146
147
148
149
# File 'documented/gemstone/infomon.rb', line 145

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 types

Parameters:

  • key (String)

    The key to validate

  • value (Object)

    The value to validate

Returns:

  • (Object)

    The validated value

Raises:

  • (RuntimeError)

    If the value type is not allowed



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

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



154
155
156
157
158
# File 'documented/gemstone/infomon.rb', line 154

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 instance used by Infomon

Returns:

  • (Cache)

    The cache instance



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



134
135
136
137
138
139
140
# File 'documented/gemstone/infomon.rb', line 134

def self.cache_load
  sleep(0.01) if XMLData.name.empty?
  dataset = Infomon.table
  h = Hash[dataset.map(:key).zip(dataset.map(:value))]
  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 Sequel database instance

Returns:

  • (Sequel::Database)

    The database 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



244
245
246
247
248
# File 'documented/gemstone/infomon.rb', line 244

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 database file path



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:

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



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

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



208
209
210
211
212
213
214
215
216
217
# File 'documented/gemstone/infomon.rb', line 208

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

.mutexMutex

Returns the mutex used for thread safety

Returns:

  • (Mutex)

    The mutex instance



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 for database operations

Returns:

  • (Queue)

    The SQL queue instance



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, clearing the cache and database table



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 operation



232
233
234
235
236
237
238
239
# File 'documented/gemstone/infomon.rb', line 232

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 database table for Infomon

Returns:

  • (Sequel::Dataset)

    The dataset for the created table



122
123
124
125
126
127
128
129
130
# File 'documented/gemstone/infomon.rb', line 122

def self.setup!
  self.mutex_lock
  @db.create_table?(self.table_name) do
    text :key, primary_key: true
    any :value
  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 database table for Infomon

Returns:

  • (Sequel::Dataset)

    The dataset for the table



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

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

.table_nameSymbol

Generates the table name based on game and XMLData

Returns:

  • (Symbol)

    The generated table name



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 key-value pair in the database

Parameters:

  • args (Array)

    The key-value pairs to insert



222
223
224
225
226
# File 'documented/gemstone/infomon.rb', line 222

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 key-value pairs in the database

Parameters:

  • blob (Array)

    An array of key-value pairs to insert



253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'documented/gemstone/infomon.rb', line 253

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

Instance Method Details

#Background(SQLQueueProcessor) ⇒ void

This method returns an undefined value.

Background thread for processing SQL statements from the queue. This thread continuously processes queued SQL statements asynchronously.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'documented/gemstone/infomon.rb', line 271

Thread.new do
  loop do
    sql_statement = Infomon.queue.pop
    begin
      Infomon.mutex.synchronize do
        begin
          Infomon.db.run(sql_statement)
        rescue StandardError => e
          pp(e)
        end
      end
    rescue StandardError
      respond "--- Lich: error: Infomon ThreadQueue: #{$!}"
      Lich.log "error: Infomon ThreadQueue: #{$!}\n\t#{$!.backtrace.join("\n\t")}"
    end
  end
end