Module: Lich::Gemstone::Infomon

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

Defined Under Namespace

Modules: Parser, XMLParser Classes: Cache

Constant Summary collapse

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

Class Method Summary collapse

Class Method Details

._key(key) ⇒ String

Normalizes the key by converting it to a string and formatting it.

Parameters:

  • key (Object)

    the key to normalize

Returns:

  • (String)

    the normalized key



156
157
158
159
160
# File 'lib/gemstone/infomon.rb', line 156

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 for 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 not allowed



179
180
181
182
# File 'lib/gemstone/infomon.rb', line 179

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, Object

Normalizes the value to a boolean or returns the original value.

Parameters:

  • val (Object)

    the value to normalize

Returns:

  • (Boolean, Object)

    the normalized value



166
167
168
169
170
# File 'lib/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

.cacheInfomon::Cache

Returns the cache object used by Infomon.

Returns:



36
37
38
# File 'lib/gemstone/infomon.rb', line 36

def self.cache
  @cache
end

.cache_loadvoid

Note:

This method will sleep for a short duration if XMLData.name is not loaded.

This method returns an undefined value.

Loads the cache from the database.



144
145
146
147
148
149
150
# File 'lib/gemstone/infomon.rb', line 144

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

Ensures that the context is valid before accessing Infomon.

Raises:

  • (RuntimeError)

    if XMLData.name is not loaded



95
96
97
98
99
# File 'lib/gemstone/infomon.rb', line 95

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



50
51
52
# File 'lib/gemstone/infomon.rb', line 50

def self.db
  @db
end

.db_refresh_needed?Boolean

Checks if a refresh of the Infomon database is needed.

Examples:

Lich::Gemstone::Infomon.db_refresh_needed?

Returns:

  • (Boolean)

    Returns true if a refresh is needed, false otherwise.



95
96
97
98
# File 'lib/gemstone/infomon/cli.rb', line 95

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.
  Infomon.get("infomon.last_sync").nil? || Infomon.get("infomon.last_sync") < Time.new(2024, 8, 5, 20, 0, 0).to_i
end

.delete!(key) ⇒ void

This method returns an undefined value.

Deletes a key from the cache and database.

Examples:

Infomon.delete!("some_key")

Parameters:

  • key (Object)

    the key to delete



268
269
270
271
272
# File 'lib/gemstone/infomon.rb', line 268

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

Returns:

  • (String)

    the file path for the database



43
44
45
# File 'lib/gemstone/infomon.rb', line 43

def self.file
  @file
end

.get(key) ⇒ Boolean, ...

Retrieves a value from the cache or database.

Examples:

value = Infomon.get("some_key")

Parameters:

  • key (Object)

    the key to retrieve

Returns:

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 'lib/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.

Examples:

is_enabled = Infomon.get_bool("feature_enabled")

Parameters:

  • key (Object)

    the key to retrieve

Returns:

  • (Boolean)

    the retrieved boolean value



224
225
226
227
228
229
230
231
232
233
# File 'lib/gemstone/infomon.rb', line 224

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 SQL operations.

Returns:

  • (Mutex)

    the mutex for SQL operations



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

def self.mutex
  @sql_mutex
end

.mutex_lockObject

Locks the mutex for thread-safe operations.

Raises:

  • (StandardError)

    if an error occurs while locking



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

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 for thread-safe operations.

Raises:

  • (StandardError)

    if an error occurs while unlocking



76
77
78
79
80
81
82
83
# File 'lib/gemstone/infomon.rb', line 76

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 pending operations.

Returns:

  • (Queue)

    the SQL queue



88
89
90
# File 'lib/gemstone/infomon.rb', line 88

def self.queue
  @sql_queue
end

.redo!void

This method returns an undefined value.

Resets the Infomon data for the character.

This method deletes the character table, recreates it, and repopulates it.

Examples:

Lich::Gemstone::Infomon.redo!

Raises:

  • (StandardError)

    Raises an error if the reset fails.



59
60
61
62
63
64
65
# File 'lib/gemstone/infomon/cli.rb', line 59

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.



112
113
114
115
116
117
118
# File 'lib/gemstone/infomon.rb', line 112

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.

Examples:

Infomon.set("some_key", "some_value")

Parameters:

  • key (Object)

    the key to set

  • value (Object)

    the value to set

Returns:

  • (Symbol)

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

Raises:

  • (RuntimeError)

    if the value type is not valid



253
254
255
256
257
258
259
260
# File 'lib/gemstone/infomon.rb', line 253

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 in the database.

Returns:

  • (Sequel::Dataset)

    the created table object



130
131
132
133
134
135
136
137
138
# File 'lib/gemstone/infomon.rb', line 130

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) ⇒ Array<String>

Displays stored Infomon data for the character.

Examples:

Lich::Gemstone::Infomon.show

Parameters:

  • full (Boolean) (defaults to: false)

    If true, displays all data including zero values.

Returns:

  • (Array<String>)

    An array of strings representing the stored data.



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

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

This method synchronizes the character’s Infomon settings.

Examples:

Lich::Gemstone::Infomon.sync

Raises:

  • (StandardError)

    Raises an error if the command execution fails.



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 'lib/gemstone/infomon/cli.rb', line 14

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/ }

  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', Time.now.to_i)
end

.tableSequel::Dataset

Returns the table object for the Infomon database.

Returns:

  • (Sequel::Dataset)

    the table object



123
124
125
# File 'lib/gemstone/infomon.rb', line 123

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

.table_nameSymbol

Returns the table name based on the game and XMLData name.

Returns:

  • (Symbol)

    the table name



104
105
106
107
# File 'lib/gemstone/infomon.rb', line 104

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 updates a record in the database.

Parameters:

  • args (Array)

    the arguments for the insert operation



239
240
241
242
243
# File 'lib/gemstone/infomon.rb', line 239

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

.upsert_batch(*blob) ⇒ Symbol

Inserts or updates multiple records in the database.

Examples:

Infomon.upsert_batch([["key1", "value1"], ["key2", "value2"]])

Parameters:

  • blob (Array)

    an array of key-value pairs to upsert

Returns:

  • (Symbol)

    :noop if no updates are made

Raises:

  • (RuntimeError)

    if the value type is not valid



281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/gemstone/infomon.rb', line 281

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