You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AardDurel-Dinv/aard_inventory.xml

20701 lines
844 KiB

<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<!-- Saved on Saturday, August 12, 2017, 8:51 AM -->
<!-- MuClient version 4.98 -->
<!-- Plugin "aard_inventory" generated by Plugin Wizard -->
<!--
=====================
Aard Inventory Plugin
=====================
Durel's Inventory Manager (dinv)
Durel's Bag-of-Tricks (dbot)
Author: Durel
Version history:
v0.1 - 2017-07-01 - Initial code
v0.2 - 2017-08-12 - Converted scripts into a plugin
v0.3 - 2017-09-26 - Functional plugin published to github
v1.0 - 2017-10-01 - It's alive! Most pieces are verified by alpha testers.
v2.0 - 2017-10-03 - Bumping major because mush plugins think v1.10 == v1.1 :P We'll use 2.0001 next.
Description
===========
This plugin includes two primary packages: dinv and dbot. The dinv package
provides options to manage inventory and analyze equipment sets. The dbot
package is a framework of common code that could be shared among multiple
plugins.
Durel's Inventory (dinv) Layout
===============================
inv.* : Plugin management
inv.version : Plugin and component versions
inv.config : Inventory plugin configuration
inv.cli : Command-line interfaces
inv.items : Inventory table creation, access, and display
inv.cache : Item cache management and access
inv.priority : How to prioritize / weight each stat for equipment sets
inv.score : Scoring items and sets based on an equipment set prioritization
inv.set : Equipment set management creation and management
inv.snapshot : Snapshots of specific equipment sets
inv.statBonus : Calculation and storage of character bonuses due to spells and equipment
inv.analyze : Calculation and storage of a priority's "optimal" equipment sets at each level
inv.usage : Calculate and display for which levels and priorities an item is used
inv.tags : Module to manage displaying terminating tags when a command completes
inv.consume : Module to manage buying, storing, and using consumable items (e.g., potions, pills)
inv.portal : Module to manage using portals from your inventory
inv.pass : Module to use area passes (not keys)
Durel's Bag-of-Tricks (dbot) Layout
===================================
dbot init : Init / de-init code for the dbot package
dbot generic : Top-level generic utility functions (e.g., custom version of tonumber, etc.)
dbot.retval : Return values / error codes
dbot.table : Convenient functions to manage table accesses
dbot.notify : Message notification sub-system
dbot.gmcp : Character and world state access functions via the GMCP protocol
dbot.storage : Save and load data using persistent storage
dbot.backup : System to support backing up and restoring all plugin state
dbot.emptyLine: Module to allow suppression of empty output lines
dbot.prompt : Module to transparently enable or disable the prompt and related output
dbot.invmon : Module to check if invmon is enabled
dbot.ability : Module to track if a character has access to a particular skill or spell
dbot.wish : Module to track which wishes a character has purchased
dbot.pagesize : Module to determine a character's current page size (# lines before page prompt)
dbot.execute : Execute one or more commands without contention from user-entered commands
dbot.callback : Module to help manage callback functions and parameters
dbot.remote : Module to retrieve remote files
dbot.version : Module to track version and changelog information and update the plugin
-->
<muclient>
<plugin
name="aard_inventory"
author="Durel"
id="88c86ea252fc1918556df9fe"
language="Lua"
purpose="Inventory management and equipment set analysis"
save_state="y"
date_written="2017-08-12 08:45:15"
requires="4.98"
version="2.0014"
>
<description trim="y">
<![CDATA[
Aard Inventory Manager
This plugin manages your inventory and gives you tools to analyze and use
items in your inventory.
The first step you'll need to take is to find an out-of-the-way room where
you won't disturb anyone and then run "dinv build confirm". This step
will identify all items you are wearing, all items in your main inventory,
and all items in containers in your inventory. This will take roughly 5
minutes depending on how many items are in your inventory. If you need to
halt the build simply go to sleep or go AFK. You can "refresh" your
inventory later to complete the process of identifying everything you are
carrying.
Once you have a completed inventory table available, I recommend making a
manual backup in case something goes wrong in the future. You can restore
from the backup and avoid the long build process again. If anything in
your inventory has changed since the backup, your next refresh will simply
update it to what you currently have. To make a manual backup named
"my_first_awesome_backup" type "dinv backup create my_first_awesome_backup".
See "dinv help backup" for more details about creating, viewing, and restoring
backups.
Type "dinv help" for all available usage options. For detailed examples of
how to use each option, simply type "dinv help <option>".
Usage
=====
Inventory table access
dinv build confirm
dinv refresh [on | off | eager | all] <minutes>
dinv search [id | full] <query>
Item management
dinv get <query>
dinv put <container relative name> <query>
dinv store <query>
dinv keyword [add | remove] <keyword name> <query>
dinv organize [add | clear | display] <container relative name> <query>
Equipment sets
dinv set [display | wear] <priority name> <level>
dinv snapshot [create | delete | list | display | wear] <snapshot name>
dinv priority [list | display | create | clone | delete | edit | copy | paste | compare] <name 1> <name 2>
Equipment analysis
dinv analyze [list | create | delete | display] <priority name> <positions>
dinv usage <priority name | all> <query>
dinv compare <priority name> <relative name>
dinv covet <priority name> <auction #>
Advanced options
dinv backup [list | create | delete | restore] <backup name>
dinv reset [list | confirm] <module names | all>
dinv forget <query>
dinv notify [none | light | standard | all]
dinv cache [reset | size] [recent | frequent | all] <# entries>
dinv tags <names | all> [on | off]
dinv reload
Using equipment items
dinv consume [add | remove | display | buy | small | big] <type> <name or quantity> <container>
dinv portal [use] <portal object ID>
dinv pass <pass ID> <# of seconds>
Plugin info
dinv version [check | changelog | update confirm]
dinv help <command>
Release Notes
=============
1) If you give an item to an enchanter to boost the item's stats, you may pull the old stats from a
cache instead of using the new enchantments when you get the item back. In this case, you may use
the "dinv forget <query>" option to remove that item from your inventory table and its related caches.
The next inventory refresh will pick up the new stats for the item.
2) Most aard operations that modify an item's stats are detected and automatically trigger a
re-identification. For example, enchantment spells, sharpening, reinforcing, tpenchanting, and wset
all are handled transparently. There are two known exceptions to this. First, the setweight
command does not result in an invitem update from aard's invitem system. Second, scribing a scroll
does not trigger an invitem update. As a result, until this is changed on aard's side of things
(or we manually add triggers in the plugin to watch for setweight and scribing) you will need to use
the "dinv forget <query>" option on an item that changes weight or a scroll that is scribed. This
will cause the plugin to "forget" the existing data on the item so that the new information will be
picked up on the next inventory refresh.
3) Wands and staves are not re-identified as they are used. As a result, the number of charges
shown in the item's display may not match reality as the item is used. If this mode is not added
to the aard invitem system, we may need to add a trigger to watch for this and update charges
accordingly.
4) The plugin does not automatically open containers that are closed. As a result, you won't be able
to get/put items in a closed container. Keep your containers open! :)
5) If you add a keyword to an item, drop it, and then pick it back up, the new keyword will still
be available on the item if it is still in your cache. However, this is not the case for common
consumable items. For example, we treat all duff beer potions as being identical so that you don't
need to individually identify each potion. As a result, the cached versions of those common items
won't maintain custom keywords.
6) If the plugin tags are enabled, they will echo an end tag at the conclusion of an operation. However,
if the user goes into a state that doesn't allow echoing (e.g., AFK) then the plugin cannot report the
end tag. In this scenario, the plugin will notify the user about the end tag via a warning notification
instead of an echo. Triggers cannot catch notifications though so any code relying on end tags should
either detect when you go AFK or cleanly time out after a reasonable amount of time.
7) If you add the portal wish after you have built your inventory table, you will need to either rebuild
the table (dinv build confirm) or forget/re-identify your portals (dinv forget type portal) and
(dinv refresh all).
Feature Wishlist
================
1) Implement a mechanism to (more) fully identify items if the identify wish is not available.
For example, we could use lore, the identify or object read spells, or Hester's identify
service found at "runto identify". This would be a manual process to "clean up" partially
identified items.
]]>
</description>
</plugin>
<!-- Get our standard constants -->
<include name="constants.lua"/>
<!-- Aliases -->
<aliases>
<alias
script="inv.cli.put.fn"
match="^[ ]*dinv[ ]+put[ ]+(.*?)[ ]+(.*?)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.build.fn"
match="^[ ]*dinv[ ]+build[ ]*( confirm|.*)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.refresh.fn"
match="^[ ]*dinv[ ]+refresh[ ]*(off|all)?[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.refresh.fn"
match="^[ ]*dinv[ ]+refresh[ ]*(on|eager)[ ]*([0-9]+)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.reset.fn"
match="^[ ]*dinv[ ]+reset[ ]+(list|confirm)[ ]*(.*?)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.search.fn"
match="^[ ]*dinv[ ]+search[ ]*(basic|id|full|raw|)[ ]*(.*?)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.set.fn"
match="^[ ]*dinv[ ]+set[ ]+(display|wear)[ ]+(.*?)[ ]*([0-9]*)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.snapshot.fn"
match="^[ ]*dinv[ ]+snapshot[ ]+(create|delete|list|display|wear)[ ]*([^ ]+)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.store.fn"
match="^[ ]*dinv[ ]+store[ ]+(.*?)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.tags.fn"
match="^[ ]*dinv[ ]+tags[ ]*(.*?)?[ ]*(on|off)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.usage.fn"
match="^[ ]*dinv[ ]+usage[ ]+(all|[^ ]+)[ ]*(.*?)?$"
enabled="y"
expand_variables="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.version.fn"
match="^[ ]*dinv[ ]+version[ ]*(check|changelog|update[ ]+confirm)?[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.organize.fn1"
match="^[ ]*dinv[ ]+organize[ ]+(add|clear)[ ]+([^ ]+)[ ]*(.*?)$"
enabled="y"
regexp="y"
send_to="12"
sequence="99"
>
</alias>
<alias
script="inv.cli.organize.fn2"
match="^[ ]*dinv[ ]+organize[ ]+(display)[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="99"
>
</alias>
<alias
script="inv.cli.help.fn"
match="^[ ]*dinv[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.analyze.fn"
match="^[ ]*dinv[ ]+analyze[ ]+(create|delete|display)[ ]+([^ ]*)[ ]*(.*?)?$"
enabled="y"
expand_variables="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.analyze.fn2"
match="^[ ]*dinv[ ]+analyze[ ]+list[ ]*$"
enabled="y"
expand_variables="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.keyword.fn"
match="^[ ]*dinv[ ]+keyword[ ]+(add|remove)[ ]+(.*?)([ ]+.*?)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.cache.fn"
match="^[ ]*dinv[ ]+cache[ ]+(reset|display|size)[ ]+(recent|frequent|all)[ ]*([0-9]+)?.*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.compare.fn"
match="^[ ]*dinv[ ]+compare[ ]+([^ ]+)[ ]+([^ ]+)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.consume.fn"
match="^[ ]*dinv[ ]+consume[ ]+(add|remove|display)[ ]*([^ ]+)?[ ]*(.*?)?[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.consume.fn"
match="^[ ]*dinv[ ]+consume[ ]+(buy|small|big)[ ]*([^ ]+)?[ ]*([^ ]+)?[ ]*([^ ]+)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.covet.fn"
match="^[ ]*dinv[ ]+covet[ ]+([^ ]+)[ ]+([^ ]+)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.forget.fn"
match="^[ ]*dinv[ ]+forget[ ]*(.*?)?$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.get.fn"
match="^[ ]*dinv[ ]+get[ ]+(.*?)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.help.fn"
match="^[ ]*dinv[ ]+help[ ]*([^ ]*)?[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.notify.fn"
match="^[ ]*dinv[ ]+notify[ ]+(none|light|standard|all)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.organize.fn3"
match="^[ ]*dinv[ ]+organize[ ]*(.*?)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.pass.fn"
match="^[ ]*dinv[ ]+pass[ ]+([^ ]+)[ ]+([0-9]+)[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.portal.fn"
match="^[ ]*dinv[ ]+portal[ ]+(use)[ ]+([0-9]+)[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.priority.fn"
match="^[ ]*dinv[ ]+priority[ ]+(list|display|create|delete|clone|compare|copy|paste)([ ]+[^ ]+)?([ ]+[^ ]+)?[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.priority.fn2"
match="^[ ]*dinv[ ]+priority[ ]+(edit)([ ]+[^ ]+)?([ ]+[^ ]+)?[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.backup.fn"
match="^[ ]*dinv[ ]+backup[ ]+(on|off|list|create|delete|restore|auto)[ ]*([^ ]+)?[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.reload.fn"
match="^[ ]*dinv[ ]+reload[ ]*$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
<alias
script="inv.cli.debug.fn"
match="^[ ]*dinv[ ]+debug(.*)$"
enabled="y"
regexp="y"
send_to="12"
sequence="100"
>
</alias>
</aliases>
<!-- Script -->
<script>
<![CDATA[
----------------------------------------------------------------------------------------------------
-- Plugin Information
----------------------------------------------------------------------------------------------------
pluginNameCmd = "dinv"
pluginNameAbbr = "DINV"
pluginId = "88c86ea252fc1918556df9fe"
-- Get an absolute path to the plugin's state directory. We don't want to use just the relative
-- path in GetInfo(85) in case someone's current directory isn't where we'd expect. We no longer
-- care where the plugin file is installed because we will use mush's state directory for our
-- saved data regardless of where the plugin xml file lives.
pluginStatePath = GetInfo(56) .. GetInfo(85) .. pluginNameCmd .. "-" .. pluginId
-- Some versions of windows don't like if a path has something like "foo\.\bar" in it. This
-- strips out any redundant ".\" in the path if it exists.
pluginStatePath = string.gsub(pluginStatePath, "\\.\\", "\\")
--print("Plugin state: " .. pluginStatePath)
----------------------------------------------------------------------------------------------------
-- External dependencies
----------------------------------------------------------------------------------------------------
require "wait"
require "check"
require "serialize"
require "tprint"
require "gmcphelper"
require "async"
-- Use the absolute path to the file in case a user's current directory isn't what we'd expect
dofile(GetInfo(56) .. GetInfo(60) .. "aardwolf_colors.lua")
----------------------------------------------------------------------------------------------------
-- Mushclient plugin callbacks
----------------------------------------------------------------------------------------------------
function OnPluginInstall()
dbot.debug("OnPluginInstall!")
local retval = inv.reload(drlDoSaveState)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("OnPluginInstall: Failed to load/reload plugin: " .. dbot.retval.getString(retval))
end -- if
end -- OnPluginInstall
-- You might think that this would be a great place to de-init the plugin. Unfortunately,
-- MUSHclient calls OnPluginSaveState AFTER calling OnPluginClose. This means that the state
-- won't save properly if we fully de-init our data and clean things up. Bummer. Instead, we
-- currently use a somewhat convoluted scheme duplicating code in OnPluginDisconnect and in
-- the inv.reload() function called by OnPluginInstall.
--
-- I'm leaving this here as a placeholder for now. Maybe we could use it for something in the
-- future.
function OnPluginClose()
dbot.debug("OnPluginClose")
end -- OnPluginClose
-- There currently isn't a need for this callback in our plugin. This is just a placeholder for now.
function OnPluginWorldSave()
dbot.debug("OnPluginWorldSave")
end -- OnPluginWorldSave
function OnPluginSaveState()
local retval
dbot.debug("OnPluginSaveState!")
-- We can't save state if GMCP isn't initialized because we don't know which character's state
-- we need to save or where to save it
if (dbot.gmcp.isInitialized == false) then
return
end -- if
-- We also can't save state if we aren't initialized yet
if (not dbot.init.initializedActive) then
dbot.debug("OnPluginSaveState: Skipping save because plugin is not yet initialized")
local charState = dbot.gmcp.getState() or "Uninitialized"
if (charState ~= dbot.stateActive) then
dbot.info("You must be in the active state to save your data but your state is \"@C" ..
dbot.gmcp.getStateString(charState) .. "@W\"")
end -- if
return
end -- if
-- The inv and dbot modules always call the appropriate *.save() function as soon as possible
-- when saved state needs to be updated. However, it doesn't hurt to be a bit paranoid and
-- allow the user to explicitly save plugin state here.
-- Save state of all inventory modules
for module in inv.modules:gmatch("%S+") do
if (inv[module].save ~= nil) then
retval = inv[module].save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("OnPluginSaveState: Failed to save state for inv." .. module .. " module: " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- for
-- Save state of all dbot modules
for module in dbot.modules:gmatch("%S+") do
if (dbot[module].save ~= nil) then
retval = dbot[module].save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("OnPluginSaveState: Failed to save state for dbot." .. module .. " module: " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- for
-- Update any automatic backups for the saved plugin state
retval = dbot.backup.current()
if (retval ~= DRL_RET_SUCCESS) and (retval ~= DRL_RET_BUSY) then
dbot.warn("OnPluginSaveState: Failed to backup plugin state: " .. dbot.retval.getString(retval))
end -- if
end -- OnPluginSaveState
function OnPluginConnect()
dbot.debug("OnPluginConnect!")
-- If we aren't initialized yet, initialize everything...Yes, this technically isn't "install time"
-- but it is close enough for our purposes. The important thing is that we don't try to init any
-- "at active" things such as loading saved state. We don't know which char's state to load until
-- the user logs in and GMCP can give us the username.
if (inv.init.initializedInstall == false) then
local retval = inv.init.atInstall()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("OnPluginConnect: Failed to init \"at install\" inventory code: " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- OnPluginConnect
function OnPluginDisconnect()
dbot.debug("OnPluginDisconnect!")
local retval = inv.fini(drlDoSaveState)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("OnPluginDisconnect: Failed to de-init the inventory module: " .. dbot.retval.getString(retval))
end -- if
end -- OnPluginDisconnect
function OnPluginEnable()
dbot.debug("OnPluginEnable!")
end -- OnPluginEnable
function OnPluginDisable()
dbot.debug("OnPluginDisable!")
end -- OnPluginDisable
function OnPluginTelnetOption(msg)
if (msg == string.char(100, 1)) then
dbot.debug("Player is at login screen")
elseif (msg == string.char(100, 2)) then
dbot.debug("Player is at MOTD or login sequence")
elseif (msg == string.char(100, 3)) then
dbot.debug("Player is fully active!")
-- We already have code to do the atActive init when we detect that GMCP is alive. However, it
-- is also convenient to duplicate it here so that we can attempt to init the moment we come out
-- of AFK. This makes the init a little more reponsive than waiting for GMCP. This could happen
-- if the user is AFK when they log in and then exits AFK at some indeterminate time in the future.
if (dbot.gmcp.isInitialized) and (not inv.init.initializedActive) then
local retval = inv.init.atActive()
if (retval ~= DRL_RET_SUCCESS) and (retval ~= DRL_RET_BUSY) then
dbot.warn("OnPluginTelnetOption: Failed to init \"at active\" inventory modules: " ..
dbot.retval.getString(retval))
end -- if
end -- if
elseif (msg == string.char(100, 4)) then
dbot.debug("Player is AFK!")
-- We keep track of the time between when an "afk" command is sent to the mud and when we actually
-- go into afk mode. This is helpful because there is a small window where we want to hold off
-- on starting an atomic operation if we will shortly be in afk mode. Once we know we are in AFK
-- mode, we no longer have a "pending" state.
dbot.execute.afkIsPending = false
-- If we are AFK, we may as well make a backup. We don't have anything better to do... We don't
-- want to do this if someone quickly toggles AFK though so we wait a few seconds and verify that
-- we are still AFK before we kick off the backup.
check (DoAfterSpecial(5, "dbot.backup.atAFK()", sendto.script))
end -- if
end -- OnPluginTelnetOption
gmcpPluginId = "3e7dedbe37e44942dd46d264"
function OnPluginBroadcast(msg, pluginId, pluginName, text)
local retval = DRL_RET_SUCCESS
-- We want to wait to init things until plugins are loaded and the system is stable. This is
-- a little ugly, but we wait until the GMCP plugin broadcasts something. That seems as likely
-- a time as any for it to be safe for us to init things. We manually kick GMCP by requesting the
-- char.base info in the OnPluginInstall() function just to be sure that we are initialized.
if (pluginId == gmcpPluginId) then
dbot.debug("OnPluginBroadcast: pluginName = \"" .. (pluginName or "main script") .. "\", text = \"" ..
text .. "\"")
-- Once we know GMCP is alive, we allow accesses to it
if (dbot.gmcp.isInitialized == false) and (text == "char.base") then
dbot.debug("GMCP base broadcast detected: GMCP is initialized!")
dbot.gmcp.isInitialized = true
if dbot.gmcp.stateIsActive() then
retval = inv.init.atActive()
if (retval ~= DRL_RET_SUCCESS) and (retval ~= DRL_RET_BUSY) then
dbot.warn("OnPluginBroadcast: Failed to init \"at active\" inventory modules: " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- if
end -- if
end -- OnPluginBroadcast
-- We monitor traffic to the mud in OnPluginSend() in order to scan for a few specific commands.
-- If we find one of the special commands that can impact safe execution calls, then we set an
-- appropriate "pending" flag. For example, if the plugin sees that an "afk" message is in
-- transit to the mud, we want to let the safe execution framework know that we'll be in the
-- AFK state shortly.
function setPending(msg)
if (string.lower(msg) == "afk") and (dbot.gmcp.getState() ~= dbot.stateAFK) then
dbot.execute.afkIsPending = true
elseif (string.lower(msg) == "quit") then
dbot.execute.quitIsPending = true
-- Add a trigger to clear the quitIsPending flag if the quit is cancelled
AddTriggerEx("drlQuitCancelConfirmationTrigger",
"^These items will be lost if you quit. Use .quit quit. if you are sure.$",
"dbot.execute.quitIsPending = false",
drlTriggerFlagsBaseline + trigger_flag.OneShot,
custom_colour.NoChange, 0, "", "", sendto.script, 0)
elseif (string.lower(msg) == "note write") then
dbot.execute.noteIsPending = true
-- Add a trigger to clear the noteIsPending flag once the note starts
AddTriggerEx("drlNoteWriteConfirmationTrigger",
"^(" ..
"You are now creating a new post in the .* forum.|" ..
"You cannot post notes in this forum." ..
")$",
"dbot.execute.noteIsPending = false",
drlTriggerFlagsBaseline + trigger_flag.OneShot,
custom_colour.NoChange, 0, "", "", sendto.script, 0)
end -- if
end -- setPending
function OnPluginSend(msg)
local baseCommand
-- Can this ever happen? I guess it doesn't hurt to be paranoid...
if (msg == nil) then
return false
end -- if
--dbot.note("@MOnPluginSend@W: Detected request to send \"@G" .. msg .. "@W\"")
-- If the command has a special "bypass" prefix appended to it, we strip off the prefix
-- and send it to the mud server immediately no questions asked. In this case, we return
-- false because we don't want to send something with the prefix to the mud server. The
-- server wouldn't know what to do with that.
_, _, baseCommand = string.find(msg, dbot.execute.bypassPrefix .. "(.*)")
if (baseCommand ~= nil) then
--dbot.note("@mBypass command = @W\"@G" .. (baseCommand or "nil") .. "@W\"")
-- It is helpful in some scenarios for us to know that something special is pending. For
-- example, we might be sending a command to the mud to go AFK, or quit, or write a note.
setPending(baseCommand)
check (SendNoEcho(baseCommand))
return false
end -- if
-- If we are at this point, then we know that we don't have a "bypass" command. This means
-- that we should either queue up the command if we are in a state where we are delaying
-- command execution, or we should allow the command to go through as normal.
if dbot.execute.doDelayCommands then
dbot.execute.queue.pushFast(msg)
-- We just added a new command to the command queue. If we don't already have a
-- co-routine processing commands on that queue, start one now.
if (not dbot.execute.queue.isDequeueRunning) then
dbot.execute.queue.isDequeueRunning = true
wait.make(dbot.execute.queue.dequeueCR)
end -- if
return false -- Don't send the command right now
else
-- It is helpful in some scenarios for us to know that something special is pending. For
-- example, we might be sending a command to the mud to go AFK, or quit, or write a note.
setPending(msg)
return true -- Allow the command to go to the mud server
end -- if
end -- OnPluginSend
----------------------------------------------------------------------------------------------------
-- Top-level inventory functions
--
-- Functions
-- inv.init.atInstall() -- add triggers, timers, etc.
-- inv.init.atActive() -- kick off inv.init.atActiveCR co-routine
-- inv.init.atActiveCR() -- load tables, etc.
-- inv.fini(doSaveState) -- remove triggers, save tables, etc.
-- inv.reset(endTag) -- Reset some or all of the inventory components / modules
-- inv.reload(doSaveState) -- De-init and re-init everything
----------------------------------------------------------------------------------------------------
inv = {}
inv.init = {}
inv.modules = "config items cache priority set statBonus consume snapshot tags"
inv.inSafeMode = false
inv.init.initializedInstall = false
inv.init.initializedActive = false
inv.init.activePending = false
drlDoSaveState = true
drlDoNotSaveState = false
function inv.init.atInstall()
local retval = DRL_RET_SUCCESS
-- Initialize all of the "at install" dbot modules (this is a common code framework for multiple plugins)
if (dbot.init.initializedInstall == false) then
retval = dbot.init.atInstall()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.init.atInstall: Failed to initialize \"at install\" dbot modules: " ..
dbot.retval.getString(retval))
else
dbot.init.initializedInstall = true
end -- if
end -- if
if inv.init.initializedInstall then
dbot.note("Skipping inv.init.atInstall request: it is already initialized")
return retval
end -- if
-- We aren't running the discovery or identification processes yet
inv.state = invStateIdle
-- Loop through all of the "at install" inv modules and call their init functions
retval = DRL_RET_SUCCESS
if (inv.init.initializedInstall == false) then
for module in inv.modules:gmatch("%S+") do
if (inv[module].init.atInstall ~= nil) then
local initVal = inv[module].init.atInstall()
if (initVal ~= DRL_RET_SUCCESS) then
dbot.warn("inv.init.atInstall: Failed to initialize \"at install\" inv." .. module ..
" module: " .. dbot.retval.getString(initVal))
retval = initVal
else
dbot.debug("Initialized \"at install\" module inv." .. module)
end -- if
end -- if
end -- for
if (retval == DRL_RET_SUCCESS) then
inv.init.initializedInstall = true
end -- if
end -- if
-- We need access to GMCP in order to determine our state in some circumstances. If we start
-- mush from scratch, then we can determine our state via OnPluginTelnetOption. Easy Peasy.
-- However, if we manually reinstall the plugin, then our plugin may have missed the output from
-- OnPluginTelnetOption and we don't know what our state is. We can get that from GMCP, but we
-- don't know if GMCP is available yet and querying the gmcp plugin will crash if it's not in
-- an initialized state. So...what do we do? We wait for OnPluginBroadcast to detect that GMCP
-- is up and running. We'd rather not wait too long to detect this though so we nudge things
-- along by asking GMCP to send out the char data. We can detect this broadcast and know that
-- GMCP is alive when the broadcast arrives.
Send_GMCP_Packet("request char")
-- Return success or the most recently encountered init error
return retval
end -- inv.init.atInstall
-- This is only called when we know we are in the active state. Many of the actions we take
-- at the "active" state involve waiting for results so we use a co-routine to handle all of
-- the "at active" operations.
function inv.init.atActive()
local retval = DRL_RET_SUCCESS
if (not inv.init.activePending) then
inv.init.activePending = true
wait.make(inv.init.atActiveCR)
else
dbot.debug("inv.init.atActive: Another initialization is in progress")
retval = DRL_RET_BUSY
end -- if
return retval
end -- inv.init.atActive
function inv.init.atActiveCR()
local retval = DRL_RET_SUCCESS
if (dbot.gmcp.isInitialized == false) then
dbot.error("inv.init.atActiveCR: GMCP is not initialized when we are active!?!")
inv.init.activePending = false
return DRL_RET_INTERNAL_ERROR
end -- if
-- Initialize all of the "at active" dbot modules (this is a common code framework for multiple plugins)
if (dbot.init.initializedActive == false) then
retval = dbot.init.atActive()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.init.atActiveCR: Failed to initialize \"at active\" dbot modules: " ..
dbot.retval.getString(retval))
else
dbot.debug("Initialized dbot \"at active\" modules")
dbot.init.initializedActive = true
end -- if
end -- if
-- Initialize all of the "at active" inventory modules
retval = DRL_RET_SUCCESS
if (inv.init.initializedActive == false) then
for module in inv.modules:gmatch("%S+") do
if (inv[module].init.atActive ~= nil) then
local initVal = inv[module].init.atActive()
if (initVal ~= DRL_RET_SUCCESS) then
dbot.warn("inv.init.atActiveCR: Failed to initialize \"at active\" inv." .. module ..
" module: " .. dbot.retval.getString(initVal))
retval = initVal
else
dbot.debug("Initialized \"at active\" module inv." .. module)
end -- if
end -- if
end -- for
if (retval == DRL_RET_SUCCESS) then
inv.init.initializedActive = true
local fullVer = string.format("%d.%04d", inv.version.pluginMajor, inv.version.pluginMinor)
dbot.info("Plugin version " .. fullVer .. " is fully initialized")
-- Kick off an immediate full inventory refresh so that we have an accurate view of what
-- the user has. They may have logged in without using the plugin and moved things around
-- or added and removed items. Ideally, we would do this at every login. However, some
-- users may have refreshes disabled because they want to handle things manually. That's
-- fine too. If refreshes are disabled (their period is 0 minutes) then we skip this.
if (inv.items.refreshGetPeriods() > 0) then
dbot.info("Running initial full scan to check if your inventory was modified outside of this plugin")
retval = inv.items.refresh(0, invItemsRefreshLocAll, nil, nil)
if (retval ~= DRL_RET_SUCCESS) and (retval ~= DRL_RET_UNINITIALIZED) then
dbot.info("Initial full inventory rescan could not complete: " .. dbot.retval.getString(retval))
dbot.info("Please run \"@Gdinv refresh all@W\" to ensure the plugin knows that you didn't do " ..
"something evil like logging in via telnet to move items around :P")
end -- if
end -- if
end -- if
end -- if
-- Return success or the most recently encountered init error
inv.init.activePending = false
return retval
end -- inv.init.atActiveCR
function inv.fini(doSaveState)
local retval = DRL_RET_SUCCESS
-- Stop automatic refreshes as we de-init things
inv.state = invStateHalted
if dbot.gmcp.isInitialized then
-- Update any automatic backups for the saved plugin state
retval = dbot.backup.current()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.fini: Failed to backup plugin state: " .. dbot.retval.getString(retval))
end -- if
-- Loop through all of the inv modules and call their de-init functions
for module in inv.modules:gmatch("%S+") do
local initVal = inv[module].fini(doSaveState)
if (initVal ~= DRL_RET_SUCCESS) then
dbot.warn("inv.fini: Failed to de-initialize inv." .. module .. " module: " ..
dbot.retval.getString(initVal))
retval = initVal
else
dbot.debug("De-initialized inv module \"" .. module .. "\"")
end -- if
end -- for
-- De-init all of the dbot modules (common framework code for multiple plugins)
retval = dbot.fini(doSaveState)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("init.fini: De-initialization of dbot module failed: " .. dbot.retval.getString(retval))
end -- if
end -- if
-- This indicates that we are now uninitialized
inv.init.initializedInstall = false
inv.init.initializedActive = false
inv.init.activePending = false
inv.state = nil
-- Return success or the most recently encountered de-init error
return retval
end -- inv.fini
-- Takes a string containing module names to reset (e.g., "items config portal cache")
function inv.reset(moduleNames, endTag)
local retval = DRL_RET_SUCCESS
local numModulesReset = 0
if (moduleNames == nil) or (modulesNames == "") then
dbot.warn("inv.reset: missing module names to reset")
end -- if
if (moduleNames == "all") then
moduleNames = inv.modules
end -- if
-- Loop through all of the module names in the list and attempt to reset each module with a
-- corresponding name
for moduleName in moduleNames:gmatch("%S+") do
if dbot.isWordInString(moduleName, inv.modules) then
dbot.note("Resetting module \"@C" .. moduleName .. "@W\"")
local currentRetval = inv[moduleName].reset()
-- Remember the most recent error so that we can return it
if (currentRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.reset: Failed to reset module \"@C" .. moduleName .. "@W\": " ..
dbot.retval.getString(currentRetval))
retval = currentRetval
else
numModulesReset = numModulesReset + 1
end -- if
else
dbot.warn("inv.reset: Attempted to reset invalid module name \"@C" .. moduleName .. "@W\"")
retval = DRL_RET_MISSING_ENTRY
end -- if
end -- for
local suffix = ""
if (numModulesReset ~= 1) then
suffix = "s"
end -- if
dbot.info("Successfully reset " .. numModulesReset .. " module" .. suffix)
return inv.tags.stop(invTagsReset, endTag, retval)
end -- inv.reset
function inv.reload(doSaveState)
local retval = DRL_RET_SUCCESS
-- De-init the plugin if it is already initialized. This could happen if we restore state from
-- a backup and want to re-init everything
if inv.init.initializedInstall then
dbot.note("inv.reload: Reinitializing plugin")
retval = inv.fini(doSaveState)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.reload: Failed to de-initialize inventory module: " ..
dbot.retval.getString(retval))
end -- if
end -- if
-- Init everything that we can at "install time". This typically would entail adding triggers,
-- timers, and aliases. We don't want to try loading saved state here though (that's at "active time")
-- because we need GMCP initialized to get the user's name in order to get the correct user's state.
retval = inv.init.atInstall()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.reload: Failed to init \"at install\" inventory code: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.reload
----------------------------------------------------------------------------------------------------
-- Versions of the plugin and plugin components
--
-- Functions:
-- inv.version.get()
-- inv.version.display()
--
-- Data:
-- inv.version.table
----------------------------------------------------------------------------------------------------
inv.version = {}
inv.version.full = GetPluginInfo(GetPluginID(), 19)
inv.version.pluginMajor = math.floor(inv.version.full)
inv.version.pluginMinor = tonumber(string.format("%.4f", (inv.version.full - inv.version.pluginMajor) * 10000))
inv.version.table = { pluginVer = { major = inv.version.pluginMajor, minor = inv.version.pluginMinor },
tableFormat = { major = 0, minor = 1 },
cacheFormat = { major = 0, minor = 1 },
consumeFormat = { major = 0, minor = 1 },
priorityFormat = { major = 0, minor = 1 },
setFormat = { major = 0, minor = 1 },
snapshotFormat = { major = 0, minor = 1 }
}
function inv.version.get()
return inv.version.table
end -- inv.version.get
function inv.version.display()
dbot.print("\n @y" .. pluginNameAbbr .. " Aardwolf Plugin\n" ..
"-------------------------@w")
dbot.print("@WPlugin Version: @G" ..
string.format("%01d", inv.version.table.pluginVer.major) .. "." ..
string.format("%04d", inv.version.table.pluginVer.minor) .. "@w")
dbot.print("")
dbot.print("@WInv. Table Format: @G" ..
inv.version.table.tableFormat.major .. "." ..
inv.version.table.tableFormat.minor .. "@w")
dbot.print("@WInv. Cache Format: @G" ..
inv.version.table.cacheFormat.major .. "." ..
inv.version.table.cacheFormat.minor .. "@w")
dbot.print("@WConsumable Format: @G" ..
inv.version.table.consumeFormat.major .. "." ..
inv.version.table.consumeFormat.minor .. "@w")
dbot.print("@WPriorities Format: @G" ..
inv.version.table.priorityFormat.major .. "." ..
inv.version.table.priorityFormat.minor .. "@w")
dbot.print("@WEquip Set Format: @G" ..
inv.version.table.setFormat.major .. "." ..
inv.version.table.setFormat.minor .. "@w")
dbot.print("@WSnapshot Format: @G" ..
inv.version.table.snapshotFormat.major .. "." ..
inv.version.table.snapshotFormat.minor .. "@w")
dbot.print("")
return DRL_RET_SUCCESS
end -- inv.version.display
----------------------------------------------------------------------------------------------------
-- Versions of the plugin and plugin components
--
-- Functions:
-- inv.config.init.atActive()
-- inv.config.fini(doSaveState)
--
-- inv.config.save()
-- inv.config.load()
-- inv.config.reset()
-- inv.config.new()
--
-- Data:
-- inv.config.table
-- inv.config.stateName -- name for file holding state in persistent storage
----------------------------------------------------------------------------------------------------
inv.config = {}
inv.config.init = {}
inv.config.table = {}
inv.config.stateName = "inv-config.state"
function inv.config.init.atActive()
local retval = DRL_RET_SUCCESS
retval = inv.config.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.config.init.atActive: failed to load config data from storage: " ..
dbot.retval.getString(retval))
end -- if
-- It's possible that we previously disabled the prompt and could not re-enable it. For example,
-- maybe the user closed mushclient in the middle of a refresh. In that case, we wouldn't have the
-- opportunity to turn the prompt back on. As a result, we keep track of the user's prompt state
-- and put it back to the last known value here if the current state doesn't match what we expect.
if (inv.config.table.isPromptEnabled ~= nil) and
(inv.config.table.isPromptEnabled ~= dbot.prompt.isEnabled) then
dbot.info("Prompt state does not match expected state: toggling prompt")
-- We don't use an execute.safe call here because we haven't finished initializing and the safe
-- execution framework requires us to be fully initialized.
dbot.execute.fast.command("prompt")
dbot.execute.queue.fence()
end -- if
return retval
end -- inv.config.init.atActive
function inv.config.fini(doSaveState)
local retval = DRL_RET_SUCCESS
-- Save our current data
if (doSaveState) then
retval = inv.config.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.config.fini: Failed to save inv.config module data: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.config.fini
function inv.config.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.config.stateName,
"inv.config.table", inv.config.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.config.save: Failed to save config table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.config.save
function inv.config.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.config.stateName, inv.config.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.config.load: Failed to load table from file \"@R" .. dbot.backup.getCurrentDir() ..
inv.config.stateName .. "@W\": " .. dbot.retval.getString(retval))
end -- if
if (inv.config.table == nil) or (inv.config.table.tableFormat == nil) then
dbot.error("inv.config.load: inventory configuration table format is nil")
return DRL_RET_INTERNAL_ERROR
end -- if
return retval
end -- inv.config.load
function inv.config.reset()
local retval
inv.config.table = inv.config.new()
retval = inv.config.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.config.reset: Failed to save configuration data: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.config.reset
function inv.config.new()
local version = inv.version.get()
return { pluginVer = version.pluginVer,
tableFormat = version.tableFormat,
cacheFormat = version.cacheFormat,
consumeFormat = version.consumeFormat,
priorityFormat = version.priorityFormat,
setFormat = version.setFormat,
snapshotFormat = version.snapshotFormat,
isPromptEnabled = true,
isBackupEnabled = true,
isBuildExecuted = false,
refreshPeriod = 0,
refreshEagerSec = 0
}
end -- inv.config.new
----------------------------------------------------------------------------------------------------
-- Command-Line Interface Parser
--
-- The CLI interface consists of components that each have a base parsing function, a function to
-- display the component's usage, and a function to display examples using the component.
--
-- The components are:
-- Inventory table access: build, refresh, search
-- Item management: get, put, store, keyword, organize
-- Equipment sets: set, snapshot, priority
-- Equipment analysis: analyze, usage, compare, covet
-- Advanced options: backup, notify, forget, reset, cache, tags
-- Using equipment: portal, consume, pass
-- About the plugin: version, help
--
-- Functions
-- inv.cli.fullUsage()
--
-- inv.cli.build.fn(name, line, wildcards)
-- inv.cli.build.usage()
-- inv.cli.build.examples()
-- inv.cli.refresh.fn(name, line, wildcards)
-- inv.cli.refresh.usage()
-- inv.cli.refresh.examples()
-- inv.cli.search.fn(name, line, wildcards)
-- inv.cli.search.usage()
-- inv.cli.search.examples()
--
-- inv.cli.get.fn(name, line, wildcards)
-- inv.cli.get.usage()
-- inv.cli.get.examples()
-- inv.cli.put.fn(name, line, wildcards)
-- inv.cli.put.usage()
-- inv.cli.put.examples()
-- inv.cli.store.fn(name, line, wildcards)
-- inv.cli.store.usage()
-- inv.cli.store.examples()
-- inv.cli.keyword.fn(name, line, wildcards)
-- inv.cli.keyword.usage()
-- inv.cli.keyword.examples()
--
-- inv.cli.set.fn(name, line, wildcards)
-- inv.cli.set.usage()
-- inv.cli.set.examples()
-- inv.cli.priority.fn(name, line, wildcards)
-- inv.cli.priority.fn2(name, line, wildcards)
-- inv.cli.priority.usage()
-- inv.cli.priority.examples()
-- inv.cli.snapshot.fn(name, line, wildcards)
-- inv.cli.snapshot.usage()
-- inv.cli.snapshot.examples()
--
-- inv.cli.analyze.fn(name, line, wildcards)
-- inv.cli.analyze.fn2(name, line, wildcards)
-- inv.cli.analyze.usage()
-- inv.cli.analyze.examples()
-- inv.cli.usage.fn(name, line, wildcards)
-- inv.cli.usage.usage()
-- inv.cli.usage.examples()
-- inv.cli.compare.fn(name, line, wildcards)
-- inv.cli.compare.usage()
-- inv.cli.compare.examples()
-- inv.cli.covet.fn(name, line, wildcards)
-- inv.cli.covet.usage()
-- inv.cli.covet.examples()
--
-- inv.cli.notify.fn(name, line, wildcards)
-- inv.cli.notify.usage()
-- inv.cli.notify.examples()
-- inv.cli.forget.fn(name, line, wildcards)
-- inv.cli.forget.usage()
-- inv.cli.forget.examples()
-- inv.cli.reset.fn(name, line, wildcards)
-- inv.cli.reset.usage()
-- inv.cli.reset.examples()
-- inv.cli.backup.fn(name, line, wildcards)
-- inv.cli.backup.usage()
-- inv.cli.backup.examples()
-- inv.cli.cache.fn(name, line, wildcards)
-- inv.cli.cache.usage()
-- inv.cli.cache.examples()
-- inv.cli.tags.fn(name, line, wildcards)
-- inv.cli.tags.usage()
-- inv.cli.tags.examples()
--
-- inv.cli.portal.fn(name, line, wildcards)
-- inv.cli.portal.usage()
-- inv.cli.portal.examples()
-- inv.cli.consume.fn(name, line, wildcards)
-- inv.cli.consume.usage()
-- inv.cli.consume.examples()
--
-- inv.cli.organize.fn1(name, line, wildcards)
-- inv.cli.organize.fn2(name, line, wildcards)
-- inv.cli.organize.fn3(name, line, wildcards)
-- inv.cli.organize.usage()
-- inv.cli.organize.examples()
--
-- inv.cli.version.fn(name, line, wildcards)
-- inv.cli.version.usage()
-- inv.cli.version.examples()
-- inv.cli.reload.fn(name, line, wildcards)
-- inv.cli.reload.usage()
-- inv.cli.reload.examples()
-- inv.cli.help.fn(name, line, wildcards)
-- inv.cli.help.usage()
-- inv.cli.help.examples()
--
----------------------------------------------------------------------------------------------------
inv.cli = {}
function inv.cli.fullUsage()
dbot.print("@C" .. pluginNameCmd .. " usage:@W Command @GRequired @YOptional@w")
dbot.print("\n@C Inventory table access@w")
inv.cli.build.usage()
inv.cli.refresh.usage()
inv.cli.search.usage()
dbot.print("\n@C Item management@w")
inv.cli.get.usage()
inv.cli.put.usage()
inv.cli.store.usage()
inv.cli.keyword.usage()
inv.cli.organize.usage()
dbot.print("\n@C Equipment sets@w")
inv.cli.set.usage()
inv.cli.snapshot.usage()
inv.cli.priority.usage()
dbot.print("\n@C Equipment analysis@w")
inv.cli.analyze.usage()
inv.cli.usage.usage()
inv.cli.compare.usage()
inv.cli.covet.usage()
dbot.print("\n@C Advanced options@w")
inv.cli.backup.usage()
inv.cli.reset.usage()
inv.cli.forget.usage()
inv.cli.notify.usage()
inv.cli.cache.usage()
inv.cli.tags.usage()
inv.cli.reload.usage()
dbot.print("\n@C Using equipment items@W")
inv.cli.consume.usage()
inv.cli.portal.usage()
inv.cli.pass.usage()
dbot.print("\n@C Plugin info@w")
inv.cli.version.usage()
inv.cli.help.usage()
end -- inv.cli.fullUsage
inv.cli.build = {}
function inv.cli.build.fn(name, line, wildcards)
local confirmation = Trim(wildcards[1] or "")
dbot.debug("inv.cli.build.fn: confirmation = \"" .. confirmation .. "\"")
if (confirmation == "") then
dbot.print(
[[
Building your inventory table can take several minutes and may disturb
other players as you shuffle items through your inventory.
If you truly want to build your inventory table:
1) Go to a room where you won't disturb other people]])
dbot.print(" 2) Enter \"" .. pluginNameCmd .. " build confirm\"")
dbot.print(" 3) Wait for the build to complete or enter \"" .. pluginNameCmd ..
" refresh off\" to halt early\n")
inv.tags.stop(invTagsBuild, line, DRL_RET_UNINITIALIZED)
elseif (confirmation == "confirm") then
dbot.info("Build confirmed: commencing inventory build...")
inv.items.build(line)
else
inv.cli.build.usage()
inv.tags.stop(invTagsBuild, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.build.fn
function inv.cli.build.usage()
dbot.print("@W " .. pluginNameCmd .. " build confirm@w")
end -- inv.cli.build.usage
function inv.cli.build.examples()
dbot.print("@W\nUsage:\n")
inv.cli.build.usage()
dbot.print(
[[@W
The heart of this plugin is an inventory table that tracks information about
all of your items. Before you can use that table, you must first build it.
This is a relatively long process and can take several minutes to complete.
A build requires a fair amount of communication with the mud and latency to
the mud will have a significant impact on the speed of the build.
As an example, my primary character currently is holding 548 items and took
just under 5 minutes to build an inventory table from scratch when I used
my normal internet connection with a ~40 ms latency to the mud. I also used
a VPN to access the mud from various points around the world and found that
a connection with a ~130 ms latency completed the same build in 8 minutes and
a ~440 ms latency connection (thank you New Zealand!) took 18 minutes to
complete a full build. Fortunately, building your inventory table should be
a one-time operation and subsequent changes to your table will be quick and
easy.
Building your inventory table requires the plugin to run the "identify"
operation on all of your items. If an item is in a container, it will first
take the item out of the container before identifying it and putting it back.
Similarly, if you are wearing an item, the plugin will first remove the item,
identify it, and then re-wear it at its original location. All of this will
be transparent to you. However, anyone in the same room as you will see lots
of activity as the plugin shuffles items around. As a result, please be kind
to those around you and find an out-of-the-way room for the build operation.
Don't do this at recall! :)
While the build executes, you are free to do your normal mudding activities.
However, the build will halt if you sleep, go AFK, enter combat, or do
something that puts you at a paging prompt so it's probably easiest if you
sit back and just let the plugin do its thing :)
If you need to stop the build for some reason, simply go to sleep or go AFK.
Either of those modes will halt the build. You can pick up where a partial
build stopped by running "@Gdinv refresh@W".
If you interrupted a build by going to sleep or going AFK, the build will
automatically continue at the next refresh attempt (see "@Gdinv help refresh@W"
for more details.) You can disable automatic refreshes with the command
"@Gdinv refresh off@W" and you can re-enable them with "@Gdinv refresh on@W".
A new installation starts with automatic refreshes disabled by default.
Once you have a completed inventory table available, you probably want to make
a manual backup in case something goes wrong in the future. You can restore
from the backup and avoid the long build process again. If anything in your
inventory has changed since the backup, your next refresh will simply update
it to what you currently have. To make a manual backup named
"@Gmy_first_awesome_backup@W" type "@Gdinv backup create my_first_awesome_backup@W".
See "@Gdinv help backup@W" for more details about creating, viewing, and restoring
backups.
]])
end -- inv.cli.build.examples
inv.cli.refresh = {}
function inv.cli.refresh.fn(name, line, wildcards)
local command = wildcards[1] or ""
local refreshPeriod = tonumber(wildcards[2] or "") or inv.items.timer.refreshMin
local refreshLoc
local retval
dbot.debug("inv.cli.refresh.fn: command=\"" .. command .. "\", period=\"" .. refreshPeriod .. "\"")
if (command == "all") then
refreshLoc = invItemsRefreshLocAll
else
refreshLoc = invItemsRefreshLocDirty
end -- if
if (command == "off") then
retval = inv.items.refreshOff()
dbot.note("Automatic inventory refresh is disabled: run \"@G" .. pluginNameCmd ..
" refresh on@W\" to re-enable it")
inv.tags.stop(invTagsRefresh, line, retval)
elseif (command == "on") then
retval = inv.items.refreshOn(refreshPeriod, 0)
dbot.note("Inventory refresh is enabled")
inv.tags.stop(invTagsRefresh, line, retval)
elseif (command == "eager") then
retval = inv.items.refreshOn(refreshPeriod, inv.items.timer.refreshEagerSec or 0)
dbot.note("Inventory refresh is enabled and uses eager refreshes after acquiring items")
inv.tags.stop(invTagsRefresh, line, retval)
elseif (command == "") or (command == "all") then
if (inv.state == invStatePaused) then
inv.state = invStateIdle
end -- if
local retval = inv.items.refresh(0, refreshLoc, line, nil)
if (retval == DRL_RET_HALTED) then
dbot.note("Run \"" .. pluginNameCmd .. " refresh on\" to re-enable automatic inventory refreshes")
end -- if
else
inv.cli.refresh.usage()
inv.tags.stop(invTagsRefresh, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.refresh.fn
function inv.cli.refresh.usage()
dbot.print("@W " .. pluginNameCmd .. " refresh @Y[on | off | eager | all] <minutes>@w")
end -- inv.cli.refresh.usage
function inv.cli.refresh.examples()
dbot.print("@W\nUsage:\n")
inv.cli.refresh.usage()
dbot.print(
[[@W
If you add a new item to your inventory or remove an item from your inventory, your inventory
table must be informed about the change. A refresh operation is the means through which the
plugin updates your inventory table. When the plugin detects changes to your inventory it will
check if automated refreshes are enabled and, if so, schedule an automated "refresh" to identify
anything that has changed.
A refresh operation may require the plugin to get an item from a container or remove a worn item
in order to identify the item. If the plugin moves (or removes) the item, it will automatically
put the item back when the identification completes. The plugin suppresses mud output related
to this moving and identification so it will appear to happen transparently from the user's
perspective.
There are two types of refreshes: manual and automatic. A manual refresh simply performs a
refresh when the user requests one. An automatic refresh occurs when a timer expires after a
specified period of time. Automatic refreshes are disabled by default on a new installation.
If automatic refreshes are turned on ("@Gdinv refresh on <minutes>@W") then an automatic refresh
runs every N minutes since the previous automatic refresh (if N is not supplied, it will default
to 5 minutes.) If nothing has changed since the last refresh, the refresh simply returns.
If you really *really* like your inventory to always be up-to-date, you should use the "eager"
refresh mode ("@Gdinv refresh eager <minutes>@W"). This is identical to the "refresh on" mode
described above but it will also schedule a refresh to run 5 seconds after an item is added to
your inventory.
The plugin will skip a refresh or halt it early if you go to sleep, go AFK, enter combat, or hit
a paging prompt. In this case, any changes that were missed will be picked up the next time a
refresh is in progress.
You may also execute a refresh that performs a full scan of all worn items, items in your main
inventory, and items in containers. Your first refresh after starting up will be a full scan to
ensure that everything is in the expected place. Otherwise, your inventory table could become
out of sync if you logged in with another client and moved items around. The full scan guarantees
that the plugin knows where everything is.
Examples:
1) Perform a manual refresh
"@Gdinv refresh@W"
2) Disable automatic refreshes
"@Gdinv refresh off@W"
3) Enable automatic refreshes with the default period (5 minutes since the last refresh)
"@Gdinv refresh on@W"
4) Enable automatic refreshes with a 10-minute delay between refreshes
"@Gdinv refresh on 10@W"
5) Enable automatic refreshes with a 7-minute delay between refreshes and an "eager" refresh
a few seconds after a new item is added to your inventory
"@Gdinv refresh eager 7@W"
6) Perform a manual full refresh scan
"@Gdinv refresh all@W"
]])
end -- inv.cli.refresh.examples
inv.cli.search = {}
function inv.cli.search.fn(name, line, wildcards)
local verbosity = wildcards[1] or ""
local query = wildcards[2] or ""
-- Use the "basic" display mode for searches by default
if (verbosity == "") then
verbosity = "basic"
end -- if
dbot.debug("verbosity=\"" .. verbosity .. "\", query=\"" .. query .. "\"")
local retval = inv.items.display(query, verbosity, line)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cli.search.fn: Failed to display search query: " .. dbot.retval.getString(retval))
end -- if
end -- inv.cli.search.fn
function inv.cli.search.usage()
dbot.print("@W " .. pluginNameCmd .. " search @Y[id | full] @G<query>@w")
end -- inv.cli.search.usage
function inv.cli.search.examples()
dbot.print("@W\nUsage:\n")
inv.cli.search.usage()
dbot.print(
[[@W
An inventory table isn't much help if you can't access it! That's where search queries come
into play. A query specifies characteristics about inventory items and returns matches for
all items that match the query. Queries are used in many of the dinv plugin's modes. For
example, the "@Cget@W", "@Cput@W", "@Cstore@W", "@Ckeyword@W", "@Corganize@W", and "@Cusage@W" options all take query
arguments and then get, put, store, etc. whatever items match the query. See the helpfile at
"@Gdinv help query@W" for more details and examples.
A query consists of one or more sets of key-value pairs where the key can be any key listed
when you identify/lore an item. For example, a query could be "@Gtype container keyword box@W"
if you wanted to find everything with a type value of "container" that has a keyword "box".
Queries also support three prefixes that can be prepended onto a normal key: "@Cmin@W", "@Cmax@W",
and "@C~@W" (where "@C~@W" means "not"). To find weapons with a minimum level of 100 that do not have
a vorpal special, you could use this query: "@Gtype weapon minlevel 100 ~specials vorpal@W".
You can also "OR" multiple query clauses together into a larger query using the "@C||@W" operator.
If you want move all of your potions and pills into a container named "2.bag" you could do that
with this command: "@Gdinv put 2.bag type potion || type pill@W".
Most queries are in the form "someKey someValue" but there are a few one-word queries that make
life a bit simpler. If a query is the string "all" it will match everything in your inventory --
including everything you are wearing, everything you are holding in your main inventory, and
everything in your containers. If you use the "worn" query, it will only match items that are
currently equipped. If you use an empty query (i.e., the query is "") then it will match
everything in your inventory that is not currently equipped.
Search queries support both absolute and relative names and locations. If you want to specify
all weapons that have "axe" in their name, use "@Gtype weapon name axe@W". If you want to
specifically target the third axe in your main inventory, use "@Gtype weapon rname 3.axe@W"
(or you could just get by with "@Grname 3.axe@W" and skip the "@Gtype weapon@W" clause.) The use
of the key "rname" instead of "name" means that the search is relative to your main inventory
and you can use the format [number].[name] to target a specific item. Similarly, you can use
"@Grlocation 3.bag@W" to target every item contained by the third bag in your main inventory
(i.e., the third bag is their relative location.)
There are a few "one-off" query modes for convenience. It is so common to search for just a
name that the default is to assume you are searching within an item's name if no other data
is supplied. In other words, "@Gdinv search sunstone@W" will find any item with "sunstone" in
its name. Also, queries will accept "key" instead of "keywords", "loc" instead of "location",
and "rloc" instead of "rlocation". Yeah, I'm lazy sometimes...
Performing a search will display relevant information about the items whose characteristics match
the query. There are three modes of searches: "@Cbasic@W", "@Cid@W", and "@Cfull@W". A basic search displays
just basic information about the items -- surprise! An id search shows everything in the basic
search in addition to the item's unique ID. A full search shows lots of info for each item and is
very verbose.
Examples:
1) Show basic info for all weapons between the levels of 1 to 40
"@Gdinv search type weapon minlevel 1 maxlevel 40@W"
@WLvl Name of Weapon Type Ave Wgt HR DR Dam Type Specials Int Wis Lck Str Dex Con
@W 8@w a @yFlame@rthrower@w @Wexotic @G 4@W 0 @G 10@W @G 18@W Fire none @G 2@W 0 @G 3@W 0 0 0
@W 11@w @YDagger of @RAardwolf@w @Wdagger @G 27@W @G 1@W @G 5@W @G 5@W Cold sharp 0 0 0 0 0 0
@W 20@w @WS@we@Wa@wr@Wi@wn@Wg @wB@Wl@wa@Wz@we @Wwhip @G 30@W @G 3@W @G 2@W @G 2@W Fire flaming @G 1@W @G 4@W @G 7@W @G 1@W 0 0
@W 26@w @bM@Belpomene's @bB@Betrayal@w @Wdagger @G 36@W @G 1@W @G 2@W @G 2@W Pierce sharp 0 0 0 0 @G 2@W 0
@W 40@w @YDagger of @RAardwolf@w @Wdagger @G100@W 0 @G 20@W @G 20@W Fire sharp @G 1@W 0 0 0 0 0
@W 40@w @YDagger of @RAardwolf@w @Wdagger @G100@W @G 10@W @G 5@W @G 5@W Mental sharp 0 0 0 0 0 0
@W
2) Show unique IDs and info for all level 91 ear and neck items
"@Gdinv search id wearable ear level 91 || wearable neck level 91@W"
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 91@w @c:@C:@W:@CSt@cerling @G(1851993170) @Wear 0 @G 12@W @G 4@W 0 @G 3@W 0 0 0 @G 9@W 0 @G 30@W @R -60
@W 91@w Kilhil's Ble @G(907478999) @Wear 0 @G 16@W @G 2@W @G 2@W @G 2@W 0 @G 6@W 0 @G 9@W 0 0 0
@W 91@w @y>@Y}@rPho@Ren@Yix's @G(1584559998) @Wneck @G 2@W @G 14@W @G 1@W @G 4@W @G 1@W @G 2@W @G 1@W @G 2@W @G 9@W 0 0 0
@W 91@w @Dthe @YAmulet @G(1235973081) @Wneck 0 @G 12@W 0 0 @G 3@W @G 4@W 0 0 @G 9@W 0 0 0
@W 91@w @Dthe @YCharm @G(1745132926) @Wneck @G 6@W @G 6@W @G 4@W 0 @G 2@W 0 0 0 @G 9@W 0 0 0
@W
3) Show full info for anything with an anti-evil flag
"@Gdinv search full flag anti-evil@W"
@WLvl Name of Weapon Type Ave Wgt HR DR Dam Type Specials Int Wis Lck Str Dex Con
@W 20@w @WS@we@Wa@wr@Wi@wn@Wg @wB@Wl @G(1743467081) @Wwhip @G 30@W @G 3@W @G 2@W @G 2@W Fire flaming @G 1@W @G 4@W @G 7@W @G 1@W 0 0
@w colorName:"@WS@we@Wa@wr@Wi@wn@Wg @wB@Wl@wa@Wz@we" objectID:1743467081
@w keywords:"searing blaze vengeance"
@w flags:"unique, glow, hum, magic, anti-evil, held, resonated, illuminated, V3"
@w score:309 worth:2690 material:steel foundAt:"Unknown"
@w allphys:0 allmagic:0 slash:0 pierce:0 bash:0 acid:0 poison:0
@w disease:0 cold:0 energy:0 holy:0 electric:0 negative:0 shadow:0
@w air:0 earth:0 fire:0 water:0 light:0 mental:0 sonic:0 magic:0
@w weight:3 ownedBy:""
@w clan:"From Crusaders of the Nameless One" affectMods:""@w
@W
4) Show info on any containers that are wearable on your back
"@Gdinv search type container wearable back@W"
@WLvl Name of Container Type HR DR Int Wis Lck Str Dex Con Wght Cap Hold Hvy #In Wgt%
@W201@w @MP@mandora@w'@ms @R[@GBox@R]@w @GContain @W @G 20@W @G 26@W @G 5@W 0 @G 3@W 0 0 @G 5@W @R 8@W @G1500@W @G 16@W @G 50@W @G 33@W @G 50
@W
5) Show info on any portals leading to the Empire of Talsa
"@Gdinv search type portal leadsTo talsa@W"
@WLvl Name of Portal Type Leads to HR DR Int Wis Lck Str Dex Con
@W 60@w @BIrresistible Calling@w @Wportal The Empire of Tals 0 0 0 0 0 0 0 0
@W100@w @REvil Intentions@w @Wportal The Empire of Tals 0 0 0 0 0 0 0 0
@W150@w @BCosmic Calling@w @Wportal The Empire of Tals 0 0 0 0 0 0 0 0@W
6) Look at sorted lists of your poker cards and aardwords tiles
"@Gdinv search key poker || key aardwords@W"
@WLvl Name of Trash Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 1@w @Y|@C4@Y[@CFour of Air@Y]@C4@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@y4@Y[@yFour of Earth@Y]@y4@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@c4@Y[@cFour of Water@Y]@c4@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@CA@Y[@CAce of Air@Y]@CA@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@cA@Y[@cAce of Water@Y]@cA@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@CD@Y[@CDemon of Air@Y]@CD@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@cE@Y[@cElemental of Water@Y]@cE @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@CM@Y[@CMephit of Air@Y]@CM@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @Y|@cM@Y[@cMephit of Water@Y]@cM@Y|@w @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@w
@WLvl Name of Treasure Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 1@w @RAardWords (TM)@Y - Double @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @RAardWords (TM)@Y - Double @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @RAardWords (TM)@Y - Triple @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @RAardWords (TM)@Y - [B] - S @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @RAardWords (TM)@Y - [E] - S @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @RAardWords (TM)@Y - [H] - S @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @RAardWords (TM)@Y - [P] - S @Whold 0 0 0 0 0 0 0 0 0 0 0 0
@W 1@w @RAardWords (TM)@Y - [W] - S @Whold 0 0 0 0 0 0 0 0 0 0 0 0
7) Display your items that are equipped
"@Gdinv search worn@W"
8) Display EVERYTHING (no I'm not pasting that output here!)
"@Gdinv search all@W"
]])
end -- inv.cli.search.examples
-- This isn't a full CLI module, but the query example helpfile seemed to fit well here
inv.cli.query = {}
function inv.cli.query.examples()
dbot.print(
[[@W
Queries are based on key values found in an item's description when you identify
an item. All fields are visible if you have the identify wish. If you do not have
the identify wish, some fields may only be seen with an identify spell or with the
lore ability.
A query's format includes one or more key-value pairs. Details on this can be
found at the helpfile displayed by the command "@Gdinv help search@W" but let's give a
few more examples here too. You can never have too many examples :)
The plugin supports a few special queries that are not in the "someKey someValue"
format:
@Call@W: Matches everything you have equipped or are carrying
@Cworn@W: Matches all of your worn equipment
@C""@W: The "empty query" matches everything in your inventory that is not equipped
Examples:
1) Use a single key-value pair to find items that are level 42
[key] [value]
"@Gdinv search level 42@W"
2) Use two key-value pairs to find items that match *both* pairs. In this example
we find all weapons with the "aardwolf" keyword (i.e., aard quest weapons). By
default, two key-value pairs next to each other require an item to match the
first pair and the second pair.
[key1] [value1] [key2] [value2]
"@Gdinv search type weapon keyword aardwolf@W"
3) Use the "@C||@W" operator (it means "or") with two key-value pairs to find items
that match *either* pair. To find all wearable finger or wrist items, use the
query shown below.
[key1] [value1] || [key2] [value2]
"@Gdinv search wearable finger || wearable wrist@W"
4) Use the "@Cmin@W" and "@Cmax@W" prefixes. You can prepend "min" or "max" prefixes to
any numeric key (e.g., level, weight, str, etc.) to indicate that you only want
to match items up to a minimum or maximum value. Let's find all wearable head
items between levels 50 to 100.
"min"[key 1] [value1] "max"[key2] [value 2]
"@Gdinv search wearable head minlevel 50 maxlevel 100@W"
5) Things get a little more complicated if we want to use both "and" and "or" clauses
in the same query. The "and" operation (putting two key-value pairs next to each
other) has a higher precedence than "or", as represented by the "@C||@W" symbols.
If we want to find weapons with the "mental" or "pierce" damage types that are at
least level 100, we need to duplicate the "minlevel 100" key-value pair for both
halves of the query. We don't use parentheses to indicate precedence. That would
add more complexity than I'm comfortable with at this point...
[key1] [value1] [key2] [value2] || [key3] [value3] [key2] [value2]
"@Gdinv search damtype mental minlevel 100 || damtype pierce minlevel 100@W"
6) Find all armor that has a weight of at least 10 and at most 20 that does not have
an "anti-evil" flag. The "@C~@W" symbol indicates "not" when it is used as a prefix
for a key in a key-value pair.
"@Gdinv search type armor minweight 10 maxweight 20 ~flag anti-evil@W"
7) Find everything in the container with relative location name "2.bag"
"@Gdinv search rloc 2.bag@W"
8) Match everything you currently have equipped
"@Gdinv search worn@W"
9) Match everything in your inventory that is not equipped
"@Gdinv search@W"
10) Match everything you have equipped or are carrying
"@Gdinv search all@W"
Queries support lots of keys that are found when you identify an item. Here is the
list of currently supported keys:
]])
local sortedStats = {}
for key,statField in pairs(inv.stats) do
table.insert(sortedStats, statField)
end -- for
table.sort(sortedStats, function (entry1, entry2) return entry1.name < entry2.name end)
for _, statField in ipairs(sortedStats) do
dbot.print(string.format("@C%15s@W: %s", statField.name, statField.desc))
end -- for
end -- inv.cli.query.examples
inv.cli.get = {}
function inv.cli.get.fn(name, line, wildcards)
local query = wildcards[1] or ""
dbot.debug("CLI: " .. pluginNameCmd .. " get \"" .. query .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping get request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsGet, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping get request: character's state does not allow actions")
return inv.tags.stop(invTagsGet, line, DRL_RET_NOT_ACTIVE)
end -- if
inv.items.get(query, line)
end -- inv.cli.get.fn
function inv.cli.get.usage()
dbot.print("@W " .. pluginNameCmd .. " get @G<query>@w")
end -- inv.cli.get.usage
function inv.cli.get.examples()
dbot.print("@W\nUsage:\n")
inv.cli.get.usage()
dbot.print(
[[@W
The "get" option works in a similar manner to the normal mud "get" command. It moves
something (or somethings) into your main inventory. The main difference is that you don't
need to specify from where you are getting the item(s). An item may be worn or in a
container or on your keyring and it will still move automagically into your main inventory.
A "get" request takes a query argument as a parameter. See "@Gdinv help search@W" for more
details on how queries work and to see several examples of queries.
Examples:
1) Move all level 42 items into your main inventory
"@Gdinv get level 42@W"
2) Move all weapons with a fire damage type into your main inventory
"@Gdinv get damtype fire@W"
3) Get all potions or pills that are in a container at relative location 4.bag
"@Gdinv get rloc 4.bag type potion || rloc 4.bag type pill@W"
4) Get anything with a key type or a flag indicating it's a key
"@Gdinv get type key || flags iskey@W"
5) Get everything with a custom keyword named "borrowedFromBob" (see "@Gdinv help keyword@W"
for details on how to add custom keywords to items)
"@Gdinv get key borrowedFromBob@W"
6) Get anything that is worn on a finger at level 131
"@Gdinv get wearable finger level 131"
]])
end -- inv.cli.get.examples
inv.cli.put = {}
function inv.cli.put.fn(name, line, wildcards)
local container = wildcards[1] or ""
local query = wildcards[2] or ""
dbot.debug("CLI: " .. pluginNameCmd .. " put \"" .. container .. "\", \"" .. query .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping put request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsPut, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping put request: character's state does not allow actions")
return inv.tags.stop(invTagsPut, line, DRL_RET_NOT_ACTIVE)
end -- if
inv.items.put(container, query, line)
end -- inv.cli.put.fn
function inv.cli.put.usage()
dbot.print("@W " .. pluginNameCmd .. " put @G<container relative name> <query>@w")
end -- inv.cli.put.usage
function inv.cli.put.examples()
dbot.print("@W\nUsage:\n")
inv.cli.put.usage()
dbot.print(
[[@W
The "put" option works in a similar manner to the normal mud "put" command. It moves
something (or somethings) from your main inventory into a container at a relative location.
The main difference is that you don't need to specify from where you are getting the items.
If you "put" an item into a container, it can originate from a worn location, your main
inventory, or a container. Regardless of where it starts, the plugin will move it to the
container that you specify.
A "put" request takes a relative location name for the target container and a query that
specifies which items should move into the container. See "@Gdinv help search@W" for examples
on how queries work and how relative names and locations work.
Examples:
1) Put all aardwolf quest weapons into container 3.bag
"@Gdinv put 3.bag type weapon keyword aardwolf@W"
2) Put all potions and pills into container 2.box
"@Gdinv put 2.box type potion || type pill@W"
3) Put all portals into container 4.case
"@Gdinv put 4.case type portal@W"
4) Put all armor pieces between level 1 and level 100 into container "luggage"
"@Gdinv put luggage type armor minlevel 1 maxlevel 100@W"
]])
end -- inv.cli.put.examples
inv.cli.store = {}
function inv.cli.store.fn(name, line, wildcards)
local query = wildcards[1] or ""
dbot.debug("CLI: " .. pluginNameCmd .. " store \"" .. query .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping store request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsStore, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping store request: character's state does not allow actions")
return inv.tags.stop(invTagsStore, line, DRL_RET_NOT_ACTIVE)
end -- if
inv.items.store(query, line)
end -- inv.cli.store.fn
function inv.cli.store.usage()
dbot.print("@W " .. pluginNameCmd .. " store @G<query>@w")
end -- inv.cli.store.usage
function inv.cli.store.examples()
dbot.print("@W\nUsage:\n")
inv.cli.store.usage()
dbot.print(
[[@W
The dinv plugin remembers the container that was most recently used for each item
in the inventory. This gives you a convenient way to store an item (or items) back
where you got them. This is very similar to "dinv put ..." but you don't need to
specify the target container. Each item will go back to the container from which it
was most recently removed. If an item has never been in a container, "storing" it
will put it in your main inventory.
The query parameter specifies which items will be stored. See the helpfile at
"@Gdinv help search@W" for examples and more details on using queries.
Examples:
1) Store all items that are level 71
"@Gdinv store level 71@W"
2) Store all aardwolf quest items
"@Gdinv store keyword aardwolf@W"
3) Store all portals back into their container(s)
"@Gdinv store type portal@W"
]])
end -- inv.cli.store.examples
inv.cli.keyword = {}
function inv.cli.keyword.fn(name, line, wildcards)
local operation = wildcards[1] or ""
local keyword = wildcards[2] or ""
local query = wildcards[3] or ""
inv.items.keyword(keyword, operation, query, false, line)
end -- inv.cli.keyword.fn
function inv.cli.keyword.usage()
dbot.print("@W " .. pluginNameCmd .. " keyword @G[add | remove] <keyword name> <query>@w")
end -- inv.cli.keyword.usage
function inv.cli.keyword.examples()
dbot.print("@W\nUsage:\n")
inv.cli.keyword.usage()
dbot.print(
[[@W
Wouldn't it be great if you could easily add or remove keywords from an item? Now
you can! Although custom keywords won't be recognized by the mud server, you can use
them with any query used by this plugin. See "@Gdinv help search@W" for examples and
more details about how to use queries.
Examples:
1) Add a "@CborrowedFromBob@W" keyword to everything in the container at relative
location "3.bag". You can then use the items and when you are ready to give them
back, you can put them back with "@Gdinv put 3.bag keyword borrowedFromBob@W".
Nice!
"@Gdinv keyword add borrowedFromBob rloc 3.bag@W"
2) Add "@Cfavorite@W" keyword to a level 80 aardwolf sword.
"@Gdinv keyword add favorite level 80 keyword aardwolf name sword@W"
3) Remove "@Cfavorite@W" keyword from everything in your inventory. Remember that an
empty search query matches everything in your inventory.
"@Gdinv keyword remove favorite@W"
]])
end -- inv.cli.keyword.examples
inv.cli.set = {}
function inv.cli.set.fn(name, line, wildcards)
local command = wildcards[1] or ""
local priority = wildcards[2] or ""
local level = wildcards[3] or ""
if (not inv.init.initializedActive) then
dbot.info("Skipping set request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsSet, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping set request: character's state does not allow actions")
return inv.tags.stop(invTagsSet, line, DRL_RET_NOT_ACTIVE)
end -- if
-- If the user doesn't provide a level, use the current level
level = tonumber(level) or dbot.gmcp.getLevel()
dbot.debug("inv.cli.set.fn: command=\"" .. command .. "\", priority=\"" .. priority ..
"\", level=" .. level)
if (command == "display") then
inv.set.display(priority, level, line)
elseif (command == "wear") then
inv.set.createAndWear(priority, level, inv.set.createIntensity, line)
else
inv.cli.set.usage()
inv.tags.stop(invTagsSet, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.set.fn
function inv.cli.set.usage()
dbot.print("@W " .. pluginNameCmd .. " set @G[display | wear] <priority name> @Y<level>@w")
end -- inv.cli.set.usage()
function inv.cli.set.examples()
dbot.print("@W\nUsage:\n")
inv.cli.set.usage()
dbot.print(
[[@W
This plugin can automatically generate equipment sets based on statistic priorities
defined by the user. This is similar to aardwolf's default "score" value for items.
If you enter "@Gcompare set all@W", you will see aardwolf's default weighting for each
statistic based on your class.
The plugin implements a similar approach, but with many more options. For example, the
plugin's "priority" feature allows you to define statistic weightings for particular levels
or ranges of levels. It also supports weightings for item effects such as dual wielding,
iron grip, sanctuary, or regeneration. You can even indicate how important it is to you to
max out specific stats. Also, the plugin provides controls that are much more fine-grained
than the default aardwolf implementation. See "@Gdinv help priority@W" for more details and
examples using stat priorities.
Once you define a group of priorities, you have the ability to create equipment sets based
on those priorities. The plugin finds the optimal (OK, technically it is near-optimal)
set of items that maximizes your equipment set's score relative to the specified priority.
The plugin accounts for overmaxing stats and at times may use items that superficially
appear to be worse than other items in your inventory. An item that looks "better" may
be contributing points to stats that are already maxed and alternative "lesser" items may
be more valuable when combined with your other equipment.
If you create a set for your current level, the plugin knows how many bonus stats you
have due to your current spellup. It can find the exact combination of equipment relative
to your current state so that you don't overmax stats unnecessarily. If you create one
equipment set while having a normal spellup and a second equipment set after getting a
superhero spellup, chances are high that there would be different equipment in both sets.
However, if you create a set for a level that is either higher or lower than your current
level, then the plugin must make some estimates since it can't know how many stats you would
have due to spells at that level. It starts by guessing what an "average" spellup should
look like at a specific level. The plugin also periodically samples your stats as you
play the game and keeps a running weighted average of spell bonuses for each level. If
you play a style that involves always maintaining an SH spellup, then over time the plugin
will learn to use high estimates for your spell bonuses when it creates a set. Similarly,
if you don't bother to use spellups, then over time the plugin will learn to use lower
spell bonuses that more accurately reflect your playing style.
The set creation algorithm is smart enough to detect if you have the ability to dual wield
either from aard gloves or naturally via the skill and will base the set accordingly. It
also checks weapon weights to find the most optimal combination of weapons if dual wield
is available and it is prioritized.
The key point is that we care about maximizing the total *usable* stats in an equipment
set. Finding pieces that are complementary without wasting points on overmaxed stats is
a process that is well-suited for a plugin -- hence this plugin :)
The "@Cset@W" mode creates the specified set and then either wears the equipment or displays
the results depending on if the "@Cwear@W" or the "@Cdisplay@W" option is specified. An
optional "@Clevel@W" parameter will create the set targeted at a specific level. If the
level is not provided, the plugin will default to creating a set for your current level.
For example, consider a scenario where a user creates a priority designed for a primary psi
with at least one melee class and names that priority "@Cpsi-melee@W" (yes, this is what
I normally use -- psis are awesome if you haven't noticed :)). The following examples
will use this priority.
Examples:
1) Display what equipment set best matches the psi-melee priority for level 20. The
stat summary listed on the last line indicates the cumulative stats for the entire
set. This reflects just the stats provided directly by the equipment and it does not
include any bonuses you may get naturally or via spells. Also, note the long list
of effects provided by equipment in this set (haste, regen, etc.). Each of those
effects is given a weighting in the psi-melee priority table.
"@Gdinv set display psi-melee 20@W"
@WEquipment set: @GLevel 20 @Cpsi-melee
@w
@Y light@W( 16): @GLevel 1@W "a hallowed light"
@Y head@W( 40): @GLevel 1@W "@RAardwolf@Y Helm of True Sight@w"
@Y eyes@W( 8): @GLevel 1@W "@C(@W+@C) @WH@Cowlin@Wg T@Cempes@Wt @C(@W+@C)@w"
@Y lear@W( 8): @GLevel 1@W "@R(@G+@B) @WMagica Elemental @C(@G+@y)@w"
@Y rear@W( 8): @GLevel 1@W "@R(@G+@B) @WMagica Elemental @C(@G+@y)@w"
@Y neck1@W( 8): @GLevel 1@W "@C(@W+@C) @WB@Citin@Wg W@Cind@Ws @C(@W+@C)@w"
@Y neck2@W( 8): @GLevel 1@W "@C(@W+@C) @WB@Citin@Wg W@Cind@Ws @C(@W+@C)@w"
@Y back@W( 8): @GLevel 1@W "@C(@W+@C) @WC@Cyclon@We B@Clas@Wt @C(@W+@C)@w"
@Y medal1@W( 9): @GLevel 1@W "@RA@rcademy @GG@graduation @CM@cedal@w"
@Y medal2@W( 7): @GLevel 1@W "@YV3 @RA@rardwolf @GS@gupporters @CP@cin@w"
@Y medal3@W( 19): @GLevel 1@W "V3 @RO@rrder @GO@gf @CT@che @RF@rirst @GT@gier@w"
@Y torso@W( 17): @GLevel 1@W "@RAardwolf @YBreastplate of Magic Resistance@w"
@Y body@W( 6): @GLevel 1@W "@ga Tr@ye@wnc@gh C@yo@wa@gt@w"
@Y waist@W( 8): @GLevel 1@W "@C(@W+@C) @WS@Ctif@Wf B@Creez@We @C(@W+@C)@w "
@Y arms@W( 8): @GLevel 1@W "@C(@W+@C) @WF@Crost@Wy D@Craf@Wt @C(@W+@C)@w"
@Y lwrist@W( 12): @GLevel 16@W "@m-=< @BClasp @Wof the @RKeeper@m >=-@w"
@Y rwrist@W( 8): @GLevel 15@W "thieves' patch"
@Y hands@W( 30): @GLevel 1@W "@RAardwolf@Y Gloves of Dexterity@w"
@Y lfinger@W( 31): @GLevel 1@W "@RAardwolf@Y Ring of Regeneration@w"
@Y rfinger@W( 31): @GLevel 1@W "@RAardwolf@Y Ring of Regeneration@w"
@Y legs@W( 6): @GLevel 1@W "@C(@W+@C) @WC@Coolin@Wg Z@Cephy@Wr @C(@W+@C)@w"
@Y feet@W( 65): @GLevel 1@W "@RAardwolf @YBoots of Speed@w"
@Y wielded@W( 36): @GLevel 20@W "S@we@Wa@wr@Wi@wn@Wg @wB@Wl@wa@Wz@we"
@Y second@W( 27): @GLevel 8@W "@wa @yFlame@rthrower@w"
@Y float@W(110): @GLevel 1@W "@RAardwolf @YAura of Sanctuary@w"
@Y above@W( 14): @GLevel 1@W "@RAura @Yof @GTrivia@w"
@Y portal@W( 3): @GLevel 5@W "@RA@rura @Ro@rf @Bt@bhe @BS@bage@w"
@Y sleeping@W( 0): @GLevel 1@W "V3 @RTrivia @gSleeping Bag@w"
@w
@WAve Sec HR DR Int Wis Lck Str Dex Con Res HitP Mana Move Effects
@G 30@W @G 4@W @G114@W @G205@W @G 20@W @G 37@W @G 71@W @G 22@W @G 24@W @G 13@W @G103@W @G 405@W @G 235@W @G 385@W haste regeneration sanctuary dualwield@W detectgood detectevil detecthidden detectinvis detectmagic@W
2) Display the psi-melee equipment set for my current level (which was 211 at the
time I ran this example -- 201 + 10 levels as a T1 tier bonus)
"@Gdinv set display psi-melee@W"
@WEquipment set: @GLevel 211 @Cpsi-melee
@w
@Y light@W( 59): @GLevel 200@W "@cShining Aqua Light@w"
@Y head@W( 56): @GLevel 200@W "@R(@YO@R)@YCirclet @Rof@Y Autumn Leaves@R(@YO@R)@w"
@Y eyes@W( 52): @GLevel 201@W "forest vision"
@Y lear@W( 41): @GLevel 200@W "@wa @YS@ymall @RR@ruby @YEar@yring@w"
@Y rear@W( 50): @GLevel 211@W "@m@-@-@YGe@ynie's Magical Ear@Yring@m@-@-@w"
@Y neck1@W( 42): @GLevel 201@W "a protective cloak skinned from a leaf scorpionfish"
@Y neck2@W( 40): @GLevel 201@W "a protective cloak skinned from a salamander cocoon"
@Y back@W( 45): @GLevel 201@W "@MP@mandora@w'@ms @R[@GBox@R]@w"
@Y medal1@W( 11): @GLevel 1@W "@RA@rcademy @GG@graduation @CM@cedal@w"
@Y medal2@W( 12): @GLevel 1@W "@YV3 @RA@rardwolf @GS@gupporters @CP@cin@w"
@Y medal3@W( 23): @GLevel 1@W "V3 @RO@rrder @GO@gf @CT@che @RF@rirst @GT@gier@w"
@Y torso@W( 83): @GLevel 201@W "@RAardwolf @YBreastplate of Magic Resistance@w"
@Y body@W( 49): @GLevel 200@W "-@m=@W*@m)@WA @MP@mure @Ma@mnd @MT@mrue @WHeart@m(@W*@m=@W-@w"
@Y waist@W( 46): @GLevel 200@W "a @YS@ytu@wdd@yed @YL@yea@wth@yer @YB@ye@wl@yt@w"
@Y arms@W(101): @GLevel 211@W "@RAardwolf@Y Bracers of Iron Grip@w"
@Y lwrist@W( 38): @GLevel 200@W "@rCuff @wof @B@-@D{@C*@W}@rSou@Rls@W{@C*@D}@B@-@w"
@Y rwrist@W( 37): @GLevel 200@W "a twig bracelet"
@Y hands@W( 73): @GLevel 211@W "@RAardwolf@Y Gloves of Dexterity@w"
@Y lfinger@W( 44): @GLevel 200@W "@YG@yold @WS@wignet of @CL@cocksley@w"
@Y rfinger@W( 44): @GLevel 200@W "a ring of the Dark Eight@w"
@Y legs@W( 44): @GLevel 200@W "@R(FAKE) @GXeno's @YKnickers @cof @CAwesomeness@w"
@Y feet@W( 52): @GLevel 200@W "@g.o@GO@go.@BDra@Gbani Bo@Blers @MSkorni @g.o@GO@go.@w"
@Y wielded@W(666): @GLevel 211@W "@YAxe of @RAardwolf@w"
@Y second@W(709): @GLevel 211@W "@YDagger of @RAardwolf@w"
@Y float@W( 45): @GLevel 201@W "a @YGolden Halo@w"
@Y above@W( 18): @GLevel 1@W "@RAura @Yof @GTrivia@w"
@Y portal@W( 28): @GLevel 180@W "the @YTiger @Wof @CKai@w"
@Y sleeping@W( 0): @GLevel 1@W "V3 @RTrivia @gSleeping Bag@w"
@w
@WAve Sec HR DR Int Wis Lck Str Dex Con Res HitP Mana Move Effects
@G633@W @G633@W @G501@W @G643@W @G 95@W @G111@W @G138@W @G 73@W @G 42@W @G 33@W @G436@W @G1532@W @G 657@W @R-404@W dualwield irongrip@W
3) I also use an "@Cenchanter@W" priority group to boost int, wis, and luck when I
want to enchant something. To wear the equipment set associated with this priority
I would use the command given below. It automatically removes any currently worn
items that are not in the new set and stores those items in their respective "home"
containers. It then pulls the new items from wherever they are stored and wears
them. Easy peasy.
"@Gdinv set wear enchanter@W"
]])
end -- inv.cli.set.examples
inv.cli.priority = {}
function inv.cli.priority.fn(name, line, wildcards)
local command = Trim(wildcards[1] or "")
local priorityName1 = Trim(wildcards[2] or "")
local priorityName2 = Trim(wildcards[3] or "")
dbot.debug("inv.cli.priority.fn: command=\"" .. command .. "\", name1=\"" .. priorityName1 ..
"\", name2=\"" .. priorityName2 .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping priority request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsPriority, line, DRL_RET_UNINITIALIZED)
end -- if
if (command == "list") then
inv.priority.list(line)
elseif (command == "display") then
inv.priority.display(priorityName1, line)
elseif (command == "compare") then
inv.priority.compare(priorityName1, priorityName2, line)
elseif (command == "create") then
inv.priority.create(priorityName1, line)
elseif (command == "delete") then
inv.priority.delete(priorityName1, line)
elseif (command == "clone") then
inv.priority.clone(priorityName1, priorityName2, line)
elseif (command == "copy") then
inv.priority.copy(priorityName1, line)
elseif (command == "paste") then
inv.priority.paste(priorityName1, line)
else
inv.cli.priority.usage()
return inv.tags.stop(invTagsPriority, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.priority.fn
function inv.cli.priority.fn2(name, line, wildcards)
local command = Trim(wildcards[1] or "")
local priorityName = Trim(wildcards[2] or "")
local editFields = Trim(wildcards[3] or "")
local level = tonumber(wildcards[3] or "")
dbot.debug("inv.cli.priority.fn2: command=\"" .. command .. "\", priority=\"" .. priorityName ..
"\", level=\"" .. (level or "nil") .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping priority request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsPriority, line, DRL_RET_UNINITIALIZED)
end -- if
if (command == "edit") then
local useAllFields
if (editFields == "full") then
useAllFields = true
elseif (editFields == "") then
useAllFields = false
else
inv.cli.priority.usage()
return inv.tags.stop(invTagsPriority, line, DRL_RET_INVALID_PARAM)
end -- if
inv.priority.edit(priorityName, useAllFields, false, line)
elseif (command == "split") then
inv.priority.split(priorityName, level, line)
elseif (command == "join") then
inv.priority.join(priorityName, level, line)
else
inv.cli.priority.usage()
return inv.tags.stop(invTagsPriority, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.priority.fn2
function inv.cli.priority.usage()
dbot.print("@W " .. pluginNameCmd ..
" priority @G[list | display | create | clone | delete | edit | copy | paste | compare] " ..
"@Y<name 1> <name 2>@w")
end -- inv.cli.priority.usage
function inv.cli.priority.examples()
dbot.print("@W\nUsage:\n")
inv.cli.priority.usage()
dbot.print(
[[@W
Aardwolf provides a system to "score" an item relative to how important particular statistics
are you to. Run "@Gcompare set@W" to see the current scoring weighting for your character.
This is a great system and it allows you to customize how important particular statistics are
to you and your particular playing style.
However, this system has several limitations. This plugin addresses those limitations by
giving users the ability to implement one or more customizable "priority" groups. Extensions
to the mud's scoring system include:
1) Entire equipment sets are scored collectively. Knowing how a particular item is scored is
great, but it doesn't show the entire picture. What you really care about is how many stats
or bonuses are available by a combination of your equipment.
2) Stat bonuses are capped at each level. If you only look at individual item scores, it can
be very hard to find an optimal set of equipment that doesn't waste stats that will be over
the max and ignored by the mud. The plugin's priority implementation uses a simulated
annealing algorithm to search through combinations of your equipment to find the optimal (or
near-optimal) set of items that scores highest for your particular priorities.
3) The "optimal" equipment at any given time is highly dependent on how many bonuses a character
has due to spells at that moment. If you get a great spellup, changes are high that you will
want to use different equipment than if you have a normal spellup because the better spellup
may push some stats over the max and you are wasting opportunities to bump up other stats
with different equipment.
4) The plugin's priority implementation allows you to specify your priorities at specific levels
or a range of levels. For example, a primary spellcaster may want to emphasize str and dex
more at lower levels and emphasize int and luck more at higher levels. You might also give
the "haste" effect a high priority at lower levels and a lower priority once you have access
to the haste spell. Your priorities will change relative to your level and this plugin
gives you that opportunity.
5) We provide the ability to prioritize item effects such as sanctuary, haste, or detect invis.
6) We also allow you to specify the importance of the dual wield and irongrip effects given
by aard gloves and bracers.
7) You can prioritize the defensive bonus provided by a shield.
8) You can prioritize the value of maxing a particular stat. For example, if a navigator is
one stat point away from getting access to another bypassed area, that navigator could bump
up the value of maxing that stat.
9) You can also prioritize the damage of primary hand weapons and secondary hand weapons
separately. This gives you a lever to control how important dual wielding is to you.
10) The plugin provides much more fine-grained control of specific stats and resists than what
is found in the default mud's scoring system.
The plugin includes very crude priorities for each aardwolf class that have names matching the
name of the class ("psi", "mage", "warrior", "ranger", "paladin", "thief", "cleric"). These
default priorities match the "score" priorities found by running "@Gcompare set@W" on aard for
a given class. You will almost certainly want to tweak (or massively overhaul!) these, but they
give you a rough idea for a starting point.
NOTE: Editing a priority will invalidate any previous equipment set analysis based on that
priority. See "@Gdinv help analyze@W" for instructions on recreating a set analysis. In most
instances, you will simply run "@Gdinv analyze create <priority name>@W".
Examples:
1) List all existing priorities defined for the plugin
"@Gdinv priority list@W"
2) Clone an existing priority. In this example we make a copy of the default "warrior" priority
and name it "myAwesomeWarrior".
"@Gdinv priority clone warrior myAwesomeWarrior@W"
3) Display the "psi-melee" priority bundled with the plugin. This is intended for a primary
psi with at least one melee class. You may or may not agree with these priorities. That's
why this plugin gives you the ability to tweak things to your heart's content :) This
priority defines 6 different level ranges with different priorities at each range.
"@Gdinv priority display psi-melee@W"
@WPriority: "@Cpsi-melee@W"
@W Field L001- L051- L071- L131- L171- L201-
Name L050 L070 L130 L170 L200 L291
@C
@C str@w 1.00 1.00 0.80@y 0.70 0.70 0.50@W : @cValue of 1 point of the strength stat
@C int@w 0.80 1.00 1.00 1.00 1.00 1.00@W : @cValue of 1 point of the intelligence stat
@C wis@y 0.70@w 0.80 0.90 1.00 1.00 1.00@W : @cValue of 1 point of the wisdom stat
@C dex@w 0.80@y 0.50 0.60 0.50@r 0.40 0.40@W : @cValue of 1 point of the dexterity stat
@C con@r 0.20 0.20 0.40 0.40 0.40 0.25@W : @cValue of 1 point of the constitution stat
@C luck@w 1.00 1.00 1.00 1.00 1.00 1.00@W : @cValue of 1 point of the luck stat
@C dam@w 0.90 0.90 0.85 0.85 0.85 0.80@W : @cValue of 1 point of damroll
@C hit@w 0.85 0.80@y 0.75@w 0.85 0.85 0.80@W : @cValue of 1 point of hitroll
@C avedam@w 1.00 1.00 1.00 1.00 1.00 1.00@W : @cValue of 1 point of primary weapon ave damage
@C offhandDam@r 0.33 0.40@y 0.50 0.60 0.70@w 0.85@W : @cValue of 1 point of offhand weapon ave damage
@C hp@r 0.02 0.01 0.01 0.01 0.01 0.01@W : @cValue of 1 hit point
@C mana@r 0.01 0.01 0.01 0.01 0.01 0.01@W : @cValue of 1 mana point
@C sanctuary@G 50.00 10.00 10.00 10.00 20.00 5.00@W : @cValue placed on the sanctuary effect
@C haste@G 20.00 5.00@g 2.00 2.00 2.00 2.00@W : @cValue placed on the haste effect
@C flying@G 5.00@g 4.00 2.00@w 1.00 1.00 1.00@W : @cValue placed on the flying effect
@C invis@G 10.00 5.00@g 3.00@w 1.00 1.00 1.00@W : @cValue placed on the invisible effect
@Cregeneration@G 5.00 5.00 5.00 5.00 5.00@g 2.00@W : @cValue placed on the regeneration effect
@C detectinvis@g 4.00 4.00 2.00 2.00 2.00 2.00@W : @cValue placed on the detect invis effect
@Cdetecthidden@g 3.00 3.00 2.00 2.00 2.00 2.00@W : @cValue placed on the detect hidden effect
@C detectevil@g 2.00 2.00 2.00 2.00 2.00 2.00@W : @cValue placed on the detect evil effect
@C detectgood@g 2.00 2.00 2.00 2.00 2.00 2.00@W : @cValue placed on the detect good effect
@C dualwield@G 20.00@R 0.00 0.00 0.00 0.00 0.00@W : @cValue of an item's dual wield effect
@C irongrip@g 2.00 3.00@G 20.00 20.00 25.00 30.00@W : @cValue of an item's irongrip effect
@C shield@G 5.00 5.00 10.00 20.00 25.00 40.00@W : @cValue of a shield's damage reduction effect
@C allmagic@r 0.03 0.03 0.05 0.05 0.05 0.05@W : @cValue of 1 point in each magical resist type
@C allphys@r 0.03 0.05 0.10 0.10 0.10 0.10@W : @cValue of 1 point in each physical resist type
@C maxint@R 0.00 0.00 0.00 0.00@G 5.00 20.00@W : @cValue of hitting a level's intelligence ceiling
@C maxwis@R 0.00 0.00 0.00 0.00@G 5.00 20.00@W : @cValue of hitting a level's wisdom ceiling
@C maxluck@R 0.00 0.00 0.00 0.00@G 5.00 20.00@W : @cValue of hitting a level's luck ceiling
@W
4) Create a new priority from scratch. This will pop up a window populated with a single level
range (1 - 291) and values of 0 for each possible priority field. You can break the level
range into multiple ranges by adding additional columns and ensuring that each column's min
and max level fields do not overlap with another column. Once you enter values for each
field, hit the "Done!" button to save your work.
"@Gdinv priority create sillyTankMage@W"
5) Yeah, that tank mage thing was probably too silly. Let's delete it.
"@Gdinv priority delete sillyTankMage@W"
6) Edit an existing priority. This could be something that you cloned, something you made from
scratch, or even a modified default priority. This example does not use the "full" mode and
only lists fields that have a non-zero value. It also does not include descriptions for each
priority field. That makes things more compact and easier to see.
"@Gdinv priority edit psi-no-melee@W"
7) Edit a priority with the "full" mode. This shows everything -- including fields that only
have a priority of zero. It also shows a description for each field. If you use the "full"
mode on a large priority, you may need to resize your edit window to see everything.
"@Gdinv priority edit mage full@W"
8) Use an external editor to modify a priority. You can copy the priority data to the system
clipboard to make it easy to transfer the priority to your own editor.
"@Gdinv priority copy psi-melee@W"
9) Paste priority data from the system clipboard and use that data to either create a new
priority (if it doesn't exist yet) or update an existing priority. This is convenient if you
used an external editor to modify the priority data and you want to import that data back into
the plugin.
"@Gdinv priority paste myThief@W"
10) Copy/paste a priority to make a duplicate. Yes, this is essentially the "@Cclone@W" mode,
but it shows off what you can do with "@Ccopy@W" and "@Cpaste@W".
"@Gdinv priority copy mage@W"
"@Gdinv priority paste myMage@W"
11) Compare the stat differences at all levels for the equipment sets generated by two different
priorities. This will generate a big report that I didn't include here because this helpfile
is already enormous :) If you have not already performed a full analysis of both priorities
you will be prompted to do so before the comparison can execute. The output shown below is
just a snippet. For my equipment at level 11, switching from the "psi" to the "psi-melee"
priority loses my shield and a little hitroll, con, and resists. However, it gives me a lot
more weapon damage and damroll, a little more int, and the regeneration effect (it must use a
ring of regen while the "psi" priority doesn't.)
"@Gdinv priority compare psi psi-melee@W"
@WSwitching from priority "@Gpsi@W" to priority "@Gpsi-melee@W" would result in these changes:
@W Ave Sec HR DR Int Wis Lck Str Dex Con Res HitP Mana Move Effects
@WLevel 11: @G 23@W @G 4@W @R -6@W @G 26@W @G 2@W 0 0 0 0 @R -4@W @R -2@W 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 12: @G 23@W @G 4@W @R -6@W @G 26@W @G 2@W 0 0 0 0 @R -4@W @R -2@W 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 13: @G 23@W @G 4@W @R -6@W @G 26@W @G 2@W 0 0 0 0 @R -4@W @R -2@W 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 14: @G 23@W @G 4@W @R -6@W @G 26@W @G 2@W 0 0 0 0 @R -4@W @R -2@W 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 15: @G 23@W @G 4@W @R -3@W @G 22@W 0 0 0 0 0 @R -4@W @R -2@W 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 16: @G 23@W @G 4@W @R -9@W @G 32@W 0 0 0 0 0 @R -4@W 0 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 17: @G 23@W @G 4@W @R -9@W @G 32@W 0 0 0 0 0 @R -4@W 0 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 18: @G 23@W @G 4@W @R -9@W @G 32@W 0 0 0 0 0 @R -4@W 0 0 0 0 @Gregeneration@W @Rshield@W
@WLevel 19: @G 23@W @G 4@W @R -9@W @G 32@W 0 0 0 0 0 @R -3@W 0 0 0 0 @Gregeneration@W @Rshield@W
]])
end -- inv.cli.priority.examples
inv.cli.snapshot = {}
function inv.cli.snapshot.fn(name, line, wildcards)
local command = wildcards[1] or ""
local snapshotName = wildcards[2] or ""
dbot.debug("inv.cli.snapshot: command=\"" .. command .. "\", name=\"" .. snapshotName .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping snapshot request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsSnapshot, line, DRL_RET_UNINITIALIZED)
end -- if
if (command == "create") then
inv.snapshot.add(snapshotName, line)
elseif (command == "delete") then
inv.snapshot.remove(snapshotName, line)
elseif (command == "list") then
inv.snapshot.list(line)
elseif (command == "display") then
inv.snapshot.display(snapshotName, line)
elseif (command == "wear") then
if dbot.gmcp.statePreventsActions() then
dbot.info("Skipping snapshot wear request: character's state does not allow actions")
return inv.tags.stop(invTagsSnapshot, line, DRL_RET_NOT_ACTIVE)
end -- if
inv.snapshot.wear(snapshotName, line)
else
inv.cli.snapshot.usage()
inv.tags.stop(invTagsSnapshot, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.snapshot.fn
function inv.cli.snapshot.usage()
dbot.print("@W " .. pluginNameCmd ..
" snapshot @G[create | delete | list | display | wear] @Y<snapshot name>")
end -- inv.cli.snapshot.usage
function inv.cli.snapshot.examples()
dbot.print("@W\nUsage:\n")
inv.cli.snapshot.usage()
dbot.print(
[[@W
It's quite easy to take an equipment "snapshot" consisting of everything you are wearing
at the time of the snapshot. You can then easily re-wear the items contained in the
snapshot at a later time. My guess is that most people will want to use automatically
generated equipment sets (see "@Gdinv help set@W") in most cases. However, it could also
be very convenient to explicitly manage what is in a particular set and that is where
snapshots come into play.
If you wear a snapshot, it will remove any currently worn items that are not in the
snapshot and put them away in each item's "home" container (the container where the item
was most recently removed). If a removed item has never been in a container, the plugin
will put it in your main inventory.
Examples!
1) Take a snapshot of what you currently are wearing and name it "myAwesomeSnapshot"
"@Gdinv snapshot create myAwesomeSnapshot@W"
2) List existing snapshots that you have previously taken
"@Gdinv snapshot list@W"
3) Display what equipment is in a particular snapshot. The output shown below is in
the same format that you would see with a regular automatically generated set. The
main difference is that each item's score (the number in parentheses) is 0 here because
we are not scoring the item relative to a priority. We are just showing what items
would be present in the snapshot. See the helpfile at "@Gdinv help set@W" for details.
"@Gdinv snapshot display myAwesomeSnapshot@W"
@WEquipment set: "@CmyAwesomeSnapshot@W"
@w
@Y light@W( 0): @GLevel 200@W "@cShining Aqua Light@w"
@Y head@W( 0): @GLevel 200@W "@R(@YO@R)@YCirclet @Rof@Y Autumn Leaves@R(@YO@R)@w"
@Y eyes@W( 0): @GLevel 200@W "/@D[_]@W-@D[_]@W @DHorn@W-@DRimmed @WGlasses@w"
@Y lear@W( 0): @GLevel 200@W "@wa @YS@ymall @RR@ruby @YEar@yring@w"
@Y rear@W( 0): @GLevel 211@W "@m@-@-@YGe@ynie's Magical Ear@Yring@m@-@-@w"
@Y neck1@W( 0): @GLevel 201@W "a protective cloak skinned from a leaf scorpionfish"
@Y neck2@W( 0): @GLevel 201@W "a protective cloak skinned from a salamander cocoon"
@Y back@W( 0): @GLevel 201@W "@MP@mandora@w'@ms @R[@GBox@R]@w"
@Y medal1@W( 0): @GLevel 1@W "@RA@rcademy @GG@graduation @CM@cedal@w"
@Y medal2@W( 0): @GLevel 1@W "@YV3 @RA@rardwolf @GS@gupporters @CP@cin@w"
@Y medal3@W( 0): @GLevel 1@W "V3 @RO@rrder @GO@gf @CT@che @RF@rirst @GT@gier@w"
@Y torso@W( 0): @GLevel 201@W "@RAardwolf @YBreastplate of Magic Resistance@w"
@Y body@W( 0): @GLevel 200@W "-@m=@W*@m)@WA @MP@mure @Ma@mnd @MT@mrue @WHeart@m(@W*@m=@W-@w"
@Y waist@W( 0): @GLevel 200@W "a @YS@ytu@wdd@yed @YL@yea@wth@yer @YB@ye@wl@yt@w"
@Y arms@W( 0): @GLevel 211@W "@RAardwolf@Y Bracers of Iron Grip@w"
@Y lwrist@W( 0): @GLevel 200@W "@rCuff @wof @B@-@D{@C*@W}@rSou@Rls@W{@C*@D}@B@-@w"
@Y rwrist@W( 0): @GLevel 200@W "a twig bracelet"
@Y hands@W( 0): @GLevel 211@W "@RAardwolf@Y Gloves of Dexterity@w"
@Y lfinger@W( 0): @GLevel 200@W "a ring of the Dark Eight@w"
@Y rfinger@W( 0): @GLevel 200@W "a ring of the Dark Eight@w"
@Y legs@W( 0): @GLevel 200@W "@R(FAKE) @GXeno's @YKnickers @cof @CAwesomeness@w"
@Y feet@W( 0): @GLevel 200@W "@g.o@GO@go.@BDra@Gbani Bo@Blers @MSkorni @g.o@GO@go.@w"
@Y wielded@W( 0): @GLevel 211@W "@YAxe of @RAardwolf@w"
@Y second@W( 0): @GLevel 211@W "@YDagger of @RAardwolf@w"
@Y float@W( 0): @GLevel 201@W "a @YGolden Halo@w"
@Y above@W( 0): @GLevel 1@W "@RAura @Yof @GTrivia@w"
@Y portal@W( 0): @GLevel 180@W "the @YTiger @Wof @CKai@w"
@Y sleeping@W( 0): @GLevel 1@W "V3 @RTrivia @gSleeping Bag@w"
@w
@WAve Sec HR DR Int Wis Lck Str Dex Con Res HitP Mana Move Effects
@G633@W @G633@W @G500@W @G642@W @G121@W @G 99@W @G129@W @G 62@W @G 44@W @G 32@W @G407@W @G1832@W @G 757@W @R-604@W dualwield irongrip @W
4) Wear a snapshot named "my_level_171_set"
"@Gdinv snapshot wear my_level_171_set@W"
5) Delete a snapshot named myEqIs1337
"@Gdinv snapshot delete myEqIs1337@W"
]])
end -- inv.cli.snapshot.examples
inv.cli.analyze = {}
inv.cli.analyzePkg = nil
function inv.cli.analyze.fn(name, line, wildcards)
local command = wildcards[1] or ""
local priorityName = wildcards[2] or ""
local wearableLocs = wildcards[3] or ""
local expandedLocs = ""
local endTag = line
local retval
dbot.debug("inv.cli.analyze.fn: priority=\"" .. priorityName .. "\", loc=\"" .. wearableLocs .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping analyze request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsAnalyze, line, DRL_RET_UNINITIALIZED)
elseif (not dbot.gmcp.stateIsActive()) then
dbot.info("Skipping analyze request: character is not in the active state")
return inv.tags.stop(invTagsAnalyze, line, DRL_RET_NOT_ACTIVE)
end -- if
-- If the user gave a wearable location, check if it is actually valid. We also support the
-- user giving us wearable types (e.g., "neck") in addition to wearable locations (e.g., "neck1 neck2").
if (wearableLocs ~= "") then
for loc in wearableLocs:gmatch("%S+") do
if inv.items.isWearableLoc(loc) then
expandedLocs = expandedLocs .. " " .. loc
elseif inv.items.isWearableType(loc) then
expandedLocs = expandedLocs .. " " .. inv.items.wearableTypeToLocs(loc)
else
dbot.warn("inv.cli.analyze.fn: Invalid wearable type or location \"@R" .. loc .. "@W\"")
dbot.info("Run \"@Gwearables@W\" to see keywords for valid wearable locations.")
return inv.tags.stop(invTagsAnalyze, endTag, DRL_RET_INVALID_PARAM)
end -- if
end -- for
end -- if
if (command == "create") then
if (inv.cli.analyzePkg ~= nil) then
dbot.info("Skipping analysis of priority \"@C" .. priorityName ..
"@W\": another analysis is in progress")
return inv.tags.stop(invTagsAnalyze, endTag, DRL_RET_BUSY)
else
inv.cli.analyzePkg = {}
inv.cli.analyzePkg.priorityName = priorityName
inv.cli.analyzePkg.wearableLocs = expandedLocs
inv.cli.analyzePkg.intensity = inv.set.analyzeIntensity --TODO: let user specify this?
inv.cli.analyzePkg.endTag = endTag
wait.make(inv.cli.analyzeCR)
end -- if
elseif (command == "delete") then
retval = inv.analyze.delete(priorityName)
inv.tags.stop(invTagsAnalyze, endTag, retval)
elseif (command == "display") then
inv.analyze.display(priorityName, expandedLocs, endTag)
else
inv.cli.analyze.usage()
inv.tags.stop(invTagsAnalyze, endTag, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.analyze.fn
function inv.cli.analyze.fn2(name, line, wildcards)
if (not inv.init.initializedActive) then
dbot.info("Skipping analyze request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsAnalyze, line, DRL_RET_UNINITIALIZED)
end -- if
local retval = inv.analyze.list()
inv.tags.stop(invTagsAnalyze, line, retval)
end -- inv.cli.analyze.fn2
function inv.cli.analyzeCR()
local retval
local tierLevel = 10 * dbot.gmcp.getTier()
if (inv.cli.analyzePkg == nil) then
dbot.error("inv.cli.analyzeCR: analyze package is nil!")
return DRL_RET_INTERNAL_ERROR
end -- if
local priorityName = inv.cli.analyzePkg.priorityName or "nil"
local wearableLocs = inv.cli.analyzePkg.wearableLocs
local intensity = inv.cli.analyzePkg.intensity
local endTag = inv.cli.analyzePkg.endTag
dbot.info("Performing equipment analysis for priority \"@C" .. priorityName .. "@W\"...")
dbot.info("This analysis could take up to a minute on a slow system. Be patient!\n")
local resultData = dbot.callback.new()
retval = inv.analyze.sets(priorityName, 1 + tierLevel, resultData, intensity)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cli.analyzeCR: Failed to analyze sets: " .. dbot.retval.getString(retval))
else
-- Wait until the analysis is complete
retval = dbot.callback.wait(resultData, inv.analyze.timeoutThreshold)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cli.analyzeCR: Analysis of set failed: " .. dbot.retval.getString(retval))
end -- if
end -- if
if (retval == DRL_RET_SUCCESS) then
retval = inv.analyze.display(priorityName, wearableLocs, nil)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cli.analyzeCR: analysis display failed: " .. dbot.retval.getString(retval))
end -- if
end -- if
inv.cli.analyzePkg = nil
return inv.tags.stop(invTagsAnalyze, endTag, retval)
end -- inv.cli.analyzeCR
function inv.cli.analyze.usage()
dbot.print("@W " .. pluginNameCmd ..
" analyze @G[list | create | delete | display] <priority name> @Y<positions>@w")
end -- inv.cli.analyze.usage
function inv.cli.analyze.examples()
dbot.print("@W\nUsage:\n")
inv.cli.analyze.usage()
dbot.print(
[[@W
The plugin has the ability to analyze your equipment relative to a priority group. This
analysis identifies the "best" equipment for you at each wearable location at each level.
Creating a full analysis takes roughly 10 seconds (I'm using a 5-year old mac mini running
wine -- your times could be better or worse depending on your hardware.)
You can "@Clist@W" all of the analyses that have been created. If an analysis exists for all
200 levels available to your character, the analysis is shown in @Ggreen@W. If one or more
levels does not yet have an equipment set (e.g., maybe you used "@Gdinv set ...@W" to create one
set but you didn't perform a full analysis) then that analysis name is shown in @Yyellow@W.
Once you "@Ccreate@W" the analysis data, you can "@Cdisplay@W" it quickly without regenerating
all of the data. If you add new equipment to your inventory, you should recreate the analysis
to pick up any changes due to the new equipment. Note that the "@Gdinv set ...@W" options
automatically use all of your equipment when creating sets but we don't proactively create new
sets for all 200 levels unless you explicitly request that via "@Gdinv analyze create [name]@W".
If you edit a priority (e.g., "@Gdinv priority edit [name]@W") then any analysis created with
the previous version of that priority is invalid. As a result, the plugin will erase any stale
analysis when a priority changes and you will need to create it again using the updated version
of the priority.
You have the option of displaying equipment results for one or more specific wearable locations.
For example, you could display results for the arms, head, and neck locations if you don't wish
to see the full analysis.
You can "@Cdelete@W" an existing analysis by providing the name of the analysis.
Examples:
1) Create a set analysis for the psi-melee priority (see "@Gdinv help priority@W" for details.)
This generates a lot of output so I am just including a snippet below for levels 150 - 161.
The "@R<<@W" symbol at the front of each entry indicates an item that is being removed at
that level while the "@G>>@W" symbol indicates that the item is the new replacement. If an
item stays the same from one level to the next, it is not displayed by default.
"@Gdinv analyze create psi-melee@W"
@Y--------------------------------------------@W Level 150 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W121@w @R<<@W a @RSta@Wre of @RAgo@Wny@w @Weyes @G 12@W @G 12@W @G 2@W @G 3@W @G 7@W 0 @G 2@W 0 @G 13@W 0 0 0
@W150@w @G>>@W the @cO@Dcu@cl@Dus@w of the @cK' @Weyes 0 @G 26@W 0 @G 10@W @G 10@W 0 0 0 @G 10@W 0 0 0
@w
@WLvl Name of Treasure Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W100@w @R<<@W @RAardwolf@Y Bracers of I @Warms @G 20@W @G 20@W 0 0 0 0 0 0 0 @G 100@W @G 100@W 0
@W150@w @G>>@W @RAardwolf@Y Bracers of I @Warms @G 30@W @G 30@W 0 0 0 0 0 0 0 @G 150@W @G 150@W 0
@W100@w @R<<@W @RAardwolf@Y Gloves of De @Whands @G 20@W @G 20@W 0 0 0 0 @G 6@W 0 0 @G 100@W @G 100@W @G 100
@W150@w @G>>@W @RAardwolf@Y Gloves of De @Whands @G 30@W @G 30@W 0 0 0 0 @G 6@W 0 0 @G 150@W @G 150@W @G 150
@w
@Y--------------------------------------------@W Level 151 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W131@w @R<<@W (@ySubstance@w of the @gUni @Wback 0 @G 14@W @G 12@W 0 @G 2@W 0 0 0 @G 13@W @G 60@W 0 @R-120
@W151@w @G>>@W a black tunic lined w @Wback @G 5@W @G 21@W @G 12@W @G 4@W @G 2@W @G 1@W @G 1@W 0 0 0 0 0
@w
@Y--------------------------------------------@W Level 160 @Y--------------------------------------------
@w
@WLvl Name of Weapon Type Ave Wgt HR DR Dam Type Specials Int Wis Lck Str Dex Con
@W140@w @R<<@W @YDagger of @RAardwolf@w @Wwielded @G420@W @G 10@W @G 14@W @G 14@W Light sharp 0 0 0 0 0 0
@W160@w @G>>@W @YAxe of @RAardwolf@w @Wwielded @G480@W @G 20@W @G 16@W @G 16@W Slash vorpal 0 0 0 0 0 0
@W140@w @R<<@W @YAxe of @RAardwolf@w @Wsecond @G420@W @G 1@W @G 14@W @G 14@W Pierce flaming 0 0 0 0 0 0
@W160@w @G>>@W @YDagger of @RAardwolf@w @Wsecond @G480@W @G 10@W @G 16@W @G 16@W Pierce sharp 0 0 0 0 0 0
@w
@Y--------------------------------------------@W Level 161 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W131@w @R<<@W @Dthe @YEye @Dof @MHorus Earr @Wlear @G 6@W @G 8@W @G 8@W 0 @G 3@W 0 0 0 @G 12@W 0 0 0
@W161@w @G>>@W @Y(@y%@c*@C=@W- @CR@coar @wof @YV@yictory @Wlear 0 @G 20@W @G 4@W @G 4@W @G 3@W @G 8@W @G 4@W 0 @G 14@W @G 60@W 0 @R-120
@W131@w @R<<@W @Dthe @YEye @Dof @MHorus Earr @Wrear @G 6@W @G 8@W @G 8@W 0 @G 3@W 0 0 @G 1@W @G 12@W 0 0 0
@W161@w @G>>@W @Y(@y%@c*@C=@W- @CR@coar @wof @YV@yictory @Wrear @G 2@W @G 14@W @G 4@W @G 4@W @G 6@W @G 8@W @G 4@W 0 @G 14@W @G 60@W 0 @R-120
@W121@w @R<<@W @gSt@Ge@Wa@Gd@gfa@Gs@Wt@Gn@ges@Gs@w @Wlwrist @G 4@W @G 13@W 0 @G 4@W @G 3@W @G 3@W 0 @G 4@W @G 12@W @G 50@W 0 @R -90
@W161@w @G>>@W @BTeran's @bDeath @BGrip@w @Wlwrist @G 3@W @G 18@W 0 @G 3@W @G 5@W @G 12@W 0 0 0 0 0 0
@W131@w @R<<@W S@wp@Wi@wk@We @wS@Wt@wu@Wd@wd@We@wd @GB@gr@Ga@gc@Ge@gr @Wrwrist 0 @G 12@W @G 9@W 0 @G 3@W 0 @G 2@W 0 @G 13@W @G 25@W 0 @R -50
@W161@w @G>>@W @YM@yana@ccle@Cs of S@capi@yenc@Ye @Wrwrist 0 @G 14@W @G 12@W @G 2@W @G 2@W 0 0 0 @G 16@W 0 @G 60@W @R-120
@W
2) Let's see what the available "optimal" leg items are at each level. Hmm. Looks like I'm
missing some decent L71 legs and I don't have anything between L91 and L181. That helps
me identify places where I could potentially get better equipment. I'm using the "@Cdisplay@W"
mode here because I'm happy with the sets we created in the previous example and I don't
want to duplicate that work again by creating the analysis a second time.
"@Gdinv analyze display psi-melee legs@W"
@Y--------------------------------------------@W Level 11 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 1@w @G>>@W @C(@W+@C) @WC@Coolin@Wg Z@Cephy@Wr @C( @Wlegs @G 5@W @G 2@W 0 0 @G 2@W @G 1@W 0 0 0 0 0 0
@w
@Y--------------------------------------------@W Level 21 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 1@w @R<<@W @C(@W+@C) @WC@Coolin@Wg Z@Cephy@Wr @C( @Wlegs @G 5@W @G 2@W 0 0 @G 2@W @G 1@W 0 0 0 0 0 0
@W 21@w @G>>@W a hero's leggings @Wlegs @G 6@W @G 2@W 0 @G 4@W @G 3@W 0 @G 4@W 0 0 0 0 0
@w
@Y--------------------------------------------@W Level 41 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 21@w @R<<@W a hero's leggings @Wlegs @G 6@W @G 2@W 0 @G 4@W @G 3@W 0 @G 4@W 0 0 0 0 0
@W 41@w @G>>@W @G(>@RL@Ya@Rv@Ya S@Rh@Yi@Rn G@Yu@Ra@Yr@Rd@Ys@G<) @Wlegs 0 @G 10@W 0 @G 4@W @G 4@W @G 4@W 0 0 @G 4@W @G 20@W 0 @R -40
@w
@Y--------------------------------------------@W Level 91 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 41@w @R<<@W @G(>@RL@Ya@Rv@Ya S@Rh@Yi@Rn G@Yu@Ra@Yr@Rd@Ys@G<) @Wlegs 0 @G 10@W 0 @G 4@W @G 4@W @G 4@W 0 0 @G 4@W @G 20@W 0 @R -40
@W 91@w @G>>@W @r-=@RAn@rcie@Rnt @WSamurai @RS@rou @Wlegs @G 7@W @G 12@W @G 1@W @G 2@W @G 1@W @G 2@W @G 2@W @G 1@W @G 9@W 0 0 0
@w
@Y--------------------------------------------@W Level 181 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W 91@w @R<<@W @r-=@RAn@rcie@Rnt @WSamurai @RS@rou @Wlegs @G 7@W @G 12@W @G 1@W @G 2@W @G 1@W @G 2@W @G 2@W @G 1@W @G 9@W 0 0 0
@W181@w @G>>@W @GT@gou@Ggh @YL@yeath@Yer @WC@wha@Wps@w @Wlegs @G 6@W @G 16@W 0 @G 3@W 0 @G 18@W 0 0 @G 18@W 0 0 0
@w
@Y--------------------------------------------@W Level 200 @Y--------------------------------------------
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W181@w @R<<@W @GT@gou@Ggh @YL@yeath@Yer @WC@wha@Wps@w @Wlegs @G 6@W @G 16@W 0 @G 3@W 0 @G 18@W 0 0 @G 18@W 0 0 0
@W200@w @G>>@W @R(FAKE) @GXeno's @YKnicker @Glegs @W @G 23@W @G 20@W 0 @G 3@W @G 5@W 0 0 0 @G 19@W @G 200@W 0 0
@W
3) You can also specify multiple wearable locations to display. In this example, we will show
results for the hands, feet, neck1, and rwrist locations. I'm not displaying the output here.
I think the helpfile is big enough already :)
"@Gdinv analyze display psi-melee hands feet neck1 rwrist@W"
4) Now that you have created the "psi-melee" analysis, it will show up on your analysis list.
"@Gdinv analyze list@W"
5) If you are done with the analysis, you can delete it. You may wish to do this to save disk
space or to speed up the speed of creating a backup.
"@Gdinv analyze delete psi-melee@W"
]])
end -- inv.cli.analyze.examples
inv.cli.usage = {}
function inv.cli.usage.fn(name, line, wildcards)
local priorityName = wildcards[1] or ""
local query = wildcards[2] or ""
dbot.debug("inv.cli.usage.fn: priority=\"" .. priorityName .. "\", query=\"" .. query .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping usage request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsUsage, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping usage request: character's state does not allow actions")
return inv.tags.stop(invTagsUsage, line, DRL_RET_NOT_ACTIVE)
end -- if
if (priorityName == "") then
inv.cli.usage.usage()
return inv.tags.stop(invTagsUsage, line, DRL_RET_INVALID_PARAM)
else
inv.usage.display(priorityName, query, line)
end -- if
end -- inv.cli.usage.fn
function inv.cli.usage.usage()
dbot.print("@W " .. pluginNameCmd .. " usage @G<priority name | all> <query>@w")
end -- inv.cli.usage.usage
function inv.cli.usage.examples()
dbot.print("@W\nUsage:\n")
inv.cli.usage.usage()
dbot.print(
[[@W
It is very useful to see which items are being used and at what levels they are in your
equipment sets. To use this feature, you must specify a priority (see "@Gdinv help priority@W")
that has a completed analysis available (see "@Gdinv help analyze@W") and a query indicating
which items you wish to examine (see "@Gdinv help search@W"). Any items that match the
specified query will be displayed along with information about where (and if) the item is
used for the given priority.
If an item isn't used with one priority, it may still be used by another priority. For
example, a high int/wis/luck item may be useful for an "@Cenchanter@W" priority even if that
item isn't used by your normal leveling equipment set.
Also, the usage analysis isn't perfect. The plugin can't know what your spell bonuses will
be at any given time so it must make some educated guesses. If you get a superhero spellup
then the optimal equipment for you while that spellup lasts could be different than what
is shown in the usage output.
In short, use the usage report as a best guess but don't assume that the results won't
change depending on your circumstances.
Examples:
1) See which level 1-100 weapons are used and at which levels they are used for the
"@Cpsi-melee@W" priority. Notice the L60 dagger that is used at two level ranges.
It is used between L60-L79 and again at L90-L99. It's a maxed dagger (I bought it
cheap on the market, yay :) and the extra DR and HR would be great between L80-L89
but I can't use it there because of weight restrictions. This is helpful for me
to see that I may want to setweight that dagger.
"@Gdinv usage psi-melee type weapon maxlevel 100@W"
@G 8@W @wa @yFlame@rthrower@w @G(1805970172) @YWeapon@W psi-melee @G11-39
@R 8@W the captain's bastard sword @G(1852932476) @YWeapon@W psi-melee @RUnused
@G 11@W @YDagger of @RAardwolf@w @G(808961542) @YWeapon@W psi-melee @G11-19
@G 20@W S@we@Wa@wr@Wi@wn@Wg @wB@Wl@wa@Wz@we @G(1743467081) @YWeapon@W psi-melee @G20-25
@G 26@W @bM@Belpomene's @bB@Betrayal@w @G(1839990561) @YWeapon@W psi-melee @G26-39
@G 40@W @YDagger of @RAardwolf@w @G(1649835494) @YWeapon@W psi-melee @G40-59
@G 40@W @YDagger of @RAardwolf@w @G(2278063) @YWeapon@W psi-melee @G40-70
@G 60@W @YAxe of @RAardwolf@w @G(769621598) @YWeapon@W psi-melee @G71-79
@G 60@W @YDagger of @RAardwolf@w @G(323630037) @YWeapon@W psi-melee @G60-79 90-99
@G 80@W @YAxe of @RAardwolf@w @G(1759116162) @YWeapon@W psi-melee @G80-89
@G 80@W @YDagger of @RAardwolf@w @G(1778400033) @YWeapon@W psi-melee @G80-89
@G 90@W @YDagger of @RAardwolf@w @G(404748066) @YWeapon@W psi-melee @G90-99
@G100@W @YAxe of @RAardwolf@w @G(250640058) @YWeapon@W psi-melee @G100-109
@G100@W @YDagger of @RAardwolf@w @G(1778448920) @YWeapon@W psi-melee @G100-109@W
2) Let's look at my neck gear between levels 1 - 100. Wow, I have a lot of junk that I should
probably dump. My "cute widdle ears" aren't endearing enough to keep around if I never use
them...
"@Gdinv usage psi-melee wearable neck maxlevel 100@W"
@G 1@W @C(@W+@C) @WB@Citin@Wg W@Cind@Ws @C(@W+@C)@w @G(1834123713) @YArmor@W psi-melee @G11-40
@R 1@W @C(@W+@C) @WB@Citin@Wg W@Cind@Ws @C(@W+@C)@w @G(1743021081) @YArmor@W psi-melee @RUnused
@G 1@W @C(@W+@C) @WB@Citin@Wg W@Cind@Ws @C(@W+@C)@w @G(1834123697) @YArmor@W psi-melee @G11-40
@R 1@W @wc@Wut@we @yw@Yiddl@ye @mka@Mwa@wi@Wi c@wat@M ea@mrs@w @G(1834121351) @YArmor@W psi-melee @RUnused
@R 1@W @wc@Wut@we @yw@Yiddl@ye @mka@Mwa@wi@Wi c@wat@M ea@mrs@w @G(1834121344) @YArmor@W psi-melee @RUnused
@R 1@W @wc@Wut@we @yw@Yiddl@ye @mka@Mwa@wi@Wi c@wat@M ea@mrs@w @G(1834121347) @YArmor@W psi-melee @RUnused
@R 1@W @wc@Wut@we @yw@Yiddl@ye @mka@Mwa@wi@Wi c@wat@M ea@mrs@w @G(1753181926) @YArmor@W psi-melee @RUnused
@R 41@W (>@cAs@Cura@W's Az@Buri@Wte P@Cea@crl@W<)@w @G(1744225929) @YArmor@W psi-melee @RUnused
@G 41@W (>@cAs@Cura@W's Az@Buri@Wte P@Cea@crl@W<)@w @G(1834452605) @YArmor@W psi-melee @G41-70
@G 41@W (>@cAs@Cura@W's Az@Buri@Wte P@Cea@crl@W<)@w @G(1834452600) @YArmor@W psi-melee @G41-99
@R 41@W (>@cAs@Cura@W's Az@Buri@Wte P@Cea@crl@W<)@w @G(1757388666) @YArmor@W psi-melee @RUnused
@G 71@W >@M.@m: @RC@Mr@Ye@Ga@Bt@ci@mv@We License @m:@M.@W<@w @G(1758619847) @YArmor@W psi-melee @G71-90
@R 71@W >@M.@m: @RC@Mr@Ye@Ga@Bt@ci@mv@We License @m:@M.@W<@w @G(1813070241) @YArmor@W psi-melee @RUnused
@G 91@W @y>@Y}@rPho@Ren@Yix's @RPe@rrch@Y{@y<@w @G(1584559998) @YArmor@W psi-melee @G91-140
@R 91@W @Dthe @YAmulet @Dof @MAnubis@w @G(1235973081) @YArmor@W psi-melee @RUnused
@R 91@W @Dthe @YCharm @Dof @MKnowledge@w @G(1745132926) @YArmor@W psi-melee @RUnused
@R100@W a protective cloak skinned from @G(1695078138) @YArmor@W psi-melee @RUnused
@G100@W a protective cloak skinned from @G(1672257124) @YArmor@W psi-melee @G100-170@W
3) You can even use the empty search string to find the usage for all of your items. I'm
not copying that output into this helpfile though :)
"@Gdinv usage psi-melee@W"
]])
end -- inv.cli.usage.examples
inv.cli.compare = {}
function inv.cli.compare.fn(name, line, wildcards)
local priorityName = wildcards[1] or ""
local relativeName = wildcards[2] or ""
dbot.debug("inv.cli.compare.fn: priority=\"" .. priorityName .. "\", relativeName=\"" ..
relativeName .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping compare request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsCompare, line, DRL_RET_UNINITIALIZED)
elseif (not dbot.gmcp.stateIsActive()) then
dbot.info("Skipping compare request: character is not in the active state")
return inv.tags.stop(invTagsCompare, line, DRL_RET_NOT_ACTIVE)
end -- if
if (priorityName == "") or (relativeName == "") then
inv.cli.compare.usage()
inv.tags.stop(invTagsCompare, line, DRL_RET_INVALID_PARAM)
else
inv.set.compare(priorityName, relativeName, line)
end -- if
end -- inv.cli.compare.fn
function inv.cli.compare.usage()
dbot.print("@W " .. pluginNameCmd .. " compare @G<priority name> <relative name>@w")
end -- inv.cli.compare.usage
function inv.cli.compare.examples()
dbot.print("@W\nUsage:\n")
inv.cli.compare.usage()
dbot.print(
[[@W
The plugin gives you the ability to see the impact that a particular item has on all
equipment sets for all levels. It is very difficult to determine how valuable an item
is until you evaluate what your equipment sets would look like if it were not available.
It isn't simply a matter of finding a replacement part and looking at the difference
betwen the two items. Removing one item can have a cascading effect on other wearable
locations. In some cases, the overall impact may be very small while in other cases it
could be significant.
The "@Ccompare@W" mode requires you to have the item you wish to evaluate in your main
inventory. It cannot be worn or be in a container. You must also have a completed
analysis available (see "@Gdinv help analyze@W") for the priority (see "@Gdinv help priority@W")
that is specified.
Once these conditions are met, the plugin will temporarily remove the item from your
inventory table and re-run a full equipment analysis for all levels potentially impacted
by the item. Once the analysis is complete, it will add the item back to your inventory
and display the impact of the item at each level.
Example:
1) I removed my body item (A Pure and True Heart) and performed a comparison to see how
valuable that item is. Having the item increaes my DR, int, and luck but decreases
my wis, str, dex, and con. According to the priorities I specified, the additions
outweigh the negatives and the plugin chose to use this item in my equipment sets for
levels 200 - 211 (level 211 includes 10 levels for my T1 tier bonus.) This is a
helpful and concrete way to evaulate priorities.
"@Gdinv compare psi-melee heart@W"
@WAnalyzing optimal "@Cpsi-melee@W" equipment sets with and without "-@m=@W*@m)@WA @MP@mure @Ma@mnd @MT@mrue @WHeart@m(@W*@m=@W-@w"
@w
@WEquipment analysis of "@Cpsi-melee@W": @G 0%
@WEquipment analysis of "@Cpsi-melee@W": @G 90%
@WEquipment analysis of "@Cpsi-melee@W": @G100%
@w
@WPriority "@Cpsi-melee@W" advantages with "-@m=@W*@m)@WA @MP@mure @Ma@mnd @MT@mrue @WHeart@m(@W*@m=@W-":
@w
@W Ave Sec HR DR Int Wis Lck Str Dex Con Res HitP Mana Move Effects
@WLevel 200: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 201: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 202: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 203: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 204: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 205: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 206: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 207: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 208: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 209: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 210: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0
@WLevel 211: 0 0 0 @G 7@W @G 8@W @R -2@W @G 2@W @R -7@W @R -2@W @R -5@W 0 0 0 0 @W
]])
end -- inv.cli.compare.examples
inv.cli.covet = {}
function inv.cli.covet.fn(name, line, wildcards)
local priorityName = wildcards[1] or ""
local auctionNum = tonumber(wildcards[2] or "")
if (not inv.init.initializedActive) then
dbot.info("Skipping covet request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsCovet, line, DRL_RET_UNINITIALIZED)
elseif (not dbot.gmcp.stateIsActive()) then
dbot.info("Skipping covet request: character is not in the active state")
return inv.tags.stop(invTagsCovet, line, DRL_RET_NOT_ACTIVE)
end -- if
if (auctionNum == nil) then
dbot.warn("inv.cli.covet: auction # is not a number")
inv.cli.covet.usage()
return inv.tags.stop(invTagsCovet, line, DRL_RET_INVALID_PARAM)
end -- if
if (priorityName == "") then
dbot.warn("inv.cli.covet: priorityName is nil")
inv.cli.covet.usage()
return inv.tags.stop(invTagsCovet, line, DRL_RET_INVALID_PARAM)
end -- if
dbot.debug("inv.cli.covet.fn: priority=\"" .. priorityName .. "\", auctionNum=" .. auctionNum)
inv.set.covet(priorityName, auctionNum, line)
end -- inv.cli.covet.fn
function inv.cli.covet.usage()
dbot.print("@W " .. pluginNameCmd .. " covet @G<priority name> <auction #>@w")
end -- inv.cli.covet.usage
function inv.cli.covet.examples()
dbot.print("@W\nUsage:\n")
inv.cli.covet.usage()
dbot.print(
[[@W
The plugin's "@Ccovet@W" mode helps you monitor short-term and long-term auctions to help
you find items that can improve stats over your existing equipment.
Pick a priority (see "@Gdinv help priority@W") that has a completed analysis available
(see "@Gdinv help analyze@W"), find a short-term or long-term auction number and you're
good to go. The plugin will scrape the auction for details about the item, temporarily
add it to your inventory table, re-run a full analysis, and then discard the item from
your inventory table. By comparing your equipment sets both with and without the item you
can determine the advantages/disadvantages of using that item.
Example:
1) Evaluate a body item at long-term market #80561. In this case, it wasn't better than
my existing equipment, but that is still handy to know! If it would have been better
for at least one level, the improvements would have been displayed. See the helpfile
at "@Gdinv help compare@W" for examples showing how the output would have looked.
"@Gdinv covet psi-melee 80561@W"
@WAnalyzing optimal "@Cpsi-melee@W" equipment sets with and without @Gauction 80561
@w
@WLvl Name of Armor Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move
@W200@w Auction #80561 @Gbody @W @G 10@W @G 26@W 0 @G 3@W @G 3@W @G 15@W @G 5@W @G 5@W @G 19@W 0 @G 100@W @R-200
@w
@WPriority "@Cpsi-melee@W" advantages with @Gauction #80561@w:
@w
@WNo set with item "Synthetic Power" is optimal for any level between 11 and 211@W
]])
end -- inv.cli.covet.examples
inv.cli.notify = {}
function inv.cli.notify.fn(name, line, wildcards)
local level = wildcards[1] or ""
if (not inv.init.initializedActive) then
dbot.info("Skipping notify request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsNotify, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping notify request: character's state does not allow actions")
return inv.tags.stop(invTagsNotify, line, DRL_RET_NOT_ACTIVE)
end -- if
if (level == "none") or (level == "light") or (level == "standard") or (level == "all") then
dbot.notify.setLevel(level, line, true)
else
inv.cli.notify.usage()
inv.tags.stop(invTagsNotify, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.notify.fn
function inv.cli.notify.usage()
dbot.print("@W " .. pluginNameCmd .. " notify @G[none | light | standard | all]@w")
end -- inv.cli.notify.usage()
function inv.cli.notify.examples()
dbot.print("@W\nUsage:\n")
inv.cli.notify.usage()
dbot.print(
[[@W
The notification system divides optional messages into three classes based on the message's
priority. The "all" notification mode displays all three optional classes and is the most
verbose mode. The "standard" notification mode displays the two highest-priority optional
message classes. The "light" notification mode displays only the most critical optional
messages. Take a wild guess what the "none" notification mode displays...Warnings and errors
are never optional and the user cannot disable them.
The notification system suppresses all messages -- even warnings and errors -- if the user is
in note-writing mode. We don't want notifications to appear on a user's note.
]])
dbot.print("@WThe default notification mode is \"@C" .. notifyLevelDefault .. "@W\"\n")
-- Enable all message levels so that we can demonstrate them here
local origMsgLevel = dbot.notify.getLevel()
dbot.notify.setLevel("all", nil, false)
dbot.debug("This is a debug message that you probably don't care about.")
dbot.note("This is a note that might be interesting in some cases.")
dbot.info("This is information the user probably wants to know.")
dbot.warn("This is what a warning looks like.")
dbot.error("This is what an error looks like.")
dbot.notify.setLevel(origMsgLevel, nil, false)
dbot.print(
[[@W
Examples:
1) Display all optional (debug, note, and info) messages
"@Gdinv notify all@W"
2) Display everything except low-priority debug messages
"@Gdinv notify standard@W"
3) Display only the most critical messages
"@Gdinv notify light@W"
4) Disable all optional messages and display only warnings and errors
"@Gdinv notify none@W"
]])
end -- inv.cli.notify.examples
inv.cli.forget = {}
function inv.cli.forget.fn(name, line, wildcards)
local query = wildcards[1] or ""
if (not inv.init.initializedActive) then
dbot.info("Skipping forget request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsForget, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping forget request: character's state does not allow actions")
return inv.tags.stop(invTagsForget, line, DRL_RET_NOT_ACTIVE)
end -- if
inv.items.forget(query, line)
end -- inv.cli.forget.fn
function inv.cli.forget.usage()
dbot.print("@W " .. pluginNameCmd .. " forget @G<query>@w")
end -- inv.cli.forget.usage
function inv.cli.forget.examples()
dbot.print("@W\nUsage:\n")
inv.cli.forget.usage()
dbot.print(
[[@W
You may occasionally want to "forget" everything you know about an item and
re-identify it for your inventory table. As noted in the plugin release notes,
there are a few situations where this may occur.
You can enchant an item yourself and the plugin will notice and update stats for
the newly enchanted item. However, if item caching is enabled, you could hit an
issue if you give an item to an enchanter and receive it back after it gets a
boost in stats. By default, the plugin will pull information about the item from
the cache -- but the cache now has old information. In this case, you will need
to "forget" the item and then re-run an inventory refresh to pick up the change.
Most aard operations that modify an item's stats are detected and automatically
trigger a re-identification. For example, enchantment spells, sharpening,
reinforcing, tpenchanting, and wset all are handled transparently. The one known
exception is the setweight command which is not currently handled by aard's invitem
system. Until this is changed (or until we include a trigger watching for setweight)
you will need to use the "@Gdinv forget <query>@W" option on an item that changes
weight in order to "forget" the old stats and then pick up the correct weight (and
other stats) on the next inventory refresh.
See the "@Gdinv help search@W" helpfile for examples and more information about
creating search queries for items that you want to forget.
Examples:
1) Forget and re-identify a ring you just received back from an enchanter
"@Gdinv forget rname 2.ring@W"
"@Gdinv refresh all@W"
2) Forget about all of your aard weapons (maybe you just changed their weights)
"@Gdinv forget type weapon keyword aardwolf@W"
]])
end -- inv.cli.forget.examples
inv.cli.reset = {}
function inv.cli.reset.fn(name, line, wildcards)
local command = wildcards[1] or ""
local modules = wildcards[2] or ""
dbot.debug("reset CLI: command = \"" .. command .. "\", modules = \"" .. modules .. "\"")
if (command == "list") then
dbot.print("@WResettable \"@G" .. pluginNameAbbr .. "@W\" modules: \"@C" .. inv.modules .. "@W\"")
elseif (command == "confirm") then
inv.reset(modules, line)
else
inv.cli.reset.usage()
inv.tags.stop(invTagsReset, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.reset.fn
function inv.cli.reset.usage()
dbot.print("@W " .. pluginNameCmd .. " reset @G[list | confirm] <module names | all>@w")
end -- inv.cli.reset.usage
function inv.cli.reset.examples()
dbot.print("@W\nUsage:\n")
inv.cli.reset.usage()
dbot.print(
[[@W
In a perfect world, there would never be a reason to use the "@Creset@W" mode.
However, it is useful to have the ability to reset particular components of the
plugin even if (hopefully) they are never required. Please look at "@Gdinv help
backup@W" and create a backup before you use this. I *really* don't want to get
notes complaining about losing something because you reset it :).
The following plugin components currently have the ability to be individually
reset back to default values:
@Cconfig@W: This holds version info and some of your preferences. You will
need to rebuild your inventory table if you reset this.
@Citems@W: This is your inventory table. You'll need to rebuild it if you
reset this.
@Ccache@W: This clears both your "recent item cache" and "frequently used
item cache".
@Csnapshot@W: This table stores all custom equipment set snapshots that you have
created.
@Cpriority@W: This wipes out all custom stat priorities and implements the
default values.
@Cset@W: This is where all of your equipment set data is stored when you
run a "@Gdinv analyze create [...]@W" operation. If your backups
are getting a little big, you may want to wipe the equipment sets
and regenerate just the ones you currently care about.
@CstatBonus@W: This table maintains a weighted average of your spell bonuses at
each level.
@Cconsume@W: This table keeps track of which consumable items (typically pills
and potions) you use and where you can buy the items.
@Ctags@W: This table tracks which plugin tags are enabled and if the tag
framework is enabled.
Examples:
1) See which modules are currently resetable with this plugin. Hopefully the list
matches the components shown above in this helpfile. If that's not the case,
please send a note to Durel and let me know so that I can update the helpfile.
"@Gdinv reset list@W"
2) Reset the "@Cset@W" component because it is filled with data for lots of junk
priorities and you want smaller backups.
"@Gdinv reset confirm set@W"
3) Reset the "@Cconfig@W" table and inventory table "@Citems@W".
"@Gdinv reset confirm config items@W"
4) Reset everything. This is equivalent to whacking the entire plugin and starting
from scratch.
"@Gdinv reset confirm all@W"
]])
end -- inv.cli.reset.examples
inv.cli.backup = {}
function inv.cli.backup.fn(name, line, wildcards)
local command = wildcards[1] or ""
local backupName = wildcards[2] or ""
local retval = DRL_RET_SUCCESS
dbot.debug("backup CLI: command = \"" .. command .. "\", backupName = \"" .. backupName .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping backup request: plugin is not yet initialized (are you AFK or sleeping?)")
retval = inv.tags.stop(invTagsBackup, line, DRL_RET_UNINITIALIZED)
elseif (command == "on") then
inv.config.table.isBackupEnabled = true
dbot.info("Automatic backups are @GENABLED@W")
retval = inv.config.save()
retval = inv.tags.stop(invTagsBackup, line, retval)
elseif (command == "off") then
inv.config.table.isBackupEnabled = false
dbot.info("Automatic backups are @RDISABLED@W")
retval = inv.config.save()
retval = inv.tags.stop(invTagsBackup, line, retval)
elseif (command == "list") then
retval = dbot.backup.list(line)
elseif (not dbot.gmcp.stateIsActive()) then
dbot.info("Skipping backup request: character is not in the active state")
retval = inv.tags.stop(invTagsBackup, line, DRL_RET_NOT_ACTIVE)
elseif (command == "create") and (backupName ~= "") then
retval = dbot.backup.create(backupName, line)
elseif (command == "delete") and (backupName ~= "") then
retval = dbot.backup.delete(backupName, line, false)
elseif (command == "restore") and (backupName ~= "") then
retval = dbot.backup.restore(backupName, line)
elseif (command == "auto") then -- Note: auto is a hidden mode and not included in the help file
retval = dbot.backup.current()
else
inv.cli.backup.usage()
retval = inv.tags.stop(invTagsBackup, line, DRL_RET_INVALID_PARAM)
end -- if
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.cli.backup.fn: Unable to perform backup request \"@Y" .. command .. " " .. backupName ..
"@W\": " .. dbot.retval.getString(retval))
end -- if
end -- inv.cli.backup.fn
function inv.cli.backup.usage()
dbot.print("@W " .. pluginNameCmd ..
" backup @G[on | off | list | create | delete | restore] <backup name>@w")
end -- inv.cli.backup.usage
function inv.cli.backup.examples()
dbot.print("@W\nUsage:\n")
inv.cli.backup.usage()
dbot.print(
[[@W
The plugin creates automatic backups for all of your plugin data. It also gives
you the ability to create manual backups at any time.
By default, the plugin enables automatic backups and maintains backups for the
three most recent days you used the plugin. Automatic backups are taken when you
log out, once every 4 hours you are logged in, and every time you go AFK for at
least 5 seconds. You can enable or disable automatic backups by running
"@Gdinv backup on@W" or "@Gdinv backup off@W".
Most automatic backup systems rotate all previous automatic backups when a new
backup is created. In other words, if you have automatic backups 1, 2, 3, and 4
and then create a new backup, the previous backup #1 would become #2, the old #2
would become #3, #3 would become #4 and the old backup #4 would be deleted. That's
not quite how it works here. I like the idea of frequently updating the most current
backup but I also like keeping stable backups from previous days around. If
something does wrong, you might not notice for a couple of days so you don't a
situation where you overwrite all of the backups in a relatively short amount of
time.
The compromise solution implemented in this plugin is for automatic backups to only
overwrite backup #1 and to rotate the backups at most once per day. This gives you
frequent backups from "today" (backup #1) and backups from two previous days
(backups #2 and #3).
You have the option to list existing backups (sorted by creation date), create
new manual backups, delete a backup (automatic or manual), or restore from an existing
backup. These options are cleverly named "@Clist@W", "@Ccreate@W", "@Cdelete@W", and "@Crestore@W".
Examples:
1) Create a new manual backup named "dummyBackupToShowItWorksForTheHelpfile".
"@Gdinv backup create dummyBackupToShowItWorksForTheHelpfile@W"
2) List all current backups. The "auto*" backups are the automatic backups and
all other backups were created manually.
"@Gdinv backup list@W"
@WDINV@W Detected 5 backups
@w @W(@c09/18/17 18:30:57@W) @GdummyBackupToShowItWorksForTheHelpfile
@w @W(@c09/18/17 18:06:16@W) @Gauto
@w @W(@c09/17/17 22:51:49@W) @Gauto2
@w @W(@c09/16/17 23:12:53@W) @Gauto3
@w @W(@c09/15/17 23:42:49@W) @Gbaseline@W
3) Delete the silly manual backup "dummyBackupToShowItWorksForTheHelpfile".
"@Gdinv backup delete dummyBackupToShowItWorksForTheHelpfile@W"
4) Restore the first "auto" backup. This is the most recent automatic backup
taken today.
"@Gdinv backup restore auto@W"
]])
end -- inv.cli.backup.examples
inv.cli.cache = {}
function inv.cli.cache.fn(name, line, wildcards)
local cacheCommand = wildcards[1] or ""
local cacheType = wildcards[2] or ""
local cacheSize = -1
local retval = DRL_RET_SUCCESS
if (wildcards[3] ~= nil) and (wildcards[3] ~= "") then
cacheSize = tonumber(wildcards[3]) or 0
end -- if
dbot.debug("command=\"" .. cacheCommand .. "\", type=\"" .. cacheType .. "\", size=" .. cacheSize)
if (not inv.init.initializedActive) then
dbot.info("Skipping cache request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsCache, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping cache request: character's state does not allow actions")
return inv.tags.stop(invTagsCache, line, DRL_RET_NOT_ACTIVE)
end -- if
if (cacheCommand == "reset") then
if (cacheType == "recent") or (cacheType == "all") then
retval = inv.cache.resetCache(inv.cache.recent.name)
end -- if
if (cacheType == "frequent") or (cacheType == "all") then
retval = inv.cache.resetCache(inv.cache.frequent.name)
end -- if
elseif (cacheCommand == "display") then
if (cacheType == "recent") or (cacheType == "all") then
retval = inv.cache.dump(inv.cache.recent.table)
end -- if
if (cacheType == "frequent") or (cacheType == "all") then
retval = inv.cache.dump(inv.cache.frequent.table)
end -- if
elseif (cacheCommand == "size") then
if (cacheType == "recent") or (cacheType == "all") then
if (cacheSize < 0) then
dbot.print("@WRecent item cache: " .. dbot.table.getNumEntries(inv.cache.recent.table.entries) ..
" / " .. (inv.cache.getSize(inv.cache.recent.table) or 0) .. " entries are in use@w")
else
retval = inv.cache.setSize(inv.cache.recent.table, cacheSize)
end -- if
end -- if
if (cacheType == "frequent") or (cacheType == "all") then
if (cacheSize < 0) then
dbot.print("@WFrequent item cache: " .. dbot.table.getNumEntries(inv.cache.frequent.table.entries) ..
" / " .. (inv.cache.getSize(inv.cache.frequent.table) or 0) .. " entries are in use@w")
else
retval = inv.cache.setSize(inv.cache.frequent.table, cacheSize)
end -- if
end -- if
else
dbot.warn("inv.cli.cache.fn: Invalid cache command \"" .. cacheCommand .. "\" detected")
retval = DRL_RET_INVALID_PARAM
end -- if
if (retval == DRL_RET_SUCCESS) then
dbot.info("Cache request completed successfully")
end -- if
inv.tags.stop(invTagsCache, line, retval)
end -- inv.cli.cache.fn
function inv.cli.cache.usage()
dbot.print("@W " .. pluginNameCmd ..
" cache @G[reset | size] [recent | frequent | all] @Y<# entries>@w")
end -- inv.cli.cache.usage
function inv.cli.cache.examples()
dbot.print("@W\nUsage:\n")
inv.cli.cache.usage()
dbot.print(
[[@W
This plugin implements two types of item caches. The first type is the "@Crecent item@W"
cache. If an identified item leaves your inventory (e.g., you dropped it or you put
it into your vault) then information about that item is moved to the recent cache.
If you add that item back to your inventory at some point in the future then you won't
need to re-identify the item. The plugin will pull the necessary info directly from the
recent cache. This is very convenient and speeds up accessing your vault or using a bag
filled with keys. By default, the recent cache keeps entries for the 500 most-recently
used items that left your inventory but you can adjust the cache size as shown below.
The second type of cache is the "@Cfrequently used@W" item cache. The recent cache stores
information for a specific instance of an item. In contrast, the frequent cache keeps
generic information for a fungible item (wow, I just used "fungible" in an appropriate
context -- cross that off my bucket list!). For example, if you have one duff beer, it
will be identical to the other 99 duff beers you just bought so it would be silly to
individually identify all 100 of those beers. Instead, the plugin will identify your
first duff beer and info on any subsequent duff beers will come from the frequent cache
and avoid re-identification. By default, the plugin will use the frequent cache for
all potions, pills, and consumable items. The frequent cache stores information on up
to 100 different items at a time.
Examples:
1) Reset just the recent cache
"@Gdinv cache reset recent@W"
2) Reset both the recent and frequent caches
"@Gdinv cache reset all@W"
3) Set the number of entries in the frequent cache to 200
"@Gdinv cache size frequent 200
]])
end -- inv.cli.cache.examples
inv.cli.tags = {}
function inv.cli.tags.fn(name, line, wildcards)
local retval = DRL_RET_SUCCESS
local tagNames = wildcards[1] or ""
local enabled = wildcards[2] or ""
dbot.debug("tagNames=\"" .. tagNames .. "\", enabled=\"" .. enabled .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping tags request: plugin is not yet initialized (are you AFK or sleeping?)")
return DRL_RET_UNINITIALIZED
end -- if
if (tagNames == "all") then
tagNames = inv.tags.modules
end -- if
if (tagNames == "") then
if (enabled == drlInvTagOn) then
retval = inv.tags.enable()
elseif (enabled == drlInvTagOff) then
retval = inv.tags.disable()
elseif (enabled == "") then
retval = inv.tags.display()
else
dbot.warn("inv.cli.tags.fn: Invalid tag value")
retval = DRL_RET_INVALID_PARAM
end -- if
else
retval = inv.tags.set(tagNames, enabled)
end -- if
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cli.tags.fn: Tags command failed: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.cli.tags.fn
function inv.cli.tags.usage()
dbot.print("@W " .. pluginNameCmd .. " tags @Y<names | all>@G [on | off]")
end -- inv.cli.tags.usage
function inv.cli.tags.examples()
dbot.print("@W\nUsage:\n")
inv.cli.tags.usage()
dbot.print(
[[@W
This plugin supports optional end tags for all operations. An end tag has the
form "@G{/the command line:return value:return value string}@W". This gives users
an easy way to use the plugin in other scripts because those scripts can trigger on
the end tag to know an operation is done and what result the operation had.
For example, if you type "@Gdinv refresh@W", you could trigger on an end tag that has
this output "@G{/dinv refresh:0:success}@W" to know when the refresh completed. Of
course, you would want to double check the return value in the end tag to ensure
that everything happened the way you want.
The plugin tags subsystem mirrors the syntax for the aardwolf tags subsystem. Using
"@Gdinv tags@W" by itself will display a list of all supported tags. You can toggle
one or more individual tags on or off by providing the tag names as follows:
"@Gdinv tags tagName1 tagName2 [on | off]@W". You can also enable or disable the
entire tag subsystem at once by using "@Gdinv tags [on | off]@W".
If the plugin tags are enabled, they will echo an end tag at the conclusion of an operation.
However, if the user goes into a state (e.g., AFK) that doesn't allow echoing then the plugin
cannot report the end tag. In this scenario, the plugin will notify the user about the end
tag via a warning notification instead of an echo. Triggers cannot catch notifications
though so any code relying on end tags should either detect when you go AFK or cleanly time
out after a reasonable amount of time.
Examples:
1) Display all supported tags
"@Gdinv tags@W"
2) Temporarily disable the entire tags subsystem
"@Gdinv tags off@W"
3) Turn on tags for the "@Crefresh@W", "@Corganize@W", and "@Cset@W" components
"@Gdinv tags refresh organize set on@W"
4) Turn all tags off (but leave the tags subsystem enabled)
"@Gdinv tags all off@W"
]])
end -- inv.cli.tags.examples
inv.cli.reload = {}
function inv.cli.reload.fn(name, line, wildcards)
dbot.info("Reloading plugin")
return dbot.reload()
end -- inv.cli.reload.fn
function inv.cli.reload.usage()
dbot.print("@W " .. pluginNameCmd .. " reload")
end -- inv.cli.reload.usage
function inv.cli.reload.examples()
dbot.print("@W\nUsage:\n")
inv.cli.reload.usage()
dbot.print(
[[@W
This will unload and then load the plugin. You should not need to do this but
it never hurts to have the ability if something goes wrong. This is equivalent
to opening the plugin menu and reinstalling the plugin.
]])
end -- inv.cli.reload.examples
inv.cli.portal = {}
function inv.cli.portal.fn(name, line, wildcards)
local command = wildcards[1] or ""
local portalId = tonumber(wildcards[2] or "")
if (not inv.init.initializedActive) then
dbot.info("Skipping portal request: plugin is not yet initialized (are you AFK or sleeping?)")
return DRL_RET_UNINITIALIZED
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping portal request: character's state does not allow actions")
return DRL_RET_NOT_ACTIVE
end -- if
if (portalId == nil) then
dbot.warn("inv.cli.portal.fn: portalId parameter is not a number")
else
dbot.debug("CLI: " .. pluginNameCmd .. " portal " .. command .. " " .. portalId)
inv.portal.use(portalId)
end -- if
end -- inv.cli.portal.fn
function inv.cli.portal.usage()
dbot.print("@W " .. pluginNameCmd .. " portal @G[use] <portal object ID>@w")
end -- inv.cli.portal.usage
function inv.cli.portal.examples()
dbot.print("@W\nUsage:\n")
inv.cli.portal.usage()
dbot.print(
[[@W
A common situation involves holding a portal and entering it. This is complicated
by the fact that you might be holding something else originally and you would need
to remember what you held to put it back when you are done with the portal. This
is further complicated by the portal wish which gives an additional wearable location
that might hold the portal.
Fortunately, we have the plugin to handle all of this for us :) The plugin checks
if you have a portal wish and uses the correct location automagically. It also
remembers what was at the location used by the portal so that we can put everything
back when we are done.
The plugin's portal component currently supports only a single mode: "@Cuse@W". We
may add additional modes in the future. In the meantime, you can use the following
syntax to automatically get a portal, hold it, enter it, restore anything at the
portal's location, and then put the portal back from whence it came:
"@Gdinv portal use [portal ID]@W".
You can get a portal's unique ID by searching with the "id" query mode. See the
"@Gdinv help search@W" helpfile for details. As an example, you could see the IDs
of all of your portals by typing "@Gdinv search id type portal@W".
The plugin's portal mode is particularly convenient when used in conjuction with the
mapper's portal mode.
Examples:
1) Use the portal with a unique ID of 123456789
"@Gdinv portal use 123456789@W"
2) Tell the mapper plugin to use a particular portal automatically from the room
targeted by portal 123456789
"@Gmapper portal dinv portal use 123456789@W"
]])
end -- inv.cli.portal.examples
inv.cli.pass = {}
function inv.cli.pass.fn(name, line, wildcards)
local passNameOrId = wildcards[1] or ""
local useTimeSec = tonumber(wildcards[2] or "")
if (not inv.init.initializedActive) then
dbot.info("Skipping pass request: plugin is not yet initialized (are you AFK or sleeping?)")
return DRL_RET_UNINITIALIZED
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping pass request: character's state does not allow actions")
return DRL_RET_NOT_ACTIVE
end -- if
if (useTimeSec == nil) then
dbot.warn("inv.cli.pass.fn: # of seconds to use the pass is a required parameter")
inv.cli.pass.usage()
else
dbot.debug("CLI: " .. pluginNameCmd .. " pass " .. passNameOrId .. " " .. useTimeSec)
inv.pass.use(passNameOrId, useTimeSec)
end -- if
end -- inv.cli.pass.fn
function inv.cli.pass.usage()
dbot.print("@W " .. pluginNameCmd .. " pass @G<pass ID> <# of seconds>@w")
end -- inv.cli.pass.usage
function inv.cli.pass.examples()
dbot.print("@W\nUsage:\n")
inv.cli.pass.usage()
dbot.print(
[[@W
Some areas require specific items to be in your main inventory in order for you to
pass through certain rooms or doors. These are not keys. This plugin refers to
such items as "passes". A pass is saveable (unlike a key) and can be kept in a
container.
What we want is the ability to quickly pull a pass out of its container, keep it
in the main inventory for a specific period of time, and then put the pass away.
That would allow us to uses passes easily within a mapper cexit operation.
For example, the "Pet Store Employee ID Card" from the area "Giant's Pet Store"
is not a key, is saveable, and is required to access certain rooms. We use the
"@Gdinv pass [name or ID] [# of seconds]@W" syntax to pull out the Employee Card
at the appropriate time (via a cexit) and then put it away a few seconds later.
Example:
1) Pull out the pass with the unique ID 1761322232 and hold it in main inventory
for 3 seconds before putting it back into its original container
"@Gdinv pass 1761322232 3@W"
]])
end -- inv.cli.pass.examples
inv.cli.consume = {}
function inv.cli.consume.fn(name, line, wildcards)
local command = wildcards[1] or ""
local itemType = wildcards[2] or ""
local itemName = wildcards[3] or ""
local itemNum = tonumber(itemName)
local container = wildcards[4] or ""
dbot.debug("CLI: " .. pluginNameCmd .. " consume command=\"" .. (command or "") .. "\", itemType=\"" ..
(itemType or "") .. "\", itemName/Num=\"" .. (itemName or "") .. "\", container=\"" ..
container .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping consume request: plugin is not yet initialized (are you AFK or sleeping?)")
return DRL_RET_UNINITIALIZED
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping consume request: character's state does not allow actions")
return DRL_RET_NOT_ACTIVE
end -- if
if (command == "add") then
inv.consume.add(itemType, itemName)
elseif (command == "remove") then
inv.consume.remove(itemType, itemName)
elseif (command == "display") then
inv.consume.display(itemType)
elseif (command == "buy") then
inv.consume.buy(itemType, itemNum, container)
elseif (command == drlConsumeSmall) or (command == drlConsumeBig) then
inv.consume.use(itemType, command, itemNum, container)
else
inv.cli.consume.usage()
end -- if
end -- inv.cli.consume.fn
function inv.cli.consume.usage()
dbot.print("@W " .. pluginNameCmd .. " consume @G[add | remove | display | " ..
"buy | small | big] <type> <name or quantity> @Y<container>@w")
end -- inv.cli.consume.usage
function inv.cli.consume.examples()
dbot.print("@W\nUsage:\n")
inv.cli.consume.usage()
dbot.print(
[[@W
Using consumable items such as potions, pills, and scrolls is a very common
occurance. The plugin facilitates this by giving users the ability to specify
types and locations of consumable items. Users can then ask the plugin to
restock or use particular types of things. Confused yet? Let me explain by
giving you a walk-through.
Run to the Aylor potion shop, define a new type of consumable named "@Cfly@W", and
then specify the name of the potion
"@Grecall; runto potion@W"
"@Gdinv consume add fly griff@W"
We can now buy items of type "@Cfly@W". Let's buy three for now. We happen to
be starting at the shop already, but we could be anywhere on the mud and the
plugin will try to run us back to the shop before purchasing the item.
"@Gdinv consume buy fly 3@W"
On second thought, let's buy 2 more "@Cfly@W" potions and automatically put
the fly potions in container 3.bag
"@Gdinv consume buy fly 2 3.bag@W"
Let's use a "@Cfly@W" potion! Don't worry about the "small" option yet. We'll
explain that shortly. [EDIT: I just proofread this and nearly choked on my coffee.
Yes, we have an example here that instructs you to consume a small fly. At least
we stopped before the instructions said to consume a big fly.]
"@Gdinv consume small fly@W"
This looks promising. Let's add several potions for a new type called "@Cmana@W".
We start with one potion at the Aylor shop and then run to a shop in the Seekers' clan
area. You can specify the shop item either by keyword or by the shop's item number.
It is probably easiest to just use the number and that is what we do at the Seekers'
shop below.
"@Grecall; runto potion@W"
"@Gdinv consume add mana rush@W"
"@Grecall; runto seekers; e@W"
"@Gdinv consume add mana 1@W"
"@Gdinv consume add mana 6@W"
"@Gdinv consume add mana 16@W"
Let's display all of our consumable types and instances of those types. The
output shown below is for my favorite set of items. You can run to whatever shops
you wish and add items of your own choice. The consumable types that I have
defined include "@Cmove@W", "@Cmana@W", "@Csight@W", "@Cheal@W", and "@Cfly@W. Some of these such
as "@Cmana@W" and "@Cheal@W" include multiple instances for different levels. That will
become important later on in this walk-through.
"@Gdinv consume display@W"
@WDINV Consumable Type "move"
@W Level 90 (@CRoom 23160@W) "@Gmoonshine@W"
@WDINV Consumable Type "mana"
@W Level 1 (@CRoom 32476@W) "@Glotus rush@W"
@W Level 20 (@CRoom 30525@W) "@Gnachos@W"
@W Level 30 (@CRoom 14141@W) "@Glotus seed@W"
@W Level 50 (@CRoom 23160@W) "@Gtequila@W"
@W Level 60 (@CRoom 14141@W) "@Glotus stem@W"
@W Level 81 (@CRoom 47106@W) "@Gwaterspirit@W"
@W Level 91 (@CRoom 49373@W) "@Gblood@W"
@W Level 100 (@CRoom 14141@W) "@Glotus bud@W"
@W Level 130 (@CRoom 30525@W) "@Gpopcorn@W"
@W Level 150 (@CRoom 14141@W) "@Glotus bloom@W"
@W Level 200 (@CRoom 23160@W) "@Gdaniel@W"
@W Level 201 (@CRoom 14141@W) "@Glotus flower@W"
@WDINV Consumable Type "sight"
@W Level 1 (@CRoom 32476@W) "@Gwolf@W"
@WDINV Consumable Type "heal"
@W Level 1 (@CRoom 32476@W) "@Glight relief@W"
@W Level 20 (@CRoom 32476@W) "@Gserious relief@W"
@W Level 30 (@CRoom 14141@W) "@Gminor healing@W"
@W Level 60 (@CRoom 14141@W) "@Gseekers60heal@W"
@W Level 201 (@CRoom 50209@W) "@Gfrank@W"
@WDINV Consumable Type "fly"
@W Level 1 (@CRoom 32476@W) "@Ggriff@W"@W
Now we want to buy one item of type "@Cheal@W". Note that this doesn't necessarily
mean that it has the "heal" spell. It could be any type of potion or pill that
heals us in some way.
"@Gdinv consume buy heal@W"
So...out of all of the options of type "@Cheal@W", which one did the plugin choose?
It will pick the highest level item that is accessible to you at your current
level. With the consumable table shown above, if you are level 25, it will
pick the Level 20 "serious relief" potion. If you are level 70, it would buy
the Level 60 "seekers60heal" potion instead.
What's with the "small" and "big" options? If you are in combat and need to
use a healing potion, you probably want to quaff the biggest and baddest potion
that you have available: "@Gdinv consume use heal big@W". However, if you aren't
in the middle of combat and want to use up some lower-level healing pots that are
just taking up space, you could use the "small" option instead. That will use up
the lowest-level "@Cheal@W" consumables in your inventory first before moving on to
higher-level items. If you want to use your three lowest-level items of type "@Cheal@W",
use this: "@Gdinv consume small heal 3@W".
The plugin will always choose to consume items that are in your main inventory
before using an equivalent item from a container unless you specify a container.
That makes it more efficient in combat because you don't need to waste an
action removing the item from a container.
Examples:
1) Add a consumable item ("nachos") that gives something of type "mana". Nachos
can be found at "runto bard; run wn" and you should be at the shopkeeper to
use this command.
"@Gdinv consume add mana nachos@W"
2) Remove "nachos" from the "mana" consumable type table
"@Gdinv consume remove mana nachos@W")
3) Remove the consumable type "mana" and everything that is that type
"@Gdinv consume remove mana@W"
4) Display info about all consumable items in the table regardless of type
"@Gdinv consume display@W"
5) Display info about each consumable item that is of type "mana"
"@Gdinv consume display mana@W"
6) Buy 5 "mana" items that are the highest level available in the table
"@Gdinv consume buy mana 5@W"
7) Consume (quaff, eat, etc.) the lowest level "mana" item in your inventory
This is useful when you want to clean out low-level potions. You probably won't
use this option in combat but it is convenient out of combat.
"@Gdinv consume small mana@W"
8) Consume (quaff, eat, etc.) 2 of the highest-level items in your inventory that
are of type "mana". This is handy in combat.
"@Gdinv consume big mana 2@W"
9) Consume 3 of your highest-level mana items and look in container 2.bag before
checking for the items in other locations
"@Gdinv consume big mana 3 2.bag@W"
]])
end -- inv.cli.consume.examples
inv.cli.organize = {}
function inv.cli.organize.fn1(name, line, wildcards)
local command = wildcards[1] or ""
local container = wildcards[2] or ""
local queryString = wildcards[3] or ""
dbot.debug("CLI: " .. pluginNameCmd .. " organize command=\"" .. (command or "") .. "\", container=\"" ..
container .. "\", query=\"" .. queryString .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping organize request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsOrganize, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping organize request: character's state does not allow actions")
return inv.tags.stop(invTagsOrganize, line, DRL_RET_NOT_ACTIVE)
end -- if
if (command == "add") then
inv.items.organize.add(container, queryString, line or "")
elseif (command == "clear") then
inv.items.organize.clear(container, line or "")
else
inv.cli.organize.usage()
inv.tags.stop(invTagsOrganize, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.organize.fn1
function inv.cli.organize.fn2(name, line, wildcards)
local command = wildcards[1] or ""
dbot.debug("CLI: " .. pluginNameCmd .. " organize command=\"" .. (command or "") .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping organize request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsOrganize, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping organize request: character's state does not allow actions")
return inv.tags.stop(invTagsOrganize, line, DRL_RET_NOT_ACTIVE)
end -- if
if (command == "display") then
inv.items.organize.display(line or "")
else
inv.cli.organize.usage()
inv.tags.stop(invTagsOrganize, line, DRL_RET_INVALID_PARAM)
end -- if
end -- inv.cli.organize.fn2
function inv.cli.organize.fn3(name, line, wildcards)
local queryString = wildcards[1] or ""
dbot.debug("CLI: " .. pluginNameCmd .. " organize query=\"" .. queryString .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping organize request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsOrganize, line, DRL_RET_UNINITIALIZED)
elseif dbot.gmcp.statePreventsActions() then
dbot.info("Skipping organize request: character's state does not allow actions")
return inv.tags.stop(invTagsOrganize, line, DRL_RET_NOT_ACTIVE)
end -- if
inv.items.organize.cleanup(queryString, line or "")
end -- inv.cli.organize.fn3
function inv.cli.organize.usage()
dbot.print("@W " .. pluginNameCmd .. " organize @G[add | clear | display] " ..
"@Y<container relative name> <query>@w")
end -- inv.cli.organize.usage
function inv.cli.organize.examples()
dbot.print("@W\nUsage:\n")
inv.cli.organize.usage()
dbot.print(
[[@W
The "@Gdinv put ...@W" and "@Gdinv store ...@W" modes are incredibly convenient for putting items
away into containers. However, wouldn't it be even more convenient if you could assign
one or more queries to each container and automagically store items matching those queries
into their respective containers? The "@Corganize@W" mode gives you this ability.
You can add search queries (see "@Gdinv help search@W" for query details and examples) to
containers. You can also display or clear out a container's queries.
Once you assign search queries to containers, you can specify one or more items in your
inventory with a search query. The plugin will check all items matching your query to see if
they would also match an "organize" query for a container. If an item matches both queries,
the plugin moves it to that container.
It's up to you to ensure that multiple containers don't match the same item(s) in an organize
request. If an item could be organized into two different containers, it will eventually end
up in one, but there are no guarantees as to which of the containers it will be. For example,
if you assign all quest items to one container and all portals to another container, the
aardwolf amulet will match both the quest container and the portal container in an organize
request and it could end up in either container.
Let's get to some examples!
1) Specify that all potions and pills belong in container 3.bag
"@Gdinv organize add 3.bag type potion || type pill@W"
2) Organize all potions and pills by moving them to 3.bag (as specified in the previous
example).
"@Gdinv organize type potion || type pill@W"
3) In my personal setup, I have one bag for potions and pills, another bag for portals, a
third bag for weapons, armor, and lights between levels 1 - 160, and a fourth bag for
weapons, armor, and lights with a level of 161 or higher. Some quest items are flagged
as type "treasure" instead of type "armor" -- even if they clearly are armor. As a result
I also added "|| key aardwolf ..." to the equipment bags so that they also pick up the
(mislabeled?) armor. Doing this adds a conflict with the aardwolf amulet though because
it matches both the portal container and the aardwolf quest equipment container. That is
why I added a "~key aardwolf" query to the portal container.
Let's see what that looks like:
"@Gdinv organize display@W"
@WContainers that have associated organizational queries:
@W "a @YBag of @RAardwolf@W": @Ctype potion || type pill
@W "a @YBag of @RAardwolf@W": @Ctype portal @-key aardwolf
@W "a @YBag of @RAardwolf@W": @Ctype armor maxlevel 160 || type weapon maxlevel 160 || type light maxlevel@C 160 || key aardwolf maxlevel 160
@W "a @YBag of @RAardwolf@W": @Ctype armor minlevel 161 || type weapon minlevel 161 || type light minlevel@C 161 || key aardwolf minlevel 161
@W
4) If I want to put everything away, I would use an empty search query to match everything
in my inventory. Bingo. Everything is now tucked away in containers. This will also put
away your worn equipment so you may want to take an equipment snapshot or ensure you have
a priority set available to re-wear your equipment before you do this.
"@Gdinv organize@W"
5) You can clear a container's organize queries like this:
"@Gdinv organize clear 3.bag@W"
6) Here is how you would organize just your aardwolf quest weapons:
"@Gdinv organize type weapon keyword aardwolf@W"
7) If you want to organize a specific item by using a relative name, you could do
something like this:
"@Gdinv organize rname 3.cloak@W"
8) If you just want to organize all items with "sword" in their names, you can use the
fact that queries assume keys are names by default:
"@Gdinv organize sword@W"
]])
end -- inv.cli.organize.examples
inv.cli.version = {}
function inv.cli.version.fn(name, line, wildcards)
local command = wildcards[1] or ""
local retval = DRL_RET_SUCCESS
dbot.debug("CLI: command=\"" .. command .. "\"")
if (not inv.init.initializedActive) then
dbot.info("Skipping version request: plugin is not yet initialized (are you AFK or sleeping?)")
return inv.tags.stop(invTagsVersion, line, DRL_RET_UNINITIALIZED)
elseif (command == "") then
retval = inv.version.display()
return inv.tags.stop(invTagsVersion, line, retval)
elseif (not dbot.gmcp.stateIsActive()) then
dbot.info("Skipping version request: character is not in the active state")
return inv.tags.stop(invTagsVersion, line, DRL_RET_NOT_ACTIVE)
elseif (command == "changelog") then
dbot.info("Full changelog:")
retval = dbot.version.changelog.get(0, line) -- show changelog from version 0 to the latest
elseif (command == "check") then
retval = dbot.version.update.release(drlDbotUpdateCheck, line)
else
retval = dbot.version.update.release(drlDbotUpdateInstall, line)
end -- if
return retval
end -- inv.cli.version.fn
function inv.cli.version.usage()
dbot.print("@W " .. pluginNameCmd .. " version @Y[check | changelog | update confirm]@w")
end -- inv.cli.version.usage
function inv.cli.version.examples()
dbot.print("@W\nUsage:\n")
inv.cli.version.usage()
dbot.print(
[[@W
The version mode without arguments will tell you the version information for the
plugin and format versions for components of the plugin. You can also check if
you have the latest official plugin release and optionally view the changelog
between your current version and the latest version. If you wish to upgrade to
the latest release, you can do that too :)
Examples:
1) Display your current version information
"@Gdinv version@W"
2) Compare your plugin version to the version of the latest published release
and display the changelog between your version and the latest release
"@Gdinv version check@W"
3) Display the entire plugin changelog
"@Gdinv version changelog@W"
4) Check if you have the latest plugin version. If your version is not the
latest and greatest, download the latest release and install it. You do
not need to log out or restart mush.
"@Gdinv version update confirm@W"
]])
end -- inv.cli.version.examples
inv.cli.help = {}
function inv.cli.help.fn(name, line, wildcards)
local command = wildcards[1] or ""
dbot.debug("inv.cli.help.fn: command=\"" .. command .. "\"")
if (inv.cli[command] ~= nil) and (inv.cli[command].examples ~= nil) then
inv.cli[command].examples()
else
inv.cli.fullUsage()
end -- if
inv.tags.stop(invTagsHelp, line, DRL_RET_SUCCESS)
end -- inv.cli.help.fn
function inv.cli.help.usage()
dbot.print("@W " .. pluginNameCmd .. " help @Y<command>@w")
end -- inv.cli.help.usage
function inv.cli.help.examples()
dbot.print("@W\nUsage:\n")
inv.cli.help.usage()
dbot.print(
[[@W
Run "@Gdinv help@W" by itself to see a list of this plugin's modes. You can
pick any of the modes and see more details and examples by running this:
"@Gdinv help [something]@W".
Examples:
1) Learn about building an inventory table
"@Gdinv help build@W"
2) Read the helpfile for analyzing equipment sets
"@Gdinv help analyze@W"
]])
end -- inv.cli.help.examples
inv.cli.debug = {}
function inv.cli.debug.fn(name, line, wildcards)
local command = wildcards[1] or ""
command = Trim(command)
dbot.note("Debug params = \"" .. command .. "\"")
dbot.shell(command)
end -- inv.cli.debug.fn
----------------------------------------------------------------------------------------------------
-- Item management module: create an inventory table and provide access to it
--
-- Functions:
-- inv.items.init.atInstall
-- inv.items.init.atActive
-- inv.items.fini
--
-- inv.items.save
-- inv.items.load
-- inv.items.reset
-- inv.items.new
--
-- inv.items.getEntry
-- inv.items.setEntry
-- inv.items.getField
-- inv.items.setField
-- inv.items.getStatField
-- inv.items.setStatField
--
-- inv.items.add
-- inv.items.remove
-- inv.items.forget
-- inv.items.forgetCR
--
-- inv.items.discoverCR(maxNumItems, refreshLocations)
-- inv.items.discoverLocation
-- inv.items.discoverSetupFn
--
-- inv.items.identifyCR(maxNumItems, refreshLocations)
-- inv.items.identifyItem
-- inv.items.identifyItemSetupFn
-- inv.items.identifyAtomicSetup()
-- inv.items.identifyAtomicCleanup(resultData, retval)
--
-- inv.items.refresh(maxNumItems, refreshLocations, endTag, tagProxy)
-- inv.items.refreshCR
-- inv.items.refreshAtTime
-- inv.items.refreshDefault()
-- inv.items.refreshGetPeriods()
-- inv.items.refreshSetPeriods(autoMin, eagerSec)
-- inv.items.refreshOn(autoMin, eagerSec)
-- inv.items.refreshOff()
-- inv.items.isDirty()
--
-- inv.items.build
--
-- inv.items.get
-- inv.items.getCR
-- inv.items.getItem
--
-- inv.items.put
-- inv.items.putCR
-- inv.items.putItem
--
-- inv.items.store
-- inv.items.storeCR
-- inv.items.storeItem
--
-- inv.items.wearItem(objId, objLoc, commandArray, doCheckLocation)
-- inv.items.wearSetupFn()
-- inv.items.wearResultFn()
--
-- inv.items.isWorn(objId)
-- inv.items.isWearableLoc(wearableLoc)
-- inv.items.isWearableType(wearableType)
-- inv.items.wearableTypeToLocs(wearableType)
--
-- inv.items.removeItem(objId, commandArray)
-- inv.items.removeSetupFn()
-- inv.items.removeResultFn()
--
-- inv.items.keyword
-- inv.items.keywordCR
--
-- inv.items.search
-- inv.items.searchCR
-- inv.items.sort
-- inv.items.compare
-- inv.items.convertRelative
-- inv.items.convertSetupFn
--
-- inv.items.display
-- inv.items.displayCR
-- inv.items.displayItem
-- inv.items.colorizeStat
--
-- inv.items.isInvis(objId)
--
-- Data:
-- inv.items.table
-- inv.items.stateName -- name for the state file holding the table in persistent storage
----------------------------------------------------------------------------------------------------
-- We don't want to scan worn equipment, the main inventory, and all containers each time that we
-- do a refresh because that can take 5-10 seconds depending on how many containers you have.
-- Instead, we maintain "clean" or "dirty" flags for each possible place to scan. At startup, we
-- consider everything to be "dirty" and we require a full scan. After that, we mark locations as
-- "dirty" when something new is placed at that location. We can then selectively scan the dirty
-- locations, identify things there, and then mark the newly scanned location as "clean". This cuts
-- down on refresh overhead significantly.
--
-- If we identify a container from the recent cache, we mark that container as being dirty because
-- we don't know what items are in the container. Something could have been added or removed since
-- the last time we identified the container.
invItemsRefreshLocAll = "all"
invItemsRefreshLocWorn = "worn"
invItemsRefreshLocMain = "inventory"
invItemsRefreshLocKey = "keyring"
invItemsRefreshLocDirty = "dirty"
invItemsRefreshClean = "isScanned"
invItemsRefreshDirty = "isNotScanned"
inv.items = {}
inv.items.init = {}
inv.items.table = {}
inv.items.stateName = "inv-items.state"
inv.items.mainState = invItemsRefreshDirty -- state for the main inventory (as detected by invdata)
inv.items.wornState = invItemsRefreshDirty -- state for items you are wearing (as detected by eqdata)
inv.items.keyringState = invItemsRefreshDirty -- state for keyring items (as detected by keyring data)
inv.items.burstSize = 20 -- max # of items that can be moved in one atomic operation
function inv.items.init.atInstall()
local retval = DRL_RET_SUCCESS
-- Trigger on invmon
check (AddTriggerEx(inv.items.trigger.invmonName,
"^{invmon}(.*?),(.*?),(.*?),(.*?)$",
"inv.items.trigger.invmon(\"%1\",\"%2\",\"%3\",\"%4\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
-- Trigger on invitem
check (AddTriggerEx(inv.items.trigger.invitemName,
"^{invitem}(.*?),(.*?),(.*?),(.*?),(.*?),(.*?),(.*?),(.*?)$",
"inv.items.trigger.itemDataStats(" ..
"\"%1\",\"%2\",\"%3\",\"%4\", \"%5\",\"%6\",\"%7\",\"%8\",true)",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
-- Trigger on the start of an identify-ish command (lore, identify, object read, bid, lbid, etc.)
check (AddTriggerEx(inv.items.trigger.itemIdStartName,
"^(" ..
".-----------------------------------------------------------------.*|" ..
"You do not have that item.*|" ..
"You dream about being able to identify.*|" ..
".*does not have that item for sale.*|" ..
"There is no auction item with that id.*|" ..
".*currently holds no inventory.*|" ..
"There is no marketplace item with that id.*" ..
")$",
"inv.items.trigger.itemIdStart(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.itemIdStartName, false)) -- default to off
-- Trigger on one of the detail/stat lines of an item's id report (lore, identify, bid, etc.)
check (AddTriggerEx(inv.items.trigger.itemIdStatsName,
"^(\\| .*\\|| A full appraisal will reveal further information on this item.)$",
"inv.items.trigger.itemIdStats(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.itemIdStatsName, false)) -- default to off
-- Suppress output messages from the identification (lore, cast identify, cast object read, etc.)
check (AddTriggerEx(inv.items.trigger.suppressIdMsgName,
"^Your natural intuition reveals the item's properties.....*$",
"",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.suppressIdMsgName, false)) -- default to off
-- Trigger on an eqdata, invdata, or keyring data tag
check (AddTriggerEx(inv.items.trigger.itemDataStartName,
"^{(eqdata|invdata|keyring)[ ]?([0-9]+)?}$",
"inv.items.trigger.itemDataStart(\"%1\",\"%2\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.itemDataStartName, false)) -- default to off
-- Trigger on the stats for an eqdata, invdata, or keyring data item
check (AddTriggerEx(inv.items.trigger.itemDataStatsName,
"^([0-9]+?),(.*?),(.*?),(.*?),(.*?),(.*?),(.*?),(.*?)$",
"inv.items.trigger.itemDataStats" ..
"(\"%1\",\"%2\",\"%3\",\"%4\", \"%5\",\"%6\",\"%7\",\"%8\",false)",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.itemDataStatsName, false)) -- default to off
-- Trigger on an identify command to capture the item's object ID
check (AddTriggerEx(inv.items.trigger.idItemName,
"^(" ..
".------.*|" ..
"\\|.*|" ..
"You do not have that item.*|" ..
"You dream about being able to identify.*|" ..
".*does not have that item for sale.*|" ..
"There is no auction item with that id.*|" ..
".*currently holds no inventory.*|" ..
"There is no marketplace item with that id.*" ..
"|)$", -- accept an empty capture on the last line
"inv.items.trigger.idItem(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.idItemName, false)) -- default to off
-- Trigger on "special" wear messages for unique items
check (AddTriggerEx(inv.items.trigger.wearSpecialName,
"^(" ..
"You proudly pin.*to your chest." ..
"|Your gloves tighten around.*with a loud snap!" ..
"|.* feels like a part of you!" ..
"|You are skilled with .*" ..
"|You feel quite confident with .*" ..
")$",
"",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.wearSpecialName, false)) -- default to off
-- Trigger on the output of the "wear" command
check (AddTriggerEx(inv.items.trigger.wearName,
"^(" ..
"You do not have that item.*" .. -- wear BADNAME
"|You wear.*" .. -- wear the item
"|You wield .*" .. -- hold weapon
"|You light .*" .. -- wear light
"|You hold .*" .. -- held item
"|You equip .*" .. -- wear portal or sleeping bag
"|.* begins floating around you.*" .. -- wear float
"|.* begins floating above you.*" .. -- wear aura of trivia
"|You dream about being able to wear.*" .. -- you are sleeping
"|You cannot wear .*" .. -- item type can't be worn
"|You must be at least level.*to use.*" .. -- your level is too low
")$",
"inv.items.trigger.wear(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.wearName, false)) -- default to off
-- Trigger on the output of the "remove" command
check (AddTriggerEx(inv.items.trigger.removeName,
"^(" ..
"You are not wearing that item." .. -- remove BADNAME
"|You remove .*" .. -- wear item
"|You stop using .*" .. -- shield
"|You stop holding.*" .. -- held item
"|You stop wielding .*" .. -- weapon
"|.* stops floating around you.*" .. -- float
"|.* stops floating above you.*" .. -- above
"|You stop using.* as a portal.*" .. -- portal
"|You dream about removing your equipment.*" ..
")$",
"inv.items.trigger.remove(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.removeName, false)) -- default to off
-- Trigger on the output of the "get" command
check (AddTriggerEx(inv.items.trigger.getName,
"^(" ..
"You get.*" ..
"|You do not see.*" ..
"|You dream about being able to get.*" ..
")$",
"inv.items.trigger.get(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.getName, false)) -- default to off
-- Trigger on the output of the "put" command
check (AddTriggerEx(inv.items.trigger.putName,
"^(" ..
"You don't have that.*" ..
"|You do not see.*" ..
"|You dream about putting items away.*" ..
"|You put .* into .*" ..
")$",
"inv.items.trigger.put(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.putName, false)) -- default to off
-- Trigger on the output of the "keyring get" command
check (AddTriggerEx(inv.items.trigger.getKeyringName,
"^(" ..
"You remove.*from your keyring.*" ..
"|You did not find that on your keyring.*" ..
"|You dream about being able to keyring.*" ..
")$",
"inv.items.trigger.getKeyring(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.getKeyringName, false)) -- default to off
-- Trigger on the output of the "keyring put" command
check (AddTriggerEx(inv.items.trigger.putKeyringName,
"^(" ..
"You put.*on your keyring.*" ..
"|You do not have that item.*" ..
"|You dream about being able to keyring.*" ..
")$",
"inv.items.trigger.putKeyring(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(inv.items.trigger.putKeyringName, false)) -- default to off
return retval
end -- inv.items.init.atInstall
function inv.items.init.atActive()
local retval = inv.items.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.init.atActive: failed to load items data from storage: " ..
dbot.retval.getString(retval))
end -- if
-- Mark all known containers as not being "clean" (i.e., fully scanned). Someone could
-- have logged in without this plugin (or *gasp* used telnet) and moved items or added
-- and/or removed items. We need to know exactly what items are actually present and
-- where those items are.
--
-- Note: This isn't a perfect solution because someone may have added a container without
-- us knowing and we can't mark it as being dirty if we don't know it exists. However,
-- this is largely redundant with the full scan we do when we init the plugin and it
-- is essentially just a backup way of handling things if the user stops the full scan
-- for some reason.
for objId, _ in pairs(inv.items.table) do
if (inv.items.getStatField(objId, invStatFieldType) == invmon.typeStr[invmonTypeContainer]) then
inv.items.setField(objId, invFieldIdentifyLevel, invIdLevelNone)
inv.items.keyword(invItemsRefreshClean, invKeywordOpRemove, "id " .. objId, true)
end -- if
end -- for
-- If automatic refreshes are enabled (i.e., the period is > 0 minutes), kick off the
-- refresh timer to periodically scan our inventory and update the inventory table
local refreshPeriod = inv.items.refreshGetPeriods() or inv.items.timer.refreshMin
if (refreshPeriod > 0) then
inv.items.refreshAtTime(refreshPeriod, 0)
else
dbot.deleteTimer(inv.items.timer.refreshName)
end -- if
return retval
end -- inv.items.init.atActive
function inv.items.fini(doSaveState)
local retval = DRL_RET_SUCCESS
dbot.deleteTrigger(inv.items.trigger.invmonName)
dbot.deleteTrigger(inv.items.trigger.invitemName)
dbot.deleteTrigger(inv.items.trigger.itemIdStartName)
dbot.deleteTrigger(inv.items.trigger.itemIdStatsName)
dbot.deleteTrigger(inv.items.trigger.suppressIdMsgName)
dbot.deleteTrigger(inv.items.trigger.itemDataStartName)
dbot.deleteTrigger(inv.items.trigger.itemDataStatsName)
dbot.deleteTrigger(inv.items.trigger.idItemName)
dbot.deleteTrigger(inv.items.trigger.wearSpecialName)
dbot.deleteTrigger(inv.items.trigger.wearName)
dbot.deleteTrigger(inv.items.trigger.removeName)
dbot.deleteTrigger(inv.items.trigger.getName)
dbot.deleteTrigger(inv.items.trigger.putName)
dbot.deleteTrigger(inv.items.trigger.getKeyringName)
dbot.deleteTrigger(inv.items.trigger.putKeyringName)
dbot.deleteTimer(inv.items.timer.refreshName)
dbot.deleteTimer(inv.items.timer.idTimeoutName)
if (doSaveState) then
-- Save our current data
retval = inv.items.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.fini: Failed to save inv.items module data: " .. dbot.retval.getString(retval))
end -- if
end -- if
inv.items.fullScanCompleted = false
return retval
end -- inv.items.fini
function inv.items.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.items.stateName,
"inv.items.table", inv.items.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.save: Failed to save items table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.items.save
function inv.items.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.items.stateName, inv.items.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.items.stateName .. "@W\": " .. dbot.retval.getString(retval))
end -- if
if (inv.items.table == nil) then
dbot.error("inv.items.load: Failed to load inventory table")
return DRL_RET_INTERNAL_ERROR
end -- if
return retval
end -- inv.items.load
function inv.items.reset()
inv.items.table = {}
return inv.items.save()
end -- inv.items.reset
function inv.items.new(objId)
assert(objId ~= nil, "inv.items.new: objId is nil")
inv.items.table[objId] = {}
inv.items.table[objId][invFieldIdentifyLevel] = invIdLevelNone
inv.items.table[objId][invFieldObjLoc] = invItemLocUninitialized
inv.items.table[objId][invFieldColorName] = ""
inv.items.table[objId][invFieldStats] = {}
return DRL_RET_SUCCESS
end -- inv.items.new
function inv.items.getEntry(objId)
assert(objId ~= nil, "inv.items.getEntry: objId is nil")
-- dbot.debug("inv.items.getEntry: retrieved item " .. objId)
return inv.items.table[objId]
end -- inv.items.getEntry
function inv.items.setEntry(objId, entry)
assert(objId ~= nil, "inv.items.setEntry: objId is nil")
inv.items.table[objId] = entry
-- dbot.debug("inv.items.setEntry: updated item " .. objId)
return DRL_RET_SUCCESS
end -- inv.items.setEntry
invFieldIdentifyLevel = "identifyLevel"
invFieldObjLoc = "objectLocation"
invFieldHomeContainer = "homeContainer"
invFieldColorName = "colorName"
invFieldStats = "stats"
function inv.items.getField(objId, field)
-- Check the params
assert((objId ~= nil) and (field ~= nil), "inv.items.getField: nil parameters")
objId = tonumber(objId)
assert((objId ~= nil), "Invalid non-numeric objId detected")
local entry = inv.items.getEntry(objId)
if (entry == nil) then
dbot.debug("inv.items.getField: Failed to get field \"" .. field .. "\", entry " .. objId ..
" does not exist")
return nil,DRL_RET_MISSING_ENTRY
end -- if
return entry[field], DRL_RET_SUCCESS
end -- inv.items.getField
function inv.items.setField(objId, field, value)
-- Check the params
assert((objId ~= nil) and (field ~= nil) and (value ~= nil), "inv.items.setField: nil parameters")
objId = tonumber(objId)
assert((objId ~= nil), "Invalid non-numeric objId detected")
local entry = inv.items.getEntry(objId)
if (entry == nil) then
dbot.warn("inv.items.setField: Failed to set field \"" .. field .. "\", entry " .. objId ..
" does not exist")
return DRL_RET_MISSING_ENTRY
end -- if
-- Update the field
entry[field] = value
return DRL_RET_SUCCESS
end -- inv.items.setField
function inv.items.getStatField(objId, field)
objId = tonumber(objId or "")
if (objId == nil) then
dbot.warn("inv.items.getStatField: objId parameter is missing")
return nil, DRL_RET_INVALID_PARAM
end -- if
if (field == nil) or (field == "") then
dbot.warn("inv.items.getStatField: field parameter is missing")
return nil, DRL_RET_INVALID_PARAM
end -- if
local entry = inv.items.table[objId]
if (entry == nil) then
dbot.debug("inv.items.getStatField failed: no inventory entry found for objectID " .. objId ..
" for field " .. (field or "nil"))
return nil, DRL_RET_MISSING_ENTRY
end -- if
if (entry.stats == nil) then
dbot.warn("inv.items.getStatField: Missing stats for objectID " .. objId)
return nil, DRL_RET_UNIDENTIFIED
end -- if
return entry.stats[field], DRL_RET_SUCCESS
end -- inv.items.getStatField
function inv.items.setStatField(objId, field, value)
assert(objId ~= nil, "inv.items.setStatField: nil objId parameter")
assert(field ~= nil, "inv.items.setStatField: nil field parameter for item " .. objId)
assert(value ~= nil, "inv.items.setStatField: nil value parameter for item " .. objId)
local entry = inv.items.table[objId]
if (entry == nil) then
dbot.warn("inv.items.setStatField failed: no inventory entry found for objectID " .. objId)
dbot.debug("Attempted to set field " .. field .. " to value \"" .. (value or "nil"))
return DRL_RET_MISSING_ENTRY
end -- if
entry.stats[field] = value
end -- inv.items.setStatField
function inv.items.add(objId)
local retval = DRL_RET_SUCCESS
local objIdNum = tonumber(objId)
if (objIdNum == nil) then
dbot.warn("inv.items.add: Failed to add non-numeric objId " .. objId)
return DRL_RET_INVALID_PARAM
end -- if
-- Check if we can pull details on this item instance from the "recently removed"
-- item cache. If we can, do it :) Otherwise, start a new entry for this objId.
local entry = inv.cache.get(inv.cache.recent.table, objId)
if (entry == nil) then
retval = inv.items.new(objId)
else
retval = inv.items.setEntry(objId, dbot.table.getCopy(entry))
if (retval == DRL_RET_SUCCESS) then
dbot.debug("Added \"" .. (inv.items.getField(objId, invFieldColorName) or "Unidentified") ..
DRL_ANSI_WHITE .. "\" (" .. objId .. ") from recent item cache")
-- The item is now in our inventory table so we can remove it from the recent item cache
retval = inv.cache.remove(inv.cache.recent.table, objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.add: Failed to remove " .. objId .. " from recent item cache: " ..
dbot.retval.getString(retval))
end -- if
-- If the new item is a container, mark it as "dirty" so that we will rescan it on the
-- next discovery phase of a refresh. We don't know the current contents of the container
-- since it left our possession for a while.
if (inv.items.getStatField(objId, invStatFieldType) == invmon.typeStr[invmonTypeContainer]) then
inv.items.keyword(invItemsRefreshClean, invKeywordOpRemove, "id " .. objId, true)
end -- if
end -- if
end -- if
return retval
end -- inv.items.add
function inv.items.remove(objId)
local retval = DRL_RET_SUCCESS
local item = inv.items.getEntry(objId)
if (item == nil) then
dbot.warn("inv.items.removeFailed to remove item " .. objId ..
" from inventory table because it is not in the table")
return DRL_RET_MISSING_ENTRY
end -- if
-- Remove any item whose location matches the objId for the removed item (it could be a container)
-- It would be nice if we could prune the search a bit and only do this if the item is a container.
-- Unfortunately, we can't be guaranteed that the item is identified enough for us to know it is a
-- container so we must do this for every item. It's a bit more overhead...oh well.
for k,v in pairs(inv.items.table) do
if (v[invFieldObjLoc] ~= nil) and (v[invFieldObjLoc] == objId) then
dbot.debug("Removed item " .. k .. " from removed container " .. objId)
inv.items.remove(k)
end -- if
end -- for
-- If the item is not in the frequent item cache, then cache the item in the "recently removed" item
-- cache in case we want to pick it up again soon. We could duplicate things in both the frequent
-- and recent caches, but it helps keep things uncluttered if we don't add an item to the recent cache
-- if it is easily identified from the frequent cache.
local name = inv.items.getStatField(objId, invStatFieldName)
if (name ~= nil) and (inv.cache.get(inv.cache.frequent.table, name) == nil) then
retval = inv.cache.add(inv.cache.recent.table, objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.remove: failed to cache \"" ..
(inv.items.getField(objId, invFieldColorName) or "Unidentified") ..
"\" in recently removed item cache: " .. dbot.retval.getString(retval))
end -- if
end -- if
-- Whack the entry we just removed
inv.items.setEntry(objId, nil)
return retval
end -- inv.items.remove
inv.items.forgetPkg = nil
function inv.items.forget(query, endTag)
if (inv.items.forgetPkg ~= nil) then
dbot.note("Skipping forget request: another forget request is in progress")
return inv.tags.stop(invTagsForget, endTag, DRL_RET_BUSY)
end -- if
inv.items.forgetPkg = {}
inv.items.forgetPkg.query = query or ""
inv.items.forgetPkg.endTag = endTag
wait.make(inv.items.forgetCR)
return DRL_RET_SUCCESS
end -- inv.items.forget
function inv.items.forgetCR()
if (inv.items.forgetPkg == nil) or (inv.items.forgetPkg.query == nil) then
dbot.error("inv.items.forgetCR: Aborting forget request -- forget package or query is nil!")
inv.items.forgetPkg = nil
return DRL_RET_INTERNAL_ERROR
end -- if
local endTag = inv.items.forgetPkg.endTag
local idArray, retval = inv.items.searchCR(inv.items.forgetPkg.query)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.forgetCR: failed to search inventory table: " .. dbot.retval.getString(retval))
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("No match found for forget query: \"" .. inv.items.forgetPkg.query .. "\"")
-- Forget everything that matched the query by removing it from the inventory table and cache
else
for _, objId in ipairs(idArray) do
dbot.note("Forgetting item \"" .. (inv.items.getField(objId, invFieldColorName) or "Unknown") .. "@W\"")
inv.items.remove(objId)
inv.cache.remove(inv.cache.recent.table, objId)
end -- for
dbot.info("Forgot " .. #idArray .. " items: run \"@Gdinv refresh all@W\" to rescan your inventory.")
end -- if
-- Save our changes so that they don't get picked up again accidentally if we reload the plugin
inv.items.save()
inv.items.forgetPkg = nil
return inv.tags.stop(invTagsForget, endTag, retval)
end -- inv.items.forgetCR
function inv.items.discoverCR(maxNumItems, refreshLocations)
local retval
-- If maxNumItems isn't given, default to 0 -- which means there is no maximum
maxNumItems = tonumber(maxNumItems or 0) or 0
-- If refreshLocations is not given, default to scanning everything
refreshLocation = refreshLocations or invItemsRefreshLocAll
-- Discover equipment that is currently worn. We only do this if the user asked to scan "all"
-- locations, if the user specifically asked to scan "worn" locations, or if the user asked to
-- scan dirty locations and worn locations are marked as dirty because we haven't yet scanned them.
if (refreshLocation == invItemsRefreshLocAll) or
(refreshLocation == invItemsRefreshLocWorn) or
((refreshLocation == invItemsRefreshLocDirty) and (inv.items.wornState == invItemsRefreshDirty)) then
retval = inv.items.discoverLocation(invItemLocWorn)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.items.discoverCR: Failed to discover worn equipment: " .. dbot.retval.getString(retval))
return retval
else
inv.items.wornState = invItemsRefreshClean
end -- if
end -- if
-- Discover items in the main inventory
if (refreshLocation == invItemsRefreshLocAll) or
(refreshLocation == invItemsRefreshLocMain) or
((refreshLocation == invItemsRefreshLocDirty) and (inv.items.mainState == invItemsRefreshDirty)) then
retval = inv.items.discoverLocation(invItemLocInventory)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.items.discoverCR: Failed to discover main inventory contents: " ..
dbot.retval.getString(retval))
return retval
else
inv.items.mainState = invItemsRefreshClean
end -- if
end -- if
-- Discover items in the keyring
if (refreshLocation == invItemsRefreshLocAll) or
(refreshLocation == invItemsRefreshLocKey) or
((refreshLocation == invItemsRefreshLocDirty) and (inv.items.keyringState == invItemsRefreshDirty)) then
retval = inv.items.discoverLocation(invItemLocKeyring)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.items.discoverCR: Failed to discover keyring contents: " ..
dbot.retval.getString(retval))
return retval
else
inv.items.keyringState = invItemsRefreshClean
end -- if
end -- if
-- Identify everything discovered so far (we mainly just want to find containers so that we can discover
-- their contents next)
retval = inv.items.identifyCR(maxNumItems, refreshLocations)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.items.discoverCR: Inventory identification did not complete: " ..
dbot.retval.getString(retval))
return retval
end -- if
-- Discover all containers
if (refreshLocation == invItemsRefreshLocAll) or (refreshLocation == invItemsRefreshLocDirty) then
for objId,v in pairs(inv.items.table) do
if (inv.items.getStatField(objId, invStatFieldType) == invmon.typeStr[invmonTypeContainer]) then
-- Scan this container if the caller asked us to scan everything or if we need to scan all
-- dirty containers and this container is dirty (i.e., it hasn't been verified to be clean
-- in a previous scan)
local keywordField = inv.items.getStatField(objId, invStatFieldKeywords) or ""
if (refreshLocation == invItemsRefreshLocAll) or
((refreshLocation == invItemsRefreshLocDirty) and
(not dbot.isWordInString(invItemsRefreshClean, keywordField))) then
dbot.debug("Discovering contents of container " .. objId .. ": " .. v[invFieldColorName])
-- Discover items in the container
retval = inv.items.discoverLocation(objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.discoverCR: Failed to discover container " .. objId ..
": " .. dbot.retval.getString(retval))
else
inv.items.keyword(invItemsRefreshClean, invKeywordOpAdd, "id " .. objId, true)
end -- if
end -- if
end -- if
end -- for
end -- if
return retval
end -- inv.items.discoverCR
drlInvDiscoveryTimeoutThresholdSec = 30 -- make this large enough to handle a speedwalk in the middle
function inv.items.discoverLocation(location)
local retval
local containerId
-- Valid discovery locations are worn equipment, main inventory, the keyring, or a container
if (location ~= nil) then
containerId = tonumber(location)
end -- if
assert((location == invItemLocWorn) or (location == invItemLocInventory) or
(location == invItemLocKeyring) or (containerId ~= nil),
"inv.items.discoverLocation: invalid location parameter")
-- Only allow one discovery request at a time
if (inv.items.discoverPkg ~= nil) then
dbot.note("Skipping inventory discovery because another discovery is in progress")
return DRL_RET_BUSY
end -- if
inv.items.discoverPkg = {}
-- Start the discovery!!!
local command
if (location == invItemLocWorn) then
command = "eqdata"
elseif (location == invItemLocInventory) then
command = "invdata"
elseif (location == invItemLocKeyring) then
command = "keyring data"
else
command = "invdata " .. containerId
end -- if
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command(command, inv.items.discoverSetupFn, nil,
dbot.callback.default, resultData)
if (retval ~= DRL_RET_SUCCESS) then
if (retval ~= DRL_RET_IN_COMBAT) then
dbot.warn("inv.items.discoverLocation: Failed to execute command \"@G" .. command .. "@W\": " ..
dbot.retval.getString(retval))
end -- if
inv.items.trigger.itemDataEnd() -- Call this to clean up any lingering state from the failed discovery
return retval
end -- if
-- Wait for the callback to confirm that the safe execution completed
retval = dbot.callback.wait(resultData, drlInvDiscoveryTimeoutThresholdSec)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Inventory discovery did not complete: " .. dbot.retval.getString(retval) ..
". We'll try again later.")
inv.items.trigger.itemDataEnd() -- Call this to clean up any lingering state from the failed discovery
end -- if
-- Wait until the eqdata, invdata, keyring data triggers complete
if (retval == DRL_RET_SUCCESS) then
local timeout = 0
while (inv.items.discoverPkg ~= nil) do
wait.time(drlSpinnerPeriodDefault)
timeout = timeout + drlSpinnerPeriodDefault
if (timeout > 2) then -- use a short timeout since the callback.wait() call above already waited a while
dbot.note("Inventory discovery timed out -- maybe you were busy. We'll try again later...")
inv.items.trigger.itemDataEnd() -- Call this to clean up any lingering state from the failed discovery
return DRL_RET_TIMEOUT
end -- if
end -- while
end -- if
return retval
end -- inv.items.discoverLocation
function inv.items.discoverSetupFn()
EnableTrigger(inv.items.trigger.itemDataStartName, true)
end -- inv.items.discoverSetupFn
function inv.items.identifyCR(maxNumItems, refreshLocations)
local retval = DRL_RET_SUCCESS
-- TODO: Possibly support capping the number of items identified in one call and/or limiting
-- which locations have items to identify (e.g., main inventory, containers, etc.)
-- For now, we ignore the "maxNumItems" and "refreshLocations" parameters.
-- Count the number of items to identify and keep a record of which items require identification.
-- We use a temporary array for the objIds of these items so that we can avoid walking the entire
-- inventory table again and so that we don't need to handle the case where item idLevel changes
-- during identification. For example, if the user removes an unidentified item from a container,
-- we mark the container as unidentified so that we can pick up the current weight statistics on
-- the next identification. That scenario complicates things because we could get into a situation
-- where we pull lots of unidentified items from a container and then immediately re-identify the
-- container after each one. The current implementation avoids that entirely by determining up front
-- which items should be identified during this identification pass.
local objsToIdentify = {}
local numItemsToIdentify = 0
for objId, _ in pairs(inv.items.table) do
local idLevel = inv.items.getField(objId, invFieldIdentifyLevel)
if (idLevel ~= nil) and (idLevel == invIdLevelNone) then
numItemsToIdentify = numItemsToIdentify + 1
table.insert(objsToIdentify, objId)
end -- if
end -- for
local numItemsIdentified = 0
for _, objId in ipairs(objsToIdentify) do
-- Stop and return if we if the user requested that we disable the refresh
if (inv.state == invStatePaused) then
retval = DRL_RET_HALTED
break
end -- if
-- Stop and return if we are not in the "active" state. We don't want to try identifying
-- things when we are AFK, writing a note, sleeping, running, etc.
local charState = dbot.gmcp.getState()
if (charState ~= dbot.stateActive) then
dbot.note("Skipping remainder of identification request: you are now in state \"" ..
dbot.gmcp.getStateString(charState) .. "\"")
if (charState == dbot.stateCombat) then
retval = DRL_RET_IN_COMBAT
else
retval = DRL_RET_NOT_ACTIVE
end -- if
break
end -- if
-- Attempt to get both the colorized name and the regular name for the item. If we have
-- the colorized name, we can get the regular name by stripping the colors from the colorized
-- version of the name.
local colorName = inv.items.getField(objId, invFieldColorName)
if (colorName == "") then
colorName = nil
end -- if
local name = inv.items.getStatField(objId, invStatFieldName)
if (name == nil) or (name == "") then
if (colorName ~= nil) then
name = strip_colours(colorName)
else
name = nil
end -- if
end -- if
-- Check if we can get details on this item from the "frequently acquired" item cache.
-- If it is in the cache, use the cached copy instead of manually identifying it.
local idLevel = inv.items.getField(objId, invFieldIdentifyLevel)
if (idLevel ~= nil) and (idLevel == invIdLevelNone) and (name ~= nil) then
local cachedEntry = inv.cache.get(inv.cache.frequent.table, name)
if (cachedEntry ~= nil) then
cachedEntry.stats.id = objId
retval = inv.items.setEntry(objId, (cachedEntry))
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.identifyCR: Failed to set \"" .. name .. DRL_ANSI_WHITE ..
"\" from frequent cache entry: " .. dbot.retval.getString(retval))
else
numItemsIdentified = numItemsIdentified + 1
dbot.note("Identify (" .. numItemsIdentified .. " / " .. numItemsToIdentify ..
") \"" .. (colorName or name or "Unidentified") .. "@W" .. DRL_ANSI_WHITE ..
"\" (" .. objId .. ") from frequent cache")
end -- if
end -- if
end -- if
-- If we don't have any identification completed yet, do a basic ID
idLevel = inv.items.getField(objId, invFieldIdentifyLevel)
if (idLevel ~= nil) and (idLevel == invIdLevelNone) then
local resultData = dbot.callback.new()
numItemsIdentified = numItemsIdentified + 1
dbot.note(string.format("Identify (%d / %d)", numItemsIdentified, numItemsToIdentify) ..
": \"" .. (colorName or name or "Unidentified") .. "@W" .. DRL_ANSI_WHITE ..
"\" (" .. objId .. ")")
local commandArray = dbot.execute.new()
retval = inv.items.identifyItem(objId, idCommandBasic, resultData, commandArray)
-- Wait until we have confirmation the identification completed
if (retval == DRL_RET_SUCCESS) then
if (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, inv.items.identifyAtomicSetup, nil,
inv.items.identifyAtomicCleanup, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to identify item \"" .. (colorName or name or "Unidentified") ..
"@W" .. DRL_ANSI_WHITE .. "\": " .. dbot.retval.getString(retval))
end -- if
else
local timeout = 0
while (resultData.isDone == false) do
wait.time(inv.items.timer.idTimeoutPeriodSec)
timeout = timeout + inv.items.timer.idTimeoutPeriodSec
if (timeout > inv.items.timer.idTimeoutThresholdSec) then
dbot.warn("inv.items.identifyCR: Basic identification timed out for item " .. objId ..
": \"" .. (colorName or name or "Unidentified") .. DRL_ANSI_WHITE .. "\"")
break
end -- if
end -- while
end -- if
end -- if
end -- if
-- If the item is an instance of a frequently acquired item (potion, pill, etc.) add it
-- to the "frequently acquired item" cache if it is not already in the cache.
-- Grab the latest name because it may have been filled in during the ID
name = inv.items.getStatField(objId, invStatFieldName)
if (name ~= nil) then
local cacheEntry = inv.cache.get(inv.cache.frequent.table, name)
if (cacheEntry == nil) then
-- TODO: It would be nice if we could keep wands and staves in the frequent cache, but wands
-- and staves can have variable levels and they aren't necessarily identical. Also, they
-- can have varying numbers of charges which means that they can't be considered identical.
itemType = inv.items.getStatField(objId, invStatFieldType)
if (itemType == invmon.typeStr[invmonTypePotion]) or
(itemType == invmon.typeStr[invmonTypePill]) or
(itemType == invmon.typeStr[invmonTypeFood]) or
(itemType == invmon.typeStr[invmonTypeScroll]) then
colorName = inv.items.getField(objId, invFieldColorName)
retval = inv.cache.add(inv.cache.frequent.table, objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.identifyCR: Failed to add \"" .. (colorName or name or "Unidentified") ..
"@W\" to frequent item cache: " .. dbot.retval.getString(retval))
else
dbot.note("Added \"" .. (colorName or "Unidentified") .. "@W\" to frequent item cache")
end -- if
end -- if
end -- if
end -- if
end -- for objId,_ in pairs
-- We are done (at least for now)
if (retval == DRL_RET_SUCCESS) then
dbot.debug("Inventory identification procedure completed")
elseif (retval == DRL_RET_HALTED) then
dbot.debug("Inventory identification halted early")
end -- if
return retval
end -- inv.items.identifyCR
idCommandBasic = "identify"
-- Asynchronous routine to identify an item
function inv.items.identifyItem(objId, idCommand, resultData, commandArray)
local retval = DRL_RET_SUCCESS
local command
assert((objId ~= nil) and (idCommand ~= nil), "inv.items.identifyItem: nil parameters detected")
local item = inv.items.getEntry(objId)
if (item == nil) then
dbot.warn("inv.items.identifyItem: Failed to identify item " .. objId ..
" in inventory table because it is not in the table")
return DRL_RET_MISSING_ENTRY
end -- if
-- Check where the item is (main inventory, worn location, container, etc.). We will want to
-- put the item back to its original location once we finish identifying it.
local objLoc = inv.items.getField(objId, invFieldObjLoc)
if (objLoc == nil) or (objLoc == invItemLocUninitialized) then
dbot.warn("inv.items.identifyItem: Failed to identify item " .. objId ..
": item's location could not be determined")
return DRL_RET_MISSING_ENTRY
end -- if
-- Check if another id is in progress before we proceed
if (inv.items.identifyPkg ~= nil) then
dbot.info("inv.items.identifyItem: Skipping identification of " .. objId ..
": another identification is in progress")
return DRL_RET_BUSY
end -- if
-- Use globals to hold state for the identify triggers
inv.items.identifyPkg = {}
inv.items.identifyPkg.objId = objId
inv.items.identifyPkg.objLoc = objLoc
inv.items.identifyPkg.command = idCommand
if (commandArray == nil) then
-- Add a timeout timer to clear the identify request if we take too long. For example,
-- we may have tried to identify an item that isn't in our inventory table or we may have
-- tried to use an invalid identify command.
check (AddTimer(inv.items.timer.idTimeoutName, 0, 0, inv.items.timer.idTimeoutThresholdSec, "",
timer_flag.Enabled + timer_flag.OneShot, "inv.items.timer.idTimeout"))
end -- if
-- Identify the item. If it is in a container, keyring, vault, or worn, we must first put the
-- item into the main inventory before we ID it. If we needed to move the item to ID it, put
-- it back where we got it after the ID completes.
-- Main inventory
if (objLoc == invItemLocInventory) then
command = idCommand .. " " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, command)
else
retval = dbot.execute.safe.command(command, inv.items.identifyItemSetupFn, nil,
dbot.callback.default, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Main inventory identify request did not complete: " .. dbot.retval.getString(retval))
end -- if
end -- if
end -- if
-- Keyring
elseif (objLoc == invItemLocKeyring) then
retval = inv.items.getItem(objId, commandArray)
if (retval == DRL_RET_SUCCESS) then
command = idCommand .. " " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, command)
else
retval = dbot.execute.safe.command(command, inv.items.identifyItemSetupFn, nil,
dbot.callback.default, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Keyring identify request did not complete: " .. dbot.retval.getString(retval))
end -- if
end -- if
end -- if
command = "keyring put " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, command)
else
local itemName = inv.items.getField(objId, invFieldColorName) or "Unidentified"
dbot.note(" Putting \"" .. itemName .. "@W" .. DRL_ANSI_WHITE .. "\" onto keyring")
local resultData2 = dbot.callback.new()
retval = dbot.execute.safe.command(command, inv.items.putKeyringSetupFn, nil,
inv.items.putKeyringResultFn, resultData2)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Key \"" .. itemName .. "\" was not placed onto keyring: " ..
dbot.retval.getString(retval))
else
-- Wait until we have confirmation that the callback completed
retval = dbot.callback.wait(resultData2, 5)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Key \"" .. itemName .. "\" was not placed onto keyring: " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- if
end -- if
-- Vault
elseif (objLoc == invItemLocVault) then
dbot.error("inv.items.identifyItem: Identifying objects in a vault is not yet supported")
retval = DRL_RET_UNSUPPORTED
-- Auction (we may temporarily add an auction item to examine it)
elseif (objLoc == invItemLocAuction) then
command = idCommand .. " " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, command)
else
retval = dbot.execute.safe.command(command, inv.items.identifyItemSetupFn, nil,
dbot.callback.default, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Auction identify request did not complete: " .. dbot.retval.getString(retval))
end -- if
end -- if
end -- if
-- Shopkeeper (we may temporarily add an item from a shop to examine it)
elseif (objLoc == invItemLocShopkeeper) then
if (commandArray ~= nil) then
table.insert(commandArray, idCommand)
else
retval = dbot.execute.safe.command(idCommand, inv.items.identifyItemSetupFn, nil,
dbot.callback.default, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Shop identify request did not complete: " .. dbot.retval.getString(retval))
end -- if
end -- if
end -- if
-- Container
elseif (type(objLoc) == "number") then
-- Any objLoc that is a number is a container and the objLoc is the container's ID.
retval = inv.items.getItem(objId, commandArray)
if (retval == DRL_RET_SUCCESS) then
if (inv.items.identifyPkg ~= nil) then
command = idCommand .. " " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, command)
else
retval = dbot.execute.safe.command(command, inv.items.identifyItemSetupFn, nil,
dbot.callback.default, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Container identify request did not complete: " .. dbot.retval.getString(retval))
end -- if
end -- if
end -- if
end -- if
retval = inv.items.putItem(objId, objLoc, commandArray, false)
end -- if
-- Worn
else
retval = inv.items.getItem(objId, commandArray)
if (retval == DRL_RET_SUCCESS) then
if (inv.items.identifyPkg ~= nil) then
command = idCommand .. " " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, command)
else
retval = dbot.execute.safe.command(command, inv.items.identifyItemSetupFn, nil,
dbot.callback.default, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Item identify request did not complete: " .. dbot.retval.getString(retval))
end -- if
end -- if
end -- if
end -- if
retval = inv.items.wearItem(objId, objLoc, commandArray, false)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.identifyItem: Failed to wear item " .. (objId or "nil") .. ": " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- if
-- Stop the triggers if something went wrong with the identification
if (commandArray == nil) then
if (retval ~= DRL_RET_SUCCESS) then
inv.items.trigger.itemIdEnd()
end -- if
end -- if
return retval
end -- inv.items.identifyItem
function inv.items.identifyItemSetupFn()
EnableTrigger(inv.items.trigger.itemIdStartName, true)
end -- inv.items.identifyItemSetupFn
function inv.items.identifyAtomicSetup()
EnableTrigger(inv.items.trigger.wearName, true)
EnableTrigger(inv.items.trigger.removeName, true)
EnableTrigger(inv.items.trigger.getName, true)
EnableTrigger(inv.items.trigger.putName, true)
EnableTrigger(inv.items.trigger.getKeyringName, true)
EnableTrigger(inv.items.trigger.putKeyringName, true)
EnableTrigger(inv.items.trigger.wearSpecialName, true)
EnableTrigger(inv.items.trigger.itemIdStartName, true)
end -- inv.items.identifyAtomicSetup
function inv.items.identifyAtomicCleanup(resultData, retval)
EnableTrigger(inv.items.trigger.wearName, false)
EnableTrigger(inv.items.trigger.removeName, false)
EnableTrigger(inv.items.trigger.getName, false)
EnableTrigger(inv.items.trigger.putName, false)
EnableTrigger(inv.items.trigger.getKeyringName, false)
EnableTrigger(inv.items.trigger.putKeyringName, false)
EnableTrigger(inv.items.trigger.wearSpecialName, false)
inv.items.trigger.itemIdEnd() -- clears itemIdStartName and others...
dbot.callback.default(resultData, retval)
end -- inv.items.identifyAtomicCleanup
invStateIdle = "idle"
invStateRunning = "running"
invStatePaused = "paused"
invStateHalted = "halted"
inv.items.refreshPkg = nil
inv.items.fullScanCompleted = false
function inv.items.refresh(maxNumItems, refreshLocations, endTag, tagProxy)
local retval = DRL_RET_SUCCESS
local tagModule = invTagsRefresh
if (tagProxy ~= nil) and (tagProxy ~= "") then
tagModule = tagProxy
end -- if
dbot.debug("inv.items.refresh: #items=" .. (maxNumItems or "nil") .. ", locs=\"" ..
(refreshLocations or "nil") .. "\", endTag=\"" .. (endTag or "nil") .. "\"")
if (dbot.gmcp.isInitialized == false) then
dbot.info("Skipping refresh request: GMCP is not yet initialized")
return inv.tags.stop(tagModule, endTag, DRL_RET_UNINITIALIZED)
end -- if
local charState = dbot.gmcp.getState()
-- We want a user to run the build operation at least once before we allow
-- the normal refresh to occur. We don't necessarily even need to complete
-- the build. The main concern is that we don't want an auto-refresh to
-- clog up the system for several minutes without warning the first time the
-- user enables this plugin.
if (inv.config.table.isBuildExecuted == false) then
dbot.print(
[[@W
You must perform at least one manual inventory build before we allow inventory refresh
requests to proceed. Otherwise, a user's first automatic refresh could clog up the system
for several minutes as the entire inventory is scanned. We don't want the user to be
surprised by that behavior.
]])
dbot.print("@W Please see \"@G" .. pluginNameCmd .. " help build@W\" for more details.")
dbot.print("@W\nUsage:")
inv.cli.build.usage()
dbot.print("")
retval = DRL_RET_UNINITIALIZED
-- If refreshes are enabled (period > 0) but are paused (state == invStatePaused) we skip
-- the refresh now but we still schedule the next one
elseif (inv.state == invStatePaused) then
dbot.debug("Skipping refresh request: automatic refreshes are paused")
retval = DRL_RET_BUSY
-- If another refresh is in progress, try again later
elseif (inv.state == invStateRunning) then
dbot.note("Skipping refresh request: another refresh is in progress")
retval = DRL_RET_BUSY
elseif (inv.state == invStateHalted) then
dbot.info("Skipping refresh request: plugin is halted")
retval = DRL_RET_UNINITIALIZED
-- If we aren't in the "active" character state (sleeping, running, AFK, writing a note, etc.)
-- then we wait a bit and try again
elseif (charState ~= dbot.stateActive) then
dbot.debug("Skipping refresh request: char is in state \"" .. dbot.gmcp.getStateString(charState) .. "\"")
retval = DRL_RET_NOT_ACTIVE
-- If the char is in the active state (e.g., not AFK, in a note, in combat, etc.) refresh now
-- and schedule the next refresh after the default period
else
inv.state = invStateRunning
inv.items.refreshPkg = {}
inv.items.refreshPkg.maxNumItems = maxNumItems
inv.items.refreshPkg.refreshLocations = refreshLocations
inv.items.refreshPkg.endTag = endTag
inv.items.refreshPkg.tagModule = tagModule
-- If we haven't performed a full scan yet since we initialized the plugin, make this a
-- full scan. We want to run at least one full scan so that we handle orphan equipment
-- and detect if the user moved stuff around in another client or in this client when the
-- plugin was disabled.
if (not inv.items.fullScanCompleted) then
inv.items.refreshPkg.refreshLocations = invItemsRefreshLocAll
end -- if
wait.make(inv.items.refreshCR)
retval = DRL_RET_SUCCESS
end -- if
-- Schedule the next refresh if automatic refreshes are enabled (i.e., the period is > 0 minutes)
local refreshMin = inv.items.refreshGetPeriods() or 0
if (refreshMin > 0) and (inv.state ~= nil) then
dbot.debug("Scheduling automatic inventory refresh in " .. refreshMin .. " minutes")
inv.items.refreshAtTime(refreshMin, 0)
end -- if
-- If everything went as planned, we have a co-routine doing a refresh and that co-routine will
-- terminate any end tags that were specified. Otherwise, we hit an error and we should terminate
-- the tag now and return what we know to the caller.
if (retval == DRL_RET_SUCCESS) then
return retval
else
return inv.tags.stop(tagModule, endTag, retval)
end -- if
end -- inv.items.refresh
function inv.items.refreshCR()
local retval = DRL_RET_SUCCESS
-- We can skip the refresh if we've already done a full scan, there are no known "dirty"
-- locations or containers, and the user didn't explicitly request a full scan
if inv.items.fullScanCompleted and
(inv.items.refreshPkg.refreshLocations ~= invItemsRefreshLocAll) and
(not inv.items.isDirty()) then
dbot.debug("Skipping refresh because there are no known unidentified items")
inv.state = invStateIdle
return inv.tags.stop(inv.items.refreshPkg.tagModule, inv.items.refreshPkg.endTag, retval)
end -- if
dbot.note("Refreshing inventory: START")
-- Disable the prompt to avoid confusing output during the refresh
dbot.prompt.hide()
-- On each refresh request we track all items discovered and match that against the contents
-- of the inventory table. If something is in the inventory table and we didn't find it
-- during refresh, remove it from the inventory table because the table is out of sync.
inv.items.currentItems = {}
-- Discover and identify new inventory items. Both co-routines are blocking.
retval = inv.items.discoverCR(inv.items.refreshPkg.maxNumItems, inv.items.refreshPkg.refreshLocations)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("Skipping item discovery: " .. dbot.retval.getString(retval))
else -- discovery passed :)
-- Identify everything we just discovered
retval = inv.items.identifyCR(inv.items.refreshPkg.maxNumItems, inv.items.refreshPkg.refreshLocations)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("Skipping item identification: " .. dbot.retval.getString(retval))
end -- if
end -- if
-- Remove any items that are in the inventory table that were not discovered during this
-- refresh. It's possible that things are out of sync (e.g., the table wasn't saved after
-- a change to the inventory). Of course, if the user halted the current search then we
-- may not have found items that actually are present so we only remove orphans if we
-- fully completed the discovery and identification steps above. Also, if we are only
-- scanning some locations, then we don't want to remove orphans because the items could
-- be at a location we didn't scan this time.
if (retval == DRL_RET_SUCCESS) and (inv.items.refreshPkg.refreshLocations == invItemsRefreshLocAll) then
for k,v in pairs(inv.items.table) do
if (inv.items.currentItems[k] == nil) then
dbot.note("Removed orphan: \"" .. (inv.items.getField(k, invFieldColorName) or "Unidentified") ..
DRL_ANSI_WHITE .. "\" (" .. k .. ")")
inv.items.table[k] = nil
end -- if
end -- for
end -- if
inv.items.currentItems = nil
-- Re-enable the prompt
dbot.prompt.show()
-- Save everything we just discovered and identified
inv.items.save()
inv.config.save()
if (retval == DRL_RET_SUCCESS) then
resultString = "SUCCESS! (Entire inventory is identified)"
elseif (retval == DRL_RET_HALTED) then
resultString = "HALTED! (Some items may still need identification)"
elseif (retval == DRL_RET_IN_COMBAT) then
resultString = "IN COMBAT! (Skipped identification because you were fighting!)"
elseif (retval == DRL_RET_TIMEOUT) then
resultString = "TIMEOUT! (Skipped identification because you were busy)"
elseif (retval == DRL_RET_NOT_ACTIVE) then
resultString = "NOT ACTIVE! (You were not ready for item identification)"
elseif (retval == DRL_RET_UNINITIALIZED) then
resultString = "UNINITIALIZED! (The plugin is not initialized)"
else
resultString = "ERROR! (" .. dbot.retval.getString(retval) .. ")"
end -- if
dbot.note("Refreshing inventory: " .. resultString)
inv.state = invStateIdle
-- We want at least one full scan after the plugin loads. If we've successfully completed a full
-- scan, remember it so that we don't need to do it again until the plugin reloads.
if (retval == DRL_RET_SUCCESS) and (inv.items.refreshPkg.refreshLocations == invItemsRefreshLocAll) then
inv.items.fullScanCompleted = true
end -- if
return inv.tags.stop(inv.items.refreshPkg.tagModule, inv.items.refreshPkg.endTag, retval)
end -- inv.items.refreshCR
-- Kick the refresh timer to start at a time "min" minutes and "sec" seconds from the time this is called
function inv.items.refreshAtTime(min, sec)
if (min == nil) or (sec == nil) then
dbot.warn("inv.items.refreshAtTime: nil time given as parameter")
return DRL_RET_INVALID_PARAMETER
end -- if
min = tonumber(min) or 0
sec = tonumber(sec) or 0
if (min == 0) and (sec == 0) then
dbot.warn("inv.items.refreshAtTime: invalid time period 0 detected")
return DRL_RET_INVALID_PARAMETER
end -- if
-- It's possible (but highly unlikely) that we timed out and disconnected from the mud causing
-- us to de-init our modules at the exact instant we schedule the next refresh. If that happens
-- then our state will be nil and we shouldn't continue with the refresh.
if (inv.state == nil) then
dbot.warn("inv.items.refreshAtTime: inventory module is not initialized")
return DRL_RET_UNINITIALIZED
end -- if
-- We can't add the timer directly because we are in the function called by that timer. Instead,
-- we use an intermediate timer to call back and start this timer again. Yeah, it's a bit ugly...
DoAfterSpecial(0.1, -- start up in 100 ms
string.format("AddTimer(%s, 0, %d, %d, \"\", %d, " ..
"\"inv.items.refreshDefault\")",
"inv.items.timer.refreshName", min, sec,
timer_flag.Enabled + timer_flag.Replace + timer_flag.OneShot),
sendto.script)
return DRL_RET_SUCCESS
end -- inv.items.refreshAtTime
function inv.items.refreshDefault()
local retval = DRL_RET_SUCCESS
if (inv.state == nil) then
dbot.warn("inv.items.refreshDefault: inventory module is not initialized")
return DRL_RET_UNINITIALIZED
end -- if
-- By default, refresh only dirty locations and skip item locations that don't contain any
-- unidentified items.
if (inv.items.refreshGetPeriods() > 0) then
retval = inv.items.refresh(0, invItemsRefreshLocDirty, nil, nil)
end -- if
return retval
end -- inv.items.refreshDefault
function inv.items.refreshGetPeriods()
return inv.config.table.refreshPeriod, inv.config.table.refreshEagerSec
end -- inv.items.refreshGetPeriods
function inv.items.refreshSetPeriods(autoMin, eagerSec)
inv.config.table.refreshPeriod = tonumber(autoMin) or inv.items.timer.refreshMin
inv.config.table.refreshEagerSec = tonumber(eagerSec) or inv.items.timer.refreshEagerSec
return inv.config.save()
end -- inv.items.refreshSetPeriods
function inv.items.refreshOn(autoMin, eagerSec)
autoMin = tonumber(autoMin or "") or inv.items.timer.refreshMin
if (autoMin < 1) then
dbot.warn("inv.items.refreshOn: Automatic refreshes must have a period of at least one minute")
return DRL_RET_INVALID_PARAM
end -- if
inv.items.refreshSetPeriods(autoMin, eagerSec or 0)
inv.state = invStateIdle
-- Schedule the next refresh
return inv.items.refreshAtTime(autoMin, 0)
end -- inv.items.refreshOn
function inv.items.refreshOff()
inv.state = invStatePaused
dbot.deleteTimer(inv.items.timer.refreshName)
return inv.items.refreshSetPeriods(0, 0)
end -- inv.items.refreshOff
-- This checks all locations and determines if there are any known unidentified items. If there
-- is at least one unidentified item, isDirty() returns true. Otherwise, it returns false.
function inv.items.isDirty()
local isDirty = false
-- Check the easy locations first. If something unidentified is worn, on your keyring, or in
-- your main inventory, return true. We don't even need to look at containers.
if (inv.items.wornState == invItemsRefreshDirty) or
(inv.items.mainState == invItemsRefreshDirty) or
(inv.items.keyringState == invItemsRefreshDirty) then
isDirty = true
-- Check containers to see if any are "dirty" and hold at least one unidentified item
else
-- For every item in your inventory, check if it's a container. If it is a container we must
-- next check if it has the "clean" keyword indicating that it hasn't had any unidentified
-- items added to it since its last scan. If it's not "clean", then it's "dirty".
for objId,_ in pairs(inv.items.table) do
if (inv.items.getStatField(objId, invStatFieldType) == invmon.typeStr[invmonTypeContainer]) then
local keywordField = inv.items.getStatField(objId, invStatFieldKeywords) or ""
if (not dbot.isWordInString(invItemsRefreshClean, keywordField)) then
isDirty = true
break
end -- if
end -- if
end -- for
end -- if
return isDirty
end -- inv.items.isDirty
function inv.items.build(endTag)
local retval
retval = inv.items.reset()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.build: inventory reset failed: " .. dbot.retval.getString(retval))
return inv.tags.stop(invTagsBuild, endTag, retval)
end -- if
retval = inv.config.reset()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.build: configuration reset failed: " .. dbot.retval.getString(retval))
return inv.tags.stop(invTagsBuild, endTag, retval)
end -- if
retval = inv.cache.reset()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.build: reset of all caches failed: " .. dbot.retval.getString(retval))
return inv.tags.stop(invTagsBuild, endTag, retval)
end -- if
inv.config.table.isBuildExecuted = true
inv.state = invStateIdle
-- The call to refresh is a little unusual in that we pass the build endTag to the refresh
-- code. When the refresh code completes, it will output the endTag it received from build
-- instead of the normal refresh endTag. We do this to avoid the need to spin here and wait
-- for refresh to complete before we output the build endTag indicating the build is done.
retval = inv.items.refresh(0, invItemsRefreshLocAll, endTag, invTagsBuild)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.build: refresh did not complete: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.items.build()
----------------------------------------------------------------------------------------------------
-- inv.items.get(query, endTag) -- non-blocking, kicks off blocking inv.items.getCR asynchronously
-- inv.items.getCR() -- blocks until commands to move all matching items are queued, executed, and confirmed
-- inv.items.getItem(itemId) -- blocks until the command to get the item is queued, executed, and confirmed
--
-- inv.items.getSetupFn()
-- inv.items.getResultFn(resultData, retval)
--
-- inv.items.getKeyringSetupFn()
-- inv.items.getKeyringResultFn(resultData, retval)
--
-- Move item(s) to main inventory
--
-- Suppress get verbage:
-- "You remove ..." -- remove item
-- "You stop wielding ..." -- remove weapon
-- "You stop using ..." -- remove portal
-- "... stops floating around you." -- remove float
-- "... stops floating above you." -- remove aura of trivia
-- "You are not wearing that item." -- remove BADNAME
-- "You get ... from ..." -- get item container
-- "You do not see that in ..." -- get BADNAME container
----------------------------------------------------------------------------------------------------
inv.items.getPkg = nil
function inv.items.get(queryString, endTag)
if (queryString == nil) then
dbot.warn("inv.items.get: query is nil")
return inv.tags.stop(invTagsGet, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.items.getPkg ~= nil) then
dbot.info("Skipping get request for query \"" .. queryString .. "\", another get request is in progress")
return inv.tags.stop(invTagsGet, endTag, DRL_RET_BUSY)
end -- if
-- We use a background co-routine to perform the "get". The co-routine can schedule
-- itself and block until the get completes.
inv.items.getPkg = {}
inv.items.getPkg.queryString = queryString or ""
inv.items.getPkg.endTag = endTag
wait.make(inv.items.getCR)
return DRL_RET_SUCCESS
end -- inv.items.get
function inv.items.getCR()
local retval = DRL_RET_SUCCESS
local idArray
-- Be paranoid!
if (inv.items.getPkg == nil) or (inv.items.getPkg.queryString == nil) then
dbot.error("inv.items.getCR: Aborting get request -- get package or query is nil!")
inv.items.getPkg = nil
return DRL_RET_INTERNAL_ERROR
end -- if
local endTag = inv.items.getPkg.endTag
-- Get an array of object IDs that match the get request's query string
idArray, retval = inv.items.searchCR(inv.items.getPkg.queryString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.getCR: failed to search inventory table: " .. dbot.retval.getString(retval))
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("No match found for get query: \"" .. inv.items.getPkg.queryString .. "\"")
retval = DRL_RET_MISSING_ENTRY
-- We found items to move!
else
local commandArray = dbot.execute.new()
local numItemsMoved = 0
for _,id in ipairs(idArray) do
retval = inv.items.getItem(id, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("Skipping request to get item " .. id .. ": " .. dbot.retval.getString(retval))
break
else
numItemsMoved = numItemsMoved + 1
end -- if
if (commandArray ~= nil) and (#commandArray >= inv.items.burstSize) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to get items: " .. dbot.retval.getString(retval))
break
end -- if
commandArray = dbot.execute.new()
end -- if
end -- for
-- Flush any commands in the array that still need to be sent to the mud
if (retval == DRL_RET_SUCCESS) and (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to get items: " .. dbot.retval.getString(retval))
end -- if
end -- if
if (retval == DRL_RET_SUCCESS) then
dbot.info("Get request matched " .. numItemsMoved .. " items")
end -- if
end -- if
inv.items.getPkg = nil
return inv.tags.stop(invTagsGet, endTag, retval)
end -- inv.items.getCR
function inv.items.getItem(objId, commandArray)
local retval = DRL_RET_SUCCESS
if (objId == nil) or (type(objId) ~= "number") then
dbot.warn("inv.items.getItem: Non-numeric objId parameter detected")
return DRL_RET_INVALID_PARAM
end -- if
local itemLoc = inv.items.getField(objId, invFieldObjLoc)
local itemName = inv.items.getField(objId, invFieldColorName) or "Unidentified"
itemName = itemName .. "@W" .. DRL_ANSI_WHITE
if (commandArray == nil) then
dbot.prompt.hide()
end -- if
if (itemLoc == nil) then
dbot.error("inv.items.getItem: item location for objId " .. objId .. " is missing")
retval = DRL_RET_INTERNAL_ERROR
else
local itemLocNum = tonumber(itemLoc)
-- If the location is a number, it is a container's ID
if (itemLocNum ~= nil) then
local containerName = (inv.items.getField(itemLocNum, invFieldColorName) or "Unidentified") ..
"@W" .. DRL_ANSI_WHITE
local containerLoc
-- It's possible that the container is a worn item. If that is the case, we must first
-- remove the container before we can get something out of it.
if (inv.items.isWorn(itemLocNum)) then
containerLoc = inv.items.getField(itemLocNum, invFieldObjLoc) or ""
retval = inv.items.removeItem(itemLocNum, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.getItem: Failed to remove container \"" .. containerName .. "\"")
end -- if
end -- if
local getCommand = "get " .. objId .. " " .. itemLocNum
if (commandArray ~= nil) then
table.insert(commandArray, getCommand)
else
dbot.note(" Getting \"" .. itemName .. "\" from \"" .. containerName .. "\"")
-- Get the item and wait for confirmation that it moved
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command(getCommand, inv.items.getSetupFn, nil,
inv.items.getResultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to get \"" .. itemName .. "\": " .. dbot.retval.getString(retval))
else
-- Wait until we have confirmation that the callback completed
retval = dbot.callback.wait(resultData, 5)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to get \"" .. itemName .. "\": " .. dbot.retval.getString(retval))
end -- if
end -- if
-- Verify that the item is now in the main inventory. It may take a moment or two for invmon
-- to realize we moved the item. We spin a bit here to give invmon the chance to update the
-- item's location.
local totTime = 0
local timeout = 1
while (invItemLocInventory ~= inv.items.getField(objId, invFieldObjLoc)) do
if (totTime > timeout) then
if inv.items.isInvis(objId) then
dbot.info("Failed to get invisible item \"" .. itemName .. "\" from container \"" ..
containerName .. "\": can you detect invis?")
elseif inv.items.isInvis(itemLocNum) then
dbot.info("Failed to get \"" .. itemName .. "\" from invisible container \"" ..
containerName .. "\": can you detect invis?")
else
dbot.warn("inv.items.getItem: Timed out before invmon confirmed item is in target container")
end -- if
retval = DRL_RET_MISSING_ENTRY
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
end -- if
-- If we pulled the item out of a worn container, we must remember to re-wear the container
-- now that we took the item out of it
if (containerLoc ~= nil) then
local containerRetval = inv.items.wearItem(itemLocNum, nil, commandArray, false)
if (containerRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.getItem: Failed to wear item " .. (objId or "nil") .. ": " ..
dbot.retval.getString(containerRetval))
-- If we haven't hit another error yet, store the current error code
if (retval == DRL_RET_SUCCESS) then
retval = containerRetval
end -- if
end -- if
end -- if
elseif (itemLoc == invItemLocInventory) then
dbot.debug("Item \"" .. itemName .. "\" is already in your main inventory")
elseif (itemLoc == invItemLocKeyring) then
local getCommand = "keyring get " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, getCommand)
else
dbot.note(" Getting \"" .. itemName .. "\" from keyring")
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command(getCommand, inv.items.getKeyringSetupFn, nil,
inv.items.getKeyringResultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.getItem: Failed to get keyring item: " .. dbot.retval.getString(retval))
else
-- Wait until we have confirmation that the callback completed
retval = dbot.callback.wait(resultData, 5)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to get key \"" .. itemName .. "\" from keyring: " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- if
elseif (itemLoc == invItemLocUninitialized) or
(itemLoc == invItemLocWorn) or
(itemLoc == invItemLocVault) then
dbot.error("inv.items.getItem: We do not yet support uninitialized items or getting items from a vault")
retval = DRL_RET_UNSUPPORTED
-- The location is a string representing a wearable location
else
dbot.debug("Removing item \"" .. itemName .. "\" worn at location " .. itemLoc)
-- Remove the item and wait for confirmation that it moved
retval = inv.items.removeItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("Skipping removal of \"" .. itemName .. "\" worn at location " ..
itemLoc .. ": " .. dbot.retval.getString(retval))
end -- if
end -- if
end -- if
if (commandArray == nil) then
dbot.prompt.show()
end -- if
return retval
end -- inv.items.getItem
function inv.items.getSetupFn()
EnableTrigger(inv.items.trigger.getName, true)
end -- inv.items.getSetupFn
function inv.items.getResultFn(resultData, retval)
EnableTrigger(inv.items.trigger.getName, false)
dbot.callback.default(resultData, retval)
end -- inv.items.getResultFn
function inv.items.getKeyringSetupFn()
EnableTrigger(inv.items.trigger.getKeyringName, true)
end -- inv.items.getKeyringSetupFn
function inv.items.getKeyringResultFn(resultData, retval)
EnableTrigger(inv.items.trigger.getKeyringName, false)
dbot.callback.default(resultData, retval)
end -- inv.items.getKeyringResultFn
----------------------------------------------------------------------------------------------------
-- inv.items.put(container, query, endTag) -- non-blocking, kicks off blocking inv.items.putCR asynchronously
-- inv.items.putCR() -- blocks until all items are confirmed to be moved
-- inv.items.putItem(objId, containerId, commandArray, doCheckLocation) -- blocks until the put is done
--
-- inv.items.putSetupFn()
-- inv.items.putResultFn(resultData)
--
-- inv.items.putKeyringSetupFn()
-- inv.items.putKeyringResultFn(resultData)
-- Move item(s) to container
--
-- Suppress put verbage:
-- "You don't have that." -- put BADNAME container
-- "You do not see a[n] ... here." -- put item BADNAME
-- "You put ... into ..." -- put item bag
----------------------------------------------------------------------------------------------------
inv.items.putPkg = nil
function inv.items.put(container, queryString, endTag)
if (container == nil) or (container == "") then
dbot.warn("inv.items.put: missing container parameter")
return inv.tags.stop(invTagsPut, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (queryString == nil) then
dbot.warn("inv.items.put: query is nil")
return inv.tags.stop(invTagsPut, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.items.putPkg ~= nil) then
dbot.info("Skipping put request for query \"" .. queryString .. "\", another put request is in progress")
return inv.tags.stop(invTagsPut, endTag, DRL_RET_BUSY)
end -- if
-- We use a background co-routine to perform the "put". The co-routine can schedule
-- itself and block until the put completes.
inv.items.putPkg = {}
inv.items.putPkg.container = container
inv.items.putPkg.queryString = queryString or ""
inv.items.putPkg.endTag = endTag
wait.make(inv.items.putCR)
return DRL_RET_SUCCESS
end -- inv.items.put
function inv.items.putCR()
local retval = DRL_RET_SUCCESS
local idArray
local containerId
-- Be paranoid!
if (inv.items.putPkg == nil) or (inv.items.putPkg.container == nil) or
(inv.items.putPkg.queryString == nil) then
dbot.error("inv.items.putCR: Aborting put request -- put package, container or query is nil!")
inv.items.putPkg = nil
return DRL_RET_INTERNAL_ERROR
end -- if
local endTag = inv.items.putPkg.endTag
-- Determine the object ID for the target container. If the container parameter is a number
-- then we treat it as the container's object ID. Otherwise, we treat it as a relative name
-- (e.g., "3.bag") and find the container's object ID based on the relative name.
containerId = tonumber(inv.items.putPkg.container)
if (containerId == nil) then
_, containerId, retval = inv.items.convertRelative(invQueryKeyRelativeName, inv.items.putPkg.container)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.putCR: Failed to convert relative name \"" .. inv.items.putPkg.container ..
"\" to object ID: " .. dbot.retval.getString(retval))
inv.items.putPkg = nil
return inv.tags.stop(invTagsPut, endTag, retval)
end -- if
containerId = tonumber(containerId)
if (containerId == nil) then
dbot.warn("inv.items.putCR: Container \"" .. inv.items.putPkg.container ..
"\" resolved to a non-numeric object ID -- aborting put request")
inv.items.putPkg = nil
return inv.tags.stop(invTagsPut, endTag, DRL_RET_INTERNAL_ERROR)
end -- if
end -- if
local containerName = (inv.items.getField(containerId, invFieldColorName) or "Unidentified") ..
"@W" .. DRL_ANSI_WHITE
-- Get an array of object IDs that match the put request's query string
idArray, retval = inv.items.searchCR(inv.items.putPkg.queryString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.putCR: failed to search inventory table: " .. dbot.retval.getString(retval))
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("No match found for put query: \"" .. inv.items.putPkg.queryString .. "\"")
retval = DRL_RET_MISSING_ENTRY
-- We found items to move!
else
local commandArray = dbot.execute.new()
local numItemsMoved = 0
for _,objId in ipairs(idArray) do
retval = inv.items.putItem(objId, containerId, commandArray, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("Skipping request to put item " .. objId .. " in container \"" ..
containerName .. "\" (" .. containerId .. "): " .. dbot.retval.getString(retval))
break
else
numItemsMoved = numItemsMoved + 1
end -- if
if (commandArray ~= nil) then
if (#commandArray >= inv.items.burstSize) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to put items: " .. dbot.retval.getString(retval))
break
end -- if
commandArray = dbot.execute.new()
end -- if
end -- if
end -- for
-- Flush any commands in the array that still need to be sent to the mud
if (retval == DRL_RET_SUCCESS) and (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to get items: " .. dbot.retval.getString(retval))
end -- if
end -- if
if (retval == DRL_RET_SUCCESS) then
dbot.info("Put request matched " .. numItemsMoved .. " items")
end -- if
end -- if
inv.items.putPkg = nil
return inv.tags.stop(invTagsPut, endTag, retval)
end -- inv.items.putCR
function inv.items.putItem(objId, containerId, commandArray, doCheckLocation)
local retval = DRL_RET_SUCCESS
-- Parameter paranoia isn't necessarily a bad thing...
if (objId == nil) or (type(objId) ~= "number") then
dbot.warn("inv.items.putItem: Non-numeric objId parameter detected")
return DRL_RET_INVALID_PARAM
end -- if
if (containerId == nil) or (type(containerId) ~= "number") then
dbot.warn("inv.items.putItem: Non-numeric containerId parameter detected")
return DRL_RET_INVALID_PARAM
end -- if
-- Get the name of the target items. This is convenient for debug, warning, and error messages.
local itemName = (inv.items.getField(objId, invFieldColorName) or "Unidentified") ..
"@W" .. DRL_ANSI_WHITE
-- The target container may not be in our inventory (it might be on the floor or might
-- be furniture in a room).
local containerName = (inv.items.getField(containerId, invFieldColorName) or "Room container") ..
"@W" .. DRL_ANSI_WHITE
local isRoomContainer = false
if (inv.items.getEntry(containerId) == nil) then
isRoomContainer = true
end -- if
local containerLoc
-- If the item is not already in our main inventory, get it and put it in the main inventory
local itemLoc = inv.items.getField(objId, invFieldObjLoc)
if (itemLoc == nil) then
dbot.error("inv.items.putItem: item location for objId " .. objId .. " is missing")
return DRL_RET_INTERNAL_ERROR
elseif (itemLoc == containerId) then
if (commandArray == nil) or doCheckLocation then
dbot.note("Item \"" .. itemName .. "\" is already in container \"" .. containerName .. "\"")
return DRL_RET_SUCCESS
else
if (inv.items.isWorn(containerId)) then
containerLoc = inv.items.getField(containerId, invFieldObjLoc) or ""
table.insert(commandArray, "remove " .. containerId)
end -- if
table.insert(commandArray, "get " .. objId .. " " .. containerId)
end -- if
elseif (itemLoc ~= invItemLocInventory) then
-- It's possible that the container is a worn item. If that is the case, we must first
-- remove the container before we can put something into it.
if (inv.items.isWorn(containerId)) then
containerLoc = inv.items.getField(containerId, invFieldObjLoc) or ""
retval = inv.items.removeItem(containerId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to remove worn container \"" .. containerName ..
"\": " .. dbot.retval.getString(retval))
end -- if
end -- if
retval = inv.items.getItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to move item " .. objId .. " to main inventory: " ..
dbot.retval.getString(retval))
return retval
end -- if
end -- if
local putCommand = "put " .. objId .. " " .. containerId
if (commandArray ~= nil) then
table.insert(commandArray, putCommand)
else
-- We have the item and we know the containerId that should hold the item. Move it and wait
-- for confirmation that the move completed.
dbot.note(" Putting \"" .. itemName .. "\" into \"" .. containerName .. "\"")
dbot.prompt.hide()
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command(putCommand, inv.items.putSetupFn, nil,
inv.items.putResultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to put \"" .. itemName .. "\": " .. dbot.retval.getString(retval))
else
-- Wait until we have confirmation that the callback completed
retval = dbot.callback.wait(resultData, 5)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to put \"" .. itemName .. "\": " .. dbot.retval.getString(retval))
end -- if
end -- if
-- Confirm that the item is now in the target container (unless we aren't holding the container)
if (retval == DRL_RET_SUCCESS) and (isRoomContainer == false) then
local totTime = 0
local timeout = 1
while (containerId ~= inv.items.getField(objId, invFieldObjLoc)) do
if (totTime > timeout) then
if inv.items.isInvis(objId) then
dbot.info("Failed to put invisible item \"" .. itemName .. "\" into container \"" ..
containerName .. "\": can you detect invis?")
elseif inv.items.isInvis(containerId) then
dbot.info("Failed to put \"" .. itemName .. "\" into invisible container \"" ..
containerName .. "\": can you detect invis?")
else
dbot.warn("inv.items.putItem: Timed out before invmon confirmed item is in target container")
end -- if
retval = DRL_RET_TIMEOUT
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
end -- if
dbot.prompt.show()
end -- if
-- If we put the item into a worn container, we must remember to re-wear the container
if (containerLoc ~= nil) then
retval = inv.items.wearItem(containerId, nil, commandArray, doCheckLocation)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.putItem: Failed to wear container " .. (containerId or "nil") .. ": " ..
dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.items.putItem
function inv.items.putSetupFn()
EnableTrigger(inv.items.trigger.putName, true)
end -- inv.items.putSetupFn
function inv.items.putResultFn(resultData, retval)
EnableTrigger(inv.items.trigger.putName, false)
dbot.callback.default(resultData, retval)
end -- inv.items.putResultFn
function inv.items.putKeyringSetupFn()
EnableTrigger(inv.items.trigger.putKeyringName, true)
end -- inv.items.putKeyringSetupFn
function inv.items.putKeyringResultFn(resultData, retval)
EnableTrigger(inv.items.trigger.putKeyringName, false)
dbot.callback.default(resultData, retval)
end -- inv.items.putKeyringResultFn
----------------------------------------------------------------------------------------------------
-- inv.items.store(query, endTag) -- non-blocking, kicks off blocking inv.items.putCR asynchronously
-- inv.items.storeCR() -- blocks until all items are confirmed to be moved
-- inv.items.storeItem(itemId) -- blocks until the store commands are executed and confirmed
--
-- Move each item that matches the query string into the item's "home" container. An item's
-- home container is the most recent container to hold the item. Tracking this lets us wear
-- an item (maybe as part of a set) and then "store" the item back where we got it automagically.
-- You can change an item's home container simply by moving it to a different container. Easy!
----------------------------------------------------------------------------------------------------
inv.items.storePkg = nil
function inv.items.store(queryString, endTag)
if (queryString == nil) then
dbot.warn("inv.items.store: query is nil")
inv.tags.stop(invTagsStore, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.items.storePkg ~= nil) then
dbot.info("Skipping store request for query \"" .. queryString ..
"\", another store request is in progress")
inv.tags.stop(invTagsStore, endTag, DRL_RET_BUSY)
end -- if
-- We use a background co-routine to perform the "store". The co-routine can schedule
-- itself and block until the store completes.
inv.items.storePkg = {}
inv.items.storePkg.queryString = queryString or ""
inv.items.storePkg.endTag = endTag
wait.make(inv.items.storeCR)
return DRL_RET_SUCCESS
end -- inv.items.store(queryString)
function inv.items.storeCR()
local retval = DRL_RET_SUCCESS
local idArray
-- Be paranoid!
if (inv.items.storePkg == nil) or (inv.items.storePkg.queryString == nil) then
dbot.error("inv.items.storeCR: Aborting store request -- store package or query is nil!")
inv.items.storePkg = nil
return DRL_RET_INTERNAL_ERROR
end -- if
local endTag = inv.items.storePkg.endTag
-- Get an array of object IDs that match the store request's query string
idArray, retval = inv.items.searchCR(inv.items.storePkg.queryString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.storeCR: failed to search inventory table: " .. dbot.retval.getString(retval))
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("No match found for store query: \"" .. inv.items.storePkg.queryString .. "\"")
retval = DRL_RET_MISSING_ENTRY
-- We found items to store!
else
local commandArray = dbot.execute.new()
local numItemsMoved = 0
for _,objId in ipairs(idArray) do
retval = inv.items.storeItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to store item " .. objId .. ": " .. dbot.retval.getString(retval))
else
numItemsMoved = numItemsMoved + 1
end -- if
if (commandArray ~= nil) then
if (#commandArray >= inv.items.burstSize) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to store items: " .. dbot.retval.getString(retval))
break
end -- if
commandArray = dbot.execute.new()
end -- if
end -- if
end -- for
-- Flush any commands in the array that still need to be sent to the mud
if (retval == DRL_RET_SUCCESS) and (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to store items: " .. dbot.retval.getString(retval))
end -- if
end -- if
if (retval == DRL_RET_SUCCESS) then
dbot.info("Store request matched " .. numItemsMoved .. " items")
end -- if
end -- if
inv.items.storePkg = nil
inv.tags.stop(invTagsStore, endTag, retval)
end -- inv.items.storeCR
function inv.items.storeItem(objId, commandArray)
local retval
local homeLoc = tonumber(inv.items.getField(objId, invFieldHomeContainer) or "none")
-- If this item doesn't have a home container, put it in the main inventory instead
if (homeLoc == nil) then
retval = inv.items.getItem(objId, commandArray)
else
retval = inv.items.putItem(objId, homeLoc, commandArray, true)
end -- if
return retval
end -- inv.items.storeItem
----------------------------------------------------------------------------------------------------
-- Routines to handle wearing items
--
-- inv.items.wearItem(objId, objLoc, commandArray, doCheckLocation) -- must be called from a co-routine
-- inv.items.wearSetupFn()
-- inv.items.wearResultFn()
--
-- Verbage:
-- "You do not have that item." -- wear BADNAME
-- "You wear ..." -- wear item
-- "You wield ..." -- weapon
-- "You hold ..." -- held item
-- "You light ..." -- wear light
-- "You equip ..." -- wear portal or sleeping bag
-- "... begins floating around you" -- wear float
-- "... begins floating above you" -- wear aura of trivia
----------------------------------------------------------------------------------------------------
-- There could be cases where we know for a fact that the plugin doesn't have the correct location
-- info (e.g., we do an atomic get/id/put) available and this give us a way to avoid unnecessary checks.
function inv.items.wearItem(objId, targetLoc, commandArray, doCheckLocation)
local retval = DRL_RET_SUCCESS
objId = tonumber(objId or "")
if (objId == nil) then
dbot.warn("inv.items.wearItem: Missing valid object ID parameter")
return DRL_RET_INVALID_PARAM
end -- if
targetLoc = targetLoc or "" -- this is an optional parameter
local itemName = (inv.items.getField(objId, invFieldColorName) or "Unidentified") .. DRL_ANSI_WHITE .. "@W"
-- If we are already wearing the item, don't do anything -- we're good.
if (commandArray == nil) or doCheckLocation then
if inv.items.isWorn(objId) then
dbot.note("Item \"" .. itemName .. "\" is already worn")
return DRL_RET_SUCCESS
end -- if
-- Start with the item in your main inventory
retval = inv.items.getItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.wearItem: Failed to get item \"" .. itemName .. "\": " ..
dbot.retval.getString(retval))
return retval
end -- if
end -- if
local wearCommand = "wear " .. objId .. " " .. targetLoc
if (commandArray ~= nil) then
table.insert(commandArray, wearCommand)
return retval
end -- if
-- Be paranoid and ensure the object is in the main inventory
local objLoc = inv.items.getField(objId, invFieldObjLoc) or ""
if (objLoc ~= invItemLocInventory) then
dbot.warn("inv.items.wearItem: Item \"" .. itemName .. "\" is not in main inventory as expected")
return DRL_RET_INTERNAL_ERROR
end -- if
dbot.note(" Wearing \"" .. itemName .. "\"")
-- Execute the "wear" command
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command(wearCommand, inv.items.wearSetupFn, nil,
inv.items.wearResultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to wear \"" .. itemName .. "\": " .. dbot.retval.getString(retval))
else
-- Wait until we have confirmation that the callback completed
retval = dbot.callback.wait(resultData, 5)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to wear \"" .. itemName .. "\": " .. dbot.retval.getString(retval))
return retval
end -- if
-- Wait until we have confirmation the invmon trigger knows we are wearing the item
local totTime = 0
local timeout = 2
while (inv.items.isWorn(objId) == false) do
if (totTime > timeout) then
if inv.items.isInvis(objId) then
dbot.info("Failed to wear invisible item \"" .. itemName .. "\": can you detect invis?")
else
dbot.warn("inv.items.wearItem: Timed out waiting for invmon to confirm we are wearing \"" ..
itemName .. "\"")
end -- if
retval = DRL_RET_MISSING_ENTRY
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- if
end -- if
return retval
end -- inv.items.wearItem
function inv.items.wearSetupFn()
EnableTrigger(inv.items.trigger.wearName, true)
end -- inv.items.wearSetupFn
function inv.items.wearResultFn(resultData, retval)
EnableTrigger(inv.items.trigger.wearName, false)
dbot.callback.default(resultData, retval)
end -- inv.items.wearResultFn
function inv.items.isWorn(objId)
local objLoc = inv.items.getField(objId, invFieldObjLoc) or ""
for _, entry in pairs(inv.wearLoc) do
if (objLoc == entry) then
dbot.debug("inv.items.isWorn: item " .. objId .. " is worn at location \"" .. entry .. "\"")
return true
end -- if
end -- for
return false
end -- inv.items.isWorn
-- This checks if the parameter is a valid wearable location (e.g, "head" or "neck2"). This is
-- slightly different from the inv.items.isWearableType() function which checks for general types
-- such as "neck" or "finger" instead of specific locations like "neck2" or "lfinger" like we do here.
function inv.items.isWearableLoc(wearableLoc)
for _, loc in pairs(inv.wearLoc) do
if (wearableLoc == loc) then
return true
end -- if
end -- for
return false
end -- inv.items.isValidLoc
-- Determines if the input parameter is a general wearable location type like "neck" or "finger".
-- This is slightly different from inv.items.isWearableLoc() which checks for specific wearable
-- locations such as "neck2" or "rfinger".
function inv.items.isWearableType(wearableType)
if (wearableType == nil) or (wearableType == "") or (inv.wearables[wearableType] == nil) then
return false
else
return true
end -- if
end -- inv.items.isWearableType
-- This function converts a wearable type (e.g., "neck") into a string holding the specific
-- wearable locations that match the type (e.g., "neck1 neck2").
function inv.items.wearableTypeToLocs(wearableType)
if (wearableType == nil) or (wearableType == "") or (inv.wearables[wearableType] == nil) then
return ""
end -- if
local wearableArray = inv.wearables[wearableType]
local wearableLocs = ""
for i, wearableLoc in ipairs(wearableArray) do
if (i == 1) then
wearableLocs = wearableLoc
else
wearableLocs = wearableLocs .. " " .. wearableLoc
end -- if
end -- for
return wearableLocs
end -- inv.items.wearableTypeToLocs
----------------------------------------------------------------------------------------------------
-- Routines to handle removing items
--
-- inv.items.removeItem(objId, commandArray) -- must be called from a co-routine
-- inv.items.removeSetupFn()
-- inv.items.removeResultFn()
--
-- Verbage:
-- "You are not wearing that item." -- remove BADNAME
-- "You remove .*" -- wear item
-- "You stop wielding .*" -- weapon
-- ".* stops floating around you.*" -- float
-- ".* stops floating above you.*" -- above
-- "You stop using.* as a portal.*" -- portal
--
----------------------------------------------------------------------------------------------------
function inv.items.removeItem(objId, commandArray)
local retval = DRL_RET_SUCCESS
objId = tonumber(objId or "")
if (objId == nil) then
dbot.warn("inv.items.removeItem: Missing valid object ID parameter")
return DRL_RET_INVALID_PARAM
end -- if
local itemName = (inv.items.getField(objId, invFieldColorName) or "Unidentified") .. DRL_ANSI_WHITE .. "@W"
-- If we are not wearing the item, don't do anything -- we're good.
if (inv.items.isWorn(objId) == false) then
--dbot.debug("Item \"" .. itemName .. "\" is not worn")
return DRL_RET_SUCCESS
end -- if
local removeCommand = "remove " .. objId
if (commandArray ~= nil) then
table.insert(commandArray, removeCommand)
else
dbot.note(" Removing \"" .. itemName .. "\"")
-- Execute the "remove" command
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command(removeCommand, inv.items.removeSetupFn, nil,
inv.items.removeResultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping remove request: " .. dbot.retval.getString(retval))
else
-- Wait until we have confirmation that the callback completed
retval = dbot.callback.wait(resultData, 5)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping remove request: " .. dbot.retval.getString(retval))
return retval
end -- if
-- Wait until we have confirmation the invmon trigger knows we removed the item
local totTime = 0
local timeout = 2
while inv.items.isWorn(objId) do
if (totTime > timeout) then
if inv.items.isInvis(objId) then
dbot.info("Failed to remove invisible item \"" .. itemName .. "\": can you detect invis?")
else
dbot.warn("inv.items.removeItem: Timed out waiting for invmon to confirm we removed \"" ..
itemName .. "\"")
end -- if
retval = DRL_RET_MISSING_ENTRY
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
end -- if
end -- if
return retval
end -- inv.items.removeItem
function inv.items.removeSetupFn()
EnableTrigger(inv.items.trigger.removeName, true)
end -- inv.items.removeSetupFn
function inv.items.removeResultFn(resultData, retval)
EnableTrigger(inv.items.trigger.removeName, false)
dbot.callback.default(resultData, retval)
end -- inv.items.removeResultFn
----------------------------------------------------------------------------------------------------
-- Keyword support
--
-- We give users the ability to add user-defined keywords to items. They can then be used in
-- queries to search/get/put/organize items.
--
----------------------------------------------------------------------------------------------------
invKeywordOpAdd = "add"
invKeywordOpRemove = "remove"
inv.items.keywordPkg = nil
function inv.items.keyword(keyword, keywordOperation, queryString, useQuietMode, endTag)
if (keyword == nil) or (keyword == "") then
dbot.warn("inv.items.keyword: Missing keyword")
return inv.tags.stop(invTagsKeyword, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (keywordOperation == nil) or
((keywordOperation ~= invKeywordOpAdd) and (keywordOperation ~= invKeywordOpRemove)) then
dbot.warn("inv.items.keyword: Invalid keywordOperation")
return inv.tags.stop(invTagsKeyword, endTag, DRL_RET_INVALID_PARAM)
end -- if
useQuietMode = useQuietMode or false
if (inv.items.keywordPkg ~= nil) then
dbot.info("Skipping keyword request: another keyword request is in progress")
return inv.tags.stop(invTagsKeyword, endTag, DRL_RET_BUSY)
end -- if
-- We use a background co-routine to perform the keyword add or remove. The main reason we
-- do this in the background is that we want to use the background search co-routine to
-- find items to keyword (or un-keyword) via the query string and the search co-routine only runs
-- in a co-routine environment due to explicit scheduling requests.
inv.items.keywordPkg = {}
inv.items.keywordPkg.keyword = keyword
inv.items.keywordPkg.keywordOperation = keywordOperation
inv.items.keywordPkg.queryString = queryString or ""
inv.items.keywordPkg.useQuietMode = useQuietMode
inv.items.keywordPkg.endTag = endTag
wait.make(inv.items.keywordCR)
return DRL_RET_SUCCESS
end -- inv.items.keyword
function inv.items.keywordCR()
local idArray
local retval
local i
local objId
local numQueryItems = 0
local numUpdatedKeywords = 0
if (inv.items.keywordPkg == nil) or (inv.items.keywordPkg.keyword == nil) then
dbot.error("inv.items.keywordCR: Aborting keyword request -- keyword package or name is nil!")
inv.items.keywordPkg = nil
return DRL_RET_INTERNAL_ERROR
end -- if
local endTag = inv.items.keywordPkg.endTag
idArray, retval = inv.items.searchCR(inv.items.keywordPkg.queryString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.keywordCR: failed to search inventory table: " .. dbot.retval.getString(retval))
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("No match found for keyword query: \"" .. inv.items.keywordPkg.queryString .. "\"")
else
numQueryItems = #idArray
-- Update the keyword for each item that matched the query string
for i,objId in ipairs(idArray) do
local keywordField = inv.items.getStatField(objId, invStatFieldKeywords) or ""
if (inv.items.keywordPkg.keywordOperation == invKeywordOpAdd) then
dbot.debug("Adding keyword \"" .. inv.items.keywordPkg.keyword .. "\" to object " .. objId)
if (keywordField == nil) or (keywordField == "") then
inv.items.setStatField(objId, invStatFieldKeywords, inv.items.keywordPkg.keyword)
elseif dbot.isWordInString(inv.items.keywordPkg.keyword, keywordField) then
dbot.debug("Skipping keyword of item " .. objId .. ": item is already tagged with " ..
inv.items.keywordPkg.keyword)
else
inv.items.setStatField(objId, invStatFieldKeywords, keywordField .. " " ..
inv.items.keywordPkg.keyword)
end -- if
numUpdatedKeywords = numUpdatedKeywords + 1
elseif (inv.items.keywordPkg.keywordOperation == invKeywordOpRemove) then
local element
local newKeywordField = ""
-- Rebuild the keywordField and leave out any flags that match the specified removed flag
dbot.debug("Removing keyword \"" .. inv.items.keywordPkg.keyword ..
"\" from object " .. objId)
for element in keywordField:gmatch("%S+") do
if (string.lower(element) ~= string.lower(inv.items.keywordPkg.keyword)) then
if (newKeywordField == "") then
newKeywordField = element
else
newKeywordField = newKeywordField .. " " .. element
end -- if
else
numUpdatedKeywords = numUpdatedKeywords + 1
end -- if
end -- for
inv.items.setStatField(objId, invStatFieldKeywords, newKeywordField)
else
dbot.error("inv.items.keywordCR: Invalid keyword operation detected")
inv.items.keywordPkg = nil
return inv.tags.stop(invTagsKeyword, endTag, DRL_RET_INTERNAL_ERROR)
end -- if
end -- for
end -- if
if (not inv.items.keywordPkg.useQuietMode) then
dbot.info("Updated keyword \"" .. (inv.items.keywordPkg.keyword or "Unknown") .. "\" for " ..
numUpdatedKeywords .. " out of " .. numQueryItems .. " items matching query")
end -- if
inv.items.keywordPkg = nil
return inv.tags.stop(invTagsKeyword, endTag, retval)
end -- inv.items.keywordCR
-- A query consists of an array of k-v arrays (e.g., { { "type", "weapon" }, { "minlevel", "100" } }).
-- However, this function supports an array of queries and the result is the "OR" or any query matches.
-- For example, here is an array of query arrays that matches on items that are either weapons under
-- L100 or are shields under L100:
-- { { { invStatFieldType, "weapon" }, { "minlevel", "100" } },
-- { { invStatFieldWearable, "shield" }, { "minlevel", "100" } }
-- }
function inv.items.search(arrayOfQueryArrays)
local retval = DRL_RET_SUCCESS
local idArray = {}
if (arrayOfQueryArrays == nil) then
dbot.warn("inv.items.search: query array is nil")
return nil, DRL_RET_INVALID_PARAM
end -- if
for _, queryArray in ipairs(arrayOfQueryArrays) do
-- Walk through the inventory table looking for entries that match the requested queries
for itemId,itemObj in pairs(inv.items.table) do
-- Verify that the inventory entry looks reasonable
assert(itemId ~= nil, "inv.items.search: inventory table key is nil")
if (itemObj == nil) then
dbot.warn("inv.items.search: invalid nil entry found for item " .. itemId)
return nil, DRL_RET_MISSING_ENTRY
end -- if
-- Check if the item already matches the query. This could happen if we have something of the
-- form "query1 or query2" and the item matches both query1 and query2. If the item is already
-- known to match, then we don't want to waste time checking other query clauses and we don't
-- want to duplicate it in the array of IDs that we return.
local idAlreadyMatches = false
for _, id in ipairs(idArray) do
if (tonumber(itemId) == tonumber(id)) then
idAlreadyMatches = true
end -- if
end -- for
-- Get the stats entry for the given item and check if it matches the queries in the query array
local itemMatches = false
local objLoc = itemObj[invFieldObjLoc]
local stats = itemObj[invFieldStats]
if (stats ~= nil) and (idAlreadyMatches == false) then
itemMatches = true -- start by assuming we have a match and halt if we find any non-conforming query
-- If we have an empty query (query == "") and the item is equipped, we don't match it. The
-- empty query refers to everything that is not equipped.
if (queryArray ~= nil) and (#queryArray == 0) and inv.items.isWorn(itemId) then
itemMatches = false
end -- if
for queryIdx,query in ipairs(queryArray) do
local key = string.lower(query[1]) -- Stat keys and values are lower case to avoid conflicts
local value = string.lower(query[2])
local valueNum = tonumber(query[2])
if (key == nil) or (value == nil) then
dbot.warn("inv.items.search: query " .. queryIdx .. " is malformed with a nil component")
return nil, DRL_RET_MISSING_ENTRY
end -- if
-- There are a few "one-off" search queries that make life simplier. We support the
-- "all", "equipped" (or "worn"), and "unequipped" search queries.
if (key == invQueryKeyCustom) then
if (value == invQueryKeyAll) then
itemMatches = true
elseif (value == invQueryKeyEquipped) and (not inv.items.isWorn(itemId)) then
itemMatches = false
elseif (value == invQueryKeyUnequipped) and inv.items.isWorn(itemId) then
itemMatches = false
end -- if
break
end -- if
-- Check if the query has a prefix. We currently support the prefixes "~", "min", and "max".
local prefix = ""
local base = ""
local invert = false
_, _, prefix, base = string.find(key, "(~)(%S+)")
if (prefix ~= nil) and (base ~= nil) then
invert = true
key = base
end -- if
_, _, prefix, base = string.find(key, "(min)(%S+)")
if (prefix ~= nil) and (base ~= nil) then
key = base
else
_, _, prefix, base = string.find(key, "(max)(%S+)")
if (prefix ~= nil) and (base ~= nil) then
key = base
end -- if
end -- if
local statsVal = stats[key] or ""
local statsNum = tonumber(stats[key] or 0)
-- Ensure that "min" and "max" queries are only used on numbers
if (prefix == "min") or (prefix == "max") then
if (valueNum == nil) or (statsNum == nil) then
dbot.warn("inv.items.search: min or max prefix was used on non-numerical query")
return DRL_RET_INVALID_PARAM
end -- if
end -- if
-- We don't keep meta-information about the item (e.g., location, id level, etc.) inside the
-- stats table but our search queries are all relative to entries in the stats table. One
-- exception to this is the objectLocation field. It's a little awkward moving that to the
-- stats table and we don't want to duplicate it...so...we use a little kludge here and
-- explicitly check for a location query and handle it as a one-off. Yes, I should probably
-- fix this at some point...
if (key == invQueryKeyLocation) or (key == invQueryKeyLoc) then
if ((invert == false) and ((valueNum ~= nil) and (valueNum ~= objLoc))) or
((invert == true) and ((valueNum ~= nil) and (valueNum == objLoc))) then
itemMatches = false
break
end -- if
-- If we are searching for an element in a string of elements (e.g., a keyword in a keyword list
-- or a flag in a list of flags) check if the queried string is present. We use a case-insensitive
-- search by making everything in the strings lower case. We also temporarily replace special
-- characters in the search strings with their escaped equivalents (e.g., "-" becomes "%-") so
-- that we can search for things like "anti-evil" without the hyphen being interpreted as a
-- special character.
--
-- Some string fields (name and leadsTo) support a partial match. For example, searching for
-- "Nation" in the "leadsTo" field would match for both "Imperial Nation" and "The Amazon Nation".
-- Other string fields (keywords and flags) require an exact match so searching for the
-- "evil" flag won't match on "anti-evil".
elseif (key == invStatFieldName) or (key == invStatFieldLeadsTo) then
local escapedValue = string.gsub(value, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1")
local noMatch = (string.find(string.lower(statsVal), string.lower(escapedValue)) == nil)
if ((invert == false) and noMatch) or ((invert == true) and not noMatch) then
itemMatches = false
break
end -- if
elseif (key == invStatFieldKeywords) or (key == invStatFieldFlags) or (key == invStatFieldClan) then
local statField = statsVal or ""
local element
local isInField = false
for element in statField:gmatch("%S+") do
element = string.gsub(element, ",", "")
if (string.lower(element) == string.lower(value)) then
isInField = true
break
end -- if
end -- for
-- Yes, this reduces to "if invert == isInField" but I can't bring myself to use that
-- because I know I'll forget the simplifiication by the next time I look at this code...
if ((invert == false) and (isInField == false)) or
((invert == true) and (isInField == true)) then
itemMatches = false
break
end -- if
-- Check for a min or a max query
elseif ((invert == false) and (prefix == "min") and (statsNum < valueNum)) or
((invert == true) and (prefix == "min") and (statsNum >= valueNum)) or
((invert == false) and (prefix == "max") and (statsNum > valueNum)) or
((invert == true) and (prefix == "max") and (statsNum <= valueNum)) then
itemMatches = false
break
-- Check for entries that aren't present (defaults to "0")
elseif (statsVal == "") then
if ((invert == false) and (statsNum ~= valueNum)) or
((invert == true) and (statsNum == valueNum)) then
itemMatches = false
break
end -- if
-- Handle a basic string query (use lowercase only to make queries a bit simpler)
elseif ((invert == false) and (prefix == nil) and
(string.lower(statsVal) ~= string.lower(value))) or
((invert == true) and (prefix == nil) and
(string.lower(statsVal) == string.lower(value))) then
itemMatches = false
break
end -- if
end -- for
end -- if
if (itemMatches == true) then
table.insert(idArray, tonumber(itemId))
end -- if
end -- for
end -- for
return idArray, retval
end -- inv.items.search
-- Parse the string containing one or more search queries into an array of { key, value } where
-- each element is one component of the full query
-- Example queryStrings:
-- wearable hold maxlevel 10
-- level 11 type armor
-- keywords aardwords
-- flags glow minint 5
-- loc 123456789
-- name bob
-- keyword shardblade
-- Note: This must be called from within a co-routine because inv.items.convertRelative() can block
function inv.items.searchCR(queryString)
local retval = DRL_RET_SUCCESS
local arrayOfKvArrays = {}
local kvArray = {}
local idx = 1
local key = ""
local value = ""
local element
local numWordsInQuery = 0
if (queryString == nil) then
dbot.warn("inv.items.searchCR: Missing query string parameter")
return DRL_RET_INVALID_PARAM
end -- if
-- Count the number of words in the query string. We use an obscure form of gsub() for this.
-- The gsub function's 2nd return value is the number of substitutions made. If we do a dummy
-- substitution for each block of non-space characters in the query, we can get a count of the
-- number of words in the query.
if (queryString ~= nil) and (queryString ~= "") then
_, numWordsInQuery = queryString:gsub("%S+", "")
end -- if
-- An empty query matches everything that is not equipped
if (queryString == "") then
table.insert(kvArray, { invQueryKeyCustom, invQueryKeyUnequipped })
-- A query that only consists of "all" will match everything -- including equipped items
elseif (Trim(queryString) == invQueryKeyAll) then
table.insert(kvArray, { invQueryKeyCustom, invQueryKeyAll })
-- You can match all worn equipment with the "equipped" or "worn" query
elseif (Trim(queryString) == invQueryKeyEquipped) or (Trim(queryString) == invQueryKeyWorn) then
table.insert(kvArray, { invQueryKeyCustom, invQueryKeyEquipped })
-- If there is just a single word in the queryString, assume it is a name search.
-- We don't really need to support this, but it is a convenient kludge.
elseif (numWordsInQuery == 1) then
table.insert(kvArray, { invStatFieldName, queryString })
else
-- Parse the query string into key-value pairs and pass those pairs to inv.items.search()
-- to search the inventory table for items matching each key-value query
for element in queryString:gmatch("%S+") do
-- If we hit the "OR" operator, close the current query and start a new one
if (element == "||") then
table.insert(arrayOfKvArrays, kvArray)
kvArray = {}
-- If we are in a query and we are at the key location (it goes key then value), then save the key
elseif ((idx % 2) ~= 0) then
key = element
idx = idx + 1
-- If we are in a query and we are at the value location (it goes key then value), then save the value
else
value = element
--dbot.debug("key=\"" .. key .. "\", value=\"" .. value .. "\"")
-- If we are inverting the key field (e.g., "level" vs. "~level") then we want to temporarily
-- strip the "~" from the key, process the remaining key, and then add the "~" back before we
-- put the query into the returned array. We could leave the "~" in place, but it reduces the
-- parsing complexity if we pull it out before we do checks against the key type.
local isInverted = string.find(key, "^~.*")
if isInverted then
key = string.gsub(key, "^~", "")
end -- if
-- If a query has a relative name or loc in it, convert the name or loc to an object ID here
if (key == invQueryKeyRelativeName) or (key == invQueryKeyRelativeLoc) or
(key == invQueryKeyRelativeLocation) then
key, value, retval = inv.items.convertRelative(key, value) -- new value is ID of relative item
if (retval ~= DRL_RET_SUCCESS) then
return nil, retval
end -- if
end -- if
-- Add shortcuts to some commonly used query keys
if (key == invQueryKeyKey) or (key == invQueryKeyKeyword) then
key = invStatFieldKeywords
elseif (key == invQueryKeyFlag) then
key = invStatFieldFlags
end -- if
-- Add the "~" back to the key name if we stripped the inversion prefix off before parsing the key
if (isInverted) then
key = "~" .. key
end -- if
table.insert(kvArray, { key, value })
idx = idx + 1
end -- if
end -- for
end -- if
-- Close the final query and add it to the array of queries
table.insert(arrayOfKvArrays, kvArray)
-- Convert the series of queries in an array of object IDs that match the queries
local idArray, retval = inv.items.search(arrayOfKvArrays)
return idArray, retval
end -- inv.items.searchCR
-- Return an array of objIds that is a sorted version of the idArray given as an input param
-- E.g., to sort first by type and then by level, use a fieldArray like this:
-- fieldArray = { { field = invStatFieldType, isAscending = true },
-- { field = invStatFieldLevel, isAscending = true } }
function inv.items.sort(idArray, fieldArray)
if (idArray == nil) or (fieldArray == nil) then
dbot.warn("inv.items.sort: required input parameter is nil")
return DRL_RET_INVALID_PARAM
end -- if
inv.items.compareArray = fieldArray
table.sort(idArray, inv.items.compare)
return DRL_RET_SUCCESS
end -- inv.items.sort
inv.items.compareArray = nil
function inv.items.compare(item1, item2)
for i, sortEntry in ipairs(inv.items.compareArray) do
local fieldName = sortEntry.field
assert(fieldName ~= nil, "sorting field is missing")
local isAscending = sortEntry.isAscending
if (isAscending == nil) then
currentIsAscending = true -- default to "true" if the user doesn't specify an order
end -- if
field1 = inv.items.getStatField(item1, fieldName) or "unknown"
field2 = inv.items.getStatField(item2, fieldName) or "unknown"
fieldNum1 = tonumber(field1)
fieldNum2 = tonumber(field2)
-- If we are comparing one number and one string then something is wrong
if ((fieldNum1 == nil) and (fieldNum2 ~= nil)) or ((fieldNum1 ~= nil) and (fieldNum2 == nil)) then
return false
end -- if
-- If we have two numbers, compare the two numbers. If they are equal, move to the next sorting element.
-- Otherwise, return with a boolean indicating which one is first.
if (fieldNum1 ~= nil) and (fieldNum2 ~= nil) and (fieldNum1 ~= fieldNum2) then
if (isAscending) then
return fieldNum1 < fieldNum2
else
return fieldNum1 > fieldNum2
end -- if
end -- if
-- If we have two non-numerical strings, compare them!
if (fieldNum == nil) and (fieldNum2 == nil) and (field1 ~= field2) then
if (isAscending) then
return field1 < field2
else
return field1 > field2
end -- if
end -- if
end -- for
-- We made it through all of the sort specifications without finding any differences between the
-- two items. They are equivalent based on the fields given as parameters.
return false
end -- inv.items.compare
function inv.items.convertRelative(relativeName, value)
local key = nil
local id = nil
local retval = DRL_RET_SUCCESS
if (value == nil) then
dbot.warn("inv.items.convertRelative: nil value parameter detected")
return key, id, DRL_RET_INVALID_PARAM
end -- if
if (relativeName == invQueryKeyRelativeName) then
key = invStatFieldId
elseif (relativeName == invQueryKeyRelativeLoc) or (relativeName == invQueryKeyRelativeLocation) then
key = invQueryKeyLocation
end -- if
inv.lastIdentifiedObjectId = nil
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command("identify " .. value, inv.items.convertSetupFn, nil,
dbot.callback.default, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.convertRelative: Failed to submit safe identification call: " ..
dbot.retval.getString(retval))
inv.lastIdentifiedObjectId = 0
else
-- Wait until we know the relative item's object ID
retval = dbot.callback.wait(resultData, inv.items.timer.idTimeoutThresholdSec)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping relative name identify request: " .. dbot.retval.getString(retval))
inv.lastIdentifiedObjectId = 0
end -- if
end -- if
-- Grab the ID for the most recently identified object
if (inv.lastIdentifiedObjectId == 0) then
dbot.note("Skipping request: relative name \"" .. value ..
"\" did not match any item in your main inventory")
EnableTrigger(inv.items.trigger.idItemName, false)
key = nil
id = nil
retval = DRL_RET_MISSING_ENTRY
else
id = inv.lastIdentifiedObjectId
end -- if
inv.lastIdentifiedObjectId = nil
return key, id, retval
end -- inv.items.convertRelative
function inv.items.convertSetupFn()
EnableTrigger(inv.items.trigger.idItemName, true)
end -- inv.items.convertSetupFn
invDisplayVerbosityBasic = "basic" -- default mode
invDisplayVerbosityId = "id"
invDisplayVerbosityFull = "full"
invDisplayVerbosityDiffAdd = "diffAdd" -- internal only (shows diff format for replaced items)
invDisplayVerbosityDiffRemove = "diffRemove" -- internal only (shows diff format for replaced items)
invDisplayVerbosityRaw = "raw" -- internal only (shows raw table data)
inv.items.displayPkg = nil
-- Asynchronous routine to display results for a query into the inventory table
function inv.items.display(queryString, verbosity, endTag)
local retval = DRL_RET_SUCCESS
-- Default to basic display verbosity if nothing is specificed as a parameter
verbosity = verbosity or invDisplayVerbosityBasic
-- Check if another display request is in progress before we proceed
if (inv.items.displayPkg ~= nil) then
dbot.info("inv.items.display: Skipping display query: another display query is in progress")
return inv.tags.stop(invTagsSearch, endTag, DRL_RET_BUSY)
end -- if
-- Use globals to hold state for the display co-routine
inv.items.displayPkg = {}
inv.items.displayPkg.queryString = queryString
inv.items.displayPkg.verbosity = verbosity
inv.items.displayPkg.endTag = endTag
-- Sort the results first by item type, then by item level, then by location, and finally by item name
inv.items.displayPkg.sortCriteria = { { field = invStatFieldType, isAscending = true },
{ field = invStatFieldLevel, isAscending = true },
{ field = invStatFieldWearable, isAscending = true },
{ field = invStatFieldName, isAscending = true } }
-- Fire off the asynchronous co-routine to generate and display the results
wait.make(inv.items.displayCR)
return retval
end -- inv.items.display
inv.items.displayLastType = ""
function inv.items.displayCR()
local endTag = inv.items.displayPkg.endTag
local idArray, retval = inv.items.searchCR(inv.items.displayPkg.queryString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.displayCR: failed to search inventory table: " .. dbot.retval.getString(retval))
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("No items matched query: \"" .. inv.items.displayPkg.queryString .. "\"")
else
-- Sort and display the results
inv.items.sort(idArray, inv.items.displayPkg.sortCriteria)
inv.items.displayLastType = ""
for _, objId in ipairs(idArray) do
inv.items.displayItem(objId, inv.items.displayPkg.verbosity)
end -- for
end -- if
inv.items.displayPkg = nil
return inv.tags.stop(invTagsSearch, endTag, retval)
end -- inv.items.displayCR
function inv.items.displayItem(objId, verbosity, wearableLoc)
if (objId == nil) then
dbot.warn("inv.items.displayItem: objId is nil")
return DRL_RET_INVALID_PARAMETER
end -- if
local objIdNum = dbot.tonumber(objId)
if (objIdNum == nil) then
dbot.warn("inv.items.displayItem: objId is not a number")
return DRL_RET_INVALID_PARAMETER
end -- if
local entry = inv.items.getEntry(objId)
if (objId == nil) then
dbot.warn("inv.items.displayItem: Item " .. objId .. " is not in the inventory table")
return DRL_RET_INVALID_ENTRY
end -- if
-- Use the default verbosity mode if verbosity is not given
verbosity = verbosity or invDisplayVerbosityId
if (verbosity == invDisplayVerbosityRaw) then
print("\nInventory table key: \"" .. objId .. "\"")
tprint(entry)
return DRL_RET_SUCCESS
end -- if
local objLoc = inv.items.getField(objId, invFieldObjLoc)
if (objLoc == nil) then
dbot.warn("inv.items.displayItem: Item " .. objId .. " does not have a known location")
return DRL_RET_INTERNAL_ERROR
end -- if
local colorName = inv.items.getField(objId, invFieldColorName) or "@RName is not yet identified@w"
local level = inv.items.getStatField(objId, invStatFieldLevel) or 0
local typeField = inv.items.getStatField(objId, invStatFieldType) or "Unknown"
local weaponType = inv.items.getStatField(objId, invStatFieldWeaponType) or "Unknown"
local damtype = inv.items.getStatField(objId, invStatFieldDamType) or "none"
local specials = inv.items.getStatField(objId, invStatFieldSpecials) or "none"
local wearable = inv.items.getStatField(objId, invStatFieldWearable) or ""
local leadsTo = inv.items.getStatField(objId, invStatFieldLeadsTo) or "Unknown"
local spells = inv.items.getStatField(objId, invStatFieldSpells) or {}
-- Highlight items that are currently worn (the location isn't a container or inventory)
local highlightOn = ""
local highlightOff = ""
local isCurrentlyWorn = false
if (type(objLoc) ~= "number") and (inv.items.getField(objId, invFieldObjLoc) ~= invItemLocInventory)
and (inv.items.getField(objId, invFieldColorName) ~= "") then
isCurrentlyWorn = true
highlightOn = "@W"
highlightOff = "@w"
end -- if
local int = dbot.tonumber(inv.items.getStatField(objId, invStatFieldInt) or "0")
local luck = dbot.tonumber(inv.items.getStatField(objId, invStatFieldLuck) or "0")
local wis = dbot.tonumber(inv.items.getStatField(objId, invStatFieldWis) or "0")
local str = dbot.tonumber(inv.items.getStatField(objId, invStatFieldStr) or "0")
local dex = dbot.tonumber(inv.items.getStatField(objId, invStatFieldDex) or "0")
local con = dbot.tonumber(inv.items.getStatField(objId, invStatFieldCon) or "0")
local avedam = dbot.tonumber(inv.items.getStatField(objId, invStatFieldAveDam) or "0")
local dam = dbot.tonumber(inv.items.getStatField(objId, invStatFieldDam) or "0")
local hit = dbot.tonumber(inv.items.getStatField(objId, invStatFieldHit) or "0")
local hp = dbot.tonumber(inv.items.getStatField(objId, invStatFieldHP) or "0")
local mana = dbot.tonumber(inv.items.getStatField(objId, invStatFieldMana) or "0")
local moves = dbot.tonumber(inv.items.getStatField(objId, invStatFieldMoves) or "0")
local weight = dbot.tonumber(inv.items.getStatField(objId, invStatFieldWeight) or "0")
local allphys = dbot.tonumber(inv.items.getStatField(objId, invStatFieldAllPhys) or "0")
local allmagic = dbot.tonumber(inv.items.getStatField(objId, invStatFieldAllMagic) or "0")
local slash = dbot.tonumber(inv.items.getStatField(objId, invStatFieldSlash) or "0")
local pierce = dbot.tonumber(inv.items.getStatField(objId, invStatFieldPierce) or "0")
local bash = dbot.tonumber(inv.items.getStatField(objId, invStatFieldBash) or "0")
local acid = dbot.tonumber(inv.items.getStatField(objId, invStatFieldAcid) or "0")
local cold = dbot.tonumber(inv.items.getStatField(objId, invStatFieldCold) or "0")
local energy = dbot.tonumber(inv.items.getStatField(objId, invStatFieldEnergy) or "0")
local holy = dbot.tonumber(inv.items.getStatField(objId, invStatFieldHoly) or "0")
local electric = dbot.tonumber(inv.items.getStatField(objId, invStatFieldElectric) or "0")
local negative = dbot.tonumber(inv.items.getStatField(objId, invStatFieldNegative) or "0")
local shadow = dbot.tonumber(inv.items.getStatField(objId, invStatFieldShadow) or "0")
local magic = dbot.tonumber(inv.items.getStatField(objId, invStatFieldMagic) or "0")
local air = dbot.tonumber(inv.items.getStatField(objId, invStatFieldAir) or "0")
local earth = dbot.tonumber(inv.items.getStatField(objId, invStatFieldEarth) or "0")
local fire = dbot.tonumber(inv.items.getStatField(objId, invStatFieldFire) or "0")
local light = dbot.tonumber(inv.items.getStatField(objId, invStatFieldLight) or "0")
local mental = dbot.tonumber(inv.items.getStatField(objId, invStatFieldMental) or "0")
local sonic = dbot.tonumber(inv.items.getStatField(objId, invStatFieldSonic) or "0")
local water = dbot.tonumber(inv.items.getStatField(objId, invStatFieldWater) or "0")
local poison = dbot.tonumber(inv.items.getStatField(objId, invStatFieldPoison) or "0")
local disease = dbot.tonumber(inv.items.getStatField(objId, invStatFieldDisease) or "0")
-- Calculate total physical and magical resists. We weight a specific physical or magic resist
-- relative to an "all" resist. For example, 3 "slash" resists are equivalent to 1 "all" phys resist
-- because there are 3 physical resist types. Similarly, one specific magical resist is worth 1/17 of
-- one "all" magical resist value because there are 17 magical resistance types.
local physResists = allphys + (slash + pierce + bash) / 3
local magicResists = allmagic + (acid + cold + energy + holy + electric + negative + shadow + magic +
air + earth + fire + light + mental + sonic + water + poison + disease) / 17
local totResists = physResists + magicResists
local capacity = dbot.tonumber(inv.items.getStatField(objId, invStatFieldCapacity) or "0")
local holding = dbot.tonumber(inv.items.getStatField(objId, invStatFieldHolding) or "0")
local heaviestItem = dbot.tonumber(inv.items.getStatField(objId, invStatFieldHeaviestItem) or "0")
local itemsInside = dbot.tonumber(inv.items.getStatField(objId, invStatFieldItemsInside) or "0")
local totWeight = dbot.tonumber(inv.items.getStatField(objId, invStatFieldTotWeight) or "0")
local itemBurden = dbot.tonumber(inv.items.getStatField(objId, invStatFieldItemBurden) or "0")
local weightReduction = dbot.tonumber(inv.items.getStatField(objId, invStatFieldWeightReduction) or "0")
-- If we are in basic display mode, don't print the object ID; otherwise print it
local displayObjId = false
if (verbosity == invDisplayVerbosityId) or (verbosity == invDisplayVerbosityFull) then
displayObjId = true
end -- if
-- If we are in "diff" mode, we prepend the addition or removal indicator to the name of the item
if (verbosity == invDisplayVerbosityDiffAdd) then
colorName = "@G>>@W " .. colorName
elseif (verbosity == invDisplayVerbosityDiffRemove) then
colorName = "@R<<@W " .. colorName
end -- if
-- We color-code the ID field as follows: unidentified = red, partial ID = yellow, full ID = green
local formattedId = ""
local colorizedId = ""
local idPrefix = DRL_ANSI_WHITE
local idSuffix = DRL_ANSI_WHITE
local idLevel = inv.items.getField(objId, invFieldIdentifyLevel)
if (idLevel ~= nil) and (displayObjId == true) then
if (idLevel == invIdLevelNone) then
idPrefix = DRL_ANSI_RED
elseif (idLevel == invIdLevelPartial) then
idPrefix = DRL_ANSI_YELLOW
elseif (idLevel == invIdLevelFull) then
idPrefix = DRL_ANSI_GREEN
else
dbot.error("inv.items.displayItem: Invalid identify level state detected: idLevel")
return DRL_INTERNAL_ERROR
end -- if
formattedId = "(" .. objId .. ") "
colorizedId = idPrefix .. formattedId .. idSuffix
end -- if
-- Format the name field for the stat display. This is complicated because we have a fixed
-- number of spaces reserved for the field but color codes could take up some of those spaces.
-- We iterate through the string byte by byte checking the length of the non-colorized equivalent
-- to see when we've hit the limit that we can print.
local maxNameLen = 24
local formattedName = ""
local index = 0
while (#strip_colours(formattedName) < maxNameLen - #formattedId) and (index < 50) do
formattedName = string.sub(colorName, 1, maxNameLen - #formattedId + index)
index = index + 1
end
if (#strip_colours(formattedName) < maxNameLen - #formattedId) then
formattedName = formattedName ..
string.rep(" ", maxNameLen - #strip_colours(formattedName) - #formattedId)
end -- if
-- The trimmed name could end on an "@" which messes up color codes and spacing
formattedName = string.gsub(formattedName, "@$", " ") .. " " .. DRL_XTERM_GREY
formattedName = formattedName .. colorizedId
-- If we have a wearable location, use it in the display. Otherwise, use the item's type.
local typeExtended
if (invIdLevelNone == inv.items.getField(objId, invFieldIdentifyLevel)) then
typeExtended = "Unknown"
elseif (wearableLoc ~= nil) and (wearableLoc ~= "") then
typeExtended = wearableLoc
elseif (typeField == invmon.typeStr[invmonTypeWeapon]) then
typeExtended = weaponType
elseif (typeField == invmon.typeStr[invmonTypeContainer]) then
typeExtended = "Contain"
elseif (wearable == "") or (wearable == invmon.typeStr[invmonTypeHold]) then
typeExtended = typeField
else
typeExtended = wearable
end -- if
typeExtended = string.format("%-8s", typeExtended)
-- Make the item's type show up in bright green if the item is currently worn
if (isCurrentlyWorn == true) then
typeExtended = DRL_ANSI_GREEN .. typeExtended .. DRL_ANSI_WHITE
end -- if
-- Truncate some field strings that are limits on how long they can be
local maxAreaNameLen = 18
local formattedLeadsTo = string.sub(leadsTo, 1, maxAreaNameLen)
local formattedType = string.format("%-17s", typeField)
-- Format the output for the item's stat display
local header
local statLine
if (typeField == invmon.typeStr[invmonTypePotion]) or
(typeField == invmon.typeStr[invmonTypePill]) or
(typeField == invmon.typeStr[invmonTypeScroll]) or
(typeField == invmon.typeStr[invmonTypeWand]) or
(typeField == invmon.typeStr[invmonTypeStaff]) then
header = "@WLvl Name of " .. formattedType .. "Type Lvl # Spell name@w"
local spellDetails = ""
for i,v in ipairs(spells) do
spellDetails = string.format("%s%3d x%1d %s ", spellDetails, v.level, v.count, v.name)
end -- for
statLine = string.format("@W%3d@w %s%s%s", level, formattedName, typeExtended, spellDetails)
elseif (typeField == invmon.typeStr[invmonTypePortal]) then
header = "@WLvl Name of " .. formattedType ..
"Type Leads to HR DR Int Wis Lck Str Dex Con@w"
statLine = string.format("@W%3d@w %s%s %-18s %s %s %s %s %s %s %s %s",
level, formattedName, typeExtended, formattedLeadsTo,
inv.items.colorizeStat(hit, 3), inv.items.colorizeStat(dam, 3),
inv.items.colorizeStat(int, 3), inv.items.colorizeStat(wis, 3),
inv.items.colorizeStat(luck, 3), inv.items.colorizeStat(str, 3),
inv.items.colorizeStat(dex, 3), inv.items.colorizeStat(con, 3))
elseif (typeField == invmon.typeStr[invmonTypeContainer]) then
header = "@WLvl Name of " .. formattedType ..
"Type HR DR Int Wis Lck Str Dex Con Wght Cap Hold Hvy #In Wgt%@w"
statLine = string.format("@W%3d@w %s%s %s %s %s %s %s %s %s %s %s %s %s %s %s %s",
level, formattedName, typeExtended,
inv.items.colorizeStat(hit, 4), inv.items.colorizeStat(dam, 4),
inv.items.colorizeStat(int, 3), inv.items.colorizeStat(wis, 3),
inv.items.colorizeStat(luck, 3), inv.items.colorizeStat(str, 3),
inv.items.colorizeStat(dex, 3), inv.items.colorizeStat(con, 3),
inv.items.colorizeStat(totWeight, 4, true), inv.items.colorizeStat(capacity, 4),
inv.items.colorizeStat(holding, 4), inv.items.colorizeStat(heaviestItem, 3),
inv.items.colorizeStat(itemsInside, 3), inv.items.colorizeStat(weightReduction, 4))
elseif (typeField == invmon.typeStr[invmonTypeWeapon]) then
header = "@WLvl Name of " .. formattedType ..
"Type Ave Wgt HR DR Dam Type Specials Int Wis Lck Str Dex Con@w"
statLine = string.format("@W%3d@w %s%s %s %s %s %s %-8s %-8s %s %s %s %s %s %s",
level, formattedName, typeExtended,
inv.items.colorizeStat(avedam, 3), inv.items.colorizeStat(weight, 3),
inv.items.colorizeStat(hit, 4), inv.items.colorizeStat(dam, 4),
damtype, specials,
inv.items.colorizeStat(int, 3), inv.items.colorizeStat(wis, 3),
inv.items.colorizeStat(luck, 3), inv.items.colorizeStat(str, 3),
inv.items.colorizeStat(dex, 3), inv.items.colorizeStat(con, 3))
elseif (typeField == "Unknown") then
header = "@WLvl Name of Unknown Item Type"
statLine = string.format("@WN/A@w %sItem has not yet been identified", formattedName)
else
header = "@WLvl Name of " .. formattedType ..
"Type HR DR Int Wis Lck Str Dex Con Res HitP Mana Move@w"
statLine = string.format("@W%3d@w %s%s %s %s %s %s %s %s %s %s %s %s %s %s",
level, formattedName, typeExtended,
inv.items.colorizeStat(hit, 4), inv.items.colorizeStat(dam, 4),
inv.items.colorizeStat(int, 3), inv.items.colorizeStat(wis, 3),
inv.items.colorizeStat(luck, 3), inv.items.colorizeStat(str, 3),
inv.items.colorizeStat(dex, 3), inv.items.colorizeStat(con, 3),
inv.items.colorizeStat(totResists, 3),
inv.items.colorizeStat(hp, 4), inv.items.colorizeStat(mana, 4),
inv.items.colorizeStat(moves, 4))
end -- if
-- Dump the stats for this item. We print a header if we are in full verbosity mode or if this
-- is the first item of its type to be displayed.
if (inv.items.displayLastType ~= typeField) or (verbosity == invDisplayVerbosityFull) then
dbot.print("\n" .. header)
inv.items.displayLastType = typeField
end -- if
dbot.print(statLine)
-- Return now if the user requested anything except the full view -- everything has been displayed for those
if (verbosity ~= invDisplayVerbosityFull) then
return DRL_RET_SUCCESS
end -- if
local score = dbot.tonumber(inv.items.getStatField(objId, invStatFieldScore) or "0")
local worth = dbot.tonumber(inv.items.getStatField(objId, invStatFieldWorth) or "0")
local keywords = inv.items.getStatField(objId, invStatFieldKeywords) or ""
local flags = inv.items.getStatField(objId, invStatFieldFlags) or ""
local material = inv.items.getStatField(objId, invStatFieldMaterial) or "Unknown"
local foundAt = inv.items.getStatField(objId, invStatFieldFoundAt) or "Unknown"
local ownedBy = inv.items.getStatField(objId, invStatFieldOwnedBy) or ""
local clan = inv.items.getStatField(objId, invStatFieldClan) or ""
local affectMods = inv.items.getStatField(objId, invStatFieldAffectMods) or ""
local organize = inv.items.getStatField(objId, invQueryKeyOrganize) or ""
dbot.print(" colorName:\"" .. colorName .. "\" objectID:" .. objId)
dbot.print(" keywords:\"" .. keywords .. "\"")
if (organize ~= "") then
dbot.print(" organize query:\"" .. organize .. "\"")
end -- if
dbot.print(" flags:\"" .. flags .. "\"")
dbot.print(" score:" .. score .. " worth:" .. worth .. " material:" .. material ..
" foundAt:\"" .. foundAt .. "\"")
dbot.print(" allphys:" .. allphys .. " allmagic:" .. allmagic .. " slash:" .. slash ..
" pierce:" .. pierce .. " bash:" .. bash .. " acid:" .. acid .. " poison:" .. poison)
dbot.print(" disease:" .. disease .. " cold:" .. cold .. " energy:" .. energy ..
" holy:" .. holy .. " electric:" .. electric .. " negative:" .. negative ..
" shadow:" .. shadow)
dbot.print(" air:" .. air .. " earth:" .. earth .. " fire:" .. fire .. " water:" .. water ..
" light:" .. light .. " mental:" .. mental .. " sonic:" .. sonic .." magic:" .. magic)
dbot.print(" weight:" .. weight .. " ownedBy:\"" .. ownedBy .. "\"")
dbot.print(" clan:\"" .. clan .. "\" affectMods:\"" .. affectMods .. "\"")
return DRL_RET_SUCCESS
end -- inv.items.displayItem
function inv.items.colorizeStat(value, numDigits, invertColors)
if (numDigits == nil) then
dbot.error("inv.items.colorizeStat: Invalid nil numDigits parameter detected")
return nil, DRL_RET_INTERNAL_ERROR
end -- if
if (value == nil) then
value = 0
end -- if
local prefix = ""
local suffix = ""
value = tonumber(value)
numDigits = tonumber(numDigits)
if (value == nil) or (numDigits == nil) then
dbot.warn("inv.items.colorizeStat: non-numeric parameter detected: value=\"" .. (value or "nil") ..
"\", numDigits=\"" .. (numDigits or "nil") .. "\"")
return nil
end -- if
invertColors = invertColors or false
if ((value < 0) and (invertColors == false)) or ((value > 0) and (invertColors == true)) then
prefix = DRL_ANSI_RED
suffix = DRL_ANSI_WHITE
elseif ((value > 0) and (invertColors == false)) or ((value < 0) and (invertColors == true)) then
prefix = DRL_ANSI_GREEN
suffix = DRL_ANSI_WHITE
end -- if
return string.format(prefix .. "%" .. numDigits .. "d" .. suffix, value)
end -- inv.items.colorizeStat
function inv.items.isInvis(objId)
local flags = inv.items.getStatField(objId, invStatFieldFlags) or ""
if dbot.isWordInString("invis", flags) or dbot.isWordInString("invis,", flags) then
return true
else
return false
end -- if
end -- inv.items.isInvis
----------------------------------------------------------------------------------------------------
--
-- Module to organize inventory items into containers based on queries assigned to containers
--
-- Each container may optionally have a set of item queries assigned to it. If we "organize" an
-- item that matches a query on a container, we move that item to the associated container. For
-- example, we might assign a container a query like "type Key || flag isKey" and then we can
-- automagically organize it to move all keys into the container. We could do similar things to
-- put all portals together into a container or all potions together. We can even use more
-- complicated queries that would put items of particular types and levels together.
--
-- Note: If an item matches queries on multiple containers, there currently is no way to specify
-- container priorities and the item could end up in any matching container. For now, it is
-- up to the user to not create conflicting container queries.
-- TODO: Check if there is any overlap in the item arrays returned from container queries and
-- warn the user. We could also implement a priority scheme for containers too, but that
-- seems like overkill...
--
-- dinv organize [add | clear] <container relative name> <query>
-- dinv organize [display]
-- dinv organize <query>
--
-- inv.items.organize.add(containerName, queryString, endTag)
-- inv.items.organize.addCR()
-- inv.items.organize.clear(containerName, endTag)
-- inv.items.organize.clearCR()
-- inv.items.organize.display(endTag)
--
-- inv.items.organize.cleanup(queryString, endTag)
-- inv.items.organize.cleanupCR()
--
----------------------------------------------------------------------------------------------------
inv.items.organize = {}
inv.items.organize.addPkg = nil
function inv.items.organize.add(containerName, queryString, endTag)
if (containerName == nil) or (containerName == "") then
dbot.warn("inv.items.organize.add: Missing container relative name")
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_INVALID_PARAM)
end -- if
-- We allow users to organize their entire inventory in one shot with an empty string that matches
-- everything. However, we do NOT allow a single container to own everything by giving a container
-- an empty organization query string. That almost certainly was not what the user intended.
if (queryString == nil) or (queryString == "") then
dbot.warn("inv.items.organize.add: Containers are not allowed to own all possible items (empty query)")
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.items.organize.addPkg ~= nil) then
dbot.info("Skipping add request in organize package: another add request is in progress")
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_BUSY)
end -- if
inv.items.organize.addPkg = {}
inv.items.organize.addPkg.container = containerName
inv.items.organize.addPkg.query = queryString
inv.items.organize.addPkg.endTag = endTag
wait.make(inv.items.organize.addCR)
return DRL_RET_SUCCESS
end -- inv.items.organize.add
function inv.items.organize.addCR()
local retval
local objId
local idArray
if (inv.items.organize.addPkg == nil) then
dbot.error("inv.items.organize.addCR: addPkg is nil!")
return inv.tags.stop(invTagsOrganize, "nil end tag", DRL_RET_INTERNAL_ERROR)
end -- if
-- Find the unique container specified by the user via a relative name (e.g., "2.bag")
idArray, retval = inv.items.searchCR("type container rname " .. inv.items.organize.addPkg.container)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.organize.addCR: failed to search inventory table: " .. dbot.retval.getString(retval))
elseif (#idArray ~= 1) then
-- There should only be a single match to the container's relative name (e.g., "2.bag")
dbot.warn("Container relative name \"" .. inv.items.organize.addPkg.container ..
"\" did not have a unique match: skipping organization query request")
else
-- We found a single unique match for the relative name
objId = idArray[1]
end -- if
local endTag = inv.items.organize.addPkg.endTag
-- Handle the error case where we couldn't find a matching container
if (objId == nil) then
inv.items.organize.addPkg = nil
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_MISSING_ENTRY)
end -- if
-- We have the container and a query for the container. Append the query to any previous organization
-- queries for that container.
local organizeField = inv.items.getStatField(objId, invQueryKeyOrganize) or ""
if (organizeField ~= "") then
organizeField = organizeField .. " || " -- on 2nd and subsequent queries, use the OR operator
end -- if
organizeField = organizeField .. inv.items.organize.addPkg.query
inv.items.setStatField(objId, invQueryKeyOrganize, organizeField)
inv.items.save()
dbot.note("Added organization query \"@C" .. inv.items.organize.addPkg.query .. "@W\" to container \"" ..
(inv.items.getField(objId, invFieldColorName) or "Unidentified"))
-- Clean up, print an end tag (if necessary), and return
inv.items.organize.addPkg = nil
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_SUCCESS)
end -- inv.items.organize.addCR
inv.items.organize.clearPkg = nil
function inv.items.organize.clear(containerName, endTag)
if (containerName == nil) or (containerName == "") then
dbot.warn("inv.items.organize.clear: Missing container relative name")
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.items.organize.clearPkg ~= nil) then
dbot.info("Skipping clear request in organize package: another clear request is in progress")
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_BUSY)
end -- if
inv.items.organize.clearPkg = {}
inv.items.organize.clearPkg.container = containerName
inv.items.organize.clearPkg.endTag = endTag
wait.make(inv.items.organize.clearCR)
return DRL_RET_SUCCESS
end -- inv.items.organize.clear
function inv.items.organize.clearCR()
local retval
local objId
local idArray
if (inv.items.organize.clearPkg == nil) then
dbot.error("inv.items.organize.clearCR: clearPkg is nil!")
return inv.tags.stop(invTagsOrganize, "nil end tag", DRL_RET_INTERNAL_ERROR)
end -- if
-- Find the unique container specified by the user via a relative name (e.g., "2.bag")
idArray, retval = inv.items.searchCR("type container rname " .. inv.items.organize.clearPkg.container)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.organize.clearCR: failed to search inventory table: " .. dbot.retval.getString(retval))
elseif (#idArray ~= 1) then
-- There should only be a single match to the container's relative name (e.g., "2.bag")
dbot.warn("Container relative name \"" .. inv.items.organize.clearPkg.container ..
"\" did not have a unique match: skipping organization query request")
else
-- We found a single unique match for the relative name
objId = idArray[1]
end -- if
local endTag = inv.items.organize.clearPkg.endTag
-- Handle the error case where we couldn't find a matching container
if (objId == nil) then
inv.items.organize.clearPkg = nil
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_MISSING_ENTRY)
end -- if
-- We have the container. Whack it!
inv.items.setStatField(objId, invQueryKeyOrganize, "")
inv.items.save()
dbot.note("Cleared all organization queries from container \"" ..
(inv.items.getField(objId, invFieldColorName) or "Unidentified") .. DRL_ANSI_WHITE .. "@W\"")
-- Clean up, print an end tag (if necessary) and return
inv.items.organize.clearPkg = nil
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_SUCCESS)
end -- inv.items.organize.clearCR
function inv.items.organize.display(endTag)
local foundContainerWithQuery = false
local retval = DRL_RET_SUCCESS
dbot.print("@WContainers that have associated organizational queries:@w")
for objId, _ in pairs(inv.items.table) do
local organizeQuery = inv.items.getStatField(objId, invQueryKeyOrganize) or ""
if (organizeQuery ~= "") then
dbot.print("@W \"" .. (inv.items.getField(objId, invFieldColorName) or "Unidentified") ..
DRL_ANSI_WHITE .. "@W\": @C" .. organizeQuery .. "@w")
foundContainerWithQuery = true
end -- if
end -- for
if (foundContainerWithQuery == false) then
dbot.print("@W No containers with organizational queries were found")
retval = DRL_RET_MISSING_ENTRY
end -- if
return inv.tags.stop(invTagsOrganize, endTag, retval)
end -- inv.items.organize.display
inv.items.organize.cleanupPkg = nil
function inv.items.organize.cleanup(queryString, endTag)
if (inv.items.organize.cleanupPkg ~= nil) then
dbot.info("Skipping request to organize inventory: another request is in progress")
return inv.tags.stop(invTagsOrganize, endTag, DRL_RET_BUSY)
end -- if
inv.items.organize.cleanupPkg = {}
inv.items.organize.cleanupPkg.query = queryString
inv.items.organize.cleanupPkg.endTag = endTag
wait.make(inv.items.organize.cleanupCR)
return DRL_RET_SUCCESS
end -- inv.items.organize.cleanup
function inv.items.organize.cleanupCR()
local invIdArray
local retval
if (inv.items.organize.cleanupPkg == nil) then
dbot.error("inv.items.organize.cleanupCR: cleanupPkg is nil!")
return inv.tags.stop(invTagsOrganize, "nil end tag", DRL_RET_INTERNAL_ERROR)
end -- if
local endTag = inv.items.organize.cleanupPkg.endTag
-- Track how many items we move due to organization. It's handy to report this when we're done.
local numItemsOrganized = 0
-- Find all items that match the given inventory query
invIdArray, retval = inv.items.searchCR(inv.items.organize.cleanupPkg.query)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.organize.cleanupCR: failed to search inventory table: " ..
dbot.retval.getString(retval))
inv.items.organize.cleanupPkg = nil
return inv.tags.stop(invTagsOrganize, endTag, retval)
end -- if
-- For each container that has an organization query associated with it, find all items that
-- match that query. Any item that appears in both the container's ID array and the inventory
-- ID array belongs to the container and should be moved there.
for objId, _ in pairs(inv.items.table) do
local organizeQuery = inv.items.getStatField(objId, invQueryKeyOrganize) or ""
if (organizeQuery ~= "") then
local containerIdArray, retval = inv.items.searchCR(organizeQuery)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.organize.cleanupCR: failed to search inventory table: " ..
dbot.retval.getString(retval))
inv.items.organize.cleanupPkg = nil
return inv.tags.stop(invTagsOrganize, endTag, retval)
end -- if
local commandArray = dbot.execute.new()
-- This n^2 algorithm isn't efficient, but I don't think the speed is an issue for us
for _, invId in ipairs(invIdArray) do
for _, containerId in ipairs(containerIdArray) do
-- Note that we don't want to try sorting containers into other containers
if (invId == containerId) and
(inv.items.getStatField(invId, invStatFieldType) ~= invmon.typeStr[invmonTypeContainer]) then
dbot.debug("Found item to organize: \"" ..
(inv.items.getField(invId, invFieldColorName) or "Unidentified") ..
DRL_ANSI_WHITE .. "@W\"")
-- If the item isn't already in the container, move it there
local itemLoc = inv.items.getField(invId, invFieldObjLoc)
if (itemLoc ~= nil) and (itemLoc ~= "") and (itemLoc ~= objId) then
retval = inv.items.putItem(invId, objId, commandArray, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.items.organize.cleanupCR: failed to put item \"" ..
(inv.items.getField(invId, invFieldColorName) or "Unidentified") ..
"\" in container \"" ..
(inv.items.getField(objId, invFieldColorName) or "Unidentified") .. "\": " ..
dbot.retval.getString(retval))
break
else
numItemsOrganized = numItemsOrganized + 1
end -- if
if (commandArray ~= nil) and (#commandArray >= inv.items.burstSize) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to organize items: " .. dbot.retval.getString(retval))
break
end -- if
commandArray = dbot.execute.new()
end -- if
end -- if
end -- if
end -- for
end -- for
-- Flush any commands in the array that still need to be sent to the mud
if (retval == DRL_RET_SUCCESS) and (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.info("Skipping request to get items: " .. dbot.retval.getString(retval))
break
end -- if
end -- if
if dbot.gmcp.statePreventsActions() then
dbot.info("Skipping organize request: character's state does not allow actions")
retval = DRL_RET_NOT_ACTIVE
break
end -- if
end -- if
end -- for
-- Report the results
if (retval == DRL_RET_SUCCESS) then
local suffix = ""
if (numItemsOrganized ~= 1) then
suffix = "s"
end -- if
dbot.info("Organized " .. numItemsOrganized .. " item" .. suffix .. " matching query \"@C" ..
inv.items.organize.cleanupPkg.query .. "@W\"")
end -- if
-- Clean up the function, print an end tag (if necessary), and return
inv.items.organize.cleanupPkg = nil
return inv.tags.stop(invTagsOrganize, endTag, retval)
end -- inv.items.organize.cleanupCR
----------------------------------------------------------------------------------------------------
-- inv.items.trigger: Trigger functions for the inv.items module
--
-- Functions:
--
-- Get or put items
-- inv.items.trigger.get
-- inv.items.trigger.put
-- inv.items.trigger.getKeyring
-- inv.items.trigger.putKeyring
--
-- Wear or remove items
-- inv.items.trigger.wear
-- inv.items.trigger.remove
--
-- Parses identify, auction, or shop items
-- inv.items.trigger.itemIdStart
-- inv.items.trigger.itemIdStats
-- inv.items.trigger.itemIdEnd
--
-- Quick and dirty parse of item to get the item's object ID
-- inv.items.trigger.idItem
--
-- Parses eqdata, invdata, keyring data; itemDataStats is also re-used to handle invitem
-- inv.items.trigger.itemDataStart
-- inv.items.trigger.itemDataStats
-- inv.items.trigger.itemDataEnd
--
-- Parse invmon output
-- inv.items.trigger.invmon
--
----------------------------------------------------------------------------------------------------
inv.items.trigger = {}
inv.items.trigger.wearSpecialName = "drlInvItemsTriggerWearSpecial"
inv.items.trigger.wearName = "drlInvItemsTriggerWear"
inv.items.trigger.removeName = "drlInvItemsTriggerRemove"
inv.items.trigger.getName = "drlInvItemsTriggerGet"
inv.items.trigger.putName = "drlInvItemsTriggerPut"
inv.items.trigger.getKeyringName = "drlInvItemsTriggerGetKeyring"
inv.items.trigger.putKeyringName = "drlInvItemsTriggerPutKeyring"
inv.items.trigger.itemIdStartName = "drlInvItemsTriggerIdStart"
inv.items.trigger.itemIdStatsName = "drlInvItemsTriggerIdStats"
inv.items.trigger.itemIdEndName = "drlInvItemsTriggerIdEnd"
inv.items.trigger.suppressIdMsgName = "drlInvItemsTriggerSuppressIdMsg" -- suppress output for lore, etc.
function inv.items.trigger.wear(line)
if (line ~= nil) and (line ~= "") then
dbot.debug("Triggered \"wear\" output on: " .. line)
if (string.find(line, "You do not have that item")) then
dbot.debug("inv.items.trigger.wear: Failed to wear item: You do not have that item.")
end -- if
end -- if
end -- inv.items.trigger.wear
function inv.items.trigger.remove(line)
if (line ~= nil) and (line ~= "") then
dbot.debug("Triggered \"remove\" output on: " .. line)
if (string.find(line, "You are not wearing that item")) then
dbot.debug("inv.items.trigger.wear: Failed to wear item: You are not wearing that item.")
end -- if
end -- if
end -- inv.items.trigger.remove
function inv.items.trigger.get(line)
if (line ~= nil) and (line ~= "") then
dbot.debug("Triggered \"get\" output on: " .. line)
if (string.find(line, "You do not see")) then
dbot.debug("inv.items.trigger.get: Failed to get item")
end -- if
end -- if
end -- inv.items.trigger.get
function inv.items.trigger.put(line)
if (line ~= nil) and (line ~= "") then
dbot.debug("Triggered \"put\" output on: " .. line)
if (string.find(line, "You don't have that")) then
dbot.debug("inv.items.trigger.put: Failed to put item because it is not in your inventory.")
elseif (string.find(line, "You do not see")) then
dbot.debug("inv.items.trigger.put: Failed to put item because the container is not in your inventory.")
end -- if
end -- if
end -- inv.items.trigger.put
function inv.items.trigger.getKeyring(line)
if (line ~= nil) and (line ~= "") then
dbot.debug("Triggered \"keyring get\" output on: " .. line)
local errMsg = "You did not find that"
if (string.find(line, errMsg)) then
dbot.debug("inv.items.trigger.getKeyring: Failed to get keyring item: " .. errMsg)
end -- if
end -- if
end -- inv.items.trigger.getKeyring
function inv.items.trigger.putKeyring(line)
if (line ~= nil) and (line ~= "") then
dbot.debug("Triggered \"keyring put\" output on: " .. line)
local errMsg = "You do not have that item"
if (string.find(line, errMsg)) then
dbot.debug("inv.items.trigger.putKeyring: Failed to put keyring item: " .. errMsg)
end -- if
end -- if
end -- inv.items.trigger.putKeyring
function inv.items.trigger.itemIdStart(line)
if (line == "You do not have that item.") or
string.find(line, "currently holds no inventory") or
string.find(line, "There is no auction item with that id") or
string.find(line, "There is no marketplace item with that id") or
string.find(line, "does not have that item for sale") then
inv.items.trigger.itemIdEnd()
return DRL_RET_MISSING_ENTRY
end -- if
if (inv.items.identifyPkg == nil) then
return DRL_INTERNAL_ERROR
end -- if
-- Clear the ID level field. If we detect a partial identification this time, we
-- flag the item as having a partial ID. If we don't detect a partial identification,
-- we flag it as having a full ID when we hit the end trigger.
inv.items.setField(inv.items.identifyPkg.objId, invFieldIdentifyLevel, invIdLevelNone)
-- Start watching for stat lines in the item description
EnableTrigger(inv.items.trigger.itemIdStatsName, true)
-- Watch for the end of the item description so that we can stop scanning
AddTriggerEx(inv.items.trigger.itemIdEndName,
"^$",
"inv.items.trigger.itemIdEnd()",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput + trigger_flag.OneShot,
custom_colour.Custom11,
0, "", "", sendto.script, 0)
end -- inv.items.trigger.itemIdStart
flagsContinuation = false
affectModsContinuation = false
function inv.items.trigger.itemIdStats(line)
dbot.debug("stats for item " .. inv.items.identifyPkg.objId .. ":\"" .. line .. "\"")
local isPartialId, id, name, level, weight, wearable, score, keywords, itemType, worth, flags,
affectMods, continuation, material, foundAt, ownedBy, clan
isPartialId = string.find(line, "A full appraisal will reveal further information on this item")
_, _, id = string.find(line, "Id%s+:%s+(%d+)%s+")
_, _, name = string.find(line, "Name%s+:%s+(.-)%s+|")
_, _, level = string.find(line, "Level%s+:%s+(%d+)%s+")
_, _, weight = string.find(line, "Weight%s+:%s+([0-9,-]+)%s+")
_, _, wearable = string.find(line, "Wearable%s+:%s+(.*) %s+")
_, _, score = string.find(line, "Score%s+:%s([0-9,]+)%s+")
_, _, keywords = string.find(line, "Keywords%s+:%s+(.-)%s+|")
_, _, itemType = string.find(line, "| Type%s+:%s+(%a+)%s+")
_, _, worth = string.find(line, "Worth%s+:%s+([0-9,]+)%s+")
_, _, flags = string.find(line, "Flags%s+:%s+(.-)%s+|")
_, _, affectMods = string.find(line, "Affect Mods:%s+(.-)%s+|")
_, _, continuation = string.find(line, "|%s+:%s+(.-)%s+|")
_, _, material = string.find(line, "Material%s+:%s+(.*)%s+")
_, _, foundAt = string.find(line, "Found at%s+:%s+(.-)%s+|")
_, _, ownedBy = string.find(line, "Owned By%s+:%s+(.-)%s+|")
_, _, clan = string.find(line, "Clan Item%s+:%s+(.-)%s+|")
-- Potions, pills, wands, and staves
local spellUses, spellLevel, spellName
_, _, spellUses, spellLevel, spellName = string.find(line, "([0-9]+) uses? of level ([0-9]+) '(.*)'")
-- Portal-only fields
local leadsTo
_, _, leadsTo = string.find(line, "Leads to%s+:%s+(.*)%s+")
-- Container-only fields
local capacity, holding, heaviestItem, itemsInside, totWeight, itemBurden, weightReduction
_, _, capacity = string.find(line, "Capacity%s+:%s+([0-9,]+)%s+")
_, _, holding = string.find(line, "Holding%s+:%s+([0-9,]+)%s+")
_, _, heaviestItem = string.find(line, "Heaviest Item:%s+([0-9,]+)%s+")
_, _, itemsInside = string.find(line, "Items Inside%s+:%s+([0-9,]+)%s+")
_, _, totWeight = string.find(line, "Tot Weight%s+:%s+([0-9,-]+)%s+")
_, _, itemBurden = string.find(line, "Item Burden%s+:%s+([0-9,]+)%s+")
_, _, weightReduction = string.find(line, "Items inside weigh (%d+). of their usual weight%s+")
local int, wis, luck, str, dex, con
_, _, int = string.find(line, "Intelligence%s+:%s+([+-]?%d+)%s+")
_, _, wis = string.find(line, "Wisdom%s+:%s+([+-]?%d+)%s+")
_, _, luck = string.find(line, "Luck%s+:%s+([+-]?%d+)%s+")
_, _, str = string.find(line, "Strength%s+:%s+([+-]?%d+)%s+")
_, _, dex = string.find(line, "Dexterity%s+:%s+([+-]?%d+)%s+")
_, _, con = string.find(line, "Constitution%s+:%s+([+-]?%d+)%s+")
local hp, mana, moves
_, _, hp = string.find(line, "Hit points%s+:%s+([+-]?%d+)%s+")
_, _, mana = string.find(line, "Mana%s+:%s+([+-]?%d+)%s+")
_, _, moves = string.find(line, "Moves%s+:%s+([+-]?%d+)%s+")
local hit, dam
_, _, hit = string.find(line, "Hit roll%s+:%s+([+-]?%d+)%s+")
_, _, dam = string.find(line, "Damage roll%s+:%s+([+-]?%d+)%s+")
local allphys, allmagic
_, _, allphys = string.find(line, "All physical%s+:%s+([+-]?%d+)%s+")
_, _, allmagic = string.find(line, "All magic%s+:%s+([+-]?%d+)%s+")
local acid, cold, energy, holy, electric, negative, shadow, magic, air, earth, fire, light, mental,
sonic, water, poison, disease
_, _, acid = string.find(line, "Acid%s+:%s+([+-]?%d+)%s+")
_, _, cold = string.find(line, "Cold%s+:%s+([+-]?%d+)%s+")
_, _, energy = string.find(line, "Energy%s+:%s+([+-]?%d+)%s+")
_, _, holy = string.find(line, "Holy%s+:%s+([+-]?%d+)%s+")
_, _, electric = string.find(line, "Electric%s+:%s+([+-]?%d+)%s+")
_, _, negative = string.find(line, "Negative%s+:%s+([+-]?%d+)%s+")
_, _, shadow = string.find(line, "Shadow%s+:%s+([+-]?%d+)%s+")
_, _, magic = string.find(line, "Magic%s+:%s+([+-]?%d+)%s+")
_, _, air = string.find(line, "Air%s+:%s+([+-]?%d+)%s+")
_, _, earth = string.find(line, "Earth%s+:%s+([+-]?%d+)%s+")
_, _, fire = string.find(line, "Fire%s+:%s+([+-]?%d+)%s+")
_, _, light = string.find(line, "Light%s+:%s+([+-]?%d+)%s+")
_, _, mental = string.find(line, "Mental%s+:%s+([+-]?%d+)%s+")
_, _, sonic = string.find(line, "Sonic%s+:%s+([+-]?%d+)%s+")
_, _, water = string.find(line, "Water%s+:%s+([+-]?%d+)%s+")
_, _, poison = string.find(line, "Poison%s+:%s+([+-]?%d+)%s+")
_, _, disease = string.find(line, "Disease%s+:%s+([+-]?%d+)%s+")
local slash, pierce, bash
_, _, slash = string.find(line, "Slash%s+:%s+([+-]?%d+)%s+")
_, _, pierce = string.find(line, "Pierce%s+:%s+([+-]?%d+)%s+")
_, _, bash = string.find(line, "Bash%s+:%s+([+-]?%d+)%s+")
local avedam, inflicts, damtype, weaponType, specials
_, _, avedam = string.find(line, "Average Dam%s+:%s+(%d+)%s+")
_, _, inflicts = string.find(line, "Inflicts%s+:%s+(%a+)%s+")
_, _, damtype = string.find(line, "Damage Type%s+:%s+(%a+)%s+")
_, _, weaponType = string.find(line, "Weapon Type:%s+(%a+)%s+")
_, _, specials = string.find(line, "Specials%s+:%s+(%a+)%s+")
if (id ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldId, dbot.tonumber(id or ""))
dbot.debug("Id = \"" .. id .. "\"")
end -- if
if (name ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldName, name)
dbot.debug("Name = \"" .. name .. "\"")
end -- if
if (level ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldLevel, dbot.tonumber(level or ""))
dbot.debug("Level = \"" .. level .. "\"")
end -- if
if (weight ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldWeight, dbot.tonumber(weight or ""))
dbot.debug("Weight = \"" .. weight .. "\"")
end -- if
if (wearable ~= nil) then
wearable = Trim(wearable)
-- Portals can be either "hold" or "portal" depending on whether or not you have the portal wish
if (wearable == "hold, portal") then
if dbot.wish.has("Portal") then
wearable = inv.wearLoc[invWearableLocPortal]
else
wearable = inv.wearLoc[invWearableLocHold]
end -- if
end -- if
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldWearable, wearable)
dbot.debug("Wearable = \"" .. wearable .. "\"")
end -- if
if (score ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldScore, dbot.tonumber(score or ""))
dbot.debug("Score = \"" .. score .. "\"")
end -- if
if (keywords ~= nil) then
-- Merge this with any previous keywords. Someone may have added custom keywords to the
-- item and then re-identified it for some reason. For example, someone may have toggled the
-- keep flag which would cause invitem to flag the item to be re-identified.
local oldKeywords = inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldKeywords) or ""
local mergedKeywords = dbot.mergeFields(keywords, oldKeywords) or keywords
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldKeywords, mergedKeywords)
dbot.debug("Keywords = \"" .. mergedKeywords .. "\"")
end -- if
if (itemType ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldType, itemType)
dbot.debug("Type = \"" .. itemType .. "\"")
end -- if
if (worth ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldWorth, dbot.tonumber(worth))
dbot.debug("Worth = \"" .. worth .. "\"")
end -- if
if (isPartialId ~= nil) then
inv.items.setField(inv.items.identifyPkg.objId, invFieldIdentifyLevel, invIdLevelPartial)
dbot.debug("Id level = \"" .. invIdLevelPartial .. "\"")
end -- if
if (flags ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldFlags, flags)
dbot.debug("Flags = \"" .. flags .. "\"")
-- If the flags are continued (they end in a ",") watch for the continuation
if (string.find(flags, ",$")) then
flagsContinuation = true
else
flagsContinuation = false
end -- if
end -- if
if (affectMods ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldAffectMods, affectMods)
dbot.debug("AffectMods = \"" .. affectMods .. "\"")
-- If the affectMods are continued (they end in a ",") watch for the continuation
if (string.find(affectMods, ",$")) then
affectModsContinuation = true
else
affectModsContinuation = false
end -- if
end -- if
if (continuation ~= nil) then
dbot.debug("Continuation = \"" .. continuation .. "\"")
if (flagsContinuation) then
-- Add the continuation to the existing flags
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldFlags,
(inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldFlags) or "") ..
" " .. continuation)
-- If the continued flags end in a comma, keep the continuation going; otherwise stop it
if not (string.find(continuation, ",$")) then
flagsContinuation = false
end -- if
elseif (affectModsContinuation) then
-- Add the continuation to the existing affectMods
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldAffectMods,
(inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldAffectMods)
or "") .. " " .. continuation)
-- If the continued affectMods end in a comma, keep the continuation going; otherwise stop it
if not (string.find(continuation, ",$")) then
affectModsContinuation = false
end -- if
else
-- Placeholder to add continuation support for other things (notes? others?)
end -- if
end -- if
if (material ~= nil) then
material = Trim(material)
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldMaterial, material)
dbot.debug("Material = \"" .. material .. "\"")
end -- if
if (foundAt ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldFoundAt, foundAt)
dbot.debug("Found at = \"" .. foundAt .. "\"")
end -- if
if (ownedBy ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldOwnedBy, ownedBy)
dbot.debug("Found at = \"" .. ownedBy .. "\"")
end -- if
if (clan ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldClan, clan)
dbot.debug("From clan \"" .. clan .. "\"")
end -- if
if (spellUses ~= nil) and (spellLevel ~= nil) and (spellName ~= nil) then
local spellArray = inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldSpells) or {}
spellUses = tonumber(spellUses) or 0
-- If we already have an entry for this spell, update the count
local foundSpellMatch = false
for _, v in ipairs(spellArray) do
if (v.level == spellLevel) and (v.name == spellName) then
v.count = v.count + spellUses
foundSpellMatch = true
break
end -- if
end -- if
-- If we don't have an entry yet for this spell, add one
if (foundSpellMatch == false) then
table.insert(spellArray, { level=spellLevel, name=spellName, count=spellUses })
end -- if
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldSpells, spellArray)
end -- if
if (leadsTo ~= nil) then
leadsTo = Trim(leadsTo)
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldLeadsTo, leadsTo)
dbot.debug("Leads to = \"" .. leadsTo .. "\"")
end -- if
-- Container stats
if (capacity ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldCapacity, dbot.tonumber(capacity))
dbot.debug("Capacity = \"" .. capacity .. "\"")
end -- if
if (holding ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldHolding, dbot.tonumber(holding))
dbot.debug("Holding = \"" .. holding .. "\"")
end -- if
if (heaviestItem ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldHeaviestItem, dbot.tonumber(heaviestItem))
dbot.debug("Container heaviest item = \"" .. heaviestItem .. "\"")
end -- if
if (itemsInside ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldItemsInside, dbot.tonumber(itemsInside))
dbot.debug("Container items inside = \"" .. itemsInside .. "\"")
end -- if
if (totWeight ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldTotWeight, dbot.tonumber(totWeight))
dbot.debug("Container total weight = \"" .. totWeight .. "\"")
end -- if
if (itemBurden ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldItemBurden, dbot.tonumber(itemBurden))
dbot.debug("Container item burden = \"" .. itemBurden .. "\"")
end -- if
if (weightReduction ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldWeightReduction,
dbot.tonumber(weightReduction))
dbot.debug("Container weight reduction = \"" .. weightReduction .. "\"")
end -- if
if (int ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldInt, dbot.tonumber(int))
dbot.debug("int = \"" .. int .. "\"")
end -- if
if (wis ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldWis, dbot.tonumber(wis))
dbot.debug("wis = \"" .. wis .. "\"")
end -- if
if (luck ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldLuck, dbot.tonumber(luck))
dbot.debug("luck = \"" .. luck .. "\"")
end -- if
if (str ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldStr, dbot.tonumber(str))
dbot.debug("str = \"" .. str .. "\"")
end -- if
if (dex ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldDex, dbot.tonumber(dex))
dbot.debug("dex = \"" .. dex .. "\"")
end -- if
if (con ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldCon, dbot.tonumber(con))
dbot.debug("con = \"" .. con .. "\"")
end -- if
if (hp ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldHP, dbot.tonumber(hp))
dbot.debug("hp = \"" .. hp .. "\"")
end -- if
if (mana ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldMana, dbot.tonumber(mana))
dbot.debug("mana = \"" .. mana .. "\"")
end -- if
if (moves ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldMoves, dbot.tonumber(moves))
dbot.debug("moves = \"" .. moves .. "\"")
end -- if
if (hit ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldHit, dbot.tonumber(hit))
dbot.debug("hit = \"" .. hit .. "\"")
end -- if
if (dam ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldDam, dbot.tonumber(dam))
dbot.debug("dam = \"" .. dam .. "\"")
end -- if
if (allphys ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldAllPhys, dbot.tonumber(allphys))
dbot.debug("allphys = \"" .. allphys .. "\"")
end -- if
if (allmagic ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldAllMagic, dbot.tonumber(allmagic))
dbot.debug("allmagic = \"" .. allmagic .. "\"")
end -- if
if (acid ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldAcid, dbot.tonumber(acid))
dbot.debug("acid = \"" .. acid .. "\"")
end -- if
if (cold ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldCold, dbot.tonumber(cold))
dbot.debug("cold = \"" .. cold .. "\"")
end -- if
if (energy ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldEnergy, dbot.tonumber(energy))
dbot.debug("energy = \"" .. energy .. "\"")
end -- if
if (holy ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldHoly, dbot.tonumber(holy))
dbot.debug("holy = \"" .. holy .. "\"")
end -- if
if (electric ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldElectric, dbot.tonumber(electric))
dbot.debug("electric = \"" .. electric .. "\"")
end -- if
if (negative ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldNegative, dbot.tonumber(negative))
dbot.debug("negative = \"" .. negative .. "\"")
end -- if
if (shadow ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldShadow, dbot.tonumber(shadow))
dbot.debug("shadow = \"" .. shadow .. "\"")
end -- if
if (magic ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldMagic, dbot.tonumber(magic))
dbot.debug("magic = \"" .. magic .. "\"")
end -- if
if (air ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldAir, dbot.tonumber(air))
dbot.debug("air = \"" .. air .. "\"")
end -- if
if (earth ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldEarth, dbot.tonumber(earth))
dbot.debug("earth = \"" .. earth .. "\"")
end -- if
if (fire ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldFire, dbot.tonumber(fire))
dbot.debug("fire = \"" .. fire .. "\"")
end -- if
if (light ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldLight, dbot.tonumber(light))
dbot.debug("light = \"" .. light .. "\"")
end -- if
if (mental ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldMental, dbot.tonumber(mental))
dbot.debug("mental = \"" .. mental .. "\"")
end -- if
if (sonic ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldSonic, dbot.tonumber(sonic))
dbot.debug("sonic = \"" .. sonic .. "\"")
end -- if
if (water ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldWater, dbot.tonumber(water))
dbot.debug("water = \"" .. water .. "\"")
end -- if
if (poison ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldPoison, dbot.tonumber(poison))
dbot.debug("poison = \"" .. poison .. "\"")
end -- if
if (disease ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldDisease, dbot.tonumber(disease))
dbot.debug("disease = \"" .. disease .. "\"")
end -- if
if (slash ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldSlash, dbot.tonumber(slash))
dbot.debug("slash = \"" .. slash .. "\"")
end -- if
if (pierce ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldPierce, dbot.tonumber(pierce))
dbot.debug("pierce = \"" .. pierce .. "\"")
end -- if
if (bash ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldBash, dbot.tonumber(bash))
dbot.debug("bash = \"" .. bash .. "\"")
end -- if
if (avedam ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldAveDam, dbot.tonumber(avedam))
dbot.debug("avedam = \"" .. avedam .. "\"")
end -- if
if (inflicts ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldInflicts, inflicts)
dbot.debug("inflicts = \"" .. inflicts .. "\"")
end -- if
if (damtype ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldDamType, damtype)
dbot.debug("damtype = \"" .. damtype .. "\"")
end -- if
if (weaponType ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldWeaponType, weaponType)
dbot.debug("weaponType = \"" .. weaponType .. "\"")
end -- if
if (specials ~= nil) then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldSpecials, specials)
dbot.debug("specials = \"" .. specials .. "\"")
end -- if
end -- inv.items.trigger.itemIdStats
function inv.items.trigger.itemIdEnd()
-- We are at the end of the identification trigger process. We no longer need the
-- identification timeout timer and we don't want it going off later. The deletion
-- can fail if we are here because the timeout timer called this function. However,
-- in that case, the timer will go away anyway because it is a one-shot so we don't
-- care if the deletion fails.
dbot.deleteTimer(inv.items.timer.idTimeoutName)
-- We are done id'ing this item so disable the item's trigger
EnableTrigger(inv.items.trigger.itemIdStartName, false)
EnableTrigger(inv.items.trigger.itemIdStatsName, false)
-- Because I'm paranoid...
if (inv.items.identifyPkg == nil) then
dbot.debug("inv.items.trigger.itemIdEnd: identify package is nil!")
return DRL_RET_INTERNAL_ERROR
end -- if
-- Check if something interferred with the identification. The "dbot.execute" package
-- guarantees that the user can't manually try to ID something at the same moment we
-- are doing our background identification. In theory, there shouldn't be any potential
-- conflict here (i.e., we get back ID results for a different item). However, it's
-- probably still helpful to check if the ID we get back matches what we expect. I'd
-- rather find out what is happening the easy way than the hard way...
-- Note: Auctions and shop items are not ID'ed with their objId (we don't know the objID
-- until identification completes) so we don't worry about this check for those cases.
local objId = inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldId)
local objLoc = inv.items.getField(inv.items.identifyPkg.objId, invFieldObjLoc)
if (objId ~= inv.items.identifyPkg.objId) and (objLoc ~= invItemLocAuction) and
(objLoc ~= invItemLocShopkeeper) then
dbot.debug("Identification wasn't successful for item " .. inv.items.identifyPkg.objId ..
": Try again later...")
inv.items.identifyPkg = nil
return DRL_RET_BUSY
end -- if
-- If we made it through the identification process without discovering we have a
-- partial identification (e.g., "A full appraisal will reveal further information...")
-- then we flag it as having passed a full identification. As a precaution, we also
-- verify, at a minimum, that we were able to detect the object's ID. If something
-- went wrong during the item's identification then the item's stat table might be empty.
-- In that case, we want to leave the identifyLevel at "none" so that we re-identify the
-- item on the next scan.
if (inv.items.getField(inv.items.identifyPkg.objId, invFieldIdentifyLevel) == invIdLevelNone) then
inv.items.setField(inv.items.identifyPkg.objId, invFieldIdentifyLevel, invIdLevelFull)
end -- if
local itemType = inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldType) or ""
local itemName = inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldName) or ""
local itemWearable = inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldWearable) or ""
local affectMods = inv.items.getStatField(inv.items.identifyPkg.objId, invStatFieldAffectMods) or ""
-- Add "pseudo-stats" based on item effects (aard terminology is "affect mods") and on
-- skills specific to particular items or item types. These make it easier to score
-- items and item sets.
if (affectMods ~= "") then
-- Strip out commas from the list so that we can easily pull mod words out of the string
local modList = string.gsub(affectMods, ",", ""):lower()
for mod in modList:gmatch("%S+") do
inv.items.setStatField(inv.items.identifyPkg.objId, mod, 1)
end -- for
end -- if
-- Add one-off item skills that are not officially affectMods but are very similar
if (itemName == "Aardwolf Bracers of Iron Grip") then
inv.items.setStatField(inv.items.identifyPkg.objId, invItemEffectsIronGrip, 1)
elseif (itemName == "Aardwolf Gloves of Dexterity") then
inv.items.setStatField(inv.items.identifyPkg.objId, invItemEffectsDualWield, 1)
elseif (itemWearable == "shield") then
inv.items.setStatField(inv.items.identifyPkg.objId, invItemEffectsShield, 1)
end -- if
-- Some items have item types that simply don't make sense. For example, many aarditems that
-- are armor are labeled with type "Treasure". Although they could certainly be considered
-- treasure, I think it makes more sense to treat them as armor. Otherwise someone searching
-- for armor in the inventory table won't find them. We use a kludge here to "fix" this...
--[[ TODO: do we really want this?
if (itemType == "Treasure") then
if (itemName == "Aardwolf Aura of Sanctuary") or
(itemName == "Aardwolf Ring of Regeneration") or
(itemName == "Aardwolf Gloves of Dexterity") or
(itemName == "Aardwolf Bracers of Iron Grip") or
(itemName == "a ring of the Dark Eight") then
inv.items.setStatField(inv.items.identifyPkg.objId, invStatFieldType, "Armor")
end -- if
end -- if
--]]
-- The identification process is done!
inv.items.identifyPkg = nil
end -- inv.items.trigger.itemIdEnd
inv.items.trigger.idItemName = "drlInvItemsTriggerIdItem"
inv.items.trigger.lastLineHyphens = false
function inv.items.trigger.idItem(line)
local _, _, id = string.find(line, "Id%s+:%s+(%d+)%s+")
if (id ~= nil) then
inv.lastIdentifiedObjectId = id
end -- if
if (line == "You do not have that item.") then
dbot.debug("You do not have the relative item.")
inv.lastIdentifiedObjectId = 0
elseif (inv.items.trigger.lastLineHyphens == true) and ((line == nil) or (line == "")) then
EnableTrigger(inv.items.trigger.idItemName, false)
dbot.debug("Disabling ID trigger on line=\"" .. line .. "\"")
end -- if
if (string.find(line, "^[+][-]+[+]")) then
inv.items.trigger.lastLineHyphens = true
else
inv.items.trigger.lastLineHyphens = false
end -- if
end -- inv.items.trigger.idItem
inv.items.trigger.itemDataStartName = "drlInvItemsTriggerItemDataStart"
inv.items.trigger.itemDataStatsName = "drlInvItemsTriggerItemDataStats"
inv.items.trigger.itemDataEndName = "drlInvItemsTriggerItemDataEnd"
function inv.items.trigger.itemDataStart(dataType, containerId)
assert((inv.items.discoverPkg ~= nil), "Discovery start trigger executed when discovery is not in progress")
-- We are scanning worn items, main inventory items, keyring items, or a container
if (dataType == "eqdata") then
inv.items.discoverPkg.loc = invItemLocWorn
elseif (dataType == "invdata") then
containerIdNum = tonumber(containerId)
if (containerIdNum == nil) then
inv.items.discoverPkg.loc = invItemLocInventory
else
inv.items.discoverPkg.loc = containerId
end -- if
elseif (dataType == "keyring") then
inv.items.discoverPkg.loc = invItemLocKeyring
else
dbot.error("inv.items.trigger.itemDataStart: invalid dataType parameter")
inv.items.trigger.itemDataEnd() -- clean up state
return DRL_RET_INTERNAL_ERROR
end -- if
-- Watch for the eqdata, invdata, or keyring end tag so that we can stop scanning
AddTriggerEx(inv.items.trigger.itemDataEndName,
"^{/(eqdata|invdata|keyring)}$",
"inv.items.trigger.itemDataEnd()",
drlTriggerFlagsBaseline + trigger_flag.OneShot + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0)
-- Start watching for eqdata or invdata stat lines in the item description
EnableTrigger(inv.items.trigger.itemDataStatsName, true)
return DRL_RET_SUCCESS
end -- inv.items.trigger.itemDataStart
function inv.items.trigger.itemDataStats(objId, flags, itemName, level, typeField, unique, wearLoc,
timer, isInvItem)
local retval = DRL_RET_SUCCESS
-- Verify the input params exist
assert(objId ~= nil, "invitem objectId is nil")
assert(flags ~= nil, "invitem flags is nil")
assert(itemName ~= nil, "invitem itemName is nil")
assert(level ~= nil, "invitem level is nil")
assert(typeField ~= nil, "invitem typeField is nil")
assert(unique ~= nil, "invitem unique is nil")
assert(wearLoc ~= nil, "invitem wear location is nil")
assert(timer ~= nil, "invitem timer is nil")
assert((isInvItem == true) or (isInvItem == false), "isInvItem parameter is not a boolean")
-- Verify the numeric input params are numbers
objId = tonumber(objId)
level = tonumber(level)
typeField = tonumber(typeField)
unique = tonumber(unique)
wearLoc = tonumber(wearLoc)
timer = tonumber(timer)
if (objId == nil) or (level == nil) or (typeField == nil) or (unique == nil) or
(wearLoc == nil) or (timer == nill) then
dbot.warn("inv.items.trigger.itemDataStats: Detected malformed invitem trigger: " ..
"numeric parameters are not numbers")
return DRL_RET_INVALID_PARAM
end -- if
-- Get a text name for the item type
assert((invmon.typeStr ~= nil) and (invmon.typeStr[typeField] ~= nil),
"Invalid invdata item type " .. typeField)
local typeName = invmon.typeStr[typeField]
-- Get the wear location
local wearLocText = inv.wearLoc[wearLoc]
if (wearLocText == nil) or (wearLocText == "") then
dbot.error("inv.items.trigger.itemDataStats: undefined wear location \"" .. (wearLoc or "nil") .. "\"")
return DRL_RET_INTERNAL_ERROR
end -- if
-- Check if the item is already in the table and get a reference to it if it is
item = inv.items.getEntry(objId)
-- If we got here via the invitem trigger and the item already exists, then flag the item as having
-- possibly changed so that we can re-identify it to see what changed
if (isInvItem == true) then
if (item ~= nil) then
retval = inv.items.setField(objId, invFieldIdentifyLevel, invIdLevelNone)
else
-- Check if the item is in the frequent item cache. If it is, there's no need to identify it :)
local cachedEntry = inv.cache.get(inv.cache.frequent.table, itemName)
if (cachedEntry ~= nil) then
cachedEntry.stats.id = objId
retval = inv.items.setEntry(objId, cachedEntry)
dbot.note("Identified \"" .. (inv.items.getField(objId, invFieldColorName) or "Unidentified") ..
"@W" .. DRL_ANSI_WHITE .. "\" (" .. objId .. ") from frequent cache")
-- This item instance probably wasn't in the recent item cache because we don't cache
-- items that are duplicated in the frequent item cache. However, it's possible that
-- the item wasn't in the frequent cache at the time it left our inventory but it is
-- in the cache now because another instance added it to the frequent cache. In this
-- scenario, we want to ensure that we remove the instance from the recent cache to
-- help keep that cache uncluttered.
inv.cache.remove(inv.cache.recent.table, objId)
else
-- Create an entry for this item. We don't know much yet, but fill in the little we know
-- about the item. We don't know the full colorized name yet, but we can use the basic name
-- for now and this is sufficient to support the frequent item cache. We will fill in the
-- full colorized name once we know it.
retval = inv.items.add(objId)
-- Use a basic name for the colorized name if necessary
inv.items.setField(objId, invFieldColorName, itemName)
end -- if
end -- if
else -- we got here from an eqdata or invdata request
-- Remember that we saw this item during a discovery/refresh. This lets us prune items that
-- are listed in the inventory table but are no longer in our inventory. This situation could
-- happen if the user exited(crashed?) after making an inventory change but before saving the change.
-- It could also happen if the user temporarily disables invmon or if the user makes a change outside
-- of mushclient (e.g., via telnet).
inv.items.currentItems[objId] = { discovered = true }
-- Add the current item to the inventory table if it doesn't exist yet and we are in eqdata or invdata
-- discovery mode
if (item == nil) and (inv.items.discoverPkg ~= nil) then
retval = inv.items.add(objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.trigger.itemDataStats: Failed to add item " .. objId ..
": error " .. dbot.retval.getString(retval))
return retval
end -- if
end -- if
-- Set the item's location: worn equipment slot, main inventory, keyring, or a container
-- You may wonder why we always set the location instead of just doing it in the above clause
-- for when the item is added. We do this to help recover from the situation where the inventory
-- table gets out of sync (e.g., user exits before save completes, makes changes outside of
-- mushclient, accidentally disables invmon, etc.)
if (inv.items.discoverPkg.loc == invItemLocWorn) then
retval = inv.items.setField(objId, invFieldObjLoc, wearLocText)
elseif (inv.items.discoverPkg.loc == invItemLocInventory) then
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocInventory)
elseif (inv.items.discoverPkg.loc == invItemLocKeyring) then
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocKeyring)
else -- the item is in a container
retval = inv.items.setField(objId, invFieldObjLoc, tonumber(inv.items.discoverPkg.loc))
end -- if
-- Set the colorized name of the
retval = inv.items.setField(objId, invFieldColorName, itemName)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.trigger.itemDataStats: Failed to set colorName for item " .. objId ..
": error " .. dbot.retval.getString(retval))
return retval
end -- if
end -- if
dbot.debug("inv.items.trigger.itemDataStats: object " .. objId .. ", flags=\"" .. flags ..
"\", itemName=\"" .. itemName .. "@W\", level=" .. level .. ", type=" .. typeName ..
", unique=" .. unique .. ", wearLoc=\"" .. wearLocText .. "\", timer=" .. timer)
-- We're done!
return retval
end -- inv.items.trigger.itemDataStats
function inv.items.trigger.itemDataEnd()
-- We are done with the eqdata or invdata output
EnableTrigger(inv.items.trigger.itemDataStartName, false)
EnableTrigger(inv.items.trigger.itemDataStatsName, false)
inv.items.discoverPkg = nil
end -- inv.items.trigger.itemDataEnd
inv.items.trigger.invmonName = "drlInvItemsTriggerInvmon"
inv.items.trigger.invitemName = "drlInvItemsTriggerInvitem"
function inv.items.trigger.invmon(action, objId, containerId, wearLoc)
local retval = DRL_RET_SUCCESS
-- Verify the input params exist
assert(action ~= nil, "invmon action is nil")
assert(objId ~= nil, "invmon objectId is nil")
assert(containerId ~= nil, "invmon containerId is nil")
assert(wearLoc ~= nil, "invmon wear location is nil")
-- Verify the input params are numbers
action = tonumber(action)
objId = tonumber(objId)
containerId = tonumber(containerId)
wearLoc = tonumber(wearLoc)
if (action == nil) or (objId == nil) or (containerId == nil) or (wearLoc == nil) then
dbot.debug("Detected malformed invmon trigger: parameters are not numbers")
return DRL_RET_INVALID_PARAM
end -- if
-- Get the action
assert(invmon.action[action] ~= nil, "Undefined invmon action " .. action)
-- Get the containerId and container basic stats (only valid for invmonActionTakenOutOfContainer
-- and invmonActionPutIntoContainer)
local containerText
local holding, itemsInside, totWeight, weightReduction, itemWeight
if (containerId == -1) then
containerText = "none"
else
containerText = containerId
holding = tonumber(inv.items.getStatField(containerId, invStatFieldHolding) or "")
itemsInside = tonumber(inv.items.getStatField(containerId, invStatFieldItemsInside) or "")
totWeight = tonumber(inv.items.getStatField(containerId, invStatFieldTotWeight) or "")
weightReduction = tonumber(inv.items.getStatField(containerId, invStatFieldWeightReduction) or "")
itemWeight = tonumber(inv.items.getStatField(objId, invStatFieldWeight) or "")
end -- if
-- Get the wear location (only valid for invmonActionRemoved or invmonActionWorn)
local wearLocText = inv.wearLoc[wearLoc]
if (wearLocText == nil) or (wearLocText == "") then
dbot.error("inv.items.trigger.invmon: undefined wear location \"" .. (wearLoc or "nil") .. "\"")
return DRL_RET_INTERNAL_ERROR
end -- if
dbot.debug("Invmon trigger: " .. invmon.action[action] .. " object " .. objId ..
", container=" .. containerText .. ", wearLoc=" .. wearLocText)
-- Add the current item to the inventory table if it doesn't exist yet
local item = inv.items.getEntry(objId)
if (item == nil) then
retval = inv.items.add(objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.trigger.invmon: Failed to add item " .. objId .. ": error " ..
dbot.retval.getString(retval))
return retval
end -- if
end -- if
-- If the item isn't identified and we aren't already in the middle of a refresh, schedule an
-- inventory refresh a few seconds from now. That will give some time to buffer up a few items
-- if we picked up several things.
local idLevel = inv.items.getField(objId, invFieldIdentifyLevel)
local eagerRefreshSec = tonumber(inv.config.table.refreshEagerSec or 0)
if (idLevel == invIdLevelNone) and (inv.state == invStateIdle) and (eagerRefreshSec > 0) then
inv.items.refreshAtTime(0, eagerRefreshSec)
end -- if
if (action == invmonActionRemoved) then
if (idLevel == invIdLevelNone) then
inv.items.mainState = invItemsRefreshDirty -- we want to rescan main inventory now
end -- if
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocInventory)
elseif (action == invmonActionWorn) then
retval = inv.items.setField(objId, invFieldObjLoc, wearLocText)
elseif (action == invmonActionRemovedFromInv) then
-- If the item is a container, this will remove any items in the container too
retval = inv.items.remove(objId)
elseif (action == invmonActionAddedToInv) then
inv.items.mainState = invItemsRefreshDirty -- we want to rescan main inventory now
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocInventory)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.items.trigger.invmon: Failed to set location for " .. objId .. ": error "
.. dbot.retval.getString(retval))
return retval
end -- if
elseif (action == invmonActionTakenOutOfContainer) then
if (idLevel == invIdLevelNone) then
-- An unidentified item is now in main inventory so we want to rescan our main inventory
inv.items.mainState = invItemsRefreshDirty
-- The container's stats just changed because the item was removed. We don't know the
-- item's weight (or anything else) yet so we can't automatically update the container's
-- stats. In this case, we mark the container as not being identified so that we will
-- re-identify it.
inv.items.setField(containerId, invFieldIdentifyLevel, invIdLevelNone)
else
-- Update the container's stats based on this item's removal
if (holding == nil) or (itemsInside == nil) or (totWeight == nil) or
(weightReduction == nil) or (weightReduction == 0) or (itemWeight == nil) then
-- If we don't have all of the container's stats, force a full re-identification
inv.items.setField(containerId, invFieldIdentifyLevel, invIdLevelNone)
else
holding = holding - (tonumber(inv.items.getStatField(objId, invStatFieldWeight) or "") or 0)
itemsInside = itemsInside - 1
totWeight = totWeight - (itemWeight * weightReduction / 100)
end -- if
end -- if
inv.items.setField(objId, invFieldHomeContainer, containerId)
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocInventory)
elseif (action == invmonActionPutIntoContainer) then
-- If we are putting an item into a container before we get a chance to ID it, flag the
-- container as being dirty so that we rescan it at our next opportunity
if (idLevel == invIdLevelNone) then
inv.items.keyword(invItemsRefreshClean, invKeywordOpRemove, "id " .. containerId, true)
-- The container's stats just changed because the item was added. We don't know the
-- item's weight (or anything else) yet so we can't automatically update the container's
-- stats. In this case, we mark the container as not being identified so that we will
-- re-identify it.
inv.items.setField(containerId, invFieldIdentifyLevel, invIdLevelNone)
else
-- Update the container's stats based on this item's addition
if (holding == nil) or (itemsInside == nil) or (totWeight == nil) or
(weightReduction == nil) or (weightReduction == 0) or (itemWeight == nil) then
-- If we don't have all of the container's stats, force a full re-identification
inv.items.setField(containerId, invFieldIdentifyLevel, invIdLevelNone)
else
holding = holding + (tonumber(inv.items.getStatField(objId, invStatFieldWeight) or "") or 0)
itemsInside = itemsInside + 1
totWeight = totWeight + (itemWeight * weightReduction / 100)
end -- if
end -- if
retval = inv.items.setField(objId, invFieldObjLoc, containerId)
elseif (action == invmonActionConsumed) then
retval = inv.items.remove(objId)
elseif (action == invmonActionPutIntoVault) then
-- If the item is a container, this will remove any items in the container too
retval = inv.items.remove(objId)
elseif (action == invmonActionRemovedFromVault) then
inv.items.mainState = invItemsRefreshDirty -- we want to rescan main inventory now
-- If we removed a container from the vault, we'll recursively add the contents of
-- the container when we identify the container
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocInventory)
elseif (action == invmonActionPutIntoKeyring) then
-- If we are putting an item into the keyring before we get a chance to ID it, flag the
-- keyring as being dirty so that we rescan it at our next opportunity
if (idLevel == invIdLevelNone) then
inv.items.keyringState = invItemsRefreshDirty
end -- if
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocKeyring)
elseif (action == invmonActionGetFromKeyring) then
if (idLevel == invIdLevelNone) then
inv.items.mainState = invItemsRefreshDirty -- we want to rescan main inventory now
end -- if
retval = inv.items.setField(objId, invFieldObjLoc, invItemLocInventory)
end -- if
-- If we updated a container's stats by adding or removing an item and if the container is not
-- already in line to be re-identified, update the container's stats now. We don't bother saving
-- this state because even if mushclient crashes before we save the state, the container's stats
-- will be re-read and updated automatically the next time the plugin is loaded. There's no
-- reason to pay for the overhead of writing out the inventory table state right now.
if ((action == invmonActionTakenOutOfContainer) or (action == invmonActionPutIntoContainer)) and
(inv.items.getField(containerId, invFieldIdentifyLevel) ~= invIdLevelNone) then
inv.items.setStatField(containerId, invStatFieldHolding, holding)
inv.items.setStatField(containerId, invStatFieldItemsInside, itemsInside)
inv.items.setStatField(containerId, invStatFieldTotWeight, totWeight)
end -- if
return retval
end -- inv.items.trigger.invmon
----------------------------------------------------------------------------------------------------
-- inv.items.timer: Timer functions for the inv.items module
--
-- Functions:
-- inv.items.timer.idTimeout()
--
----------------------------------------------------------------------------------------------------
inv.items.timer = {}
inv.items.timer.refreshName = "drlInvItemsTimerRefresh"
inv.items.timer.refreshMin = 5 -- by default, run the item refresh timer every 5 minutes, 0 seconds
inv.items.timer.refreshSec = 0
inv.items.timer.refreshEagerSec = 5 -- If enabled, run a refresh 5 seconds after acquiring a new item
inv.items.timer.idTimeoutName = "drlInvItemsTimerIdTimeout"
inv.items.timer.idTimeoutThresholdSec = 15 -- timeout the id request if it doesn't complete in this # sec
inv.items.timer.idTimeoutPeriodSec = 0.1 -- # sec to sleep between polls for an id request to complete
-- If we fail to complete an identification request in the allotted time, a timer will call
-- this function to clean up the pending identification request.
function inv.items.timer.idTimeout()
dbot.warn("inv.items.timer.idTimeout: Identification timeout timer just triggered! " ..
"Item identification did not complete!")
-- Clean up the identification request
inv.items.trigger.itemIdEnd()
end -- inv.items.timer.idTimeout
----------------------------------------------------------------------------------------------------
-- Item locations and wearable locations
----------------------------------------------------------------------------------------------------
invItemLocUninitialized = "uninitialized"
invItemLocInventory = "inventory"
invItemLocVault = "vault"
invItemLocKeyring = "keyring"
invItemLocWorn = "worn"
invItemLocAuction = "auction"
invItemLocShopkeeper = "shopkeeper"
invIdLevelNone = "none" -- item has not be ID'ed in any way
invIdLevelPartial = "partial" -- item has been partially ID'ed but more details are hidden
invIdLevelFull = "full" -- item has been ID'ed fully and no details are hidden
invWearableLocUndefined = -1
invWearableLocLight = 0
invWearableLocHead = 1
invWearableLocEyes = 2
invWearableLocLear = 3
invWearableLocRear = 4
invWearableLocNeck1 = 5
invWearableLocNeck2 = 6
invWearableLocBack = 7
invWearableLocMedal1 = 8
invWearableLocMedal2 = 9
invWearableLocMedal3 = 10
invWearableLocMedal4 = 11
invWearableLocTorso = 12
invWearableLocBody = 13
invWearableLocWaist = 14
invWearableLocArms = 15
invWearableLocLwrist = 16
invWearableLocRwrist = 17
invWearableLocHands = 18
invWearableLocLfinger = 19
invWearableLocRfinger = 20
invWearableLocLegs = 21
invWearableLocFeet = 22
invWearableLocShield = 23
invWearableLocWielded = 24
invWearableLocSecond = 25
invWearableLocHold = 26
invWearableLocFloat = 27
invWearableLocAbove = 30
invWearableLocPortal = 31
invWearableLocSleeping = 32
inv.wearLoc = {}
inv.wearLoc[invWearableLocUndefined] = "undefined"
inv.wearLoc[invWearableLocLight] = "light"
inv.wearLoc[invWearableLocHead] = "head"
inv.wearLoc[invWearableLocEyes] = "eyes"
inv.wearLoc[invWearableLocLear] = "lear"
inv.wearLoc[invWearableLocRear] = "rear"
inv.wearLoc[invWearableLocNeck1] = "neck1"
inv.wearLoc[invWearableLocNeck2] = "neck2"
inv.wearLoc[invWearableLocBack] = "back"
inv.wearLoc[invWearableLocMedal1] = "medal1"
inv.wearLoc[invWearableLocMedal2] = "medal2"
inv.wearLoc[invWearableLocMedal3] = "medal3"
inv.wearLoc[invWearableLocMedal4] = "medal4"
inv.wearLoc[invWearableLocTorso] = "torso"
inv.wearLoc[invWearableLocBody] = "body"
inv.wearLoc[invWearableLocWaist] = "waist"
inv.wearLoc[invWearableLocArms] = "arms"
inv.wearLoc[invWearableLocLwrist] = "lwrist"
inv.wearLoc[invWearableLocRwrist] = "rwrist"
inv.wearLoc[invWearableLocHands] = "hands"
inv.wearLoc[invWearableLocLfinger] = "lfinger"
inv.wearLoc[invWearableLocRfinger] = "rfinger"
inv.wearLoc[invWearableLocLegs] = "legs"
inv.wearLoc[invWearableLocFeet] = "feet"
inv.wearLoc[invWearableLocShield] = "shield"
inv.wearLoc[invWearableLocWielded] = "wielded"
inv.wearLoc[invWearableLocSecond] = "second"
inv.wearLoc[invWearableLocHold] = "hold"
inv.wearLoc[invWearableLocFloat] = "float"
inv.wearLoc[invWearableLocAbove] = "above"
inv.wearLoc[invWearableLocPortal] = "portal"
inv.wearLoc[invWearableLocSleeping] = "sleeping"
inv.wearables = { light = { "light" },
head = { "head" },
eyes = { "eyes" },
ear = { "lear", "rear" },
neck = { "neck1", "neck2" },
back = { "back" },
medal = { "medal1", "medal2", "medal3", "medal4" },
torso = { "torso" },
body = { "body" },
waist = { "waist" },
arms = { "arms" },
wrist = { "lwrist", "rwrist" },
hands = { "hands" },
finger = { "lfinger", "rfinger" },
legs = { "legs" },
feet = { "feet" },
shield = { "shield" },
wield = { "wielded", "second" },
hold = { "hold" },
float = { "float" },
above = { "above" },
portal = { "portal" },
sleeping = { "sleeping" } }
----------------------------------------------------------------------------------------------------
-- Definitions for fields in identified items
----------------------------------------------------------------------------------------------------
inv.stats = {}
inv.stats.id = { name = "id",
desc = "Unique identifier for the item" }
inv.stats.name = { name = "name",
desc = "List of words in the name of the item" }
inv.stats.level = { name = "level",
desc = "Level at which you may use the item (doesn't account for tier bonuses)" }
inv.stats.weight = { name = "weight",
desc = "Base weight of the item" }
inv.stats.wearable = { name = "wearable",
desc = "The item is wearable. Run \"@Gwearable@W\" to see a list of locations." }
inv.stats.score = { name = "score",
desc = "Item's score based on aard's priorities: see \"@Gcompare set@W\"" }
inv.stats.keywords = { name = "keywords",
desc = "List of keywords representing the item" }
inv.stats.type = { name = "type",
desc = "Type of item: see \"@Ghelp eqdata@W\" to see available types" }
inv.stats.worth = { name = "worth",
desc = "How much gold this item is worth" }
inv.stats.flags = { name = "flags",
desc = "List of flags assigned to the item" }
inv.stats.affectMods = { name = "affectMods",
desc = "List of effects given by the item" }
inv.stats.material = { name = "material",
desc = "Specifies what the item is made of" }
inv.stats.foundAt = { name = "foundAt",
desc = "The item was found at this area" }
inv.stats.ownedBy = { name = "ownedBy",
desc = "Character who owns this item" }
inv.stats.clan = { name = "clan",
desc = "If this is a clan item, this indicates which clan made it" }
inv.stats.spells = { name = "spells",
desc = "Spells that this item can cast" }
inv.stats.leadsTo = { name = "leadsTo",
desc = "Target destination of a portal" }
inv.stats.capacity = { name = "capacity",
desc = "How much weight the container can hold" }
inv.stats.holding = { name = "holding",
desc = "Number of items held by the container" }
inv.stats.heaviestItem = { name = "heaviestItem",
desc = "Weight of the heaviest item in the container" }
inv.stats.itemsInside = { name = "itemsInside",
desc = "Number of items currently inside the container" }
inv.stats.totWeight = { name = "totWeight",
desc = "Total weight of the container and its contents" }
inv.stats.itemBurden = { name = "itemBurden",
desc = "Number of items in the container + 1 (for the container itself)" }
inv.stats.weightReduction = { name = "weightReduction",
desc = "Container reduces an item's weight to this % of the original weight" }
inv.stats.int = { name = "int",
desc = "Intelligence points provided by the item" }
inv.stats.wis = { name = "wis",
desc = "Wisdom points provided by the item" }
inv.stats.luck = { name = "luck",
desc = "Luck points provided by the item" }
inv.stats.str = { name = "str",
desc = "Strength points provided by the item" }
inv.stats.dex = { name = "dex",
desc = "Dexterity points provided by the item" }
inv.stats.con = { name = "con",
desc = "Constitution points provided by the item" }
inv.stats.hp = { name = "hp",
desc = "Hit points provided by the item" }
inv.stats.mana = { name = "mana",
desc = "Mana points provided by the item" }
inv.stats.moves = { name = "moves",
desc = "Movement points provided by the item" }
inv.stats.hit = { name = "hit",
desc = "Hit roll bonus due to the item" }
inv.stats.dam = { name = "dam",
desc = "Damage roll bonus due to the item " }
inv.stats.allPhys = { name = "allPhys",
desc = "Resistance provided against each of the physical resistance types" }
inv.stats.allMagic = { name = "allMagic",
desc = "Resistance provided against each of the magical resistance types" }
inv.stats.acid = { name = "acid",
desc = "Resistance provided against magical attacks of type \"acid\"" }
inv.stats.cold = { name = "cold",
desc = "Resistance provided against magical attacks of type \"cold\"" }
inv.stats.energy = { name = "energy",
desc = "Resistance provided against magical attacks of type \"energy\"" }
inv.stats.holy = { name = "holy",
desc = "Resistance provided against magical attacks of type \"holy\"" }
inv.stats.electric = { name = "electric",
desc = "Resistance provided against magical attacks of type \"electric\"" }
inv.stats.negative = { name = "negative",
desc = "Resistance provided against magical attacks of type \"negative\"" }
inv.stats.shadow = { name = "shadow",
desc = "Resistance provided against magical attacks of type \"shadow\"" }
inv.stats.magic = { name = "magic",
desc = "Resistance provided against magical attacks of type \"magic\"" }
inv.stats.air = { name = "air",
desc = "Resistance provided against magical attacks of type \"air\"" }
inv.stats.earth = { name = "earth",
desc = "Resistance provided against magical attacks of type \"earth\"" }
inv.stats.fire = { name = "fire",
desc = "Resistance provided against magical attacks of type \"fire\"" }
inv.stats.light = { name = "light",
desc = "Resistance provided against magical attacks of type \"light\"" }
inv.stats.mental = { name = "mental",
desc = "Resistance provided against magical attacks of type \"mental\"" }
inv.stats.sonic = { name = "sonic",
desc = "Resistance provided against magical attacks of type \"sonic\"" }
inv.stats.water = { name = "water",
desc = "Resistance provided against magical attacks of type \"water\"" }
inv.stats.poison = { name = "poison",
desc = "Resistance provided against magical attacks of type \"poison\"" }
inv.stats.disease = { name = "disease",
desc = "Resistance provided against magical attacks of type \"disease\"" }
inv.stats.slash = { name = "slash",
desc = "Resistance provided against physical attacks of type \"slash\"" }
inv.stats.pierce = { name = "pierce",
desc = "Resistance provided against physical attacks of type \"pierce\"" }
inv.stats.bash = { name = "bash",
desc = "Resistance provided against physical attacks of type \"bash\"" }
inv.stats.aveDam = { name = "aveDam",
desc = "Average damage from the weapon" }
inv.stats.inflicts = { name = "inflicts",
desc = "Wound type from item: see Wset column in \"@Ghelp damage types@W\"" }
inv.stats.damType = { name = "damType",
desc = "Damage type of item: see Damtype column in \"@Ghelp damage types@W\"" }
inv.stats.weaponType = { name = "weaponType",
desc = "Type of weapon: see \"@Ghelp weapons@W\" for a list" }
inv.stats.specials = { name = "specials",
desc = "See \"@Ghelp weapon flags@W\" for an explanation of special behaviors" }
inv.stats.location = { name = "location",
desc = "Item ID for the container holding this item" }
inv.stats.rlocation = { name = "rlocation",
desc = "Relative name (e.g., \"3.bag\") for the container holding this item" }
inv.stats.rname = { name = "rname",
desc = "Relative name (e.g., \"2.dagger\") for the item" }
inv.stats.organize = { name = "organize",
desc = "Queries assigned to a container by \"@Gdinv organize ...@W\"" }
inv.stats.loc = { name = "loc",
desc = "Shorthand for the \"@G" .. inv.stats.location.name .. "@W\" search key" }
inv.stats.rloc = { name = "rloc",
desc = "Shorthand for the \"@G" .. inv.stats.rlocation.name .. "@W\" search key" }
inv.stats.key = { name = "key",
desc = "Shorthand for the \"@G" .. inv.stats.keywords.name .. "@W\" search key" }
inv.stats.keyword = { name = "keyword",
desc = "Shorthand for the \"@G" .. inv.stats.keywords.name .. "@W\" search key" }
inv.stats.flag = { name = "flag",
desc = "Shorthand for the \"@G" .. inv.stats.flags.name .. "@W\" search key" }
invStatFieldId = string.lower(inv.stats.id.name)
invStatFieldName = string.lower(inv.stats.name.name)
invStatFieldLevel = string.lower(inv.stats.level.name)
invStatFieldWeight = string.lower(inv.stats.weight.name)
invStatFieldWearable = string.lower(inv.stats.wearable.name)
invStatFieldScore = string.lower(inv.stats.score.name)
invStatFieldKeywords = string.lower(inv.stats.keywords.name)
invStatFieldType = string.lower(inv.stats.type.name)
invStatFieldWorth = string.lower(inv.stats.worth.name)
invStatFieldFlags = string.lower(inv.stats.flags.name)
invStatFieldAffectMods = string.lower(inv.stats.affectMods.name)
invStatFieldMaterial = string.lower(inv.stats.material.name)
invStatFieldFoundAt = string.lower(inv.stats.foundAt.name)
invStatFieldOwnedBy = string.lower(inv.stats.ownedBy.name)
invStatFieldClan = string.lower(inv.stats.clan.name)
invStatFieldSpells = string.lower(inv.stats.spells.name)
invStatFieldLeadsTo = string.lower(inv.stats.leadsTo.name)
invStatFieldCapacity = string.lower(inv.stats.capacity.name)
invStatFieldHolding = string.lower(inv.stats.holding.name)
invStatFieldHeaviestItem = string.lower(inv.stats.heaviestItem.name)
invStatFieldItemsInside = string.lower(inv.stats.itemsInside.name)
invStatFieldTotWeight = string.lower(inv.stats.totWeight.name)
invStatFieldItemBurden = string.lower(inv.stats.itemBurden.name)
invStatFieldWeightReduction = string.lower(inv.stats.weightReduction.name)
invStatFieldInt = string.lower(inv.stats.int.name)
invStatFieldWis = string.lower(inv.stats.wis.name)
invStatFieldLuck = string.lower(inv.stats.luck.name)
invStatFieldStr = string.lower(inv.stats.str.name)
invStatFieldDex = string.lower(inv.stats.dex.name)
invStatFieldCon = string.lower(inv.stats.con.name)
invStatFieldHP = string.lower(inv.stats.hp.name)
invStatFieldMana = string.lower(inv.stats.mana.name)
invStatFieldMoves = string.lower(inv.stats.moves.name)
invStatFieldHit = string.lower(inv.stats.hit.name)
invStatFieldDam = string.lower(inv.stats.dam.name)
invStatFieldAllPhys = string.lower(inv.stats.allPhys.name)
invStatFieldAllMagic = string.lower(inv.stats.allMagic.name)
invStatFieldAcid = string.lower(inv.stats.acid.name)
invStatFieldCold = string.lower(inv.stats.cold.name)
invStatFieldEnergy = string.lower(inv.stats.energy.name)
invStatFieldHoly = string.lower(inv.stats.holy.name)
invStatFieldElectric = string.lower(inv.stats.electric.name)
invStatFieldNegative = string.lower(inv.stats.negative.name)
invStatFieldShadow = string.lower(inv.stats.shadow.name)
invStatFieldMagic = string.lower(inv.stats.magic.name)
invStatFieldAir = string.lower(inv.stats.air.name)
invStatFieldEarth = string.lower(inv.stats.earth.name)
invStatFieldFire = string.lower(inv.stats.fire.name)
invStatFieldLight = string.lower(inv.stats.light.name)
invStatFieldMental = string.lower(inv.stats.mental.name)
invStatFieldSonic = string.lower(inv.stats.sonic.name)
invStatFieldWater = string.lower(inv.stats.water.name)
invStatFieldPoison = string.lower(inv.stats.poison.name)
invStatFieldDisease = string.lower(inv.stats.disease.name)
invStatFieldSlash = string.lower(inv.stats.slash.name)
invStatFieldPierce = string.lower(inv.stats.pierce.name)
invStatFieldBash = string.lower(inv.stats.bash.name)
invStatFieldAveDam = string.lower(inv.stats.aveDam.name)
invStatFieldInflicts = string.lower(inv.stats.inflicts.name)
invStatFieldDamType = string.lower(inv.stats.damType.name)
invStatFieldWeaponType = string.lower(inv.stats.weaponType.name)
invStatFieldSpecials = string.lower(inv.stats.specials.name)
----------------------------------------------------------------------------------------------------
-- Plugin-specific query keys
--
-- Queries can use all of the invStatFieldXXX values as query keys.
-- Here are some other supported query keys that are convenient.
----------------------------------------------------------------------------------------------------
invQueryKeyLocation = string.lower(inv.stats.location.name)
invQueryKeyRelativeLocation = string.lower(inv.stats.rlocation.name)
invQueryKeyRelativeName = string.lower(inv.stats.rname.name)
invQueryKeyOrganize = string.lower(inv.stats.organize.name)
invQueryKeyLoc = string.lower(inv.stats.loc.name)
invQueryKeyRelativeLoc = string.lower(inv.stats.rloc.name)
invQueryKeyKey = string.lower(inv.stats.key.name)
invQueryKeyKeyword = string.lower(inv.stats.keyword.name)
invQueryKeyFlag = string.lower(inv.stats.flag.name)
invQueryKeyCustom = "custom"
invQueryKeyAll = "all"
invQueryKeyEquipped = "equipped"
invQueryKeyWorn = "worn" -- this is an alias for invQueryKeyEquipped
invQueryKeyUnequipped = "unequipped"
----------------------------------------------------------------------------------------------------
-- The "affect mods" item fields (yes, I really think it should be "effect mods" but I'm sticking
-- with the aard terminology) include effects you get by wearing certain items. For example, the
-- regen ring gives the "regeneration" ability. However, some item effects are not included in
-- the "affect mods" field and they do not have official names. We add a few custom ones here so
-- that we can have a way to prioritize items that prevent you from dropping weapons, allow you
-- to dual weapons, or get a defensive bonus because it is a shield.
----------------------------------------------------------------------------------------------------
invItemEffectsIronGrip = "irongrip"
invItemEffectsDualWield = "dualwield"
invItemEffectsShield = "shield"
----------------------------------------------------------------------------------------------------
-- Inventory cache subsystem
----------------------------------------------------------------------------------------------------
-- The "recent cache" attempts to cache the items that have been most recently removed
-- from the inventory table. This lets us re-identify those items very quickly if we
-- add them back to the inventory at some point. For example, we may want to drop a bag
-- of keys and then pick it up again later. In that case we don't want to re-identify
-- everything. Also, we don't want to re-identify everything in our entire inventory
-- if we die and lose all of our items.
--
-- The recent cache is indexed by objId because we are tracking specific item instances.
-- This differs from the "frequently acquired" cache that is indexed by an item's name.
--
-- The "frequently acquired" cache tracks items that are frequently added to your inventory.
-- For example, we don't want to re-identify the same healing potion 50 times if you buy a
-- stack of 50 potions. We keep a generic item entry for items that don't change but are
-- acquired a lot. We can skip the full identification procedure if the name of a potion,
-- pill, food, etc. matches a key in the cache. In that case, we use the cached details
-- without performing the identification.
--
-- Functions:
-- inv.cache.init.atActive()
-- inv.cache.fini(doSaveState)
--
-- inv.cache.config(cacheName, numEntries)
-- inv.cache.save()
-- inv.cache.load()
-- inv.cache.reset() -- reset all caches
--
-- inv.cache.resetCache(cacheName) -- reset a single specific cache
-- inv.cache.add
-- inv.cache.remove
-- inv.cache.prune
--
-- inv.cache.get
-- inv.cache.getSize
-- inv.cache.setSize
--
-- inv.cache.dump
-- inv.cache.clearOld
--
-- Data:
-- inv.cache.recent.table
-- inv.cache.frequent.table
----------------------------------------------------------------------------------------------------
inv.cache = {}
inv.cache.init = {}
inv.cache.recent = {}
inv.cache.frequent = {}
inv.cache.recent.table = nil
inv.cache.frequent.table = nil
inv.cache.recent.name = "recent"
inv.cache.frequent.name = "frequent"
inv.cache.recent.stateName = "inv-cache-recent.state"
inv.cache.frequent.stateName = "inv-cache-frequent.state"
inv.cache.recent.defaultNumEntries = 1000
inv.cache.frequent.defaultNumEntries = 100
inv.cache.recent.prunePercent = 0.2
inv.cache.frequent.prunePercent = 0.2
function inv.cache.init.atActive()
local retval = DRL_RET_SUCCESS
retval = inv.cache.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.init.atActive: failed to load cache data from storage: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.cache.init.atActive
function inv.cache.fini(doSaveState)
local retval = DRL_RET_SUCCESS
if (doSaveState) then
-- Save our current data
retval = inv.cache.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.fini: Failed to save inv.cache module data: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.cache.fini
function inv.cache.config(cacheName, numEntries)
if (cacheName ~= inv.cache.recent.name) and (cacheName ~= inv.cache.frequent.name) then
dbot.warn("inv.cache.config: Invalid cache name \"" .. (cacheName or "nil") .. "\"")
return DRL_RET_INVALID_PARAM
end -- if
if (numEntries == nil) or (tonumber(numEntries) == nil) then
dbot.warn("inv.cache.config: Invalid numEntries parameter: It is not a number")
return DRL_RET_INVALID_PARAM
end -- if
-- Build the cache data structure
local cache = {}
cache.entries = {}
cache.name = cacheName
cache.maxEntries = tonumber(numEntries)
if (cacheName == inv.cache.recent.name) then
inv.cache.recent.table = cache
elseif (cacheName == inv.cache.frequent.name) then
inv.cache.frequent.table = cache
else
dbot.error("inv.cache.config: Invalid cache name detected: \"" .. (cacheName or "nil") .. "\"")
return DRL_RET_INTERNAL_ERROR
end -- if
inv.cache.save()
return DRL_RET_SUCCESS
end -- inv.cache.config
function inv.cache.save()
local recentRetval = DRL_RET_SUCCESS
local frequentRetval = DRL_RET_SUCCESS
if (inv.cache.recent.table ~= nil) then
recentRetval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.cache.recent.stateName,
"inv.cache.recent.table", inv.cache.recent.table)
if (recentRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.save: Failed to save cache.recent table: " .. dbot.retval.getString(recentRetval))
end -- if
end -- if
if (inv.cache.frequent.table ~= nil) then
frequentRetval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.cache.frequent.stateName,
"inv.cache.frequent.table", inv.cache.frequent.table)
if (frequentRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.save: Failed to save cache.frequent table: " .. dbot.retval.getString(frequentRetval))
end -- if
end -- if
if (recentRetval ~= DRL_RET_SUCCESS) then
return recentRetval
else
return frequentRetval
end -- if
end -- inv.cache.save
function inv.cache.load()
local recentRetval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.cache.recent.stateName,
inv.cache.reset)
if (recentRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.load: Failed to load cache table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.cache.recent.stateName .. "@W\": " ..
dbot.retval.getString(recentRetval))
end -- if
local frequentRetval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.cache.frequent.stateName,
inv.cache.reset)
if (frequentRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.load: Failed to load cache table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.cache.frequent.stateName .. "@W\": " ..
dbot.retval.getString(frequentRetval))
end -- if
if (inv.version.table ~= nil) and (inv.version.table.tableFormat ~= nil) and
(inv.config.table ~= nil) and (inv.config.table.tableFormat ~= nil) and
(inv.version.table.cacheFormat ~= nil) and (inv.config.table.cacheFormat ~= nil) then
-- Check if the inventory table version we loaded is compatible with the current code
if (inv.version.table.tableFormat.major ~= inv.config.table.tableFormat.major) and
(inv.version.table.tableFormat.minor ~= inv.config.table.tableFormat.minor) then
-- TODO: This is a placeholder for when (or if?) we ever change the table format
end -- if
-- Check if the inventory cache version we loaded is compatible with the current code
if (inv.version.table.cacheFormat.major ~= inv.config.table.cacheFormat.major) and
(inv.version.table.cacheFormat.minor ~= inv.config.table.cacheFormat.minor) then
-- TODO: This is a placeholder for when (or if?) we ever change the cache format
end -- if
else
dbot.error("inv.cache.load: Missing inv.version components")
return DRL_RET_INTERNAL_ERROR
end -- if
if (recentRetval ~= DRL_RET_SUCCESS) then
return recentRetval
else
return frequentRetval
end -- if
end -- inv.cache.load
function inv.cache.reset()
local recentRetval = DRL_RET_SUCCESS
local frequentRetval = DRL_RET_SUCCESS
if (inv.cache.recent ~= nil) then
recentRetval = inv.cache.resetCache(inv.cache.recent.name)
if (recentRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.reset: recent cache reset failed: " .. dbot.retval.getString(recentRetval))
end -- if
end -- if
if (inv.cache.frequent ~= nil) then
frequentRetval = inv.cache.resetCache(inv.cache.frequent.name)
if (frequentRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.reset: frequent cache reset failed: " .. dbot.retval.getString(frequentRetval))
end -- if
end -- if
-- If the recent cache reset failed, return that error code. Otherwise, return the frequent
-- cache reset's return value.
if (recentRetval ~= DRL_RET_SUCCESS) then
return recentRetval
else
return frequentRetval
end -- if
end -- inv.cache.reset
function inv.cache.resetCache(cacheName)
if (cacheName == nil) or (cacheName == "") then
dbot.warn("inv.cache.reset: Missing cache name")
return DRL_RET_INVALID_PARAM
end -- if
local numEntries = 0
if (cacheName == inv.cache.recent.name) then
numEntries = inv.cache.recent.defaultNumEntries
elseif (cacheName == inv.cache.frequent.name) then
numEntries = inv.cache.frequent.defaultNumEntries
end -- if
local retval = inv.cache.config(cacheName, numEntries)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.resetCache: Failed to configure cache: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.cache.resetCache
function inv.cache.add(cache, objId)
assert(cache ~= nil, "Cache is nil!!!")
local retval = DRL_RET_SUCCESS
assert(objId ~= nil, "Received nil objId!!!")
objId = tonumber(objId)
local entry = inv.items.getEntry(objId)
if (entry == nil) then
dbot.warn("inv.cache.add: Cannot add item to cache because it is not in the inventory table")
return DRL_RET_MISSING_ENTRY
end -- if
local cacheEntry = { timeCached = dbot.getTime(), entry = dbot.table.getCopy(entry) }
-- Cache the object if we've done some type of identification on it
local idLevel = inv.items.getField(objId, invFieldIdentifyLevel)
if (idLevel ~= nil) and (idLevel ~= invIdLevelNone) then
if (cache.name == inv.cache.recent.name) then
cache.entries[objId] = cacheEntry
elseif (cache.name == inv.cache.frequent.name) then
local name = inv.items.getStatField(objId, invStatFieldName)
if (name ~= nil) and (name ~= "") then
cache.entries[name] = cacheEntry
end -- if
end -- if
dbot.debug("Added \"" .. (inv.items.getField(objId, "colorName") or "Unidentified") .. "\" " ..
"to the \"" .. cache.name .. "\" cache")
end -- if
-- Check if the cache is full and remove entries as needed to reduce the size. We always
-- remove the least recently used item when necessary. As an optimization once the cache
-- starts to get full, we whack a certain percentage (10%? 20%?) of entries when we prune the
-- cache so that we don't need to go through the overhead of sorting everything every time
-- we drop an item.
if (dbot.table.getNumEntries(cache.entries) > cache.maxEntries) then
retval = inv.cache.prune(cache)
end -- if
-- Note: To cut down on overhead, we currently do not call inv.cache.save() here. Instead,
-- the cache is saved in inv.cache.fini when we disconnect or reload the plugin. There
-- is a chance we may miss some cache updates this way if mush exits uncleanly, but the
-- downside is low because we we'll just re-identify anything we need if that happens.
return retval
end -- inv.cache.add
function inv.cache.remove(cache, key)
assert(cache ~= nil, "Cache is nil!!!")
assert(key ~= nil, "Received nil key!!!")
local cacheKey = key
-- The recent cache uses a numeric object ID as a key and we do a little extra parameter
-- checking in this case because I'm paranoid...
if (cache.name == inv.cache.recent.name) then
cacheKey = tonumber(key)
if (cacheKey == nil) then
dbot.warn("inv.cache.remove: failed to remove item for non-numeric objId key " .. key)
return DRL_RET_INVALID_PARAM
end -- if
dbot.debug("Removed \"" .. (inv.items.getField(key, "colorName") or "Unidentified") .. "\" " ..
"from the \"" .. cache.name .. "\" cache")
end -- if
cache.entries[cacheKey] = nil
-- Note: To cut down on overhead, we currently do not call inv.cache.save() here. Instead,
-- the cache is saved in inv.cache.fini when we disconnect or reload the plugin. There
-- is a chance we may miss some cache updates this way if mush exits uncleanly, but the
-- downside is low because we we'll just re-identify anything we need if that happens.
return DRL_RET_SUCCESS
end -- inv.cache.remove
function inv.cache.prune(cache)
assert(cache ~= nil, "Cache is nil!!!")
local retval = DRL_RET_SUCCESS
local numEntriesInCache = dbot.table.getNumEntries(cache.entries)
local numEntriesToPrune = 0
-- Determine how many entries in the cache to remove in this call. We don't want
-- to need to prune things on every cache access because it is expensive to sort
-- the arrays. Instead, if we detect a full cache, we whack several cache entries
-- at once so that we can amortize the overhead of pruning over several accesses.
if (cache.name == inv.cache.recent.name) then
numEntriesToPrune = math.floor(numEntriesInCache * inv.cache.recent.prunePercent) + 1 or 0
elseif (cache.name == inv.cache.frequent.name) then
numEntriesToPrune = math.floor(numEntriesInCache * inv.cache.frequent.prunePercent) + 1 or 0
end -- if
dbot.note("The " .. cache.name .. " cache is full, removing the " ..
numEntriesToPrune .. " least recently used items")
-- Sort the cache entries by the date they were last used. We create a temporary array of the
-- entries so that we can sort them (you can't sort a table.)
local entryArray = {}
for k,v in pairs(cache.entries) do
table.insert(entryArray, { key=k, timeCached=v.timeCached })
end -- for
table.sort(entryArray, function (entry1, entry2) return entry1.timeCached < entry2.timeCached end)
-- Remove the "numEntriesToPrune" first entries in the array
for i = 1, numEntriesToPrune do
local key = entryArray[i].key
retval = inv.cache.remove(cache, entryArray[i].key)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.cache.prune: Failed to remove cache item " .. key)
break
end -- if
end -- for
return retval
end -- inv.cache.prune
function inv.cache.get(cache, key)
if (cache == nil) or (key == nil) or (key == "") then
return nil
end -- if
local cacheKey = key
-- The recent cache uses a numeric object ID as a key and we do a little extra parameter
-- checking in this case because I'm paranoid...
if (cache.name == inv.cache.recent.name) then
cacheKey = tonumber(key)
if (cacheKey == nil) then
dbot.warn("inv.cache.get: failed to get item for non-numeric objId key " .. key)
return nil
end -- if
end -- if
local cacheEntry = cache.entries[cacheKey]
if (cacheEntry == nil) then
return nil -- it isn't in the cache
end -- if
-- Update the timestamp
cacheEntry.timeCached = dbot.getTime()
-- Return a copy of the cached entry (we don't want the caller directly modifying the entry in our cache)
return dbot.table.getCopy(cacheEntry.entry)
end -- inv.cache.get
function inv.cache.getSize(cache)
assert(cache ~= nil, "Cache is nil!!!")
return cache.maxEntries
end -- inv.cache.getSize
function inv.cache.setSize(cache, numEntries)
assert(cache ~= nil, "Cache is nil!!!")
assert(tonumber(numEntries) ~= nil, "numEntries parameter is not numeric!")
cache.maxEntries = numEntries
return inv.cache.save()
end -- inv.cache.getSize
function inv.cache.dump(cache)
assert(cache ~= nil, "Cache is nil!!!")
tprint(cache)
return DRL_RET_SUCCESS
end -- inv.cache.dump
-- Remove all cached items older than ageInSec
-- We don't currently use this, but it might be handy at some point
function inv.cache.clearOld(cache, ageInSec)
assert(cache ~= nil, "Cache is nil!!!")
local currentTime = dbot.getTime()
for key,cacheEntry in pairs(cache.entries) do
if (cacheEntry == nil) or (cacheEntry.timeCached == nil) or
(currentTime - cacheEntry.timeCached > ageInSec) then
inv.cache.remove(cache, key)
end -- if
end -- for
end -- inv.cache.clearOld
----------------------------------------------------------------------------------------------------
--
-- Inventory priorities
--
-- inv.priority.init.atActive()
-- inv.priority.fini(doSaveState)
--
-- inv.priority.save()
-- inv.priority.load()
-- inv.priority.reset()
--
-- inv.priority.create(priorityName, endTag)
-- inv.priority.clone(origPriorityName, clonedPriorityName, endTag)
-- inv.priority.delete(priorityName, endTag)
--
-- inv.priority.list(endTag)
-- inv.priority.display(priorityName, endTag)
--
-- inv.priority.edit(priorityName, useAllFields, isQuiet, endTag)
-- inv.priority.update(priorityName, priorityString, isQuiet)
-- inv.priority.copy(priorityName, endTag)
-- inv.priority.paste(priorityName, endTag)
--
-- inv.priority.compare(priorityName1, priorityName2, endTag)
--
-- inv.priority.new(priorityName)
-- inv.priority.add(priorityName, priorityTable)
-- inv.priority.remove(priorityName)
-- inv.priority.get(priorityName, level)
--
-- inv.priority.tableToString(priorityTable, doDisplayUnused, doDisplayColors, doDisplayDesc)
-- inv.priority.stringToTable(priorityString)
--
-- inv.priority.locIsAllowed(wearableLoc, priorityName, level)
--
-- inv.priority.addDefault() -- add some default priorities
--
-- Data:
-- inv.priority = {}
-- inv.priority.table = {}
-- inv.priority.fieldTable = {}
--
----------------------------------------------------------------------------------------------------
inv.priority = {}
inv.priority.init = {}
inv.priority.table = {}
inv.priority.stateName = "inv-priorities.state"
function inv.priority.init.atActive()
local retval = DRL_RET_SUCCESS
retval = inv.priority.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.init.atActive: failed to load priority data from storage: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.priority.init.atActive
function inv.priority.fini(doSaveState)
local retval = DRL_RET_SUCCESS
if (doSaveState) then
-- Save our current data
retval = inv.priority.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.fini: Failed to save inv.priority module data: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.priority.fini
function inv.priority.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.priority.stateName,
"inv.priority.table", inv.priority.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.save: Failed to save priority table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.priority.save
function inv.priority.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.priority.stateName,
inv.priority.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.priority.stateName .. "@W\": " ..
dbot.retval.getString(retval))
end -- if
-- Check if the priority table version we loaded is compatible with the current code
if (inv.version.table ~= nil) and (inv.version.table.priorityFormat ~= nil) and
(inv.config.table ~= nil) and (inv.config.table.priorityFormat ~= nil) then
if (inv.version.table.priorityFormat.major ~= inv.config.table.priorityFormat.major) and
(inv.version.table.priorityFormat.minor ~= inv.config.table.priorityFormat.minor) then
-- TODO: This is a placeholder for when (or if?) we ever change the priority table format
end -- if
else
dbot.error("inv.priority.load: Missing inventory version information")
retval = DRL_RET_INTERNAL_ERROR
end -- if
return retval
end -- inv.priority.load
function inv.priority.reset()
local retval
inv.priority.table = {}
retval = inv.priority.addDefault()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.reset: Failed to add default priorities: " .. dbot.retval.getString(retval))
return retval
end -- if
retval = inv.priority.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.reset: Failed to save priorities: " .. dbot.retval.getString(retval))
return retval
end -- if
return retval
end -- inv.priority.reset
function inv.priority.create(priorityName, endTag)
local retval = DRL_RET_SUCCESS
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.create: priority name is missing!")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.priority.table[priorityName] ~= nil) then
dbot.warn("inv.priority.create: Priority \"@C" .. priorityName .. "@W\" already exists")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_BUSY)
end -- if
retval = inv.priority.new(priorityName)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.create: Failed to add new priority \"@C" .. priorityName .. "@W\": " ..
dbot.retval.getString(retval))
else
retval = inv.priority.edit(priorityName, true, true, endTag)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.create: Failed to edit priority \"@C" .. priorityName .. "@W\": " ..
dbot.retval.getString(retval))
else
dbot.info("Created priority \"@C" .. priorityName .. "@W\"")
end -- if
end -- if
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.create
function inv.priority.clone(origPriorityName, clonedPriorityName, endTag)
local retval
if (clonedPriorityName == nil) or (clonedPriorityName == "") then
dbot.warn("inv.priority.clone: cloned priority name is missing!")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (origPriorityName == nil) or (origPriorityName == "") then
dbot.warn("inv.priority.clone: original priority name is missing!")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.priority.table[origPriorityName] == nil) then
dbot.warn("inv.priority.clone: original priority \"@C" .. origPriorityName .. "@W\" does not exist")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
if (inv.priority.table[clonedPriorityName] ~= nil) then
dbot.warn("inv.priority.clone: cloned priority \"@C" .. clonedPriorityName .. "@W\" already exists")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_BUSY)
end -- if
-- Copy the priority into a new table entry
inv.priority.table[clonedPriorityName] = dbot.table.getCopy(inv.priority.table[origPriorityName])
dbot.info("Cloned priority \"@C" .. clonedPriorityName .. "@W\" from priority \"@C" ..
origPriorityName .."@W\"")
-- Save the table with the new priority. We're done! :)
retval = inv.priority.save()
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.clone
function inv.priority.delete(priorityName, endTag)
local retval
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.delete: priority name is missing!")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.priority.table[priorityName] == nil) then
dbot.info("Skipping priority deletion: Priority \"@C" .. priorityName .. "@W\" does not exist")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
retval = inv.priority.remove(priorityName)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.delete: Failed to remove priority \"@C" .. priorityName .. "@W\": " ..
dbot.retval.getString(retval))
else
dbot.info("Deleted priority \"@C" .. priorityName .. "@W\"")
end -- if
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.delete
function inv.priority.list(endTag)
if (inv.priority == nil) or (inv.priority.table == nil) then
dbot.error("inv.priority.list: Priority table is missing!")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INTERNAL_ERROR)
end -- if
-- Alphabetize the priorities before we list them
local sortedPriorities = {}
local numPriorities = 0
for k,_ in pairs(inv.priority.table) do
table.insert(sortedPriorities, k)
numPriorities = numPriorities + 1
end -- for
table.sort(sortedPriorities, function (v1, v2) return v1 < v2 end)
if (numPriorities == 0) then
dbot.info("Priority table is empty")
else
dbot.print("@WPriorities:")
for _, priority in ipairs(sortedPriorities) do
dbot.print("@W \"@C" .. priority .. "@W\"@w")
end -- for
end -- if
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_SUCCESS)
end -- inv.priority.list
function inv.priority.display(priorityName, endTag)
local retval = DRL_RET_SUCCESS
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.display: Missing priorityName parameter")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
local priority = inv.priority.table[priorityName]
if (priority == nil) then
dbot.info("Priority \"" .. priorityName .. "\" is not in the priority table")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
local priString = inv.priority.tableToString(priority, false, true, true)
dbot.print("@WPriority: \"@C" .. priorityName .. "@W\"\n")
dbot.print(priString)
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.display
function inv.priority.edit(priorityName, useAllFields, isQuiet, endTag)
local retval = DRL_RET_SUCCESS
local priorityString = ""
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.edit: priority name is missing!")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.priority.table[priorityName] == nil) then
dbot.warn("inv.priority.edit: Priority \"@C" .. priorityName .. "@W\" does not exist")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
-- Get a string representation of the priority we want to edit
priorityString, retval = inv.priority.tableToString(inv.priority.table[priorityName],
useAllFields, false, useAllFields)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.edit: Failed to get string representation of priority \"@C" ..
priorityName .. "@W\": " .. dbot.retval.getString(retval))
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- if
local instructions =
[[Edit your priority! See "dinv help priority" for more details.
The first column lists the names of each available priority field. Subsequent columns specify the numeric values of that field for a level range. You may have as many level ranges as you wish, but ranges should not overlap and they should cover all levels between 1 - 291.
]]
local fontName = GetAlphaOption("output_font_name")
if (fontName == nil) then
fontName = "Consolas"
end -- if
-- Use a slightly smaller font if there is lots of info to display
local fontSize = 12
if useAllFields then
fontSize = 10
end -- if
repeat
priorityString = utils.editbox(instructions,
"DINV: Editing priority \"" .. priorityName .. "\"",
priorityString, -- default text
fontName, -- font
fontSize, -- font size
{ ok_button = "Done!" }) -- extras
if (priorityString == nil) then
dbot.info("Cancelled request to edit priority \"@C" .. priorityName .. "@W\"")
retval = DRL_RET_SUCCESS
break
else
retval = inv.priority.update(priorityName, priorityString, isQuiet)
end -- if
until (retval == DRL_RET_SUCCESS)
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.edit
function inv.priority.update(priorityName, priorityString, isQuiet)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.update: Missing priority name parameter")
return DRL_RET_INVALID_PARAM
end -- if
if (priorityString == nil) or (priorityString == "") then
dbot.warn("inv.priority.update: Missing priority string parameter")
return DRL_RET_INVALID_PARAM
end -- if
local priorityEntry, retval = inv.priority.stringToTable(priorityString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.priority.update: Failed to convert priority string into priority: " ..
dbot.retval.getString(retval))
else
inv.priority.table[priorityName] = priorityEntry
if (not isQuiet) then
dbot.info("Updated priority \"@C" .. priorityName .. "@W\"")
end -- if
inv.priority.save()
-- Invalidate any previous equipment set analyzis based on this priority
inv.set.table[priorityName] = nil
inv.set.save()
end -- if
return retval
end -- inv.priority.update
function inv.priority.copy(priorityName, endTag)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.copy: Missing priority name parameter")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.priority.table[priorityName] == nil) then
dbot.warn("inv.priority.copy: priority \"@C" .. priorityName .. "@W\" does not exist")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
-- Get a string representation of the priority we want to copy
local priorityString, retval = inv.priority.tableToString(inv.priority.table[priorityName],
true, false, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.copy: Failed to get string representation of priority \"@C" ..
priorityName .. "@W\": " .. dbot.retval.getString(retval))
else
SetClipboard(priorityString)
dbot.info("Copied priority \"@C" .. priorityName .. "@W\" to clipboard")
end -- if
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.copy
function inv.priority.paste(priorityName, endTag)
local retval = DRL_RET_SUCCESS
local operation
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.paste: Missing priority name parameter")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.priority.table[priorityName] == nil) then
operation = "Created"
else
operation = "Updated"
end -- if
local priorityString = GetClipboard()
if (priorityString == nil) or (priorityString == "") then
dbot.warn("inv.priority.paste: Failed to get priority from clipboard")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
retval = inv.priority.update(priorityName, priorityString, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.paste: Failed to update priority from clipboard data: " ..
dbot.retval.getString(retval))
else
dbot.info(operation .. " priority \"@C" .. priorityName .. "@W\" from clipboard data")
end -- if
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.paste
function inv.priority.compare(priorityName1, priorityName2, endTag)
local retval = DRL_RET_SUCCESS
if (priorityName1 == nil) or (priorityName1 == "") or (priorityName2 == nil) or (priorityName2 == "") then
dbot.warn("inv.priority.compare: missing priority name")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.priority == nil) or (inv.priority.table == nil) then
dbot.error("inv.priority.list: Priority table is missing!")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_INTERNAL_ERROR)
end -- if
if (inv.priority.table[priorityName1] == nil) then
dbot.warn("inv.priority.compare: Priority \"" .. priorityName1 .. "\" is not present")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
if (inv.priority.table[priorityName2] == nil) then
dbot.warn("inv.priority.compare: Priority \"" .. priorityName2 .. "\" is not present")
return inv.tags.stop(invTagsPriority, endTag, DRL_RET_MISSING_ENTRY)
end -- if
local startLevel = 1 + 10 * dbot.gmcp.getTier()
local endLevel = startLevel + 200
local doPrintHeader = true
dbot.print("@WSwitching from priority \"@G" .. priorityName1 .. "@W\" to priority \"@G" ..
priorityName2 .. "@W\" would result in these changes:\n@w")
for level = startLevel, endLevel do
local set1 = inv.set.get(priorityName1, level)
local set2 = inv.set.get(priorityName2, level)
local didPrintHeader
if (set1 == nil) then
dbot.info("Priority \"@C" .. priorityName1 .. "@W\" is missing a set analysis at level " .. level .. ".")
dbot.info("Please run \"@Gdinv analyze create " .. priorityName1 .. "@W\" before comparing the priority.")
retval = DRL_RET_MISSING_ENTRY
break
elseif (set2 == nil) then
dbot.info("Priority \"@C" .. priorityName2 .. "@W\" is missing a set analysis at level " .. level .. ".")
dbot.info("Please run \"@Gdinv analyze create " .. priorityName2 .. "@W\" before comparing the priority.")
retval = DRL_RET_MISSING_ENTRY
break
end -- if
didPrintHeader, retval = inv.set.displayDiff(set1, set2, level, string.format("Level %3d: ", level),
doPrintHeader)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.compare: Failed to display priority differences at level " .. level ..
": " .. dbot.retval.getString(retval))
break
end -- if
doPrintHeader = not didPrintHeader -- If we already printed a header, don't print one again
end -- for
return inv.tags.stop(invTagsPriority, endTag, retval)
end -- inv.priority.compare
function inv.priority.new(priorityName)
local retval = DRL_RET_SUCCESS
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.new: priority name is missing!")
return DRL_RET_INVALID_PARAM
end -- if
if (inv.priority.table[priorityName] ~= nil) then
dbot.warn("Skipping request for new priority \"@C" .. priorityName .. "@W\": priority already exists")
return DRL_RET_INVALID_PARAM
end -- if
local priorities = {}
for _, entry in ipairs(inv.priority.fieldTable) do
priorities[entry[2]] = 0
end -- if
retval = inv.priority.add(priorityName, { { minLevel = 1, maxLevel = 291, priorities = priorities } })
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.new: Failed to add priority \"@C" .. priorityName .. "@W\": " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.priority.new
function inv.priority.add(priorityName, priorityTable)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.add: Missing priorityName parameter")
return DRL_RET_INVALID_PARAM
end -- if
if (priorityTable == nil) then
dbot.warn("inv.priority.add: priorityTable is nil")
return DRL_RET_INVALID_PARAM
end -- if
inv.priority.table[priorityName] = priorityTable
local retval = inv.priority.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.add: Failed to save priorities: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.priority.add
function inv.priority.remove(priorityName)
local retval
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.remove: Missing priorityName parameter")
return DRL_RET_INVALID_PARAM
end -- if
if (inv.priority.table[priorityName] == nil) then
dbot.warn("inv.priority.remove: Priority table does not contain an entry for priority \"" ..
priorityName .. "\"")
return DRL_RET_MISSING_ENTRY
end -- if
inv.priority.table[priorityName] = nil
retval = inv.priority.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.remove: Failed to save priorities: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.priority.remove
-- Returns table/nil, return value
function inv.priority.get(priorityName, level)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.get: Missing priorityName parameter")
return nil, DRL_RET_INVALID_PARAM
end -- if
local levelNum = tonumber(level or "none")
if (levelNum == nil) then
dbot.warn("inv.priority.get: level parameter is not a number")
return nil, DRL_RET_INVALID_PARAM
end -- if
local priority = inv.priority.table[priorityName]
if (priority == nil) then
dbot.warn("inv.priority.get: Priority \"" .. priorityName .. "\" is not in the priority table")
return nil, DRL_RET_MISSING_ENTRY
end -- if
-- Find the priority block for our level
local priorityBlock = nil
for i,v in ipairs(priority) do
if (levelNum >= v.minLevel) and (levelNum <= v.maxLevel) then
priorityBlock = v
break;
end -- if
end -- for
-- Verify that we found an appropriate priority block for our level
if (priorityBlock == nil) then
dbot.warn("inv.priority.get: Failed to find a priority block for level " ..
levelNum .. " in priority \"" .. priorityName .. "\"")
return nil
end -- if
return priorityBlock.priorities, DRL_RET_SUCCESS
end -- inv.priority.get
-- Returns string, retval
--[[ String format looks something like this:
Field L001- L050- L100- L201-
Name L049 L099 L200 L299
int 0.800 0.900 1.000 1.000
str 1.000 1.000 0.800 0.700
...
--]]
function inv.priority.tableToString(priorityTable, doDisplayUnused, doDisplayColors, doDisplayDesc)
local retval = DRL_RET_SUCCESS
local generalPrefix, generalSuffix = "", ""
local fieldPrefix, fieldSuffix = "", ""
local levelPrefix, levelSuffix = "", ""
local descPrefix, descSuffix = "", ""
if doDisplayColors then
generalPrefix, generalSuffix = "@W", "@w"
fieldPrefix, fieldSuffix = "@C", "@w"
levelPrefix, levelSuffix = "@W", "@W"
descPrefix, descSuffix = "@c", "@w"
end -- if
-- Create the first line of the header
local priString = generalPrefix .. string.format("%12s", "MinLevel")
for _, blockEntry in ipairs(priorityTable) do
priString = priString .. string.format(" %s%3d%s", levelPrefix, blockEntry.minLevel, levelSuffix)
end -- for
-- Create the second line of the header
priString = priString .. string.format("\r\n%12s", "MaxLevel")
for _, blockEntry in ipairs(priorityTable) do
priString = priString .. string.format(" %s%3d%s", levelPrefix, blockEntry.maxLevel, levelSuffix)
end -- for
priString = priString .. "\r\n" .. generalSuffix
for _, fieldEntry in ipairs(inv.priority.fieldTable) do
local fieldName = fieldEntry[1]
local fieldDesc = fieldEntry[2]
local useField = true
-- Check if we should display this field or not. We only use the field if at least one entry
-- block has a non-zero entry for the field or if the doDisplayUnused param is true.
if (not doDisplayUnused) then
useField = false
for _, blockEntry in ipairs(priorityTable) do
local fieldValue = tonumber(blockEntry.priorities[fieldName] or "") or 0
if (fieldValue ~= 0) then
useField = true
break
end -- if
end -- if
end -- if
if (useField) then
priString = priString .. fieldPrefix .. string.format("\r\n%12s", fieldName) .. fieldSuffix
for _, blockEntry in ipairs(priorityTable) do
local fieldValue = tonumber(blockEntry.priorities[fieldName] or "") or 0
local valuePrefix, valueSuffix = "", ""
if doDisplayColors then
if (fieldValue <= 0) then
valuePrefix = "@R"
elseif (fieldValue < 0.5) then
valuePrefix = "@r"
elseif (fieldValue < 0.8) then
valuePrefix = "@y"
elseif (fieldValue < 1.4) then
valuePrefix = "@w"
elseif (fieldValue < 5) then
valuePrefix = "@g"
else
valuePrefix = "@G"
end -- if
valueSuffix = "@W"
end -- if
priString = priString .. valuePrefix .. string.format(" %5.2f", fieldValue) .. valueSuffix
end -- for
if doDisplayDesc then
priString = priString .. " : " .. descPrefix .. fieldDesc .. descSuffix
end -- if
end -- if
end -- for
return priString, retval
end -- inv.priority.tableToString
-- Returns priority table entry, retval
function inv.priority.stringToTable(priorityString)
local retval = DRL_RET_SUCCESS
local priEntry = {}
if (priorityString == nil) or (priorityString == "") then
dbot.warn("inv.priority.stringToTable: Missing priority string parameter")
return priEntry, DRL_RET_INVALID_ENTRY
end -- if
local lines = utils.split(priorityString, "\n")
-- Remove any color codes and comments. This makes parsing everything much simpler.
for i, line in ipairs(lines) do
lines[i] = string.gsub(strip_colours(line), ":.*$", "")
end -- for
-- Verify the integrity of the string table. Each line should have the same number
-- of columns.
local numColumns = nil
for i, line in ipairs(lines) do
local words, retval = dbot.wordsToArray(line)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.stringToTable: Failed to convert line into array: " ..
dbot.retval.getString(retval))
return priEntry, retval
end -- if
-- Remove any lines containing only white space. Those just complicate things...
if (#words == 0) then
table.remove(lines, i)
elseif (numColumns == nil) then
numColumns = #words
elseif (numColumns ~= #words) then
dbot.warn("Malformed line has wrong number of columns:\n\"" .. line .. "\"")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
end -- for
if (numColumns == nil) then
dbot.warn("No valid lines were detected in the priority")
return priEntry, DRL_RET_INVALID_PARAM
elseif (numColumns < 2) then
dbot.warn("Missing one or more columns in the priority")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
-- Parse the header lines
if (#lines < 2) then
dbot.warn("Missing header lines in priority")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
local header1, retval = dbot.wordsToArray(lines[1])
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("The priority's first line (part of the header) is malformed: " ..
dbot.retval.getString(retval))
return priEntry, retval
end -- if
if (header1[1] ~= "MinLevel") then
dbot.warn("Missing or malformed minLevel header line in priority")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
local header2, retval = dbot.wordsToArray(lines[2])
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("The priority's second line (part of the header) is malformed: " ..
dbot.retval.getString(retval))
return priEntry, retval
end -- if
if (header2[1] ~= "MaxLevel") then
dbot.warn("Missing or malformed maxLevel header line in priority")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
-- Set up the initial block entries and level ranges
for i = 2, numColumns do -- Skip the first column (min/max levels and the field names)
local _, _, minLevel = string.find(header1[i], "(%d+)")
local _, _, maxLevel = string.find(header2[i], "(%d+)")
minLevel = tonumber(minLevel or "") or 0
maxLevel = tonumber(maxLevel or "") or 0
-- Ensure that there aren't any gaps in the level ranges. The minLevel for this block
-- should be exactly one more than the maxLevel from the previous block.
if (#priEntry > 0) and (priEntry[#priEntry].maxLevel + 1 ~= minLevel) then
dbot.warn("Detected level gap between consecutive priority blocks\n" ..
" Previous level block [" .. priEntry[#priEntry].minLevel .. "-" ..
priEntry[#priEntry].maxLevel .. "], current level block [" .. minLevel .. "-" ..
maxLevel .. "]")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
table.insert(priEntry, { minLevel = minLevel, maxLevel = maxLevel, priorities = {} })
end -- for
-- The priority must start at level 1 and end at 291
if (priEntry[1].minLevel ~= 1) or (priEntry[#priEntry].maxLevel ~= 291) then
dbot.warn("Priority must start at level 1 and continue to level 291")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
-- For each priority field, add the field's value to each block entry
for i = 3, #lines do
local fieldLine, retval = dbot.wordsToArray(lines[i])
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("Failed to parse priority line \"" .. lines[i] .. "\"")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
if (#fieldLine > 0) then
-- Verify that the field name is valid
local fieldName = fieldLine[1]
local fieldIsValid = false
for _, entry in ipairs(inv.priority.fieldTable) do
if (entry[1] == fieldName) then
fieldIsValid = true
break
end -- if
end -- if
if (not fieldIsValid) then
dbot.warn("Unsupported priority field \"" .. (fieldName or "nil") .. "\" in line\n \"" ..
(lines[i] or "nil") .. "\"")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
for blockIdx, priorityBlock in ipairs(priEntry) do
local fieldValueRaw = fieldLine[blockIdx + 1] -- add one to skip over the field name
if (fieldValueRaw == nil) then
dbot.warn("Missing one or more columns for priority field \"" .. fieldName .. "\"")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
local fieldValue = tonumber(fieldValueRaw or "")
if (fieldValue == nil) then
dbot.warn("Non-numeric field value in priority at column " .. blockIdx + 1 ..
" in line\n \"" .. lines[i] .. "\"")
return priEntry, DRL_RET_INVALID_PARAM
end -- if
priorityBlock.priorities[fieldName] = fieldValue
end -- for
end -- if
end -- for
return priEntry, retval
end -- inv.priority.stringToTable
-- Priorities can have lines of the form "~hold 1 0 0 1 1" to indicate that the
-- "hold" location should be ignored when creating the priority equipment sets
function inv.priority.locIsAllowed(wearableLoc, priorityName, level)
if (wearableLoc == nil) or (wearableLoc == "") then
dbot.warn("inv.priority.locIsAllowed: Missing wearable location parameter")
return false
end -- if
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.priority.locIsAllowed: Missing priority name parameter")
return false
end -- if
level = tonumber(level or "")
if (level == nil) or (level < 1) or (level > 291) then
dbot.warn("inv.priority.locIsAllowed: Invalid level parameter")
return false
end -- if
-- Check if the specified priority exists for the specified level
local priorityTable, retval = inv.priority.get(priorityName, level)
if (priorityTable == nil) then
dbot.warn("inv.priority.locIsAllowed: Priority \"" .. priorityName ..
"\" does not have a priority table " .. "for level " .. level)
return false
end -- if
local value = tonumber(priorityTable["~" .. wearableLoc] or "") or 0
if (value == 0) then
return true
else
return false
end -- if
end -- inv.priority.locIsAllowed
function inv.priority.addDefault()
local retval
----------------
-- Priority: psi
----------------
--[[ Here are the default statistic weightings from the Aardwolf scoring system for a
primary psi. This is what we will model in the "psi" priority. Keep in mind that
the plugin provides *many* more options to adjust your scoring and the aardwolf
defaults are very simple. Look at the "psi-melee" priority for an example of what
you can do. The table below was taken by running the "compare set" command on the
Aardwolf mud using a primary psi character. Hopefully it isn't copyrighted... :)
Default Your
Affect Bonus Keyword Score Score
----------------- ------- ------- -------
Strength str 10 10
Intelligence int 15 15
Wisdom wis 15 15
Dexterity dex 10 10
Constitution con 10 10
Luck lck 12 12
------------------------------------------
Hit points hp 0 0
Mana mana 0 0
Moves moves 0 0
------------------------------------------
Hit roll hr 5 5
Damage roll dr 5 5
Saves save 0 0
Resists resist 0 0
------------------------------------------
Damage dam 4 4
------------------------------------------
--]]
retval = inv.priority.add(
"psi", -- Equipment priorities using the default psi weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.0,
int = 1.5,
wis = 1.5,
dex = 1.0,
con = 1.0,
luck = 1.2,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"psi\": " .. dbot.retval.getString(retval))
end -- if
--------------------
-- Priority: warrior
--------------------
--[[ Here are the default statistic weightings from the Aardwolf scoring system for a
primary warrior. This is what we will model in the "warrior" priority.
Default Your
Affect Bonus Keyword Score Score
----------------- ------- ------- -------
Strength str 15 15
Intelligence int 10 10
Wisdom wis 10 10
Dexterity dex 15 15
Constitution con 10 10
Luck lck 10 10
------------------------------------------
Hit points hp 0 0
Mana mana 0 0
Moves moves 0 0
------------------------------------------
Hit roll hr 5 5
Damage roll dr 5 5
Saves save 0 0
Resists resist 0 0
------------------------------------------
Damage dam 4 4
------------------------------------------
--]]
retval = inv.priority.add(
"warrior", -- Equipment priorities using the default warrior weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.5,
int = 1.0,
wis = 1.0,
dex = 1.5,
con = 1.0,
luck = 1.0,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"warrior\": " .. dbot.retval.getString(retval))
end -- if
-----------------
-- Priority: mage
-----------------
--[[ Here are the default statistic weightings from the Aardwolf scoring system for a
primary mage. This is what we will model in the "mage" priority.
Default Your
Affect Bonus Keyword Score Score
----------------- ------- ------- -------
Strength str 10 10
Intelligence int 15 15
Wisdom wis 10 10
Dexterity dex 10 10
Constitution con 10 10
Luck lck 10 10
------------------------------------------
Hit points hp 0 0
Mana mana 0 0
Moves moves 0 0
------------------------------------------
Hit roll hr 5 5
Damage roll dr 5 5
Saves save 0 0
Resists resist 0 0
------------------------------------------
Damage dam 4 4
------------------------------------------
--]]
retval = inv.priority.add(
"mage", -- Equipment priorities using the default mage weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.0,
int = 1.5,
wis = 1.0,
dex = 1.0,
con = 1.0,
luck = 1.0,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"mage\": " .. dbot.retval.getString(retval))
end -- if
------------------
-- Priority: thief
------------------
--[[ Here are the default statistic weightings from the Aardwolf scoring system for a
primary thief. This is what we will model in the "thief" priority.
Default Your
Affect Bonus Keyword Score Score
----------------- ------- ------- -------
Strength str 12 12
Intelligence int 10 10
Wisdom wis 10 10
Dexterity dex 15 15
Constitution con 10 10
Luck lck 10 10
------------------------------------------
Hit points hp 0 0
Mana mana 0 0
Moves moves 0 0
------------------------------------------
Hit roll hr 5 5
Damage roll dr 5 5
Saves save 0 0
Resists resist 0 0
------------------------------------------
Damage dam 4 4
------------------------------------------
--]]
retval = inv.priority.add(
"thief", -- Equipment priorities using the default thief weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.2,
int = 1.0,
wis = 1.0,
dex = 1.5,
con = 1.0,
luck = 1.0,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"thief\": " .. dbot.retval.getString(retval))
end -- if
-------------------
-- Priority: ranger
-------------------
--[[ Here are the default statistic weightings from the Aardwolf scoring system for a
primary ranger. This is what we will model in the "ranger" priority.
Default Your
Affect Bonus Keyword Score Score
----------------- ------- ------- -------
Strength str 10 10
Intelligence int 10 10
Wisdom wis 15 15
Dexterity dex 10 10
Constitution con 15 15
Luck lck 10 10
------------------------------------------
Hit points hp 0 0
Mana mana 0 0
Moves moves 0 0
------------------------------------------
Hit roll hr 5 5
Damage roll dr 5 5
Saves save 0 0
Resists resist 0 0
------------------------------------------
Damage dam 4 4
------------------------------------------
--]]
retval = inv.priority.add(
"ranger", -- Equipment priorities using the default ranger weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.0,
int = 1.0,
wis = 1.5,
dex = 1.0,
con = 1.5,
luck = 1.0,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"ranger\": " .. dbot.retval.getString(retval))
end -- if
--------------------
-- Priority: paladin
--------------------
--[[ Here are the default statistic weightings from the Aardwolf scoring system for a
primary paladin. This is what we will model in the "paladin" priority.
Default Your
Affect Bonus Keyword Score Score
----------------- ------- ------- -------
Strength str 10 10
Intelligence int 15 15
Wisdom wis 10 10
Dexterity dex 10 10
Constitution con 15 15
Luck lck 10 10
------------------------------------------
Hit points hp 0 0
Mana mana 0 0
Moves moves 0 0
------------------------------------------
Hit roll hr 5 5
Damage roll dr 5 5
Saves save 0 0
Resists resist 0 0
------------------------------------------
Damage dam 4 4
------------------------------------------
--]]
retval = inv.priority.add(
"paladin", -- Equipment priorities using the default paladin weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.0,
int = 1.5,
wis = 1.0,
dex = 1.0,
con = 1.5,
luck = 1.0,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"paladin\": " .. dbot.retval.getString(retval))
end -- if
-------------------
-- Priority: cleric
-------------------
--[[ Here are the default statistic weightings from the Aardwolf scoring system for a
primary cleric. This is what we will model in the "cleric" priority.
Default Your
Affect Bonus Keyword Score Score
----------------- ------- ------- -------
Strength str 10 10
Intelligence int 10 10
Wisdom wis 15 15
Dexterity dex 10 10
Constitution con 10 10
Luck lck 10 10
------------------------------------------
Hit points hp 0 0
Mana mana 0 0
Moves moves 0 0
------------------------------------------
Hit roll hr 5 5
Damage roll dr 5 5
Saves save 0 0
Resists resist 0 0
------------------------------------------
Damage dam 4 4
------------------------------------------
--]]
retval = inv.priority.add(
"cleric", -- Equipment priorities using the default cleric weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.0,
int = 1.0,
wis = 1.5,
dex = 1.0,
con = 1.0,
luck = 1.0,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"cleric\": " .. dbot.retval.getString(retval))
end -- if
retval = inv.priority.add(
"cleric-tank", -- Equipment priorities using the default cleric weightings from the aardwolf scoring system
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = {
str = 1.2,
int = 1.2,
wis = 1.5,
dex = 1.3,
con = 1.5,
luck = 1.3,
hit = 0.5,
dam = 0.5,
avedam = 0.4,
offhandDam = 0.4,
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"cleric-tank\": " ..
dbot.retval.getString(retval))
end -- if
-------------------------
-- Priority: psi-no-melee
-------------------------
-- This is designed for a single-class psi. I'm not entirely happy with the values included
-- in it. I'll probably tweak those once I tier again and go back to a single class psi. Feel
-- free to mess with these values and try out your own experiments. That's fun too :)
retval = inv.priority.add(
"psi-no-melee", -- Equipment priorities for a psi with no melee classes yet
{
{ -- Priorities for levels 1 - 50
minLevel = 1,
maxLevel = 50,
priorities = { int = 1,
luck = 1,
wis = 0.7,
str = 1,
dex = 0.6,
con = 0.2,
dam = 0.7,
hit = 0.7,
hp = 0.02,
mana = 0.01,
moves = 0,
allphys = 0.03,
allmagic = 0.03,
avedam = 1,
offhandDam = 0.33,
regeneration = 5,
sanctuary = 50,
haste = 20,
detectgood = 2,
detectevil = 2,
detecthidden = 3,
detectinvis = 4,
detectmagic = 0, -- I don't care if we can detect magic
shield = 5,
dualwield = 20,
irongrip = 2,
invis = 10,
flying = 5
}
},
{ -- Priorities for levels 51 - 70
minLevel = 51,
maxLevel = 70,
priorities = { int = 1,
luck = 1,
wis = 0.8,
str = 1.2,
dex = 0.4,
con = 0.2,
dam = 0.8,
hit = 0.8,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.05,
allmagic = 0.03,
avedam = 1,
offhandDam = 0.4,
regeneration = 5,
sanctuary = 10,
haste = 5,
detectgood = 2,
detectevil = 2,
detecthidden = 3,
detectinvis = 4,
detectmagic = 0, -- I don't care if we can detect magic
shield = 5,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 3,
invis = 5,
flying = 4
}
},
{ -- Priorities for levels 71 - 130
minLevel = 71,
maxLevel = 130,
priorities = { int = 1,
luck = 1,
wis = 0.9,
str = 0.8,
dex = 0.6,
con = 0.4,
dam = 0.8,
hit = 0.7,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.5,
regeneration = 5,
sanctuary = 10,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 10,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 20,
invis = 3,
flying = 2
}
},
{ -- Priorities for levels 131 - 170
minLevel = 131,
maxLevel = 170,
priorities = { int = 1,
luck = 1,
wis = 1,
str = 0.7,
dex = 0.5,
con = 0.4,
dam = 0.8,
hit = 0.8,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.6,
regeneration = 5,
sanctuary = 10,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 20,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 20,
invis = 1,
flying = 1
}
},
{ -- Priorities for levels 171 - 200
minLevel = 171,
maxLevel = 200,
priorities = { int = 1,
luck = 1,
wis = 1,
str = 0.7,
dex = 0.4,
con = 0.4,
dam = 0.8,
hit = 0.8,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.7,
regeneration = 5,
sanctuary = 20,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 25,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 25,
invis = 1,
flying = 1
}
},
{ -- Priorities for level 201 - 291
minLevel = 201,
maxLevel = 291,
priorities = { int = 1.0,
luck = 1.0,
wis = 1.0,
str = 0.5,
dex = 0.4,
con = 0.25,
dam = 0.8,
hit = 0.8,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.8,
regeneration = 2,
sanctuary = 5,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 40,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 30,
invis = 1,
flying = 1
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"psi-no-melee\": " ..
dbot.retval.getString(retval))
end -- if
----------------------
-- Priority: psi-melee
----------------------
-- This is designed for a psi with at least one melee class. It fits my playing style well, but
-- feel free to tweak it for your own use :)
retval = inv.priority.add(
"psi-melee",
{
{ -- Priorities for levels 1 - 50
minLevel = 1,
maxLevel = 50,
priorities = { int = 0.8,
luck = 1,
wis = 0.7,
str = 1,
dex = 0.8,
con = 0.2,
dam = 0.9,
hit = 0.85,
hp = 0.02,
mana = 0.01,
moves = 0,
allphys = 0.03,
allmagic = 0.03,
avedam = 1,
offhandDam = 0.33,
regeneration = 5,
sanctuary = 50,
haste = 20,
detectgood = 2,
detectevil = 2,
detecthidden = 3,
detectinvis = 4,
detectmagic = 0, -- I don't care if we can detect magic
shield = 5,
dualwield = 20,
irongrip = 2,
invis = 10,
flying = 5,
maxint = 0,
maxluck = 0,
maxwis = 0,
maxstr = 0,
maxdex = 0,
maxcon = 0
}
},
{ -- Priorities for levels 51 - 70
minLevel = 51,
maxLevel = 70,
priorities = { int = 1,
luck = 1,
wis = 0.8,
str = 1,
dex = 0.5,
con = 0.2,
dam = 0.9,
hit = 0.8,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.05,
allmagic = 0.03,
avedam = 1,
offhandDam = 0.4,
regeneration = 5,
sanctuary = 10,
haste = 5,
detectgood = 2,
detectevil = 2,
detecthidden = 3,
detectinvis = 4,
detectmagic = 0, -- I don't care if we can detect magic
shield = 5,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 3,
invis = 5,
flying = 4,
maxint = 0,
maxluck = 0,
maxwis = 0,
maxstr = 0,
maxdex = 0,
maxcon = 0
}
},
{ -- Priorities for levels 71 - 130
minLevel = 71,
maxLevel = 130,
priorities = { int = 1,
luck = 1,
wis = 0.9,
str = 0.8,
dex = 0.6,
con = 0.4,
dam = 0.85,
hit = 0.75,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.5,
regeneration = 5,
sanctuary = 10,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 10,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 20,
invis = 3,
flying = 2,
maxint = 0,
maxluck = 0,
maxwis = 0,
maxstr = 0,
maxdex = 0,
maxcon = 0
}
},
{ -- Priorities for levels 131 - 170
minLevel = 131,
maxLevel = 170,
priorities = { int = 1,
luck = 1,
wis = 1,
str = 0.7,
dex = 0.5,
con = 0.4,
dam = 0.85,
hit = 0.85,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.6,
regeneration = 5,
sanctuary = 10,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 20,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 20,
invis = 1,
flying = 1,
maxint = 0,
maxluck = 0,
maxwis = 0,
maxstr = 0,
maxdex = 0,
maxcon = 0
}
},
{ -- Priorities for levels 171 - 200
minLevel = 171,
maxLevel = 200,
priorities = { int = 1,
luck = 1,
wis = 1,
str = 0.7,
dex = 0.4,
con = 0.4,
dam = 0.85,
hit = 0.85,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.7,
regeneration = 5,
sanctuary = 20,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 25,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 25,
invis = 1,
flying = 1,
maxint = 5,
maxluck = 5,
maxwis = 5,
maxstr = 0,
maxdex = 0,
maxcon = 0
}
},
{ -- Priorities for level 201 - 291
minLevel = 201,
maxLevel = 291,
priorities = { int = 1.0,
luck = 1.0,
wis = 1.0,
str = 0.5,
dex = 0.4,
con = 0.25,
dam = 0.8,
hit = 0.8,
hp = 0.01,
mana = 0.01,
moves = 0,
allphys = 0.1,
allmagic = 0.05,
avedam = 1,
offhandDam = 0.85,
regeneration = 2,
sanctuary = 5,
haste = 2,
detectgood = 2,
detectevil = 2,
detecthidden = 2,
detectinvis = 2,
detectmagic = 0, -- I don't care if we can detect magic
shield = 40,
dualwield = 0, -- I don't care once I have dual wield skill
irongrip = 30,
invis = 1,
flying = 1,
maxint = 20,
maxluck = 20,
maxwis = 20,
maxstr = 0,
maxdex = 0,
maxcon = 0
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"psi-melee\": " ..
dbot.retval.getString(retval))
end -- if
----------------------
-- Priority: enchanter
----------------------
-- This refers to anyone enchanting, not just an enchanter sub-class. It boosts the three
-- stats responsible for improving enchantments. You probably don't want to try leveling
-- with a set based on this :)
retval = inv.priority.add(
"enchanter", -- Equipment priorities for an enchanter (only care about int, luck, wis)
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = { int = 1,
luck = 1,
wis = 1
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"enchanter\": " ..
dbot.retval.getString(retval))
end -- if
--------------------
-- Priority: balance
--------------------
-- Yeah, it's a little tasteless to game the system and take advantage of the mental balance
-- implementation. This priority lowers wis as much as possible while boosting int. This
-- will give you the biggest possible bonus to wis when you cast mental balance. You can then
-- wear your normal equipment while retaining the wis bonus.
retval = inv.priority.add(
"balance", -- Equipment priorities to game the mental balance spell
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = { int = 1,
wis = -1
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"balance\": " ..
dbot.retval.getString(retval))
end -- if
------------------
-- Priority: debug
------------------
-- Feel free to play around with this one :) Look at the "psi-melee" priority for examples on
-- all of the options you have at your disposal.
retval = inv.priority.add(
"debug", -- Debug priorities: this is helpful when playing around with sets
{
{ -- Priorities for levels 1 - 291
minLevel = 1,
maxLevel = 291,
priorities = { int = 1,
luck = 1,
wis = 1,
str = 0.5,
dex = 0.25,
con = 0.2,
dam = 0.5,
hit = 0.33,
hp = 0.01,
mana = 0.005,
allphys = 0.05,
allmagic = 0.03,
avedam = 1,
offhandDam = 0.5,
shield = 10,
sanctuary = 50
}
}
})
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.priority.addDefault: Failed to add priority \"debug\": " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.priority.addDefault
-- ordinal number, name, and description of each possible priority field
inv.priority.fieldTable = {
{ "str" , "Value of 1 point of the strength stat" },
{ "int" , "Value of 1 point of the intelligence stat" },
{ "wis" , "Value of 1 point of the wisdom stat" },
{ "dex" , "Value of 1 point of the dexterity stat" },
{ "con" , "Value of 1 point of the constitution stat" },
{ "luck" , "Value of 1 point of the luck stat" },
{ "dam" , "Value of 1 point of damroll" },
{ "hit" , "Value of 1 point of hitroll" },
{ "avedam" , "Value of 1 point of primary weapon ave damage" },
{ "offhandDam" , "Value of 1 point of offhand weapon ave damage" },
{ "hp" , "Value of 1 hit point" },
{ "mana" , "Value of 1 mana point" },
{ "moves" , "Value of 1 movement point" },
{ "sanctuary" , "Value placed on the sanctuary effect " },
{ "haste" , "Value placed on the haste effect " },
{ "flying" , "Value placed on the flying effect " },
{ "invis" , "Value placed on the invisible effect " },
{ "regeneration", "Value placed on the regeneration effect" },
{ "detectinvis" , "Value placed on the detect invis effect " },
{ "detecthidden", "Value placed on the detect hidden effect " },
{ "detectevil" , "Value placed on the detect evil effect " },
{ "detectgood" , "Value placed on the detect good effect " },
{ "detectmagic" , "Value placed on the detect magic effect " },
{ "dualwield" , "Value of an item's dual wield effect" },
{ "irongrip" , "Value of an item's irongrip effect" },
{ "shield" , "Value of a shield's damage reduction effect" },
{ "allmagic" , "Value of 1 point in each magical resist type" },
{ "allphys" , "Value of 1 point in each physical resist type" },
{ "maxstr" , "Value of hitting a level's strength ceiling" },
{ "maxint" , "Value of hitting a level's intelligence ceiling" },
{ "maxwis" , "Value of hitting a level's wisdom ceiling" },
{ "maxdex" , "Value of hitting a level's dexterity ceiling" },
{ "maxcon" , "Value of hitting a level's constitution ceiling" },
{ "maxluck" , "Value of hitting a level's luck ceiling" },
{ "bash" , "Value of 1 point of bash physical resistance" },
{ "pierce" , "Value of 1 point of pierce physical resistance" },
{ "slash" , "Value of 1 point of slash physical resistance" },
{ "acid" , "Value of 1 point of acid magical resistance" },
{ "air" , "Value of 1 point of air magical resistance" },
{ "cold" , "Value of 1 point of cold magical resistance" },
{ "disease" , "Value of 1 point of disease magical resistance" },
{ "earth" , "Value of 1 point of earth magical resistance" },
{ "electric" , "Value of 1 point of electric magical resistance" },
{ "energy" , "Value of 1 point of energy magical resistance" },
{ "fire" , "Value of 1 point of fire magical resistance" },
{ "holy" , "Value of 1 point of holy magical resistance" },
{ "light" , "Value of 1 point of light magical resistance" },
{ "magic" , "Value of 1 point of magic magical resistance" },
{ "mental" , "Value of 1 point of mental magical resistance" },
{ "negative" , "Value of 1 point of negative magical resistance" },
{ "poison" , "Value of 1 point of poison magical resistance" },
{ "shadow" , "Value of 1 point of shadow magical resistance" },
{ "sonic" , "Value of 1 point of sonic magical resistance" },
{ "water" , "Value of 1 point of water magical resistance" },
{ "~light" , "Set to 1 to disable the light location" },
{ "~head" , "Set to 1 to disable the head location" },
{ "~eyes" , "Set to 1 to disable the eyes location" },
{ "~lear" , "Set to 1 to disable the left ear location" },
{ "~rear" , "Set to 1 to disable the right ear location" },
{ "~neck1" , "Set to 1 to disable the neck1 location" },
{ "~neck2" , "Set to 1 to disable the neck2 location" },
{ "~back" , "Set to 1 to disable the back location" },
{ "~medal1" , "Set to 1 to disable the medal1 location" },
{ "~medal2" , "Set to 1 to disable the medal2 location" },
{ "~medal3" , "Set to 1 to disable the medal3 location" },
{ "~medal4" , "Set to 1 to disable the medal4 location" },
{ "~torso" , "Set to 1 to disable the torso location" },
{ "~body" , "Set to 1 to disable the body location" },
{ "~waist" , "Set to 1 to disable the waist location" },
{ "~arms" , "Set to 1 to disable the arms location" },
{ "~lwrist" , "Set to 1 to disable the left wrist location" },
{ "~rwrist" , "Set to 1 to disable the right wrist location" },
{ "~hands" , "Set to 1 to disable the hands location" },
{ "~lfinger" , "Set to 1 to disable the left finger location" },
{ "~rfinger" , "Set to 1 to disable the right finger location" },
{ "~legs" , "Set to 1 to disable the legs location" },
{ "~feet" , "Set to 1 to disable the feet location" },
{ "~shield" , "Set to 1 to disable the shield location" },
{ "~wielded" , "Set to 1 to disable the wielded location" },
{ "~second" , "Set to 1 to disable the second location" },
{ "~hold" , "Set to 1 to disable the hold location" },
{ "~float" , "Set to 1 to disable the float location" },
{ "~above" , "Set to 1 to disable the above location" },
{ "~portal" , "Set to 1 to disable the portal location" },
{ "~sleeping" , "Set to 1 to disable the sleeping location" }
}
----------------------------------------------------------------------------------------------------
--
-- Module to score items and sets based on a specified priority, level, and handicap
--
-- We "handicap" an item or set when it is overmax on one or more stats. Handicapping an overmax
-- stat lets us see if we can increase the overall score of a set by prioritizing other stats
-- instead. In other words, we try to trade off unused overmax stats for another stat that we can
-- actually use.
--
-- The base scoring function is inv.score.extended(). It takes a structure representing either an
-- item or a set and scores that structure based on the given parameters. The inv.score.item() and
-- inv.score.set() functions convert an itemId or a set into the structure required by the
-- inv.score.extended() function and then they call that function behind the scenes.
--
-- inv.score.item(itemId, priorityName, handicap, level)
-- inv.score.set(set, priorityName, handicap, level)
-- inv.score.extended(itemOrSet, priorityName, handicap, level, isOffhand)
--
----------------------------------------------------------------------------------------------------
inv.score = {}
-- The handicap and level params are optional. If level is not given, we use the current level.
-- This returns both a primary score and, for weapons, a score for that weapon in an offhand
-- position. Non-weapons do not have an offhandScore.
function inv.score.item(itemId, priorityName, handicap, level)
local retval
local score = 0
local offhandScore = 0
itemId = tonumber(itemId) or ""
if (itemId == nil) or (itemId == "") then
dbot.warn("inv.score.item: itemId parameter is not a number")
return score, offhandScore, DRL_RET_INVALID_PARAM
end -- if
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.score.item: priorityName parameter is missing")
return score, offhandScore, DRL_RET_INVALID_PARAM
end -- if
-- Find the item's stats entry corresponding to the given itemId
local itemStats = inv.items.getField(itemId, invFieldStats)
if (itemStats == nil) then
dbot.warn("inv.score.item: Object ID " .. itemId .. " does not match an identified item in your inventory")
return score, offhandScore, DRL_RET_MISSING_ENTRY
end -- if
-- Get the basic score for the item
score, retval = inv.score.extended(itemStats, priorityName, handicap, level, false)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.score.item: Failed to score item " .. itemId .. ": " .. dbot.retval.getString(retval))
return score, offhandScore, retval
end -- if
-- Weapons can have two scores: a primary score, and an offhand score
local itemType = inv.items.getStatField(itemId, invStatFieldWearable) or ""
if (itemType == "wield") then
offhandScore, retval = inv.score.extended(itemStats, priorityName, handicap, level, true)
end -- if
return score, offhandScore, retval
end -- inv.score.item
-- Get a score for either an item or a set
function inv.score.extended(itemOrSet, priorityName, handicap, level, isOffhand)
local score = 0
local priorityTable
local retval
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.score.extended: priorityName parameter is missing")
return score, DRL_RET_INVALID_PARAM
end -- if
if (itemOrSet == nil) then
dbot.warn("inv.score.extended: Missing item or set to be scored")
return score, DRL_RET_INVALID_PARAM
end -- if
-- Determine our level (this accounts for tiers so T1 L10 would have a level of 20)
-- The caller may tell us the target level, otherwise, we use our current level
level = tonumber(level)
if (level == nil) then
level = dbot.gmcp.getLevel()
end -- if
-- Pull out the priority table from the priority block for our level
priorityTable, retval = inv.priority.get(priorityName, level)
if (priorityTable == nil) then
dbot.warn("inv.score.extended: Priority \"" .. priorityName .. "\" does not have a priority table " ..
"for level " .. level)
return score, retval
end -- if
-- Run through all stats and add up the item's score
for k,v in pairs(itemOrSet) do
local statKey = k
-- Use the "offhandDam" priority instead of "avedam" priority if we are evaluating a weapon
-- for the offhand location
if (isOffhand == true) and (k == invStatFieldAveDam) then
statKey = "offhandDam"
end -- if
-- Update the score for any effects in the "affectMods" field (yes, I really think it should be
-- "effectMods", but I'm keeping the terminology aard uses)
if (k == invStatFieldAffectMods) and (v ~= nil) and (v ~= "") then
-- Strip out commas from the list so that we can easily pull mod words out of the string
local modList = string.gsub(v, ",", ""):lower()
for mod in modList:gmatch("%S+") do
dbot.debug("inv.score.extended: mod = \"" .. mod .. "\"")
local modValue = tonumber(priorityTable[mod] or "")
if (modValue ~= nil) and (modValue ~= 0) then
score = score + modValue
end -- if
end -- for
-- Update the score for individual stats
elseif (priorityTable[statKey] ~= nil) then
if (priorityTable[statKey] == 0) then
multiplier = 0
else
multiplier = priorityTable[statKey]
end -- if
-- some stats have handicaps that reduce their score
if (handicap ~= nil) and (handicap[statKey] ~= nil) then
multiplier = multiplier + handicap[statKey]
end -- if
score = score + (multiplier * v)
dbot.debug("Score: " .. string.format("%.3f", score) .. " after key \"" ..
statKey .. "\" with value \"" .. v .. "\", multiplier=" .. multiplier)
-- Update the score for consolidated stats
else
-- If a field in the item or set's stats matches a field in the priority table, we've already
-- accounted for that portion of the score (see the if clause above.) In the else clause
-- here, we handle situations where stats are consolidated. For example, a priority may
-- include the "allmagic" or "allphys" resistances while an item may list individual resists.
-- In that case, we count each individual resist as being worth 1/(# resists) of a full
-- magic or physical resist. For example, 1 bash resist is worth 1/3 of a full physical
-- resist in our scoring because there are three potential types of physical resistances.
-- If a user wants more granularity than that, they can call out specific resistances in the
-- priority table (e.g., use invStatFieldBash instead of "allphys").
local dtype
local physResists = { invStatFieldBash, invStatFieldPierce, invStatFieldSlash }
local magicResists = { invStatFieldAcid, invStatFieldCold, invStatFieldEnergy,
invStatFieldHoly, invStatFieldElectric, invStatFieldNegative,
invStatFieldShadow, invStatFieldMagic, invStatFieldAir,
invStatFieldEarth, invStatFieldFire, invStatFieldLight,
invStatFieldMental, invStatFieldSonic, invStatFieldWater,
invStatFieldDisease, invStatFieldPoison }
for i,v2 in ipairs(magicResists) do
dtype = v2:lower()
if (statKey == dtype) and ((priorityTable[invStatFieldAllMagic] or 0) > 0) then
score = score + (priorityTable[invStatFieldAllMagic] * v / #magicResists)
break
end -- if
end -- for
for i,v2 in ipairs(physResists) do
dtype = v2:lower()
if (statKey == dtype) and ((priorityTable[invStatFieldAllPhys] or 0) > 0) then
score = score + (priorityTable[invStatFieldAllPhys] * v / #physResists)
break
end -- if
end -- for
end -- if
end -- for
-- In some situations, we may want to give a score bump if we have maxed a particular stat.
-- For example, navigators gain an extra bypassed area with maxed int and wis. In that case,
-- it is far more valuable to have maxed int and wis than to be "off-by-one" and be one less
-- than the max.
local statList = "int luck wis str dex con"
for stat in statList:gmatch("%S+") do
local valueOfMaxStat = tonumber(priorityTable["max" .. stat] or 0)
if (valueOfMaxStat > 0) and
(inv.statBonus.equipBonus[level] ~= nil) and (inv.statBonus.equipBonus[level][stat] ~= nil) and
(tonumber(itemOrSet[stat] or 0) >= inv.statBonus.equipBonus[level][stat]) then
dbot.debug("Added " .. valueOfMaxStat .. " to score for maxing stat \"@G" .. stat .. "@W\"")
score = score + valueOfMaxStat
end -- if
end -- for
-- Round to nearest hundredth place
score = tonumber(string.format("%.2f", score))
dbot.debug("Item \"" .. itemOrSet.name .. "\" has a score of " .. string.format("%.3f", score) ..
" for priority \"" .. priorityName .. "\"")
return score, DRL_RET_SUCCESS
end -- inv.score.extended
-- The "set" parameter is a table with wearable locations as the keys and objID as the value.
-- Note that we don't have a "handicap" parameter here. We don't handicap sets. We handicap
-- the scoring of items as we generate sets.
function inv.score.set(set, priorityName, level)
local retval
local setStats
local setScore
if (set == nil) then
dbot.warn("inv.score.set: \"set\" parameter is missing")
return nil, nil, DRL_RET_INVALID_PARAM
end -- if
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.score.set: priorityName is missing")
return nil, nil, DRL_RET_INVALID_PARAM
end -- if
setStats = inv.set.getStats(set, level)
setStats.name = "Set for level " .. (level or "Unknown") .. " " .. priorityName
setScore, _, retval = inv.score.extended(setStats, priorityName, nil, level, false)
dbot.debug("setScore = " .. setScore)
return setScore, setStats, retval
end -- inv.score.set
----------------------------------------------------------------------------------------------------
--
-- Module to manage equipment sets
--
-- dinv set [display | wear | stats] [priority name] <level>
--
-- inv.set.init.atActive()
-- inv.set.fini(doSaveState)
--
-- inv.set.save()
-- inv.set.load()
-- inv.set.reset()
--
-- inv.set.create(priorityName, level, synchronous, intensity)
-- inv.set.createCR()
-- inv.set.createWithHandicap(priorityName, level, handicap)
--
-- inv.set.display(priorityName, level, endTag)
-- inv.set.displayCR()
-- inv.set.displaySet(setName, level, equipSet)
--
-- inv.set.createAndWear(priorityName, level, intensity, endTag)
-- inv.set.createAndWearCR()
-- inv.set.wear(equipSet)
--
-- inv.set.diff(set1, set2, level)
-- inv.set.displayDiff(set1, set2, level, msgString, doPrintHeader)
--
-- inv.set.get(priorityName, level)
-- inv.set.getStats(set, level)
-- inv.set.displayStats(setStats, msgString, doPrintHeader, doDisplayIfZero)
--
-- inv.set.compare(priorityName, rname, endTag)
-- inv.set.compareCR()
--
-- inv.set.covet(priorityName, auctionNum, endTag)
-- inv.set.covetCR()
--
-- We maintain a table holding the most recently created sets for each priority and level. This
-- is handy when we want to analyze the usage of a particular item. It's not a perfect system
-- because we may need to re-run set creation after aquiring new items or getting a good spellup,
-- but it's a decent approximation -- and the user can always re-generate the full table if they
-- have the time and inclination.
--
-- Example format:
-- inv.set.table = { enchanter = { 1 = { hands = { id = someItemId, score = myScore },
-- ...
-- back = { id = someItemId, score = myScore } },
-- ...
-- 291 = { hands = { id = someItemId, score = myScore },
-- ...
-- back = { id = someItemId, score = myScore } } }
--
-- psi-melee = { 1 = { abc },
-- ...
-- 291 = { xyz } } }
--
----------------------------------------------------------------------------------------------------
inv.set = {}
inv.set.init = {}
inv.set.table = {}
inv.set.stateName = "inv-set.state"
inv.set.createPkg = nil
inv.set.displayPkg = nil
inv.set.createAndWearPkg = nil
-- We spend more time trying to find optimal sets if we are only looking at one set instead of
-- a full analysis of 200 sets. The equipment search will be more rigorous at higher intensities.
inv.set.analyzeIntensity = 8
inv.set.createIntensity = 20
function inv.set.init.atActive()
local retval = DRL_RET_SUCCESS
retval = inv.set.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.init.atActive: failed to load set data from storage: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.set.init.atActive
function inv.set.fini(doSaveState)
local retval = DRL_RET_SUCCESS
if (doSaveState) then
-- Save our current data
retval = inv.set.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.fini: Failed to save inv.set module data: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.set.fini
function inv.set.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.set.stateName,
"inv.set.table", inv.set.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.save: Failed to save set table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.set.save
function inv.set.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.set.stateName, inv.set.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.set.stateName .. "@W\": " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.set.load
function inv.set.reset()
inv.set.table = {}
return inv.set.save()
end -- inv.set.reset
-- Find a set of items in the equipment table that most closely matches the
-- priorities given as a parameter. Label each of those items in the equipment
-- table with the given name for the set.
--
-- If level is not provided to us, use the character's current level
function inv.set.create(priorityName, level, synchronous, intensity)
local retval
local priorityTable
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.set.create: Missing priorityName parameter")
return DRL_RET_INVALID_PARAM
end -- if
-- Attempt to get a valid level. If the caller doesn't provide a level, use our current level
level = tonumber(level) or ""
if (level == nil) or (level == "") then
level = dbot.gmcp.getLevel()
end -- if
-- If the caller doesn't specify if this is a synchronous or asynchronous call, assume it is asynch
if (synchronous == nil) or (synchronous == "") then
synchronous = drlAsynchronous
end -- if
-- This is a rather ugly kludge. Checking if the char can access the dual wield skill is currently
-- the only reason (other than getting current stats) we might need to run asynchronously in a co-routine.
-- If we have already cached whether or not the ability is available, we can potentially run the set
-- creation synchronously. If we haven't cached if the ability is available, we must go asynchronous.
if (dbot.ability.isCached("dual wield", level) == false) then
synchronous = drlAsynchronous
end -- if
-- Check if the specified priority exists for the specified level
priorityTable, retval = inv.priority.get(priorityName, level)
if (priorityTable == nil) then
dbot.warn("inv.set.create: Priority \"" .. priorityName .. "\" does not have a priority table " ..
"for level " .. level)
return retval
end -- if
-- Only allow one set creation at a time
if (inv.set.createPkg ~= nil) then
dbot.info("Set creation skipped: another set creation is in progress")
return DRL_RET_BUSY
end -- if
-- Whack the previous equipment set for this priority and level (if it exists). The creation
-- of the set happens asynchronously in a co-routine. If we want to know when the creation is
-- done, we can spin and wait for the set to become non-nil.
if (inv.set.table[priorityName] == nil) then
inv.set.table[priorityName] = {}
end -- if
inv.set.table[priorityName][level] = nil
-- Everything looks good :) Kick off the actual creation of the set!
inv.set.createPkg = {}
inv.set.createPkg.level = level
inv.set.createPkg.name = priorityName
inv.set.createPkg.bonusType = invStatBonusTypeAve -- default to using a weighted average for stats
inv.set.createPkg.intensity = intensity
-- We want to call inv.set.createCR() but we may or may not want it in a co-routine. If the
-- caller asked us to complete the set creation synchronously, we do not use a co-routine. Also,
-- if we are creating a set for the user's current level, we want to use the stat bonus that
-- they have at this exact moment instead of using the weighted average (which is useful for
-- guessing bonuses when we don't have the exact data available.) The only oddity here is that
-- we also use the weighted average regardless of level when this is a synchronous call. We need
-- to sleep/wait to get the exact current stats and we can't do that in synchronous mode.
if (synchronous == drlSynchronous) then
inv.set.createCR() -- Call this directly instead of as a co-routine
elseif (synchronous == drlAsynchronous) then
if (level == dbot.gmcp.getLevel()) then
inv.statBonus.timer.update(0, 0.1)
inv.set.createPkg.bonusType = invStatBonusTypeCurrent
inv.set.createPkg.waitForStatBonus = true
end -- if
wait.make(inv.set.createCR)
else
dbot.warn("inv.set.create: Invalid synchronous parameter \"" .. (synchronous or "") .. "\"")
retval = DRL_INVALID_PARAM
end -- if
return retval
end -- inv.set.create
function inv.set.createCR()
local retval = DRL_RET_SUCCESS
-- Pull params from our co-routine package
local priorityName = inv.set.createPkg.name
local level = inv.set.createPkg.level
local intensity = inv.set.createPkg.intensity or inv.set.createIntensity
-- If we want to use the exact bonuses that the char has right now instead of using the weighted
-- average for the level, we need to wait for the statBonus timer to complete. In practice, we
-- will only stall when we are creating a set for the user's current level. We can skip this if
-- the user is creating sets for analysis of other levels.
if (inv.set.createPkg.waitForStatBonus == true) then
-- Give the stat bonus timer a chance to run. Yes, this is a little awkward and we probably
-- should use a call-by-reference parameter as a callback mechanism to know when the timer
-- completes. This probably isn't as evil as it looks at first glance though. This is not
-- time critical because this code only runs if someone wears or displays a single equipment
-- set and waiting a little while won't kill you. Also, if the timer doesn't manage to
-- run (which isn't likely) then the worst case scenario is that we pick up the stat bonuses
-- from the previous set creation for the user's current level.
wait.time(1)
local totTime = 0
local timeout = 2
while (inv.statBonus.inProgress == true) do
if (totTime > timeout) then
dbot.warn("inv.set.createCR: timed out waiting for stat bonus to be detected")
retval = DRL_RET_TIMEOUT
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
end -- if
-- Determine how much each stat can increase due to equipment without hitting that stat's ceiling
local statDelta = inv.statBonus.get(level, inv.set.createPkg.bonusType)
-- Start with no stat handicaps. We handicap stats when a set goes "overstat" so that we gradually
-- bump up the priority of "lesser" stats in an attempt to increase the set's overall score. We
-- adjust an overstat stat by x% each iteration. That allows us to do up to "intensity" iterations.
local handicap = { int = 0, wis = 0, luck = 0, str = 0, dex = 0, con = 0 }
local handicapDelta = (1 / intensity) -- Amount that we increase a stat's handicap each iteration
-- Keep building new sets with greater stat handicaps until we fully compensate for any
-- overmax stats
local bestScore = 0
local bestSet = nil
local numIters = 0
local newSet
local stats
local score
repeat
newSet, stats, score = inv.set.createWithHandicap(priorityName, level, handicap)
if (score > bestScore) then
local wearLoc
local itemStruct
dbot.debug("Updating set based on handicap")
for wearLoc,itemStruct in pairs(newSet) do
if (bestSet ~= nil) and (bestSet[wearLoc] ~= nil) and (bestSet[wearLoc].id ~= itemStruct.id) then
dbot.debug("Updating set: " .. wearLoc .. " from " .. bestSet[wearLoc].id .. ", to " ..
itemStruct.id)
end -- if
end -- for
bestScore = score
bestSet = newSet
end -- if
-- For every stat that is "overstat", discount that stat by handicapping it in the next iteration
local handicapExistsThisIter = false -- if we see an overmax stat below, we set this to true
for k,v in pairs(stats) do
if (statDelta[k] ~= nil) then
dbot.debug("delta[" .. k .. "] = " .. statDelta[k] .. ", statValue = " .. v)
end -- if
if (statDelta[k] ~= nil) and ((v - statDelta[k]) >= 0) then
handicap[k] = handicap[k] - handicapDelta
dbot.debug("Set handicap for \"" .. k .. "\" to " .. handicap[k])
handicapExistsThisIter = true
end -- if
end -- for
if (handicapExistsThisIter == false) then
break
end -- if
numIters = numIters + 1
-- Each iteration drops the weighting of a handicapped stat
if (numIters >= intensity) then
dbot.debug("Breaking out of inv.set.createCR, looped over handicap " .. numIters .. " times")
break
end -- if
until (score < (bestScore * 0.8)) -- Let things anneal a bit, but cut it off if we are < x% of previous best
-- Some items can be worn in multiple locations (e.g., a ring could be on "lfinger" or "rfinger" or
-- a medal could be on "medal1" through "medal4"). We want to always be consistent on where items
-- go so that we don't unnecessarily swap 2 rings back and forth between fingers each time we wear
-- a set. The "normalize" code below sorts items by object ID and puts them in order within the set.
-- For example, the lfinger item will always have a smaller object ID than the rfinger item.
for _, wlocArray in pairs(inv.wearables) do
-- If this is a wearable type with more than one location (e.g., "finger", or "medal") then
-- we sort the locations for that type. The one exception is that we don't want to sort
-- the "wielded" and "second" locations because those have very different meanings.
if (#wlocArray > 1) and (wlocArray[1] ~= "wielded") then
local idArray = {}
for _, wloc in ipairs(wlocArray) do
if (bestSet ~= nil) and (bestSet[wloc] ~= nil) then
table.insert(idArray, bestSet[wloc])
end -- if
end -- for
table.sort(idArray, function (v1, v2) return v1.id < v2.id end)
-- We now know the order that the items should be for this wearable type
-- Put them back in the set in the proper order
for i, wloc in ipairs(wlocArray) do
if (i <= #idArray) then
bestSet[wloc] = idArray[i]
end -- if
end -- for
end -- if
end -- for
-- If our best set is empty (maybe we don't have anything in our inventory) then treat that as a
-- special case and let the caller know about it
if (bestSet == nil) then
dbot.warn("inv.set.createCR: No items in your inventory fit the set. Is your inventory empty?")
dbot.warn("Do you need to refresh or build your inventory?")
end -- if
-- Wait until the end to save the set (calling functions can know it isn't ready yet if it is nil)
inv.set.table[priorityName][level] = bestSet
dbot.debug("Created " .. priorityName .. "[" .. level .. "] set with score " ..
string.format("%.2f", bestScore))
-- We are done!
inv.set.createPkg = nil
return DRL_RET_SUCCESS
end -- inv.set.createCR
function inv.set.createWithHandicap(priorityName, level, handicap)
local objId
local setScore
local setStats
local score
local offhandScore
local weaponArray = {}
-- Start a new set that we'll gradually fill in
local newSet = {}
-- If level is nil, use the current level
if (level == nil) then
level = dbot.gmcp.getLevel() or 0
end -- if
-- We don't want to scan GMCP for each item so we grab the char's alignment here outside
-- of the for loop
local isGood = dbot.gmcp.isGood()
local isNeutral = dbot.gmcp.isNeutral()
local isEvil = dbot.gmcp.isEvil()
for objId,_ in pairs(inv.items.table) do
local objIdentified = inv.items.getField(objId, invFieldIdentifyLevel) or ""
local objLevel = tonumber(inv.items.getStatField(objId, invStatFieldLevel) or "")
local objWearable = inv.items.getStatField(objId, invStatFieldWearable) or ""
local objWeight = tonumber(inv.items.getStatField(objId, invStatFieldWeight) or 0)
local objFlags = inv.items.getStatField(objId, invStatFieldFlags) or ""
if ((objIdentified == invIdLevelPartial) or (objIdentified == invIdLevelFull)) and
(objLevel ~= nil) and (objLevel <= level) then
-- Check the object alignment (we first strip out commas in the flags to make searching easier)
objFlags = string.gsub(objFlags, ",", "")
if (dbot.isWordInString("anti-good", objFlags) and isGood) or
(dbot.isWordInString("anti-neutral", objFlags) and isNeutral) or
(dbot.isWordInString("anti-evil", objFlags) and isEvil) then
dbot.debug("Skipping item: align=" .. (dbot.gmcp.getAlign() or "nil") ..
", flags=\"" .. (objFlags or "nil") .. "\"")
-- The alignment is acceptable. Check the other requirements...
elseif (objWearable ~= nil) and (objWearable ~= "") and (inv.wearables[objWearable] ~= nil) then
score, offhandScore = inv.score.item(objId, priorityName, handicap, level)
local nextBest = { id = objId, score = score }
-- We keep track of all weapons so that we can evaluate the best combination after we
-- see everything in our inventory
if (objWearable == "wield") then
table.insert(weaponArray, { id = objId, score = score, offhand = offhandScore, weight = objWeight })
end -- if
for _,w in ipairs(inv.wearables[objWearable]) do
-- Set a default (low) score if we haven't used this slot yet
if (newSet[w] == nil) then
newSet[w] = { id = -1, score = -1 }
end -- if
-- If the current item's score is greater than the slot's score, iterate and bump
if (nextBest.score > newSet[w].score) then
local tmp = newSet[w]
newSet[w] = nextBest
local nextBestName = inv.items.getStatField(nextBest.id, invStatFieldName)
if (nextBestName ~= nil) then
dbot.debug("Upgrading \"" .. w .. "\" to \"" .. nextBestName .. "\"")
end -- if
nextBest = tmp
end -- if
end -- for
-- prune slots that don't have any items or slots that are ignored
for _,w in ipairs(inv.wearables[objWearable]) do
if (newSet[w].id == -1) or (not inv.priority.locIsAllowed(w, priorityName, level)) then
newSet[w] = nil
end -- if
end -- for
end -- if
end -- if
end -- for
-- Check if the char has access to dual weapons at this level by checking the char's
-- class and checking if aard gloves are in the set
--
-- If the ability is not cached, this will need to be called asynchronously so that we can
-- call "showskill" and see at what level the ability is available. If the ability is cached
-- then we can run this synchronously if we wish.
local dualWieldAvailable = dbot.ability.isAvailable("dual wield", level)
-- Check if the set has aard gloves in it. If so, we automatically have access to dual wield :)
if (dualWieldAvailable == false) and (newSet["hands"] ~= nil) then
local handsId = tonumber(newSet["hands"].id or "")
if (handsId ~= nil) then
local handsName = inv.items.getStatField(handsId, invStatFieldName) or ""
if (handsName == "Aardwolf Gloves of Dexterity") then
dualWieldAvailable = true
end -- if
end -- if
end -- if
-- Check if the priority explicitly bans the "second" wield slot
if (dualWieldAvailable) then
dualWieldAvailable = inv.priority.locIsAllowed(inv.wearLoc[invWearableLocSecond], priorityName, level)
end -- if
-- We already know the highest scoring solo weapon (it is in the "wielded" slot). We now
-- find the highest scoring combination of compatible weapons ("wielded" + "second") if the
-- char has access to dual wield.
local bestWeaponSet = { score = 0, primary = nil, offhand = nil }
if (dualWieldAvailable) then
-- Get sorted arrays for all primary weapons and offhand weapons
local offhandArray = dbot.table.getCopy(weaponArray)
table.sort(weaponArray, function (entry1, entry2) return entry1.score > entry2.score end)
table.sort(offhandArray, function (entry1, entry2) return entry1.offhand > entry2.offhand end)
for _, primary in ipairs(weaponArray) do
for _, offhand in ipairs(offhandArray) do
if (primary.weight >= offhand.weight * 2) and (primary.id ~= offhand.id) then
if (primary.score + offhand.offhand > bestWeaponSet.score) then
bestWeaponSet.score = primary.score + offhand.offhand
bestWeaponSet.primary = { id = primary.id, score = primary.score }
bestWeaponSet.offhand = { id = offhand.id, score = offhand.offhand }
end -- if
break -- this is the highest possible offhand score for the current primary weapon
end -- if
end -- for
end -- for
end -- if
local scorePrimary = 0
local scoreSecond = 0
local scoreShield = 0
local scoreHold = 0
if (newSet[inv.wearLoc[invWearableLocWielded]] ~= nil) then
scorePrimary = newSet[inv.wearLoc[invWearableLocWielded]].score or 0
end -- if
if (newSet[inv.wearLoc[invWearableLocSecond]] ~= nil) then
scoreSecond = newSet[inv.wearLoc[invWearableLocSecond]].score or 0
end -- if
if (newSet[inv.wearLoc[invWearableLocShield]] ~= nil) then
scoreShield = newSet[inv.wearLoc[invWearableLocShield]].score or 0
end -- if
if (newSet[inv.wearLoc[invWearableLocHold]] ~= nil) then
scoreHold = newSet[inv.wearLoc[invWearableLocHold]].score or 0
end -- if
-- Check if we want dual weapons or primary weapon + shield + hold. If we are using dual weapons,
-- whack the shield and hold items in the set because we won't be using them. Otherwise, stick with
-- the highest scoring weapon, shield, and hold items that we found in the initial search.
if dualWieldAvailable and (bestWeaponSet.score > scorePrimary + scoreShield + scoreHold) then
if inv.priority.locIsAllowed(inv.wearLoc[invWearableLocWielded], priorityName, level) then
newSet[inv.wearLoc[invWearableLocWielded]] = bestWeaponSet.primary
newSet[inv.wearLoc[invWearableLocSecond]] = bestWeaponSet.offhand
else
newSet[inv.wearLoc[invWearableLocWielded]] = nil
newSet[inv.wearLoc[invWearableLocSecond]] = bestWeaponSet.primary
end -- if
newSet[inv.wearLoc[invWearableLocShield]] = nil
newSet[inv.wearLoc[invWearableLocHold]] = nil
else
newSet[inv.wearLoc[invWearableLocSecond]] = nil
end -- if
setScore, setStats = inv.score.set(newSet, priorityName, level)
return newSet, setStats, setScore
end -- inv.set.createWithHandicap
inv.set.displayPkg = nil
function inv.set.display(priorityName, level, endTag)
local retval
local priorityTable
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.set.display: missing priorityName parameter")
return inv.tags.stop(invTagsSet, endTag, DRL_RET_INVALID_PARAM)
end -- if
level = tonumber(level) or ""
if (level == nil) or (level == "") then
level = dbot.gmcp.getLevel()
end -- if
-- Check if the specified priority exists for the specified level
priorityTable, retval = inv.priority.get(priorityName, level)
if (priorityTable == nil) then
dbot.warn("inv.set.display: Priority \"" .. priorityName .. "\" does not have a priority table " ..
"for level " .. level)
return inv.tags.stop(invTagsSet, endTag, retval)
end -- if
-- Only allow one set creation at a time
if (inv.set.displayPkg ~= nil) then
dbot.info("Set display skipped: another set display is in progress")
return inv.tags.stop(invTagsSet, endTag, DRL_RET_BUSY)
end -- if
inv.set.displayPkg = {}
inv.set.displayPkg.name = priorityName
inv.set.displayPkg.level = level
inv.set.displayPkg.intensity = inv.set.createIntensity
inv.set.displayPkg.endTag = endTag
-- Kick off the display co-routine to display the set
wait.make(inv.set.displayCR)
return DRL_RET_SUCCESS
end -- inv.set.display
function inv.set.displayCR()
local retval = DRL_RET_SUCCESS
local priorityName = inv.set.displayPkg.name or "Unknown"
local level = inv.set.displayPkg.level or 0
local intensity = inv.set.displayPkg.intensity or inv.set.createIntensity
local endTag = inv.set.displayPkg.endTag
-- Create the set that we want to display
retval = inv.set.create(priorityName, level, drlAsynchronous, intensity)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.displayCR: failed to create set " .. priorityName .. "[" .. level .. "]: " ..
dbot.retval.getString(retval))
inv.set.displayPkg = nil
return inv.tags.stop(invTagsSet, endTag, retval)
end -- if
-- Spin and wait until the new set is not nil
local waitForSetDisplayTimeout = 0
local waitForSetDisplayThreshold = 10
while (inv.set.table[priorityName][level] == nil) do
wait.time(drlSpinnerPeriodDefault)
waitForSetDisplayTimeout = waitForSetDisplayTimeout + drlSpinnerPeriodDefault
if (waitForSetDisplayTimeout > waitForSetDisplayThreshold) then
dbot.error("inv.set.displayCR: Failed to create set " .. priorityName .. "[" .. level .. "] within " ..
waitForSetDisplayThreshold .. " seconds")
inv.set.displayPkg = nil
return inv.tags.stop(invTagsSet, endTag, DRL_RET_TIMEOUT)
end -- if
end -- while
retval = inv.set.displaySet(priorityName, level, inv.set.table[priorityName][level])
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.displayCR: Failed to display set: " .. dbot.retval.getString(retval))
end -- if
inv.set.displayPkg = nil
return inv.tags.stop(invTagsSet, endTag, retval)
end -- inv.set.displayCR
function inv.set.displaySet(setName, level, equipSet)
local retval = DRL_RET_SUCCESS
if (setName == nil) or (setName == "") then
dbot.warn("inv.set.displaySet: missing priority name parameter")
return DRL_RET_INVALID_PARAM
end -- if
level = tonumber(level or "")
if (level == nil) then
dbot.print("\n@WEquipment set: \"@C" .. setName .. "@W\"\n")
else
dbot.print("\n@WEquipment set: @GLevel " .. string.format("%3d", level) ..
" @C" .. setName .. "@w\n")
end -- if
for _,v in pairs(inv.wearLoc) do
if (equipSet ~= nil) and (v ~= "undefined") and (equipSet[v] ~= nil) then
local score = equipSet[v].score
local objId = equipSet[v].id
dbot.print("@Y " .. string.format("%08s", v) .. "@W(" .. string.format("%4d", score) ..
"): @GLevel " ..
string.format("%3d", inv.items.getStatField(objId, invStatFieldLevel or "Unknown")) ..
"@W \"" .. (inv.items.getField(objId, invFieldColorName) or "Unidentified") .. "\"")
end -- if
end -- for
local setStats = inv.set.getStats(equipSet, level)
if (setStats ~= nil) then
dbot.print("")
inv.set.displayStats(setStats, "", true, true)
else
dbot.warn("inv.set.displaySet: Failed to retrieve equipment stats for set \"@C" .. setName .. "@W\"")
retval = DRL_RET_MISSING_ENTRY
end -- if
return retval
end -- inv.set.displaySet
function inv.set.createAndWear(priorityName, level, intensity, endTag)
local retval = DRL_RET_SUCCESS
local priorityTable
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.set.createAndWear: missing priorityName parameter")
return inv.tags.stop(invTagsSet, endTag, DRL_RET_INVALID_PARAM)
end -- if
level = tonumber(level) or ""
if (level == nil) or (level == "") then
level = dbot.gmcp.getLevel()
end -- if
-- Check if the specified priority exists for the specified level
priorityTable, retval = inv.priority.get(priorityName, level)
if (priorityTable == nil) then
dbot.warn("inv.set.createAndWear: Priority \"" .. priorityName .. "\" does not have a priority table " ..
"for level " .. level)
return inv.tags.stop(invTagsSet, endTag, retval)
end -- if
if (inv.set.createAndWearPkg ~= nil) then
dbot.info("Skipping request to wear set: another request is in progress")
return inv.tags.stop(invTagsSet, endTag, DRL_RET_BUSY)
end -- if
inv.set.createAndWearPkg = {}
inv.set.createAndWearPkg.priorityName = priorityName
inv.set.createAndWearPkg.level = level
inv.set.createAndWearPkg.intensity = intensity
inv.set.createAndWearPkg.endTag = endTag
wait.make(inv.set.createAndWearCR)
return retval
end -- inv.set.createAndWear
function inv.set.createAndWearCR()
local retval
if (inv.set.createAndWearPkg == nil) then
dbot.error("inv.set.createAndWear: package is nil!")
return DRL_RET_INTERNAL_ERROR
end -- if
local priorityName = inv.set.createAndWearPkg.priorityName or "Unknown"
local level = inv.set.createAndWearPkg.level or 0
local intensity = inv.set.createAndWearPkg.intensity or inv.set.createIntensity
local endTag = inv.set.createAndWearPkg.endTag
-- Create the set that we want to wear
retval = inv.set.create(priorityName, level, drlAsynchronous, intensity)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.createAndWearCR: failed to create set " .. priorityName .. "[" .. level .. "]: " ..
dbot.retval.getString(retval))
inv.set.createAndWearPkg = nil
return inv.tags.stop(invTagsSet, endTag, retval)
end -- if
-- Spin and wait until the new set is not nil
local totTime = 0
local timeout = 10
while (inv.set.table[priorityName][level] == nil) do
if (totTime > timeout) then
dbot.warn("inv.set.createAndWearCR: Failed to create set " .. priorityName ..
"[" .. level .. "] within " .. timeout .. " seconds")
inv.set.createAndWearPkg = nil
return inv.tags.stop(invTagsSet, endTag, DRL_RET_TIMEOUT)
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
-- Attempt to wear the set we just created
retval = inv.set.wear(inv.set.table[priorityName][level])
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.createAndWearCR: Failed to wear set: " .. dbot.retval.getString(retval))
end -- if
-- Clean up and return
inv.set.createAndWearPkg = nil
return inv.tags.stop(invTagsSet, endTag, retval)
end -- inv.set.createAndWearCR
-- Wear all items from the specified set and put away any items unequipped as
-- part of the process.
-- Note: This must be called from within a co-routine
function inv.set.wear(equipSet)
local retval = DRL_RET_SUCCESS
local commandArray = dbot.execute.new()
if (equipSet == nil) then
dbot.warn("inv.set.wear: missing set parameter")
return DRL_RET_INVALID_PARAM
end -- if
local itemLoc
local itemInfo
-- Disable refreshes while we are in the middle of wearing an equipment set
local didDisableRefresh = false
if (inv.state == invStateIdle) then
inv.state = invStatePaused
didDisableRefresh = true
elseif (inv.state == invStateRunning) then
dbot.info("Skipping request to wear an inventory set: you are in the middle of an inventory refresh")
return DRL_RET_BUSY
end -- if
-- Suppress misc. unique messages for item slots and items (e.g., "You proudly pin Academy Graduation
-- Medal to your chest.")
EnableTrigger(inv.items.trigger.wearSpecialName, true)
-- Disable the prompt to make the output look cleaner
dbot.prompt.hide()
-- It's possible that an equipment set doesn't specify what to wear at a particular location.
-- In this case we would like to simply leave that wearable location alone and keep using
-- the equipment (if any) that is already equipped at that location. However, some locations
-- are incompatible with each other and we may be forced to store an item to avoid a conflict.
-- For example, if a set does not have anything at the "second" location but it does include
-- a "hold" or "shield" item, them we don't have any choice. We must store the item that
-- previously was at the "second" location. The code below loops through all items to find
-- all currently equipped items and then stores anything that would be incompatible with the
-- new set.
for _, v in pairs(inv.wearLoc) do
itemLoc = v or "none"
if (equipSet[itemLoc] == nil) then
for objId, objInfo in pairs(inv.items.table) do
local currentLoc = inv.items.getField(objId, invFieldObjLoc) or ""
if (currentLoc == itemLoc) then
local eqPrimary = equipSet[inv.wearLoc[invWearableLocWielded]]
local eqSecond = equipSet[inv.wearLoc[invWearableLocSecond]]
local eqHold = equipSet[inv.wearLoc[invWearableLocHold]]
local eqShield = equipSet[inv.wearLoc[invWearableLocWielded]]
if ((itemLoc == inv.wearLoc[invWearableLocSecond]) and ((eqHold ~= nil) or (eqShield ~= nil))) or
((itemLoc == inv.wearLoc[invWearableLocHold]) and (eqSecond ~= nil)) or
((itemLoc == inv.wearLoc[invWearableLocShield]) and (eqSecond ~= nil)) or
((itemLoc == inv.wearLoc[invWearableLocSecond]) and (eqPrimary ~= nil) and
(2 * tonumber(inv.items.getStatField(objId, invStatFieldWeight) or 0) >
tonumber(inv.items.getStatField(eqPrimary.id, invStatFieldWeight) or 0))) then
dbot.debug("Storing incompatible item at location \"" .. itemLoc .. "\"")
retval = inv.items.storeItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.set.wear: Failed to store item " .. objId .. ": " ..
dbot.retval.getString(retval))
end -- if
end -- if
end -- if
end -- for
end -- if
end -- for
-- Execute the command to store item types that aren't part of the equipment set
if (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 30)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to store unused item types: " .. dbot.retval.getString(retval))
end -- if
commandArray = dbot.execute.new()
end -- if
-- Create a temporary array that we can sort by item location. We need to wear hands before
-- we wear wielded or second so that we can dual wield via aard gloves if they are available.
-- The most convenient way to do that is to simply sort the wearable location alphabetically.
local sortedEq = {}
for itemLoc, itemInfo in pairs(equipSet) do
table.insert(sortedEq, { itemLoc = itemLoc, itemInfo = itemInfo })
end -- for
table.sort(sortedEq, function (v1, v2) return v1.itemLoc < v2.itemLoc end)
-- For each item in the new set, we get the item's object ID and location and then check
-- what is at that item's desired location. If it is already worn at that location, we're
-- done. Otherwise, store the item that is currently worn. It would be convenient if we
-- could simply wear the new item at this point. However, some items conflict with other
-- items (e.g., weapon weights, shields, held items, etc.) and it is easier to simply store
-- everything first and then wear everything in the new set. We know that there are no
-- conflicts with the new set (otherwise it wouldn't be a set!) so we don't have to worry
-- about interference between items in the middle of swapping equipment.
for _, entry in ipairs(sortedEq) do
local itemLoc = entry.itemLoc
local newObjId = entry.itemInfo.id
local objId
local objInfo
-- Find what currently is worn at this item's location
for objId, objInfo in pairs(inv.items.table) do
local currentLoc = inv.items.getField(objId, invFieldObjLoc) or ""
if (currentLoc == itemLoc) then
if (objId == newObjId) then
dbot.debug("Loc \"" .. itemLoc .. "\": Keeping objId=" .. objId)
else
dbot.debug("Loc \"" .. itemLoc .. "\": Swapping " .. objId .. " for " .. newObjId)
retval = inv.items.storeItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to store item " .. objId .. ": " ..
dbot.retval.getString(retval))
end -- if
end -- if
break -- no need to keep searching for the item we are wearing at the target location
end -- if
end -- for
end -- for
-- Execute the command to store old items
if (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 30)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to store old set: " .. dbot.retval.getString(retval))
end -- if
commandArray = dbot.execute.new()
end -- if
local numNewWornItems = 0
-- Get all of the new items to wear and wear them! Yes, we are doing the exact same for loop
-- that we did above. It is a little redundant, but it really simplifies things if we can
-- separate storing old items and wearing new items. If we mix those two steps, we can have
-- conflicts where a new item is conflicting with another item worn in a different location
-- from the previous set (e.g., weapon weights).
for _, entry in ipairs(sortedEq) do
local itemLoc = entry.itemLoc
local itemInfo = entry.itemInfo
local currentLoc = inv.items.getField(itemInfo.id, invFieldObjLoc) or ""
-- Swap out items that are not already in the right location
if (currentLoc ~= itemLoc) then
retval = inv.items.wearItem(itemInfo.id, itemLoc, commandArray, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.set.wear: Failed to wear item " .. (objId or "nil") .. ": " ..
dbot.retval.getString(retval))
else
numNewWornItems = numNewWornItems + 1
end -- if
end -- if
end -- for
-- Execute the command to wear new items
if (commandArray ~= nil) then
retval = dbot.execute.safe.blocking(commandArray, nil, nil, dbot.callback.default, 30)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to wear set: " .. dbot.retval.getString(retval))
end -- if
end -- if
if (retval == DRL_RET_SUCCESS) then
local suffix = ""
if (numNewWornItems ~= 1) then
suffix = "s"
end -- if
dbot.info("Wore " .. numNewWornItems .. " new item" .. suffix)
end -- if
dbot.prompt.show()
-- Stop suppressing unique item or item slot wear messages (e.g., pinning a medal, aard gloves
-- snapping, etc.)
EnableTrigger(inv.items.trigger.wearSpecialName, false)
-- Re-enable refreshes if we disabled them to wear an equipment set
if (didDisableRefresh) then
inv.state = invStateIdle
end -- if
inv.items.save()
return retval
end -- inv.set.wear
function inv.set.diff(set1, set2, level)
local diff = {}
if (set1 == nil) or (set2 == nil) then
dbot.warn("inv.set.diff: nil set given as parameter")
return diff, DRL_RET_INVALID_PARAM
end -- if
local stats1 = inv.set.getStats(set1, level)
local stats2 = inv.set.getStats(set2, level)
if (stats1 == nil) or (stats2 == nil) then
dbot.warn("inv.set.diff: Failed to get stats for given sets")
return diff, DRL_RET_MISSING_ENTRY
end -- if
for statName, statValue in pairs(stats1) do
diff[statName] = stats2[statName] - stats1[statName]
end -- if
return diff, DRL_RET_SUCCESS
end -- if
-- Returns "didFindAStat", retval. This is helpful in knowing if there was actually a difference
-- between the two given sets. The "msgString" is a prefix prepended to each display line. The
-- "doPrintHeader" boolean indicates if we should print a stat header before displaying the stats.
function inv.set.displayDiff(set1, set2, level, msgString, doPrintHeader)
if (set1 == nil) or (set2 == nil) then
dbot.warn("inv.set.displayDiff: nil set given as parameter")
return false, DRL_RET_INVALID_PARAM
end -- if
msgString = msgString or ""
local diffStats = inv.set.diff(set1, set2, level)
return inv.set.displayStats(diffStats, msgString, doPrintHeader, false)
end -- inv.set.diffStats
-- Returns a set table in the form described for the inv.set.getStats input parameter
function inv.set.get(priorityName, level)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.set.get: missing priorityName parameter")
return nil, DRL_RET_INVALID_PARAM
end -- if
level = tonumber(level) or dbot.gmcp.getLevel()
if (inv.set.table[priorityName] == nil) then
dbot.debug("inv.set.get: priority \"" .. priorityName .. "\" sets do not exist")
return nil, DRL_RET_MISSING_ENTRY
end -- if
return inv.set.table[priorityName][level], DRL_RET_SUCCESS
end -- inv.set.get
-- The "set" parameter is a table of the form:
-- { head = { id = someItemId, score = 123 }, lfinger = { id = anotherItemId, score = 456 }, ... }
-- where each wearable location is a key in the "set" table and each value in the table is a
-- structure holding the stats for the item.
function inv.set.getStats(set, level)
local retval = DRL_RET_SUCCESS
if (set == nil) then
dbot.warn("inv.set.getStats: Attempted to get set stats for nil set")
return nil, DRL_RET_INVALID_PARAM
end -- if
local setStats = { int = 0, wis = 0, luck = 0, str = 0, dex = 0, con = 0,
hp = 0, mana = 0, moves = 0,
hit = 0, dam = 0,
avedam = 0, offhandDam = 0,
slash = 0, pierce = 0, bash = 0,
acid = 0, cold = 0, energy = 0, holy = 0, electric = 0, negative = 0, shadow = 0,
poison = 0, disease = 0, magic = 0, air = 0, earth = 0, fire = 0, light = 0,
mental = 0, sonic = 0, water = 0,
allphys = 0, allmagic = 0,
haste = 0, regeneration = 0, sanctuary = 0, invis = 0, flying = 0,
detectgood = 0, detectevil = 0, detecthidden = 0, detectinvis = 0, detectmagic = 0,
dualwield = 0, irongrip = 0, shield = 0 -- these last 3 are not official "affect mods"
}
for itemLoc, itemStruct in pairs(set) do
for statName, statValue in pairs(setStats) do
local objId = tonumber(itemStruct.id) or 0
local itemValue = inv.items.getStatField(objId, statName)
-- Offhand weapons should give stats to offhandDam, not avedam
if (itemLoc == "second") and (statName == invStatFieldAveDam) then
statName = "offhandDam"
statValue = setStats[statName]
end -- if
if (itemValue ~= nil) then
setStats[statName] = statValue + itemValue
end -- if
end -- for
end -- for
-- If the level is available, use it to cap the stats if they are overmax
level = tonumber(level or "")
if (level ~= nil) then
local statsWithCaps = "int wis luck str dex con"
for statName in statsWithCaps:gmatch("%S+") do
if (inv.statBonus.equipBonus[level] ~= nil) and
(inv.statBonus.equipBonus[level][statName] ~= nil) and
(tonumber(setStats[statName] or 0) > inv.statBonus.equipBonus[level][statName]) then
setStats[statName] = inv.statBonus.equipBonus[level][statName]
end -- if
end -- for
end -- if
return setStats, retval
end -- inv.set.getStats
function inv.set.displayStats(setStats, msgString, doPrintHeader, doDisplayIfZero)
local setStr = DRL_XTERM_GREY .. (msgString or "")
local totResists = 0
-- Track if at least one stat has something in it. Unless doDisplayIfZero is true, we will
-- skip displaying stats that don't affect things.
local didFindAStat = false
if (setStats == nil) then
dbot.warn("inv.set.displayStats: set stats are nil")
return didFindAStat, DRL_RET_INVALID_PARAM
end -- if
-- We weight a specific physical or magic resist relative to an "all" resist. For example, 3 "slash"
-- resists are equivalent to 1 "all" phys resist because there are 3 physical resist types. Similarly,
-- one specific magical resist is worth 1/17 of one "all" magical resist value because there are 17
-- magical resistance types.
local resistNames = {}
resistNames[1] = { invStatFieldAllPhys, invStatFieldAllMagic }
resistNames[3] = { invStatFieldBash, invStatFieldPierce, invStatFieldSlash }
resistNames[17] = { invStatFieldAcid, invStatFieldCold, invStatFieldEnergy,
invStatFieldHoly, invStatFieldElectric, invStatFieldNegative,
invStatFieldShadow, invStatFieldMagic, invStatFieldAir,
invStatFieldEarth, invStatFieldFire, invStatFieldLight,
invStatFieldMental, invStatFieldSonic, invStatFieldWater,
invStatFieldDisease, invStatFieldPoison }
for resistWeight, resistTable in pairs(resistNames) do
for _, resistName in ipairs(resistTable) do
totResists = totResists + tonumber(setStats[resistName] or 0) / tonumber(resistWeight)
end -- for
end -- for
setStats.totResists = totResists
local statSizes = { { avedam = 4 }, { offhandDam = 4 }, { hit = 3 }, { dam = 3 },
{ int = 3 }, { wis = 3 }, { luck = 3 }, { str = 3 }, { dex = 3 }, { con = 3 },
{ totResists = 3 }, { hp = 4 }, { mana = 4 }, { moves = 4 } }
local basicHeader = "@W" .. string.rep(" ", #msgString) ..
" Ave Sec HR DR Int Wis Lck Str Dex Con Res HitP Mana Move Effects"
for i, statTable in ipairs(statSizes) do
for statName, statDigits in pairs(statTable) do
if (setStats[statName] ~= nil) and (tonumber(setStats[statName]) ~= 0) then
didFindAStat = true
end -- if
setStr = setStr .. (inv.items.colorizeStat(setStats[statName] or 0, statDigits, false) or "nil") .. " "
end -- for
end -- for
-- Effects (these are known on aard as affectMods)
local effectList = "haste regeneration sanctuary invis flying dualwield irongrip shield " ..
"detectgood detectevil detecthidden detectinvis detectmagic"
for effect in effectList:gmatch("%S+") do
local effectVal = tonumber(setStats[effect] or "")
if (effectVal ~= nil) then
if (effectVal > 0) then
setStr = setStr .. "@G" .. DRL_ANSI_GREEN .. effect .. "@W" .. DRL_ANSI_WHITE .. " "
elseif (effectVal < 0) then
setStr = setStr .. "@R" .. DRL_ANSI_RED .. effect .. "@W" .. DRL_ANSI_WHITE .. " "
end -- if
end -- if
end -- for
if (doDisplayIfZero == true) or (didFindAStat == true) then
if (doPrintHeader == true) then
dbot.print(basicHeader)
end -- if
dbot.print(setStr)
end -- if
return didFindAStat, DRL_RET_SUCCESS
end -- inv.set.setDisplay
inv.set.comparePkg = nil
function inv.set.compare(priorityName, relativeName, endTag)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.set.compare: Missing priorityName parameter")
return inv.tags.stop(invTagsCompare, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (relativeName == nil) or (relativeName == "") then
dbot.warn("inv.set.compare: Missing relativeName parameter")
return inv.tags.stop(invTagsCompare, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.set.table[priorityName] == nil) then
dbot.warn("inv.set.compare: priority \"" .. priorityName .. "\" does not have analysis results. " ..
"You may need to run \"" .. pluginNameCmd .. " analyze create " .. priorityName .. "\".")
return inv.tags.stop(invTagsCompare, endTag, DRL_RET_MISSING_ENTRY)
end -- if
if (inv.set.comparePkg ~= nil) then
dbot.info("Skipping comparison of \"" .. relativeName .. "\" for priority \"" .. priorityName ..
"\": another comparison is in progress")
return inv.tags.stop(invTagsCompare, endTag, DRL_RET_BUSY)
end -- if
inv.set.comparePkg = {}
inv.set.comparePkg.priorityName = priorityName
inv.set.comparePkg.queryString = "rname " .. relativeName
inv.set.comparePkg.endTag = endTag
wait.make(inv.set.compareCR)
return DRL_RET_SUCCESS
end -- inv.set.compare
function inv.set.compareCR()
local retval = DRL_RET_SUCCESS
local idArray = nil
local startLevel = 1 + 10 * dbot.gmcp.getTier()
local didDisableRefresh = false
local endTag = inv.set.comparePkg.endTag
idArray, retval = inv.items.searchCR(inv.set.comparePkg.queryString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("inv.set.compareCR: failed to search inventory table: " .. dbot.retval.getString(retval))
dbot.info("Skipping compare request: could not find the specified item in main inventory")
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("Skipping comparison: No items matched query: \"" .. inv.set.comparePkg.queryString .. "\"")
-- We have the objId for the target item
elseif (#idArray == 1) then
objId = tonumber(idArray[1] or "")
-- If there are residual sets left over from previous analyses that are too low a level to use, we
-- whack those sets so that they don't confuse the analysis
for level = 1, (startLevel - 1) do
local priorityTable = inv.set.table[inv.set.comparePkg.priorityName]
if (priorityTable ~= nil) and (priorityTable[level] ~= nil) then
priorityTable[level] = nil
end -- if
end -- for
-- Save the previous set analysis that includes the target item so that we have something to compare
local tmpAnalysis = inv.set.table[inv.set.comparePkg.priorityName]
if (tmpAnalysis == nil) then
dbot.warn("inv.set.compareCR: Failed to find analysis table for priority \"" ..
inv.set.comparePkg.priorityName .. "\"")
inv.set.comparePkg = nil
return inv.tags.stop(invTagsCompare, endTag, DRL_RET_MISSING_ENTRY)
end -- if
-- Disable refresh during the comparison
if (inv.state == invStateIdle) then
inv.state = invStatePaused
didDisableRefresh = true
elseif (inv.state == invStateRunning) then
dbot.info("Skipping set comparison: you are in the middle of an inventory refresh")
inv.set.comparePkg = nil
return inv.tags.stop(invTagsCompare, endTag, DRL_RET_BUSY)
end -- if
-- We have a temporary copy of this that we will restore after we are done comparing the new analysis
inv.set.table[inv.set.comparePkg.priorityName] = nil
-- Get the item's level so that we can skip analyzing levels below what the item can be used.
-- If we don't know the item's level, start analyzing at the user's lowest possible level.
local itemName = inv.items.getField(objId, invFieldColorName) or "Unknown item name"
local itemLevel = tonumber(inv.items.getStatField(objId, invStatFieldLevel) or "")
if (itemLevel == nil) or (itemLevel < startLevel) then
itemLevel = startLevel
end -- if
-- Remove the item
retval = inv.items.remove(objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.compareCR: Failed to remove objId " .. (objId or "nil") .. ": " ..
dbot.retval.getString(retval))
else
dbot.print("@WAnalyzing optimal \"@C" .. inv.set.comparePkg.priorityName ..
"@W\" equipment sets with and without \"" .. itemName .. "\"\n")
-- Analyze the priority with the item removed so that we can compare the results with what
-- we had when the item was included
local resultData = dbot.callback.new()
retval = inv.analyze.sets(inv.set.comparePkg.priorityName, itemLevel,
resultData, inv.set.analyzeIntensity)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.compareCR: Failed to analyze sets: " .. dbot.retval.getString(retval))
else
-- Wait until the analysis is complete
retval = dbot.callback.wait(resultData, inv.analyze.timeoutThreshold)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.compareCR: Analysis of comparison set failed: " .. dbot.retval.getString(retval))
end -- if
end -- if
-- Add the item back into our inventory
retval = inv.items.add(objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.compareCR: Failed to add objId " .. (objId or "nil") .. ": " ..
dbot.retval.getString(retval))
end -- if
dbot.print("\n@WPriority \"@C" .. inv.set.comparePkg.priorityName .. "@W\" advantages with \"" ..
itemName .. DRL_ANSI_WHITE .. "@W\":\n")
-- Display the difference between when the item was present and when it was removed
local doDisplayHeader = true
local minCharLevel = 1 + (10 * dbot.gmcp.getTier())
local maxCharLevel = 200 + minCharLevel
for level = itemLevel, maxCharLevel do
local s1 = inv.set.table[inv.set.comparePkg.priorityName][level]
local s2 = tmpAnalysis[level]
if (s1 ~= nil) and (s2 ~= nil) then
didFindStat = inv.set.displayDiff(s1, s2, level, string.format("Level %3d: ", level), doDisplayHeader)
if (didFindStat) then
doDisplayHeader = false
end -- if
end -- if
end -- for
if (doDisplayHeader) then
dbot.print("No set with item \"" .. itemName .. DRL_ANSI_WHITE ..
"\" is optimal for any level between " .. minCharLevel .. " and " .. maxCharLevel)
end -- if
end -- if
-- Restore the original set analysis that includes the target item
inv.set.table[inv.set.comparePkg.priorityName] = tmpAnalysis
-- We shouldn't have more than one item match the relative name query string. This check is just
-- paranoia...
else
dbot.error("inv.set.compareCR: More than one item matched query string \"" ..
inv.set.comparePkg.queryString .. "\"")
retval = DRL_RET_INTERNAL_ERROR
end -- if
-- Re-enable refreshes if we disabled them during the comparison
if (didDisableRefresh) then
inv.state = invStateIdle
end -- if
-- We may have updated and saved the interim state during the comparison. Ensure that we have
-- the latest state saved now that the comparison is done and everything is back to where we started.
inv.items.save()
inv.set.save()
inv.set.comparePkg = nil
return inv.tags.stop(invTagsCompare, endTag, retval)
end -- inv.set.compareCR
inv.set.covetPkg = nil
function inv.set.covet(priorityName, auctionNum, endTag)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.set.covet: Missing priorityName parameter")
return inv.tags.stop(invTagsCovet, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (auctionNum == nil) or (type(auctionNum) ~= "number") then
dbot.warn("inv.set.covet: Auction # parameter is not a number!")
return inv.tags.stop(invTagsCovet, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.set.table[priorityName] == nil) then
dbot.warn("inv.set.covet: priority \"" .. priorityName .. "\" does not have analysis results. " ..
"You may need to run \"" .. pluginNameCmd .. " analyze create " .. priorityName .. "\".")
return inv.tags.stop(invTagsCovet, endTag, DRL_RET_MISSING_ENTRY)
end -- if
if (inv.set.covetPkg ~= nil) then
dbot.info("Skipping evaluation of auction #" .. auctionNum .. ": another evaluation is in progress")
return inv.tags.stop(invTagsCovet, endTag, DRL_RET_BUSY)
end -- if
inv.set.covetPkg = {}
inv.set.covetPkg.priorityName = priorityName
inv.set.covetPkg.auctionNum = auctionNum
inv.set.covetPkg.endTag = endTag
wait.make(inv.set.covetCR)
return DRL_RET_SUCCESS
end -- inv.set.covet
function inv.set.covetCR()
local startLevel = 1 + 10 * dbot.gmcp.getTier()
local endTag = inv.set.covetPkg.endTag
-- This is either an incredibly evil hack or a clever solution to reuse code -- I haven't decided yet.
-- Everything in the inventory table is predicated on having an object ID for each item. We don't
-- know the item's ID yet when we are pulling info from an auction. So...if we pretend the auction #
-- is the objId we can move forward with the identification and analysis. Real object IDs should be
-- way outside of the range of numbers used for auctions so we are probably not in danger of a conflict
-- with an actual item. Probably.
local objId = inv.set.covetPkg.auctionNum
-- If there are residual sets left over from previous analyses that are too low a level to use, we
-- whack those sets so that they don't confuse the analysis
for level = 1, (startLevel - 1) do
local priorityTable = inv.set.table[inv.set.covetPkg.priorityName]
if (priorityTable ~= nil) and (priorityTable[level] ~= nil) then
priorityTable[level] = nil
end -- if
end -- for
-- Save the previous set analysis that includes the target item so that we have something to compare
local tmpAnalysis = inv.set.table[inv.set.covetPkg.priorityName]
if (tmpAnalysis == nil) then
dbot.warn("inv.set.covetCR: Failed to find analysis table for priority \"" ..
inv.set.covetPkg.priorityName .. "\"")
inv.set.covetPkg = nil
return inv.tags.stop(invTagsCovet, endTag, DRL_RET_MISSING_ENTRY)
end -- if
-- We don't want an inventory refresh triggering in the middle of this auction item evaluation.
-- Disable refresh during the comparison
local origRefreshState = inv.state
if (inv.state == invStateIdle) then
inv.state = invStatePaused
elseif (inv.state == invStateRunning) then
dbot.info("Skipping auction evaluation: you are in the middle of an inventory refresh")
inv.set.covetPkg = nil
return inv.tags.stop(invTagsCovet, endTag, DRL_RET_BUSY)
end -- if
-- Temporarily create an item placeholder with a fake object ID. We will fill in this placeholder
-- with information from an auction later.
local retval = inv.items.add(objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.covetCR: Failed to add auction fake objId " .. (objId or "nil") .. ": " ..
dbot.retval.getString(retval))
inv.set.covetPkg = nil
inv.state = origRefreshState
return inv.tags.stop(invTagsCovet, endTag, retval)
end -- if
-- Fake a location for the auction item
inv.items.setField(objId, invFieldObjLoc, invItemLocAuction)
-- Treat any auction # less than a threshold as a short-term market bid and anything over the threshold
-- as a long-term market bid.
local auctionShortLongThreshold = 1000 -- I think anything below 1000 is guaranteed to be short-term
local auctionCmd
if (inv.set.covetPkg.auctionNum < auctionShortLongThreshold) then
auctionCmd = "bid "
else
auctionCmd = "lbid "
end -- if
-- Attempt to identify the auction item and wait until we have confirmation that the ID completed
local resultData = dbot.callback.new()
retval = inv.items.identifyItem(objId, auctionCmd, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, inv.items.timer.idTimeoutThresholdSec)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.covetCR: Identification timed out for auction # " .. inv.set.covetPkg.auctionNum)
end -- if
end -- if
-- Get the item's level so that we can skip analyzing levels below what the item can be used.
-- If we don't know the item's level, start analyzing at the user's lowest possible level.
local itemLevel = tonumber(inv.items.getStatField(objId, invStatFieldLevel) or "") or startLevel
local itemName = inv.items.getStatField(objId, invStatFieldName)
-- If the identification failed, give the user as much info as possible on the failure
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("inv.set.covetCR: Failed to identify auction item #" .. inv.set.covetPkg.auctionNum ..
": " .. dbot.retval.getString(retval))
-- If the identification worked, we'll know the item's name. Skip the comparison if the
-- identification did not fully succeed. This is probably redundant with the above clause where
-- we check if (retval ~= DRL_RET_SUCCESS) but maybe there are weird corner cases and it doesn't
-- hurt to be extra paranoid.
elseif (itemName == nil) then
retval = DRL_RET_MISSING_ENTRY
dbot.note("inv.set.covetCR: Failed to identify auction item #" .. inv.set.covetPkg.auctionNum)
-- Compare the inventory with and without the auction item
elseif (retval == DRL_RET_SUCCESS) then
-- We have a temporary copy of this that we will restore after we are done comparing the new analysis
inv.set.table[inv.set.covetPkg.priorityName] = nil
dbot.print("@WAnalyzing optimal \"@C" .. inv.set.covetPkg.priorityName ..
"@W\" equipment sets with and without @Gauction " .. inv.set.covetPkg.auctionNum .. "@w\n")
-- Analyze the priority with the item added so that we can compare the results with what
-- we had when the item was not included
local resultData = dbot.callback.new()
retval = inv.analyze.sets(inv.set.covetPkg.priorityName, itemLevel, resultData, inv.set.analyzeIntensity)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.covetCR: Failed to analyze sets: " .. dbot.retval.getString(retval))
else
-- Wait until the analysis is complete
retval = dbot.callback.wait(resultData, inv.analyze.timeoutThreshold, 1)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.covetCR: Analysis of set failed: " .. dbot.retval.getString(retval))
end -- if
end -- if
inv.items.setField(objId, invFieldColorName, "Auction #" .. inv.set.covetPkg.auctionNum)
inv.items.displayLastType = ""
inv.items.displayItem(objId, invDisplayVerbosityBasic)
dbot.print("\n@WPriority \"@C" .. inv.set.covetPkg.priorityName .. "@W\" advantages with " ..
"@Gauction #" .. inv.set.covetPkg.auctionNum .. "@w:\n")
-- Display the difference between when the item was present and when it was removed
local doDisplayHeader = true
local minCharLevel = 1 + (10 * dbot.gmcp.getTier())
local maxCharLevel = 200 + minCharLevel
for level = itemLevel, maxCharLevel do
local s1 = tmpAnalysis[level]
local s2 = inv.set.table[inv.set.covetPkg.priorityName][level]
if (s1 ~= nil) and (s2 ~= nil) then
didFindStat = inv.set.displayDiff(s1, s2, level, string.format("Level %3d: ", level), doDisplayHeader)
if (didFindStat) then
doDisplayHeader = false
end -- if
end -- if
end -- for
if (doDisplayHeader) then
dbot.print("@WNo set with item \"" .. itemName .. DRL_ANSI_WHITE ..
"\" is optimal for any level between " .. minCharLevel .. " and " .. maxCharLevel)
end -- if
end -- if
-- Remove the item from the inventory and the recent cache
local removeRetval = inv.items.remove(objId)
if (removeRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.covetCR: Failed to remove auction fake objId " .. (objId or "nil") .. ": " ..
dbot.retval.getString(removeRetval))
if (retval == DRL_RET_SUCCESS) then
retval = removeRetval
end -- if
end -- if
removeRetval = inv.cache.remove(inv.cache.recent.table, objId)
if (removeRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.set.covetCR: Failed to remove auction fake objId " .. (objId or "nil") .. ": " ..
" from the recent cache: " .. dbot.retval.getString(removeRetval))
if (retval == DRL_RET_SUCCESS) then
retval = removeRetval
end -- if
end -- if
-- Restore the original set analysis that predates adding the temporary auction item
inv.set.table[inv.set.covetPkg.priorityName] = tmpAnalysis
-- We may have saved the interim state during the comparison. Ensure that we have the latest
-- state saved now that the comparison is done and everything is back to where we started.
inv.items.save()
inv.set.save()
-- Re-enable refreshes if we disabled them during the comparison
inv.state = origRefreshState
-- Clean up and return
inv.set.covetPkg = nil
return inv.tags.stop(invTagsCovet, endTag, retval)
end -- inv.set.covetCR
----------------------------------------------------------------------------------------------------
--
-- Module to manage snapshots of equipment sets
--
-- The inv.set module handles creating and wearing equipment sets automatically generated from a
-- priority weighting of stats. That is probably what most people will use to manage equipment
-- sets. However, it may be convenient to take a snapshot of what you are wearing at a particular
-- moment and easily go back to re-wear the exact same items at a later date. That's where this
-- snapshot module comes in.
--
-- You can add and remove snapshots (big surprise) with the cleverly named inv.snapshot.add and
-- inv.snapshot.remove functions. You can also re-wear the items from an existing snapshot by
-- calling inv.snapshot.wear. The inv.snapshot.list function prints a listing of all existing
-- saved snapshots. The inv.snapshot.display function prints details about a specific snapshot.
-- Easy peasy.
--
-- inv.snapshot.init.atActive()
-- inv.snapshot.fini(doSaveState)
--
-- inv.snapshot.save()
-- inv.snapshot.load()
-- inv.snapshot.reset()
--
-- inv.snapshot.add(snapshotName, endTag)
-- inv.snapshot.remove(snapshotName, endTag)
--
-- inv.snapshot.list(endTag)
-- inv.snapshot.display(snapshotName, endTag)
-- inv.snapshot.wear(snapshotName, endTag)
--
----------------------------------------------------------------------------------------------------
inv.snapshot = {}
inv.snapshot.init = {}
inv.snapshot.table = {}
inv.snapshot.stateName = "inv-snapshot.state"
function inv.snapshot.init.atActive()
local retval = DRL_RET_SUCCESS
retval = inv.snapshot.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.init.atActive: failed to load snapshot data from storage: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.snapshot.init.atActive
function inv.snapshot.fini(doSaveState)
local retval = DRL_RET_SUCCESS
if (doSaveState) then
-- Save our current data
retval = inv.snapshot.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.fini: Failed to save inv.snapshot module data: " ..
dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.snapshot.fini
function inv.snapshot.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.snapshot.stateName,
"inv.snapshot.table", inv.snapshot.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.save: Failed to save snapshot table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.snapshot.save
function inv.snapshot.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.snapshot.stateName, inv.snapshot.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.snapshot.stateName .. "@W\": " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.snapshot.load()
function inv.snapshot.reset()
inv.snapshot.table = {}
return inv.snapshot.save()
end -- inv.snapshot.reset
function inv.snapshot.add(snapshotName, endTag)
local retval = DRL_RET_SUCCESS
local numItemsInSnap = 0
local snap = {}
if (snapshotName == nil) or (snapshotName == "") then
dbot.warn("inv.snapshot.add: Missing snapshot name")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_INVALID_PARAM)
end -- if
for objId, _ in pairs(inv.items.table) do
if inv.items.isWorn(objId) then
local objLoc = inv.items.getField(objId, invFieldObjLoc) or ""
if (objLoc ~= "") then
snap[objLoc] = { id = objId, score = 0 } -- snapshots don't have scores but the set format needs them
numItemsInSnap = numItemsInSnap + 1
end -- if
end -- if
end -- for
local suffix = ""
if (numItemsInSnap ~= 1) then
suffix = "s"
end -- if
if (numItemsInSnap > 0) then
inv.snapshot.table[snapshotName] = snap
dbot.info("Created \"@C" .. snapshotName .. "@W\" snapshot with " .. numItemsInSnap .. " item" .. suffix)
retval = inv.snapshot.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.remove: Failed to save snapshot table: " .. dbot.retval.getString(retval))
end -- if
else
dbot.info("No items were added to snapshot \"@C" .. snapshotName .. "@W\"")
retval = DRL_RET_MISSING_ENTRY
end -- if
return inv.tags.stop(invTagsSnapshot, endTag, retval)
end -- inv.snapshot.add
function inv.snapshot.remove(snapshotName, endTag)
local retval = DRL_RET_SUCCESS
if (snapshotName == nil) or (snapshotName == "") then
dbot.warn("inv.snapshot.remove: Missing snapshot name")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.snapshot.table[snapshotName] == nil) then
dbot.warn("inv.snapshot.remove: Failed to remove snapshot \"@C" .. snapshotName ..
"@W\": it does not exist")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_MISSING_ENTRY)
end -- if
-- Remove the snapshot
inv.snapshot.table[snapshotName] = nil
retval = inv.snapshot.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.remove: Failed to save snapshot table: " .. dbot.retval.getString(retval))
end -- if
dbot.info("Removed snapshot \"@C" .. snapshotName .. "@W\"")
return inv.tags.stop(invTagsSnapshot, endTag, retval)
end -- inv.snapshot.remove
-- print names of all snapshots
function inv.snapshot.list(endTag)
local retval = DRL_RET_SUCCESS
local numSnapshots = 0
for snapName, snapSet in pairs(inv.snapshot.table) do
if (numSnapshots == 0) then
dbot.print("@WSaved Snapshots:")
end -- if
dbot.print(" @C" .. snapName)
numSnapshots = numSnapshots + 1
end -- for
local suffix = ""
if (numSnapshots ~= 1) then
suffix = "s"
end -- if
dbot.info("Found " .. numSnapshots .. " saved snapshot" .. suffix)
return inv.tags.stop(invTagsSnapshot, endTag, retval)
end -- inv.snapshot.list
function inv.snapshot.display(snapshotName, endTag)
retval = DRL_RET_SUCCESS
if (snapshotName == nil) or (snapshotName == "") then
dbot.warn("inv.snapshot.display: Missing snapshot name")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.snapshot.table[snapshotName] == nil) then
dbot.warn("inv.snapshot.display: Failed to display snapshot \"@C" .. snapshotName ..
"@W\": it does not exist")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_MISSING_ENTRY)
end -- if
retval = inv.set.displaySet(snapshotName, nil, inv.snapshot.table[snapshotName])
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.display: Failed to display snapshot \"@C" .. snapshotName ..
"@W\": " .. dbot.retval.getString(retval))
end -- if
return inv.tags.stop(invTagsSnapshot, endTag, retval)
end -- inv.snapshot.display
inv.snapshot.wearPkg = nil
function inv.snapshot.wear(snapshotName, endTag)
retval = DRL_RET_SUCCESS
if (snapshotName == nil) or (snapshotName == "") then
dbot.warn("inv.snapshot.wear: Missing snapshot name")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.snapshot.table[snapshotName] == nil) then
dbot.warn("inv.snapshot.wear: Failed to wear snapshot \"@C" .. snapshotName ..
"@W\": it does not exist")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_MISSING_ENTRY)
end -- if
if (inv.snapshot.wearPkg ~= nil) then
dbot.info("Skipping request to wear snapshot \"@C" .. snapshotName .. "@W\": " ..
dbot.retval.getString(retval))
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_BUSY)
end -- if
inv.snapshot.wearPkg = {}
inv.snapshot.wearPkg.snapshotName = snapshotName
inv.snapshot.wearPkg.endTag = endTag
wait.make(inv.snapshot.wearCR)
return retval
end -- inv.snapshot.wear
function inv.snapshot.wearCR()
local retval = DRL_RET_SUCCESS
if (inv.snapshot.wearPkg == nil) then
dbot.error("inv.snapshot.wearCR: wear package is nil!")
return inv.tags.stop(invTagsSnapshot, "", DRL_RET_INTERNAL_ERROR)
end -- if
local endTag = inv.snapshot.wearPkg.endTag or ""
local snapshotName = inv.snapshot.wearPkg.snapshotName
if (inv.snapshot.table[snapshotName] == nil) then
dbot.warn("inv.snapshot.wearCR: Failed to wear snapshot \"@C" .. snapshotName ..
"@W\": it does not exist")
return inv.tags.stop(invTagsSnapshot, endTag, DRL_RET_MISSING_ENTRY)
end -- if
retval = inv.set.wear(inv.snapshot.table[snapshotName])
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.snapshot.wearCR: Failed to wear snapshot \"@C" .. snapshotName ..
"@W\": " .. dbot.retval.getString(retval))
end -- if
-- Clean up and return
inv.snapshot.wearPkg = nil
return inv.tags.stop(invTagsSnapshot, endTag, retval)
end -- inv.snapshot.wearCR
----------------------------------------------------------------------------------------------------
--
-- Module to calculate how many bonuses are available to each stat due to equipment
--
-- There are limits to how many stat bonuses are applied due to equipment. The limits vary based
-- on a character's level and the spell bonuses that are active for the character. This module
-- checks the current stats and determines how many stats can be provided by equipment.
--
-- This is not an exact science. The equipment bonuses can be very different for the same
-- character at the same level if the character has a good spellup on one call and a poor spellup
-- on another call. Our policy is to assume that the character has done their best to spellup
-- prior to wearing a set. They can always re-wear a set to pick up the optimal available equipment
-- if a good (e.g., SH) spellup wears off.
--
-- This module remembers the current, max, and average values for each stat at each level. This
-- helps make bonus estimates more accurate when we are creating a set for a different level than
-- the character currently has.
--
-- inv.statBonus.init.atInstall()
-- inv.statBonus.init.atActive()
-- inv.statBonus.fini(doSaveState)
--
-- inv.statBonus.save()
-- inv.statBonus.load()
-- inv.statBonus.reset()
--
-- inv.statBonus.estimate(level)
-- inv.statBonus.get(level, bonusType) -- types include current, ave, and max
-- inv.statBonus.set
-- inv.statBonus.setCR
-- inv.statBonus.setSetupFn()
--
-- inv.statBonus.timer.update
--
-- inv.statBonus.trigger.get
-- inv.statBonus.trigger.start
--
----------------------------------------------------------------------------------------------------
inv.statBonus = {}
inv.statBonus.init = {}
inv.statBonus.closingMsg = "{ \\dinv inv.statBonus }"
inv.statBonus.currentBonus = { int = 0, luck = 0, wis = 0, str = 0, dex = 0, con = 0 }
inv.statBonus.stateNameSpells = "inv-stats-spells.state"
inv.statBonus.stateNameEquip = "inv-stats-equip.state"
function inv.statBonus.init.atInstall()
local retval = DRL_RET_SUCCESS
-- Trigger on a call to "stats" to determine how many stat bonuses currently are available to equipment
check (AddTriggerEx(inv.statBonus.trigger.getName,
"^(.*)$",
"inv.statBonus.trigger.get(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11,
0, "", "", sendto.script, 0))
check (EnableTrigger(inv.statBonus.trigger.getName, false)) -- default to off
return retval
end -- inv.statBonus.init.atInstall
function inv.statBonus.init.atActive()
local retval = DRL_RET_SUCCESS
retval = inv.statBonus.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.init.atActive: failed to load statBonus data from storage: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.statBonus.init.atActive
function inv.statBonus.fini(doSaveState)
local retval = DRL_RET_SUCCESS
dbot.deleteTrigger(inv.statBonus.trigger.getName)
dbot.deleteTimer(inv.statBonus.timer.name)
if (doSaveState) then
-- Save our current data
retval = inv.statBonus.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.fini: Failed to save inv.statBonus module data: " ..
dbot.retval.getString(retval))
end -- if
end -- if
inv.statBonus.currentBonus = { int = 0, luck = 0, wis = 0, str = 0, dex = 0, con = 0 }
return retval
end -- inv.statBonus.fini
function inv.statBonus.save()
local spellRetval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.statBonus.stateNameSpells,
"inv.statBonus.spellBonus", inv.statBonus.spellBonus)
if (spellRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.save: Failed to save spellBonus table: " .. dbot.retval.getString(spellRetval))
end -- if
local equipRetval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.statBonus.stateNameEquip,
"inv.statBonus.equipBonus", inv.statBonus.equipBonus)
if (equipRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.save: Failed to save equipBonus table: " .. dbot.retval.getString(equipRetval))
end -- if
if (spellRetval ~= DRL_RET_SUCCESS) then
return spellRetval
else
return equipRetval
end -- if
end -- inv.statBonus.save
function inv.statBonus.load()
local spellRetval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.statBonus.stateNameSpells,
inv.statBonus.reset)
if (spellRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.load: Failed to load spellBonus table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.statBonus.stateNameSpells .. "@W\": " ..
dbot.retval.getString(spellRetval))
end -- if
local equipRetval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.statBonus.stateNameEquip,
inv.statBonus.reset)
if (equipRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.load: Failed to load equipBonus table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.statBonus.stateNameEquip .. "@W\": " ..
dbot.retval.getString(equipRetval))
end -- if
-- Kick off a timer to continually update the bonuses. Ideally, we would just call the
-- inv.statBonus.timer.update() function here. Unfortunately, that function relies on mushclient
-- components that may not be available when we load right at the start. Instead, we simply
-- kick off the timer manually and then let it self-perpetuate once it is going.
check (AddTimer(inv.statBonus.timer.name, 0, inv.statBonus.timer.min, inv.statBonus.timer.sec, "",
timer_flag.Enabled + timer_flag.Replace + timer_flag.OneShot,
"inv.statBonus.set"))
if (spellRetval ~= DRL_RET_SUCCESS) then
return spellRetval
else
return equipRetval
end -- if
end -- inv.statBonus.load()
function inv.statBonus.reset()
inv.statBonus.spellBonus = {}
inv.statBonus.equipBonus = {}
local retval = inv.statBonus.save()
return retval
end -- inv.statBonus.reset
-- This is a moderately crude hack. We would like a way to know what a character's spellup bonus
-- will be at any specific level. The problem is that this varies dramatically from level to level.
-- Even worse, the spellup bonuses vary even more dramatically based on what moons are present and
-- if someone (e.g., a superhero!) gives a spellup. So...what can we do? This is a rough estimate
-- for what an "average" spellup should be for an "average" mort character based on averages I saw
-- during various morts. Of course, this could be very different for someone else depending on what
-- classes are available, what spellup potions are used, and if someone else (a groupmate or clannee)
-- gives a spellup to the character.
--
-- The good news is that this table is just the starting point for a character. The stat bonus
-- timer will periodically check what spell bonuses are present on the character and track a weighted
-- average and also the max value for each stat over time. Those values will help improve accuracy
-- the longer someone uses this plugin. If someone frequently has SH spellups their estimates will
-- eventually reflect that. Similarly, if someone never bothers to spellup, that character's
-- estimated stats will reflect that too after enough time passes.
--
-- Also, you should note that the spellup estimates are only used when real spellup values aren't
-- available. If you are creating/wearing a set at your current level, we only use the actual/real
-- values. However, if you want to estimate what equipment you would wear at a different level then
-- these estimates can be convenient until we have enough data to know what a particular character's
-- "typical" spellups are.
inv.statBonus.estimateTable = {}
inv.statBonus.estimateTable[ 1] = { str = 1, int = 0, wis = 0, dex = 1, con = 0, luck = 0 }
inv.statBonus.estimateTable[ 2] = { str = 1, int = 0, wis = 0, dex = 1, con = 0, luck = 0 }
inv.statBonus.estimateTable[ 3] = { str = 1, int = 0, wis = 0, dex = 1, con = 0, luck = 0 }
inv.statBonus.estimateTable[ 4] = { str = 1, int = 0, wis = 0, dex = 2, con = 0, luck = 0 }
inv.statBonus.estimateTable[ 5] = { str = 1, int = 0, wis = 0, dex = 2, con = 2, luck = 0 }
inv.statBonus.estimateTable[ 6] = { str = 2, int = 0, wis = 0, dex = 2, con = 2, luck = 0 }
inv.statBonus.estimateTable[ 7] = { str = 2, int = 0, wis = 0, dex = 3, con = 3, luck = 0 }
inv.statBonus.estimateTable[ 8] = { str = 2, int = 0, wis = 0, dex = 3, con = 3, luck = 0 }
inv.statBonus.estimateTable[ 9] = { str = 2, int = 0, wis = 0, dex = 4, con = 4, luck = 0 }
inv.statBonus.estimateTable[ 10] = { str = 3, int = 0, wis = 0, dex = 4, con = 4, luck = 0 }
inv.statBonus.estimateTable[ 11] = { str = 3, int = 0, wis = 0, dex = 4, con = 4, luck = 0 }
inv.statBonus.estimateTable[ 12] = { str = 3, int = 0, wis = 0, dex = 5, con = 5, luck = 0 }
inv.statBonus.estimateTable[ 13] = { str = 3, int = 0, wis = 0, dex = 5, con = 5, luck = 0 }
inv.statBonus.estimateTable[ 14] = { str = 3, int = 0, wis = 0, dex = 5, con = 5, luck = 0 }
inv.statBonus.estimateTable[ 15] = { str = 7, int = 4, wis = 4, dex = 7, con = 7, luck = 4 }
inv.statBonus.estimateTable[ 16] = { str = 7, int = 4, wis = 4, dex = 7, con = 7, luck = 4 }
inv.statBonus.estimateTable[ 17] = { str = 8, int = 4, wis = 4, dex = 8, con = 8, luck = 4 }
inv.statBonus.estimateTable[ 18] = { str = 8, int = 4, wis = 4, dex = 8, con = 8, luck = 4 }
inv.statBonus.estimateTable[ 19] = { str = 9, int = 4, wis = 4, dex = 9, con = 9, luck = 4 }
inv.statBonus.estimateTable[ 20] = { str = 9, int = 4, wis = 5, dex = 9, con = 9, luck = 4 }
inv.statBonus.estimateTable[ 21] = { str = 10, int = 5, wis = 5, dex = 10, con = 10, luck = 4 }
inv.statBonus.estimateTable[ 22] = { str = 10, int = 5, wis = 5, dex = 10, con = 10, luck = 4 }
inv.statBonus.estimateTable[ 23] = { str = 11, int = 5, wis = 5, dex = 11, con = 11, luck = 4 }
inv.statBonus.estimateTable[ 24] = { str = 11, int = 5, wis = 5, dex = 11, con = 11, luck = 5 }
inv.statBonus.estimateTable[ 25] = { str = 12, int = 5, wis = 6, dex = 12, con = 12, luck = 5 }
inv.statBonus.estimateTable[ 26] = { str = 12, int = 6, wis = 6, dex = 12, con = 12, luck = 5 }
inv.statBonus.estimateTable[ 27] = { str = 13, int = 6, wis = 6, dex = 13, con = 13, luck = 5 }
inv.statBonus.estimateTable[ 28] = { str = 14, int = 6, wis = 6, dex = 13, con = 13, luck = 5 }
inv.statBonus.estimateTable[ 29] = { str = 14, int = 6, wis = 7, dex = 14, con = 14, luck = 5 }
inv.statBonus.estimateTable[ 30] = { str = 14, int = 7, wis = 7, dex = 14, con = 14, luck = 5 }
inv.statBonus.estimateTable[ 31] = { str = 15, int = 7, wis = 7, dex = 15, con = 15, luck = 5 }
inv.statBonus.estimateTable[ 32] = { str = 15, int = 7, wis = 7, dex = 15, con = 15, luck = 6 }
inv.statBonus.estimateTable[ 33] = { str = 15, int = 7, wis = 8, dex = 16, con = 16, luck = 6 }
inv.statBonus.estimateTable[ 34] = { str = 16, int = 8, wis = 8, dex = 16, con = 16, luck = 6 }
inv.statBonus.estimateTable[ 35] = { str = 16, int = 8, wis = 8, dex = 17, con = 17, luck = 6 }
inv.statBonus.estimateTable[ 36] = { str = 16, int = 8, wis = 9, dex = 17, con = 17, luck = 6 }
inv.statBonus.estimateTable[ 37] = { str = 17, int = 8, wis = 9, dex = 18, con = 18, luck = 6 }
inv.statBonus.estimateTable[ 38] = { str = 17, int = 9, wis = 9, dex = 18, con = 18, luck = 6 }
inv.statBonus.estimateTable[ 39] = { str = 17, int = 9, wis = 10, dex = 19, con = 19, luck = 6 }
inv.statBonus.estimateTable[ 40] = { str = 18, int = 9, wis = 10, dex = 19, con = 19, luck = 7 }
inv.statBonus.estimateTable[ 41] = { str = 18, int = 9, wis = 11, dex = 19, con = 19, luck = 7 }
inv.statBonus.estimateTable[ 42] = { str = 18, int = 10, wis = 11, dex = 20, con = 20, luck = 7 }
inv.statBonus.estimateTable[ 43] = { str = 19, int = 10, wis = 11, dex = 20, con = 20, luck = 7 }
inv.statBonus.estimateTable[ 44] = { str = 19, int = 10, wis = 12, dex = 21, con = 21, luck = 7 }
inv.statBonus.estimateTable[ 45] = { str = 19, int = 10, wis = 12, dex = 21, con = 21, luck = 8 }
inv.statBonus.estimateTable[ 46] = { str = 20, int = 11, wis = 12, dex = 22, con = 21, luck = 8 }
inv.statBonus.estimateTable[ 47] = { str = 20, int = 11, wis = 13, dex = 24, con = 23, luck = 10 }
inv.statBonus.estimateTable[ 48] = { str = 20, int = 11, wis = 13, dex = 25, con = 24, luck = 11 }
inv.statBonus.estimateTable[ 49] = { str = 20, int = 11, wis = 13, dex = 25, con = 24, luck = 11 }
inv.statBonus.estimateTable[ 50] = { str = 21, int = 12, wis = 13, dex = 25, con = 25, luck = 12 }
inv.statBonus.estimateTable[ 51] = { str = 21, int = 12, wis = 13, dex = 26, con = 25, luck = 12 }
inv.statBonus.estimateTable[ 52] = { str = 21, int = 13, wis = 14, dex = 26, con = 25, luck = 12 }
inv.statBonus.estimateTable[ 53] = { str = 22, int = 14, wis = 15, dex = 26, con = 25, luck = 12 }
inv.statBonus.estimateTable[ 54] = { str = 22, int = 14, wis = 15, dex = 26, con = 25, luck = 12 }
inv.statBonus.estimateTable[ 55] = { str = 22, int = 15, wis = 16, dex = 26, con = 26, luck = 12 }
inv.statBonus.estimateTable[ 56] = { str = 23, int = 15, wis = 17, dex = 26, con = 26, luck = 12 }
inv.statBonus.estimateTable[ 57] = { str = 23, int = 16, wis = 18, dex = 26, con = 26, luck = 12 }
inv.statBonus.estimateTable[ 58] = { str = 24, int = 18, wis = 18, dex = 26, con = 27, luck = 13 }
inv.statBonus.estimateTable[ 59] = { str = 24, int = 20, wis = 19, dex = 27, con = 27, luck = 13 }
inv.statBonus.estimateTable[ 60] = { str = 24, int = 20, wis = 19, dex = 27, con = 27, luck = 14 }
inv.statBonus.estimateTable[ 61] = { str = 24, int = 20, wis = 19, dex = 27, con = 27, luck = 15 }
inv.statBonus.estimateTable[ 62] = { str = 24, int = 20, wis = 19, dex = 28, con = 27, luck = 15 }
inv.statBonus.estimateTable[ 63] = { str = 24, int = 20, wis = 19, dex = 28, con = 27, luck = 16 }
inv.statBonus.estimateTable[ 64] = { str = 24, int = 20, wis = 19, dex = 28, con = 28, luck = 17 }
inv.statBonus.estimateTable[ 65] = { str = 24, int = 20, wis = 19, dex = 29, con = 28, luck = 18 }
inv.statBonus.estimateTable[ 66] = { str = 24, int = 20, wis = 19, dex = 29, con = 28, luck = 19 }
inv.statBonus.estimateTable[ 67] = { str = 24, int = 20, wis = 19, dex = 29, con = 28, luck = 20 }
inv.statBonus.estimateTable[ 68] = { str = 24, int = 20, wis = 19, dex = 30, con = 29, luck = 21 }
inv.statBonus.estimateTable[ 69] = { str = 24, int = 20, wis = 19, dex = 30, con = 29, luck = 21 }
inv.statBonus.estimateTable[ 70] = { str = 24, int = 21, wis = 19, dex = 30, con = 29, luck = 21 }
inv.statBonus.estimateTable[ 71] = { str = 24, int = 21, wis = 19, dex = 31, con = 29, luck = 22 }
inv.statBonus.estimateTable[ 72] = { str = 24, int = 21, wis = 19, dex = 31, con = 29, luck = 22 }
inv.statBonus.estimateTable[ 73] = { str = 24, int = 21, wis = 19, dex = 31, con = 30, luck = 23 }
inv.statBonus.estimateTable[ 74] = { str = 24, int = 22, wis = 19, dex = 32, con = 30, luck = 23 }
inv.statBonus.estimateTable[ 75] = { str = 24, int = 22, wis = 20, dex = 32, con = 30, luck = 23 }
inv.statBonus.estimateTable[ 76] = { str = 24, int = 22, wis = 20, dex = 32, con = 30, luck = 24 }
inv.statBonus.estimateTable[ 77] = { str = 24, int = 22, wis = 20, dex = 32, con = 31, luck = 24 }
inv.statBonus.estimateTable[ 78] = { str = 24, int = 23, wis = 20, dex = 33, con = 31, luck = 25 }
inv.statBonus.estimateTable[ 79] = { str = 24, int = 23, wis = 20, dex = 33, con = 31, luck = 25 }
inv.statBonus.estimateTable[ 80] = { str = 25, int = 23, wis = 20, dex = 33, con = 31, luck = 25 }
inv.statBonus.estimateTable[ 81] = { str = 25, int = 24, wis = 20, dex = 34, con = 32, luck = 26 }
inv.statBonus.estimateTable[ 82] = { str = 25, int = 24, wis = 20, dex = 34, con = 32, luck = 26 }
inv.statBonus.estimateTable[ 83] = { str = 25, int = 24, wis = 20, dex = 34, con = 33, luck = 26 }
inv.statBonus.estimateTable[ 84] = { str = 26, int = 24, wis = 20, dex = 35, con = 33, luck = 26 }
inv.statBonus.estimateTable[ 85] = { str = 26, int = 25, wis = 21, dex = 35, con = 33, luck = 26 }
inv.statBonus.estimateTable[ 86] = { str = 26, int = 25, wis = 21, dex = 36, con = 34, luck = 27 }
inv.statBonus.estimateTable[ 87] = { str = 26, int = 25, wis = 21, dex = 36, con = 34, luck = 27 }
inv.statBonus.estimateTable[ 88] = { str = 27, int = 26, wis = 22, dex = 36, con = 34, luck = 27 }
inv.statBonus.estimateTable[ 89] = { str = 27, int = 26, wis = 22, dex = 36, con = 34, luck = 28 }
inv.statBonus.estimateTable[ 90] = { str = 28, int = 27, wis = 22, dex = 36, con = 34, luck = 28 }
inv.statBonus.estimateTable[ 91] = { str = 28, int = 27, wis = 22, dex = 36, con = 34, luck = 28 }
inv.statBonus.estimateTable[ 92] = { str = 29, int = 28, wis = 22, dex = 36, con = 34, luck = 28 }
inv.statBonus.estimateTable[ 93] = { str = 29, int = 28, wis = 22, dex = 36, con = 34, luck = 29 }
inv.statBonus.estimateTable[ 94] = { str = 30, int = 28, wis = 22, dex = 36, con = 34, luck = 29 }
inv.statBonus.estimateTable[ 95] = { str = 30, int = 29, wis = 22, dex = 36, con = 34, luck = 29 }
inv.statBonus.estimateTable[ 96] = { str = 31, int = 29, wis = 22, dex = 36, con = 34, luck = 30 }
inv.statBonus.estimateTable[ 97] = { str = 31, int = 30, wis = 22, dex = 36, con = 34, luck = 30 }
inv.statBonus.estimateTable[ 98] = { str = 32, int = 30, wis = 22, dex = 37, con = 35, luck = 30 }
inv.statBonus.estimateTable[ 99] = { str = 32, int = 31, wis = 22, dex = 37, con = 35, luck = 30 }
inv.statBonus.estimateTable[100] = { str = 32, int = 31, wis = 23, dex = 37, con = 35, luck = 30 }
inv.statBonus.estimateTable[101] = { str = 32, int = 31, wis = 24, dex = 38, con = 36, luck = 30 }
inv.statBonus.estimateTable[102] = { str = 32, int = 32, wis = 25, dex = 38, con = 36, luck = 30 }
inv.statBonus.estimateTable[103] = { str = 32, int = 32, wis = 26, dex = 38, con = 37, luck = 30 }
inv.statBonus.estimateTable[104] = { str = 32, int = 32, wis = 27, dex = 38, con = 37, luck = 30 }
inv.statBonus.estimateTable[105] = { str = 33, int = 33, wis = 28, dex = 39, con = 38, luck = 30 }
inv.statBonus.estimateTable[106] = { str = 33, int = 33, wis = 29, dex = 39, con = 38, luck = 30 }
inv.statBonus.estimateTable[107] = { str = 33, int = 33, wis = 30, dex = 39, con = 39, luck = 30 }
inv.statBonus.estimateTable[108] = { str = 33, int = 33, wis = 31, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[109] = { str = 33, int = 34, wis = 32, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[110] = { str = 33, int = 35, wis = 33, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[111] = { str = 33, int = 35, wis = 33, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[112] = { str = 33, int = 36, wis = 33, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[113] = { str = 33, int = 36, wis = 34, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[114] = { str = 33, int = 37, wis = 34, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[115] = { str = 33, int = 37, wis = 34, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[116] = { str = 33, int = 37, wis = 35, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[117] = { str = 33, int = 38, wis = 35, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[118] = { str = 33, int = 38, wis = 36, dex = 40, con = 39, luck = 30 }
inv.statBonus.estimateTable[119] = { str = 33, int = 39, wis = 36, dex = 40, con = 40, luck = 31 }
inv.statBonus.estimateTable[120] = { str = 34, int = 40, wis = 37, dex = 41, con = 40, luck = 31 }
inv.statBonus.estimateTable[121] = { str = 34, int = 42, wis = 38, dex = 42, con = 40, luck = 31 }
inv.statBonus.estimateTable[122] = { str = 34, int = 44, wis = 39, dex = 43, con = 40, luck = 31 }
inv.statBonus.estimateTable[123] = { str = 34, int = 46, wis = 40, dex = 44, con = 40, luck = 32 }
inv.statBonus.estimateTable[124] = { str = 34, int = 47, wis = 42, dex = 45, con = 40, luck = 32 }
inv.statBonus.estimateTable[125] = { str = 35, int = 48, wis = 43, dex = 46, con = 41, luck = 32 }
inv.statBonus.estimateTable[126] = { str = 35, int = 50, wis = 44, dex = 47, con = 41, luck = 32 }
inv.statBonus.estimateTable[127] = { str = 35, int = 51, wis = 46, dex = 48, con = 41, luck = 32 }
inv.statBonus.estimateTable[128] = { str = 35, int = 52, wis = 48, dex = 49, con = 41, luck = 32 }
inv.statBonus.estimateTable[129] = { str = 35, int = 52, wis = 50, dex = 50, con = 42, luck = 32 }
inv.statBonus.estimateTable[130] = { str = 36, int = 53, wis = 51, dex = 51, con = 42, luck = 32 }
inv.statBonus.estimateTable[131] = { str = 36, int = 53, wis = 51, dex = 52, con = 42, luck = 32 }
inv.statBonus.estimateTable[132] = { str = 36, int = 53, wis = 52, dex = 52, con = 43, luck = 33 }
inv.statBonus.estimateTable[133] = { str = 37, int = 53, wis = 53, dex = 53, con = 43, luck = 33 }
inv.statBonus.estimateTable[134] = { str = 37, int = 54, wis = 54, dex = 53, con = 44, luck = 33 }
inv.statBonus.estimateTable[135] = { str = 38, int = 54, wis = 54, dex = 54, con = 44, luck = 33 }
inv.statBonus.estimateTable[136] = { str = 38, int = 54, wis = 55, dex = 54, con = 45, luck = 33 }
inv.statBonus.estimateTable[137] = { str = 39, int = 54, wis = 55, dex = 55, con = 45, luck = 33 }
inv.statBonus.estimateTable[138] = { str = 39, int = 54, wis = 55, dex = 55, con = 46, luck = 33 }
inv.statBonus.estimateTable[139] = { str = 40, int = 54, wis = 56, dex = 56, con = 46, luck = 33 }
inv.statBonus.estimateTable[140] = { str = 41, int = 55, wis = 56, dex = 58, con = 47, luck = 34 }
inv.statBonus.estimateTable[141] = { str = 42, int = 55, wis = 56, dex = 59, con = 47, luck = 34 }
inv.statBonus.estimateTable[142] = { str = 44, int = 55, wis = 56, dex = 60, con = 47, luck = 35 }
inv.statBonus.estimateTable[143] = { str = 45, int = 55, wis = 56, dex = 61, con = 48, luck = 35 }
inv.statBonus.estimateTable[144] = { str = 47, int = 55, wis = 56, dex = 62, con = 48, luck = 36 }
inv.statBonus.estimateTable[145] = { str = 48, int = 55, wis = 56, dex = 63, con = 49, luck = 36 }
inv.statBonus.estimateTable[146] = { str = 49, int = 55, wis = 56, dex = 64, con = 49, luck = 37 }
inv.statBonus.estimateTable[147] = { str = 50, int = 55, wis = 56, dex = 65, con = 50, luck = 37 }
inv.statBonus.estimateTable[148] = { str = 51, int = 55, wis = 56, dex = 66, con = 50, luck = 38 }
inv.statBonus.estimateTable[149] = { str = 51, int = 55, wis = 56, dex = 67, con = 51, luck = 38 }
inv.statBonus.estimateTable[150] = { str = 51, int = 55, wis = 56, dex = 67, con = 52, luck = 38 }
inv.statBonus.estimateTable[151] = { str = 52, int = 55, wis = 56, dex = 67, con = 54, luck = 38 }
inv.statBonus.estimateTable[152] = { str = 52, int = 55, wis = 56, dex = 67, con = 56, luck = 38 }
inv.statBonus.estimateTable[153] = { str = 52, int = 55, wis = 56, dex = 67, con = 58, luck = 38 }
inv.statBonus.estimateTable[154] = { str = 53, int = 55, wis = 56, dex = 67, con = 60, luck = 38 }
inv.statBonus.estimateTable[155] = { str = 53, int = 55, wis = 56, dex = 67, con = 61, luck = 38 }
inv.statBonus.estimateTable[156] = { str = 53, int = 55, wis = 56, dex = 67, con = 62, luck = 38 }
inv.statBonus.estimateTable[157] = { str = 53, int = 55, wis = 56, dex = 67, con = 64, luck = 39 }
inv.statBonus.estimateTable[158] = { str = 54, int = 55, wis = 56, dex = 67, con = 66, luck = 39 }
inv.statBonus.estimateTable[159] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[160] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[161] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[162] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[163] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[164] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[165] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[166] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[167] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[168] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[169] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[170] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[171] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[172] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[173] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[174] = { str = 54, int = 55, wis = 56, dex = 67, con = 68, luck = 39 }
inv.statBonus.estimateTable[175] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[176] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[177] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[178] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[179] = { str = 54, int = 55, wis = 56, dex = 68, con = 68, luck = 39 }
inv.statBonus.estimateTable[180] = { str = 54, int = 56, wis = 56, dex = 69, con = 68, luck = 40 }
inv.statBonus.estimateTable[181] = { str = 54, int = 56, wis = 56, dex = 69, con = 68, luck = 40 }
inv.statBonus.estimateTable[182] = { str = 54, int = 57, wis = 56, dex = 69, con = 69, luck = 40 }
inv.statBonus.estimateTable[183] = { str = 54, int = 57, wis = 56, dex = 69, con = 69, luck = 40 }
inv.statBonus.estimateTable[184] = { str = 55, int = 57, wis = 56, dex = 70, con = 69, luck = 41 }
inv.statBonus.estimateTable[185] = { str = 55, int = 58, wis = 56, dex = 70, con = 69, luck = 41 }
inv.statBonus.estimateTable[186] = { str = 55, int = 58, wis = 56, dex = 70, con = 69, luck = 41 }
inv.statBonus.estimateTable[187] = { str = 55, int = 59, wis = 56, dex = 70, con = 70, luck = 41 }
inv.statBonus.estimateTable[188] = { str = 56, int = 59, wis = 56, dex = 70, con = 70, luck = 42 }
inv.statBonus.estimateTable[189] = { str = 56, int = 60, wis = 56, dex = 70, con = 70, luck = 42 }
inv.statBonus.estimateTable[190] = { str = 57, int = 62, wis = 57, dex = 71, con = 70, luck = 43 }
inv.statBonus.estimateTable[191] = { str = 57, int = 64, wis = 57, dex = 71, con = 70, luck = 43 }
inv.statBonus.estimateTable[192] = { str = 57, int = 66, wis = 58, dex = 71, con = 70, luck = 44 }
inv.statBonus.estimateTable[193] = { str = 57, int = 68, wis = 59, dex = 72, con = 70, luck = 44 }
inv.statBonus.estimateTable[194] = { str = 57, int = 70, wis = 59, dex = 72, con = 70, luck = 44 }
inv.statBonus.estimateTable[195] = { str = 57, int = 72, wis = 60, dex = 72, con = 70, luck = 45 }
inv.statBonus.estimateTable[196] = { str = 57, int = 74, wis = 60, dex = 72, con = 70, luck = 45 }
inv.statBonus.estimateTable[197] = { str = 57, int = 76, wis = 61, dex = 73, con = 70, luck = 46 }
inv.statBonus.estimateTable[198] = { str = 57, int = 77, wis = 61, dex = 73, con = 70, luck = 46 }
inv.statBonus.estimateTable[199] = { str = 57, int = 78, wis = 62, dex = 73, con = 70, luck = 47 }
inv.statBonus.estimateTable[200] = { str = 57, int = 78, wis = 62, dex = 73, con = 70, luck = 47 }
inv.statBonus.estimateTable[201] = { str = 57, int = 78, wis = 62, dex = 73, con = 70, luck = 47 }
inv.statBonus.estimateTable[202] = { str = 57, int = 78, wis = 63, dex = 73, con = 70, luck = 47 }
inv.statBonus.estimateTable[203] = { str = 57, int = 78, wis = 63, dex = 73, con = 70, luck = 47 }
inv.statBonus.estimateTable[204] = { str = 57, int = 79, wis = 63, dex = 73, con = 70, luck = 48 }
inv.statBonus.estimateTable[205] = { str = 57, int = 79, wis = 64, dex = 73, con = 70, luck = 48 }
inv.statBonus.estimateTable[206] = { str = 57, int = 79, wis = 64, dex = 73, con = 71, luck = 48 }
inv.statBonus.estimateTable[207] = { str = 57, int = 79, wis = 64, dex = 73, con = 71, luck = 48 }
inv.statBonus.estimateTable[208] = { str = 57, int = 79, wis = 65, dex = 73, con = 71, luck = 49 }
inv.statBonus.estimateTable[209] = { str = 57, int = 80, wis = 65, dex = 73, con = 71, luck = 49 }
inv.statBonus.estimateTable[210] = { str = 57, int = 78, wis = 62, dex = 73, con = 70, luck = 47 }
inv.statBonus.estimateTable[211] = { str = 70, int = 86, wis = 70, dex = 105, con = 90, luck = 65 }
function inv.statBonus.estimate(level)
level = tonumber(level or "")
if (level == nil) then
dbot.warn("inv.statBonus.estimate: Missing level parameter")
return nil, DRL_RET_INVALID_PARAM
end -- if
-- Minor hack: spell bonuses don't change much once we SH
if (level > 211) then
level = 211
end -- if
if (inv.statBonus.estimateTable[level] ~= nil) then
return inv.statBonus.estimateTable[level], DRL_RET_SUCCESS
else
dbot.warn("Failed to get stat bonus estimate for level " .. level .. ": estimate does not exist")
return nil, DRL_RET_MISSING_ENTRY
end -- if
end -- inv.statBonus.estimate
-- Returns a table of the form { int = 1, luck = 3, ... } where the values of each stat in the
-- table indicate how many points of that stat are available to equipment before we hit the max
-- stats for the specified level.
-- Note: This function is synchronous
invStatBonusTypeCurrent = "current"
invStatBonusTypeAve = "average"
invStatBonusTypeMax = "max"
function inv.statBonus.get(level, bonusType)
local spellBonus
local equipBonus
-- Be paranoid about input params
level = tonumber(level or "")
if (level == nil) then
dbot.warn("inv.statBonus.get: Invalid level parameter")
return nil, DRL_RET_INVALID_PARAM
end -- if
-- Generate very crude default values if we don't have info for this level in the table yet.
-- This will get more accurate as someone uses this package and actual bonuses are available.
if (inv.statBonus.spellBonus[level] == nil) then
inv.statBonus.spellBonus[level] = {}
inv.statBonus.spellBonus[level].ave, retval = inv.statBonus.estimate(level)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.get: Failed to get estimate for level " .. level .. ": " ..
dbot.retval.getString(retval))
return nil, retval
end -- if
-- Only update the max for values we have seen in real situations; don't use default values
inv.statBonus.spellBonus[level].max = { int = 0, luck = 0, wis = 0, str = 0, dex = 0, con = 0 }
end -- if
if (bonusType == invStatBonusTypeCurrent) then
spellBonus = inv.statBonus.currentBonus
elseif (bonusType == invStatBonusTypeAve) then
spellBonus = inv.statBonus.spellBonus[level].ave
elseif (bonusType == invStatBonusTypeMax) then
spellBonus = inv.statBonus.spellBonus[level].max
end -- if
-- Now that we know the spell bonus, calculate how many bonus stats are available to equipment
local levelBonus = level - (10 * dbot.gmcp.getTier())
if (levelBonus < 25) then
levelBonus = 25
elseif (levelBonus > 200) then
levelBonus = 200
end -- if
inv.statBonus.equipBonus[level] = { int = levelBonus - spellBonus.int,
wis = levelBonus - spellBonus.wis,
luck = levelBonus - spellBonus.luck,
str = levelBonus - spellBonus.str,
dex = levelBonus - spellBonus.dex,
con = levelBonus - spellBonus.con }
return inv.statBonus.equipBonus[level], DRL_RET_SUCCESS
end -- inv.statBonus.get
function inv.statBonus.set()
wait.make(inv.statBonus.setCR)
end -- inv.statBonus.set
-- This function must run in a co-routine because it potentially blocks
function inv.statBonus.setCR()
local retval = DRL_RET_SUCCESS
local level = tonumber(dbot.gmcp.getLevel() or "")
local charState = dbot.gmcp.getState()
inv.statBonus.bonusInProgress = true
-- If we are in the active state (i.e., not AFK, sleeping, running, writing a note, etc.) then
-- we get the current bonuses
if (charState == dbot.stateActive) then
dbot.prompt.hide()
-- Run the stats command so that we can trigger on the stats and save them
local resultData = dbot.callback.new()
local commandArray = {}
table.insert(commandArray, "stats")
table.insert(commandArray, "echo " .. inv.statBonus.closingMsg)
retval = dbot.execute.safe.commands(commandArray, inv.statBonus.setSetupFn, nil,
dbot.callback.default, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.statBonus.setCR: Failed to safely execute \"@Gstats@W\": " ..
dbot.retval.getString(retval))
dbot.deleteTrigger(inv.statBonus.trigger.startName)
else
-- Wait for the callback to confirm that the safe execution completed
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping statBonus \"stats\" request: " .. dbot.retval.getString(retval))
end -- if
-- Wait for the trigger to fill in the stat bonuses due to spells that are active right now.
-- We've already allocated quite a bit of time to handle the callback above. As a result,
-- the trigger should have already kicked off at this point. We allow it just a little more
-- time though just to be paranoid...
local totTime = 0
local timeout = 2
while (inv.statBonus.bonusInProgress == true) do
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
if (totTime > timeout) then
dbot.warn("inv.statBonus.setCR: Failed to get spell stat bonus information: timed out")
dbot.deleteTrigger(inv.statBonus.trigger.startName)
retval = DRL_RET_TIMEOUT
break
end -- if
end -- while
end -- if
dbot.prompt.show()
else
dbot.debug("Skipping @Glevel " .. level .. " @Wspell bonus update: you are in state \"@C" ..
dbot.gmcp.getStateString(charState) .. "@W\"")
retval = DRL_RET_NOT_ACTIVE
end -- if
-- Update the spell bonus stats if we were able to get new stats
if (retval == DRL_RET_SUCCESS) then
-- If we don't have any bonus history yet, start with the bonuses we just discovered
if (inv.statBonus.spellBonus[level] == nil) then
inv.statBonus.spellBonus[level] = {}
inv.statBonus.spellBonus[level].ave = dbot.table.getCopy(inv.statBonus.currentBonus)
inv.statBonus.spellBonus[level].max = dbot.table.getCopy(inv.statBonus.currentBonus)
-- If we have bonus history, average the current stats with what we had before. This gives
-- recent bonus stat scans a higher weight than older scans. The weighted average helps keep
-- things up-to-date as spellups improve with additional classes.
else
local statList = "int luck wis str dex con"
for spellStat in statList:gmatch("%S+") do
-- Update the weighted average (current stats are weighted 50% by default)
inv.statBonus.spellBonus[level].ave[spellStat] =
(inv.statBonus.currentBonus[spellStat] + inv.statBonus.spellBonus[level].ave[spellStat]) / 2
-- Update the max stats
if (inv.statBonus.currentBonus[spellStat] > inv.statBonus.spellBonus[level].max[spellStat]) then
inv.statBonus.spellBonus[level].max[spellStat] = inv.statBonus.currentBonus[spellStat]
end -- if
end -- for
end -- if
dbot.debug("Updated @GL" .. level .. "@W spell bonuses: " ..
string.format("@Cint@W=@G%.2f@W, @Cluck@W=@G%.2f@W, @Cwis@W=@G%.2f@W, " ..
"@Cstr@W=@G%.2f@W, @Cdex@W=@G%.2f@W, @Ccon@W=@G%.2f@w",
inv.statBonus.spellBonus[level].ave.int,
inv.statBonus.spellBonus[level].ave.luck,
inv.statBonus.spellBonus[level].ave.wis,
inv.statBonus.spellBonus[level].ave.str,
inv.statBonus.spellBonus[level].ave.dex,
inv.statBonus.spellBonus[level].ave.con))
inv.statBonus.save()
end -- if
inv.statBonus.timer.update(inv.statBonus.timer.min, inv.statBonus.timer.sec)
return retval
end -- inv.statBonus.setCR
function inv.statBonus.setSetupFn()
-- Run the "stats" command and pick off the current spell bonuses
check (AddTriggerEx(inv.statBonus.trigger.startName,
"^.*Str.*Int.*Wis.*Dex.*Con.*Luck.*Total.*$",
"EnableTrigger(inv.statBonus.trigger.getName, true)",
drlTriggerFlagsBaseline + trigger_flag.OneShot + trigger_flag.OmitFromOutput,
custom_colour.Custom11,
0, "", "", sendto.script, 0))
end -- inv.statBonus.setSetupFn
inv.statBonus.timer = {}
inv.statBonus.timer.name = "drlInvStatBonusTimer"
inv.statBonus.timer.min = 5
inv.statBonus.timer.sec = 30
function inv.statBonus.timer.update(min, sec)
min = tonumber(min or "")
sec = tonumber(sec or "")
if (min == nil) or (sec == nil) then
dbot.warn("inv.statBonus.timer.update: missing parameters")
return DRL_RET_INVALID_PARAM
end -- if
dbot.debug("Scheduling stat bonus timer in " .. min .. " minutes, " .. sec .. " seconds")
check (AddTimer(inv.statBonus.timer.name, 0, min, sec, "",
timer_flag.Enabled + timer_flag.Replace + timer_flag.OneShot,
"inv.statBonus.set"))
end -- inv.statBonus.timer.update
inv.statBonus.trigger = {}
inv.statBonus.trigger.getName = "drlInvStatBonusTriggerGet"
inv.statBonus.trigger.startName = "drlInvStatBonusTriggerStart"
function inv.statBonus.trigger.get(line)
-- Look for the current spells bonus
local matchStart, matchEnd, eqStr, eqInt, eqWis, eqDex, eqCon, eqLuck =
string.find(line, "Spells Bonus%s+:%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%s+.*$")
if (matchStart ~= nil) then
inv.statBonus.currentBonus.str = tonumber(eqStr)
inv.statBonus.currentBonus.int = tonumber(eqInt)
inv.statBonus.currentBonus.wis = tonumber(eqWis)
inv.statBonus.currentBonus.dex = tonumber(eqDex)
inv.statBonus.currentBonus.con = tonumber(eqCon)
inv.statBonus.currentBonus.luck = tonumber(eqLuck)
inv.statBonus.bonusInProgress = false
dbot.debug("Spells bonus: str=" .. eqStr .. ", int=" .. eqInt .. ", wis=" .. eqWis ..
", dex=" .. eqDex .. ", con=" .. eqCon .. ", luck=" .. eqLuck)
end -- if
-- Shut off the trigger if we hit the end of the stats output
if (line == inv.statBonus.closingMsg) then
EnableTrigger(inv.statBonus.trigger.getName, false)
end -- if
end -- inv.statBonus.trigger.get
----------------------------------------------------------------------------------------------------
--
-- Module to manage analysis of equipment sets
--
-- dinv analyze [list | create | delete | display] <priorityName> <wearable location>
--
-- inv.analyze.sets(priorityName, minLevel, resultData, intensity)
-- inv.analyze.setsCR()
-- inv.analyze.delete(priorityName)
-- inv.analyze.list()
-- inv.analyze.display(priorityName, wearableLoc, endTag)
--
----------------------------------------------------------------------------------------------------
inv.analyze = {}
inv.analyze.setsPkg = nil
inv.analyze.timeoutThreshold = 60
function inv.analyze.sets(priorityName, minLevel, resultData, intensity)
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.analyze.sets: missing priorityName parameter")
return DRL_RET_INVALID_PARAM
end -- if
minLevel = tonumber(minLevel or "")
if (minLevel == nil) then
dbot.warn("inv.analyze.sets: invalid non-numeric minLevel parameter")
return DRL_RET_INVALID_PARAM
end -- if
-- Ensure we don't have multiple analyses in progress
if (inv.analyze.setsPkg ~= nil) then
dbot.info("Skipping analysis of sets: another analysis is in progress")
return DRL_RET_BUSY
end -- if
inv.analyze.setsPkg = {}
inv.analyze.setsPkg.priorityName = priorityName
inv.analyze.setsPkg.minLevel = minLevel
inv.analyze.setsPkg.intensity = intensity
inv.analyze.setsPkg.resultData = resultData
wait.make(inv.analyze.setsCR)
return DRL_RET_SUCCESS
end -- inv.analyze.sets
drlSynchronous = "synchronous"
drlAsynchronous = "asynchronous"
function inv.analyze.setsCR()
local currentLevel
local didDisableRefresh = false
local retval = DRL_RET_SUCCESS
local maxLevel = 201 + 10 * dbot.gmcp.getTier()
local totalLevels
if (inv.state == invStateRunning) then
dbot.info("Skipping set analysis: you are in the middle of an inventory refresh")
retval = DRL_RET_BUSY
else
totalLevels = maxLevel - inv.analyze.setsPkg.minLevel
if (totalLevels == 0) then
totalLevels = 1 -- we don't want to divide by zero in our analysis
elseif (totalLevels < 0) then
dbot.info("Skipping set analysis: minLevel " .. inv.analyze.setsPkg.minLevel ..
" exceeds your current maxLevel " .. maxLevel)
retval = DRL_RET_SUCCESS
end -- if
end -- if
-- If we hit a problem either with the current state or with the level range, abort the request
-- and let the caller know what happened by updating the callback parameter.
if (retval ~= DRL_RET_SUCCESS) then
inv.analyze.setsPkg = nil
-- If the user gave us a callback, use it to let the caller know we are done because we failed in some way
if (inv.analyze.setsPkg.resultData ~= nil) then
dbot.callback.default(inv.analyze.setsPkg.resultData, retval)
end -- if
return retval
end -- if
if (inv.state == invStateIdle) then
inv.state = invStatePaused
didDisableRefresh = true
end -- if
if (inv.set.table[inv.analyze.setsPkg.priorityName] == nil) then
inv.set.table[inv.analyze.setsPkg.priorityName] = {}
end -- if
dbot.prompt.hide()
for currentLevel = inv.analyze.setsPkg.minLevel,maxLevel do
dbot.debug("Creating analysis set for " .. inv.analyze.setsPkg.priorityName .. ", level=" .. currentLevel)
inv.set.table[inv.analyze.setsPkg.priorityName][currentLevel] = nil
retval = inv.set.create(inv.analyze.setsPkg.priorityName, currentLevel,
drlSynchronous, inv.analyze.setsPkg.intensity)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.analyze.setsCR: Failed to create \"" .. (inv.analyze.setsPkg.priorityName or "nil") ..
"\" set at level " .. currentLevel .. ": " .. dbot.retval.getString(retval))
break
end -- if
-- Show progress and let something else run occasionally
local levelsChecked = currentLevel - inv.analyze.setsPkg.minLevel
local progressPercent
if (inv.analyze.setsPkg.minLevel == maxLevel) then
progressPercent = 100
else
progressPercent = levelsChecked / totalLevels * 100
end -- if
if (levelsChecked % 10 == 0) or (currentLevel == maxLevel) then
dbot.print("@WEquipment analysis of \"@C" .. inv.analyze.setsPkg.priorityName .. "@W\": @G" ..
string.format("%3d", progressPercent) .. "%")
wait.time(0.1)
end -- if
-- Wait for the table to be filled in (or time out)
local totTime = 0
local timeout = 10
while (inv.set.table[inv.analyze.setsPkg.priorityName][currentLevel] == nil) do
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
if (totTime > timeout) then
dbot.error("inv.analyze.setsCR: Failed to analyze \"" .. inv.analyze.setsPkg.priorityName ..
"\" priority for level " .. currentLevel .. ": timed out")
retval = DRL_RET_TIMEOUT
break
end -- if
end -- while
-- If we had a problem with one set analysis, break out and don't continue
if (retval == DRL_RET_TIMEOUT) then
break
end -- if
end -- for
dbot.prompt.show()
-- Re-enable refreshes if we disabled them during this analysis
if (didDisableRefresh) then
inv.state = invStateIdle
end -- if
-- Save what we found
inv.set.save()
inv.statBonus.save()
-- If the user gave us a callback, use it to let the caller know we are done
if (inv.analyze.setsPkg.resultData ~= nil) then
dbot.callback.default(inv.analyze.setsPkg.resultData, retval)
end -- if
-- Clean up and return
inv.analyze.setsPkg = nil
return retval
end -- inv.analyze.setsCR
function inv.analyze.delete(priorityName)
local retval = DRL_RET_SUCCESS
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.analyze.delete: missing priority name parameter")
return DRL_RET_INVALID_PARAM
end -- if
if (inv.set.table[priorityName] == nil) then
dbot.info("Analysis for priority \"@C" .. priorityName .. "@W\" does not exist")
retval = DRL_RET_MISSING_ENTRY
else
dbot.info("Deleted set analysis for priority \"@C" .. priorityName .. "@W\"")
inv.set.table[priorityName] = nil
inv.set.save()
end -- if
return retval
end -- inv.analyze.delete
function inv.analyze.list()
local retval = DRL_RET_SUCCESS
local numAnalyses = 0
local sortedNames = {}
-- Sort the analysis names
for name, _ in pairs(inv.set.table) do
table.insert(sortedNames, name)
end -- for
table.sort(sortedNames, function (v1, v2) return v1 < v2 end)
dbot.print("@WSet analysis: @Gcomplete@W or @Ypartial@W")
for _, name in ipairs(sortedNames) do
local minLevel = 1 + 10 * dbot.gmcp.getTier()
local maxLevel = minLevel + 200
local analysisPrefix = "@G"
local analysisSuffix = ""
-- Scan through all levels for the analysis to see if any are missing at least one set
for level = minLevel, maxLevel do
if (inv.set.table[name][level] == nil) then
analysisPrefix = "@Y"
analysisSuffix = "@W -- Run \"@Gdinv analyze create " .. name .. "@W\" to complete the analysis"
break
end -- if
end -- for
dbot.print(" " .. analysisPrefix .. name .. analysisSuffix)
numAnalyses = numAnalyses + 1
end -- for
if (numAnalyses == 0) then
dbot.print("@W No set analyses were detected.")
retval = DRL_RET_MISSING_ENTRY
end -- if
dbot.print("")
return retval
end -- inv.analyze.list
function inv.analyze.display(priorityName, wearableLoc, endTag)
local currentLevel
local retval = DRL_RET_SUCCESS
local lastSet = nil
local setWearLoc
if (priorityName == nil) or (priorityName == "") then
dbot.warn("inv.analyze.display: Priority name parameter is missing")
return inv.tags.stop(invTagsAnalyze, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.set.table == nil) or (inv.set.table[priorityName] == nil) then
dbot.warn("Analysis is not available for priority \"@C" .. priorityName ..
"@W\". Run \"@Gdinv analyze create " .. priorityName .. "@W\" to create it.")
return inv.tags.stop(invTagsAnalyze, endTag, DRL_RET_MISSING_ENTRY)
end -- if
dbot.debug("inv.analyze.display: priority=\"" .. priorityName .. "\", locs=\"" .. wearableLoc .. "\"")
local tierLevel = 10 * dbot.gmcp.getTier()
local startLevel = 1 + tierLevel
local endLevel = 201 + tierLevel
for currentLevel = startLevel, endLevel do
local didUpdateThisLevel = false
for _,setWearLoc in pairs(inv.wearLoc) do
if (wearableLoc == nil) or (wearableLoc == "") or dbot.isWordInString(setWearLoc, wearableLoc) then
local set = inv.set.table[priorityName][currentLevel]
local prevSet = inv.set.table[priorityName][currentLevel - 1]
if (set ~= nil) and (set[setWearLoc] ~= nil) then
local objId = tonumber(set[setWearLoc].id or "")
local prevObjId
if (prevSet ~= nil) and (prevSet[setWearLoc] ~= nil) then
prevObjId = tonumber(prevSet[setWearLoc].id or "")
end -- if
-- Display the item if this is our first level or if something changed from the previous level
if (objId ~= nil) and ((objId ~= prevObjId) or (currentLevel == startLevel)) then
if (didUpdateThisLevel == false) then
didUpdateThisLevel = true
dbot.print(string.format("\n@Y%s@W Level %3d @Y%s@s",
string.rep("-", 44), currentLevel, string.rep("-", 44)))
inv.items.displayLastType = "" -- kludge to force print the display header
end -- if
-- If an item was just replaced, give info on it so that we can compare it to the new item
if (currentLevel ~= startLevel) and (prevObjId ~= nil) then
inv.items.displayItem(prevObjId, invDisplayVerbosityDiffRemove, setWearLoc)
end -- if
inv.items.displayItem(objId, invDisplayVerbosityDiffAdd, setWearLoc)
end -- if
end -- if
end -- if
end -- for
end -- for
return inv.tags.stop(invTagsAnalyze, endTag, retval)
end -- inv.analyze.display
----------------------------------------------------------------------------------------------------
--
-- Module to scan equipment sets for items matching a query and then provide usage information
-- for those items. This includes listing all equipment sets that use the item and at what level(s)
-- the item is used.
--
-- dinv usage <priority name> <query>
--
-- inv.usage.display(priorityName, query, endTag)
-- inv.usage.displayCR()
-- inv.usage.displayItem(priorityName, objId)
-- inv.usage.get(priorityName, objId)
--
----------------------------------------------------------------------------------------------------
inv.usage = {}
inv.usage.displayPkg = nil
function inv.usage.display(priorityName, query, endTag)
if (priorityName == nil) or (query == nil) then
dbot.error("inv.usage.display: input parameters are nil!")
return inv.tags.stop(invTagsUsage, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (inv.usage.displayPkg ~= nil) then
dbot.info("Skipping display of item usage: another request is in progress")
return inv.tags.stop(invTagsUsage, endTag, DRL_RET_BUSY)
end -- if
inv.usage.displayPkg = {}
inv.usage.displayPkg.priorityName = priorityName
inv.usage.displayPkg.query = query
inv.usage.displayPkg.endTag = endTag
wait.make(inv.usage.displayCR)
return DRL_RET_SUCCESS
end -- inv.usage.display
function inv.usage.displayCR()
if (inv.usage.displayPkg == nil) then
dbot.error("inv.usage.displayCR: inv.usage.displayPkg is nil!")
return DRL_RET_INTERNAL_ERROR
end -- if
local retval
local idArray
local priorityName = inv.usage.displayPkg.priorityName or ""
local query = inv.usage.displayPkg.query or ""
local endTag = inv.usage.displayPkg.endTag
-- Get an array of IDs for items that match the specified query
idArray, retval = inv.items.searchCR(query)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.usage.displayCR: failed to search inventory table: " .. dbot.retval.getString(retval))
-- Let the user know if no items matched their query
elseif (idArray == nil) or (#idArray == 0) then
dbot.info("No match found for usage query: \"" .. query .. "\"")
else
-- Sort the items in the array before we display them
inv.items.sort(idArray, { { field = invStatFieldType, isAscending = true },
{ field = invStatFieldLevel, isAscending = true },
{ field = invStatFieldWearable, isAscending = true },
{ field = invStatFieldName, isAscending = true } })
-- Display the items!
for _, id in ipairs(idArray) do
local wearableField = inv.items.getStatField(id, invStatFieldWearable)
local typeField = inv.items.getStatField(id, invStatFieldType)
-- Only consider an item available to be used if it has a wearable location,
-- is not a potion, pill, or food, and if it is not both a treasure and hold item.
if (wearableField ~= nil) and
(typeField ~= invmon.typeStr[invmonTypePotion]) and
(typeField ~= invmon.typeStr[invmonTypePill]) and
(typeField ~= invmon.typeStr[invmonTypeFood]) and
((typeField ~= invmon.typeStr[invmonTypeTreasure]) or
(wearableField ~= inv.wearLoc[invWearableLocHold])) then
if (priorityName == "all") then
for priority, _ in pairs(inv.priority.table) do
inv.usage.displayItem(priority, id)
end -- for
else
inv.usage.displayItem(priorityName, id)
end -- if
end -- if
end -- for
end -- if
-- Clean up and return
inv.usage.displayPkg = nil
return inv.tags.stop(invTagsUsage, endTag, retval)
end -- inv.usage.displayCR
function inv.usage.displayItem(priorityName, objId)
-- TODO: this is very similar to code in displayItem: consolidate it into a helper function
local colorName = inv.items.getField(objId, invFieldColorName) or "Unknown"
local maxNameLen = 44
-- We color-code the ID field as follows: unidentified = red, partial ID = yellow, full ID = green
local formattedId = ""
local colorizedId = ""
local idPrefix = DRL_ANSI_WHITE
local idSuffix = DRL_ANSI_WHITE
local idLevel = inv.items.getField(objId, invFieldIdentifyLevel)
if (idLevel ~= nil) then
if (idLevel == invIdLevelNone) then
idPrefix = DRL_ANSI_RED
elseif (idLevel == invIdLevelPartial) then
idPrefix = DRL_ANSI_YELLOW
elseif (idLevel == invIdLevelFull) then
idPrefix = DRL_ANSI_GREEN
end -- if
formattedId = "(" .. objId .. ") "
colorizedId = idPrefix .. formattedId .. idSuffix
end -- if
-- Format the name field for the stat display. This is complicated because we have a fixed
-- number of spaces reserved for the field but color codes could take up some of those spaces.
-- We iterate through the string byte by byte checking the length of the non-colorized equivalent
-- to see when we've hit the limit that we can print.
local formattedName = ""
local index = 0
while (#strip_colours(formattedName) < maxNameLen - #formattedId) and (index < 50) do
formattedName = string.sub(colorName, 1, maxNameLen - #formattedId + index)
-- It's possible for an item to have "%@" as part of its name (e.g., Roar of Victory). This bombs
-- when we try to display it because our print routine interprets it as a single format option. We
-- replace it with doubled % so that the print routine knows it is a literal.
formattedName = string.gsub(formattedName, "%%@", "%%%%@")
index = index + 1
end
if (#strip_colours(formattedName) < maxNameLen - #formattedId) then
formattedName = formattedName .. string.rep(" ", maxNameLen - #strip_colours(formattedName) - #formattedId)
end -- if
-- The trimmed name could end on an "@" which messes up color codes and spacing
formattedName = string.gsub(formattedName, "@$", " ") .. " " .. DRL_XTERM_GREY
formattedName = formattedName .. colorizedId
local levelUsage = inv.usage.get(priorityName, objId)
local itemLevel = inv.items.getStatField(objId, invStatFieldLevel) or "N/A"
local itemType = DRL_ANSI_YELLOW .. (inv.items.getStatField(objId, invStatFieldType) or "No Type") ..
DRL_ANSI_WHITE
local levelStr = ""
local levelPrefix = "@G"
local levelSuffix = "@W"
if (levelUsage == nil) or (#levelUsage == 0) then
levelStr = DRL_ANSI_RED .. "Unused"
levelPrefix = "@R"
else
levelStr = DRL_ANSI_GREEN
-- Convert the list of levels into a string with ranges
for i = 1, #levelUsage do
-- If we have consecutive numbers on either side, we are in a range and can whack this item
if (levelUsage[i - 1] ~= nil) and
((levelUsage[i] == levelUsage[i - 1] + 1) or (levelUsage[i - 1] == 0 )) and
(levelUsage[i + 1] ~= nil) and (levelUsage[i] == levelUsage[i + 1] - 1) then
levelUsage[i] = 0
end -- if
end -- for
local inRange = false
for i = 1, #levelUsage do
if (inRange == false) then
if (levelUsage[i] == 0) then
levelStr = levelStr .. "-"
inRange = true
elseif (i == 1) then
levelStr = levelStr .. levelUsage[i]
else
levelStr = levelStr .. " " .. levelUsage[i]
end -- if
elseif (levelUsage[i] ~= 0) then
levelStr = levelStr .. levelUsage[i]
inRange = false
end -- if
end -- for
end -- if
dbot.print(string.format("%s%3d%s " .. formattedName .. itemType .. " " .. priorityName ..
" " .. levelStr, levelPrefix, itemLevel, levelSuffix))
end -- inv.usage.displayItem
-- Returns an array of levels in which the item is used by the specified priority
function inv.usage.get(priorityName, objId)
if (priorityName == nil) then
dbot.warn("inv.usage.get: priorityName parameter is nil!")
return nil, DRL_RET_INVALID_PARAM
end -- if
objId = tonumber(objId or "")
if (objId == nil) then
dbot.warn("inv.usage.get: objId parameter is not a number")
return nil, DRL_RET_INVALID_PARAM
end -- if
dbot.debug("Usage: priority=\"" .. priorityName .. "\", objId=" .. objId)
local startLevel = tonumber(inv.items.getStatField(objId, invStatFieldLevel) or "")
local endLevel = 201 + 10 * dbot.gmcp.getTier()
local wearType = inv.items.getStatField(objId, invStatFieldWearable)
local levelArray = {}
-- wearType is a general location (e.g., wrist, finger, etc.)
-- wearLoc is a specific location of wearType (e.g., lwrist, rfinger, etc.)
--
-- Scan through every possible wearLoc in the priority at the specified level to
-- see if the current object is at that location. If it is, remember it by putting
-- the level it is used in an array that we return to the caller.
if (wearType ~= nil) and (inv.wearables[wearType] ~= nil) then
for _, wearLoc in ipairs(inv.wearables[wearType]) do
if (wearLoc ~= nil) and (startLevel ~= nil) and (inv.set.table[priorityName] ~= nil) then
for level = startLevel, endLevel do
if (inv.set.table[priorityName][level] ~= nil) and
(inv.set.table[priorityName][level][wearLoc] ~= nil) and
(inv.set.table[priorityName][level][wearLoc].id == objId) then
table.insert(levelArray, level)
end -- if
end -- for
end -- if
end -- for
end -- if
return levelArray, DRL_RET_SUCCESS
end -- inv.usage.get
----------------------------------------------------------------------------------------------------
-- Module to track which tags are enabled
--
-- Tags add opening and terminating strings to the output. They surround a particular operation
-- so that the user can know when the operation starts, when it stops, and what the final return
-- value of the operation is.
--
-- inv.tags.init.atActive()
-- inv.tags.fini(doSaveState)
--
-- inv.tags.save()
-- inv.tags.load()
-- inv.tags.reset()
--
-- inv.tags.enable()
-- inv.tags.disable()
-- inv.tags.isEnabled()
--
-- inv.tags.display()
-- inv.tags.set(tagNames, tagValue)
--
-- inv.tags.start(moduleName, startTag)
-- inv.tags.stop(moduleName, endTag, returnValue)
--
----------------------------------------------------------------------------------------------------
inv.tags = {}
inv.tags.init = {}
inv.tags.table = {}
inv.tags.stateName = "inv-tags.state"
invTagsRefresh = "refresh"
invTagsBuild = "build"
invTagsSearch = "search"
invTagsGet = "get"
invTagsPut = "put"
invTagsStore = "store"
invTagsKeyword = "keyword"
invTagsOrganize = "organize"
invTagsSet = "set"
invTagsSnapshot = "snapshot"
invTagsPriority = "priority"
invTagsAnalyze = "analyze"
invTagsUsage = "usage"
invTagsCompare = "compare"
invTagsCovet = "covet"
invTagsBackup = "backup"
invTagsReset = "reset"
invTagsForget = "forget"
invTagsNotify = "notify"
invTagsCache = "cache"
invTagsVersion = "version"
invTagsHelp = "help"
inv.tags.modules = invTagsBuild .. " " ..
invTagsRefresh .. " " ..
invTagsSearch .. " " ..
invTagsGet .. " " ..
invTagsPut .. " " ..
invTagsStore .. " " ..
invTagsKeyword .. " " ..
invTagsOrganize .. " " ..
invTagsSet .. " " ..
invTagsSnapshot .. " " ..
invTagsPriority .. " " ..
invTagsAnalyze .. " " ..
invTagsUsage .. " " ..
invTagsCompare .. " " ..
invTagsCovet .. " " ..
invTagsBackup .. " " ..
invTagsReset .. " " ..
invTagsForget .. " " ..
invTagsNotify .. " " ..
invTagsCache .. " " ..
invTagsVersion .. " " ..
invTagsHelp
drlInvTagOn = "on"
drlInvTagOff = "off"
function inv.tags.init.atActive()
local retval = DRL_RET_SUCCESS
-- Pull in what we already know
retval = inv.tags.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.tags.init.atActive: Failed to load tags data: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.tags.init.atActive
function inv.tags.fini(doSaveState)
local retval = DRL_RET_SUCCESS
if (doSaveState) then
-- Save our current tags data
retval = inv.tags.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.tags.fini: Failed to save tags data: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.tags.fini
function inv.tags.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.tags.stateName,
"inv.tags.table", inv.tags.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.tags.save: Failed to save tags table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.tags.save
function inv.tags.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.tags.stateName, inv.tags.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.tags.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.tags.stateName .. "@W\": " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.tags.load
function inv.tags.reset()
inv.tags.table = {}
for tag in inv.tags.modules:gmatch("%S+") do
inv.tags.table[tag] = drlInvTagOff
end -- for
-- This is a top-level flag enabling or disabling all other tags
inv.tags.table["tags"] = drlInvTagOn
local retval = inv.tags.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.tags.reset: Failed to save tags persistent data: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.tags.reset
function inv.tags.enable()
inv.tags.table["tags"] = drlInvTagOn
dbot.info("Tags module is @GENABLED@W (specific tags may or may not be enabled)")
return inv.tags.save()
end -- inv.tags.enable
function inv.tags.disable()
inv.tags.table["tags"] = drlInvTagOff
dbot.info("Tags module is @RDISABLED@W (individual tag status is ignored when the module is disabled)")
return inv.tags.save()
end -- inv.tags.disable
function inv.tags.isEnabled()
if (inv.tags.table ~= nil) and (inv.tags.table["tags"] ~= nil) and
(inv.tags.table["tags"] == drlInvTagOn) then
return true
else
return false
end -- if
end -- inv.tags.isEnabled
function inv.tags.display()
local retval = DRL_RET_SUCCESS
local isEnabled
if inv.tags.isEnabled() then
isEnabled = "@GENABLED@W"
else
isEnabled = "@RDISABLED@W"
end -- if
dbot.print("@y" .. pluginNameAbbr .. "@W : tags are " .. isEnabled)
dbot.print("@WSupported tags")
for tag in inv.tags.modules:gmatch("%S+") do
local tagValue = inv.tags.table[tag] or "uninitialized"
local valuePrefix
if (tagValue == drlInvTagOn) then
valuePrefix = "@G"
else
valuePrefix = "@R"
end -- if
dbot.print(string.format("@C %10s@W = ", tag) .. valuePrefix .. tagValue)
end -- for
return retval
end -- inv.tags.display
function inv.tags.set(tagNames, tagValue)
local retval = DRL_RET_SUCCESS
if (tagValue ~= drlInvTagOn) and (tagValue ~= drlInvTagOff) then
dbot.warn("inv.tags.set: Invalid tag value \"" .. (tagValue or "nil") .. "\"")
return DRL_RET_INVALID_PARAM
end -- if
for tag in tagNames:gmatch("%S+") do
if dbot.isWordInString(tag, inv.tags.modules) then
inv.tags.table[tag] = tagValue
local valuePrefix
if (tagValue == drlInvTagOn) then
valuePrefix = "@G"
else
valuePrefix = "@R"
end -- if
dbot.note("Set tag \"@C" .. tag .. "@W\" to \"" .. valuePrefix .. tagValue .. "@W\"")
else
dbot.warn("inv.tags.set: Failed to set tag \"@C" .. tag .. "@W\": Unsupported tag")
retval = DRL_RET_INVALID_PARAM
end -- if
end -- for
local saveRetval = inv.tags.save()
if (saveRetval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.tags.set: Failed to save tags persistent data: " .. dbot.retval.getString(saveRetval))
end -- if
-- If the only problem that arose was with the save, return the save's return value. Otherwise,
-- return whatever return value we hit while setting the tags.
if (retval == DRL_RET_SUCCESS) and (saveRetval ~= DRL_RET_SUCCESS) then
return saveRetval
else
return retval
end -- if
end -- inv.tags.set
function inv.tags.start(moduleName, startTag)
local retval = DRL_RET_SUCCESS
--[[ TODO: I don't think that we actually want to use this after all. The problem is that
the start tag won't be displayed until any pending commands that are queued up on
the server actually complete. We could wait until we get confirmation that the
command finished and our completion message was displayed before we continue.
However, that would add quite a bit of latency and overhead for the user.
Also, I'm not convinced that seeing a start tag is that helpful. If someone kicks
off a command, they probably are much more interested in knowing when it is done
(i.e., seeing the end tag) than knowing that they made the original request -- which
really shouldn't be a surprise since they are the ones that made the request ;-)
if (moduleName ~= nil) and (startTag ~= nil) and (startTag ~= "") and
(inv.tags.table ~= nil) and (inv.tags.table[moduleName] == drlInvTagOn) and
inv.tags.isEnabled() then
retval = dbot.execute.fast.command("echo " .. "{" .. startTag .. "}")
end -- if
--]]
return retval
end -- inv.tags.start
function inv.tags.stop(moduleName, endTag, retval)
if (retval == nil) then
retval = DRL_RET_INTERNAL_ERROR
end -- if
if (moduleName ~= nil) and (endTag ~= nil) and (endTag ~= "") and
(inv.tags.table ~= nil) and (inv.tags.table[moduleName] == drlInvTagOn) and
inv.tags.isEnabled() then
local tagMsg = "{/" .. endTag .. ":" .. retval .. ":" .. dbot.retval.getString(retval) .. "}"
local charState = dbot.gmcp.getState()
-- If we are in a state that allows echo'ing messages, send the end tag. Otherwise, warn the
-- user.
if (charState == dbot.stateActive) or
(charState == dbot.stateCombat) or
(charState == dbot.stateSleeping) or
(charState == dbot.stateTBD) or
(charState == dbot.stateResting) or
(charState == dbot.stateRunning) then
dbot.execute.fast.command("echo " .. tagMsg)
else
dbot.warn("You are in state \"@C" .. dbot.gmcp.getStateString(charState) ..
"@W\": Could not echo end tag \"@G" .. tagMsg .. "@W\"")
end -- if
end -- if
return retval
end -- inv.tags.end
----------------------------------------------------------------------------------------------------
--
-- Module to manage buying and using consumables (potions, pills, scrolls, etc.)
--
-- dinv consume add [type] [itemName]
-- remove [type] <itemName>
-- display <type>
-- list
-- buy [type] <numItems>
-- small [type] <numItems>
-- big [type] <numItems>
--
-- inv.consume.init.atActive()
-- inv.consume.fini(doSaveState)
--
-- inv.consume.save()
-- inv.consume.load()
-- inv.consume.reset()
--
-- inv.consume.add(typeName, itemName)
-- inv.consume.addCR() -- async so that we can appraise the new item
-- inv.consume.remove(typeName, itemName)
-- inv.consume.display(typeName) -- if typeName is missing, display all types
-- inv.consume.displayType(typeName)
--
-- inv.consume.buy(typeName, numItems, containerName)
-- inv.consume.buyCR() -- async so that we can run to the shopkeeper
-- inv.consume.get(typeName, size, containerId)
-- inv.consume.use(typeName, size, numItems, containerName)
-- inv.consume.useCR()
-- inv.consume.useItem(objId, commandArray)
--
-- Consumable table format:
-- table[typeName] =
-- { heal = { { level=1, name="light relief", room="32476", fullName="(!(Light Relief)!)" },
-- { level=20, name="serious relief", room="32476", fullName="(!(Serious Relief)!)" } },
-- mana = { { level=1, name="lotus rush", room="32476", fullName="(!(Lotus Rush)!)" } },
-- fly = { { level=1, name="griff", room="32476", fullName="(!(Griffon's Blood)!)" } }
-- }
--
----------------------------------------------------------------------------------------------------
inv.consume = {}
inv.consume.init = {}
inv.consume.table = {}
inv.consume.stateName = "inv-consume.state"
function inv.consume.init.atActive()
local retval = DRL_RET_SUCCESS
retval = inv.consume.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.init.atActive: failed to load consume data from storage: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- inv.consume.init.atActive
function inv.consume.fini(doSaveState)
local retval = DRL_RET_SUCCESS
if (doSaveState) then
-- Save our current data
retval = inv.consume.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.fini: Failed to save inv.consume module data: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- inv.consume.fini
function inv.consume.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. inv.consume.stateName,
"inv.consume.table", inv.consume.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.save: Failed to save consume table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.consume.save
function inv.consume.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. inv.consume.stateName, inv.consume.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. inv.consume.stateName .. "@W\": " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.consume.load
function inv.consume.reset()
local retval
-- Start with a few basic consumables from the Aylor potion shop ("runto potion")
inv.consume.table =
{ heal = { { level=1, name="light relief", room="32476", fullName="(!(Light Relief)!)" },
{ level=20, name="serious relief", room="32476", fullName="(!(Serious Relief)!)" } },
mana = { { level=1, name="lotus rush", room="32476", fullName="(!(Lotus Rush)!)" } },
fly = { { level=1, name="griff", room="32476", fullName="(!(Griffon's Blood)!)" } }
}
retval = inv.consume.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.reset: Failed to save consumable data: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- inv.consume.reset
inv.consume.addPkg = nil
function inv.consume.add(typeName, itemName)
if (typeName == nil) or (typeName == "") then
dbot.warn("inv.consume.add: Missing type name")
return DRL_RET_INVALID_PARAM
end -- if
if (itemName == nil) or (itemName == "") then
dbot.warn("inv.consume.add: Missing item name")
return DRL_RET_INVALID_PARAM
end -- if
itemLevel = tonumber(itemLevel or "")
if (inv.consume.addPkg ~= nil) then
dbot.info("Skipping request to add a consumable item: another request is in progress")
return DRL_RET_BUSY
end -- if
inv.consume.addPkg = {}
inv.consume.addPkg.type = typeName
inv.consume.addPkg.name = itemName
inv.consume.addPkg.level = itemLevel
inv.consume.addPkg.room = roomId
wait.make(inv.consume.addCR)
return DRL_RET_SUCCESS
end -- inv.consume.add
function inv.consume.addCR()
local retval = DRL_RET_SUCCESS
local typeName = inv.consume.addPkg.type
local itemName = inv.consume.addPkg.name or ""
local itemLevel = inv.consume.addPkg.level
-- We don't want an inventory refresh triggering in the middle of this shop item evaluation.
local origRefreshState = inv.state
if (inv.state == invStateIdle) then
inv.state = invStatePaused
elseif (inv.state == invStateRunning) then
dbot.info("Skipping shop item addition: you are in the middle of an inventory refresh")
inv.consume.addPkg = nil
return DRL_RET_BUSY
end -- if
-- If the optional room ID is not present, use the current room
local roomId = tonumber(inv.consume.addPkg.room or "")
if (roomId == nil) then
roomId = dbot.gmcp.getRoomId() or 0
end -- if
if (inv.consume.table[typeName] == nil) then
inv.consume.table[typeName] = {}
end -- if
-- Temporarily create an item placeholder with a fake object ID and a fake location.
-- We will fill in this placeholder with information from a shopkeeper appraisal later.
local objId = 42
retval = inv.items.add(objId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.addCR: Failed to add fake objId " .. (objId or "nil") .. ": " ..
dbot.retval.getString(retval))
inv.consume.addPkg = nil
inv.state = origRefreshState
return retval
end -- if
-- Fake a location for the shop item
inv.items.setField(objId, invFieldObjLoc, invItemLocShopkeeper)
-- Attempt to identify the shopkeeper item and wait until we have confirmation that the ID completed
local resultData = dbot.callback.new()
retval = inv.items.identifyItem(objId, "appraise " .. itemName, resultData)
if (retval == DRL_RET_SUCCESS) then
retval = dbot.callback.wait(resultData, inv.items.timer.idTimeoutThresholdSec)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.addCR: Appraisal timed out for shopkeeper item" ..
inv.consume.addPkg.auctionNum)
end -- if
end -- if
-- Get the level of the shop item we just identified
itemLevel = tonumber(inv.items.getStatField(objId, invStatFieldLevel) or "")
local fullName = inv.items.getStatField(objId, invStatFieldName) or ""
-- Remove the temporary item and ensure it isn't stuck in the cache
inv.items.remove(objId)
inv.cache.remove(inv.cache.recent.table, objId)
if (itemLevel ~= nil) then
-- If the item is already in the table, don't add it again!
local itemExists = false
for i, entry in ipairs(inv.consume.table[typeName]) do
if (entry.level == itemLevel) and (entry.name == itemName) then
dbot.note("Skipping addition of consumable item \"" .. itemName .. "\" of type \"" ..
typeName .. "\": item already exists")
itemExists = true
break
end -- if
end -- for
-- If the item isn't already in the consumable table, add it and then re-sort the table to
-- account for the new item
if (itemExists == false) then
table.insert(inv.consume.table[typeName],
{ level = itemLevel, name = itemName, room = roomId, fullName = fullName })
table.sort(inv.consume.table[typeName], function (v1, v2) return v1.level < v2.level end)
inv.consume.save()
end -- if
else
dbot.warn("inv.consume.addCR: Failed to identify shop item \"" .. itemName .. "\" in room \"" .. roomId)
retval = DRL_MISSING_ENTRY
end -- if
-- Restore the original refresh state (we may have paused it during this operation)
inv.state = origRefreshState
-- Clean up and return
inv.consume.addPkg = nil
return retval
end -- inv.consume.addCR
function inv.consume.remove(typeName, itemName)
if (typeName == nil) or (typeName == "") then
dbot.warn("inv.consume.remove: Missing type name")
return DRL_RET_INVALID_PARAM
end -- if
if (inv.consume.table == nil) or (inv.consume.table[typeName] == nil) then
dbot.info("Type \"" .. typeName .. "\" is not in the consumable table")
return DRL_RET_MISSING_ENTRY
end -- if
local retval = DRL_RET_MISSING_ENTRY
-- If itemName is nil, remove all of the specified type
if (itemName == nil) or (itemName == "") then
inv.consume.table[typeName] = nil
retval = DRL_RET_SUCCESS
-- Search the table for the item matching "itemName" and remove just that item
else
for i, entry in ipairs(inv.consume.table[typeName]) do
if (entry.name == itemName) then
dbot.note("Removed \"" .. itemName .. "\" from \"" .. typeName .. "\" consumable table")
table.remove(inv.consume.table[typeName], i)
inv.consume.save()
retval = DRL_RET_SUCCESS
break
end -- if
end -- for
end -- if
if (retval == DRL_RET_MISSING_ENTRY) then
dbot.info("Skipping removal of consumable \"" .. itemName .. "\": item is not in consumable table")
end -- if
return retval
end -- inv.consume.remove
-- If typeName is nil or "", display all types in the table
function inv.consume.display(typeName)
local retval = DRL_RET_SUCCESS
local numEntries = 0
if (typeName ~= nil) and (typeName ~= "") then
numEntries = inv.consume.displayType(typeName)
else
for itemType,_ in pairs(inv.consume.table) do
numEntries = numEntries + inv.consume.displayType(itemType)
end -- for
end -- if
if (numEntries == 0) then
dbot.print("@W No items of type \"" .. typeName .. "\" are in the consumable table@w")
retval = DRL_RET_MISSING_ENTRY
end -- if
return retval
end -- inv.consume.display
function inv.consume.displayType(typeName)
local numEntries = 0
if (inv.consume.table == nil) or (typeName == nil) or (typeName == "") or
(inv.consume.table[typeName] == nil) then
dbot.warn("inv.consume.displayType: Type \"" .. (typeName or "nil") ..
"\" is not in the consumable table")
return numEntries, DRL_RET_MISSING_ENTRY
end -- if
dbot.print("@W" .. pluginNameAbbr .. " Consumable Type \"" .. typeName .. "\"@w")
if (inv.consume.table[typeName] ~= nil) then
for _, entry in ipairs(inv.consume.table[typeName]) do
dbot.print("@W Level " .. string.format("%3d", (entry.level or "")) ..
" (@CRoom " .. string.format("%3d", (entry.room or 0)) .. "@W)" ..
" \"@G" .. (entry.name or "") .. "@W\"@w")
numEntries = numEntries + 1
end -- for
end -- if
return numEntries, DRL_RET_SUCCESS
end -- inv.consume.displayType
inv.consume.buyPkg = nil
function inv.consume.buy(typeName, numItems, containerName)
if (typeName == nil) or (typeName == "") then
dbot.warn("inv.consume.buy: Missing type name")
return DRL_RET_INVALID_PARAM
end -- if
-- If the user didn't specify how many items to buy, default to 1 item
numItems = tonumber(numItems or "")
if (numItems == nil) or (numItems == "") then
numItems = 1
end -- if
-- If there are no entries of the specified type, there's no need to keep searching
if (inv.consume.table[typeName] == nil) then
dbot.info("No items of type \"" .. typeName .. "\" are in the consumable table")
return DRL_RET_MISSING_ENTRY
end -- if
-- The containerName parameter is optional. If it is present, we move the bought items
-- into the container after the purchase is complete.
containerName = containerName or ""
-- Find the highest level item that is available to the char that matches typeName
local curLevel = dbot.gmcp.getLevel()
local bestEntry = nil
for _, entry in ipairs(inv.consume.table[typeName]) do
if (entry.level < curLevel) then
bestEntry = entry
end -- if
end -- for
if (bestEntry == nil) then
dbot.info("No items of type \"" .. typeName .. "\" are available at level " .. curLevel)
return DRL_RET_MISSING_ENTRY
end -- if
if (inv.consume.buyPkg ~= nil) then
dbot.info("Skipping request to buy consumable \"" .. typeName .. "\": another request is in progress")
return DRL_RET_BUSY
end -- if
inv.consume.buyPkg = {}
inv.consume.buyPkg.room = bestEntry.room
inv.consume.buyPkg.itemName = bestEntry.name
inv.consume.buyPkg.numItems = numItems
inv.consume.buyPkg.containerName = containerName
wait.make(inv.consume.buyCR)
return DRL_RET_SUCCESS
end -- inv.consume.buy
-- need to block so that we can run to the shopkeeper
function inv.consume.buyCR()
local retval = DRL_RET_SUCCESS
local room = tonumber(inv.consume.buyPkg.room or "")
if (room == nil) then
dbot.warn("inv.consume.buyCR: Target room is missing")
inv.consume.buyPkg = nil
return DRL_RET_INVALID_PARAM
end -- if
dbot.debug("Running to \"" .. inv.consume.buyPkg.room .. "\" to buy \"" .. inv.consume.buyPkg.numItems ..
"\" of \"" .. inv.consume.buyPkg.itemName .. "\"")
-- Run!
dbot.execute.fast.command("mapper goto " .. inv.consume.buyPkg.room)
-- Wait until we get to the target room
local totTime = 0
local timeout = 10
while (room ~= tonumber(dbot.gmcp.getRoomId())) do
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
if (totTime > timeout) then
dbot.warn("inv.consume.buyCR: Timed out running to room " .. room)
reval = DRL_RET_TIMEOUT
break
end -- if
end -- if
-- Buy the items if no problems came up going to the room
if (retval == DRL_RET_SUCCESS) then
local commands = {}
table.insert(commands, "buy " .. inv.consume.buyPkg.numItems .. " " .. inv.consume.buyPkg.itemName)
if (inv.consume.buyPkg.containerName ~= nil) and (inv.consume.buyPkg.containerName ~= "") then
table.insert(commands,
"put all.\'" .. inv.consume.buyPkg.itemName .. "\' " .. inv.consume.buyPkg.containerName)
end -- if
dbot.execute.fast.commands(commands)
end -- if
-- Clean up and return
inv.consume.buyPkg = nil
return retval
end -- inv.consume.buyCR
-- Returns objId for an item
function inv.consume.get(typeName, size, containerId)
local curLevel = dbot.gmcp.getLevel()
if (typeName == nil) or (typeName == "") then
dbot.warn("inv.consume.get: type name is missing")
return DRL_RET_INVALID_PARAM
end -- if
if (inv.consume.table[typeName] == nil) then
dbot.warn("inv.consume.get: no consumables of type \"" .. typeName .. "\" are available")
return DRL_RET_INVALID_PARAM
end -- if
-- If the user specified a preferred container, use items from that container first. If they
-- didn't specify a preferred container, use items from the main inventory first before using
-- items from other locations.
local preferredLocation
containerId = tonumber(containerId or "")
if (containerId ~= nil) then
preferredLocation = containerId
else
preferredLocation = invItemLocInventory
end -- if
-- If we are getting a "small" item, keep the default table order of small-to-big so that we
-- hit small items first. If we are getting a "big" item, get a copy of the table (so we don't
-- mess up the original) and then sort the copy in reverse order with high-level items first.
local typeTable
if (size == drlConsumeBig) then
typeTable = dbot.table.getCopy(inv.consume.table[typeName])
table.sort(typeTable, function (v1, v2) return v1.level > v2.level end)
elseif (size == drlConsumeSmall) then
typeTable = inv.consume.table[typeName]
else
dbot.warn("inv.consume.get: invalid size parameter")
return DRL_RET_INVALID_PARAM
end -- if
for _, entry in pairs(typeTable) do
if (entry.level <= curLevel) then
-- If we have one of these items available, return the ID for it. Otherwise, try the next entry
-- in the consumable table
local objInContainerId = nil
local finalId = nil
for objId, itemEntry in pairs(inv.items.table) do
local itemLevel = tonumber(inv.items.getStatField(objId, invStatFieldLevel) or "")
local itemName = inv.items.getStatField(objId, invStatFieldName) or ""
if (entry.level == itemLevel) and (entry.fullName == itemName) then
-- We try to use items from the preferred location first (e.g., a user-specified container or
-- the main inventory if no container is specified). If we find an item instance at the
-- preferred location, break immediately and use it. Otherwise, remember the item and keep
-- searching for something at the preferred location.
finalId = objId
if (inv.items.getField(objId, invFieldObjLoc) == preferredLocation) then
break
end -- if
end -- if
end -- for
-- If we found a matching item instance, return it!
if (finalId ~= nil) then
dbot.note("@WFound \"@C" .. typeName .. "@W\"@G L" .. entry.level .. " @W\"@Y" ..
(inv.items.getStatField(finalId, invStatFieldName) or "") ..
"@W\" @@ \"@R" .. (inv.items.getField(finalId, invFieldObjLoc) or "") .. "@W\"")
return finalId, DRL_RET_SUCCESS
end -- if
end -- if
end -- for
return 0, DRL_RET_MISSING_ENTRY
end -- inv.consume.get
drlConsumeBig = "big"
drlConsumeSmall = "small"
inv.consume.usePkg = nil
function inv.consume.use(typeName, size, numItems, containerName)
if (typeName == nil) or (typeName == "") then
dbot.warn("inv.consume.use: Missing type name")
return DRL_RET_INVALID_PARAM
end -- if
-- If the number of items isn't specified, use a single item as the default
numItems = tonumber(numItems or "")
if (numItems == nil) then
numItems = 1
end -- if
if (inv.consume.usePkg ~= nil) then
dbot.info("Skipping request to use \"" .. typeName .. "\": another request is in progress")
return DRL_RET_BUSY
end -- if
if (size ~= drlConsumeBig) and (size ~= drlConsumeSmall) then
dbot.warn("inv.consume.use: size must be either \"" .. drlConsumeBig .. "\" or \"" ..
drlConsumeSmall .. "\"")
return DRL_RET_INVALID_PARAM
end -- if
-- The containerName parameter is optional. If it is present, we use items from the
-- specified container before we items outside of that container.
containerName = containerName or ""
inv.consume.usePkg = {}
inv.consume.usePkg.numItems = numItems
inv.consume.usePkg.typeName = typeName
inv.consume.usePkg.size = size
inv.consume.usePkg.container = containerName
wait.make(inv.consume.useCR)
return DRL_RET_SUCCESS
end -- inv.consume.use
drlConsumeMaxConsecutiveItems = 4
function inv.consume.useCR()
local retval = DRL_RET_SUCCESS
local objId
if (inv.consume.usePkg == nil) or (inv.consume.usePkg.size == nil) or
(inv.consume.usePkg.numItems == nil) or (inv.consume.usePkg.typeName == nil) then
dbot.error("inv.consume.useCR: usePkg is nil or contains nil components")
return DRL_RET_INTERNAL_ERROR
end -- if
if (inv.consume.usePkg.numItems > drlConsumeMaxConsecutiveItems) then
dbot.note("Capping number of \"" .. inv.consume.usePkg.size .. "\" items to consume to " ..
drlConsumeMaxConsecutiveItems .. " in one burst")
inv.consume.usePkg.numItems = drlConsumeMaxConsecutiveItems
end -- if
-- If the user specified a preferred container, use items from that container first
local containerId = nil
if (inv.consume.usePkg.container ~= nil) and (inv.consume.usePkg.container ~= "") then
local idArray, retval = inv.items.searchCR("rname " .. inv.consume.usePkg.container)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.useCR: failed to search inventory table: " .. dbot.retval.getString(retval))
elseif (#idArray ~= 1) then
-- There should only be a single match to the container's relative name (e.g., "2.bag")
dbot.warn("Container relative name \"" .. inv.consume.usePkg.container ..
"\" did not have a unique match: no preferred container will be used for consume request")
else
-- We found a single unique match for the relative name
containerId = idArray[1]
end -- if
end -- if
local commandArray = {}
for i = 1, inv.consume.usePkg.numItems do
objId, retval = inv.consume.get(inv.consume.usePkg.typeName, inv.consume.usePkg.size, containerId)
if (objId ~= nil) and (retval == DRL_RET_SUCCESS) then
retval = inv.consume.useItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.useCR: Failed to consume item: " .. dbot.retval.getString(retval))
break
end -- if
end -- if
if (retval ~= DRL_RET_SUCCESS) then
break;
end -- if
end -- for
-- We use the "fast" mode instead of "safe" mode because we don't want the extra overhead when
-- consuming items. There's a good chance that you are in combat and stalling combat isn't a
-- great idea. The worst case scenario is that the user goes AFK or something silly in the
-- middle of consuming the items and we try to consume them anyway. It's not a huge issue.
if (commandArray ~= nil) then
if (#commandArray > 0) then
retval = dbot.execute.fast.commands(commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping request to consume items: " .. dbot.retval.getString(retval))
end -- if
else
dbot.note("Skipping request to consume items: no items matching the request were found")
end -- if
end -- if
-- Clean up
inv.consume.usePkg = nil
return retval
end -- inv.consume.useCR
function inv.consume.useItem(objId, commandArray)
local retval = DRL_RET_SUCCESS
local itemType = inv.items.getStatField(objId, invStatFieldType) or ""
local consumeCmd
if (itemType == "Potion") then
consumeCmd = "quaff"
elseif (itemType == "Pill") then
consumeCmd = "eat"
else
--TODO: we don't currently handle wands or staves here yet
dbot.warn("inv.consume.useItem: Unsupported item type \"" .. itemType .. "\"")
return DRL_RET_UNSUPPORTED
end -- if
-- If the item isn't already in the main inventory, get it so that we can consume it!
if (inv.items.getField(objId, invFieldObjLoc) ~= invItemLocInventory) then
retval = inv.items.getItem(objId, commandArray)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.consume.useItem: Failed to get item " .. objId .. ": " .. dbot.retval.getString(retval))
return retval
end -- if
end -- if
-- Consume the item and wait until we have confirmation the command executed
if (commandArray ~= nil) then
table.insert(commandArray, consumeCmd .. " " .. objId)
-- We don't want the next consume command to try to use the same item again and we also
-- don't want to wait for confirmation that we consumed it. We might be in combat and have
-- lots of commands queued up on the mud server so delaying everything until we know we
-- consumed the item would take more overhead than we are willing to have. If something goes
-- wrong (e.g., the user goes AFK unexpectedly) and we don't actually end up consuming the item
-- then we'll re-identify this item automagically on the next refresh so there isn't much harm done.
retval = inv.items.remove(objId)
end -- if
return retval
end -- inv.consume.useItem
----------------------------------------------------------------------------------------------------
--
-- Module to manage portals
--
-- dinv portal use portalId
--
-- inv.portal.use(portalId)
--
----------------------------------------------------------------------------------------------------
inv.portal = {}
function inv.portal.use(portalId)
local origId, origLoc
local portalWish = dbot.wish.has("Portal")
portalId = tonumber(portalId) or ""
if (portalId == nil) then
dbot.warn("inv.portal.use: Invalid portal ID parameter")
return DRL_RET_INVALID_PARAM
end -- if
-- If we have the portal wish, the new portal will go into the "portal" slot. If we do not
-- have the portal wish, we will use the "held" slot. This checks if anything is already at
-- the target location. If something is there, remember what it is so that we can put it
-- back when we are done.
for objId, objInfo in pairs(inv.items.table) do
local currentLoc = inv.items.getField(objId, invFieldObjLoc) or ""
-- Check if we are holding a portal in either the "held" slot or the "portal" slot. If we
-- have the portal wish, we have the portal slot; otherwise portals are in the held slot.
if ((currentLoc == inv.wearLoc[invWearableLocPortal]) and (portalWish == true)) or
((currentLoc == inv.wearLoc[invWearableLocHeld]) and (portalWish == false)) then
origLoc = currentLoc
origId = objId
break
end -- if
end -- for
-- If the new portal is already at the target location, enter it and return success. Easy peasy.
if (origId == portalId) then
return dbot.execute.fast.command("enter")
end -- if
-- Queue up several commands to use the portal and then send them to the mud in one burst. This
-- is a bit more efficient than sending them one at a time and waiting for the result.
local commands = {}
-- If something is at the target location, move it to your main inventory
if (origId ~= nil) then
table.insert(commands, "remove " .. origId)
end -- if
-- Find the target portal. If it is in a container, get it out of the container.
local objLoc = inv.items.getField(portalId, invFieldObjLoc) or ""
local objLocNum = tonumber(objLoc)
if (objLoc ~= invItemLocInventory) and (objLocNum ~= nil) then
table.insert(commands, "get " .. portalId .. " " .. objLocNum)
end -- if
-- We have the portal ready. Hold it and go whoosh.
table.insert(commands, "hold " .. portalId)
table.insert(commands, "enter")
table.insert(commands, "remove " .. portalId)
-- If we were holding something at the beginning, hold it again now
if (origId ~= nil) then
table.insert(commands, "wear " .. origId .. " " .. origLoc)
end -- if
-- Put the portal away if we pulled it out of a container to use it here
if (objLoc ~= invItemLocInventory) and (objLocNum ~= nil) then
table.insert(commands, "put " .. portalId .. " " .. objLoc)
end -- if
return dbot.execute.safe.commands(commands, nil, nil, nil, nil)
end -- inv.portal.use
----------------------------------------------------------------------------------------------------
--
-- Module to manage saveable passes
--
-- Some areas require certain items to be in your main inventory in order to move through the
-- area. These items, which we will refer to as "passes", are not keys and are saveable. For
-- example, the area "Giant's Pet Store" requires an employee ID card as part of the process to
-- get a set of keys. The card is on a mob and we cannot (without botting) automatically kill
-- the mob to get the ID card.
--
-- If a user has aquired a pass and stored it in a container, the "dinv pass" option provides
-- automatic access to the pass whenever desired. For example, a custom exit could call
-- "dinv pass 12345678 3" to automagically grab the pass item with object ID 12345678 and put
-- it in main inventory and then automagically put the pass item back into its home container
-- after 3 seconds.
--
-- dinv pass [pass id or name] [# seconds]
--
-- inv.pass.use(passNameOrId, useTimeSec)
--
----------------------------------------------------------------------------------------------------
inv.pass = {}
function inv.pass.use(passNameOrId, useTimeSec)
if (passNameOrId == nil) or (passNameOrId == "") then
dbot.warn("inv.pass.use: Missing pass name")
return DRL_RET_INVALID_PARAM
end -- if
useTimeSec = tonumber(useTimeSec or "")
if (useTimeSec == nil) then
dbot.warn("inv.pass.use: useTimeSec parameter is not a number")
return DRL_RET_INVALID_PARAM
end -- if
-- Find anything that has an objectId or name matching the "passNameOrId" parameter and put it
-- in main inventory
local retval = inv.items.get("id " .. passNameOrId .. " || name " .. passNameOrId)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("inv.pass.use: Failed to get pass \"" .. passNameOrId .. "\": " ..
dbot.retval.getString(retval))
return retval
end -- if
-- Schedule a timer to put the pass away after the specified amount of time. The inv.items.store()
-- function puts each specified item into the container that was most recently used to hold the item.
local storeCommand = "inv.items.store(\"" .. "id " .. passNameOrId .. " || name " .. passNameOrId .. "\")"
check (DoAfterSpecial(useTimeSec, storeCommand, sendto.script))
return retval
end -- inv.pass.use
--[[
Durel's Bag-of-Tricks (dbot) Layout
-----------------------------------
dbot init : Init / de-init code for the dbot package
dbot generic : Top-level generic utility functions (e.g., custom version of tonumber, etc.)
dbot.retval : Return values / error codes
dbot.table : Convenient functions to manage table accesses
dbot.notify : Message notification sub-system
dbot.gmcp : Character and world state access functions via the GMCP protocol
dbot.storage : Save and load data using persistent storage
dbot.backup : System to support backing up and restoring all plugin state
dbot.emptyLine: Module to allow suppression of empty output lines
dbot.prompt : Module to transparently enable or disable the prompt and related output
dbot.invmon : Module to check if invmon is enabled
dbot.ability : Module to track if a character has access to a particular skill or spell
dbot.wish : Module to track which wishes a character has purchased
dbot.pagesize : Module to determine a character's current page size (# lines before page prompt)
dbot.execute : Execute one or more commands without contention from user-entered commands
dbot.callback : Module to help manage callback functions and parameters
dbot.remote : Module to retrieve remote files
dbot.version : Module to track version and changelog information and update the plugin
--]]
----------------------------------------------------------------------------------------------------
-- Base module
----------------------------------------------------------------------------------------------------
dbot = {}
----------------------------------------------------------------------------------------------------
-- Init / de-init routines for the dbot package
--
-- Some modules should be initialized at "install" time. Others should be initialized only once
-- the user is at the "active" state after a login.
--
-- Triggers, timers, and aliases most likely should be initialized at "install" time.
--
-- Loading state most likely should be done at "active" time (because we need the username to get
-- the correct state and the username isn't available at install time)
--
-- dbot.init.atInstall()
-- dbot.init.atActive()
-- dbot.fini(doSaveState)
--
----------------------------------------------------------------------------------------------------
dbot.init = {}
dbot.init.initializedInstall = false
dbot.init.initializedActive = false
-- storage should be first (to create state directories) and gmcp should be last (so we can save data)
dbot.modules = "storage emptyLine backup notify ability prompt invmon wish execute pagesize gmcp"
function dbot.init.atInstall()
local retval = DRL_RET_SUCCESS
-- Loop through all of the dbot modules that need to be initialized at "install" time and then call
-- the init functions of those modules
for module in dbot.modules:gmatch("%S+") do
if (dbot[module].init.atInstall ~= nil) then
local initVal = dbot[module].init.atInstall()
if (initVal ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.init.atInstall: Failed to initialize \"at install\" dbot." .. module .. " module: " ..
dbot.retval.getString(initVal))
retval = initVal
else
dbot.debug("Initialized \"at install\" module dbot." .. module)
end -- if
end -- if
end -- for
-- Return success or the most recently encountered init error
return retval
end -- dbot.init.atInstall
function dbot.init.atActive()
local retval = DRL_RET_SUCCESS
-- Loop through all of the dbot modules that need to be initialized when the user is at the "active"
-- state and call those modules' init functions
for module in dbot.modules:gmatch("%S+") do
if (dbot[module].init.atActive ~= nil) then
local initVal = dbot[module].init.atActive()
if (initVal ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.init.atActive: Failed to initialize \"at active\" dbot." .. module .. " module: " ..
dbot.retval.getString(initVal))
retval = initVal
else
dbot.debug("Initialized \"at active\" module dbot." .. module)
end -- if
end -- if
end -- for
-- Return success or the most recently encountered init error
return retval
end -- dbot.init.atActive
function dbot.fini(doSaveState)
local retval = DRL_RET_SUCCESS
-- Loop through all of the dbot modules and call their de-init functions
for module in dbot.modules:gmatch("%S+") do
if (dbot[module].fini ~= nil) then
local initVal = dbot[module].fini(doSaveState)
if (initVal ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.fini: Failed to de-initialize dbot." .. module .. " module: " ..
dbot.retval.getString(initVal))
retval = initVal
else
dbot.debug("De-initialized dbot module \"" .. module .. "\"")
end -- if
dbot.debug("De-initialized module dbot." .. module)
end -- if
end -- for
dbot.init.initializedInstall = false
dbot.init.initializedActive = false
return retval
end -- dbot.fini
----------------------------------------------------------------------------------------------------
-- dbot.print: Basic print function that supports color codes
--
-- This accepts strings that include aard, xterm, and ANSI color codes
--
-- Examples:
-- "@Wthis is white"
-- DRL_ANSI_RED .. "this is red"
-- DRL_XTERM_YELLOW .. "this is yellow"
----------------------------------------------------------------------------------------------------
function dbot.print(string)
-- Only print the string to the output if we are not in the middle of writing a note. If GMCP
-- isn't initialized yet, assume that we aren't writing a note and display the message.
if (dbot.gmcp.isInitialized == false) or (dbot.gmcp.getState() ~= dbot.stateNote) then
AnsiNote(stylesToANSI(ColoursToStyles(string)))
end -- if
end -- dbot.print
----------------------------------------------------------------------------------------------------
-- dbot.getTime returns the native time in seconds
----------------------------------------------------------------------------------------------------
function dbot.getTime()
return tonumber(os.time()) or 0
end -- dbot.getTime
----------------------------------------------------------------------------------------------------
-- dbot.reload: Reloads the current plugin
--
-- Note: This code was derived from part of a plugin by Arcidayne. Thanks Arcidayne!
----------------------------------------------------------------------------------------------------
function dbot.reload()
local scriptPrefix = GetAlphaOption("script_prefix")
local retval
-- If the user has not already specified the script prefix for this version of mush, pick a
-- reasonable default value
if (scriptPrefix == "") then
scriptPrefix = "\\\\\\"
SetAlphaOption("script_prefix", scriptPrefix)
end
-- Tell mush to reload the plugin in one second. We can't do it directly here because a
-- plugin can't unload itself. Even if it could, how could it tell mush to load it again
-- if it weren't installed?
retval = Execute(scriptPrefix.."DoAfterSpecial(1, \"ReloadPlugin('"..GetPluginID().."')\", sendto.script)")
if (retval ~= 0) then
dbot.warn("dbot.reload: Failed to reload the plugin: mush error " .. retval)
retval = DRL_RET_INTERNAL_ERROR
end -- if
return retval
end -- dbot.reload
----------------------------------------------------------------------------------------------------
-- dbot.shell: Run a shell command in the background without pulling up a command prompt window
----------------------------------------------------------------------------------------------------
function dbot.shell(shellCommand)
local retval = DRL_RET_SUCCESS
local mushRetval
if (shellCommand == nil) or (shellCommand == "") then
dbot.warn("dbot.shell: Missing shell command")
return DRL_RET_INVALID_PARAMETER
end -- if
dbot.debug("dbot.shell: Executing \"@G" .. "/C " .. shellCommand .. "@W\"")
local ok, error = utils.shellexecute("cmd", "/C " .. shellCommand, GetInfo(64), "open", 0)
if (not ok) then
dbot.warn("dbot.shell: Command \"@G" .. shellCommand .. "@W\" failed")
retval = DRL_INTERNAL_ERROR
end -- if
return retval
end -- dbot.shell
----------------------------------------------------------------------------------------------------
-- dbot.fileExists: Returns true if the specified file (or directory) exists and false otherwise
----------------------------------------------------------------------------------------------------
function dbot.fileExists(fileName)
if (fileName == nil) or (fileName == "") then
return false
end -- if
local dirQuery = string.gsub(string.gsub(fileName, "\\", "/"), "/$", "")
local dirTable, error = utils.readdir(dirQuery)
if (dirTable == nil) then
return false
else
--tprint(dirTable)
return true
end -- if
end -- dbot.fileExists
----------------------------------------------------------------------------------------------------
-- dbot.spinUntilExists: Spin in a sleep-loop waiting for the specified file to be created
----------------------------------------------------------------------------------------------------
function dbot.spinUntilExists(fileName, timeoutSec)
local totTime = 0
-- Wait until either we detect that the file exists or until we time out
while (not dbot.fileExists(fileName)) do
if (totTime > timeoutSec) then
dbot.warn("dbot.spinUntilExists: Timed out waiting for creation of \"@G" .. fileName .. "@W\"")
return DRL_RET_TIMEOUT
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
return DRL_RET_SUCCESS
end -- dbot.spinUntilExists
----------------------------------------------------------------------------------------------------
-- dbot.spinWhileExists: Spin in a sleep-loop waiting for the specified file to be deleted
----------------------------------------------------------------------------------------------------
function dbot.spinWhileExists(fileName, timeoutSec)
local totTime = 0
-- Wait until either we detect that the file does not exist or until we time out
while (dbot.fileExists(fileName)) do
if (totTime > timeoutSec) then
dbot.warn("dbot.spinWhileExists: Timed out waiting for deletion of \"@G" .. fileName .. "@W\"")
return DRL_RET_TIMEOUT
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
return DRL_RET_SUCCESS
end -- dbot.spinWhileExists
----------------------------------------------------------------------------------------------------
-- dbot.spinUntilExistsBusy: Spin in a busy-loop waiting for the specified file to be created
--
-- This is identical to dbot.spinUntilExists() but it uses a busy loop instead of
-- scheduling a wait. A busy loop is less efficient, but you have the option of
-- using it outside of a co-routine and that comes in handy in certain circumstances.
----------------------------------------------------------------------------------------------------
function dbot.spinUntilExistsBusy(fileName, timeoutSec)
local startTime = dbot.getTime()
-- Wait until either we detect that the file exists or until we time out
while (not dbot.fileExists(fileName)) do
-- We time out if we have been in a busy loop for over timeoutSec seconds. This
-- only has a resolution of 1 second so it's possible that we may timeout up to
-- 1 second later than the user requested. I'd rather take a chance of timing
-- out a little late than timing out a little early.
if (dbot.getTime() - startTime > timeoutSec) then
dbot.warn("dbot.spinUntilExists: Timed out waiting for creation of \"@G" .. fileName .. "@W\"")
return DRL_RET_TIMEOUT
end -- if
end -- while
return DRL_RET_SUCCESS
end -- dbot.spinUntilExistsBusy
----------------------------------------------------------------------------------------------------
-- dbot.spinWhileExistsBusy: Spin in a busy-loop waiting for the specified file to be deleted
--
-- This is identical to dbot.spinWhileExists() but it uses a busy loop instead of
-- scheduling a wait. A busy loop is less efficient, but you have the option of
-- using it outside of a co-routine and that comes in handy in certain circumstances.
----------------------------------------------------------------------------------------------------
function dbot.spinWhileExistsBusy(fileName, timeoutSec)
local startTime = dbot.getTime()
-- Wait until either we detect that the file is removed or until we time out
while (dbot.fileExists(fileName)) do
-- We time out if we have been in a busy loop for over timeoutSec seconds. This
-- only has a resolution of 1 second so it's possible that we may timeout up to
-- 1 second later than the user requested. I'd rather take a chance of timing
-- out a little late than timing out a little early.
if (dbot.getTime() - startTime > timeoutSec) then
dbot.warn("dbot.spinWhileExists: Timed out waiting for deletion of \"@G" .. fileName .. "@W\"")
return DRL_RET_TIMEOUT
end -- if
end -- while
return DRL_RET_SUCCESS
end -- dbot.spinWhileExistsBusy
----------------------------------------------------------------------------------------------------
-- dbot.tonumber: version of tonumber that strips out commas from a number
----------------------------------------------------------------------------------------------------
function dbot.tonumber(numString)
noCommas = string.gsub(numString, ",", "")
return tonumber(noCommas)
end -- dbot.tonumber
----------------------------------------------------------------------------------------------------
-- dbot.isWordInString: Returns boolean indicating if the word (separated by spaces) in in the
-- specified string
----------------------------------------------------------------------------------------------------
function dbot.isWordInString(word, field)
if (word == nil) or (word == "") or (field == nil) or (field == "") then
return false
end -- if
for element in field:gmatch("%S+") do
if (string.lower(word) == string.lower(element)) then
return true
end -- if
end -- for
return false
end -- dbot.isWordInString
----------------------------------------------------------------------------------------------------
-- dbot.wordsToArray: converts a string into an array of individual white-space separated words
--
-- Returns array, retval
----------------------------------------------------------------------------------------------------
function dbot.wordsToArray(myString)
local wordTable = {}
if (myString == nil) then
dbot.warn("dbot.wordsToArray: Missing string parameter")
return wordTable, DRL_RET_INVALID_PARAM
end -- if
for word in string.gmatch(myString, "%S+") do
table.insert(wordTable, word)
end -- for
return wordTable, DRL_RET_SUCCESS
end -- dbot.wordsToArray
----------------------------------------------------------------------------------------------------
-- dbot.mergeFields: Returns a string containing all of the unique words in the two input parameters
--
-- For example: merging "hello world" and "goodbye world" would yield "hello world goodbye"
----------------------------------------------------------------------------------------------------
function dbot.mergeFields(field1, field2)
local mergedField = field1 or ""
if (field2 ~= nil) and (field2 ~= "") then
for word in field2:gmatch("%S+") do
if (not dbot.isWordInString(word, field1)) then
mergedField = mergedField .. " " .. word
end -- if
end -- for
end -- if
return mergedField
end -- dbot.mergeFields
----------------------------------------------------------------------------------------------------
-- dbot.deleteTrigger: Wrapper around DeleteTrigger that checks the mush error codes
----------------------------------------------------------------------------------------------------
function dbot.deleteTrigger(name)
local retval = DRL_RET_SUCCESS
if (name == nil) or (name == "") then
dbot.warn("dbot.deleteTrigger: Attempted to delete a trigger missing a name")
return DRL_RET_INVALID_PARAM
end -- if
local mushRetval = IsTrigger(name)
if (mushRetval == error_code.eOK) then
check (DeleteTrigger(name))
retval = DRL_RET_SUCCESS
elseif (mushRetval == error_code.eTriggerNotFound) then
-- We don't consider it an error if we try to delete a trigger that isn't instantiated. Our
-- de-init code tries to whack all triggers without checking if they exist or not.
retval = DRL_RET_SUCCESS
elseif (mushRetval == error_code.eInvalidObjectLabel) then
dbot.warn("dbot.deleteTrigger: Failed to delete trigger: trigger name \"" .. name ..
"\" isn't a valid label")
retval = DRL_RET_INVALID_PARAM
else
dbot.warn("dbot.deleteTrigger: Detected unknown error code " .. mushRetval)
retval = DRL_RET_INTERNAL_ERROR
end -- if
return retval
end -- dbot.deleteTrigger
----------------------------------------------------------------------------------------------------
-- dbot.deleteTimer: Wrapper around DeleteTimer that checks the mush error codes
----------------------------------------------------------------------------------------------------
function dbot.deleteTimer(name)
local retval = DRL_RET_SUCCESS
if (name == nil) or (name == "") then
dbot.warn("dbot.deleteTimer: Attempted to delete a timer missing a name")
return DRL_RET_INVALID_PARAM
end -- if
local mushRetval = IsTimer(name)
if (mushRetval == error_code.eOK) then
DeleteTimer(name)
retval = DRL_RET_SUCCESS
elseif (mushRetval == error_code.eTimerNotFound) then
-- We don't consider it an error if we try to delete a timer that isn't instantiated. Our
-- de-init code tries to whack all timers without checking if they exist or not.
retval = DRL_RET_SUCCESS
elseif (mushRetval == error_code.eInvalidObjectLabel) then
dbot.warn("dbot.deleteTimer: Failed to delete timer: timer name \"" .. name .. "\" isn't a valid label")
retval = DRL_RET_INVALID_PARAM
else
dbot.warn("dbot.deleteTimer: Detected unknown error code " .. mushRetval)
retval = DRL_RET_INTERNAL_ERROR
end -- if
return retval
end -- dbot.deleteTimer
----------------------------------------------------------------------------------------------------
-- dbot.normalizeMobName strips out prefix articles from a mob's name
----------------------------------------------------------------------------------------------------
function dbot.normalizeMobName(fullMobName)
mobName = fullMobName
mobName = mobName:gsub("^a (.-)$", "%1")
mobName = mobName:gsub("^A (.-)$", "%1")
mobName = mobName:gsub("^an (.-)$", "%1")
mobName = mobName:gsub("^An (.-)$", "%1")
mobName = mobName:gsub("^the (.-)$", "%1")
mobName = mobName:gsub("^The (.-)$", "%1")
mobName = mobName:gsub("^some (.-)$", "%1")
mobName = mobName:gsub("^Some (.-)$", "%1")
-- These are convenient hacks for common mobs
mobName = mobName:gsub("^.*soul of (.-)$", "%1") -- Silver Volcano
mobName = mobName:gsub("^(.-) sea snake$", "%1") -- Woobleville
return mobName
end -- normalizeMobName
----------------------------------------------------------------------------------------------------
-- dbot.retval: Return values (AKA error codes)
--
-- Functions:
-- dbot.retval.getString(retval)
--
-- Data:
-- dbot.retval.table -- this is an in-memory static table that is not saved to persistent storage
----------------------------------------------------------------------------------------------------
DRL_RET_SUCCESS = 0
DRL_RET_UNINITIALIZED = -1
DRL_RET_INVALID_PARAM = -2
DRL_RET_MISSING_ENTRY = -3
DRL_RET_BUSY = -4
DRL_RET_UNSUPPORTED = -5
DRL_RET_TIMEOUT = -6
DRL_RET_HALTED = -7
DRL_RET_INTERNAL_ERROR = -8
DRL_RET_UNIDENTIFIED = -9
DRL_RET_NOT_ACTIVE = -10
DRL_RET_IN_COMBAT = -11
DRL_RET_VER_MISMATCH = -12
dbot.retval = {}
dbot.retval.table = {}
dbot.retval.table[DRL_RET_SUCCESS] = "success"
dbot.retval.table[DRL_RET_UNINITIALIZED] = "component is not initialized"
dbot.retval.table[DRL_RET_INVALID_PARAM] = "invalid parameter"
dbot.retval.table[DRL_RET_MISSING_ENTRY] = "missing entry"
dbot.retval.table[DRL_RET_BUSY] = "resource is in use"
dbot.retval.table[DRL_RET_UNSUPPORTED] = "unsupported feature"
dbot.retval.table[DRL_RET_TIMEOUT] = "timeout"
dbot.retval.table[DRL_RET_HALTED] = "component is halted"
dbot.retval.table[DRL_RET_INTERNAL_ERROR] = "internal error"
dbot.retval.table[DRL_RET_UNIDENTIFIED] = "item is not yet identified"
dbot.retval.table[DRL_RET_NOT_ACTIVE] = "you are not in the active state"
dbot.retval.table[DRL_RET_IN_COMBAT] = "you are in combat!"
dbot.retval.table[DRL_RET_VER_MISMATCH] = "version mismatch"
function dbot.retval.getString(retval)
local string = dbot.retval.table[retval]
if (string == nil) then
string = "Unknown return value"
end -- if
return string
end -- dbot.retval.getString
----------------------------------------------------------------------------------------------------
-- dbot.table.getCopy(origTable)
-- Returns a copy of the original table
-- Derived from: http://lua-users.org/wiki/CopyTable
----------------------------------------------------------------------------------------------------
dbot.table = {}
function dbot.table.getCopy(origItem)
local newItem
if type(origItem) == 'table' then
newItem = {}
for origKey, origValue in next, origItem, nil do
newItem[dbot.table.getCopy(origKey)] = dbot.table.getCopy(origValue)
end -- for
setmetatable(newItem, dbot.table.getCopy(getmetatable(origItem)))
else
newItem = origItem
end -- if
return newItem
end -- dbot.table.getCopy
----------------------------------------------------------------------------------------------------
-- We can't use #someTable to get the number of entries in it like we can do for an array.
-- This function counts the number of entries in a table and returns the count.
----------------------------------------------------------------------------------------------------
function dbot.table.getNumEntries(theTable)
local numEntries = 0
if (theTable ~= nil) then
for k,v in pairs(theTable) do
numEntries = numEntries + 1
end -- for
end -- if
return numEntries
end -- dbot.table.getNumEntries
----------------------------------------------------------------------------------------------------
-- Baseline trigger flags
----------------------------------------------------------------------------------------------------
drlTriggerFlagsBaseline = trigger_flag.Enabled + trigger_flag.RegularExpression + trigger_flag.Replace
----------------------------------------------------------------------------------------------------
-- Spin-loops are common. This is the default time period to sleep after each loop.
----------------------------------------------------------------------------------------------------
drlSpinnerPeriodDefault = 0.1
----------------------------------------------------------------------------------------------------
-- dbot: Color code definitions
----------------------------------------------------------------------------------------------------
DRL_COLOR_GREEN = "46"
DRL_COLOR_RED = "160"
DRL_COLOR_YELLOW = "226"
DRL_COLOR_WHITE = "231"
DRL_COLOR_GREY = "255"
DRL_XTERM_GREEN = "@x" .. DRL_COLOR_GREEN
DRL_XTERM_RED = "@x" .. DRL_COLOR_RED
DRL_XTERM_YELLOW = "@x" .. DRL_COLOR_YELLOW
DRL_XTERM_WHITE = "@x" .. DRL_COLOR_WHITE
DRL_XTERM_GREY = "@x" .. DRL_COLOR_GREY
-- Older versions of mush severely broke color codes. This gives a work-around for those versions.
drlMushClientVersion = tonumber(Version() or "")
if (drlMushClientVersion ~= nil) and (drlMushClientVersion < 5.06) then
DRL_ANSI_GREEN = ANSI(DRL_COLOR_GREEN)
DRL_ANSI_RED = ANSI(DRL_COLOR_RED)
DRL_ANSI_YELLOW = ANSI(DRL_COLOR_YELLOW)
DRL_ANSI_WHITE = ANSI(DRL_COLOR_WHITE)
else
-- TODO: Yes, these aren't really ANSI color codes but it make the rest of the code compatible
-- with the ANSI work-arounds. At some point, I'd love to stop supporting old 4.x
-- mush builds and then we could remove all of the ANSI color references in this plugin.
DRL_ANSI_GREEN = "@G"
DRL_ANSI_RED = "@R"
DRL_ANSI_YELLOW = "@Y"
DRL_ANSI_WHITE = "@W"
end -- if
----------------------------------------------------------------------------------------------------
-- Plugin info fields used by GetPluginInfo()
----------------------------------------------------------------------------------------------------
dbot.pluginInfo = {}
dbot.pluginInfo.dir = 20
-- TODO: add other pluginInfo fields
----------------------------------------------------------------------------------------------------
-- Notification Module
--
-- This provides wrapper functions to print various notification messages. If we are in "Note"
-- mode (i.e., we are writing a note) we suppress all of these notifications. Otherwise, we
-- print all warnings and errors and any debug, note, and info messages that are above the
-- user-supplied threshold. This lets a user change the verbosity of the plugin at runtime. If
-- a problem is happening, they could enable everything including debug messages. Once they are
-- comfortable with the plugin, they could suppress lower-priority messages and only leave the
-- highest priority notifications enabled. If they are particularly brave, they could disable
-- all optional messages and only leave warnings and errors on.
--
-- dbot.notify.init.atActive()
-- dbot.notify.fini(doSaveState)
--
-- dbot.notify.save()
-- dbot.notify.load()
-- dbot.notify.reset()
--
-- dbot.notify.msg
-- dbot.notify.getLevel
-- dbot.notify.setLevel(value, endTag, isVerbose)
--
-- dbot.debug
-- dbot.note
-- dbot.info
-- dbot.warn
-- dbot.error
--
----------------------------------------------------------------------------------------------------
dbot.notify = {}
dbot.notify.init = {}
dbot.notify.prefix = pluginNameAbbr
dbot.notify.name = "dbot-notify.state"
drlDbotNotifyUserLevelNone = "none"
drlDbotNotifyUserLevelLight = "light"
drlDbotNotifyUserLevelStandard = "standard"
drlDbotNotifyUserLevelAll = "all"
notifyLevelDefault = drlDbotNotifyUserLevelStandard
notifyLevelDebug = "DEBUG"
notifyLevelNote = "NOTE"
notifyLevelInfo = "INFO"
notifyLevelWarn = "WARN"
notifyLevelError = "ERROR"
dbot.notify.level = {}
dbot.notify.level[notifyLevelDebug] = { enabled = false, bg = "black", fg = "orange" }
dbot.notify.level[notifyLevelNote] = { enabled = true, bg = "white", fg = "green" }
dbot.notify.level[notifyLevelInfo] = { enabled = true, bg = "white", fg = "blue" }
dbot.notify.level[notifyLevelWarn] = { enabled = true, bg = "black", fg = "yellow" }
dbot.notify.level[notifyLevelError] = { enabled = true, bg = "white", fg = "red" }
function dbot.notify.init.atActive()
local retval
retval = dbot.notify.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.notify.init.atActive: Failed to load notify data from storage: " ..
dbot.retval.getString(retval))
end -- if
retval = dbot.notify.setLevel(dbot.notify.table.notifyLevel, nil, false)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.notify.init.atActive: Failed to set notify level from storage: " ..
dbot.retval.getString(retval))
end -- if
return retval
end -- dbot.notify.init.atActive
function dbot.notify.fini(doSaveState)
local retval = DRL_RET_SUCCESS
if (doSaveState) then
retval = dbot.notify.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.notify.fini: Failed to save notify data to storage: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- dbot.notify.fini
function dbot.notify.save()
if (dbot.notify.table == nil) then
return dbot.notify.reset()
end -- if
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. dbot.notify.name,
"dbot.notify.table", dbot.notify.table, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.notify.save: Failed to save notify table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- dbot.notify.save
function dbot.notify.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. dbot.notify.name, dbot.notify.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.notify.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. dbot.notify.name .. "@W\": " .. dbot.retval.getString(retval))
end -- if
return retval
end -- dbot.notify.load
function dbot.notify.reset()
dbot.notify.table = { notifyLevel = notifyLevelDefault }
-- We handle saving dbot.module state a little differently than other modules. Most modules can
-- check if dbot.init.initializeActive is true before saving state, but dbot modules don't have that
-- luxury because we may need to reset something before we are fully initialized. Instead, we use
-- the "doForceSave" parameter of dbot.storage.saveTable() to explicitly force saving state on a
-- reset. We don't use that parameter on a normal dbot.notify.save() call.
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. dbot.notify.name,
"dbot.notify.table", dbot.notify.table, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.notify.reset: Failed to save notification data: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- dbot.notify.reset
function dbot.notify.setLevel(value, endTag, isVerbose)
if (value == drlDbotNotifyUserLevelNone) then
dbot.notify.level[notifyLevelDebug].enabled = false
dbot.notify.level[notifyLevelNote].enabled = false
dbot.notify.level[notifyLevelInfo].enabled = false
elseif (value == drlDbotNotifyUserLevelLight) then
dbot.notify.level[notifyLevelDebug].enabled = false
dbot.notify.level[notifyLevelNote].enabled = false
dbot.notify.level[notifyLevelInfo].enabled = true
elseif (value == drlDbotNotifyUserLevelStandard) then
dbot.notify.level[notifyLevelDebug].enabled = false
dbot.notify.level[notifyLevelNote].enabled = true
dbot.notify.level[notifyLevelInfo].enabled = true
elseif (value == drlDbotNotifyUserLevelAll) then
dbot.notify.level[notifyLevelDebug].enabled = true
dbot.notify.level[notifyLevelNote].enabled = true
dbot.notify.level[notifyLevelInfo].enabled = true
else
dbot.warn("dbot.notify.setLevel: invalid value parameter")
return inv.tags.stop(invTagsNotify, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (isVerbose) then
dbot.info("Set notification level to \"@C" .. value .. "@W\"")
end -- if
dbot.notify.table.notifyLevel = value
dbot.notify.save()
return inv.tags.stop(invTagsNotify, endTag, DRL_RET_SUCCESS)
end -- dbot.notify.setLevel
function dbot.notify.getLevel()
return dbot.notify.table.notifyLevel
end -- dbot.notify.getLevel
function dbot.notify.msg(level, msg)
if (level == nil) or (level == "") then
dbot.warn("dbot.notify.msg: missing level")
return DRL_RET_INVALID_PARAM
end -- if
if (dbot.notify.level[level] == nil) then
dbot.warn("dbot.notify.msg: level \"" .. level .. "\" is not supported")
return DRL_RET_UNSUPPORTED
end -- if
msg = msg or ""
if (dbot.notify.level[level].enabled) then
-- Suppress messages if we are writing a note
if dbot.gmcp.isInitialized and (dbot.gmcp.getState() == dbot.stateNote) then
return DRL_RET_BUSY
end -- if
ColourTell(dbot.notify.level[level].bg, dbot.notify.level[level].fg, dbot.notify.prefix)
dbot.print("@W " .. msg .. "@w")
end -- if
return DRL_RET_SUCCESS
end -- dbot.notify.msg
function dbot.debug(msg)
return dbot.notify.msg(notifyLevelDebug, msg)
end -- dbot.debug
function dbot.note(msg)
return dbot.notify.msg(notifyLevelNote, msg)
end -- dbot.note
function dbot.info(msg)
return dbot.notify.msg(notifyLevelInfo, msg)
end -- dbot.info
function dbot.warn(msg)
return dbot.notify.msg(notifyLevelWarn, msg)
end -- dbot.warn
function dbot.error(msg)
return dbot.notify.msg(notifyLevelError, msg)
end -- dbot.error
----------------------------------------------------------------------------------------------------
--
-- Module to access live data via the GMCP protocol
--
-- dbot.gmcp.init.atActive
-- dbot.gmcp.fini
--
-- dbot.gmcp.getState
-- dbot.gmcp.getStateString
--
-- dbot.gmcp.getArea
-- dbot.gmcp.getClass
-- dbot.gmcp.getLevel
-- dbot.gmcp.getAlign
-- dbot.gmcp.getRoomId
-- dbot.gmcp.getTier
--
-- dbot.gmcp.isGood
-- dbot.gmcp.isNeutral
-- dbot.gmcp.isEvil
--
-- dbot.gmcp.statePreventsActions()
-- dbot.gmcp.stateIsInCombat
-- dbot.gmcp.stateIsActive
--
----------------------------------------------------------------------------------------------------
dbot.gmcp = {}
dbot.gmcp.init = {}
dbot.gmcp.isInitialized = false -- initialized when OnPluginBroadcast detects a GMCP message
dbot.stateLogin = "1"
dbot.stateMOTD = "2"
dbot.stateActive = "3"
dbot.stateAFK = "4"
dbot.stateNote = "5"
dbot.stateBuilding = "6"
dbot.statePaged = "7"
dbot.stateCombat = "8"
dbot.stateSleeping = "9"
dbot.stateTBD = "10" -- not defined in the docs
dbot.stateResting = "11"
dbot.stateRunning = "12"
dbot.stateNames = {}
dbot.stateNames[dbot.stateLogin] = "Login"
dbot.stateNames[dbot.stateMOTD] = "MOTD"
dbot.stateNames[dbot.stateActive] = "Active"
dbot.stateNames[dbot.stateAFK] = "AFK"
dbot.stateNames[dbot.stateNote] = "Note"
dbot.stateNames[dbot.stateBuilding] = "Building"
dbot.stateNames[dbot.statePaged] = "Paged"
dbot.stateNames[dbot.stateCombat] = "Combat"
dbot.stateNames[dbot.stateSleeping] = "Sleeping"
dbot.stateNames[dbot.stateTBD] = "Uninitialized"
dbot.stateNames[dbot.stateResting] = "Resting"
dbot.stateNames[dbot.stateRunning] = "Running"
function dbot.gmcp.init.atActive()
local retval = DRL_RET_SUCCESS
-- Placeholder: nothing to do for now...
return retval
end -- dbot.gmcp.init.atActive
function dbot.gmcp.fini(doSaveState)
local retval = DRL_RET_SUCCESS
dbot.gmcp.isInitialized = false
-- Note: we don't use doSaveState yet because this module doesn't have state to save
return retval
end -- dbot.gmcp.fini
function dbot.gmcp.getState()
local charStatus
if dbot.gmcp.isInitialized then
charStatus = gmcp("char.status")
else
dbot.debug("dbot.gmcp.getState: GMCP is not initialized")
end -- if
if (charStatus == nil) then
return dbot.stateTBD
else
-- dbot.showStateString(charStatus.state)
return charStatus.state
end -- if
end -- dbot.gmcp.getState
function dbot.gmcp.getStateString(state)
return (dbot.stateNames[state] or "Unknown")
end -- dbot.gmcp.getStateString
function dbot.gmcp.getArea()
local roomInfo
local area
if dbot.gmcp.isInitialized then
roomInfo = gmcp("room.info")
area = roomInfo.zone
else
dbot.note("dbot.gmcp.getArea: GMCP is not initialized")
end -- if
dbot.debug("dbot.gmcp.getArea returns " .. (area or "nil"))
return area
end -- dbot.gmcp.getArea
function dbot.gmcp.getClass()
local char, class, subclass
if dbot.gmcp.isInitialized then
char = gmcp("char.base")
class = char.class
subclass = char.subclass
else
dbot.note("dbot.gmcp.getClass: GMCP is not initialized")
end -- if
return class, subclass
end -- dbot.gmcp.getClass
dbot.gmcp.charName = "unknown"
dbot.gmcp.charPretitle = "unknown"
function dbot.gmcp.getName()
local char
if dbot.gmcp.isInitialized then
char = gmcp("char.base")
dbot.gmcp.charName = char.name
dbot.gmcp.charPretitle = char.pretitle
else
dbot.debug("dbot.gmcp.getName: GMCP is not initialized")
end -- if
return dbot.gmcp.charName, dbot.gmcp.charPretitle
end -- dbot.gmcp.getName
function dbot.gmcp.getLevel()
local charStatus, myLevel
if dbot.gmcp.isInitialized then
charStatus = gmcp("char.status")
myLevel = tonumber(charStatus.level) + (dbot.gmcp.getTier() * 10)
else
dbot.note("dbot.gmcp.getLevel: GMCP is not initialized")
myLevel = 1
end -- if
dbot.debug("dbot.gmcp.getLevel returns " .. myLevel)
return myLevel
end -- dbot.gmcp.getLevel
function dbot.gmcp.getAlign()
local charStatus, myAlign
if dbot.gmcp.isInitialized then
charStatus = gmcp("char.status")
myAlign = tonumber(charStatus.align)
else
dbot.note("dbot.gmcp.getAlign: GMCP is not initialized")
myAlign = 0
end -- if
return myAlign
end -- dbot.gmcp.getAlign
function dbot.gmcp.getRoomId()
local roomInfo, roomId
if dbot.gmcp.isInitialized then
roomInfo = gmcp("room.info")
roomId = roomInfo.num
else
dbot.note("dbot.gmcp.getRoomId: GMCP is not initialized")
roomId = 0
end -- if
dbot.debug("dbot.gmcp.getRoomId returns " .. roomId)
return roomId
end -- dbot.gmcp.getRoomId
function dbot.gmcp.getTier()
local charBase, myTier
if dbot.gmcp.isInitialized then
charBase = gmcp("char.base")
myTier = tonumber(charBase.tier)
else
dbot.note("dbot.gmcp.getTier: GMCP is not initialized")
myTier = 0
end -- if
dbot.debug("dbot.gmcp.getTier returns " .. myTier)
return myTier
end -- dbot.gmcp.getTier
function dbot.gmcp.isGood()
local align = dbot.gmcp.getAlign()
if (align >= 875) then
return true
else
return false
end -- if
end -- dbot.gmcp.isGood
function dbot.gmcp.isNeutral()
local align = dbot.gmcp.getAlign()
if (align >= -874) and (align <= 874) then
return true
else
return false
end -- if
end -- dbot.gmcp.isNeutral
function dbot.gmcp.isEvil()
local align = dbot.gmcp.getAlign()
if (align <= -875) then
return true
else
return false
end -- if
end -- dbot.gmcp.isEvil
-- We can perform actions in the "active" and "combat" states. Any other state has the potential
-- to prevent us from performing an action.
function dbot.gmcp.statePreventsActions()
local state = dbot.gmcp.getState() or "Uninitialized"
if (state == dbot.stateActive) or (state == dbot.stateCombat) then
return false
else
return true
end -- if
end -- dbot.gmcp.statePreventsActions
function dbot.gmcp.stateIsInCombat()
return (dbot.gmcp.getState() == dbot.stateCombat)
end -- dbot.gmcp.stateIsInCombat()
function dbot.gmcp.stateIsActive()
return (dbot.gmcp.getState() == dbot.stateActive)
end -- dbot.gmcp.stateIsActive()
----------------------------------------------------------------------------------------------------
-- Module to manage loading and saving data to persistent storage
----------------------------------------------------------------------------------------------------
--
-- Functions:
-- dbot.storage.init.atActive()
-- dbot.storage.fini(doSaveState)
--
-- dbot.storage.flush() -- Saves mushclient world or plugin state
--
-- dbot.storage.saveTable(fileName, tableName, theTable)
-- dbot.storage.loadTable(fileName, resetFn)
--
----------------------------------------------------------------------------------------------------
dbot.storage = {}
dbot.storage.init = {}
dbot.storage.fileVersion = 1
dbot.storage.hashChars = (2 * 20) -- utils.hash uses a 160-bit (20 byte) hash w/ 2 hex chars per byte
function dbot.storage.init.atActive()
local retval
-- Create directories for our state if they do not yet exist
retval = dbot.shell("if not exist \"" .. pluginStatePath .. "\" mkdir \"" .. pluginStatePath .. "\" > nul")
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.storage.init.atActive: Failed to create plugin state directory \"" ..
pluginStatePath .. "\"")
end -- if
dbot.spinUntilExists(pluginStatePath, 1)
local baseDir = dbot.backup.getBaseDir()
dbot.debug("dbot.storage.init.atActive: baseDir=\"" .. baseDir .. "\"")
retval = dbot.shell("if not exist \"" .. baseDir .. "\" mkdir \"" .. baseDir .. "\" > nul")
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.storage.init.atActive: Failed to create character-specific state directory \"" ..
baseDir .. "\"")
end -- if
dbot.spinUntilExists(baseDir, 1)
local currentDir = dbot.backup.getCurrentDir()
dbot.debug("dbot.storage.init.atActive: currentDir=\"" .. currentDir .. "\"")
retval = dbot.shell("if not exist \"" .. currentDir .. "\" mkdir \"" .. currentDir .. "\" > nul")
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.storage.init.atActive: Failed to create current state directory \"" .. currentDir .. "\"")
end -- if
dbot.spinUntilExists(currentDir, 1)
return DRL_RET_SUCCESS
end -- dbot.storage.init.atActive
function dbot.storage.fini(doSaveState)
-- Placeholder: nothing to clean up here yet
return DRL_RET_SUCCESS
end -- dbot.storage.fini
-- Save all of our state to disk. The modules all have xyz.save() functions to update their variables.
-- We just need to flush the state out to disk.
function dbot.storage.flush()
local isInPlugin = true
local pluginName = GetPluginName()
if (pluginName == "") then
isInPlugin = false
end -- if
if (isInPlugin) then
SaveState()
else
Save("")
end -- if
return DRL_RET_SUCCESS
end -- dbot.storage.flush
function dbot.storage.saveTable(fileName, tableName, theTable, doForceSave)
local retval = DRL_RET_SUCCESS
if (not dbot.init.initializedActive) and (not doForceSave) then
dbot.info("Skipping save for \"" .. (tableName or "Unknown") .. "\" table: plugin is not initialized")
dbot.info("Are you AFK? You must be in the active state to complete initialization.")
return DRL_RET_UNINITIALIZED
end -- if
if (fileName == nil) or (fileName == "") then
dbot.warn("dbot.storage.saveTable: Missing fileName parameter")
return DRL_RET_INVALID_PARAM
end -- if
if (tableName == nil) or (tableName == "") then
dbot.warn("dbot.storage.saveTable: Missing tableName parameter")
return DRL_RET_INVALID_PARAM
end -- if
if (theTable == nil) then
dbot.warn("dbot.storage.saveTable: Missing table parameter")
return DRL_RET_INVALID_PARAM
end -- if
local shortName = string.gsub(fileName, ".*\\", "")
dbot.debug("dbot.storage.saveTable: Saving \"@G" .. shortName .. "@W\"")
local fileData = "\n" .. serialize.save(tableName, theTable)
local fileHash = utils.hash((fileData or "") .. dbot.storage.fileVersion)
local f, errString, errNum = io.open(fileName, "w+")
if (f == nil) then
dbot.warn("dbot.storage.saveTable: Failed to save file: @R" .. (errString or "unknown error") .. "@W")
else
assert(f:write(dbot.storage.fileVersion .. "\n", fileHash, fileData))
assert(f:flush())
assert(f:close())
end -- if
return retval
end -- dbot.storage.saveTable
function dbot.storage.loadTable(fileName, resetFn)
local retval = DRL_RET_SUCCESS
if (fileName == nil) or (fileName == "") or (resetFn == nil) then
dbot.warn("dbot.storage.loadTable: Missing parameter")
return DRL_RET_INVALID_PARAM
end -- if
local shortName = string.gsub(fileName, ".*\\", "")
dbot.debug("dbot.storage.loadTable: Loading \"@G" .. shortName .. "@W\"")
local f = io.open(fileName, "r")
if (f ~= nil) then
local fileVersion, fileHash, savedState = assert(f:read("*l", dbot.storage.hashChars, "*a"))
if (fileHash ~= utils.hash((savedState or "") .. fileVersion)) then
dbot.error("dbot.storage.loadTable: failed to load table from file \"@R" .. fileName ..
"@W\": file is corrupted -- see \"dinv help backup\" to restore from a backup)")
resetFn()
return DRL_RET_INTERNAL_ERROR
end -- if
-- This is a placeholder for if/when we ever change the format of our saved state files
if (tonumber(fileVersion or "") ~= tonumber(dbot.storage.fileVersion or "")) then
dbot.error("File \"@G" .. fileName .. "@W\" uses an old file format")
return DRL_RET_UNSUPPORTED
end -- if
loadstring(savedState)()
assert(f:close())
else
retval = resetFn()
end -- if
return retval
end -- dbot.storage.loadTable
----------------------------------------------------------------------------------------------------
-- Module to handle saving and backing up state
----------------------------------------------------------------------------------------------------
--
-- dinv backup [list | create | delete | restore] [name]
--
-- Directory layout
-- dinv-[pluginId]/charname
-- current
-- backups
-- auto1-[date]
-- auto2-[date]
-- auto3-[date]
-- [name]-[date]
--
-- Functions:
-- dbot.backup.init.atActive()
-- dbot.backup.fini(doSaveState)
--
-- dbot.backup.getBaseDir()
-- dbot.backup.getCharDir()
-- dbot.backup.getCurrentDir()
-- dbot.backup.getBackupDir()
--
-- dbot.backup.getBackups() -- returns list of backup dir names
-- dbot.backup.getFile(name)
--
-- dbot.backup.current() -- if auto1-date ~= current date, rotates current and the "autoN" backups
--
-- dbot.backup.list(endTag)
-- dbot.backup.create(name, endTag)
-- dbot.backup.delete(name, endTag, isQuiet)
-- dbot.backup.restore(name, endTag)
--
-- dbot.backup.timer
----------------------------------------------------------------------------------------------------
dbot.backup = {}
dbot.backup.init = {}
dbot.backup.timer = {}
dbot.backup.timer.name = "drlInvBackupTimer"
dbot.backup.timer.hour = 4
dbot.backup.timer.min = 0
dbot.backup.timer.sec = 30
function dbot.backup.init.atActive()
local retval = DRL_RET_SUCCESS
local backupDir = dbot.backup.getBackupDir()
dbot.debug("dbot.backup.init.atActive: backupDir=\"" .. backupDir .. "\"")
retval = dbot.shell("if not exist \"" .. backupDir .. "\" mkdir \"" .. backupDir .. "\" > nul")
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.init.atActive: Failed to create backup directory \"" .. backupDir .. "\"")
return retval
end -- if
dbot.spinUntilExists(backupDir, 1)
-- Add a backup timer to periodically back up the plugin state. We keep the timer running
-- even if automatic backups are currently disabled. The dbot.backup.current() function
-- only does the backup if automatic backups are enabled so it doesn't hurt to call it
-- periodically from the timer. Yes, I should probably redo this so that we start the
-- timer when someone re-enables automatic backups but I'm feeling a bit lazy and this
-- only runs once every 4 hours by default so there's not a ton of overhead...
check (AddTimer(dbot.backup.timer.name,
dbot.backup.timer.hour, dbot.backup.timer.min, dbot.backup.timer.sec, "",
timer_flag.Enabled + timer_flag.Replace,
"dbot.backup.current"))
return retval
end -- dbot.backup.init.atActive
function dbot.backup.fini(doSaveState)
local retval = DRL_RET_SUCCESS
dbot.deleteTimer(dbot.backup.timer.name)
return retval
end -- dbot.backup.fini
function dbot.backup.getBaseDir()
return pluginStatePath .. "\\" .. dbot.gmcp.getName() .. "\\", DRL_RET_SUCCESS
end -- dbot.backup.getBaseDir
function dbot.backup.getCurrentDir()
return pluginStatePath .. "\\" .. dbot.gmcp.getName() .. "\\current\\" , DRL_RET_SUCCESS
end -- dbot.backup.getCurrentDir
function dbot.backup.getBackupDir()
return pluginStatePath .. "\\" .. dbot.gmcp.getName() .. "\\backup\\" , DRL_RET_SUCCESS
end -- dbot.backup.getBackupDir
-- Returns an array of backup directory names
function dbot.backup.getBackups()
local backupNames = {}
local backupDir, retval = dbot.backup.getBackupDir()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.getBackups: Failed to get backup directory: " .. dbot.retval.getString(retval))
return backupNames, retval
end -- if
-- Read the backup directory. We use the unix-style pathname because the utils.readdir()
-- function won't take the windows-style path. Yeah, I know that seems crazy. I'm probably
-- doing something silly that prevents it from working. The unix-style paths aren't too evil
-- as a work-around though.
local dirQuery = string.gsub(backupDir, "\\", "/") .. "*"
local backDirTable, error = utils.readdir(dirQuery)
if (backDirTable == nil) then
return backupNames, DRL_RET_MISSING_ENTRY
end -- if
-- Loop through every directory entry and pull out all of the directories that have the
-- [name]-[timestamp] format. Those are our backup candidates.
for backName, backEntry in pairs(backDirTable) do
local _, _, baseName, baseTime = string.find(backName, "(.*)-(%d+)$")
baseTime = tonumber(baseTime or 0)
if (baseName ~= nil) and (backEntry.directory ~= nil) and (backEntry.directory) then
table.insert(backupNames, { dirName = backupDir .. backName,
fullName = backName,
baseName = baseName,
baseTime = baseTime })
end -- if
end -- for
-- Sort the backups by date from most recent to oldest
if (#backupNames > 0) then
table.sort(backupNames, function (back1, back2) return back1.baseTime > back2.baseTime end)
end -- if
return backupNames, retval
end -- dbot.backup.getBackups
-- Returns a table holding info on the specific backup or nil if it doesn't exist, error code
function dbot.backup.getFile(name)
local backupNames, retval = dbot.backup.getBackups()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.getFileName: Failed to get backup list: " .. dbot.retval.getString(retval))
return nil, retval
end -- if
for _, backupName in ipairs(backupNames) do
if (backupName.baseName == name) then
return backupName, DRL_RET_SUCCESS
end -- if
end -- for
return nil, DRL_RET_SUCCESS
end -- dbot.backup.getFile(name)
-- The automatic backup scheme: auto --> auto2 --> auto3
dbot.backup.inProgress = false
function dbot.backup.current()
local retval
local backupFile
local backupDir
local autoPrefix = "auto"
local maxNumAutoBackups = 3
local newestBackupName = autoPrefix
local oldestBackupName = autoPrefix .. maxNumAutoBackups
if (dbot.gmcp.isInitialized == false) then
dbot.debug("dbot.backup.current: Skipping backup request: GMCP is not initialized")
return DRL_RET_UNINITIALIZED
end -- if
if (not inv.config.table.isBackupEnabled) then
dbot.debug("Automatic backups are disabled")
return DRL_RET_SUCCESS
end -- if
if dbot.gmcp.stateIsInCombat() then
dbot.info("Skipping automatic backup: You are in combat! We'll try again later.")
return DRL_RET_IN_COMBAT
end -- if
if dbot.backup.inProgress then
dbot.info("Skipping backup request: another backup request is in progress")
return DRL_RET_BUSY
end -- if
dbot.backup.inProgress = true
backupDir, retval = dbot.backup.getBackupDir()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.current: Failed to get backup directory: " .. dbot.retval.getString(retval))
dbot.backup.inProgress = false
return retval
end -- if
-- Check if the newest backup was made today. If it was, update it with the current data, leave
-- the other backups alone, and return. Otherwise, rotate the backups down one slot chronologically.
backupFile, retval = dbot.backup.getFile(newestBackupName)
if (backupFile ~= nil) then
if (os.date("%x", dbot.getTime()) == os.date("%x", backupFile.baseTime)) then
retval = dbot.backup.create(newestBackupName, nil)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.current: Failed to create newest automatic backup \"@G" .. newestBackupName ..
"@W\": " .. dbot.retval.getString(retval))
end -- if
dbot.backup.inProgress = false
return retval
end -- if
end -- if
-- We need to rotate the backups. Whack the oldest auto backup if we have hit our max number of
-- supported auto backups.
backupFile, retval = dbot.backup.getFile(oldestBackupName)
if (backupFile ~= nil) then
retval = dbot.backup.delete(oldestBackupName)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.current: Failed to delete backup \"@G" .. oldestBackupName .. "@W\"" ..
dbot.retval.getString(retval))
end -- if
end -- if
-- Now rename any other auto backups by bumping them back in the order (e.g., auto2 --> auto3)
for backupNum = (maxNumAutoBackups - 1), 1, -1 do
local currentBackup = autoPrefix .. backupNum
local olderBackup = autoPrefix .. (backupNum + 1)
-- As a one-off, we don't call the most recent backup "auto1". Instead, we just call it "auto".
-- I think this is a little more clear and it looks cleaner when the user sees that we just made
-- a backup named "auto".
if (backupNum == 1) then
currentBackup = autoPrefix
end -- if
backupFile, retval = dbot.backup.getFile(currentBackup)
if (backupFile ~= nil) then
dbot.note("Moving backup \"@G" .. currentBackup .. "@W\" to \"@G" .. olderBackup .. "@W\"")
local fullOlderBackup = string.gsub(backupFile.dirName, currentBackup, olderBackup)
fullOlderBackup = string.gsub(fullOlderBackup, ".*\\", "")
dbot.debug("CLI: " .. "rename \"" .. backupFile.dirName .. "\" \"" .. fullOlderBackup .. "\" > nul")
dbot.shell("rename \"" .. backupFile.dirName .. "\" \"" .. fullOlderBackup .. "\" > nul")
-- Shell commands running in the background aren't guaranteed to complete in the order
-- they were made. As a result, we spin here until we know that the backup was actually
-- renamed before we move on to the next backup.
dbot.spinUntilExistsBusy(backupDir .. fullOlderBackup, 2)
end -- if
end -- for
-- Finally, create a new auto backup by mirroring the current state
retval = dbot.backup.create(newestBackupName, nil)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.current: Failed to create newest automatic backup \"@G" .. newestBackupName ..
"@W\": " .. dbot.retval.getString(retval))
end -- if
dbot.backup.inProgress = false
return retval
end -- dbot.backup.current
-- If may be convenient for us to make an auto-backup if someone is AFK for a while. This
-- function will do that. This could potentially be called in OnPluginTelnetOption when we
-- detect that we entered AFK mode. It's not clear if this will be useful or annoying...
-- We'll kick the tires of this optimization and see if it sticks.
function dbot.backup.atAFK()
local retval = DRL_RET_SUCCESS
if dbot.gmcp.isInitialized and (dbot.gmcp.getState() == dbot.stateAFK) then
retval = dbot.backup.current()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.atAFK: Failed to backup plugin state: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- dbot.backup.atAFK
function dbot.backup.list(endTag)
local backupNames, retval = dbot.backup.getBackups()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.list: Failed to get backup list: " .. dbot.retval.getString(retval))
elseif (backupNames == nil) or
((backupNames ~= nil) and (#backupNames == 0)) then
dbot.info("No backups detected")
else
local suffix = ""
if (#backupNames ~= 1) then
suffix = "s"
end -- if
dbot.info("Detected " .. #backupNames .. " backup" .. suffix)
for _, backupName in ipairs(backupNames) do
dbot.print(" @W(@c" .. os.date("%c", backupName.baseTime) .. "@W) @G" .. backupName.baseName)
end -- if
end -- if
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- dbot.backup.list
function dbot.backup.create(name, endTag)
local retval = DRL_RET_SUCCESS
if (name == nil) or (name == "") then
dbot.warn("dbot.backup.create: Missing name parameter")
return inv.tags.stop(invTagsBackup, endTag, DRL_RET_INVALID_PARAM)
end -- if
local currentDir, retval = dbot.backup.getCurrentDir()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.create: Failed to get current directory: " .. dbot.retval.getString(retval))
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- if
currentDir = string.gsub(currentDir, "\\$", "") -- Some versions of xcopy hate if there is a trailing slash
local backupDir, retval = dbot.backup.getBackupDir()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.create: Failed to get backup directory: " .. dbot.retval.getString(retval))
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- if
-- Remove any old backups with the same name. Creating a backup "foo" will remove any previous
-- backups that were created with the name "foo".
retval = dbot.backup.delete(name, nil, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.create: Failed to remove old backup \"@G" .. name .. "@W\"" ..
dbot.retval.getString(retval))
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- if
-- We append the time to the end of the backup name to help track it
local backupTime = dbot.getTime()
local newBackupDir = backupDir .. name .. "-" .. backupTime
dbot.debug("dbot.backup.create: CLI = \"@y" .. "xcopy /E /I \"" .. currentDir .. "\" \"" .. newBackupDir ..
"\" > nul@W\"")
retval = dbot.shell("xcopy /E /I \"" .. currentDir .. "\" \"" .. newBackupDir .. "\" > nul")
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.create: Failed to create backup, xcopy shell failed: " ..
dbot.retval.getString(retval))
else
dbot.info("Created backup @W(@c" .. os.date("%c", backupTime) .. "@W) @G" .. name)
end -- if
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- dbot.backup.create
function dbot.backup.delete(name, endTag, isQuiet)
local retval = DRL_RET_SUCCESS
if (name == nil) or (name == "") then
dbot.warn("dbot.backup.delete: Missing name parameter")
return inv.tags.stop(invTagsBackup, endTag, DRL_RET_INVALID_PARAM)
end -- if
local backupNames, retval = dbot.backup.getBackups()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.delete: Failed to get backup list: " .. dbot.retval.getString(retval))
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- if
-- Check if the backup name we want to delete is one of the available backups and whack it if it is
local numBackupsDeleted = 0
for _, backupName in ipairs(backupNames) do
if (backupName.baseName == name) then
dbot.debug("dbot.backup.delete: Executing \"rmdir /s /q \"" .. backupName.dirName .. "\"\"")
dbot.shell("rmdir /s /q \"" .. backupName.dirName .. "\" > nul")
retval = dbot.spinWhileExistsBusy(backupName.dirName, 2)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.delete: Failed to delete backup \"@G" .. name .. "@W\": " ..
dbot.retval.getString(retval))
break
else
if (isQuiet == false) then
dbot.info("Deleted backup @W(@c" .. os.date("%c", backupName.baseTime) ..
"@W) @G" .. backupName.baseName)
end -- if
end -- if
numBackupsDeleted = numBackupsDeleted + 1
end -- if
end -- if
if (numBackupsDeleted == 0) and (isQuiet == false) then
dbot.info("Failed to delete backup: No backups matching name \"@G" .. name .. "@w\" were found")
end -- if
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- dbot.backup.delete
dbot.backup.restorePkg = nil
function dbot.backup.restore(name, endTag)
local retval = DRL_RET_SUCCESS
if (name == nil) or (name == "") then
dbot.warn("dbot.backup.restore: Missing name parameter")
return inv.tags.stop(invTagsBackup, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (dbot.backup.restorePkg ~= nil) then
dbot.info("Skipping backup restore request: another restore is in progress")
return inv.tags.stop(invTagsBackup, endTag, DRL_RET_BUSY)
end -- if
dbot.backup.restorePkg = {}
dbot.backup.restorePkg.name = name
dbot.backup.restorePkg.endTag = endTag
wait.make(dbot.backup.restoreCR)
return retval
end -- dbot.backup.restore
function dbot.backup.restoreCR()
if (dbot.backup.restorePkg == nil) then
dbot.warn("dbot.backup.restoreCR: restore package is nil!?!?")
return inv.tags.stop(invTagsBackup, endTag, DRL_RET_INTERNAL_ERROR)
end -- if
local name = dbot.backup.restorePkg.name
local endTag = dbot.backup.restorePkg.endTag
local backupNames, retval = dbot.backup.getBackups()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.restore: Failed to get backup list: " .. dbot.retval.getString(retval))
dbot.backup.restorePkg = nil
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- if
local currentDir, retval = dbot.backup.getCurrentDir()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.restore: Failed to get current directory: " .. dbot.retval.getString(retval))
dbot.backup.restorePkg = nil
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- if
currentDir = string.gsub(currentDir, "\\$", "") -- Some versions of xcopy hate if there is a trailing slash
-- Check if the backup name we want to restore is one of the available backups and use it if it is
local didRestore = false
for _, backupName in ipairs(backupNames) do
if (backupName.baseName == name) then
dbot.info("Restoring backup @W(@c" .. os.date("%c", backupName.baseTime) ..
"@W) @G" .. backupName.baseName)
dbot.shell("rmdir /s /q \"" .. currentDir .. "\" > nul")
dbot.spinWhileExists(currentDir, 5) -- Spin for up to 5 seconds waiting for confirmation it is gone
dbot.debug("dbot.backup.restore: \"@y" .. "xcopy /E /I \"" .. backupName.dirName .. "\" \"" ..
currentDir .. "\"@W\"")
dbot.shell("xcopy /E /I \"" .. backupName.dirName .. "\" \"" .. currentDir .. "\" > nul")
dbot.spinUntilExists(currentDir, 5) -- Spin for up to 5 seconds waiting for confirmation it is there
-- We want to re-init everything to pick up the restored state. We don't want to save the
-- current state which will be overwritten.
local retval = inv.reload(drlDoNotSaveState)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.backup.restore: Failed to reload plugin: " .. dbot.retval.getString(retval))
end -- if
didRestore = true
break -- Don't restore multiple backups if they have the same name but a different date
end -- if
end -- if
if (not didRestore) and (retval == DRL_RET_SUCCESS) then
dbot.warn("Failed to restore backup \"@G" .. name .. "@W\": could not find backup")
retval = DRL_RET_MISSING_ENTRY
end -- if
dbot.backup.restorePkg = nil
return inv.tags.stop(invTagsBackup, endTag, retval)
end -- dbot.backup.restoreCR
----------------------------------------------------------------------------------------------------
-- Telnet low-level definitions
----------------------------------------------------------------------------------------------------
dbot.telnet = {}
dbot.telnet.IAC = 255
dbot.telnet.SB = 250
dbot.telnet.SE = 240
dbot.telnet.promptOption = 52
dbot.telnet.optionOn = 1
dbot.telnet.optionOff = 2
----------------------------------------------------------------------------------------------------
--
-- Module to disable and enable empty lines
--
-- Many of the commands this plugin runs in the background generate empty lines of output. We don't
-- want the user to suddenly see empty lines showing up without warning so we provide a way to
-- suppress empty lines when desired.
--
--
-- dbot.emptyLine.init.atInstall()
-- dbot.emptyLine.fini(doSaveState)
--
-- dbot.emptyLine.disable()
-- dbot.emptyLine.enable()
--
----------------------------------------------------------------------------------------------------
dbot.emptyLine = {}
dbot.emptyLine.init = {}
dbot.emptyLine.trigger = {}
dbot.emptyLine.trigger.suppressEmptyName = "drlDbotEmptyLineTrigger"
dbot.emptyLine.numEnables = 1 -- empty lines are enabled by default
function dbot.emptyLine.init.atInstall()
local retval = DRL_RET_SUCCESS
-- Suppress empty lines (white space only)
check (AddTriggerEx(dbot.emptyLine.trigger.suppressEmptyName,
"^[ ]*$",
"",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.emptyLine.trigger.suppressEmptyName, false)) -- default to off
return retval
end -- dbot.emptyLine.init.atInstall
function dbot.emptyLine.fini(doSaveState)
dbot.deleteTrigger(dbot.emptyLine.trigger.suppressEmptyName)
dbot.emptyLine.numEnables = 1 -- empty lines are enabled by default
-- NOTE: if we ever add persistent data to this module we should use the doSaveState param to determine
-- if we need to save it
return DRL_RET_SUCCESS
end -- dbot.emptyLine.fini
function dbot.emptyLine.disable()
local retval = DRL_RET_SUCCESS
dbot.emptyLine.numEnables = dbot.emptyLine.numEnables - 1
if (dbot.emptyLine.numEnables == 0) then
dbot.debug("dbot.emptyLine.disable: suppressing empty lines")
EnableTrigger(dbot.emptyLine.trigger.suppressEmptyName, true)
end -- if
return retval
end -- dbot.emptyLine.disable
function dbot.emptyLine.enable()
local retval = DRL_RET_SUCCESS
dbot.emptyLine.numEnables = dbot.emptyLine.numEnables + 1
if (dbot.emptyLine.numEnables == 1) then
dbot.debug("dbot.emptyLine.enable: allowing empty lines")
EnableTrigger(dbot.emptyLine.trigger.suppressEmptyName, false)
end -- if
return retval
end -- dbot.emptyLine.enable
----------------------------------------------------------------------------------------------------
--
-- Module to disable and enable the prompt
--
-- This uses low-level telnet 102 commands so that we can change the prompt status even if the
-- character is AFK.
--
-- dbot.prompt.init.atActive()
-- dbot.prompt.fini(doSaveState)
--
-- dbot.prompt.disable()
-- dbot.prompt.enable()
-- dbot.prompt.hide() -- disables both the prompt and empty lines
-- dbot.prompt.show() -- enables both the prompt and empty lines
-- dbot.prompt.getStatusCR()
--
-- dbot.prompt.trigger.onEnable()
-- dbot.prompt.trigger.onDisable()
-- dbot.prompt.trigger.onToggle(msg)
--
----------------------------------------------------------------------------------------------------
dbot.prompt = {}
dbot.prompt.init = {}
dbot.prompt.trigger = {}
dbot.prompt.trigger.onEnableName = "drlDbotPromptTriggerOnEnable"
dbot.prompt.trigger.onDisableName = "drlDbotPromptTriggerOnDisable"
dbot.prompt.trigger.onToggleName = "drlDbotPromptTriggerOnToggle"
dbot.prompt.isEnabled = true -- by default, assume the prompt is enabled if we can't get the real status
function dbot.prompt.init.atActive()
local retval = DRL_RET_SUCCESS
-- Detect when the prompt is enabled
check (AddTriggerEx(dbot.prompt.trigger.onEnableName,
"^You will now see prompts.$",
"dbot.prompt.trigger.onEnable()",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.prompt.trigger.onEnableName, false)) -- default to off
-- Detect when the prompt is disabled
check (AddTriggerEx(dbot.prompt.trigger.onDisableName,
"^You will no longer see prompts.$",
"dbot.prompt.trigger.onDisable()",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.prompt.trigger.onDisableName, false)) -- default to off
-- Detect when the prompt is toggled. This is very similar to the onEnableName and the
-- onDisableName triggers above, but we don't suppress the output in this trigger like we
-- do with those triggers. That is because it tracks when the user manually toggles the
-- prompt and we don't want to hide that from the user.
--
-- The prompt code runs in two stages. First, we toggle the prompt twice in the background
-- and suppress the output so that we can detect if the prompt is enabled without the user
-- knowing that we did anything. Once we complete that, then we enable the onToggle trigger.
-- The onToggle trigger is never active at the same time as the onEnable or onDisable triggers.
check (AddTriggerEx(dbot.prompt.trigger.onToggleName,
"^(You will no longer see prompts|You will now see prompts).*$",
"dbot.prompt.trigger.onToggle(\"%1\")",
drlTriggerFlagsBaseline,
custom_colour.NoChange, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.prompt.trigger.onToggleName, false)) -- default to off
-- We will fill this in when we call dbot.prompt.isEnabledCR. We need to manually toggle "prompt"
-- two times and see what the output is in order to determine what the actual status of the prompt
-- is. We can't do that synchronously here so we kick off a co-routine to get that info.
dbot.prompt.statusEnables = 1
-- The *.init.atActive code runs in a co-routine so we don't need to explicitly kick off another CR here
dbot.prompt.getStatusCR()
return retval
end -- dbot.prompt.init.atActive
function dbot.prompt.fini(doSaveState)
dbot.deleteTrigger(dbot.prompt.trigger.onEnableName)
dbot.deleteTrigger(dbot.prompt.trigger.onDisableName)
dbot.deleteTrigger(dbot.prompt.trigger.onToggleName)
dbot.prompt.isEnabled = true
-- NOTE: if we ever add persistent data to this module we should use the doSaveState param to determine
-- if we need to save it
return DRL_RET_SUCCESS
end -- dbot.prompt.fini
function dbot.prompt.disable()
if (dbot.prompt.statusEnables ~= nil) then
-- The moment we transition from having 1 enable to 0 enables, we disable the prompt. Someone
-- could call multiple consecutive disables but we'd only do the disabling once.
dbot.prompt.statusEnables = dbot.prompt.statusEnables - 1
if (dbot.prompt.statusEnables == 0) then
SendPkt(string.char (dbot.telnet.IAC, dbot.telnet.SB, 102, dbot.telnet.promptOption,
dbot.telnet.optionOff, dbot.telnet.IAC, dbot.telnet.SE))
end -- if
end -- if
end -- dbot.prompt.disable
function dbot.prompt.enable()
if (dbot.prompt.statusEnables ~= nil) then
dbot.prompt.statusEnables = dbot.prompt.statusEnables + 1
if (dbot.prompt.statusEnables == 1) then
SendPkt(string.char (dbot.telnet.IAC, dbot.telnet.SB, 102, dbot.telnet.promptOption,
dbot.telnet.optionOn, dbot.telnet.IAC, dbot.telnet.SE))
end -- if
end -- if
end -- dbot.prompt.enable
-- Disable the prompt and suppress empty lines
function dbot.prompt.hide()
dbot.emptyLine.disable()
dbot.prompt.disable()
end -- dbot.prompt.hide
function dbot.prompt.show()
dbot.prompt.enable()
-- TODO: This is an awkward situation. We want to stop suppressing empty lines (resulting
-- from some actions this plugin executes in the background) but we don't want to do
-- that until all of the commands queued up on the server are executed. Yes, we could
-- do some extra synchronization to catch the end of the command queue, but that's
-- more work than I want to deal with for this minor issue. In the meantime, there
-- might be one or two empty lines of output that show up near the end of a large
-- operation.
dbot.emptyLine.enable()
end -- dbot.prompt.show
function dbot.prompt.getStatusCR()
local retval = DRL_RET_SUCCESS
dbot.prompt.foundEnable = false
dbot.prompt.foundDisable = false
-- Enable the prompt triggers so that we can catch what happens when we run "prompt"
EnableTrigger(dbot.prompt.trigger.onEnableName, true)
EnableTrigger(dbot.prompt.trigger.onDisableName, true)
-- Disable empty lines while we toggle the prompt so that we don't spew empty lines in the background
dbot.emptyLine.disable()
-- Run "prompt" and see what happens. Note that we can't run dbot.execute.safe.command() here because
-- that could potentially toggle the prompt status and we want to know what is going on right now.
check (Send("prompt"))
check (Send("prompt"))
-- Spin until both the enable trigger and disable trigger are detected (both "prompt" commands complete)
local totTime = 0
local timeout = 60
retval = DRL_RET_TIMEOUT
while (totTime <= timeout) do
if (dbot.prompt.foundEnable == true) and (dbot.prompt.foundDisable == true) then
retval = DRL_RET_SUCCESS
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.prompt.getStatusCR: Failed to detect prompt enable/disable: " ..
dbot.retval.getString(retval))
end -- if
EnableTrigger(dbot.prompt.trigger.onEnableName, false)
EnableTrigger(dbot.prompt.trigger.onDisableName, false)
-- Re-enable empty lines again if we disabled them during the prompt toggling test
dbot.emptyLine.enable()
if dbot.prompt.isEnabled then
dbot.prompt.statusEnables = 1
else
dbot.prompt.statusEnables = 0
end -- if
-- Once we know the current state, we monitor when the user manually toggles the prompt so
-- that we can keep our state up-to-date
EnableTrigger(dbot.prompt.trigger.onToggleName, true)
return retval
end -- dbot.prompt.getStatusCR
function dbot.prompt.trigger.onEnable()
dbot.prompt.isEnabled = true
dbot.prompt.foundEnable = true
end -- dbot.prompt.trigger.onEnable
function dbot.prompt.trigger.onDisable()
dbot.prompt.isEnabled = false
dbot.prompt.foundDisable = true
end -- dbot.prompt.trigger.onDisable
function dbot.prompt.trigger.onToggle(msg)
if (msg == "You will no longer see prompts") then
dbot.debug("dbot.prompt.trigger.onToggle: user manually turned the prompt off")
dbot.prompt.statusEnables = 0
inv.config.table.isPromptEnabled = false
elseif (msg == "You will now see prompts") then
dbot.debug("dbot.prompt.trigger.onToggle: user manually turned the prompt on")
dbot.prompt.statusEnables = 1
inv.config.table.isPromptEnabled = true
else
dbot.error("dbot.prompt.trigger.onToggle: triggered on unsupported message \"" .. (msg or "nil") .. "\"")
end -- if
if (dbot.init.initializedActive) then
inv.config.save()
end -- if
end -- dbot.prompt.trigger.onToggle
----------------------------------------------------------------------------------------------------
--
-- Module to check the status of invmon
--
-- dbot.invmon.init.atInstall()
-- dbot.invmon.init.atActive()
-- dbot.invmon.fini(doSaveState)
--
-- dbot.invmon.getStatusCR()
--
-- dbot.invmon.trigger.onEnable()
-- dbot.invmon.trigger.onDisable()
-- dbot.invmon.trigger.onToggle(msg)
--
----------------------------------------------------------------------------------------------------
dbot.invmon = {}
dbot.invmon.init = {}
dbot.invmon.trigger = {}
dbot.invmon.isEnabled = true -- by default, assume the invmon is enabled
dbot.invmon.trigger.onEnableName = "drlDbotInvmonTriggerOnEnable"
dbot.invmon.trigger.onDisableName = "drlDbotInvmonTriggerOnDisable"
dbot.invmon.trigger.onToggleName = "drlDbotInvmonTriggerOnToggle"
function dbot.invmon.init.atInstall()
local retval = DRL_RET_SUCCESS
-- Detect when the invmon is enabled
check (AddTriggerEx(dbot.invmon.trigger.onEnableName,
"^You will now see inventory update tags.$",
"dbot.invmon.trigger.onEnable()",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.invmon.trigger.onEnableName, false)) -- default to off
-- Detect when the invmon is disabled
check (AddTriggerEx(dbot.invmon.trigger.onDisableName,
"^You will no longer see inventory update tags.$",
"dbot.invmon.trigger.onDisable()",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.invmon.trigger.onDisableName, false)) -- default to off
-- Detect when the invmon is toggled. This is very similar to the onEnableName and the
-- onDisableName triggers above, but we don't suppress the output in this trigger like we
-- do with those triggers. That is because it tracks when the user manually toggles the
-- invmon and we don't want to hide that from the user.
--
-- The invmon code runs in two stages. First, we toggle the invmon twice in the background
-- and suppress the output so that we can detect if the invmon is enabled without the user
-- knowing that we did anything. Once we complete that, then we enable the onToggle trigger.
-- The onToggle trigger is never active at the same time as the onEnable or onDisable triggers.
check (AddTriggerEx(dbot.invmon.trigger.onToggleName,
"^(" ..
"You will no longer see inventory update tags." ..
"|You will now see inventory update tags." ..
").*$",
"dbot.invmon.trigger.onToggle(\"%1\")",
drlTriggerFlagsBaseline,
custom_colour.NoChange, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.invmon.trigger.onToggleName, false)) -- default to off
return retval
end -- dbot.invmon.init.atInstall
function dbot.invmon.init.atActive()
local retval = DRL_RET_SUCCESS
-- We will fill this in when we call dbot.invmon.isEnabledCR. We need to manually toggle "invmon"
-- two times and see what the output is in order to determine what the actual status of the invmon
-- is. We can't do that synchronously here so we kick off a co-routine to get that info.
dbot.invmon.statusEnables = 0
-- The *.init.atActive code runs in a co-routine so we don't need to explicitly kick off another CR here
dbot.invmon.getStatusCR()
return retval
end -- dbot.invmon.init.atActive
function dbot.invmon.fini(doSaveState)
dbot.deleteTrigger(dbot.invmon.trigger.onEnableName)
dbot.deleteTrigger(dbot.invmon.trigger.onDisableName)
dbot.deleteTrigger(dbot.invmon.trigger.onToggleName)
dbot.invmon.isEnabled = true
-- NOTE: if we ever add persistent data to this module we should use the doSaveState param to determine
-- if we need to save it
return DRL_RET_SUCCESS
end -- dbot.invmon.fini
function dbot.invmon.getStatusCR()
local retval = DRL_RET_SUCCESS
dbot.invmon.foundEnable = false
dbot.invmon.foundDisable = false
-- Enable the invmon triggers so that we can catch what happens when we run "invmon"
EnableTrigger(dbot.invmon.trigger.onEnableName, true)
EnableTrigger(dbot.invmon.trigger.onDisableName, true)
-- Disable the prompt while we toggle invmon so that we don't output lines
dbot.prompt.hide()
-- Run "invmon" and see what happens
dbot.execute.fast.command("invmon")
dbot.execute.fast.command("invmon")
-- Spin until both the enable trigger and disable trigger are detected (both "invmon" commands complete)
local totTime = 0
local timeout = 60
retval = DRL_RET_TIMEOUT
while (totTime <= timeout) do
if (dbot.invmon.foundEnable == true) and (dbot.invmon.foundDisable == true) then
retval = DRL_RET_SUCCESS
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.invmon.getStatusCR: Failed to detect invmon enable/disable: " ..
dbot.retval.getString(retval))
end -- if
EnableTrigger(dbot.invmon.trigger.onEnableName, false)
EnableTrigger(dbot.invmon.trigger.onDisableName, false)
-- Re-enable the prompt (we disabled it before running invmon twice)
dbot.prompt.show()
if dbot.invmon.isEnabled then
dbot.invmon.statusEnables = 1
else
dbot.invmon.statusEnables = 0
dbot.warn("The " .. pluginNameAbbr .. " plugin requires invmon. Please type \"invmon\" to enable it.")
end -- if
-- Once we know the current state, we monitor when the user manually toggles invmon so
-- that we can keep our state up-to-date
EnableTrigger(dbot.invmon.trigger.onToggleName, true)
return retval
end -- dbot.invmon.getStatusCR
function dbot.invmon.trigger.onEnable()
dbot.invmon.isEnabled = true
dbot.invmon.foundEnable = true
end -- dbot.invmon.trigger.onEnable
function dbot.invmon.trigger.onDisable()
dbot.invmon.isEnabled = false
dbot.invmon.foundDisable = true
end -- dbot.invmon.trigger.onDisable
function dbot.invmon.trigger.onToggle(msg)
if (msg == "You will no longer see inventory update tags.") then
dbot.debug("dbot.invmon.trigger.onToggle: user manually turned invmon off")
dbot.invmon.statusEnables = dbot.invmon.statusEnables - 1
dbot.warn("You just disabled invmon!")
dbot.warn("The " .. pluginNameAbbr .. " plugin requires invmon. Please type \"invmon\" to enable it again.")
elseif (msg == "You will now see inventory update tags.") then
dbot.debug("dbot.invmon.trigger.onToggle: user manually turned invmon on")
dbot.invmon.statusEnables = dbot.invmon.statusEnables + 1
else
dbot.error("dbot.invmon.trigger.onToggle: triggered on unsupported message \"" .. (msg or "nil") .. "\"")
end -- if
end -- dbot.invmon.trigger.onToggle
----------------------------------------------------------------------------------------------------
-- Invmon helper code and definitions
----------------------------------------------------------------------------------------------------
invmon = {}
invmonActionRemoved = 1
invmonActionWorn = 2
invmonActionRemovedFromInv = 3
invmonActionAddedToInv = 4
invmonActionTakenOutOfContainer = 5
invmonActionPutIntoContainer = 6
invmonActionConsumed = 7
invmonActionPutIntoVault = 9
invmonActionRemovedFromVault = 10
invmonActionPutIntoKeyring = 11
invmonActionGetFromKeyring = 12
invmon.action = {}
invmon.action[invmonActionRemoved] = "Removed"
invmon.action[invmonActionWorn] = "Worn"
invmon.action[invmonActionRemovedFromInv] = "Removed from inventory"
invmon.action[invmonActionAddedToInv] = "Added to inventory"
invmon.action[invmonActionTakenOutOfContainer] = "Taken out of container"
invmon.action[invmonActionPutIntoContainer] = "Put into container"
invmon.action[invmonActionConsumed] = "Consumed"
invmon.action[invmonActionPutIntoVault] = "Put into vault"
invmon.action[invmonActionRemovedFromVault] = "Removed from vault"
invmon.action[invmonActionPutIntoKeyring] = "Put into keyring"
invmon.action[invmonActionGetFromKeyring] = "Get from keyring"
invmonTypeNone = 0
invmonTypeLight = 1
invmonTypeScroll = 2
invmonTypeWand = 3
invmonTypeStaff = 4
invmonTypeWeapon = 5
invmonTypeTreasure = 6
invmonTypeArmor = 7
invmonTypePotion = 8
invmonTypeFurniture = 9
invmonTypeTrash = 10
invmonTypeContainer = 11
invmonTypeDrinkContainer = 12
invmonTypeKey = 13
invmonTypeFood = 14
invmonTypeBoat = 15
invmonTypeMobCorpse = 16
invmonTypePlayerCorpse = 17
invmonTypeFountain = 18
invmonTypePill = 19
invmonTypePortal = 20
invmonTypeBeacon = 21
invmonTypeGiftCard = 22
invmonTypeUnused = 23
invmonTypeRawMaterial = 24
invmonTypeCampfire = 25
invmonTypeForge = 26
invmonTypeRunestone = 27
invmon.typeStr = {}
invmon.typeStr[invmonTypeNone] = "None"
invmon.typeStr[invmonTypeLight] = "Light"
invmon.typeStr[invmonTypeScroll] = "Scroll"
invmon.typeStr[invmonTypeWand] = "Wand"
invmon.typeStr[invmonTypeStaff] = "Staff"
invmon.typeStr[invmonTypeWeapon] = "Weapon"
invmon.typeStr[invmonTypeTreasure] = "Treasure"
invmon.typeStr[invmonTypeArmor] = "Armor"
invmon.typeStr[invmonTypePotion] = "Potion"
invmon.typeStr[invmonTypeFurniture] = "Furniture"
invmon.typeStr[invmonTypeTrash] = "Trash"
invmon.typeStr[invmonTypeContainer] = "Container"
invmon.typeStr[invmonTypeDrinkContainer] = "Drink container"
invmon.typeStr[invmonTypeKey] = "Key"
invmon.typeStr[invmonTypeFood] = "Food"
invmon.typeStr[invmonTypeBoat] = "Boat"
invmon.typeStr[invmonTypeMobCorpse] = "Mob corpse"
invmon.typeStr[invmonTypePlayerCorpse] = "Player corpse"
invmon.typeStr[invmonTypeFountain] = "Fountain"
invmon.typeStr[invmonTypePill] = "Pill"
invmon.typeStr[invmonTypePortal] = "Portal"
invmon.typeStr[invmonTypeBeacon] = "Beacon"
invmon.typeStr[invmonTypeGiftCard] = "Gift card"
invmon.typeStr[invmonTypeUnused] = "Unused"
invmon.typeStr[invmonTypeRawMaterial] = "Raw material"
invmon.typeStr[invmonTypeCampfire] = "Campfire"
invmon.typeStr[invmonTypeForge] = "Forge"
invmon.typeStr[invmonTypeRunestone] = "Runestone"
----------------------------------------------------------------------------------------------------
--
-- dbot.ability: Module to check if a character has access to a specific skill or spell
--
-- dbot.ability.init.atInstall()
-- dbot.ability.fini(doSaveState)
--
-- dbot.ability.isAvailable
-- dbot.ability.isCached
--
-- dbot.ability.trigger.haveAbilityStart
-- dbot.ability.trigger.haveAbilityLevel
--
-- dbot.ability.setupFn() -- enable the ability trigger during a safe execute call
--
----------------------------------------------------------------------------------------------------
-- This function can block so it must be called from a co-routine
dbot.ability = {}
dbot.abilityPkg = {}
dbot.ability.init = {}
dbot.ability.trigger = {}
dbot.ability.isInProgress = false
dbot.ability.trigger.startName = "drlDbotAbilityTriggerStart"
dbot.ability.trigger.levelName = "drlDbotAbilityTriggerLevel"
function dbot.ability.init.atInstall()
local retval = DRL_RET_SUCCESS
-- Trigger on the output of "showskill" and watch for an empty line indicating that we are done
check (AddTriggerEx(dbot.ability.trigger.levelName,
"^(.*)$", -- This is only enabled when we confirm we have started the output
"dbot.ability.trigger.levelFn(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11,
0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.ability.trigger.levelName, false)) -- default to off
return retval
end -- dbot.ability.init.atInstall
function dbot.ability.fini(doSaveState)
local retval = DRL_RET_SUCCESS
dbot.deleteTrigger(dbot.ability.trigger.levelName)
dbot.deleteTrigger(dbot.ability.trigger.startName)
-- NOTE: if we ever add persistent data to this module we should use the doSaveState param to determine
-- if we need to save it
return retval
end -- dbot.ability.fini
function dbot.ability.isAvailable(ability, level)
local retval = DRL_RET_SUCCESS
local abilityIsAvailable = false
if (ability == nil) or (ability == "") then
dbot.warn("dbot.ability.isAvailable: missing ability parameter")
return DRL_RET_INVALID_PARAM
end -- if
if (level == nil) or (tonumber(level) == nil) then
dbot.warn("dbot.ability.isAvailable: level parameter is not a number")
return DRL_RET_INVALID_PARAM
end -- if
level = tonumber(level)
if (dbot.ability.isInProgress) then
dbot.info("Skipping check for skill or spell availability: another request is in progress")
return false, DRL_RET_BUSY
end -- if
-- We are starting a new request
dbot.ability.isInProgress = true
-- If we already cached whether or not the user has the specified ability, use the cached value.
-- Otherwise, call "showskill" and pick out the level at which the ability is available.
if dbot.ability.isCached(ability, level) then
dbot.debug("Using cached ability useLevel = " .. dbot.abilityPkg.useLevel)
else
dbot.abilityPkg.ability = ability
dbot.abilityPkg.checkLevel = level
dbot.abilityPkg.useLevel = nil
-- Kick off the command that will trigger the level availability info
local resultData = dbot.callback.new()
retval = dbot.execute.safe.command("showskill " .. ability, dbot.ability.setupFn, nil,
dbot.callback.default, resultData)
if (retval == DRL_RET_SUCCESS) then
-- Wait for the callback to confirm that the showskill safe command completed
retval = dbot.callback.wait(resultData, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping ability \"showskill\" request: " .. dbot.retval.getString(retval))
end -- if
-- Wait for the trigger to find the info
local waitForHaveAbilityTimeout = 0
local waitForHaveAbilityThreshold = 10
while (dbot.abilityPkg.useLevel == nil) do
wait.time(drlSpinnerPeriodDefault)
waitForHaveAbilityTimeout = waitForHaveAbilityTimeout + drlSpinnerPeriodDefault
if (waitForHaveAbilityTimeout > waitForHaveAbilityThreshold) then
dbot.error("dbot.ability.isAvailable: Failed to get level availability: timed out")
dbot.deleteTrigger(dbot.ability.trigger.startName)
abilityIsAvailable = false
retval = DRL_RET_TIMEOUT
break
end -- if
end -- while
end -- if
end -- if
-- Check the level info we found
if (dbot.abilityPkg.useLevel ~= nil) and
(dbot.abilityPkg.useLevel + (10 * dbot.gmcp.getTier()) <= level) then
abilityIsAvailable = true
end -- if
-- Clean up and return
dbot.ability.isInProgress = false
return abilityIsAvailable, retval
end -- dbot.ability.isAvailable
function dbot.ability.isCached(ability, level)
-- Check if we already checked for this ability at a previous level. If we are at a higher
-- level than before and we had the ability before, there is no need to check it again -- we
-- don't lose ability as we get higher level. We could generate a big table for each ability
-- but we probably will only use this for one skill (dual wield) so we simply cache just the
-- previous ability. We can extend this later if we want to. The worst case is that we do
-- some extra "showskill" requests.
if (dbot.abilityPkg.ability ~= nil) and (dbot.abilityPkg.ability == ability) and
(dbot.abilityPkg.checkLevel ~= nil) and (dbot.abilityPkg.checkLevel <= level) and
(dbot.abilityPkg.useLevel ~= nil) then
return true
else
return false
end -- if
end -- dbot.ability.isCached
function drlHaveAbilityStartTriggerFn(line)
if string.find(line, "is not a valid skill or spell") or string.find(line, "You cannot use this") then
dbot.abilityPkg.useLevel = 300 -- we can't use this
return
end -- if
-- Enable a trigger to watch for the output of the ability availability output. The trigger
-- will disable itself when it sees an empty line that indicates the output is done.
EnableTrigger(dbot.ability.trigger.levelName, true)
end -- drlHaveAbilityStartTriggerFn
function dbot.ability.trigger.levelFn(line)
local useLevel
_, _, useLevel = string.find(line, "Your Level%s+:%s+(%d+)")
-- If we hit an empty line, we know that the output is done and we can disable this trigger
if (line == "") then
EnableTrigger(dbot.ability.trigger.levelName, false)
-- If we can't use the skill, set the useLevel out of reach
elseif string.find(line, "You cannot use this") then
dbot.abilityPkg.useLevel = 300
-- Check if we found our level in a line like: "Your Level : 22 Learned: 95% "
elseif (useLevel ~= nil) and (tonumber(useLevel) ~= nil) then
dbot.abilityPkg.useLevel = tonumber(useLevel)
end -- if
end -- dbot.ability.trigger.levelFn
function dbot.ability.setupFn()
-- Add a trigger series that will pull out the level at which the character can use the ability
check (AddTriggerEx(dbot.ability.trigger.startName,
"^.*(" ..
"------------------------------------------------------|" ..
"is not a valid skill or spell" ..
").*$",
"drlHaveAbilityStartTriggerFn(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OneShot + trigger_flag.OmitFromOutput,
custom_colour.Custom11,
0, "", "", sendto.script, 0))
end -- dbot.ability.setupFn
----------------------------------------------------------------------------------------------------
--
-- Module to track wishes
--
-- dbot.wish.init.atActive()
-- dbot.wish.fini(doSaveState)
--
-- dbot.wish.save()
-- dbot.wish.load()
-- dbot.wish.reset()
--
-- dbot.wish.get()
-- dbot.wish.getCR()
-- dbot.wish.has(wishName)
--
-- dbot.wish.trigger.fn()
-- dbot.wish.setupFn() -- enable the trigger during a safe execute call
--
----------------------------------------------------------------------------------------------------
dbot.wish = {}
dbot.wish.table = {}
dbot.wish.init = {}
dbot.wish.trigger = {}
dbot.wish.timer = {}
dbot.wish.name = "dbot-wish.state"
dbot.wish.trigger.startName = "drlDbotWishTriggerStart"
dbot.wish.trigger.itemName = "drlDbotWishTriggerItem"
dbot.wish.timer.name = "drlDbotWishTimer"
dbot.wish.timer.min = 1
dbot.wish.timer.sec = 0
function dbot.wish.init.atActive()
local retval = DRL_RET_SUCCESS
-- Pull in what we already know
retval = dbot.wish.load()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.wish.init.atActive: Failed to load wish data: " .. dbot.retval.getString(retval))
end -- if
-- Trigger on the output of "wish list" and watch for an empty line indicating that we are done
check (AddTriggerEx(dbot.wish.trigger.itemName,
"^(.*)$",
"dbot.wish.trigger.fn(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11,
0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.wish.trigger.itemName, false)) -- default to off
-- Kick off a timer to update the wishes. It would be convenient to just run dbot.wish.get()
-- right here instead of scheduling it to run 1 second from now. However, there are cases where
-- we our telnet options code detects that we are out of AFK causing us to run this init routine
-- before GMCP notices that our state changed. If that happens, no real harm is done and we simply
-- reschedule the wish detection to try again later. However, we can (probably) avoid that extra
-- overhead if we simply give GMCP a chance to detect the state change so we are willing to wait
-- an extra second here to give it that chance.
check (AddTimer(dbot.wish.timer.name, 0, 0, 1, "",
timer_flag.Enabled + timer_flag.Replace + timer_flag.OneShot,
"dbot.wish.get"))
return retval
end -- dbot.wish.init.atActive
function dbot.wish.fini(doSaveState)
local retval = DRL_RET_SUCCESS
dbot.deleteTrigger(dbot.wish.trigger.itemName)
dbot.deleteTrigger(dbot.wish.trigger.startName)
-- Whack the timer just in case it is running
dbot.deleteTimer(dbot.wish.timer.name)
if (doSaveState) then
-- Save our current wish data
retval = dbot.wish.save()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.wish.fini: Failed to save wish data: " .. dbot.retval.getString(retval))
end -- if
end -- if
return retval
end -- dbot.wish.fini
function dbot.wish.save()
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. dbot.wish.name,
"dbot.wish.table", dbot.wish.table)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.wish.save: Failed to save wish table: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- dbot.wish.save
function dbot.wish.load()
local retval = dbot.storage.loadTable(dbot.backup.getCurrentDir() .. dbot.wish.name, dbot.wish.reset)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.wish.load: Failed to load table from file \"@R" ..
dbot.backup.getCurrentDir() .. dbot.wish.name .. "@W\": " .. dbot.retval.getString(retval))
end -- if
return retval
end -- dbot.wish.load
function dbot.wish.reset()
dbot.wish.table = {}
-- We handle saving dbot.module state a little differently than other modules. Most modules can
-- check if dbot.init.initializeActive is true before saving state, but dbot modules don't have that
-- luxury because we may need to reset something before we are fully initialized. Instead, we use
-- the "doForceSave" parameter of dbot.storage.saveTable() to explicitly force saving state on a
-- reset. We don't use that parameter on a normal dbot.wish.save() call.
local retval = dbot.storage.saveTable(dbot.backup.getCurrentDir() .. dbot.wish.name,
"dbot.wish.table", dbot.wish.table, true)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.wish.reset: Failed to save wish persistent data: " .. dbot.retval.getString(retval))
end -- if
return retval
end -- dbot.wish.reset
dbot.wish.inProgress = false
function dbot.wish.get()
if (dbot.wish.inProgress == true) then
dbot.info("Skipping request to get list of active wishes: another request is in progress")
return DRL_RET_BUSY
end -- if
dbot.wish.inProgress = true
wait.make(dbot.wish.getCR)
return DRL_RET_SUCCESS
end -- dbot.wish.get
function dbot.wish.getCR()
local retval = DRL_RET_SUCCESS
local charState = dbot.gmcp.getState()
local pageLines, retval = dbot.pagesize.get()
-- If we are not in the active state (i.e., AFK, sleeping, running, writing a note, etc.) then
-- we can't get the list of wishes and we need to try again later
if (charState ~= dbot.stateActive) then
dbot.note("Skipping request to get list of active wishes: you are in the state \"" ..
dbot.gmcp.getStateString(charState) .. "\"")
retval = DRL_RET_NOT_ACTIVE
-- We are in the active state and can execute commands on the server side
elseif (pageLines ~= nil) then
-- Execute the "wish list" command
-- TODO: Doh! I just found the pagesize option in the "help telnet" helpfile. This is in
-- the telnet 102 interface and it lets you enable or disable paging easily. It even
-- remembers pagesize for you. We may want to switch to that at some point.
local commandArray = {}
if (pageLines == 0) then
table.insert(commandArray, "wish list")
else
table.insert(commandArray, "pagesize 0")
table.insert(commandArray, "wish list")
table.insert(commandArray, "pagesize " .. pageLines)
end -- if
local resultData = dbot.callback.new()
retval = dbot.execute.safe.commands(commandArray, dbot.wish.setupFn, nil,
dbot.wish.resultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.wish.getCR: Failed to safely execute \"@Gwish list@W\": " ..
dbot.retval.getString(retval))
else
-- Wait for confirmation that the "wish list" safe execution command completed
retval = dbot.callback.wait(resultData, 30)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipping \"wish list\" request: " .. dbot.retval.getString(retval))
end -- if
-- Wait for the wish trigger to complete
local totTime = 0
local timeout = 5
while (dbot.wish.inProgress == true) do
if (totTime > timeout) then
dbot.warn("dbot.wish.getCR: Timed out getting list of wishes")
retval = DRL_RET_TIMEOUT
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
end -- if
else
dbot.warn("Failed to detect # of lines in the page size: " .. dbot.retval.getString(retval))
end -- if
dbot.wish.inProgress = false
-- If we weren't able to snag the list of wishes, try again later
if (retval ~= DRL_RET_SUCCESS) then
-- We can't add the timer directly because we are in the function called by that timer. Instead,
-- we use an intermediate timer to call back and start this timer again. Yeah, it's a bit ugly...
DoAfterSpecial(0.1,
"AddTimer(dbot.wish.timer.name, 0, dbot.wish.timer.min, dbot.wish.timer.sec, \"\", " ..
"timer_flag.Enabled + timer_flag.Replace + timer_flag.OneShot, \"dbot.wish.get\")",
sendto.script)
else
-- We found the wishes. Save them!
dbot.debug("dbot.wish.getCR: detected purchased wishes!")
dbot.wish.save()
end -- if
return retval
end -- dbot.wish.getCR
function dbot.wish.trigger.fn(line)
local wishName = string.match(line, ".*[-][-] ([^ -]+)[ ]*$")
if (line == nil) or (line == "") then
EnableTrigger(dbot.wish.trigger.itemName, false)
dbot.wish.inProgress = false
end -- if
if (wishName ~= nil) then
dbot.wish.table[wishName] = true
dbot.debug("Found wish name \"" .. wishName .. "\"")
end -- if
end -- dbot.wish.trigger.fn
function dbot.wish.has(wishName)
if (wishName == nil) or (wishName == "") or
(dbot.wish.table == nil) or (dbot.wish.table[wishName] == nil) then
return false
else
return true
end -- if
end -- dbot.wish.has
function dbot.wish.setupFn()
-- Add a trigger to watch for the start of the "wish list" output
check (AddTriggerEx(dbot.wish.trigger.startName,
"^.*Base.*Cost.*Adjustment.*Your.*Cost.*Keyword.*$",
"EnableTrigger(dbot.wish.trigger.itemName, true)",
drlTriggerFlagsBaseline + trigger_flag.OneShot + trigger_flag.OmitFromOutput,
custom_colour.Custom11,
0, "", "", sendto.script, 0))
dbot.pagesize.hide()
end -- dbot.wish.setupFn
function dbot.wish.resultFn(resultData, retval)
dbot.pagesize.show()
dbot.callback.default(resultData, retval)
end -- dbot.wish.resultFn
----------------------------------------------------------------------------------------------------
--
-- Module to get and set the mud's output page size
--
-- Page size refers to the number of lines of output that are displayed in one command before the mud
-- gives the user a page prompt
--
-- dbot.pagesize.init.atActive()
-- dbot.pagesize.fini(doSaveState)
--
-- dbot.pagesize.get() -- Note: this must be called from within a co-routine
--
-- dbot.pagesize.hide()
-- dbot.pagesize.show()
--
-- dbot.pagesize.trigger.fn() -- catch the # lines output
-- dbot.pagesize.setupFn(setupData) -- enable the trigger
-- dbot.pagesize.resultFn(resultData, retval) -- disable the trigger
--
-- Output syntax:
-- You currently display 55 lines per page.
-- Use 'pagesize <lines>' to change, or 'pagesize 0' to disable paging.
----------------------------------------------------------------------------------------------------
dbot.pagesize = {}
dbot.pagesize.init = {}
dbot.pagesize.trigger = {}
dbot.pagesize.lines = nil
dbot.pagesize.trigger.getName = "drlDbotPageSizeTrigger"
dbot.pagesize.trigger.suppressName = "drlDbotPageSizeSuppressTrigger"
function dbot.pagesize.init.atActive()
local retval = DRL_RET_SUCCESS
-- Trigger on the output of "pagesize" and watch for an empty line indicating that we are done
check (AddTriggerEx(dbot.pagesize.trigger.getName,
"^(" ..
"|You currently display [0-9]+ lines per page." ..
"|You do not page long messages." ..
"|Use .* to disable paging." ..
")$",
"dbot.pagesize.trigger.fn(\"%1\")",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.pagesize.trigger.getName, false)) -- default to off
check (AddTriggerEx(dbot.pagesize.trigger.suppressName,
"^(Paging disabled.|Page size set to .* lines.)$",
"",
drlTriggerFlagsBaseline + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
check (EnableTrigger(dbot.pagesize.trigger.suppressName, false)) -- default to off
return retval
end -- dbot.pagesize.init.atActive
function dbot.pagesize.fini(doSaveState)
local retval = DRL_RET_SUCCESS
dbot.deleteTrigger(dbot.pagesize.trigger.getName)
dbot.deleteTrigger(dbot.pagesize.trigger.suppressName)
-- We don't currently save the state of the page size, but we could add that here if we wanted to
return retval
end -- dbot.pagesize.fini
function dbot.pagesize.get()
local retval = DRL_RET_SUCCESS
local commandArray = { "pagesize" }
retval = dbot.execute.safe.blocking({"pagesize"}, dbot.pagesize.setupFn, nil, dbot.pagesize.resultFn, 10)
if (retval ~= DRL_RET_SUCCESS) then
dbot.note("Skipped getting \"@Gpagesize@W\": " ..
dbot.retval.getString(retval))
else
dbot.debug("Read current page size: " .. (dbot.pagesize.lines or "nil"))
end -- if
return dbot.pagesize.lines, retval
end -- dbot.pagesize.get
function dbot.pagesize.hide()
EnableTrigger(dbot.pagesize.trigger.suppressName, true)
end -- dbot.pagesize.hide
function dbot.pagesize.show()
EnableTrigger(dbot.pagesize.trigger.suppressName, false)
end -- dbot.pagesize.show
function dbot.pagesize.trigger.fn(msg)
local _, _, lines = string.find(msg, "You currently display ([%d]+) lines per page")
lines = tonumber(lines or "")
if (msg == "You do not page long messages.") then
dbot.pagesize.lines = 0
elseif (lines ~= nil) then
dbot.pagesize.lines = lines
end -- if
end -- dbot.pagesize.trigger.fn
function dbot.pagesize.setupFn(setupData)
EnableTrigger(dbot.pagesize.trigger.getName, true)
end -- dbot.pagesize.setupFn
function dbot.pagesize.resultFn(resultData, retval)
EnableTrigger(dbot.pagesize.trigger.getName, false)
dbot.callback.default(resultData, retval)
end -- dbot.pagesize.resultFn
----------------------------------------------------------------------------------------------------
--
-- Module to execute one more commands on the mud server side
--
-- Commands come in two flavors:
-- 1) "Fast": these are non-atomic and can be interrupted at any time. There are no guarantees that
-- we will know when "fast" commands execute which means that we can't know for certain if triggers
-- are matching on any particular command output.
-- 2) "Safe": safe commands guarantee that there will be no contention with any commands sent by the
-- user. Safe commands run atomically on the mud at a time when we guarantee that no other commands
-- can execute on the mud server. We do this by halting any new commands (via the OnPluginSend()
-- callback) and queuing up those commands to run after our atomic critical section completes. We
-- guarantee that no user commands are lost and they will execute in the same order as they were
-- sent. We also guarantee that user-supplied setup and cleanup callbacks will be executed during
-- the critical section without any contention from commands the user enters. This allows us to
-- set up and clean up triggers and know that if a trigger goes off that it is triggering on the
-- output we want -- not something that the user entered at the command line. Safe commands are
-- very convenient because they also let us cleanly handle cases where the user sleeps, enters
-- combat, or goes AFK unexpectedly. Knowing that these state changes can't occur during one of
-- our "safe" critical sections greatly simplifies the code.
--
-- Here are the steps to safely run a background command without interference
-- 1) Spin until we are in the "active" state (e.g., not AFK, sleeping, etc.)
-- 2) Prevent new commands from going to the mud by watching for them in OnPluginSend() and queuing
-- them up for future execution
-- 3) Add a one-shot trigger to catch a unique prefix message we will echo to the mud
-- 4) Echo a unique prefix message to the mud
-- 5) Wait for the trigger to hit and tell us that our unique prefix message was executed by
-- the mud. No other commands can be pending at the mud's server side at this point.
-- 6) Send the command(s) we want to run atomically
-- 7) Execute any user-supplied setup callback to configure triggers
-- 8) Capture the output (either success or failure) with user-supplied triggers
-- 9) Add a one-shot trigger to catch a unique suffix message we will echo to the mud
-- 10) Send a unique suffix message to the mud
-- 11) Wait until our trigger detects that the mud echoed our suffix
-- 12) If the caller provided us a cleanup/result callback function, execute it now to let the caller
-- know that we are done and to tell the caller the result status
-- 13) Send any pending commands that we queued up during the safe critical section
-- 14) Unblock the OnPluginSend() callback so that new commands from the user go straight to the mud again
--
-- Functions:
--
-- dbot.execute.init.atActive()
-- dbot.execute.fini(doSaveState)
--
-- dbot.execute.fast.command(commandString)
-- dbot.execute.fast.commands(commandArray)
--
-- dbot.execute.safe.command(commandString, setupFn, setupData, resultFn, resultData)
-- dbot.execute.safe.commands(commandArray, setupFn, setupData, resultFn, resultData)
-- dbot.execute.safe.commandsCR()
-- dbot.execute.safe.blocking(commandArray, setupFn, setupData, resultFn, timeout)
--
-- dbot.execute.queue.enable()
-- dbot.execute.queue.disable()
-- dbot.execute.queue.pushFast(command)
-- dbot.execute.queue.pushSafe(commandArray, setupFn, setupData, resultFn, resultData)
-- dbot.execute.queue.pop()
-- dbot.execute.queue.fence()
-- dbot.execute.queue.bypass(command)
-- dbot.execute.queue.dequeueCR()
-- dbot.execute.queue.getCommandString(commandArray)
-- dbot.execute.new()
--
-- Data:
-- dbot.execute.table
--
----------------------------------------------------------------------------------------------------
dbot.execute = {}
dbot.execute.table = {}
dbot.execute.queue = {}
dbot.execute.init = {}
dbot.execute.fast = {}
dbot.execute.safe = {}
dbot.execute.doDelayCommands = false
dbot.execute.isDequeueRunning = false
dbot.execute.afkIsPending = false
dbot.execute.quitIsPending = false
dbot.execute.noteIsPending = false
dbot.execute.fenceIsDetected = false
dbot.execute.bypassPrefix = "DINV_BYPASS "
dbot.execute.trigger = {}
dbot.execute.trigger.fenceName = "drlDbotExecuteFenceTrigger"
drlDbotExecuteTypeFast = "fast"
drlDbotExecuteTypeSafe = "safe"
function dbot.execute.init.atActive()
-- Placeholder: nothing to do here yet
return DRL_RET_SUCCESS
end -- dbot.execute.init.atActive
function dbot.execute.fini(doSaveState)
-- These are one-shot triggers and shouldn't exist here, but it doesn't hurt to verify
-- that they are gone
dbot.deleteTrigger(dbot.execute.trigger.fenceName)
dbot.execute.doDelayCommands = false
dbot.execute.isDequeueRunning = false
dbot.execute.afkIsPending = false
dbot.execute.quitIsPending = false
dbot.execute.noteIsPending = false
if (doSaveState) then
-- Placeholder: If we ever add state to the execute module we should save it here
end -- if
return DRL_RET_SUCCESS
end -- dbot.execute.fini
function dbot.execute.fast.command(commandString)
local origEchoInput = GetEchoInput()
SetEchoInput(false)
check (Execute(commandString))
SetEchoInput(origEchoInput)
return DRL_RET_SUCCESS
end -- dbot.execute.fast.command
-- This function simply pushes the commands in the command array to the mud server. It doesn't
-- do any type of error checking or handling for the commands that are sent. It does not require
-- a co-routine because it executes all in one shot and never yields.
function dbot.execute.fast.commands(commandArray)
local retval = DRL_RET_SUCCESS
if (commandArray == nil) then
dbot.warn("dbot.execute.fast.commands: Missing command array parameter")
return DRL_RET_INVALID_PARAM
end -- if
-- We don't want all of our commands to echo to the client. Remember what the original echo
-- state is so that we can restore that state when we are done.
local origEchoInput = GetEchoInput()
SetEchoInput(false)
for _, command in ipairs(commandArray) do
if (command ~= nil) and (command ~= "") then
check (Execute(command))
end -- if
end -- for
-- Restore the echo state before we return
SetEchoInput(origEchoInput)
return retval
end -- dbot.execute.fast.commands
-- This is a special case for the dbot.execute.safe.commands() call which uses just a
-- single command string. The command is run atomically with nothing else in the mud's
-- command queue to interfere with it.
function dbot.execute.safe.command(commandString, setupFn, setupData, resultFn, resultData)
local commandArray = {}
table.insert(commandArray, commandString)
return dbot.execute.safe.commands(commandArray, setupFn, setupData, resultFn, resultData)
end -- dbot.execute.safe.command
function dbot.execute.safe.commands(commandArray, setupFn, setupData, resultFn, resultData)
local retval
-- Enter a critical section if one isn't already in progress
dbot.execute.queue.enable()
-- Push the current request onto the end of the command queue
retval = dbot.execute.queue.pushSafe(commandArray, setupFn, setupData, resultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.execute.safe.commads: Failed to push request onto queue: " ..
dbot.retval.getString(retval))
else
-- We just added a new command to the command queue. If we don't already have a
-- co-routine processing commands on that queue, start one now.
if (not dbot.execute.queue.isDequeueRunning) then
dbot.execute.queue.isDequeueRunning = true
wait.make(dbot.execute.queue.dequeueCR)
end -- if
end -- if
return retval
end -- dbot.execute.safe.commands
function dbot.execute.safe.blocking(commandArray, setupFn, setupData, resultFn, timeout)
if (commandArray == nil) then
dbot.warn("dbot.execute.safe.blocking: Missing command array parameter")
return DRL_RET_INVALID_PARAM
end -- if
timeout = tonumber(timeout or "")
if (timeout == nil) then
dbot.warn("dbot.execute.safe.blocking: Missing timeout parameter")
return DRL_RET_INVALID_PARAM
end -- if
-- If there is nothing to do, do it successfully :)
if (#commandArray < 1) then
return DRL_RET_SUCCESS
end -- if
local resultData = dbot.callback.new()
local retval = dbot.execute.safe.commands(commandArray, setupFn, setupData, resultFn, resultData)
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("dbot.execute.safe.blocking: Failed to execute command array: " ..
dbot.retval.getString(retval))
else
-- Wait for the callback to confirm that the safe execution completed
retval = dbot.callback.wait(resultData, timeout)
end -- if
return retval
end -- dbot.execute.safe.blocking
function dbot.execute.queue.enable()
dbot.execute.doDelayCommands = true
end -- dbot.execute.queue.get
function dbot.execute.queue.disable()
dbot.execute.doDelayCommands = false
end -- dbot.execute.queue.get
-- command table format:
-- { commandType = drlDbotExecuteTypeFast, -- or drlDbotExecuteTypeSafe
-- commands = { "command 1",
-- "command 2",
-- ...
-- "command N"
-- }
-- setupFn = mySetupFunctionCallback
-- setupData = mySetupDataParameter
-- resultFn = myResultFunctionCallback
-- resultData = myResultDataParameter
-- }
function dbot.execute.queue.pushFast(command)
local commandEntry
if (command == nil) then
dbot.warn("dbot.execute.queue.pushFast: command is missing")
return DRL_RET_INVALID_PARAM
end -- if
commandEntry = {}
commandEntry.commandType = drlDbotExecuteTypeFast
commandEntry.commands = { command }
commandEntry.setupFn = nil
commandEntry.setupData = nil
commandEntry.resultFn = nil
commandEntry.resultData = nil
table.insert(dbot.execute.table, commandEntry)
dbot.debug("Queued fast command: \"@G" .. command .. "@W\"")
return DRL_RET_SUCCESS
end -- dbot.execute.queue.pushFast
function dbot.execute.queue.pushSafe(commandArray, setupFn, setupData, resultFn, resultData)
local commandEntry
if (commandArray == nil) then
dbot.warn("dbot.execute.queue.pushSafe: command array is missing")
return DRL_RET_INVALID_PARAM
end -- if
commandEntry = {}
commandEntry.commandType = drlDbotExecuteTypeSafe
commandEntry.commands = commandArray
commandEntry.setupFn = setupFn
commandEntry.setupData = setupData
commandEntry.resultFn = resultFn
commandEntry.resultData = resultData
table.insert(dbot.execute.table, commandEntry)
local commandString = dbot.execute.queue.getCommandString(commandArray)
dbot.debug("Queued safe commands: \"@G" .. commandString .. "@W\"")
return DRL_RET_SUCCESS
end -- dbot.execute.queue.pushSafe
function dbot.execute.queue.pop()
local commandEntry = table.remove(dbot.execute.table, 1)
return commandEntry
end -- dbot.execute.queue.pop
-- We need a unique prefix to attach to a fence command so that we don't confuse one fence
-- with another. The simplest way to do that is to simply count fence commands and prepend
-- the count to the fence name.
dbot.execute.queue.fenceCounter = 1
-- The fence call sends an echo command to the mud server and blocks until the echo is detected.
-- This is very useful in conjuction with our OnPluginSend() code that queues up and delays user
-- commands. If we delay new commands from going to the mud and we detect our fence output, then
-- we know that no other commands are pending on the mud server and there won't be interference
-- on what we send next.
--
-- We only call fence() from within a critical section where we have checked that we are in a
-- user state that allows the fence to proceed. We don't need to worry about being AFK here.
function dbot.execute.queue.fence()
local uniqueString = "{ dbot.execute.queue.fence " .. dbot.execute.queue.fenceCounter .. " }"
-- We will spin on this until we match the fence command in our trigger
dbot.execute.fenceIsDetected = false
-- Add a one-shot trigger to catch the fence message that we will echo to the mud
check (AddTriggerEx(dbot.execute.trigger.fenceName,
"^.*" .. uniqueString .. ".*$",
"dbot.execute.fenceIsDetected = true",
drlTriggerFlagsBaseline + trigger_flag.OneShot + trigger_flag.OmitFromOutput,
custom_colour.Custom11, 0, "", "", sendto.script, 0))
-- If we are blocking new commands, we must explicitly bypass that protection in order to send
-- a command to the mud server.
local retval = dbot.execute.queue.bypass("echo " .. uniqueString)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.execute.queue.fence: Failed to bypass command: " .. dbot.retval.getString(retval))
return retval
end -- if
-- Spin until we have confirmation that the mud received the fence message or until we detect
-- that we are in a state that will prevent the message from completing
totTime = 0
timeout = 20 -- wait a while since we there might be a lot of stuff queued up on the server
while (dbot.execute.fenceIsDetected == false) do
local charState = dbot.gmcp.getState()
if (inv.state == invStateHalted) then
dbot.note("Skipping fence request: plugin is halted!")
retval = DRL_RET_UNINITIALIZED
break
elseif (totTime > timeout) then
dbot.note("Skipping fence request: fence message timed out")
retval = DRL_RET_TIMEOUT
break
elseif ((charState ~= dbot.stateActive) and
(charState ~= dbot.stateCombat) and
(charState ~= dbot.stateRunning)) then
dbot.note("Skipping fence request: you are in the \"@C" .. dbot.gmcp.getStateString(charState) ..
"@W\" state")
retval = DRL_RET_NOT_ACTIVE
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
-- Remove the trigger if an error occurred and the one-shot trigger is still pending
if (retval ~= DRL_RET_SUCCESS) then
DeleteTrigger(dbot.execute.trigger.fenceName)
end -- if
dbot.execute.queue.fenceCounter = dbot.execute.queue.fenceCounter + 1
return retval
end -- dbot.execute.queue.fence
function dbot.execute.queue.bypass(command)
if (command == nil) then
dbot.warn("dbot.execute.queue.bypass: missing command parameter")
return DRL_RET_INVALID_PARAM
end -- if
--dbot.note("Bypassing command \"@G" .. command .. "@W\"")
check (SendNoEcho(dbot.execute.bypassPrefix .. command))
return DRL_RET_SUCCESS
end -- dbot.execute.queue.bypass
function dbot.execute.queue.dequeueCR()
local retval = DRL_RET_SUCCESS
while (#dbot.execute.table > 0) do
local charState = dbot.gmcp.getState()
-- Get the next command (or array of commands) from the queue
local commandEntry = dbot.execute.queue.pop()
if (commandEntry == nil) then
dbot.warn("dbot.execute.queue.dequeueCR: Popped a nil entry off of a non-empty queue")
break
end -- if
-- Creating a string that concatenates all of the commands is helpful for debugging and notifications
local commandString = dbot.execute.queue.getCommandString(commandEntry.commands)
-- If we have a "fast" command, send it directly to the mud. Easy peasy.
if (commandEntry.commandType == drlDbotExecuteTypeFast) then
if (commandEntry.commands ~= nil) then
for _, command in ipairs(commandEntry.commands) do
dbot.debug("Executing queued fast command \"@G" .. command .. "@W\"")
retval = dbot.execute.queue.bypass(command)
end -- for
end -- if
elseif (commandEntry.commandType == drlDbotExecuteTypeSafe) then
-- If we are in the running state, wait for a while until we get out of that state.
-- The char can't run forever :) We could also wait if we are in another state that
-- prevents us from executing, but the odds of someone coming out of AFK or sleeping
-- at just the right moment are low. We know that a run will end relatively soon so
-- we handle that case a little differently to reduce the odds of aborting a request.
local totTime = 0
local timeout = 20
while (timeout < totTime) do
charState = dbot.gmcp.getState()
if (charState ~= dbot.stateRunning) then
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
-- We can only run safe execution commands when we are either active or in combat
--
-- If a safe execution command cannot run because the user is too busy or if the user
-- is in the wrong state (AFK, sleeping, note, etc.) we let the caller know via the
-- callback parameter's return value field. It is up to the caller to resubmit the
-- request if they really want it to run. We do not try to re-queue the request here.
-- Yes, we could re-queue the request and set a timer to attempt to run it later, but
-- that's a lot of complexity for low odds of success. If the user really is AFK or
-- sleeping, the calling function will almost certainly time out before the state is
-- what we need so we make it our policy to leave everything in the caller's hands.
if (charState ~= dbot.stateActive) and (charState ~= dbot.stateCombat) then
dbot.note("Skipping queued safe commands: \"@G" .. commandString .. "@W\": you are in state \"@C" ..
dbot.gmcp.getStateString(charState) .. "@W\"")
retval = DRL_RET_NOT_ACTIVE
elseif dbot.execute.quitIsPending then
dbot.note("Skipping queued safe commands: \"@G" .. commandString ..
"@W\": a request to quit is pending on the mud server")
retval = DRL_RET_UNINITIALIZED
elseif dbot.execute.afkIsPending then
dbot.note("Skipping queued safe commands: \"@G" .. commandString ..
"@W\": a request to go AFK is pending on the mud server")
retval = DRL_RET_NOT_ACTIVE
elseif dbot.execute.noteIsPending then
dbot.note("Skipping queued safe commands: \"@G" .. commandString ..
"@W\": a request start a note is pending on the mud server")
retval = DRL_RET_NOT_ACTIVE
else
retval = dbot.execute.queue.fence()
if (retval ~= DRL_RET_SUCCESS) then
dbot.debug("dbot.execute.queue.dequeueCR: Failed to execute prefix fence: " ..
dbot.retval.getString(retval))
else
if (commandEntry.setupFn ~= nil) then
commandEntry.setupFn(commandEntry.setupData)
end -- if
dbot.prompt.hide()
if (commandEntry.commands ~= nil) then
for _, command in ipairs(commandEntry.commands) do
dbot.debug("Executing queued safe command \"@G" .. command .. "@W\"")
dbot.execute.queue.bypass(command)
end -- for
else
dbot.warn("dbot.execute.queue.dequeueCR: commands array is nil!!!")
end -- if
retval = dbot.execute.queue.fence()
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.execute.queue.dequeueCR: Failed to execute suffix fence: " ..
dbot.retval.getString(retval))
end -- if
dbot.prompt.show()
end -- if
end -- if
else
dbot.warn("dbot.execute.queue.dequeueCR: invalid commandType field \"@R" ..
(commandEntry.commandType or "nil") .. "@W\"")
retval = DRL_RET_INTERNAL_ERROR
end -- if
if (commandEntry.resultFn ~= nil) then
commandEntry.resultFn(commandEntry.resultData, retval)
end -- if
end -- while
dbot.execute.queue.isDequeueRunning = false
dbot.execute.queue.disable()
end -- dbot.execute.queue.dequeueCR
-- Build up a command string containing all commands in the command array delimited by semicolons.
-- This is useful for notification messages and debugging.
function dbot.execute.queue.getCommandString(commandArray)
local commandString = ""
if (commandArray ~= nil) and (#commandArray > 0) then
for _, command in ipairs(commandArray) do
if (command ~= "") then
if (commandString == nil) or (commandString == "") then
commandString = command
else
commandString = commandString .. "; " .. command
end -- if
end -- if
end -- for
end -- if
return commandString
end -- dbot.execute.queue.getCommandString
function dbot.execute.new()
if (not inv.inSafeMode) then
return {}
end -- if
return nil
end -- dbot.execute.newCommands
----------------------------------------------------------------------------------------------------
-- Module to assist with callback management
--
-- dbot.callback.new()
-- dbot.callback.setReturn(resultData, value)
-- dbot.callback.getReturn(resultData)
-- dbot.callback.isDone(resultData) -- returns true or false
-- dbot.callback.default(resultData, retval))
-- dbot.callback.wait(resultData, timeoutInSec, periodInSec)
--
----------------------------------------------------------------------------------------------------
dbot.callback = {}
function dbot.callback.new()
return { isDone = false, retval = DRL_RET_UNINITIALIZED }
end -- dbot.callback.new
function dbot.callback.setReturn(resultData, value)
if (resultData == nil) then
dbot.warn("dbot.callback.setReturn: callback parameter is nil")
return DRL_RET_INVALID_PARAM
end -- if
if (value == nil) then
dbot.warn("dbot.callback.setReturn: missing value parameter")
return DRL_RET_INVALID_PARAM
end -- if
resultData.retval = value
return DRL_RET_SUCCESS
end -- dbot.callback.setReturn
function dbot.callback.getReturn(resultData)
if (resultData == nil) then
dbot.warn("dbot.callback.getReturn: callback parameter is nil")
return DRL_RET_INVALID_PARAM
end -- if
return resultData.retval
end -- dbot.callback.getReturn
function dbot.callback.isDone(resultData) -- returns true or false
if (resultData ~= nil) then
return resultData.isDone
else
return false
end -- if
end -- dbot.callback.isDone
function dbot.callback.default(resultData, retval)
if (resultData ~= nil) then
if (retval ~= nil) then
dbot.callback.setReturn(resultData, retval)
end -- if
resultData.isDone = true
end -- if
end -- dbot.callback.default
function dbot.callback.wait(resultData, timeout, period)
local retval = DRL_RET_SUCCESS
local totTime = 0
if (resultData == nil) then
dbot.warn("dbot.callback.wait: missing callback parameter")
return DRL_RET_INVALID_PARAM
end -- if
timeout = tonumber(timeout or "")
if (timeout == nil) then
dbot.warn("dbot.callback.wait: missing timeout parameter")
return DRL_RET_INVALID_PARAM
end -- if
-- The period is optional. If it isn't provided, use the default value.
if (period == nil) then
period = drlSpinnerPeriodDefault
end -- if
while (dbot.callback.isDone(resultData) == false) do
if (totTime > timeout) then
dbot.debug("dbot.callback.wait: timed out waiting for a callback to complete")
retval = DRL_RET_TIMEOUT
break
end -- if
wait.time(period)
totTime = totTime + period
end -- while
-- If there was a problem accessing the callback, return an error corresponding to the problem
-- we hit. Otherwise, return the return value for the operation executed by the callback.
if (retval ~= DRL_RET_SUCCESS) then
return retval
else
return dbot.callback.getReturn(resultData)
end -- if
end -- dbot.callback.wait
----------------------------------------------------------------------------------------------------
-- Module to retrieve remote files
--
-- dbot.remote.get(url, protocol)
-- dbot.remote.getCR()
--
----------------------------------------------------------------------------------------------------
dbot.remote = {}
dbot.remote.getPkg = nil
-- Blocks and then returns file, retval
-- Must be called from within a co-routine
function dbot.remote.get(url, protocol)
local retval = DRL_RET_SUCCESS
local fileData = nil
if (url == nil) or (url == "") then
dbot.warn("dbot.remote.get: missing url parameter")
return fileData, DRL_RET_INVALID_PARAMETER
end -- if
if (protocol == nil) or (protocol == "") then
dbot.warn("dbot.remote.get: missing protocol parameter")
return fileData, DRL_RET_INVALID_PARAMETER
end -- if
if (dbot.remote.getPkg ~= nil) then
dbot.info("Skipping remote request: another request is in progress")
return fileData, DRL_RET_BUSY
end -- if
dbot.remote.getPkg = {}
dbot.remote.getPkg.url = url
dbot.remote.getPkg.protocol = protocol
dbot.remote.getPkg.isDone = false
wait.make(dbot.remote.getCR)
local timeout = 10
local totTime = 0
while (dbot.remote.getPkg.isDone == false) do
if (totTime > timeout) then
retval = DRL_RET_TIMEOUT
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
if (dbot.remote.getPkg ~= nil) and (dbot.remote.getPkg.fileData ~= nil) then
fileData = dbot.remote.getPkg.fileData
else
dbot.warn("dbot.remote.get: Failed to find data for file \"@G" .. url .. "@W\"")
retval = DRL_RET_MISSING_ENTRY
end -- if
dbot.remote.getPkg = nil
return fileData, retval
end -- dbot.remote.get
function dbot.remote.getCR()
local retval = DRL_RET_SUCCESS
if (dbot.remote.getPkg == nil) or (dbot.remote.getPkg.url == nil) then
dbot.error("dbot.remote.getCR: remote package is nil or corrupted!")
dbot.remote.getPkg = nil
return DRL_RET_INTERNAL_ERROR
end -- if
local urlThread = async.request(dbot.remote.getPkg.url, dbot.remote.getPkg.protocol)
if (urlThread == nil) then
dbot.warn("dbot.remote.getCR: Failed to create thread requesting remote data")
retval = DRL_RET_INTERNAL_ERROR
else
local timeout = 10
local totTime = 0
while (urlThread:alive()) do
if (totTime > timeout) then
retval = DRL_RET_TIMEOUT
break
end -- if
wait.time(drlSpinnerPeriodDefault)
totTime = totTime + drlSpinnerPeriodDefault
end -- while
local remoteRet, page, status, headers, fullStatus = urlThread:join()
if (status ~= 200) then
dbot.warn("dbot.remote.getCR: Failed to retrieve remote file")
retval = DRL_RET_INTERNAL_ERROR
else
dbot.remote.getPkg.fileData = page
end -- if
dbot.remote.getPkg.isDone = true
end -- if
return retval
end -- dbot.remote.getCR
----------------------------------------------------------------------------------------------------
-- dbot.version: Track the plugin's version and changelog and update the plugin
--
-- dbot.version.changelog.get(minVersion, endTag)
-- dbot.version.changelog.getCR()
-- dbot.version.changelog.displayChanges(minVersion, changeLog)
-- dbot.version.changelog.displayChange(changeLogEntries)
--
-- dbot.version.update.release(mode, endTag)
-- dbot.version.update.releaseCR()
-- Note: dbot.version.update is derived from a plugin written by Arcidayne. Thanks Arcidayne!
----------------------------------------------------------------------------------------------------
dbot.version = {}
dbot.version.changelog = {}
dbot.version.changelog.pkg = nil
dbot.version.update = {}
dbot.version.update.pkg = nil
drlDbotUpdateCheck = "check"
drlDbotUpdateInstall = "install"
drlDbotChangeLogTypeFix = "@RFix@W"
drlDbotChangeLogTypeNew = "@GNew@W"
drlDbotChangeLogTypeMisc = "@yMsc@W"
function dbot.version.changelog.get(minVersion, endTag)
local url = "https://raw.githubusercontent.com/Aardurel/aard-plugins/master/aard_inventory.changelog"
local protocol = "HTTPS"
if (dbot.version.changelog.pkg ~= nil) then
dbot.info("Skipping changelog request: another request is in progress")
return inv.tags.stop(invTagsVersion, endTag, DRL_RET_BUSY)
end -- if
dbot.version.changelog.pkg = {}
dbot.version.changelog.pkg.url = url
dbot.version.changelog.pkg.protocol = protocol
dbot.version.changelog.pkg.minVersion = minVersion or 0
dbot.version.changelog.pkg.endTag = endTag
wait.make(dbot.version.changelog.getCR)
return DRL_RET_SUCCESS
end -- dbot.version.changelog.get
function dbot.version.changelog.getCR()
if (dbot.version.changelog.pkg == nil) then
dbot.error("dbot.version.changelog.getCR: Change log package is missing!")
return inv.tags.stop(invTagsVersion, "missing end tag", DRL_RET_INTERNAL_ERROR)
end -- if
local fileData, retval = dbot.remote.get(dbot.version.changelog.pkg.url,
dbot.version.changelog.pkg.protocol)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.version.changelog.getCR: Failed to retrieve remote changelog file: " ..
dbot.retval.getString(retval))
elseif (fileData == nil) then
dbot.info("No changelog information was found.")
else
loadstring(fileData)()
if (dbot.changelog == nil) then
dbot.warn("dbot.version.changelog.getCR: Invalid changelog format detected")
retval = DRL_RET_INTERNAL_ERROR
else
retval = dbot.version.changelog.displayChanges(dbot.version.changelog.pkg.minVersion, dbot.changelog)
end -- if
end -- if
dbot.version.changelog.pkg = nil
return inv.tags.stop(invTagsVersion, endTag, retval)
end -- dbot.version.changelog.getCR
function dbot.version.changelog.displayChanges(minVersion, changeLog)
local sortedLog = {}
for k, v in pairs(changeLog) do
table.insert(sortedLog, { version = tonumber(k) or 0, changes = v})
end -- for
table.sort(sortedLog, function (v1, v2) return v1.version < v2.version end)
for _, clog in ipairs(sortedLog) do
if (clog.version > minVersion) then
dbot.version.changelog.displayChange(clog)
end -- if
end -- for
return DRL_RET_SUCCESS
end -- dbot.version.changelog.displayChanges
-- Format of entry is: { version = 2.0004,
-- changes = { { change = drlDbotChangeLogTypeXYZ, desc = "what changed" }
-- }
-- }
function dbot.version.changelog.displayChange(changeLogEntries)
local retval = DRL_RET_SUCCESS
if (changeLogEntries == nil) then
dbot.warn("dbot.version.changelog.displayChange: Change entries are missing!")
return DRL_RET_INVALID_PARAM
end -- if
dbot.print(string.format("@Cv%1.4f@W", changeLogEntries.version))
for _, logEntry in ipairs(changeLogEntries.changes) do
dbot.print(string.format("@W (%s): %s", logEntry.change, logEntry.desc))
end -- for
return retval
end -- dbot.version.changelog.displayChange
function dbot.version.update.release(mode, endTag)
local url = "https://raw.githubusercontent.com/Aardurel/aard-plugins/master/aard_inventory.xml"
local protocol = "HTTPS"
local retval = DRL_RET_SUCCESS
if (mode == nil) or ((mode ~= drlDbotUpdateCheck) and (mode ~= drlDbotUpdateInstall)) then
dbot.warn("dbot.version.update.release: Missing or invalid mode parameter")
return inv.tags.stop(invTagsVersion, endTag, DRL_RET_INVALID_PARAM)
end -- if
if (dbot.version.update.pkg ~= nil) then
dbot.info("Skipping update request: another update request is in progress")
return inv.tags.stop(invTagsVersion, endTag, DRL_RET_BUSY)
end -- if
dbot.version.update.pkg = {}
dbot.version.update.pkg.mode = mode
dbot.version.update.pkg.url = url
dbot.version.update.pkg.protocol = protocol
dbot.version.update.pkg.endTag = endTag
wait.make(dbot.version.update.releaseCR)
return retval
end -- dbot.version.update.release
function dbot.version.update.releaseCR()
if (dbot.version.update.pkg == nil) or (dbot.version.update.pkg.mode == nil) then
dbot.error("dbot.version.update.releaseCR: Missing or invalid update package detected")
return inv.tags.stop(invTagsVersion, "end tag is nil", DRL_RET_INVALID_PARAM)
end -- if
local endTag = dbot.version.update.pkg.endTag
-- This blocks until the plugin file is returned, an error is detected, or we time out
local pluginData, retval = dbot.remote.get(dbot.version.update.pkg.url, dbot.version.update.pkg.protocol)
if (retval ~= DRL_RET_SUCCESS) then
dbot.warn("dbot.version.update.releaseCR: Failed to retrieve latest plugin file: " ..
dbot.retval.getString(retval))
elseif (pluginData == nil) then
dbot.info("Could not find a remote plugin release")
retval = DRL_RET_MISSING_ENTRY
else
local currentVersion = GetPluginInfo(GetPluginID(), 19) or 0
local currentVerStr = string.format("%1.4f", currentVersion)
local remoteVerStr = string.match(pluginData, '%s%s+version="([0-9%.]+)"')
local remoteVersion = tonumber(remoteVerStr or "") or 0
if (remoteVersion == currentVersion) then
dbot.info("You are running the most recent plugin (v" .. currentVerStr .. ")")
elseif (remoteVersion < currentVersion) then
dbot.warn("Your current plugin (v" .. currentVerStr .. ") " ..
"is newer than the latest official release (v" .. remoteVerStr .. ")")
retval = DRL_RET_VER_MISMATCH
elseif (dbot.version.update.pkg.mode == drlDbotUpdateCheck) then
dbot.info("You are running v" .. currentVerStr .. ", latest version is v" .. remoteVerStr)
dbot.info("Changes since your last update:")
dbot.version.update.pkg = nil
return dbot.version.changelog.get(currentVersion, endTag)
elseif (dbot.version.update.pkg.mode == drlDbotUpdateInstall) then
dbot.info("Updating plugin from version " .. currentVerStr .. " to version " .. remoteVerStr)
dbot.info("Please do not enter anything until the update completes")
local pluginFile = GetPluginInfo(GetPluginID(), 6)
local file = io.open(pluginFile, "w")
file:write(pluginData)
file:close()
dbot.reload()
else
dbot.error("dbot.version.update.callback: Detected invalid mode \"@R" ..
(dbot.version.update.pkg.mode or "nil") .. "@W\"")
end -- if
end -- if
dbot.version.update.pkg = nil
return inv.tags.stop(invTagsVersion, endTag, retval)
end -- dbot.version.update.releaseCR
]]>
</script>
<!-- Plugin help -->
<aliases>
<alias
script="OnHelp"
match="dinv:help"
enabled="y"
>
</alias>
</aliases>
<script>
<![CDATA[
function OnHelp ()
world.Note (world.GetPluginInfo (world.GetPluginID (), 3))
end
]]>
</script>
</muclient
>