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.
20701 lines
844 KiB
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
|
|
>
|