Luau decompiler for crossout space program
  • Python 99.4%
  • Batchfile 0.6%
Find a file
2026-07-29 21:23:45 +03:00
.gitignore Initial commit 2026-07-29 14:53:03 +00:00
build_luau.bat V1 2026-07-29 17:53:37 +03:00
cfg.py V2 2026-07-29 21:23:45 +03:00
check_syntax.py V1 2026-07-29 17:53:37 +03:00
decomp.py V2 2026-07-29 21:23:45 +03:00
disasm.py V1 2026-07-29 17:53:37 +03:00
ir.py V2 2026-07-29 21:23:45 +03:00
LICENSE Initial commit 2026-07-29 14:53:03 +00:00
luau_bc.py V1 2026-07-29 17:53:37 +03:00
README.md V2 2026-07-29 21:23:45 +03:00
roundtrip.py V2 2026-07-29 21:23:45 +03:00
unluau.py V1 2026-07-29 17:53:37 +03:00

unluau — декомпилятор Luau

Восстанавливает читаемый исходник Lua из скомпилированных Luau-скриптов космического сассаута Нужен только Python, внешних зависимостей нет.


Как декомпилировать

Всё дерево целиком:

python unluau.py -o out example

Один файл в консоль:

python unluau.py example/def/joints.lua

Ключи:

Ключ Что делает
-o DIR куда писать результат (структура папок сохраняется)
--no-banner не добавлять комментарий-шапку в начало файла
--width N ширина, после которой конструктор таблицы разбивается на строки (по умолчанию 110)
--disasm-on-error если файл не разобрался — положить дизассемблер вместо исходника
-q без итоговой сводки

Файлы, которые не являются байткодом (например scripts/lib/readme.txt), копируются как есть.

Проверить, что всё получилось:

python check_syntax.py out          # синтаксис всех файлов
python disasm.py example/def/joints.lua   # дизассемблер, если нужно свериться

Как скомпилировать обратно

Нужен компилятор Luau. Собирается из репозитория Luau (нужны Visual Studio с C++ и CMake):

cmd /c build_luau.bat

Получится luau-repo\build\Release\luau-compile.exe. Компиляция файла:

luau-repo\build\Release\luau-compile.exe --binary -O1 -g0 out\def\joints.lua > joints.lua

Как получить именно ту версию байткода, что в игре

Это важный момент. По умолчанию компилятор выдаёт байткод версии 12, а игра ждёт версию 6. Версию видно по первому байту: у оригиналов это 0x06.

Что проверено, а что нет: прогон по всем 422 файлам подтверждает, что команда ниже даёт заголовок v6 и не использует ничего из более новых версий. Загрузит ли игра такие файлы — проверить нельзя, её виртуалки тут нет.

Версия выбирается в BytecodeBuilder::getVersion() и зависит от двух вещей:

  1. Фича-флаги поднимают версию выше базовой:

    --fflags=LuauBytecodeCostModel=false,LuauEmitCallFeedback=false
    

    Без флагов — v12, с этими двумя — v9. Ниже флагами не опустить.

  2. Базовая версия — константа LBC_VERSION_TARGET в luau-repo/Common/include/Luau/Bytecode.h. Флагами ниже неё не опуститься, поэтому её надо поставить в 6 и пересобрать:

    LBC_VERSION_TARGET = 6,   // было 9
    

    Здесь это уже сделано, luau-compile.exe собран с этой правкой. Вернуть как было — поставить 9 и пересобрать.

Одного этого мало: компилятор пишет константы TABLE_WITH_CONSTANTS (появились в v7) без оглядки на версию, и v6-виртуалка их не поймёт. Эти константы появляются при оптимизации таблиц, поэтому компилировать надо с -O0.

Итоговая команда:

luau-compile.exe --binary -O0 -g0 ^
  --fflags=LuauBytecodeCostModel=false,LuauEmitCallFeedback=false ^
  out\def\joints.lua > joints.lua

