Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions addons/libs/dialog.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
-- This library was written to help find the ID of a known
-- action message corresponding to an entry in the dialog tables.
-- While the IDs can be collected in-game, they occasionally
-- change and would otherwise need to be manually updated.
-- It can also be used to find and decode an entry given the ID.

-- Common parameters:
--
-- dat: Either the entire content of the zone dialog DAT file or
-- a file descriptor.
-- i.e. either local dat = io.open('path/to/dialog/DAT', 'rb')
-- or dat = dat:read('*a')
-- The functions are expected to be faster when passed a string,
-- but will use less memory when receiving a file descriptor.
--
-- entry: The string you are looking for. Whether or not the string
-- is expected to be encoded should be indicated in the parameter's
-- name. If you do not know the entire string, use dev.find_substring
-- and serialize the result.

local xor = require('bit').bxor
local floor = require('math').floor
local string = require('string')
local find = string.find
local sub = string.sub
local gsub = string.gsub
local format = string.format
local char = string.char
local byte = string.byte
require('pack')
local unpack = string.unpack
local pack = string.pack

local function decode(int)
return xor(int, 0x80808080)
end
local encode = decode

local function binary_search(pos, dat, n)
local l, r, m = 1, n
while l < r do
m = floor((l + r) / 2)
if decode(unpack('<I', dat, 1 + 4 * m)) < pos then
-- offset given by mth ID < offset to string
l = m + 1
else
r = m
end
end
return l - 2 -- we want the index to the left of where "pos" would be placed
end

local function plain_text_gmatch(text, substring, n)
n = n or 1
return function()
local head, tail = find(text, substring, n, true)
if head then n = head + 1 end
return head, tail
end
end

local dialog = {}

-- Returns the number of entries in the given dialog DAT file
function dialog.entry_count(dat)
if type(dat) == 'userdata' then
dat:seek('set', 4)
return decode(unpack('<I', dat:read(4))) / 4
end
return decode(unpack('<I', dat, 5)) / 4
end

-- Returns an array-like table containing every ID which matched
-- the given entry. Note that the tables contain an enormous
-- number of duplicate entries.
function dialog.get_ids_matching_entry(dat, encoded_entry)
local res = {}
local n = 0
if type(dat) == 'string' then
local last_offset = decode(unpack('<I', dat, 5))
local start = 5
for head, tail in plain_text_gmatch(dat, encoded_entry, last_offset) do
local encoded_pos = pack('<I', encode(head - 5))
local offset = find(dat, encoded_pos, start, true)
if offset then
offset = offset - 1
local next_pos
if offset > last_offset then
break
elseif offset == last_offset then
next_pos = #dat + 1
else
next_pos = decode(unpack('<I', dat, offset + 5)) + 5
end

if next_pos - head == tail - head + 1 then
n = n + 1
res[n] = (offset - 4) / 4
end
start = offset + 1
end
end

elseif type(dat) == 'userdata' then
dat:seek('set', 4)
local offset = decode(unpack('<I', dat:read(4)))
local entry_count = offset / 4
local entry_length = #encoded_entry
for i = 1, entry_count - 1 do
dat:seek('set', 4 * i + 4)
local next_offset = decode(unpack('<I', dat:read(4)))
if next_offset - offset == entry_length then
dat:seek('set', offset + 4)
if dat:read(entry_length) == encoded_entry then
n = n + 1
res[n] = i - 1
end
end

offset = next_offset
end
local m = dat:seek('end')
if m - offset - 4 == entry_length then
dat:seek('set', offset + 4)
if dat:read(entry_length) == encoded_entry then
n = n + 1
res[n] = entry_count - 1
end
end
end

return res
end

-- Returns the encoded entry from a given dialog table. If you
-- want to decode the entry, use dialog.decode_string.
function dialog.get_entry(dat, id)
local entry_count, offset, next_offset
if type(dat) == 'string' then
entry_count = decode(unpack('<I', dat, 5)) / 4
if id == entry_count - 1 then
offset = decode(unpack('<I', dat, 4 * id + 5)) + 5
next_offset = #dat + 1
else
offset, next_offset = unpack('<II', dat, 4 * id + 5)
offset, next_offset = decode(offset) + 5, decode(next_offset) + 5
end

return sub(dat, offset, next_offset - 1)
elseif type(dat) == 'userdata' then
dat:seek('set', 4)
entry_count = decode(unpack('<I', dat:read(4))) / 4
dat:seek('set', 4 * id + 4)
if id == entry_count - 1 then
offset = decode(unpack('<I', dat:read(4)))
next_offset = dat:seek('end') + 1
else
offset, next_offset = unpack('<II', dat:read(8))
offset, next_offset = decode(offset), decode(next_offset)
end

dat:seek('set', offset + 4)
return dat:read(next_offset - offset)
end
end

-- Creates a serialized representation of a string which can
-- be copied and pasted into the contents of an addon.
function dialog.serialize(entry)
return 'string.char('
.. sub(gsub(entry, '.', function(c)
return tostring(string.byte(c)) .. ','
end), 1, -2)
..')'
end

