summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua
diff options
context:
space:
mode:
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/equipviewer/icon_extractor.lua266
1 files changed, 266 insertions, 0 deletions
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