Добавлен nazarpunk,
опубликован
Раздел:
Триггеры и объекты
Продолжаем писать дешёвый сборщик и на этот раз научимся читать war3map.wct, который представляет собой двоичный (бинарный) файл, формат которого можно посмотреть в этой статье.
Подготовка карты
Для того, чтоб понять как всё работает, лучше использовать пустую карту с несколькими строчками кода и парой триггеров:
Чтение файла
Используя любой шестнадцатеричный редактор, например Hex Editor Neo, откроем war3map.wct.
Не мудрствуя лукаво, заглянем в документацию и возьмём оттуда код чтения файла, подправим под наши нужды и сохраним в /run/war3map.wct.lua
require 'lfs'
local path = lfs.currentdir() .. [[\map.w3x\war3map.wct]]
local data = assert(io.open(path, 'rb'))
local block = 16
while true do
local bytes = data:read(block)
if not bytes then break end
for b in string.gfind(bytes, '.') do
io.write(string.format('%02x ', string.byte(b)))
end
io.write(string.rep(' ', block - string.len(bytes) + 1))
io.write(string.gsub(bytes, '%c', '.'), '\n')
end
Как видно, с чтением и красивым выводом мы справились, но для дальнейшей работы нам это не подходит, да и пока непонятно что здесь вообще происходит. Сейчас мы это исправим.
Что-бы работать с данными файла, его сперва нужно прочитать:
local data = assert(io.open(path, 'rb'))
local content = data:read('*a')
data:close()
print(content)
Здесь нужно обратить внимание на две вещи: использование странной функции assert и содержимое content уж очень странно выводится. Для понимания работы assert, нужно попытаться открыть заведомо несуществующий файл:
Как видно, в первом случае, файл успешно открылся, а во втором мы сразу получили ошибку, что есть хорошо. Для ярых противников читать документацию, приведу её прям здесь:
assert (v [, message])
Вызывает функцию error, если значение своего аргумента v ложно (то есть, nil или false); в противном случае возвращает все свои аргументы. В случае ошибки, message является объектом ошибки; если этот аргумент отсутствует, то по умолчанию используется "assertion failed!" - "сбой проверочного утверждения!".
Разобраться с content нам помогут функции string.gsub и string.format:
for byte in string.gfind(content, '.') do
io.write(string.format('%02x', string.byte(byte)) .. ' ')
end
Сократим немного код, используя двоеточие, заодно запишем результат в таблицу, с которой и будем в дальнейшем работать:
local bytes = {}
for byte in content:gfind('.') do
table.insert(bytes, ('%02x'):format(byte:byte()))
end
for i = 1, #bytes do
io.write(bytes[i] .. ' ')
end
Чтение int
Как видно из статьи, в начале файла должно быть магическое число 0x80000004, а мы видим 04 00 00 80. Это потому что используется little-endian порядок байтов, тобишь байты идут в обратном порядке и читать их нужно таким образом 80 00 00 04. Покончив с теорией, извлечём из нашей таблицы bytes первые четыре элемента и записем их в строку:
local str = ''
while #str < 8 do
str = table.remove(bytes, 1) .. str
end
print(tonumber(str, 16))
Обратите внимание на особенность table.remove, которая возвращает удалённый элемент. Проверить правильность конвертации можно здесь
Наведём красоту, обернув чтение в функцию и заодно напишем обратную функцию int2str, которая сконвертирует число обратно в строку для последующей записи в файл:
local function readInt()
if #bytes < 4 then return nil end
local out = ''
while #out < 8 do
out = table.remove(bytes, 1) .. out
end
return tonumber(out, 16)
end
local function int2str(int)
return ('%8.8X'):format(int):gsub('(..)(..)(..)(..)', function(a, b, c, d)
return d .. c .. b .. a
end)
end
local int = readInt()
print(int, int2str(int)) --> 2147483652 04000080
Для дальнейшего удобства, напишем функцию чтения readWct и заодно прочитаем следущий int, который обозначает версию формата
local function readWct()
local wct, item = {}
for i = 1, 2 do
item = readInt()
if item == nil then return wct end
table.insert(wct, item)
end
return wct
end
local wct = readWct()
for i = 1, #wct do
local elem = wct[i]
print(type(elem), elem)
end
number 2147483652
number 1
Чтение string
Со строками всё немного попроще, нам нужно всеголишь читать байты, пока не встретим 00. По традиции напишем функцию чтения строки и обратную ей:
local function readStr()
if #bytes == 0 then return nil end
local out = ''
while #bytes > 0 do
local str = table.remove(bytes, 1)
if str == '00' then return out end
out = out .. string.char(tonumber(str, 16))
end
return out
end
local function str2str(str)
return str:gsub('.', function(c)
return ('%02x'):format(string.byte(c))
end)
end
Сверившись с таблицей видим, что если версия формата равна единице, то нам нужно последовательно прочитать string, int, string, что мы и сделаем в функции readWct
if item == 1 then
for i = 1, 3 do
item = i == 2 and readInt() or readStr()
if item == nil then return wct end
table.insert(wct, item)
end
end
Остальное прочитать не так уже и сложно: читаем int, если он больше нуля, читаем string, если нет, повторяем:
while #bytes > 0 do
item = readInt()
if item == nil then return wct end
table.insert(wct, item)
if item > 0 then
item = readStr()
if item == nil then return wct end
table.insert(wct, item)
end
end
И вот что получилось:
require 'lfs'
local path = lfs.currentdir() .. [[\map.w3x\war3map.wct]]
local data = assert(io.open(path, 'rb'))
local content = data:read('*a')
data:close()
local bytes = {}
for byte in content:gfind('.') do
table.insert(bytes, ('%02x'):format(byte:byte()))
end
local function readInt()
if #bytes < 4 then return nil end
local out = ''
while #out < 8 do
out = table.remove(bytes, 1) .. out
end
return tonumber(out, 16)
end
local function int2str(int)
return ('%8.8X'):format(int):gsub('(..)(..)(..)(..)', function(a, b, c, d)
return d .. c .. b .. a
end)
end
local function readStr()
if #bytes == 0 then return nil end
local out = ''
while #bytes > 0 do
local str = table.remove(bytes, 1)
if str == '00' then return out end
out = out .. string.char(tonumber(str, 16))
end
return out
end
local function str2str(str)
return str:gsub('.', function(c)
return ('%02x'):format(string.byte(c))
end)
end
local function readWct()
local wct, item = {}
for i = 1, 2 do
item = readInt()
if item == nil then return wct end
table.insert(wct, item)
end
if item == 1 then
for i = 1, 3 do
item = i == 2 and readInt() or readStr()
if item == nil then return wct end
table.insert(wct, item)
end
end
while #bytes > 0 do
item = readInt()
if item == nil then return wct end
table.insert(wct, item)
if item > 0 then
item = readStr()
if item == nil then return wct end
table.insert(wct, item)
end
end
return wct
end
local wct = readWct()
for i = 1, #wct do
local elem = wct[i]
print(type(elem), elem)
end
Заключение
В идеале можно было бы вынести код в отдельный файл и объявить глобальную функцию чтения/записи .wct и других файлов, но я пока не решил, в какую сторону развивать сборщик, да и статья писалась немного с другой целью. Так что на этом можно и закончить, по традиции оставив стандартную фразу о лайках и комментариях.
`
ОЖИДАНИЕ РЕКЛАМЫ...
Комментарии пока отсутcтвуют.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.