Добавлен , опубликован
Раздел:
Триггеры и объекты
Продолжаем писать дешёвый сборщик и на этот раз научимся читать 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 и других файлов, но я пока не решил, в какую сторону развивать сборщик, да и статья писалась немного с другой целью. Так что на этом можно и закончить, по традиции оставив стандартную фразу о лайках и комментариях.
`
ОЖИДАНИЕ РЕКЛАМЫ...