summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/organizer
diff options
context:
space:
mode:
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/organizer')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/organizer/ReadMe.md89
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/organizer/items.lua469
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua565
3 files changed, 1123 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/organizer/ReadMe.md b/Data/BuiltIn/Libraries/lua-addons/addons/organizer/ReadMe.md
new file mode 100644
index 0000000..fc1e527
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/organizer/ReadMe.md
@@ -0,0 +1,89 @@
+# Organizer (//org)
+
+A multi-purpose inventory management solution. Similar to GearCollector; uses packets.
+
+For the purpose of this addon, a `bag` is: "Safe", "Storage", "Locker", "Satchel", "Sack", "Case", "Wardrobe", "Safe 2".
+
+For commands that use a filename, if one is not specified, it defaults to Name_JOB.lua, e.g., Rooks_PLD.lua
+For commands that specify a bag, if one is not specified, it defaults to all, and will cycle through all of them.
+
+The addon command is `org`, so `org freeze` will freeze, etc.
+
+This utility is still in development and there are at least a couple of known issues (it does not always move out gear that is currently equipped, argument parsing could be better). It is designed to work simplest as a snapshotting utility (freeze and organize without arugments), but it should work no matter what you want to do with it.
+
+### Settings
+
+#### auto_heal
+Setting this feature to anything other than false will cause Organizer to use /heal after getting/storing gear.
+
+#### bag_priority
+The order that bags will be looked in for requested gear.
+
+#### dump_bags
+The order that bags will be filled with unspecified gear from your inventory.
+
+#### item_delay
+A delay, in seconds, between item storage/retrieval. Defaults to 0 (no delay)
+
+
+### Commands
+Commands below are written with their arguments indicated using square brackets, but you should not use square brackets when entering the commands in game. Default options are italicized.
+
+#### Freeze
+
+```
+freeze [bag] [filename]
+```
+
+Freezes the current contents of a `bag` or **all bags** to the specified `filename` or **Name_ShortJob.lua** in the respective data directory/directories. This effectively takes a snapshot of your inventory for that job. So using `//org freeze` as a Dancer named Pablo would result in freezing all of your bags in files named Pablo_DNC.lua.
+
+#### Get
+
+```
+get [bag] [filename]
+```
+
+Thaws the frozen state specified by `filename` or **Name_ShortJob.lua** and `bag` or **all bags** and makes one attempt to move towards that state.
+
+
+#### Tidy
+
+```
+tidy [bag] [filename]
+```
+
+Thaws a frozen state specified by `filename` or **Name_ShortJob.lua** and `bag` or **all bags** and makes one attempt to purge anything currently in inventory that shouldn't be into dump bags.
+
+#### Organize
+
+```
+organize [bag] [filename]
+```
+
+Thaws a frozen state specified by `filename` or **Name_ShortJob.lua** and `bag` or **all bags** and executes repeated Get and Tidy commands until a steady state is reached (aka. you have your gear). With no arguments, it will attempt to restore the entire thawed snapshot.
+
+### Gearswap integration
+Additionally, Organizer integrates with GearSwap. In your lua, just add this:
+
+```
+include('organizer-lib')
+```
+
+And then in your Mog House, after changing jobs:
+
+```
+//gs org
+```
+
+And it will fill your inventory with the items from your sets, and put everything else away (it does a very good job, even when there are space concerns, but it's not perfect. Make sure to do a "//gs validate" after!)
+
+Additionally, if you have extra items you want to bring along, simply define a table named `organizer_items` like so:
+
+```
+organizer_items = {
+ echos="Echo Drops",
+ shihei="Shihei",
+ orb="Macrocosmic Orb"
+}
+```
+
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/organizer/items.lua b/Data/BuiltIn/Libraries/lua-addons/addons/organizer/items.lua
new file mode 100644
index 0000000..20d9160
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/organizer/items.lua
@@ -0,0 +1,469 @@
+--Copyright (c) 2015, Byrthnoth and Rooks
+--All rights reserved.
+
+--Redistribution and use in source and binary forms, with or without
+--modification, are permitted provided that the following conditions are met:
+
+-- * Redistributions of source code must retain the above copyright
+-- notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright
+-- notice, this list of conditions and the following disclaimer in the
+-- documentation and/or other materials provided with the distribution.
+-- * Neither the name of <addon name> nor the
+-- names of its contributors may be used to endorse or promote products
+-- derived from this software without specific prior written permission.
+
+--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+--ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+--WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+--DISCLAIMED. IN NO EVENT SHALL <your name> BE LIABLE FOR ANY
+--DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+--(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+--LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+--ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+--(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+--SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+local Items = {}
+local items = {}
+local bags = {}
+local item_tab = {}
+
+do
+ local names = {'Nomad Moogle'} -- don't add Pilgrim Moogle to this list, organizer currently does not work in Odyssey.
+ local moogles = {}
+ local poked = false
+ local block_menu = false
+
+ clear_moogles = function()
+ moogles = {}
+ poked = false
+ end
+
+ local poke_moogle = function(npc)
+ local p = packets.new('outgoing', 0x1a, {
+ ["Target"] = npc.id,
+ ["Target Index"] = npc.index,
+ })
+ poked = true
+ block_menu = true
+ packets.inject(p)
+ repeat
+ coroutine.sleep(0.4)
+ until not block_menu
+ end
+
+ nomad_moogle = function()
+ if #moogles == 0 then
+ for _,name in ipairs(names) do
+ local npcs = windower.ffxi.get_mob_list(name)
+ for index in pairs(npcs) do
+ table.insert(moogles,index)
+ end
+ end
+ end
+
+ local player = windower.ffxi.get_mob_by_target('me')
+ for _, moo_index in ipairs(moogles) do
+ local moo = windower.ffxi.get_mob_by_index(moo_index)
+ if moo and (moo.x - player.x)^2 + (moo.y - player.y)^2 < 36 then
+ if not poked then
+ poke_moogle(moo)
+ end
+ return moo.name
+ end
+ end
+ return false
+ end
+
+ windower.register_event('incoming chunk',function(id)
+ if id == 0x02E and block_menu then
+ block_menu = false
+ return true
+ end
+ end)
+end
+
+windower.register_event('zone change',function()
+ clear_moogles()
+end)
+
+local function validate_bag(bag_table)
+ if type(bag_table) == 'table' and windower.ffxi.get_bag_info(bag_table.id) then
+ if bag_table.access == 'Everywhere' then
+ return true
+ elseif bag_table.access == 'Mog House' then
+ if windower.ffxi.get_info().mog_house then
+ return true
+ elseif nomad_moogle() and bag_table.english ~= 'Storage' then -- Storage is not available at Nomad Moogles
+ return true
+ end
+ end
+ end
+ return false
+end
+
+local function validate_id(id)
+ return (id and id ~= 0 and id ~= 0xFFFF) -- Not empty or gil
+end
+
+local function wardrobecheck(bag_id,id)
+ return _static.wardrobe_ids[bag_id]==nil or ( res.items[id] and (res.items[id].type == 4 or res.items[id].type == 5) )
+end
+
+function Items.new(loc_items,bool)
+ loc_items = loc_items or windower.ffxi.get_items()
+ new_instance = setmetatable({}, {__index = function (t, k) if rawget(t,k) then return rawget(t,k) else return rawget(items,k) end end})
+ for bag_id,bag_table in pairs(res.bags) do
+ org_debug("items", "Items.new::bag_id: "..bag_id)
+ bag_name = bag_table.english:lower():gsub(' ', '')
+ org_debug("items", "Items.new::bag_name: "..bag_name)
+ if (bool or validate_bag(bag_table)) and (loc_items[bag_id] or loc_items[bag_name]) then
+ org_debug("items", "Items.new: new_instance for ID#"..bag_id)
+ local cur_inv = new_instance:new(bag_id)
+ for inventory_index,item_table in pairs(loc_items[bag_id] or loc_items[bag_name]) do
+ if type(item_table) == 'table' and validate_id(item_table.id) then
+ org_debug("items", "Items.new: inventory_index="..inventory_index.." item_table.id="..item_table.id.." ("..res.items[item_table.id].english..")")
+ cur_inv:new(item_table.id,item_table.count,item_table.extdata,item_table.augments,item_table.status,inventory_index)
+ end
+ end
+ end
+ end
+ return new_instance
+end
+
+function items:new(key)
+ org_debug("items", "New items instance with key "..key)
+ local new_instance = setmetatable({_parent = self,_info={n=0,bag_id=key}}, {__index = function (t, k) if rawget(t,k) then return rawget(t,k) else return rawget(bags,k) end end})
+ self[key] = new_instance
+ return new_instance
+end
+
+function items:find(item)
+ for bag_name,bag_id in pairs(settings.bag_priority) do
+ real_bag_id = s_to_bag(bag_name)
+ org_debug("find", "Searching "..bag_name.." for "..res.items[item.id].english..".")
+ if self[real_bag_id] and self[real_bag_id]:contains(item) then
+ org_debug("find", "Found "..res.items[item.id].english.." in "..bag_name..".")
+ return real_bag_id, self[real_bag_id]:contains(item)
+ else
+ org_debug("find", "Didn't find "..res.items[item.id].english.." in "..bag_name..".")
+ end
+ end
+ org_debug("find", "Didn't find "..res.items[item.id].english.." in any bags.")
+ return false
+end
+
+function items:route(start_bag,start_ind,end_bag,count)
+ count = count or self[start_bag][start_ind].count
+ local success = true
+ local initial_ind = start_ind
+ local inventory_max = windower.ffxi.get_bag_info(0).max
+ if start_bag ~= 0 and self[0]._info.n < inventory_max then
+ start_ind = self[start_bag][start_ind]:move(0,0x52,count)
+ elseif start_bag ~= 0 and self[0]._info.n >= inventory_max then
+ success = false
+ org_warning('Cannot move more than '..inventory_max..' items into inventory')
+ end
+
+ local destination_enabled = windower.ffxi.get_bag_info(end_bag).enabled
+ local destination_max = windower.ffxi.get_bag_info(end_bag).max
+
+ if not destination_enabled then
+ success = false
+ org_warning('Cannot move to '..tostring(end_bag)..' because it is disabled')
+ elseif start_ind and end_bag ~= 0 and self[end_bag]._info.n < destination_max then
+ self[0][start_ind]:transfer(end_bag,count)
+ elseif not start_ind then
+ success = false
+ org_warning('Initial movement of the route failed. ('..tostring(start_bag)..' '..tostring(initial_ind)..' '..tostring(start_ind)..' '..tostring(end_bag)..')')
+ elseif self[end_bag]._info.n >= destination_max then
+ success = false
+ org_warning('Cannot move more than '..destination_max..' items into that inventory ('..end_bag..')')
+ end
+ return success
+end
+
+function items:it()
+ local i = 0
+ local bag_priority_list = {}
+ for i,v in pairs(settings.bag_priority) do
+ bag_priority_list[v] = i
+ end
+ return function ()
+ while i < #bag_priority_list do
+ i = i + 1
+ local id = s_to_bag(bag_priority_list[i])
+ if not id then
+ org_error('The bag name ("'..tostring(bag_priority_list[i])..'") with priority '..tostring(i)..' in the ../addons/organizer/data/settings.xml file is not valid.\nValid options are '..tostring(res.bags))
+ end
+ if self[id] and validate_bag(res.bags[id]) then
+ return id, self[id]
+ end
+ end
+ end
+
+end
+
+function bags:new(id,count,ext,augments,status,index)
+ local max_size = windower.ffxi.get_bag_info(self._info.bag_id).max
+ if self._info.n >= max_size then org_warning('Attempting to add another item to a full bag') return end
+ if index and table.with(self,'index',index) then org_warning('Cannot assign the same index twice') return end
+ self._info.n = self._info.n + 1
+ index = index or self:first_empty()
+ status = status or 0
+ augments = augments or ext and id and extdata.decode({id=id,extdata=ext}).augments
+ if augments then augments = table.filter(augments,-functions.equals('none')) end
+ self[index] = setmetatable({_parent=self,id=id,count=count,extdata=ext,index=index,status=status,
+ name=res.items[id][_global.language]:lower(),log_name=res.items[id][_global.language_log]:lower(),augments=augments},
+ {__index = function (t, k)
+ if not t or not k then print('table index is nil error',t,k) end
+ if rawget(t,k) then
+ return rawget(t,k)
+ else
+ return rawget(item_tab,k)
+ end
+ end})
+ return index
+end
+
+function bags:it()
+ local max = windower.ffxi.get_bag_info(self._info.bag_id).max
+ local i = 0
+ return function ()
+ while i < max do
+ i = i + 1
+ if self[i] then return i, self[i] end
+ end
+ end
+end
+
+function bags:first_empty()
+ local max = windower.ffxi.get_bag_info(self._info.bag_id).max
+ for i=1,max do
+ if not self[i] then return i end
+ end
+end
+
+function bags:remove(index)
+ if not rawget(self,index) then org_warning('Attempting to remove an index that does not exist') return end
+ self._info.n = self._info.n - 1
+ rawset(self,index,nil)
+end
+
+function bags:find_all_instances(item,bool,first)
+ local instances = L{}
+ for i,v in self:it() do
+ org_debug("find_all", "find_all_instances: slot="..i.." v="..res.items[v.id].english.." item="..res.items[item.id].english.." ")
+ if (bool or not v:annihilated()) and v.id == item.id then -- and v.count >= item.count then
+ if not item.augments or table.length(item.augments) == 0 or v.augments and extdata.compare_augments(item.augments,v.augments) then
+ -- May have to do a higher level comparison here for extdata.
+ -- If someone exports an enchanted item when the timer is
+ -- counting down then this function will return false for it.
+ instances:append(i)
+ if first then
+ return instances
+ end
+ end
+ end
+ end
+ if instances.n ~= 0 then
+ return instances
+ else
+ return false
+ end
+end
+
+function bags:contains(item,bool)
+ bool = bool or false -- Default to only looking at unannihilated items
+ org_debug("contains", "contains: searching for "..res.items[item.id].english.." in "..self._info.bag_id)
+ local instances = self:find_all_instances(item,bool,true)
+ if instances then
+ return instances:it()()
+ end
+ return false
+end
+
+function bags:find_unfinished_stack(item,bool)
+ local tab = self:find_all_instances(item,bool,false)
+ if tab then
+ for i in tab:it() do
+ if res.items[self[i].id] and res.items[self[i].id].stack > self[i].count then
+ return i
+ end
+ end
+ end
+ return false
+end
+
+function item_tab:transfer(dest_bag,count)
+ -- Transfer an item to a specific bag.
+ if not dest_bag then org_warning('Destination bag is invalid.') return false end
+ count = count or self.count
+ local parent = self._parent
+ local targ_inv = parent._parent[dest_bag]
+
+ local parent_bag_id = parent._info.bag_id
+ local target_bag_id = targ_inv._info.bag_id
+
+ if not (target_bag_id == 0 or parent_bag_id == 0) then
+ org_warning('Cannot move between two bags that are not inventory bags.')
+ else
+ while parent[self.index] and targ_inv:find_unfinished_stack(parent[self.index]) do
+ org_debug("stacks", "Moving ("..res.items[self.id].english..') from '..res.bags[parent_bag_id].en..' to '..res.bags[target_bag_id].en..'')
+ local rv = parent[self.index]:move(dest_bag,targ_inv:find_unfinished_stack(parent[self.index]),count)
+ if not rv then
+ org_debug("stacks", "FAILED moving ("..res.items[self.id].english..') from '..res.bags[parent_bag_id].en..' to '..res.bags[target_bag_id].en..'')
+ break
+ end
+ end
+ if parent[self.index] then
+ parent[self.index]:move(dest_bag)
+ end
+ return true
+ end
+ return false
+end
+
+function item_tab:move(dest_bag,dest_slot,count)
+ if not dest_bag then org_warning('Destination bag is invalid.') return false end
+ count = count or self.count
+ local parent = self._parent
+ local targ_inv = parent._parent[dest_bag]
+ dest_slot = dest_slot or 0x52
+
+ local parent_bag_id = parent._info.bag_id
+ local parent_bag_name = res.bags[parent_bag_id].en:lower()
+
+ local target_bag_id = targ_inv._info.bag_id
+
+ org_debug("move", "move(): Item: "..res.items[self.id].english)
+ org_debug("move", "move(): Parent bag: "..parent_bag_id)
+ org_debug("move", "move(): Target bag: "..target_bag_id)
+
+ -- issues with bazaared items makes me think we shouldn't screw with status'd items at all
+ if(self.status > 0) then
+ if(self.status == 5) then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') because it is currently equipped.')
+ return false
+ elseif(self.status == 19) then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') because it is an equipped linkshell.')
+ return false
+ elseif(self.status == 25) then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') because it is in your bazaar.')
+ return false
+ end
+ end
+
+ -- check the 'retain' lists
+ if((parent_bag_id == 0) and _retain[self.id]) then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') because it is set to be retained ('.._retain[self.id]..')')
+ return false
+ end
+
+ if((parent_bag_id == 0) and settings.retain and settings.retain.items) then
+ local cat = res.items[self.id].category
+ if(cat ~= 'Weapon' and cat ~= 'Armor') then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') because non-equipment is set be retained')
+ return false
+ end
+ end
+
+ -- respect the ignore list
+ if(_ignore_list[parent_bag_name] and _ignore_list[parent_bag_name][res.items[self.id].english]) then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') because it is on the ignore list')
+ return false
+ end
+
+ -- Make sure the source can be pulled from
+ if not _valid_pull[parent_bag_id] then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') - can not be pulled from '..res.bags[parent_bag_id].en..') ')
+ return false
+ end
+
+ -- Make sure the target can be pushed to
+ if not _valid_dump[target_bag_id] then
+ org_verbose('Skipping item: ('..res.items[self.id].english..') - can not be pushed to '..res.bags[target_bag_id].en..') ')
+ return false
+ end
+
+ if not self:annihilated() and
+ (not dest_slot or not targ_inv[dest_slot] or (targ_inv[dest_slot] and res.items[targ_inv[dest_slot].id].stack < targ_inv[dest_slot].count + count)) and
+ (targ_inv._info.bag_id == 0 or parent._info.bag_id == 0) and
+ wardrobecheck(targ_inv._info.bag_id,self.id) and
+ self:free() then
+ windower.packets.inject_outgoing(0x29,string.char(0x29,6,0,0)..'I':pack(count)..string.char(parent._info.bag_id,dest_bag,self.index,dest_slot))
+ org_verbose('Moving item! ('..res.items[self.id].english..') from '..res.bags[parent._info.bag_id].en..' '..parent._info.n..' to '..res.bags[dest_bag].en..' '..targ_inv._info.n..')')
+ local new_index = targ_inv:new(self.id, count, self.extdata, self.augments)
+ --print(parent._info.bag_id,dest_bag,self.index,new_index)
+ parent:remove(self.index)
+ return new_index
+ elseif not dest_slot then
+ org_warning('Cannot move the item ('..res.items[self.id].english..'). Target inventory is full ('..res.bags[dest_bag].en..')')
+ elseif targ_inv[dest_slot] and res.items[targ_inv[dest_slot].id].stack < targ_inv[dest_slot].count + count then
+ org_warning('Cannot move the item ('..res.items[self.id].english..'). Target inventory slot would be overly full ('..(targ_inv[dest_slot].count + count)..' items in '..res.bags[dest_bag].en..')')
+ elseif (targ_inv._info.bag_id ~= 0 and parent._info.bag_id ~= 0) then
+ org_warning('Cannot move the item ('..res.items[self.id].english..'). Attempting to move from a non-inventory to a non-inventory bag ('..res.bags[parent._info.bag_id].en..' '..res.bags[dest_bag].en..')')
+ elseif self:annihilated() then
+ org_warning('Cannot move the item ('..res.items[self.id].english..'). It has already been moved.')
+ elseif not wardrobecheck(targ_inv._info.bag_id,self.id) then
+ org_warning('Cannot move the item ('..res.items[self.id].english..') to the wardrobe. Wardrobe cannot hold an item of its type ('..tostring(res.items[self.id].type)..').')
+ elseif not self:free() then
+ org_warning('Cannot free the item ('..res.items[self.id].english..'). It has an unaddressable item status ('..tostring(self.status)..').')
+ end
+ return false
+end
+
+function item_tab:put_away(usable_bags)
+ org_debug("move", "Putting away "..res.items[self.id].english)
+ local current_items = self._parent._parent
+ usable_bags = usable_bags or _static.usable_bags
+ local bag_free
+ for _,v in ipairs(usable_bags) do
+ local bag_max = windower.ffxi.get_bag_info(v).max
+ if current_items[v]._info.n < bag_max and wardrobecheck(v,self.id) then
+ bag_free = v
+ break
+ end
+ end
+ if bag_free then
+ self:transfer(bag_free,self.count)
+ end
+end
+
+function item_tab:free()
+ if self.status == 5 then
+ local eq = windower.ffxi.get_items().equipment
+ for _,v in pairs(res.slots) do
+ local ind_name = v.english:lower():gsub(' ','_')
+ local bag_name = ind_name..'_bag'
+ local ind, bag = eq[ind_name],eq[bag_name]
+ if self.index == ind and self._parent._info.bag_id == bag then
+ windower.packets.inject_outgoing(0x50,string.char(0x50,0x04,0,0,self._parent._info.bag_id,v.id,0,0))
+ break
+ end
+ end
+ elseif self.status ~= 0 then
+ return false
+ end
+ return true
+end
+
+function item_tab:annihilate(count)
+ count = count or rawget(item_tab,'count')
+ local a_count = (rawget(item_tab,'a_count') or 0) + count
+ if a_count >count then
+ org_warning('Moving more of an item ('..item_tab.id..' : '..a_count..') than possible ('..count..'.')
+ end
+ rawset(self,'a_count',a_count)
+end
+
+function item_tab:annihilated()
+ return ( (rawget(self,'a_count') or 0) >= rawget(self,'count') )
+end
+
+function item_tab:available_amount()
+ return ( rawget(self,'count') - (rawget(self,'a_count') or 0) )
+end
+
+return Items
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua b/Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua
new file mode 100644
index 0000000..8e526d9
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua
@@ -0,0 +1,565 @@
+--Copyright (c) 2015, Byrthnoth and Rooks
+--All rights reserved.
+
+--Redistribution and use in source and binary forms, with or without
+--modification, are permitted provided that the following conditions are met:
+
+-- * Redistributions of source code must retain the above copyright
+-- notice, this list of conditions and the following disclaimer.
+-- * Redistributions in binary form must reproduce the above copyright
+-- notice, this list of conditions and the following disclaimer in the
+-- documentation and/or other materials provided with the distribution.
+-- * Neither the name of <addon name> nor the
+-- names of its contributors may be used to endorse or promote products
+-- derived from this software without specific prior written permission.
+
+--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+--ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+--WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+--DISCLAIMED. IN NO EVENT SHALL <your name> BE LIABLE FOR ANY
+--DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+--(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+--LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+--ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+--(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+--SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+res = require('resources')
+files = require('files')
+require('pack')
+Items = require('items')
+extdata = require('extdata')
+logger = require('logger')
+require('tables')
+require('lists')
+require('functions')
+config = require('config')
+slips = require('slips')
+packets = require('packets')
+
+_addon.name = 'Organizer'
+_addon.author = 'Byrth, maintainer: Rooks'
+_addon.version = 0.20210721
+_addon.commands = {'organizer','org'}
+
+_static = {
+ bag_ids = {
+ inventory=0,
+ safe=1,
+ storage=2,
+ temporary=3,
+ locker=4,
+ satchel=5,
+ sack=6,
+ case=7,
+ wardrobe=8,
+ safe2=9,
+ wardrobe2=10,
+ wardrobe3=11,
+ wardrobe4=12,
+ },
+ wardrobe_ids = {[8]=true,[10]=true,[11]=true,[12]=true},
+ usable_bags = {1,9,4,2,5,6,7,8,10,11,12}
+}
+
+_global = {
+ language = 'english',
+ language_log = 'english_log',
+}
+
+_ignore_list = {}
+_retain = {}
+_valid_pull = {}
+_valid_dump = {}
+
+default_settings = {
+ dump_bags = {['Safe']=1,['Safe2']=2,['Locker']=3,['Storage']=4},
+ bag_priority = {['Safe']=1,['Safe2']=2,['Locker']=3,['Storage']=4,['Satchel']=5,['Sack']=6,['Case']=7,['Inventory']=8,['Wardrobe']=9,['Wardrobe2']=10,['Wardrobe3']=11,['Wardrobe4']=12,},
+ item_delay = 0,
+ ignore = {},
+ retain = {
+ ["moogle_slip_gear"]=false,
+ ["seals"]=false,
+ ["items"]=false,
+ ["slips"]=false,
+ },
+ auto_heal = false,
+ default_file='default.lua',
+ verbose=false,
+}
+
+_debugging = {
+ debug = {
+ ['contains']=true,
+ ['command']=true,
+ ['find']=true,
+ ['find_all']=true,
+ ['items']=true,
+ ['move']=true,
+ ['settings']=true,
+ ['stacks']=true
+ },
+ debug_log = 'data\\organizer-debug.log',
+ enabled = false,
+ warnings = false, -- This mode gives warnings about impossible item movements and crash conditions.
+}
+
+debug_log = files.new(_debugging.debug_log)
+
+function s_to_bag(str)
+ if not str and tostring(str) then return end
+ for i,v in pairs(res.bags) do
+ if v.en:lower():gsub(' ', '') == str:lower() then
+ return v.id
+ end
+ end
+end
+
+windower.register_event('load',function()
+ debug_log:write('Organizer loaded at '..os.date()..'\n')
+
+ if debugging then windower.debug('load') end
+ options_load()
+end)
+
+function options_load( )
+ if not windower.dir_exists(windower.addon_path..'data\\') then
+ org_debug("settings", "Creating data directory")
+ windower.create_dir(windower.addon_path..'data\\')
+ if not windower.dir_exists(windower.addon_path..'data\\') then
+ org_error("unable to create data directory!")
+ end
+ end
+
+ for bag_name, bag_id in pairs(_static.bag_ids) do
+ if not windower.dir_exists(windower.addon_path..'data\\'..bag_name) then
+ org_debug("settings", "Creating data directory for "..bag_name)
+ windower.create_dir(windower.addon_path..'data\\'..bag_name)
+ if not windower.dir_exists(windower.addon_path..'data\\'..bag_name) then
+ org_error("unable to create"..bag_name.."directory!")
+ end
+ end
+ end
+
+ -- We can't just do a:
+ --
+ -- settings = config.load('data\\settings.xml', default_settings)
+ --
+ -- because the config library will try to merge them, and it will
+ -- add back anything a user has removed (like items in bag_priority)
+
+ if windower.file_exists(windower.addon_path..'data\\settings.xml') then
+ org_debug("settings", "Loading settings from file")
+ settings = config.load('data\\settings.xml')
+ else
+ org_debug("settings", "Saving default settings to file")
+ settings = config.load('data\\settings.xml', default_settings)
+ end
+
+ -- Build the ignore list
+ if(settings.ignore) then
+ for bn,i_list in pairs(settings.ignore) do
+ bag_name = bn:lower()
+ _ignore_list[bag_name] = {}
+ for _,ignore_name in pairs(i_list) do
+ org_verbose("Adding "..ignore_name.." in the "..bag_name.." to the ignore list")
+ _ignore_list[bag_name][ignore_name] = 1
+ end
+ end
+ end
+
+ -- Build a hard-wired pull list
+ for bag_name,_ in pairs(settings.bag_priority) do
+ org_verbose("Adding "..bag_name.." to the pull list")
+ _valid_pull[s_to_bag(bag_name)] = 1
+ end
+
+ -- Build a hard-wired dump list
+ for bag_name,_ in pairs(settings.dump_bags) do
+ org_verbose("Adding "..bag_name.." to the push list")
+ _valid_dump[s_to_bag(bag_name)] = 1
+ end
+
+ -- Build the retain lists
+ if(settings.retain) then
+ if(settings.retain.moogle_slip_gear == true) then
+ org_verbose("Moogle slip gear set to retain")
+ slip_lists = require('slips')
+ for slip_id,slip_list in pairs(slip_lists.items) do
+ for item_id in slip_list:it() do
+ if item_id ~= 0 then
+ _retain[item_id] = "moogle slip"
+ org_debug("settings", "Adding ("..res.items[item_id].english..') to slip retain list')
+ end
+ end
+ end
+ end
+
+ if(settings.retain.seals == true) then
+ org_verbose("Seals set to retain")
+ seals = {1126,1127,2955,2956,2957}
+ for _,seal_id in pairs(seals) do
+ _retain[seal_id] = "seal"
+ org_debug("settings", "Adding ("..res.items[seal_id].english..') to slip retain list')
+ end
+ end
+
+ if(settings.retain.items == true) then
+ org_verbose("Non-equipment items set to retain")
+ end
+
+ if(settings.retain.slips == true) then
+ org_verbose("Slips set to retain")
+ for _,slips_id in pairs(slips.storages) do
+ _retain[slips_id] = "slips"
+ org_debug("settings", "Adding ("..res.items[slips_id].english..') to slip retain list')
+ end
+ end
+ end
+
+ -- Always allow inventory and wardrobe, obviously
+ _valid_dump[0] = 1
+ _valid_pull[0] = 1
+ _valid_dump[8] = 1
+ _valid_pull[8] = 1
+ _valid_dump[10] = 1
+ _valid_pull[10] = 1
+ _valid_dump[11] = 1
+ _valid_pull[11] = 1
+ _valid_dump[12] = 1
+ _valid_pull[12] = 1
+
+end
+
+
+
+windower.register_event('addon command',function(...)
+ local inp = {...}
+ -- get (g) = Take the passed file and move everything to its defined location.
+ -- tidy (t) = Take the passed file and move everything that isn't in it out of my active inventory.
+ -- organize (o) = get followed by tidy.
+ local command = table.remove(inp,1):lower()
+ if command == 'eval' then
+ assert(loadstring(table.concat(inp,' ')))()
+ return
+ end
+
+ local moogle = nomad_moogle()
+ if moogle then
+ org_debug("command", "Using '" .. moogle .. "' for Mog House interaction")
+ end
+
+ local bag = 'all'
+ if inp[1] and (_static.bag_ids[inp[1]:lower()] or inp[1]:lower() == 'all') then
+ bag = table.remove(inp,1):lower()
+ end
+
+ org_debug("command", "Using '"..bag.."' as the bag target")
+
+
+ file_name = table.concat(inp,' ')
+ if string.length(file_name) == 0 then
+ file_name = default_file_name()
+ end
+
+ if file_name:sub(-4) ~= '.lua' then
+ file_name = file_name..'.lua'
+ end
+ org_debug("command", "Using '"..file_name.."' as the file name")
+
+
+ if (command == 'g' or command == 'get') then
+ org_debug("command", "Calling get with file_name '"..file_name.."' and bag '"..bag.."'")
+ get(thaw(file_name, bag))
+ elseif (command == 't' or command == 'tidy') then
+ org_debug("command", "Calling tidy with file_name '"..file_name.."' and bag '"..bag.."'")
+ tidy(thaw(file_name, bag))
+ elseif (command == 'f' or command == 'freeze') then
+
+ org_debug("command", "Calling freeze command")
+ local items = Items.new(windower.ffxi.get_items(),true)
+ local frozen = {}
+ items[3] = nil -- Don't export temporary items
+ if _static.bag_ids[bag] then
+ org_debug("command", "Bag: "..bag)
+ freeze(file_name,bag,items)
+ else
+ for bag_id,item_list in items:it() do
+ org_debug("command", "Bag ID: "..bag_id)
+ -- infinite loop protection
+ if(frozen[bag_id]) then
+ org_warning("Tried to freeze ID #"..bag_id.." twice, aborting")
+ return
+ end
+ frozen[bag_id] = 1
+ freeze(file_name,res.bags[bag_id].english:lower():gsub(' ', ''),items)
+ end
+ end
+ elseif (command == 'o' or command == 'organize') then
+ org_debug("command", "Calling organize command")
+ organize(thaw(file_name, bag))
+ end
+
+ if settings.auto_heal and tostring(settings.auto_heal):lower() ~= 'false' then
+ org_debug("command", "Automatically healing")
+ windower.send_command('input /heal')
+ end
+
+ if moogle then
+ clear_moogles()
+ org_debug("command", "Clearing '" .. moogle .. "' status")
+ end
+
+ org_debug("command", "Organizer complete")
+
+end)
+
+function get(goal_items,current_items)
+ org_verbose('Getting!')
+ if goal_items then
+ count = 0
+ failed = 0
+ current_items = current_items or Items.new()
+ goal_items, current_items = clean_goal(goal_items,current_items)
+ for bag_id,inv in goal_items:it() do
+ for ind,item in inv:it() do
+ if not item:annihilated() then
+ local start_bag, start_ind = current_items:find(item)
+ -- Table contains a list of {bag, pos, count}
+ if start_bag then
+ if not current_items:route(start_bag,start_ind,bag_id) then
+ org_warning('Unable to move item.')
+ failed = failed + 1
+ else
+ count = count + 1
+ end
+ simulate_item_delay()
+ else
+ -- Need to adapt this for stacking items somehow.
+ org_warning(res.items[item.id].english..' not found.')
+ end
+ end
+ end
+ end
+ org_verbose("Got "..count.." item(s), and failed getting "..failed.." item(s)")
+ end
+ return goal_items, current_items
+end
+
+function freeze(file_name,bag,items)
+ org_debug("command", "Entering freeze function with bag '"..bag.."'")
+ local lua_export = T{}
+ local counter = 0
+ for _,item_table in items[_static.bag_ids[bag]]:it() do
+ counter = counter + 1
+ if(counter > 80) then
+ org_warning("We hit an infinite loop in freeze()! ABORT.")
+ return
+ end
+ org_debug("command", "In freeze loop for bag '"..bag.."'")
+ org_debug("command", "Processing '"..item_table.log_name.."'")
+
+ local temp_ext,augments = extdata.decode(item_table)
+ if temp_ext.augments then
+ org_debug("command", "Got augments for '"..item_table.log_name.."'")
+ augments = table.filter(temp_ext.augments,-functions.equals('none'))
+ end
+ lua_export:append({name = item_table.name,log_name=item_table.log_name,
+ id=item_table.id,extdata=item_table.extdata:hex(),augments = augments,count=item_table.count})
+ end
+ -- Make sure we have something in the bag at all
+ if lua_export[1] then
+ org_verbose("Freezing "..tostring(bag)..".")
+ local export_file = files.new('/data/'..bag..'/'..file_name,true)
+ export_file:write('return '..lua_export:tovstring({'augments','log_name','name','id','count','extdata'}))
+ else
+ org_debug("command", "Got nothing, skipping '"..bag.."'")
+ end
+end
+
+function tidy(goal_items,current_items,usable_bags)
+ org_debug("command", "Entering tidy()")
+ usable_bags = usable_bags or get_dump_bags()
+ -- Move everything out of items[0] and into other inventories (defined by the passed table)
+ if goal_items and goal_items[0] and goal_items[0]._info.n > 0 then
+ current_items = current_items or Items.new()
+ goal_items, current_items = clean_goal(goal_items,current_items)
+ for index,item in current_items[0]:it() do
+ if not goal_items[0]:contains(item,true) then
+ org_debug("command", "Putting away "..item.log_name)
+ current_items[0][index]:put_away(usable_bags)
+ simulate_item_delay()
+ end
+ end
+ end
+ return goal_items, current_items
+end
+
+function organize(goal_items)
+ org_message('Starting...')
+ local current_items = Items.new()
+ local dump_bags = get_dump_bags()
+
+ local inventory_max = windower.ffxi.get_bag_info(0).max
+ if current_items[0].n == inventory_max then
+ tidy(goal_items,current_items,dump_bags)
+ end
+ if current_items[0].n == inventory_max then
+ org_error('Unable to make space, aborting!')
+ return
+ end
+
+ local remainder = math.huge
+ while remainder do
+ goal_items, current_items = get(goal_items,current_items)
+
+ goal_items, current_items = clean_goal(goal_items,current_items)
+ goal_items, current_items = tidy(goal_items,current_items,dump_bags)
+ remainder = incompletion_check(goal_items,remainder)
+ if(remainder) then
+ org_verbose("Remainder: "..tostring(remainder)..' Current: '..current_items[0]._info.n,1)
+ else
+ org_verbose("No remainder, so we found everything we were looking for!")
+ end
+ end
+ goal_items, current_items = tidy(goal_items,current_items,dump_bags)
+
+ local count,failures = 0,T{}
+ for bag_id,bag in goal_items:it() do
+ for ind,item in bag:it() do
+ if item:annihilated() then
+ count = count + 1
+ else
+ item.bag_id = bag_id
+ failures:append(item)
+ end
+ end
+ end
+ org_message('Done! - '..count..' items matched and '..table.length(failures)..' items missing!')
+ if table.length(failures) > 0 then
+ for i,v in failures:it() do
+ org_verbose('Item Missing: '..i.name..' '..(i.augments and tostring(T(i.augments)) or ''))
+ end
+ end
+end
+
+function clean_goal(goal_items,current_items)
+ for i,inv in goal_items:it() do
+ for ind,item in inv:it() do
+ local potential_ind = current_items[i]:contains(item)
+ if potential_ind then
+ -- If it is already in the right spot, delete it from the goal items and annihilate it.
+ local count = math.min(goal_items[i][ind].count,current_items[i][potential_ind].count)
+ goal_items[i][ind]:annihilate(goal_items[i][ind].count)
+ current_items[i][potential_ind]:annihilate(current_items[i][potential_ind].count)
+ end
+ end
+ end
+ return goal_items, current_items
+end
+
+function incompletion_check(goal_items,remainder)
+ -- Does not work. On cycle 1, you fill up your inventory without purging unnecessary stuff out.
+ -- On cycle 2, your inventory is full. A gentler version of tidy needs to be in the loop somehow.
+ local remaining = 0
+ for i,v in goal_items:it() do
+ for n,m in v:it() do
+ if not m:annihilated() then
+ remaining = remaining + 1
+ end
+ end
+ end
+ return remaining ~= 0 and remaining < remainder and remaining
+end
+
+function thaw(file_name,bag)
+ local bags = _static.bag_ids[bag] and {[bag]=file_name} or table.reassign({},_static.bag_ids) -- One bag name or all of them if no bag is specified
+ if settings.default_file:sub(-4) ~= '.lua' then
+ settings.default_file = settings.default_file..'.lua'
+ end
+ for i,v in pairs(_static.bag_ids) do
+ bags[i] = bags[i] and windower.file_exists(windower.addon_path..'data/'..i..'/'..file_name) and file_name or default_file_name()
+ end
+ bags.temporary = nil
+ local inv_structure = {}
+ for cur_bag,file in pairs(bags) do
+ local f,err = loadfile(windower.addon_path..'data/'..cur_bag..'/'..file)
+ if f and not err then
+ local success = false
+ success, inv_structure[cur_bag] = pcall(f)
+ if not success then
+ org_warning('User File Error (Syntax) - '..inv_structure[cur_bag])
+ inv_structure[cur_bag] = nil
+ end
+ elseif bag and cur_bag:lower() == bag:lower() then
+ org_warning('User File Error (Loading) - '..err)
+ end
+ end
+ -- Convert all the extdata back to a normal string
+ for i,v in pairs(inv_structure) do
+ for n,m in pairs(v) do
+ if m.extdata then
+ inv_structure[i][n].extdata = string.parse_hex(m.extdata)
+ end
+ end
+ end
+ return Items.new(inv_structure)
+end
+
+function org_message(msg,col)
+ windower.add_to_chat(col or 8,'Organizer: '..msg)
+ flog(_debugging.debug_log, 'Organizer [MSG] '..msg)
+end
+
+function org_warning(msg)
+ if _debugging.warnings then
+ windower.add_to_chat(123,'Organizer: '..msg)
+ end
+ flog(_debugging.debug_log, 'Organizer [WARN] '..msg)
+end
+
+function org_debug(level, msg)
+ if(_debugging.enabled) then
+ if (_debugging.debug[level]) then
+ flog(_debugging.debug_log, 'Organizer [DEBUG] ['..level..']: '..msg)
+ end
+ end
+end
+
+
+function org_error(msg)
+ error('Organizer: '..msg)
+ flog(_debugging.debug_log, 'Organizer [ERROR] '..msg)
+end
+
+function org_verbose(msg,col)
+ if tostring(settings.verbose):lower() ~= 'false' then
+ windower.add_to_chat(col or 8,'Organizer: '..msg)
+ end
+ flog(_debugging.debug_log, 'Organizer [VERBOSE] '..msg)
+end
+
+function default_file_name()
+ player = windower.ffxi.get_player()
+ job_name = res.jobs[player.main_job_id]['english_short']
+ return player.name..'_'..job_name..'.lua'
+end
+
+function simulate_item_delay()
+ if settings.item_delay and settings.item_delay > 0 then
+ coroutine.sleep(settings.item_delay)
+ end
+end
+
+function get_dump_bags()
+ local dump_bags = {}
+ for i,v in pairs(settings.dump_bags) do
+ if i and s_to_bag(i) then
+ dump_bags[tonumber(v)] = s_to_bag(i)
+ elseif i then
+ org_error('The bag name ("'..tostring(i)..'") in dump_bags entry #'..tostring(v)..' in the ../addons/organizer/data/settings.xml file is not valid.\nValid options are '..tostring(res.bags))
+ return
+ end
+ end
+ return dump_bags
+end