function dialog.encode_string(s)
return gsub(s, '.', function(c)
return char(xor(byte(c), 0x80))
end)
end

dialog.decode_string = dialog.encode_string

dialog.dev = {}

-- Returns the hex offset of the dialog entry with the given ID.
-- May be useful if you are viewing the file in a hex editor.
function dialog.dev.get_offset(dat, id)
local offset
if type(dat) == 'string' then
offset = unpack('<I', dat, 5 + 4 * id)
elseif type(dat) == 'userdata' then
dat:seek('set', 4 * id + 4)
offset = unpack('<I', dat:read(4))
end
return format('0x%08X', decode(offset))
end

-- This function is intended to be used only during development
-- to find the ID of a dialog entry given a substring.
-- This is necessary because SE uses certain bytes to indicate
-- things like placeholders or pauses and it is unlikely you
-- will know the entire content of the entry you're looking for
-- from the get-go.
-- Returns an array-like table which contains the ID of every entry
-- containing a given substring.
function dialog.dev.find_substring(dat, unencoded_string)
local last_offset = decode(unpack('<I', dat, 5)) + 5
local res = {}
-- local pos = find(dat, unencoded_string), last_offset, true)
local n = 0
for i in plain_text_gmatch(dat, dialog.encode_string(unencoded_string), last_offset) do
n = n + 1
res[n] = i
end
if n == 0 then print('No results for ', unencoded_string) return end
local entry_count = (last_offset - 5) / 4
for i = 1, n do
res[i] = binary_search(res[i] - 1, dat, entry_count)
end

return res
end

return dialog

30 changes: 15 additions & 15 deletions addons/libs/packets/fields.lua
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ types.lockstyleset = L{
}

-- lockstyleset
fields.outgoing[0x53] = L{
fields.outgoing[0x053] = L{
-- First 4 bytes are a header for the set
{ctype='unsigned char', label='Count'}, -- 04
{ctype='unsigned char', label='Type'}, -- 05 0 = "Stop locking style", 1 = "Continue locking style", 3 = "Lock style in this way". Might be flags?
Expand Down Expand Up @@ -1542,21 +1542,21 @@ fields.incoming[0x01C] = L{
{ctype='unsigned char', label='Wardrobe 2 Size'}, -- 0E
{ctype='unsigned char', label='Wardrobe 3 Size'}, -- 0F
{ctype='unsigned char', label='Wardrobe 4 Size'}, -- 10
{ctype='data[3]', label='_padding1', const=''}, -- 11
{ctype='unsigned short', label='_dupeInventory Size'}, -- 14 These "dupe" sizes are set to 0 if the inventory disabled.
{ctype='unsigned short', label='_dupeSafe Size'}, -- 16
{ctype='unsigned short', label='_dupeStorage Size'}, -- 18 The accumulated storage from all items (uncapped) -1
{ctype='unsigned short', label='_dupeTemporary Size'}, -- 1A
{ctype='unsigned short', label='_dupeLocker Size'}, -- 1C
{ctype='data[19]', label='_padding1', const=''}, -- 11
{ctype='unsigned short', label='_dupeInventory Size'}, -- 24 These "dupe" sizes are set to 0 if the inventory disabled.
{ctype='unsigned short', label='_dupeSafe Size'}, -- 26
{ctype='unsigned short', label='_dupeStorage Size'}, -- 28 The accumulated storage from all items (uncapped) -1
{ctype='unsigned short', label='_dupeTemporary Size'}, -- 2A
{ctype='unsigned short', label='_dupeLocker Size'}, -- 2C
{ctype='unsigned short', label='_dupeSatchel Size'}, -- 2E
{ctype='unsigned short', label='_dupeSack Size'}, -- 20
{ctype='unsigned short', label='_dupeCase Size'}, -- 22
{ctype='unsigned short', label='_dupeWardrobe Size'}, -- 24
{ctype='unsigned short', label='_dupeSafe 2 Size'}, -- 26
{ctype='unsigned short', label='_dupeWardrobe 2 Size'}, -- 28
{ctype='unsigned short', label='_dupeWardrobe 3 Size'}, -- 2A This is not set to 0 despite being disabled for whatever reason
{ctype='unsigned short', label='_dupeWardrobe 4 Size'}, -- 2C This is not set to 0 despite being disabled for whatever reason
{ctype='data[6]', label='_padding2', const=''}, -- 2E
{ctype='unsigned short', label='_dupeSack Size'}, -- 30
{ctype='unsigned short', label='_dupeCase Size'}, -- 32
{ctype='unsigned short', label='_dupeWardrobe Size'}, -- 34
{ctype='unsigned short', label='_dupeSafe 2 Size'}, -- 36
{ctype='unsigned short', label='_dupeWardrobe 2 Size'}, -- 38
{ctype='unsigned short', label='_dupeWardrobe 3 Size'}, -- 3A This is not set to 0 despite being disabled for whatever reason
{ctype='unsigned short', label='_dupeWardrobe 4 Size'}, -- 3C This is not set to 0 despite being disabled for whatever reason
{ctype='data[22]', label='_padding2', const=''}, -- 3E
}

-- Finish Inventory
Expand Down