diff options
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua')
-rw-r--r-- | Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua | 565 |
1 files changed, 565 insertions, 0 deletions
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 |