Module: Lich::Gemstone::Infomon

Extended by:
Common::Watchable
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

Provides functionality for monitoring and managing game data.

See Also:

Defined Under Namespace

Modules: Parser, XMLParser Classes: Cache

Constant Summary collapse

AllowedTypes =
[Integer, String, NilClass, FalseClass, TrueClass]

Class Method Summary collapse

Methods included from Common::Watchable

watch!

Class Method Details

._key(key) ⇒ String

Normalizes the key for storage in the cache.

Parameters:

  • key (String)

    the original key

Returns:

  • (String)

    the normalized key



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

def self._key(key)
  key.to_s.downcase.tr(' -', '_').gsub(/_+/, '_')
end

._validate!(key, value) ⇒ Object

Validates the key and value types 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 type is invalid



176
177
178
179
# File 'documented/gemstone/infomon.rb', line 176

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) ⇒ Boolean, String

Converts the value to a boolean if it is a string representation.

Parameters:

  • val (String, Boolean)

    the original value

Returns:

  • (Boolean, String)

    the converted value



164
165
166
167
168
# File 'documented/gemstone/infomon.rb', line 164

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

.cacheInfomon::Cache

Returns the cache instance used for storing game data.

Returns:



41
42
43
# File 'documented/gemstone/infomon.rb', line 41

def self.cache
  @cache
end

.cache_loadvoid

This method returns an undefined value.

Loads the cache with data 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

Ensures that the context is valid before accessing Infomon data.

Raises:

  • (RuntimeError)

    if XMLData.name is not loaded



93
94
95
96
97
# File 'documented/gemstone/infomon.rb', line 93

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

.current_timestampFloat

Returns the current UTC timestamp as a float.

Returns:

  • (Float)


87
88
89
# File 'documented/gemstone/infomon.rb', line 87

def self.current_timestamp
  Time.now.utc.to_f
end

.dbSequel::Database

Returns the Sequel database connection instance.

Returns:

  • (Sequel::Database)


53
54
55
# File 'documented/gemstone/infomon.rb', line 53

def self.db
  @db
end

.db_refresh_needed?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Checks if a refresh of the infomon database is needed.

This method determines if the database structure has changed or if the version of the infomon data is outdated.

Returns:

  • (Boolean)

    true if a refresh is needed, false otherwise



98
99
100
101
102
103
104
105
# File 'documented/gemstone/infomon/cli.rb', line 98

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



276
277
278
279
280
# File 'documented/gemstone/infomon.rb', line 276

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 path to the database file.

Returns:



47
48
49
# File 'documented/gemstone/infomon.rb', line 47

def self.file
  @file
end

.flush(timeout_seconds: 5) ⇒ Boolean

Flushes the SQL queue, ensuring all queued operations are executed.

Parameters:

  • timeout_seconds (Integer) (defaults to: 5)

    the maximum time to wait for the flush

Returns:

  • (Boolean)

    true if flushed successfully, false if timed out



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

def self.flush(timeout_seconds: 5)
  return true if self.queue.empty?

  # Create a barrier token - a Queue that the worker will signal when reached
  barrier = ::Queue.new
  self.queue << barrier

  # Wait for the worker to signal completion (with timeout)
  begin
    ::Timeout.timeout(timeout_seconds) { barrier.pop }
    true
  rescue ::Timeout::Error
    Lich.log "warning: Infomon.flush timed out after #{timeout_seconds}s"
    false
  end
end

.get(key) ⇒ String?

Retrieves a value from the cache or database by key.

Examples:

Get a value

Infomon.get("example_key")

Parameters:

  • key (String)

    the key to retrieve

Returns:

  • (String, nil)

    the value associated with the key or nil if not found



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

