diff options
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/organizer')
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 |