diff options
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/equipviewer')
-rw-r--r-- | Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/README.md | 44 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/encumbrance.png | bin | 0 -> 1500 bytes | |||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/equipviewer.lua | 743 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua | 266 |
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 Binary files differnew file mode 100644 index 0000000..0b0bff3 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/encumbrance.png 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 |