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.
Defined Under Namespace
Modules: Parser, XMLParser Classes: Cache
Constant Summary collapse
Class Method Summary collapse
-
._key(key) ⇒ String
Normalizes the key for storage in the cache.
-
._validate!(key, value) ⇒ Object
Validates the key and value types before insertion.
-
._value(val) ⇒ Boolean, String
Converts the value to a boolean if it is a string representation.
-
.cache ⇒ Infomon::Cache
Returns the cache instance used for storing game data.
-
.cache_load ⇒ void
Loads the cache with data from the database.
-
.context! ⇒ Object
Ensures that the context is valid before accessing Infomon data.
-
.current_timestamp ⇒ Float
Returns the current UTC timestamp as a float.
-
.db ⇒ Sequel::Database
Returns the Sequel database connection instance.
-
.db_refresh_needed? ⇒ Boolean
private
Checks if a refresh of the infomon database is needed.
-
.delete!(key) ⇒ void
Deletes a key from the cache and database.
-
.file ⇒ String
Returns the path to the database file.
-
.flush(timeout_seconds: 5) ⇒ Boolean
Flushes the SQL queue, ensuring all queued operations are executed.
-
.get(key) ⇒ String?
Retrieves a value from the cache or database by key.
-
.get_bool(key) ⇒ Boolean
Retrieves a boolean value from the cache or database by key.
-
.get_updated_at(key) ⇒ Float?
Retrieves the last updated timestamp for a given key.
-
.mutex ⇒ Mutex
Returns the mutex used for thread safety.
- .mutex_lock ⇒ Object
- .mutex_unlock ⇒ Object
- .queue ⇒ Object
-
.redo! ⇒ void
private
Resets the infomon data and repopulates it.
-
.reset! ⇒ void
Resets the Infomon state by dropping the current table and clearing the cache.
-
.set(key, value) ⇒ Symbol
Sets a key-value pair in the cache and database.
-
.setup! ⇒ Sequel::Dataset
Sets up the database table for Infomon if it does not exist.
-
.show(full = false) ⇒ void
Displays stored information for the character.
-
.sync ⇒ void
private
Synchronizes the character's infomon settings with the server.
- .table ⇒ Object
-
.table_name ⇒ Symbol
Returns the name of the database table based on game and character name.
-
.upsert(*args) ⇒ void
Inserts or replaces a record in the database.
-
.upsert_batch(*blob) ⇒ void
Inserts or replaces multiple records in the database in a batch operation.
-
.watch! ⇒ void
Starts a thread to monitor and initialize Infomon when the game is ready.
Methods included from Common::Watchable
Class Method Details
._key(key) ⇒ String
Normalizes the key for storage in the cache.
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.
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.
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 |
.cache ⇒ Infomon::Cache
Returns the cache instance used for storing game data.
41 42 43 |
# File 'documented/gemstone/infomon.rb', line 41 def self.cache @cache end |
.cache_load ⇒ void
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.
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_timestamp ⇒ Float
Returns the current UTC timestamp as a float.
87 88 89 |
# File 'documented/gemstone/infomon.rb', line 87 def self. Time.now.utc.to_f end |
.db ⇒ Sequel::Database
Returns the Sequel database connection instance.
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.
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.
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 |
.file ⇒ String
Returns the path to the database file.
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.
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 = ::Queue.new self.queue << # Wait for the worker to signal completion (with timeout) begin ::Timeout.timeout(timeout_seconds) { .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.
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.
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.
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 |
.mutex ⇒ Mutex
Returns the mutex used for thread safety.
59 60 61 |
# File 'documented/gemstone/infomon.rb', line 59 def self.mutex @sql_mutex end |
.mutex_lock ⇒ Object
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_unlock ⇒ Object
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 |
.queue ⇒ Object
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.
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), ] end |
.setup! ⇒ Sequel::Dataset
Sets up the database table for Infomon if it does not exist.
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.
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 |
.sync ⇒ 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.
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 |
.table ⇒ Object
116 117 118 |
# File 'documented/gemstone/infomon.rb', line 116 def self.table @_table ||= self.setup! end |
.table_name ⇒ Symbol
Returns the name of the database table based on game and character name.
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.
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.
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 = 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 |