summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua
diff options
context:
space:
mode:
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/organizer/organizer.lua565
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