Advertisement
Guest User

organizer.lua

a guest
Aug 5th, 2021
315
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 19.97 KB | None | 0 0
  1. --Copyright (c) 2015, Byrthnoth and Rooks
  2. --All rights reserved.
  3.  
  4. --Redistribution and use in source and binary forms, with or without
  5. --modification, are permitted provided that the following conditions are met:
  6.  
  7. --    * Redistributions of source code must retain the above copyright
  8. --      notice, this list of conditions and the following disclaimer.
  9. --    * Redistributions in binary form must reproduce the above copyright
  10. --      notice, this list of conditions and the following disclaimer in the
  11. --      documentation and/or other materials provided with the distribution.
  12. --    * Neither the name of <addon name> nor the
  13. --      names of its contributors may be used to endorse or promote products
  14. --      derived from this software without specific prior written permission.
  15.  
  16. --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  17. --ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. --WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. --DISCLAIMED. IN NO EVENT SHALL <your name> BE LIABLE FOR ANY
  20. --DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. --(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. --LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. --ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. --SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26.  
  27. res = require 'resources'
  28. files = require 'files'
  29. require 'pack'
  30. Items = require 'items'
  31. extdata = require 'extdata'
  32. logger = require 'logger'
  33. require 'tables'
  34. require 'lists'
  35. require 'functions'
  36. config = require 'config'
  37. slips = require 'slips'
  38.  
  39. _addon.name = 'Organizer'
  40. _addon.author = 'Byrth, maintainer: Rooks'
  41. _addon.version = 0.20210516
  42. _addon.commands = {'organizer','org'}
  43.  
  44. _static = {
  45.     bag_ids = {
  46.         inventory=0,
  47.         safe=1,
  48.         storage=2,
  49.         temporary=3,
  50.         locker=4,
  51.         satchel=5,
  52.         sack=6,
  53.         case=7,
  54.         wardrobe=8,
  55.         safe2=9,
  56.         wardrobe2=10,
  57.         wardrobe3=11,
  58.         wardrobe4=12,
  59.     },
  60.     wardrobe_ids = {[8]=true,[10]=true,[11]=true,[12]=true},
  61.     usable_bags = {1,9,4,2,5,6,7,8,10,11,12}
  62. }
  63.  
  64. _global = {
  65.     language = 'english',
  66.     language_log = 'english_log',
  67. }
  68.  
  69. _ignore_list = {}
  70. _retain = {}
  71. _valid_pull = {}
  72. _valid_dump = {}
  73.  
  74. default_settings = {
  75.     dump_bags = {['Safe']=1,['Safe2']=2,['Locker']=3,['Storage']=4},
  76.     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,},
  77.     item_delay = 0,
  78.     ignore = {},
  79.     retain = {
  80.         ["moogle_slip_gear"]=false,
  81.         ["seals"]=false,
  82.         ["items"]=false,
  83.         ["slips"]=false,
  84.     },
  85.     auto_heal = false,
  86.     default_file='default.lua',
  87.     verbose=false,
  88. }
  89.  
  90. _debugging = {
  91.     debug = {
  92.         ['contains']=true,
  93.         ['command']=true,
  94.         ['find']=true,
  95.         ['find_all']=true,
  96.         ['items']=true,
  97.         ['move']=true,
  98.         ['settings']=true,
  99.         ['stacks']=true
  100.     },
  101.     debug_log = 'data\\organizer-debug.log',
  102.     enabled = false,
  103.     warnings = false, -- This mode gives warnings about impossible item movements and crash conditions.
  104. }
  105.  
  106. debug_log = files.new(_debugging.debug_log)
  107.  
  108. function s_to_bag(str)
  109.     if not str and tostring(str) then return end
  110.     for i,v in pairs(res.bags) do
  111.         if v.en:lower():gsub(' ', '') == str:lower() then
  112.             return v.id
  113.         end
  114.     end
  115. end
  116.  
  117. windower.register_event('load',function()
  118.     debug_log:write('Organizer loaded at '..os.date()..'\n')
  119.  
  120.     if debugging then windower.debug('load') end
  121.     options_load()
  122. end)
  123.  
  124. function options_load( )
  125.     if not windower.dir_exists(windower.addon_path..'data\\') then
  126.         org_debug("settings", "Creating data directory")
  127.         windower.create_dir(windower.addon_path..'data\\')
  128.         if not windower.dir_exists(windower.addon_path..'data\\') then
  129.             org_error("unable to create data directory!")
  130.         end
  131.     end
  132.  
  133.     for bag_name, bag_id in pairs(_static.bag_ids) do
  134.         if not windower.dir_exists(windower.addon_path..'data\\'..bag_name) then
  135.             org_debug("settings", "Creating data directory for "..bag_name)
  136.             windower.create_dir(windower.addon_path..'data\\'..bag_name)
  137.             if not windower.dir_exists(windower.addon_path..'data\\'..bag_name) then
  138.                 org_error("unable to create"..bag_name.."directory!")
  139.             end
  140.         end
  141.     end
  142.  
  143.     -- We can't just do a:
  144.     --
  145.     -- settings = config.load('data\\settings.xml', default_settings)
  146.     --
  147.     -- because the config library will try to merge them, and it will
  148.     -- add back anything a user has removed (like items in bag_priority)
  149.  
  150.     if windower.file_exists(windower.addon_path..'data\\settings.xml') then
  151.         org_debug("settings", "Loading settings from file")
  152.         settings = config.load('data\\settings.xml')
  153.     else
  154.         org_debug("settings", "Saving default settings to file")
  155.         settings = config.load('data\\settings.xml', default_settings)
  156.     end
  157.  
  158.     -- Build the ignore list
  159.     if(settings.ignore) then
  160.         for bn,i_list in pairs(settings.ignore) do
  161.             bag_name = bn:lower()
  162.             _ignore_list[bag_name] = {}
  163.             for _,ignore_name in pairs(i_list) do
  164.                 org_verbose("Adding "..ignore_name.." in the "..bag_name.." to the ignore list")
  165.                 _ignore_list[bag_name][ignore_name] = 1
  166.             end
  167.         end
  168.     end
  169.  
  170.     -- Build a hard-wired pull list
  171.     for bag_name,_ in pairs(settings.bag_priority) do
  172.          org_verbose("Adding "..bag_name.." to the pull list")
  173.         _valid_pull[s_to_bag(bag_name)] = 1
  174.     end
  175.  
  176.     -- Build a hard-wired dump list
  177.     for bag_name,_ in pairs(settings.dump_bags) do
  178.          org_verbose("Adding "..bag_name.." to the push list")
  179.         _valid_dump[s_to_bag(bag_name)] = 1
  180.     end
  181.  
  182.     -- Build the retain lists
  183.     if(settings.retain) then
  184.         if(settings.retain.moogle_slip_gear == true) then
  185.             org_verbose("Moogle slip gear set to retain")
  186.             slip_lists = require('slips')
  187.             for slip_id,slip_list in pairs(slip_lists.items) do
  188.                 for item_id in slip_list:it() do
  189.                     if item_id ~= 0 then
  190.                         _retain[item_id] = "moogle slip"
  191.                         org_debug("settings", "Adding ("..res.items[item_id].english..') to slip retain list')
  192.                     end
  193.                 end
  194.             end
  195.         end
  196.  
  197.         if(settings.retain.seals == true) then
  198.             org_verbose("Seals set to retain")
  199.             seals = {1126,1127,2955,2956,2957}
  200.             for _,seal_id in pairs(seals) do
  201.                 _retain[seal_id] = "seal"
  202.                 org_debug("settings", "Adding ("..res.items[seal_id].english..') to slip retain list')
  203.             end
  204.         end
  205.  
  206.         if(settings.retain.items == true) then
  207.             org_verbose("Non-equipment items set to retain")
  208.         end
  209.        
  210.         if(settings.retain.slips == true) then
  211.             org_verbose("Slips set to retain")
  212.             for _,slips_id in pairs(slips.storages) do
  213.                 _retain[slips_id] = "slips"
  214.                 org_debug("settings", "Adding ("..res.items[slips_id].english..') to slip retain list')
  215.             end
  216.         end
  217.     end
  218.  
  219.     -- Always allow inventory and wardrobe, obviously
  220.     _valid_dump[0] = 1
  221.     _valid_pull[0] = 1
  222.     _valid_dump[8] = 1
  223.     _valid_pull[8] = 1
  224.     _valid_dump[10] = 1
  225.     _valid_pull[10] = 1
  226.     _valid_dump[11] = 1
  227.     _valid_pull[11] = 1
  228.     _valid_dump[12] = 1
  229.     _valid_pull[12] = 1
  230.  
  231. end
  232.  
  233.  
  234.  
  235. windower.register_event('addon command',function(...)
  236.     local inp = {...}
  237.     -- get (g) = Take the passed file and move everything to its defined location.
  238.     -- tidy (t) = Take the passed file and move everything that isn't in it out of my active inventory.
  239.     -- organize (o) = get followed by tidy.
  240.     local command = table.remove(inp,1):lower()
  241.     if command == 'eval' then
  242.         assert(loadstring(table.concat(inp,' ')))()
  243.         return
  244.     end
  245.  
  246.     local bag = 'all'
  247.     if inp[1] and (_static.bag_ids[inp[1]:lower()] or inp[1]:lower() == 'all') then
  248.         bag = table.remove(inp,1):lower()
  249.     end
  250.  
  251.     org_debug("command", "Using '"..bag.."' as the bag target")
  252.  
  253.  
  254.     file_name = table.concat(inp,' ')
  255.     if string.length(file_name) == 0 then
  256.         file_name = default_file_name()
  257.     end
  258.  
  259.     if file_name:sub(-4) ~= '.lua' then
  260.         file_name = file_name..'.lua'
  261.     end
  262.     org_debug("command", "Using '"..file_name.."' as the file name")
  263.  
  264.  
  265.     if (command == 'g' or command == 'get') then
  266.         org_debug("command", "Calling get with file_name '"..file_name.."' and bag '"..bag.."'")
  267.         get(thaw(file_name, bag))
  268.     elseif (command == 't' or command == 'tidy') then
  269.         org_debug("command", "Calling tidy with file_name '"..file_name.."' and bag '"..bag.."'")
  270.         tidy(thaw(file_name, bag))
  271.     elseif (command == 'f' or command == 'freeze') then
  272.  
  273.         org_debug("command", "Calling freeze command")
  274.         local items = Items.new(windower.ffxi.get_items(),true)
  275.         local frozen = {}
  276.         items[3] = nil -- Don't export temporary items
  277.         if _static.bag_ids[bag] then
  278.             org_debug("command", "Bag: "..bag)
  279.             freeze(file_name,bag,items)
  280.         else
  281.             for bag_id,item_list in items:it() do
  282.                 org_debug("command", "Bag ID: "..bag_id)
  283.                 -- infinite loop protection
  284.                 if(frozen[bag_id]) then
  285.                     org_warning("Tried to freeze ID #"..bag_id.." twice, aborting")
  286.                     return
  287.                 end
  288.                 frozen[bag_id] = 1
  289.                 freeze(file_name,res.bags[bag_id].english:lower():gsub(' ', ''),items)
  290.             end
  291.         end
  292.     elseif (command == 'o' or command == 'organize') then
  293.         org_debug("command", "Calling organize command")
  294.         organize(thaw(file_name, bag))
  295.     end
  296.  
  297.     if settings.auto_heal and tostring(settings.auto_heal):lower() ~= 'false' then
  298.         org_debug("command", "Automatically healing")
  299.         windower.send_command('input /heal')
  300.     end
  301.  
  302.     org_debug("command", "Organizer complete")
  303.  
  304. end)
  305.  
  306. function get(goal_items,current_items)
  307.     org_verbose('Getting!')
  308.     if goal_items then
  309.         count = 0
  310.         failed = 0
  311.         current_items = current_items or Items.new()
  312.         goal_items, current_items = clean_goal(goal_items,current_items)
  313.         for bag_id,inv in goal_items:it() do
  314.             for ind,item in inv:it() do
  315.                 if not item:annihilated() then
  316.                     local start_bag, start_ind = current_items:find(item)
  317.                     -- Table contains a list of {bag, pos, count}
  318.                     if start_bag then
  319.                         if not current_items:route(start_bag,start_ind,bag_id) then
  320.                             org_warning('Unable to move item.')
  321.                             failed = failed + 1
  322.                         else
  323.                             count = count + 1
  324.                         end
  325.                         simulate_item_delay()
  326.                     else
  327.                         -- Need to adapt this for stacking items somehow.
  328.                         org_warning(res.items[item.id].english..' not found.')
  329.                     end
  330.                 end
  331.             end
  332.         end
  333.         org_verbose("Got "..count.." item(s), and failed getting "..failed.." item(s)")
  334.     end
  335.     return goal_items, current_items
  336. end
  337.  
  338. function freeze(file_name,bag,items)
  339.     org_debug("command", "Entering freeze function with bag '"..bag.."'")
  340.     local lua_export = T{}
  341.     local counter = 0
  342.     for _,item_table in items[_static.bag_ids[bag]]:it() do
  343.         counter = counter + 1
  344.         if(counter > 80) then
  345.             org_warning("We hit an infinite loop in freeze()! ABORT.")
  346.             return
  347.         end
  348.         org_debug("command", "In freeze loop for bag '"..bag.."'")
  349.         org_debug("command", "Processing '"..item_table.log_name.."'")
  350.  
  351.         local temp_ext,augments = extdata.decode(item_table)
  352.         if temp_ext.augments then
  353.             org_debug("command", "Got augments for '"..item_table.log_name.."'")
  354.             augments = table.filter(temp_ext.augments,-functions.equals('none'))
  355.         end
  356.         lua_export:append({name = item_table.name,log_name=item_table.log_name,
  357.             id=item_table.id,extdata=item_table.extdata:hex(),augments = augments,count=item_table.count})
  358.     end
  359.     -- Make sure we have something in the bag at all
  360.     if lua_export[1] then
  361.         org_verbose("Freezing "..tostring(bag)..".")
  362.         local export_file = files.new('/data/'..bag..'/'..file_name,true)
  363.         export_file:write('return '..lua_export:tovstring({'augments','log_name','name','id','count','extdata'}))
  364.     else
  365.         org_debug("command", "Got nothing, skipping '"..bag.."'")
  366.     end
  367. end
  368.  
  369. function tidy(goal_items,current_items,usable_bags)
  370.     org_debug("command", "Entering tidy()")
  371.     usable_bags = usable_bags or get_dump_bags()
  372.     -- Move everything out of items[0] and into other inventories (defined by the passed table)
  373.     if goal_items and goal_items[0] and goal_items[0]._info.n > 0 then
  374.         current_items = current_items or Items.new()
  375.         goal_items, current_items = clean_goal(goal_items,current_items)
  376.         for index,item in current_items[0]:it() do
  377.             if not goal_items[0]:contains(item,true) then
  378.                 org_debug("command", "Putting away "..item.log_name)
  379.                 current_items[0][index]:put_away(usable_bags)
  380.                 simulate_item_delay()
  381.             end
  382.         end
  383.     end
  384.     return goal_items, current_items
  385. end
  386.  
  387. function organize(goal_items)
  388.     org_message('Starting...')
  389.     local current_items = Items.new()
  390.     local dump_bags = get_dump_bags()
  391.  
  392.     local inventory_max = windower.ffxi.get_bag_info(0).max
  393.     if current_items[0].n == inventory_max then
  394.         tidy(goal_items,current_items,dump_bags)
  395.     end
  396.     if current_items[0].n == inventory_max then
  397.         org_error('Unable to make space, aborting!')
  398.         return
  399.     end
  400.    
  401.     local remainder = math.huge
  402.     while remainder do
  403.         goal_items, current_items = get(goal_items,current_items)
  404.        
  405.         goal_items, current_items = clean_goal(goal_items,current_items)
  406.         goal_items, current_items = tidy(goal_items,current_items,dump_bags)
  407.         remainder = incompletion_check(goal_items,remainder)
  408.         if(remainder) then
  409.             org_verbose("Remainder: "..tostring(remainder)..' Current: '..current_items[0]._info.n,1)
  410.         else
  411.             org_verbose("No remainder, so we found everything we were looking for!")
  412.         end
  413.     end
  414.     goal_items, current_items = tidy(goal_items,current_items,dump_bags)
  415.    
  416.     local count,failures = 0,T{}
  417.     for bag_id,bag in goal_items:it() do
  418.         for ind,item in bag:it() do
  419.             if item:annihilated() then
  420.                 count = count + 1
  421.             else
  422.                 item.bag_id = bag_id
  423.                 failures:append(item)
  424.             end
  425.         end
  426.     end
  427.     org_message('Done! - '..count..' items matched and '..table.length(failures)..' items missing!')
  428.     if table.length(failures) > 0 then
  429.         for i,v in failures:it() do
  430.             org_verbose('Item Missing: '..i.name..' '..(i.augments and tostring(T(i.augments)) or ''))
  431.         end
  432.     end
  433. end
  434.  
  435. function clean_goal(goal_items,current_items)
  436.     for i,inv in goal_items:it() do
  437.         for ind,item in inv:it() do
  438.             local potential_ind = current_items[i]:contains(item)
  439.             if potential_ind then
  440.                 -- If it is already in the right spot, delete it from the goal items and annihilate it.
  441.                 local count = math.min(goal_items[i][ind].count,current_items[i][potential_ind].count)
  442.                 goal_items[i][ind]:annihilate(goal_items[i][ind].count)
  443.                 current_items[i][potential_ind]:annihilate(current_items[i][potential_ind].count)
  444.             end
  445.         end
  446.     end
  447.     return goal_items, current_items
  448. end
  449.  
  450. function incompletion_check(goal_items,remainder)
  451.     -- Does not work. On cycle 1, you fill up your inventory without purging unnecessary stuff out.
  452.     -- On cycle 2, your inventory is full. A gentler version of tidy needs to be in the loop somehow.
  453.     local remaining = 0
  454.     for i,v in goal_items:it() do
  455.         for n,m in v:it() do
  456.             if not m:annihilated() then
  457.                 remaining = remaining + 1
  458.             end
  459.         end
  460.     end
  461.     return remaining ~= 0 and remaining < remainder and remaining
  462. end
  463.  
  464. function thaw(file_name,bag)
  465.     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
  466.     if settings.default_file:sub(-4) ~= '.lua' then
  467.         settings.default_file = settings.default_file..'.lua'
  468.     end
  469.     for i,v in pairs(_static.bag_ids) do
  470.         bags[i] = bags[i] and windower.file_exists(windower.addon_path..'data/'..i..'/'..file_name) and file_name or default_file_name()
  471.     end
  472.     bags.temporary = nil
  473.     local inv_structure = {}
  474.     for cur_bag,file in pairs(bags) do
  475.         local f,err = loadfile(windower.addon_path..'data/'..cur_bag..'/'..file)
  476.         if f and not err then
  477.             local success = false
  478.             success, inv_structure[cur_bag] = pcall(f)
  479.             if not success then
  480.                 org_warning('User File Error (Syntax) - '..inv_structure[cur_bag])
  481.                 inv_structure[cur_bag] = nil
  482.             end
  483.         elseif bag and cur_bag:lower() == bag:lower() then
  484.             org_warning('User File Error (Loading) - '..err)
  485.         end
  486.     end
  487.     -- Convert all the extdata back to a normal string
  488.     for i,v in pairs(inv_structure) do
  489.         for n,m in pairs(v) do
  490.             if m.extdata then
  491.                 inv_structure[i][n].extdata = string.parse_hex(m.extdata)
  492.             end
  493.         end
  494.     end
  495.     return Items.new(inv_structure)
  496. end
  497.  
  498. function org_message(msg,col)
  499.     windower.add_to_chat(col or 8,'Organizer: '..msg)
  500.     flog(_debugging.debug_log, 'Organizer [MSG] '..msg)
  501. end
  502.  
  503. function org_warning(msg)
  504.     if _debugging.warnings then
  505.         windower.add_to_chat(123,'Organizer: '..msg)
  506.     end
  507.     flog(_debugging.debug_log, 'Organizer [WARN] '..msg)
  508. end
  509.  
  510. function org_debug(level, msg)
  511.     if(_debugging.enabled) then
  512.         if (_debugging.debug[level]) then
  513.             flog(_debugging.debug_log, 'Organizer [DEBUG] ['..level..']: '..msg)
  514.         end
  515.     end
  516. end
  517.  
  518.  
  519. function org_error(msg)
  520.     error('Organizer: '..msg)
  521.     flog(_debugging.debug_log, 'Organizer [ERROR] '..msg)
  522. end
  523.  
  524. function org_verbose(msg,col)
  525.     if tostring(settings.verbose):lower() ~= 'false' then
  526.         windower.add_to_chat(col or 8,'Organizer: '..msg)
  527.     end
  528.     flog(_debugging.debug_log, 'Organizer [VERBOSE] '..msg)
  529. end
  530.  
  531. function default_file_name()
  532.     player = windower.ffxi.get_player()
  533.     job_name = res.jobs[player.main_job_id]['english_short']
  534.     return player.name..'_'..job_name..'.lua'
  535. end
  536.  
  537. function simulate_item_delay()
  538.     if settings.item_delay and settings.item_delay > 0 then
  539.         coroutine.sleep(settings.item_delay)
  540.     end
  541. end
  542.  
  543. function get_dump_bags()
  544.     local dump_bags = {}
  545.     for i,v in pairs(settings.dump_bags) do
  546.         if i and s_to_bag(i) then
  547.             dump_bags[tonumber(v)] = s_to_bag(i)
  548.         elseif i then
  549.             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))
  550.             return
  551.         end
  552.     end
  553.     return dump_bags
  554. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement