--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 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 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