def self.get(key)
  self.cache_load if !@cache_loaded
  key = self._key(key)
  val = self.cache.get(key) {
    # Flush queue before reading from DB to ensure we see latest writes
    self.flush
    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 by key.

Examples:

Get a boolean value

Infomon.get_bool("example_key")

Parameters:

  • key (String)

    the key to retrieve

Returns:

  • (Boolean)

    the boolean value associated with the key



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

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 last updated timestamp for a given key.

Parameters:

  • key (String)

    the key to retrieve the timestamp for

Returns:

  • (Float, nil)

    the updated timestamp or nil if not found



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

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 used for thread safety.

Returns:

  • (Mutex)


59
60
61
# File 'documented/gemstone/infomon.rb', line 59

def self.mutex
  @sql_mutex
end

.mutex_lockObject



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



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

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

.queueObject



81
82
83
# File 'documented/gemstone/infomon.rb', line 81

def self.queue
  @sql_queue
end

.redo!void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Resets the infomon data and repopulates it.

This method deletes the character table, recreates it, and then syncs the data.



57
58
59
60
61
62
63
# File 'documented/gemstone/infomon/cli.rb', line 57

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 current table and clearing the cache.



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

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



264
265
266
267
268
269
270
271
# File 'documented/gemstone/infomon.rb', line 264

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`, `updated_at`) VALUES (%s, %s, %s)
on conflict(`key`) do update set value = excluded.value, updated_at = excluded.updated_at;" % [self.db.literal(self.table_name), self.db.literal(key), self.db.literal(value), current_timestamp]
end

.setup!Sequel::Dataset

Sets up the database table for Infomon if it does not exist.

Returns:

  • (Sequel::Dataset)


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 information for the character.

If the full parameter is set to true, all stored values are displayed. Otherwise, only non-zero values are shown.

Parameters:

  • full (Boolean) (defaults to: false)

    whether to display all values or only non-zero ones



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'documented/gemstone/infomon/cli.rb', line 72

def self.show(full = false)
  response = []
  # display all stored db values
  respond "Displaying stored information for #{XMLData.name}"
  # Flush async SQL queue before reading from DB to ensure consistency
  Infomon.flush
  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 is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Synchronizes the character's infomon settings with the server.

This method checks for active spells that may interfere with the sync process, such as the Shroud of Deception, and handles them accordingly.



12
13
14
15
16
17
18
19
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
# File 'documented/gemstone/infomon/cli.rb', line 12

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 full'          => /<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

.tableObject



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

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

.table_nameSymbol

Returns the name of the database table based on game and character name.

Returns:

  • (Symbol)


101
102
103
104
# File 'documented/gemstone/infomon.rb', line 101

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



254
255
256
257
258
# File 'documented/gemstone/infomon.rb', line 254

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 in a batch operation.

Parameters:

  • blob (Array)

    an array of key-value pairs to upsert



305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'documented/gemstone/infomon.rb', line 305

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?
  now = current_timestamp
  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, %s)] % [self.db.literal(key), self.db.literal(value), now]
  }.join(", ")
  # queue sql statement to run async
  self.queue << "INSERT OR REPLACE INTO %s (`key`, `value`, `updated_at`) VALUES %s
on conflict(`key`) do update set value = excluded.value, updated_at = excluded.updated_at;" % [self.db.literal(self.table_name), pairs]
end

.watch!void

This method returns an undefined value.

Starts a thread to monitor and initialize Infomon when the game is ready.



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'documented/gemstone/infomon.rb', line 347

def self.watch!
  @init_thread ||= Thread.new do
    begin
      # Wait for character to be ready and dialogs to load
      sleep 0.1 until GameBase::Game.autostarted? && XMLData.name && !XMLData.name.empty? &&
                      !XMLData.dialogs.empty?

      # Run initial setup if needed (GS-specific only, skip for DR)
      if XMLData.game !~ /^DR/ && db_refresh_needed?
        ExecScript.start("Infomon.redo!", { quiet: true, name: "infomon_reset" })
      end

      PostLoad.game_loaded! if defined?(PostLoad)
    rescue StandardError => e
      respond 'Error in Infomon initialization thread'
      respond e.inspect
    end
  end
end