В моде OpenComputers есть интересное устройство, которое позволяет определить плотность блока на расстоянии.
Но вот беда, данные он выдает довольно шумные и чем больше расстояние, тем больше шума.
Чтобы определить подлинную плотность блока, можно просканировать его несколько раз, а результат усреднить. Шум, мешающий сканированию, имеет вероятностную природу. И после нескольких сканирований можно статистически найти, какая вероятней всего плотность у блока.
За один тик мы можем просканировать 64 блока. Чтобы проанализировать всю доступную область (65 x 65 x 64) сотней итераций, нам понадобится 422500 тиков, что равно 21125 секунд или 352 минуты, то есть без малого 6 часов.
Но сколько раз надо сканировать? Сто? Тысячу?
Нам открыто тайное знание и есть точный ответ.
Один. Всего за одно сканирование мы можем найти руду среди любых других блоков. Если хочется абсолютной уверенности, придется сделать пару магических пассов и просканировать повторно.
Начнем с теории.
Для начала откроем код мода и найдем функцию geolyzer.scan, она располагается [здесь] src/main/scala/li/cil/oc/integration/vanilla/EventHandlerVanilla.scala и называется onGeolyzerScan()
Просмотрев код, мы можем понять, что функция принимает параметры, по этим параметрам сканирует блоки в мире. Делает разные проверки вроде world.blockExists(x, y, z) && !world.isAirBlock(x, y, z), чтобы убедится, что блок есть. Потом получает информацию о блоке по координатам, делает еще несколько проверок (опять проверить, что блок все-таки есть block != null, проверяет дополнительные параметры: includeReplaceable, isFluid(block), block.isReplaceable(world, blockPos.x, blockPos.y, blockPos.z))
Потом происходит измерение расстояния до блока. И в конце берется плотность, смешивается с шумом и расстоянием. Результат добавляется к таблице блоков и отправляется игроку.
Вроде-бы ничего необычного. Шум, расстояние, плотность. Нам и так известна зависимость силы шума от расстояния.
И вот тут начинается волшебство.
Рассмотрим поподробнее код вычисления итоговой плотности блока.
e.data(index) = e.data(index) * distance * Settings.get.geolyzerNoise + block.getBlockHardness(world, x, y, z)
Коротко можно это записать в виде формулы: R = G * D * N + H
G - это сгенерированный шум.
D - расстояние до блока.
N - множитель шума из конфига (стандартно - 2).
H - настоящая плотность.
R - результат работы геосканера.
Если мы попробуем в качестве эксперимента отнять от результата предполагаемую плотность, то ничего нового не узнаем. Если обратим все операции с известными значениями, то получим только шум.
А можем ли мы так же разобрать формулу шума? Давайте попробуем.
Несколькими строками выше [ссылка]. Можно наблюдать получение массива случайных байт.
val noise = new Array[Byte](e.data.length)
world.rand.nextBytes(noise)
Далее следует нормализация значений.
noise.map(_ / 128f / 33f).copyToArray(e.data)
Хм. Так-так-так. Если мы это все обьеденим с предыдущей формулой, то получится что-то вроде такого:
R = G(RANDOM_BYTE / 128 / 33) * D * N + H
И что это нам дает?
А то, что исходное псевдослучайное число имеет жесткую дискретность. ГПСЧ дает случайные числа типа byte, а это только 256 значений (-128, +127).
Нам известны все значения, кроме H и RANDOM_BYTE, что нам это дает?
Мы можем предположить значение H и обратить всю формулу.
(R - H) / D / N * 128 * 33
Для стандартного конфига можно сократить до:
2112 * (R - H) / D
А теперь тайное знание для тех, кто не понял самостоятельно.
Мы взяли желаемую плотность блока (например 3 для руды).
Подставили вместо H.
Получили случайное значение.
Можем легко определить, угадали ли плотность или нет.
Из-за дискретности случайных значений генератора, распределение вероятностей для блоков с разной плотностью не одинаковое.
Перейдем к практике.
Вот код простого скрипта, который в заданном радиусе ищет блоки с нужной плотностью. Результат выводится на голопроектор.
local sqrt = math.sqrt
local component = require('component')
local geolyzer = component.geolyzer
local hologram = component.hologram
local function distance(x, y, z)
return sqrt(x^2 + y^2 + z^2)
end
local function magic(R, H, D)
return 2112 * (R - H) / D % 1
end
local function visualize(hardness, elevation, size)
hologram.clear()
hologram.setScale(9)
local blocks, result
for x = -size, size do
for z = -size, size do
blocks = geolyzer.scan(x, z, elevation, 1, 1, 32)
for i_y = 1, 32 do
result = magic(blocks[i_y], hardness, distance(x, i_y+elevation-1, z))
if blocks[i_y] ~= 0 and (result > 0.9998 or result < 0.00005) then
hologram.set(x+24, i_y, z+24, true)
end
end
end
end
end
local hrd, ele, siz = table.unpack({...})
hrd = hrd or 3
ele = ele or -32
siz = siz or 16
visualize(hrd, ele, siz)
А вот результат:
При сканировании заметны артефакты. Когда разные плотности близки на целочисленных расстояниях, позникают коллизии.
Это можно частично компенсировать, если есть блок кандидат на ошибку.
На любом расстоянии можно рассчитать абсолютный минимальный и максимальный уровень шума. С расстоянием, у близких плотностей пересечение значений увеличивается, но если плотность блока не в области пересечений, то можно точно определить к какой области он относится.
Пересечение плотностей руды (3) и камня (1.5), точками обозначены три сканирования блока руды.
Результаты обратного вычисления для разных плотностей хорошо это демонстрируют.
Для компенсации артефактов надо ввести дополнительное условие: полученный RANDOM_BYTE должен быть в диапазоне -128:127.
Вот финальный скрипт и результат.
local sqrt = math.sqrt
local component = require('component')
local geolyzer = component.geolyzer
local hologram = component.hologram
local function distance(x, y, z)
return sqrt(x^2 + y^2 + z^2)
end
local function magic(R, H, D)
return 2112 * (R - H) / D
end
local function visualize(hardness, elevation, size)
hologram.clear()
hologram.setScale(9)
local blocks, result
for x = -size, size do
for z = -size, size do
blocks = geolyzer.scan(x, z, elevation, 1, 1, 32)
for i_y = 1, 32 do
result = magic(blocks[i_y], hardness, distance(x, i_y+elevation-1, z))
if blocks[i_y] ~= 0 and result > -128 and result < 127 and (result%1 > 0.9998 or result%1 < 0.0002) then
hologram.set(x+24, i_y, z+24, true)
end
end
end
end
end
local hrd, ele, siz = table.unpack({...})
hrd = hrd or 3
ele = ele or -32
siz = siz or 16
visualize(hrd, ele, siz)
Для более точного определения плотности можно сделать два сканирования. Одно сместить относительно другого так, чтобы расстояния с артефактами не совпадали.
Чтобы не выполнять тяжелую операцию sqrt, можно создать словарь, где [x^2 + y^2 + z^2] = sqrt(x^2 + y^2 + z^2), всего понадобится 1742 уникальных значений.
P.S. Пост является компиляцией знаний из [этой] темы. Собрал, чтобы перевести и опубликовать на официальном форуме.
Автор идеи хакнуть геосканер - @eu_tomat