Это проверено на всех 422 файлах: при -O0 в выходном байткоде встречаются только константы NUMBER, STRING, TABLE (все существуют в v6), а самый старший используемый опкод — FORGPREP (индекс 76), тогда как сама игра использует опкоды вплоть до индекса 80. То есть за пределы v6 компилятор не выходит.

Цена -O0 — код не оптимизируется, файлы получаются крупнее и работают чуть медленнее оригинала. Для мода это обычно не важно, но если нужен -O1, придётся дополнительно править BytecodeBuilder.cpp, чтобы он не использовал TABLE_WITH_CONSTANTS.

Альтернатива без правки исходников — собрать старый релиз Luau, у которого LBC_VERSION_TARGET изначально равен 6 (примерно линейка 0.5xx0.6xx).

Проверить результат:

python -c "print(open('joints.lua','rb').read()[0])"   # должно быть 6

Как это работает, вкратце

  1. Разбор байткода — формат вычитан из luau-repo/VM/src/lvmload.cpp: таблица строк, прототипы функций, константы, отладочная информация.

  2. Граф потока управления — код режется на базовые блоки по целям переходов.

  3. Анализ живых значений — обратный поток данных по блокам. Регистр становится именованной переменной, только если его значение живо на выходе из блока; всё остальное подставляется прямо в выражение. Именно это отличает читаемый код от простыни присваиваний вида v1 = x; v2 = v1.

  4. Символьное исполнение — по инструкциям строится дерево выражений: вызовы, индексация, арифметика, конструкторы таблиц, замыкания с захватом.

  5. Восстановление структурыif/elseif/else, while, repeat, оба вида for, break, continue, цепочки and/or, ранние return, рекурсивные local function.

  6. Расстановка local — для каждой переменной ищется самый глубокий блок, охватывающий все её использования. Это важно для замыканий в циклах: переменная должна объявляться внутри тела, иначе все итерации захватят одну ячейку.

Почему не сработали готовые открывашки

Публичные декомпиляторы Luau заточены под Roblox — это версии байткода 15, причём обычно со скриптами, где сохранена отладочная информация. Здесь версия 6 и -g0, поэтому такие инструменты либо падают, либо выдают R1 = R2[R3].

Здесь формат прочитан по исходникам самой VM, а имена переменных не «угадываются», а честно генерируются — с анализом живых значений, чтобы временные значения не превращались в лишние переменные.


Известные ограничения

Имена локальных переменных выдуманыv1, v2, arg1. В байткоде их нет, скрипты собраны с -g0. Имена глобалов, полей, методов и все константы — настоящие. Комментарии и разбиение на строки не восстановить.

Методы записаны через точкуfunction Foo.bar(self, x) вместо function Foo:bar(x). Для Lua это одно и то же, но так гарантированно не теряется параметр.

28 мест не удалось структурировать — помечены комментарием -- unstructured branch at pc N. Там сложный поток управления, код рядом присутствует, но условие перехода могло потеряться — стоит перепроверить руками. Из них 21 в scripts/lib/mobdebug/mobdebug.lua, 2 в scripts/lib/std/io.lua, остальные 5 — по одному на файл.

Порядок ключей в таблицах может отличаться от исходного — в Lua это ни на что не влияет.


Файлы

Файл Назначение
luau_bc.py чтение байткода: строки, прототипы, константы, отладочная информация
cfg.py базовые блоки, чтение/запись регистров, анализ живых значений
ir.py дерево выражений и операторов, генератор исходника Lua
decomp.py символьное исполнение и восстановление структуры управления
disasm.py дизассемблер (отладка и запасной вариант)
unluau.py точка входа CLI
check_syntax.py проверка синтаксиса результата
roundtrip.py обратная компиляция и сверка с оригиналом
build_luau.bat сборка luau-compile.exe из luau-repo

build_luau.bat конфигурирует проект с включёнными тестами, но собирает только компилятор — в luau-repo/CMakeLists.txt цель Luau.UnitTest упоминается в блоке LUAU_BUILD_CLI, хотя создаётся только при LUAU_BUILD_TESTS, и без этого конфигурация падает.