summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer
diff options
context:
space:
mode:
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/equipviewer')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/README.md44
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/encumbrance.pngbin0 -> 1500 bytes
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/equipviewer.lua743
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua266
4 files changed, 1053 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/README.md b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/README.md
new file mode 100644
index 0000000..c528c70
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/README.md
@@ -0,0 +1,44 @@
+**Author:** Tako, Rubenator<br>
+**Version:** 3.3.0<br>
+**Date:** April 13, 2021<br>
+
+* Displays current equipment grid on screen. Also can show current Ammo count and current Encumbrance.
+
+## Settings
+
+* Most settings can be modified via commands, but you can edit the settings.xml directly for a few uncommon settings.
+
+**Abbreviation:** `//ev`
+
+## Commands
+1. position <xpos> <ypos>: move display to position (from top left)
+2. size <pixels>: set pixel size of each item slot (defaults to 32 -- same as the size of the item icons)
+3. scale <factor>: scale multiplier for size of each item slot (1 is 32px) -- modifies same setting as size
+4. alpha <opacity>: set opacity of icons (out of 255)
+5. transparency <transparency>: inverse of alpha (out of 255) -- modifies same setting as alpha
+6. background <red> <green> <blue> <alpha>: sets color and opacity of background (out of 255)
+7. ammocount: toggles showing current ammo count (defaults to on/true)
+8. encumbrance: toggles showing encumbrance Xs (defaultis on/true)
+9. hideonzone: toggles hiding while crossing zone lines (default is on/true)
+10. hideoncutscene: toggles hiding when in cutscene/npc menu/etc (default is on/true)
+11. justify: toggles between ammo text being right or left justifed (default is right justified)
+12. help: displays explanations of each command
+
+Legacy Command:
+game_path <path>: sets path to FFXI folder where you want dats extracted from. Backslashes `\` must be escaped (like so: `\\`) or use forewardslash `/` instead. (legacy command as of `3.3.1` in which the game path is now pulled from the registry, but this command is still here in case you want to pull from dats that exist elsewhere)
+
+### Example Commands
+```
+//ev pos 700 400
+//ev size 64
+//ev scale 1.5
+//ev alpha 255
+//ev transparency 200
+//ev background 0 0 0 72
+//ev ammocount
+//ev encumbrance
+//ev hideonzone
+//ev hideoncutscene
+//ev justify
+//ev help
+```
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/encumbrance.png b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/encumbrance.png
new file mode 100644
index 0000000..0b0bff3
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/encumbrance.png
Binary files differ
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/equipviewer.lua b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/equipviewer.lua
new file mode 100644
index 0000000..ff5e1e1
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/equipviewer.lua
@@ -0,0 +1,743 @@
+--[[
+ Copyright © 2021, Rubenator
+ 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 EquipViewer 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 Rubenator 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.
+]]
+_addon.name = 'Equipviewer'
+_addon.version = '3.3.1'
+_addon.author = 'Tako, Rubenator'
+_addon.commands = { 'equipviewer', 'ev' }
+
+require('luau')
+local bit = require('bit')
+local config = require('config')
+local images = require('images')
+local texts = require('texts')
+local functions = require('functions')
+local packets = require('packets')
+local icon_extractor = require('icon_extractor')
+
+local equipment_data = {
+ [0] = {slot_name = 'main', slot_id = 0, display_pos = 0, item_id = 0, image = nil},
+ [1] = {slot_name = 'sub', slot_id = 1, display_pos = 1, item_id = 0, image = nil},
+ [2] = {slot_name = 'range', slot_id = 2, display_pos = 2, item_id = 0, image = nil},
+ [3] = {slot_name = 'ammo', slot_id = 3, display_pos = 3, item_id = 0, image = nil},
+ [4] = {slot_name = 'head', slot_id = 4, display_pos = 4, item_id = 0, image = nil},
+ [5] = {slot_name = 'body', slot_id = 5, display_pos = 8, item_id = 0, image = nil},
+ [6] = {slot_name = 'hands', slot_id = 6, display_pos = 9, item_id = 0, image = nil},
+ [7] = {slot_name = 'legs', slot_id = 7, display_pos = 14, item_id = 0, image = nil},
+ [8] = {slot_name = 'feet', slot_id = 8, display_pos = 15, item_id = 0, image = nil},
+ [9] = {slot_name = 'neck', slot_id = 9, display_pos = 5, item_id = 0, image = nil},
+ [10] = {slot_name = 'waist', slot_id = 10, display_pos = 13, item_id = 0, image = nil},
+ [11] = {slot_name = 'left_ear', slot_id = 11, display_pos = 6, item_id = 0, image = nil},
+ [12] = {slot_name = 'right_ear', slot_id = 12, display_pos = 7, item_id = 0, image = nil},
+ [13] = {slot_name = 'left_ring', slot_id = 13, display_pos = 10, item_id = 0, image = nil},
+ [14] = {slot_name = 'right_ring', slot_id = 14, display_pos = 11, item_id = 0, image = nil},
+ [15] = {slot_name = 'back', slot_id = 15, display_pos = 12, item_id = 0, image = nil},
+}
+local encumbrance_data = {}
+for i=0,15 do
+ encumbrance_data[i] = { slot_name = 'encumbrance', slot_id = i, display_pos = equipment_data[i].display_pos, image = nil }
+end
+local ammo_count_text = nil
+local bg_image = nil
+
+local defaults = {
+ pos = {
+ x = 500,
+ y = 500
+ },
+ size = 32,
+ ammo_text = {
+ alpha = 230,
+ red = 255,
+ green = 255,
+ blue = 255,
+ stroke = {
+ width = 1,
+ alpha = 127,
+ red = 0,
+ green = 0,
+ blue = 0,
+ },
+ flags = {
+ bold = true,
+ italic = true,
+ }
+ },
+ icon = {
+ alpha = 230,
+ red = 255,
+ green = 255,
+ blue = 255,
+ },
+ bg = {
+ alpha = 72,
+ red = 0,
+ green = 0,
+ blue = 0,
+ },
+ show_encumbrance = true,
+ show_ammo_count = true,
+ hide_on_zone = true,
+ hide_on_cutscene = true,
+ left_justify = false,
+}
+settings = config.load(defaults)
+config.save(settings)
+if settings.game_path then
+ icon_extractor.ffxi_path(settings.game_path)
+end
+local last_encumbrance_bitfield = 0
+
+-- gets the currently equipped item data for the slot information provided
+local function get_equipped_item(slotName, slotId, bag, index)
+ if not bag or not index then -- from memory
+ local equipment = windower.ffxi.get_items('equipment')
+ bag = equipment[string.format('%s_bag', slotName)]
+ index = equipment[slotName]
+ if equipment_data[slotId] then
+ equipment_data[slotId].bag_id = bag
+ equipment_data[slotId].index = index
+ end
+ end
+ if index == 0 then -- empty equipment slot
+ return 0, 0
+ end
+ local item_data = windower.ffxi.get_items(bag, index)
+ return item_data.id, item_data.count
+end
+
+-- desc: Updates the ui object(s) for the given slot
+local function update_equipment_slot(source, slot, bag, index, item, count)
+ local slot_data = equipment_data[slot]
+ slot_data.bag_id = bag or slot_data.bag_id
+ slot_data.index = index or slot_data.index
+ if not item then
+ item, count = get_equipped_item(slot_data.slot_name, slot_data.slot_id, bag, index)
+ end
+ if evdebug then
+ bag = slot_data.bag_id
+ index = slot_data.index
+ log('%s %s %d %d %d':format(source, slot_data.slot_name, item, bag or -1, index or -1))
+ print('%s %s %d %d %d':format(source, slot_data.slot_name, item, bag or -1, index or -1))
+ end
+ if slot_data.slot_name == 'ammo' then
+ slot_data.count = count or slot_data.count or 0
+ end
+ if slot_data.image and item ~= nil then
+ if item == 0 or item == 65535 then -- empty slot
+ slot_data.image:hide()
+ slot_data.image:clear()
+ slot_data.item_id = 0
+ slot_data.count = nil
+ slot_data.image:update()
+ elseif slot_data.item_id ~= item then
+ slot_data.item_id = item
+ local icon_path = string.format('%sicons/%s.bmp', windower.addon_path, slot_data.item_id)
+
+ if not windower.file_exists(icon_path) then
+ icon_extractor.item_by_id(slot_data.item_id, icon_path)
+ end
+ if windower.file_exists(icon_path) then
+ slot_data.image:path(icon_path)
+ slot_data.image:alpha(settings.icon.alpha)
+ slot_data.image:show()
+ end
+ end
+ if slot_data.slot_name == 'ammo' then
+ display_ammo_count(slot_data.count)
+ end
+ slot_data.image:update()
+ end
+end
+
+-- Updates the texture for all slots if it's a different piece of equipment
+local function update_equipment_slots(source)
+ for slot in pairs(equipment_data) do
+ update_equipment_slot(source, slot)
+ end
+end
+
+-- Sets up the image and text ui objects for our equipment
+local function setup_ui()
+ refresh_ui_settings()
+ destroy()
+
+ bg_image = images.new(bg_image_settings)
+ bg_image:show()
+
+ for key, slot in pairs(equipment_data) do
+ slot.item_id = 0
+ slot.image = images.new(equipment_image_settings)
+ position(slot)
+ end
+ update_equipment_slots('setup_ui')
+
+ for key, slot in pairs(encumbrance_data) do
+ slot.image = images.new(encumbrance_image_settings)
+ slot.image:path(windower.addon_path..'encumbrance.png')
+ slot.image:hide()
+ position(slot)
+ end
+ display_encumbrance()
+
+ ammo_count_text = texts.new(settings.left_justify and ammo_count_text_settings_left_justify or ammo_count_text_settings)
+ display_ammo_count()
+end
+
+-- Called when the addon is first loaded.
+windower.register_event('load', function()
+ --Make sure icons directory exists
+ if not windower.dir_exists(string.format('%sicons', windower.addon_path)) then
+ windower.create_dir(string.format('%sicons', windower.addon_path))
+ end
+
+ if windower.ffxi.get_info().logged_in then
+ setup_ui()
+ end
+end)
+
+-- Called whenever character logs out.
+windower.register_event('logout', function()
+ clear_all_equipment_slots()
+ destroy()
+end)
+
+-- Called whenever character logs in.
+windower.register_event('login', function()
+ setup_ui()
+ update_equipment_slots('login')
+end)
+
+-- Called when our addon receives an incoming chunk.
+windower.register_event('incoming chunk', function(id, original, modified, injected, blocked)
+ if id == 0x050 then --Equip/Unequip
+ local packet = packets.parse('incoming', original)
+ local index = packet['Inventory Index']
+ local slot = packet['Equipment Slot']
+ local bag = packet['Inventory Bag']
+ equipment_data[slot].bag_id = bag
+ equipment_data[slot].index = index
+ update_equipment_slot:schedule(0, '0x050', slot, bag, index)
+ elseif id == 0x020 or id == 0x01F or id == 0x01E then --Item Update / Item Assign (ammo consumption) / 0x01E item count/last ammo shot
+ local packet = packets.parse('incoming', original)
+ local bag = packet['Bag']
+ local index = packet['Index']
+
+ local slot = nil
+ for _,slot_data in pairs(equipment_data) do
+ if slot_data.bag_id == bag and slot_data.index == index then
+ slot = slot_data.slot_id
+ break
+ end
+ end
+
+ if slot then
+ if packet['Status'] ~= 5 and packet['Count'] == 0 then --item not equipped
+ update_equipment_slot:schedule(0, '0x%x':format(id), slot, 0, 0, 0)
+ return
+ end
+ if slot == 3 then --ammo
+ local count = packet['Count'] or 0
+ display_ammo_count(count)
+ end
+ local item = packet['Item']
+ update_equipment_slot:schedule(0,'0x%x':format(id), slot, bag, index, item, count)
+ end
+ elseif id == 0x01B then -- Job Info (Encumbrance Flags)
+ local packet = packets.parse('incoming', original)
+ display_encumbrance(packet['Encumbrance Flags'])
+ elseif id == 0x0A then -- Finish Zone
+ show()
+ elseif id == 0x0B then -- Zone
+ if settings.hide_on_zone then
+ hide()
+ end
+ end
+end)
+
+-- Called when our addon receives an outgoing chunk.
+windower.register_event('outgoing chunk', function(id, original, modified, injected, blocked)
+ if id == 0x100 then -- Job Change Request
+ clear_all_equipment_slots()
+ end
+end)
+
+
+-- Destroys all created ui objects
+function destroy()
+ if bg_image then
+ bg_image:destroy()
+ bg_image = nil
+ end
+ for key, slot_data in pairs(equipment_data) do
+ if slot_data.image ~= nil then
+ slot_data.image:destroy()
+ slot_data.image = nil
+ end
+ end
+ for key, slot_data in pairs(encumbrance_data) do
+ if slot_data.image ~= nil then
+ slot_data.image:destroy()
+ slot_data.image = nil
+ end
+ end
+ if ammo_count_text then
+ ammo_count_text:destroy()
+ ammo_count_text = nil
+ end
+end
+
+-- Shows appropriate ui objects
+function show()
+ if bg_image then
+ bg_image:show()
+ end
+ for key, slot_data in pairs(equipment_data) do
+ if slot_data.item_id ~= 0 and slot_data.image then
+ slot_data.image:show()
+ end
+ end
+ display_encumbrance()
+ display_ammo_count()
+end
+
+-- Hides all ui objects
+function hide()
+ if bg_image then
+ bg_image:hide()
+ end
+ for key, slot_data in pairs(equipment_data) do
+ if slot_data.image then
+ slot_data.image:hide()
+ end
+ end
+ for key, slot_data in pairs(encumbrance_data) do
+ if slot_data.image then
+ slot_data.image:hide()
+ end
+ end
+ if ammo_count_text then
+ ammo_count_text:hide()
+ end
+end
+
+-- Moves ui object to correct spot based on 'display_pos' field
+function position(slot)
+ local pos_x = settings.pos.x + ((slot.display_pos % 4) * settings.size)
+ local pos_y = settings.pos.y + (math.floor(slot.display_pos / 4) * settings.size)
+ slot.image:pos(pos_x, pos_y)
+end
+
+-- Clears all equipment slot data and hides ui object
+function clear_slot(slot)
+ local slot_data = equipment_data[slot]
+ slot_data.image:hide()
+ slot_data.image:clear()
+ slot_data.item_id = 0
+ slot_data.bag_id = nil
+ slot_data.index = nil
+ slot_data.count = nil
+ slot_data.image:update()
+
+ display_ammo_count()
+end
+
+-- Clears all equipment slot data and hides equipment slot ui objects
+function clear_all_equipment_slots()
+ for slot in pairs(equipment_data) do
+ clear_slot(slot)
+ end
+end
+
+-- Shows and hides appropriate encumbrance ui objects and possibly updates encumbrance
+-- flags based on provided bitfield number
+function display_encumbrance(bitfield)
+ bitfield = bitfield or last_encumbrance_bitfield
+ last_encumbrance_bitfield = bitfield
+ for key, slot in pairs(encumbrance_data) do
+ if slot.image then
+ if not settings.show_encumbrance or bit.band(bitfield, bit.lshift(1,key)) == 0 then
+ slot.image:hide()
+ else
+ slot.image:show()
+ end
+ end
+ end
+end
+
+-- Displays appropriatly and possibly updates ammo count and ui object
+function display_ammo_count(count)
+ if not ammo_count_text then return end
+ count = count or equipment_data[3] and equipment_data[3].count -- 3 == Ammo
+ equipment_data[3].count = count
+ if not settings.show_ammo_count or not count or count <= 1 then
+ ammo_count_text:hide()
+ else
+ ammo_count_text:text(count and tostring(count) or '')
+ ammo_count_text:show()
+ end
+end
+
+-- Called when player status changes.
+windower.register_event('status change', function(new_status_id)
+ if new_status_id == 4 and settings.hide_on_cutscene then --Cutscene/Menu
+ hide()
+ else
+ show()
+ end
+end)
+
+-- Called when our addon is unloaded.
+windower.register_event('unload', function()
+ destroy()
+end)
+
+-- Called when the addon receives a command.
+windower.register_event('addon command', function (...)
+ config.reload(settings)
+ coroutine.sleep(0.5)
+ local cmd = (...) and (...):lower() or ''
+ local cmd_args = {select(2, ...)}
+ if cmd == "gamepath" or cmd == "game_path" then
+ if #cmd_args == 0 then
+ error("Must provide path.")
+ log('Current Path: %s':format(
+ "\""..settings.game_path.."\"" or "(Default): \""..windower.ffxi_path
+ ))
+ return
+ end
+ local path = table.concat(cmd_args, " ")
+ if path:lower() == "default" then
+ settings.game_path = nil
+ else
+ settings.game_path = table.concat(cmd_args, " ")
+ end
+ config.save(settings)
+ icon_extractor.ffxi_path(settings.game_path)
+
+ setup_ui()
+
+ log('game_path set to "%s"':format(path))
+ elseif cmd == 'position' or cmd == 'pos' then
+ if #cmd_args < 2 then
+ error('Not enough arguments.')
+ log('Current position: '..settings.pos.x..' '..settings.pos.y)
+ return
+ end
+
+ settings.pos.x = tonumber(cmd_args[1])
+ settings.pos.y = tonumber(cmd_args[2])
+ config.save(settings)
+
+ setup_ui()
+
+ log('Position changed to '..settings.pos.x..', '..settings.pos.y)
+ elseif cmd == 'size' then
+ if #cmd_args < 1 then
+ error('Not enough arguments.')
+ log('Current size: '..settings.size)
+ return
+ end
+
+ settings.size = tonumber(cmd_args[1])
+ config.save(settings)
+
+ setup_ui()
+
+ log('Size changed to '..settings.size)
+ elseif cmd == 'scale' then
+ if #cmd_args < 1 then
+ error('Not enough arguments.')
+ log('Current scale: '..settings.size/32)
+ return
+ end
+ local size = tonumber(cmd_args[1])*32
+ if size > 100 then
+ error('Size too large')
+ end
+ settings.size = size
+ config.save(settings)
+
+ setup_ui()
+
+ log('Size changed to '..settings.size)
+ elseif cmd == 'alpha' or cmd == 'opacity' then
+ if #cmd_args < 1 then
+ error('Not enough arguments.')
+ log('Current alpha/opacity: %d/255 = %d%%':format(
+ settings.icon.alpha, math.floor(settings.icon.alpha/255*100)
+ ))
+ return
+ end
+ local alpha = tonumber(cmd_args[1])
+ if alpha <= 1 and alpha > 0 then
+ settings.icon.alpha = math.floor(255 * (alpha))
+ else
+ settings.icon.alpha = math.floor(alpha)
+ end
+ config.save(settings)
+
+ setup_ui()
+
+ log('Alpha/Opacity changed to '..settings.icon.alpha..'/255')
+ elseif cmd:contains('transpar') then
+ if #cmd_args < 1 then
+ error('Not enough arguments.')
+ log('Current transparency: %d/255 = %d%%':format(
+ (255-settings.icon.alpha), math.floor((255-settings.icon.alpha)/255)*100
+ ))
+ return
+ end
+ local transparency = tonumber(cmd_args[1])
+ if transparency <= 1 and transparency > 0 then
+ settings.icon.alpha = math.floor(255 * (1-transparency))
+ else
+ settings.icon.alpha = math.floor(255-transparency)
+ end
+ config.save(settings)
+
+ setup_ui()
+
+ log('Transparency changed to '..255-settings.icon.alpha..'/255')
+ elseif cmd == 'background' or cmd == 'bg' then
+ if #cmd_args < 1 then
+ error('Not enough arguments.')
+ log('Current BG color: RED:%d/255 GREEN:%d/255 BLUE:%d/255 ALPHA:%d/255 = %d%%':format(
+ settings.bg.red, settings.bg.green, settings.bg.blue, settings.bg.alpha, math.floor(settings.bg.alpha/255*100)
+ ))
+ return
+ elseif #cmd_args == 1 then
+ local alpha = tonumber(cmd_args[1])
+ if alpha <= 1 and alpha > 0 then
+ settings.bg.alpha = math.floor(255 * (alpha))
+ else
+ settings.bg.alpha = math.floor(alpha)
+ end
+ elseif #cmd_args >= 3 then
+ settings.bg.red = tonumber(cmd_args[1])
+ settings.bg.green = tonumber(cmd_args[2])
+ settings.bg.blue = tonumber(cmd_args[3])
+ if #cmd_args == 4 then
+ local alpha = tonumber(cmd_args[4])
+ if alpha <= 1 and alpha > 0 then
+ settings.bg.alpha = math.floor(255 * (alpha))
+ else
+ settings.bg.alpha = math.floor(alpha)
+ end
+ end
+ end
+ config.save(settings)
+
+ setup_ui()
+
+ log('BG color changed to: RED:%d/255 GREEN:%d/255 BLUE:%d/255 ALPHA:%d/255 = %d%%':format(
+ settings.bg.red, settings.bg.green, settings.bg.blue, settings.bg.alpha, math.floor(settings.bg.alpha/255*100)
+ ))
+ elseif cmd:contains('encumb') then
+ settings.show_encumbrance = not settings.show_encumbrance
+ config.save(settings)
+
+ display_encumbrance()
+
+ log('show_encumbrance changed to '..tostring(settings.show_encumbrance))
+ elseif cmd:contains('ammo') or cmd:contains('count') then
+ settings.show_ammo_count = not settings.show_ammo_count
+ config.save(settings)
+
+ display_ammo_count()
+
+ log('show_ammo_count changed to '..tostring(settings.show_ammo_count))
+ elseif cmd == 'hideonzone' or cmd == 'zone' then
+ settings.hide_on_zone = not settings.hide_on_zone
+ config.save(settings)
+
+ log('hide_on_zone changed to '..tostring(settings.hide_on_zone))
+ elseif cmd == 'hideoncutscene' or cmd == 'cutscene' then
+ settings.hide_on_cutscene = not settings.hide_on_cutscene
+ config.save(settings)
+
+ log('hide_on_cutscene changed to '..tostring(settings.hide_on_cutscene))
+ elseif cmd == 'justify' then
+ settings.left_justify = not settings.left_justify
+ config.save(settings)
+
+ setup_ui()
+
+ log('Ammo text justification changed to '..tostring(settings.left_justify and 'Left' or 'Right'))
+ elseif cmd == 'testenc' then
+ display_encumbrance(0xffff)
+ elseif cmd == 'debug' then
+ if #cmd_args < 1 then
+ local e = windower.ffxi.get_items('equipment')
+ for i=0,15 do
+ local v = equipment_data[i]
+ local b = e[string.format('%s_bag', v.slot_name)]
+ local eb = v.bag_id
+ local ind = v.index
+ local eind = e[v.slot_name]
+ local it = v.item_id
+ local eit = windower.ffxi.get_items(eb, eind).id
+ log('%s[%d] it=%d eit=%d b=%d eb=%d i=%d ei=%d':format(v.slot_name,i, it, eit, b, eb, ind, eind))
+ end
+ elseif S{'1', 'on', 'true'}:contains(cmd_args[1]) then
+ evdebug = true
+ elseif S{'0', 'off', 'false'}:contains(cmd_args[1]) then
+ evdebug = false
+ end
+ else
+ log('HELP:')
+ log('ev position <xpos> <ypos>: move to position (from top left)')
+ log('ev size <pixels>: set pixel size of each item slot')
+ log('ev scale <factor>: scale multiplier for each item slot (from 32px)')
+ log('ev alpha <opacity>: set opacity of icons (out of 255)')
+ log('ev transparency <transparency>: inverse of alpha (out of 255)')
+ log('ev background <red> <green> <blue> <alpha>: sets color and opacity of background (out of 255)')
+ log('ev ammocount: toggles showing current ammo count')
+ log('ev encumbrance: toggles showing encumbrance Xs')
+ log('ev hideonzone: toggles hiding while crossing zone line')
+ log('ev hideoncutscene: toggles hiding when in cutscene/npc menu/etc')
+ log('ev justify: toggles between ammo text left and right justify')
+ end
+end)
+
+function refresh_ui_settings()
+ --Image and text settings
+ bg_image_settings = {
+ alpha = settings.bg.alpha,
+ color = {
+ alpha = settings.bg.alpha,
+ red = settings.bg.red,
+ green = settings.bg.green,
+ blue = settings.bg.blue,
+ },
+ pos = {
+ x = settings.pos.x,
+ y = settings.pos.y,
+ },
+ size = {
+ width = settings.size * 4,
+ height = settings.size * 4,
+ },
+ draggable = false,
+ }
+ equipment_image_settings = {
+ color = {
+ alpha = settings.icon.alpha,
+ red = settings.icon.red,
+ green = settings.icon.green,
+ blue = settings.icon.blue,
+ },
+ texture = {
+ fit = false,
+ },
+ size = {
+ width = settings.size,
+ height = settings.size,
+ },
+ draggable = false,
+ }
+ encumbrance_image_settings = {
+ color = {
+ alpha = settings.icon.alpha*0.8,
+ red = settings.icon.red,
+ green = settings.icon.green,
+ blue = settings.icon.blue,
+ },
+ texture = {
+ fit = false,
+ },
+ size = {
+ width = settings.size,
+ height = settings.size,
+ },
+ draggable = false,
+ }
+ ammo_count_text_settings = {
+ text = {
+ size = settings.size*0.27,
+ alpha = settings.ammo_text.alpha,
+ red = settings.ammo_text.red,
+ green = settings.ammo_text.green,
+ blue = settings.ammo_text.blue,
+ stroke = {
+ width = settings.ammo_text.stroke.width,
+ alpha = settings.ammo_text.stroke.alpha,
+ red = settings.ammo_text.stroke.red,
+ green = settings.ammo_text.stroke.green,
+ blue = settings.ammo_text.stroke.blue,
+ },
+ },
+ bg = {
+ alpha = 0,
+ red = 255,
+ blue = 255,
+ green = 255
+ },
+ pos = {
+ x = (windower.get_windower_settings().ui_x_res - (settings.pos.x + settings.size*4))*-1,
+ y = settings.pos.y + settings.size*0.58,
+ },
+ flags = {
+ draggable = false,
+ right = true,
+ bold = settings.ammo_text.flags.bold,
+ italic = settings.ammo_text.flags.italic,
+ },
+ }
+ ammo_count_text_settings_left_justify = {
+ text = {
+ size = settings.size*0.27,
+ alpha = settings.ammo_text.alpha,
+ red = settings.ammo_text.red,
+ green = settings.ammo_text.green,
+ blue = settings.ammo_text.blue,
+ stroke = {
+ width = settings.ammo_text.stroke.width,
+ alpha = settings.ammo_text.stroke.alpha,
+ red = settings.ammo_text.stroke.red,
+ green = settings.ammo_text.stroke.green,
+ blue = settings.ammo_text.stroke.blue,
+ },
+ },
+ bg = {
+ alpha = 0,
+ red = 255,
+ blue = 255,
+ green = 255
+ },
+ pos = {
+ x = settings.pos.x + settings.size*3,
+ y = settings.pos.y + settings.size*0.58
+ },
+ flags = {
+ draggable = false,
+ right = false,
+ bold = settings.ammo_text.flags.bold,
+ italic = settings.ammo_text.flags.italic,
+ },
+ }
+end
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua
new file mode 100644
index 0000000..0424e7b
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua
@@ -0,0 +1,266 @@
+--[[
+ Copyright © 2021, Rubenator
+ 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 EquipViewer 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 Rubenator 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.
+]]
+-- icon_extractor v1.1.2
+-- Written by Rubenator of Leviathan
+-- Base Extraction Code graciously provided by Trv of Windower discord
+local icon_extractor = {}
+
+local game_path_default = windower.ffxi_path
+local game_path = game_path_default
+
+local string = require('string')
+local io = require('io')
+local math = require('math')
+
+local concat = table.concat
+local floor = math.floor
+local byte = string.byte
+local char = string.char
+local sub = string.sub
+
+local file_size = '\122\16\00\00'
+local reserved1 = '\00\00'
+local reserved2 = '\00\00'
+local starting_address = '\122\00\00\00'
+
+local default = '\00\00\00\00'
+
+local dib_header_size = '\108\00\00\00'
+local bitmap_width = '\32\00\00\00'
+local bitmap_height = '\32\00\00\00'
+local n_color_planes = '\01\00'
+local bits_per_pixel = '\32\00'
+local compression_type = '\03\00\00\00'
+local image_size = '\00\16\00\00'
+local h_resolution_target = default
+local v_resolution_target = default
+local default_n_colors = default
+local important_colors = default
+local alpha_mask = '\00\00\00\255'
+local red_mask = '\00\00\255\00'
+local green_mask = '\00\255\00\00'
+local blue_mask = '\255\00\00\00'
+local colorspace = 'sRGB'
+local endpoints = string.rep('\00', 36)
+local red_gamma = default
+local green_gamma = default
+local blue_gamma = default
+
+local header = 'BM' .. file_size .. reserved1 .. reserved2 .. starting_address
+ .. dib_header_size .. bitmap_width .. bitmap_height .. n_color_planes
+ .. bits_per_pixel .. compression_type .. image_size
+ .. h_resolution_target .. v_resolution_target
+ .. default_n_colors .. important_colors
+ .. red_mask .. green_mask .. blue_mask .. alpha_mask
+ .. colorspace .. endpoints .. red_gamma .. green_gamma .. blue_gamma
+
+--local icon_file = io.open('C:/Program Files (x86)/PlayOnline/SquareEnix/FINAL FANTASY XI/ROM/118/106.DAT', 'rb')
+local color_lookup = {}
+local bmp_segments = {}
+
+for i = 0x000, 0x0FF do
+ color_lookup[string.char(i)] = ''
+end
+
+--[[
+3072 bytes per icon
+640 bytes for stats, string table, etc.
+2432 bytes for pixel data
+--]]
+
+local item_dat_map = {
+ [1]={min=0x0001, max=0x0FFF, dat_path='118/106', offset=-1}, -- General Items
+ [2]={min=0x1000, max=0x1FFF, dat_path='118/107', offset=0}, -- Usable Items
+ [3]={min=0x2000, max=0x21FF, dat_path='118/110', offset=0}, -- Automaton Items
+ [4]={min=0x2200, max=0x27FF, dat_path='301/115', offset=0}, -- General Items 2
+ [5]={min=0x2800, max=0x3FFF, dat_path='118/109', offset=0}, -- Armor Items
+ [6]={min=0x4000, max=0x59FF, dat_path='118/108', offset=0}, -- Weapon Items
+ [7]={min=0x5A00, max=0x6FFF, dat_path='286/73', offset=0}, -- Armor Items 2
+ [8]={min=0x7000, max=0x73FF, dat_path='217/21', offset=0}, -- Maze Items, Basic Items
+ [9]={min=0x7400, max=0x77FF, dat_path='288/80', offset=0}, -- Instinct Items
+ [10]={min=0xF000, max=0xF1FF, dat_path='288/67', offset=0}, -- Monipulator Items
+ [11]={min=0xFFFF, max=0xFFFF, dat_path='174/48', offset=0}, -- Gil
+}
+
+local item_by_id = function (id, output_path)
+ local dat_stats = find_item_dat_map(id)
+ local icon_file = open_dat(dat_stats)
+
+ local id_offset = dat_stats.min + dat_stats.offset
+ icon_file:seek('set', (id - id_offset) * 0xC00 + 0x2BD)
+ local data = icon_file:read(0x800)
+
+ bmp = convert_item_icon_to_bmp(data)
+
+ local f = io.open(output_path, 'wb')
+ f:write(bmp)
+ coroutine.yield()
+ f:close()
+end
+icon_extractor.item_by_id = item_by_id
+
+function find_item_dat_map(id)
+ for _,stats in pairs(item_dat_map) do
+ if id >= stats.min and id <= stats.max then
+ return stats
+ end
+ end
+ return nil
+end
+
+function open_dat(dat_stats)
+ local icon_file = nil
+ if dat_stats.file then
+ icon_file = dat_stats.file
+ else
+ if not game_path then
+ error('ffxi_path must be set before using icon_extractor library')
+ end
+ filename = game_path .. '/ROM/' .. tostring(dat_stats.dat_path) .. '.DAT'
+ icon_file, err = io.open(filename, 'rb')
+ if not icon_file then
+ error(err)
+ return
+ end
+ dat_stats.file = icon_file
+ end
+ return icon_file
+end
+
+-- 32 bit color palette-indexed bitmaps. Bits are rotated and must be decoded.
+local encoded_to_decoded_char = {}
+local encoded_byte_to_rgba = {}
+local alpha_encoded_to_decoded_adjusted_char = {}
+local decoded_byte_to_encoded_char = {}
+for i = 0x000, 0x0FF do
+ encoded_byte_to_rgba[i] = ''
+ local n = (i % 0x20) * 0x8 + floor(i / 0x20)
+ encoded_to_decoded_char[char(i)] = char(n)
+ decoded_byte_to_encoded_char[n] = char(i)
+ n = n * 0x2
+ n = n < 0x100 and n or 0x0FF
+ alpha_encoded_to_decoded_adjusted_char[char(i)] = char(n)
+end
+local decoder = function(a, b, c, d)
+ return encoded_to_decoded_char[a]..
+ encoded_to_decoded_char[b]..
+ encoded_to_decoded_char[c]..
+ alpha_encoded_to_decoded_adjusted_char[d]
+end
+function convert_item_icon_to_bmp(data)
+ local color_palette = string.gsub(sub(data, 0x001, 0x400), '(.)(.)(.)(.)', decoder)
+ -- rather than decoding all 2048 bytes, decode only the palette and index it by encoded byte
+ for i = 0x000, 0x0FF do
+ local offset = i * 0x4 + 0x1
+ encoded_byte_to_rgba[decoded_byte_to_encoded_char[i]] = sub(color_palette, offset, offset + 0x3)
+ end
+
+ return header .. string.gsub(sub(data, 0x401, 0x800), '(.)', function(a) return encoded_byte_to_rgba[a] end)
+end
+
+
+local buff_dat_map = {
+ [1]={min=0x000, max=0x400, dat_path='119/57', offset=0},
+}
+function find_buff_dat_map(id)
+ for _,stats in pairs(buff_dat_map) do
+ if id >= stats.min and id <= stats.max then
+ return stats
+ end
+ end
+ return nil
+end
+local buff_by_id = function (id, output_path)
+ local dat_stats = find_buff_dat_map(id)
+ local icon_file = open_dat(dat_stats)
+
+ local id_offset = dat_stats.min + dat_stats.offset
+ icon_file:seek('set', (id - id_offset) * 0x1800)
+ local data = icon_file:read(0x1800)
+
+ bmp = convert_buff_icon_to_bmp(data)
+
+ local f = io.open(output_path, 'wb')
+ f:write(bmp)
+ coroutine.yield()
+ f:close()
+end
+icon_extractor.buff_by_id = buff_by_id
+
+
+local ffxi_path = function(location)
+ game_path = location or game_path_default
+ close_dats()
+end
+icon_extractor.ffxi_path = ffxi_path
+
+
+-- A mix of 32 bit color uncompressed and *color palette-indexed bitmaps
+-- Offsets defined specifically for status icons
+-- * some maps use this format as well, but at 512 x 512
+function convert_buff_icon_to_bmp(data)
+ local length = byte(data, 0x282) -- The length is technically sub(0x281, 0x284), but only 0x282 is unique
+
+ if length == 16 then -- uncompressed
+ data = sub(data, 0x2BE, 0x12BD)
+ data = string.gsub(data, '(...)\x80', '%1\xFF') -- All of the alpha bytes are currently 0 or 0x80.
+ elseif length == 08 then -- color table
+ local color_palette = sub(data, 0x2BE, 0x6BD)
+ color_palette = string.gsub(color_palette, '(...)\x80', '%1\xFF')
+
+ local n = 0x0
+ for i = 1, 0x400, 0x4 do
+ color_lookup[char(n)] = sub(color_palette, i, i + 3)
+ n = n + 1
+ end
+
+ data = string.gsub(sub(data, 0x6BE, 0xABD), '(.)', function(i) return color_lookup[i] end)
+ elseif length == 04 then -- XIVIEW
+ data = sub(data, 0x2BE, 0x12BD)
+ end
+
+ return header .. data
+end
+
+function close_dats()
+ for _,dat in pairs(item_dat_map) do
+ if dat and dat.file then
+ dat.file:close()
+ dat.file = nil
+ end
+ end
+ for _,dat in pairs(buff_dat_map) do
+ if dat and dat.file then
+ dat.file:close()
+ dat.file = nil
+ end
+ end
+end
+
+windower.register_event('unload', function()
+ close_dats()
+end);
+
+return icon_extractor