Skip to content

Instantly share code, notes, and snippets.

@Chiaia
Created December 12, 2019 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Chiaia/dfb8624dbedfd6915897bbeda0b9b5db to your computer and use it in GitHub Desktop.
Save Chiaia/dfb8624dbedfd6915897bbeda0b9b5db to your computer and use it in GitHub Desktop.
Wardrobes2-4
--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'
_addon.name = 'Organizer'
_addon.author = 'Byrth, maintainer: Rooks'
_addon.version = 0.20191212
_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,
wardrobe3=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
_retain[item_id] = "moogle slip"
org_debug("settings", "Adding ("..res.items[item_id].english..') to slip retain list')
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")
slips = {29312,29313,29314,29315,29316,29317,29318,29319,29320,29321,29322,29323,29324,29325,29326,29327,29328,29329,29330,29331,29332,29333,29334,29335,29336,29337,29338,29339}
for _,slips_id in pairs(slips) 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 wardrobes, 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 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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment