summaryrefslogtreecommitdiff
path: root/Data/BuiltIn/Libraries/lua-addons/addons/libs/dialog.lua
diff options
context:
space:
mode:
authorchai <chaifix@163.com>2021-11-15 13:53:59 +0800
committerchai <chaifix@163.com>2021-11-15 13:53:59 +0800
commit942a030afd348ab2e02eac8054b43e3c3a72ea48 (patch)
treea13459f39a3d2f1b533fbd1b5ab523d7a621f673 /Data/BuiltIn/Libraries/lua-addons/addons/libs/dialog.lua
parente307051a56a54c27f10438fd2025edf61d0dfeed (diff)
*rename
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-addons/addons/libs/dialog.lua')
-rw-r--r--Data/BuiltIn/Libraries/lua-addons/addons/libs/dialog.lua227
1 files changed, 227 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-addons/addons/libs/dialog.lua b/Data/BuiltIn/Libraries/lua-addons/addons/libs/dialog.lua
new file mode 100644
index 0000000..5598175
--- /dev/null
+++ b/Data/BuiltIn/Libraries/lua-addons/addons/libs/dialog.lua
@@ -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
+