Пост про второй и третий дни остановился 13 октября в 19:46. Этот пост — о том, что было дальше: немного того же вечера и всё 14 октября, до 20:50. За это время пять спецификаций прошли ревью, запуски воспроизвели всех монстров, которых игра поставила в пройденные комнаты, включая особых, работа над предметами дошла до уникальных и сетовых, а уровни на открытой местности теперь можно заново построить по их сидам.

Прошлые два поста были текстом и таблицами с несколькими диаграммами Mermaid, и по одному тексту было трудно понять, что происходит. Поэтому в этот раз есть графики: сетка того, что сделано и что нет, что воспроизвёл каждый запуск, как со временем росло число подтверждённых утверждений, и карта. Сетка теперь есть и в постах про первый день и про второй и третий — в том состоянии, в каком проект был к их отсечке.

Карта работы

В проекте есть карта исследования: 28 строк, по одной на каждую подсистему игры, которую нужно описать. Каждая строка проходит одни и те же стадии. Сначала план, потом находки (что мы прочитали в коде, с адресами и тегами уверенности), потом черновик спецификации и, наконец, спецификация, прошедшая ревью агента, который её не писал. Вот эта карта к отсечке каждого поста. Переключайте дни и смотрите, что сдвинулось.

Карта исследования к каждой отсечке

  • не начато
  • в плане
  • находки
  • черновик спецификации
  • спецификация после ревью

3 спецификация после ревью 1 находки 1 в плане 23 не начато из 28

Ядро

  • rng спецификация после ревью
  • items/tc находки
  • items/quality не начато
  • items/affixes не начато
  • items/uniques-sets не начато
  • items/properties не начато
  • monsters/pool не начато
  • monsters/spawn не начато
  • monsters/special не начато
  • drlg/overview в плане
  • drlg/preset не начато
  • drlg/maze не начато
  • drlg/outdoor не начато
  • drlg/warps не начато
  • objects не начато

Опоры

  • data/excel спецификация после ревью
  • data/calc не начато
  • data/strings не начато
  • rng/seeds спецификация после ревью
  • drlg/collision не начато
  • save/d2s не начато

Пути создания

  • items/vendors не начато
  • items/cube не начато
  • items/sockets-runewords не начато
  • items/quest-drops не начато
  • monsters/stats не начато
  • hirelings не начато
  • player/creation не начато

7 спецификация после ревью 1 черновик спецификации 3 находки 17 не начато из 28

Ядро

  • rng спецификация после ревью
  • items/tc спецификация после ревью ↑
  • items/quality находки ↑
  • items/affixes не начато
  • items/uniques-sets не начато
  • items/properties не начато
  • monsters/pool спецификация после ревью ↑
  • monsters/spawn находки ↑
  • monsters/special не начато
  • drlg/overview спецификация после ревью ↑
  • drlg/preset спецификация после ревью ↑
  • drlg/maze не начато
  • drlg/outdoor не начато
  • drlg/warps не начато
  • objects не начато

Опоры

  • data/excel спецификация после ревью
  • data/calc не начато
  • data/strings находки ↑
  • rng/seeds спецификация после ревью
  • drlg/collision не начато
  • save/d2s черновик спецификации ↑

Пути создания

  • items/vendors не начато
  • items/cube не начато
  • items/sockets-runewords не начато
  • items/quest-drops не начато
  • monsters/stats не начато
  • hirelings не начато
  • player/creation не начато

12 спецификация после ревью 1 черновик спецификации 6 находки 2 в плане 7 не начато из 28

Ядро

  • rng спецификация после ревью
  • items/tc спецификация после ревью
  • items/quality спецификация после ревью ↑
  • items/affixes находки ↑
  • items/uniques-sets спецификация после ревью ↑
  • items/properties находки ↑
  • monsters/pool спецификация после ревью
  • monsters/spawn спецификация после ревью ↑
  • monsters/special спецификация после ревью ↑
  • drlg/overview спецификация после ревью
  • drlg/preset спецификация после ревью
  • drlg/maze находки ↑
  • drlg/outdoor находки ↑
  • drlg/warps находки ↑
  • objects в плане ↑

Опоры

  • data/excel спецификация после ревью
  • data/calc не начато
  • data/strings находки
  • rng/seeds спецификация после ревью
  • drlg/collision спецификация после ревью ↑
  • save/d2s черновик спецификации

Пути создания

  • items/vendors не начато
  • items/cube не начато
  • items/sockets-runewords не начато
  • items/quest-drops не начато
  • monsters/stats в плане ↑
  • hirelings не начато
  • player/creation не начато

Стрелка отмечает строку, которая сдвинулась с прошлого снимка. Источник: docs/specs/README.md на коммитах 8faba4f2, c8753c82 и 2c8eb42.

Показать таблицей
Подсистема День 1День 3День 4
rng спецификация после ревьюспецификация после ревьюспецификация после ревью
items/tc находкиспецификация после ревьюспецификация после ревью
items/quality не начатонаходкиспецификация после ревью
items/affixes не начатоне начатонаходки
items/uniques-sets не начатоне начатоспецификация после ревью
items/properties не начатоне начатонаходки
monsters/pool не начатоспецификация после ревьюспецификация после ревью
monsters/spawn не начатонаходкиспецификация после ревью
monsters/special не начатоне начатоспецификация после ревью
drlg/overview в планеспецификация после ревьюспецификация после ревью
drlg/preset не начатоспецификация после ревьюспецификация после ревью
drlg/maze не начатоне начатонаходки
drlg/outdoor не начатоне начатонаходки
drlg/warps не начатоне начатонаходки
objects не начатоне начатов плане
data/excel спецификация после ревьюспецификация после ревьюспецификация после ревью
data/calc не начатоне начатоне начато
data/strings не начатонаходкинаходки
rng/seeds спецификация после ревьюспецификация после ревьюспецификация после ревью
drlg/collision не начатоне начатоспецификация после ревью
save/d2s не начаточерновик спецификациичерновик спецификации
items/vendors не начатоне начатоне начато
items/cube не начатоне начатоне начато
items/sockets-runewords не начатоне начатоне начато
items/quest-drops не начатоне начатоне начато
monsters/stats не начатоне начатов плане
hirelings не начатоне начатоне начато
player/creation не начатоне начатоне начато

Ядро — это исходная цель: создание предметов, спавн монстров и генерация уровней. В первый день хоть что-то было только в трёх его строках. Теперь каждая строка ядра хотя бы запланирована, а девять из пятнадцати — спецификации после ревью. Опоры — системы, без которых ядро не воспроизвести, например сетка коллизий, от которой зависит, где может стоять монстр. Пути создания — другие способы, которыми игра создаёт предметы и монстров: торговцы, Horadric Cube, рунные слова, наёмники. Эту полосу почти не трогали, и так и должно быть: она ждёт ядро.

С третьего дня сдвинулись двенадцать строк. Пять стали спецификациями после ревью: items/quality, items/uniques-sets, monsters/spawn, monsters/special и drlg/collision. Ещё у пяти вечером появились первые находки: аффиксы, свойства предметов, генератор лабиринтов, переходы между уровнями и генератор открытой местности. А ещё у двух, объектов и характеристик монстров, появились одобренные планы.

Всё, что сдвинулось, проверено на самой игре. Вот запуски и прогоны этого дня. В каждом модель, написанная по спецификации, сравнивается с тем, что настоящая игра сделала с тем же сидом:

ЗапускЧто воспроизвела модельРасхождений
Спавн в комнатах516 комнат со случайными монстрами, 110 302 проверки0
Сетка коллизий1 439 сеток комнат, 2 343 400 клеток, пять проверок позиции0
Особые монстры20 групп чемпионов, 54 группы уникальных, 11 суперуникальных, 390 144 проверки0
Уникальные и сетовые304 создания предмета, от treasure class до сборщика0
То же, с подменой таблицы1 519 созданий, в них 115 сборок уникальных и 72 сетовых0
Открытая местность (прогон)370 построений уровней из логов, по всем пяти актам0
Лабиринты (прогон)12 построений лабиринтов из логов0

Проверка здесь — это одно значение, которое модель предсказала, а игра записала в лог. Взятое число — одно число из сида, а «воспроизведено» значит, что модель на тех же входных данных взяла те же числа и получила тот же результат.

Вечер третьего дня

Пост про третий день закончился тем, что находки о спавне монстров в комнатах ждали своих запусков: «это будет первое воспроизведение спавна монстра по сиду». Запуск прошёл в 21:15 того же вечера.

Играл я. Три сессии в видимом окне у меня на рабочем столе, с подключённым логгером. Первая — персонажем из прошлых запусков пула монстров, через вейпоинт на Stony Field. Для второй и третьей я попросил волшебницу с Nova, и сборщик сохранений сделал NovaSorc: 80-й уровень, все вейпоинты открыты на всех сложностях, Nova на правой кнопке мыши, пояс полных зелий восстановления. Дальше были Far Oasis, Spider Forest, City of the Damned и Frigid Highlands на Normal, а Stony Field, Dark Wood и Jail — на Hell.

Итог: 516 комнат, 110 302 проверки, 0 расхождений. Сид каждой комнаты предсказан по числу из -seed вплоть до прохода спавна. В 176 комнатах он предсказан на всём пути, а в остальных один промежуточный шаг — пресеты и объекты комнаты — всё ещё брался из лога. Модель создала 916 монстров, 257 обычных групп, 4 группы чемпионов и 20 групп уникальных, и каждое взятое число совпало.

Здесь же решились два вопроса из поста про третий день. В третий день бросок на особого монстра прочитали как 6 из 100, а не 5: он сравнивает с 5 через «меньше или равно». Запуск поймал саму границу: из 252 бросков один выпал ровно на 5 и дал особого монстра. А шанс чемпиона, Range(100) < 20, действительно берёт свои 20 из первой строки MonUMod: 24 броска, и чемпион выходил ровно тогда, когда выпадало меньше 20.

Два монстра в схему не уложились. Обычно после создания монстра его сид юнита уходит от начального на два шага. У босса Goatman он ушёл на шесть шагов, а у миньона Fallen — на пятнадцать. Какая функция делает лишние шаги, мы пока не прочитали. В записи эксперимента они указаны по строкам лога и переданы дальше, в monsters/stats.

14-го все четыре роли снова работали параллельно: архитектор, архивист, реверсер и автор спецификаций, каждая — агент со своей копией репозитория. К 09:12 правки по ревью броска качества были внесены, и спецификацию items/quality закрыли как прошедшую ревью. Код весь день читали параллельно, а запуски — нет: до 20:38 был один харнесс, одна игра под отладчиком.

Где может стоять монстр

Первой большой работой дня была сетка коллизий. У каждой комнаты есть сетка клеток, и в каждой клетке лежит 16-битное значение. Биты говорят, что там: стена, вода, тело монстра, снаряд, труп. Когда проход спавна хочет куда-то поставить монстра, он спрашивает сетку. «Может ли здесь стоять монстр размера 3?» — это проверка маски на нескольких клетках.

flowchart LR
dt1["тайлы DT1 комнаты и её активных соседей: по 25 байт субтайлов"] --> grid["сетка коллизий комнаты, по 16 бит на клетку"]
units["монстры, объекты, снаряды, трупы"] -->|"ставят и снимают метки"| grid
grid --> test{"CollisionTest: маска на квадрате 2x2 или 3x3"}
test -->|свободно| place["монстр поставлен"]
test -->|занято| next["следующая позиция"]

Этот запуск я решил сделать полностью скриптовым: агент вёл игру по скриншотам, сам, без человека, — семь сессий на сиде карты 2026 на Normal. На четвёртой сессии я сказал ему, что Fire Wall убивает плохо и в неё нужно целиться, а Nova целиться не нужно. Поэтому с пятой по седьмую сессии играли персонажем с Nova, а прыжки телепорта сделали короче, чтобы реже попадать туда, где персонаж стоять не может.

1 439 сеток комнат сравнены поклеточно, 2 343 400 клеток, 0 различий. Вдобавок пять проверок позиции в проходе спавна предсказаны по сетке в том виде, в каком она была в начале прохода: 20 537 проверок, 0 расхождений.

Сколько сеток комнат сравнили поклеточно, по типам уровней

Джунгли (акт III) 453
Дикие земли (акт I) 286
Пустыня (акт II) 266
Плато (акт IV) 191
Город акта IV 147
Город акта III 57
Jail 22
Подземелье 10
Лава, пещера, Barracks, Tristram 7

Типы уровней — как в записи запуска, run:2026-10-14-collision.

Показать числа
комнат
Джунгли (акт III) 453
Дикие земли (акт I) 286
Пустыня (акт II) 266
Плато (акт IV) 191
Город акта IV 147
Город акта III 57
Jail 22
Подземелье 10
Лава, пещера, Barracks, Tristram 7

В находках было две ошибки, и обе нашлись при сверке со справочником Blizzard по файлам данных. Сначала мы прочитали, что тело мёртвого монстра в сетке всегда помечается заново. В справочнике сказано, что при deadCol, равном 1, «труп монстра становится препятствием для других юнитов». Справочник оказался прав: код проверяет этот бит и оставляет такие трупы помеченными. В запуске это потом встретилось 12 раз — на ловушках-горгульях и пойманных душах. Ещё мы прочитали, что снаряды ничего не помечают, а тот же справочник говорит, что некоторые помечают. Так и есть, если у них задан столбец Collision: Fire Wall за запуск поставила бит 0x40 5 702 раза. Обе поправки отмечены в находке как «finding wrong; corrected».

Один сюрприз дал сам запуск. Wraith создаётся со своим шаблоном коллизии. Но сразу после этого функция, которая назначает ему сторону, снимает метку и ставит её заново по шаблону по умолчанию: функция, выбирающая шаблон по размеру, отдельного случая для wraith не знает. В запуске было четыре wraith, в Jail Level 1, и все четыре в итоге оказались помечены крестом. Снять метку и поставить заново игра делает при каждом создании, во всех 410 созданиях запуска, но что-то меняется только для wraith.

Особые монстры и 256 имён

После сетки пришла очередь запуска для особых монстров. Особый монстр — это группа чемпионов или уникальный босс с миньонами. То, что монстр будет особым, решает проход спавна. А какой он будет и с какими модификаторами, решает отдельный путь.

flowchart TB
roll{"особый монстр: сид юнита, Range(100) меньше 20?"}
roll -->|да| champ["чемпион: один модификатор чемпиона, взвешенный выбор"]
roll -->|нет| uniq["уникальный: Range(1), потом модификаторов: 1 + сложность; миньоны копируют передаваемые"]
champ --> apply["работают обработчики; число берёт только rndname: один шаг, младшие 16 бит — сид имени"]
uniq --> apply
apply --> name["клиент оставляет младший байт и бросает суффикс, префикс, в половине случаев прозвище"]

Здесь у сетки коллизий нашлось второе применение: этот запуск агент вёл новым пилотом, d2pilot.py, который ходит по сетке. Телепортом пилот прыгает только на клетку, вокруг которой в квадрате 3×3 нет ни одного бита, мешающего игроку. Путь он прокладывает по таким же клеткам, к ближайшей комнате, которая ещё не активирована. В руководстве по игре на харнессе это сказано просто: «никогда не прыгать в стену, в воду, на край обрыва или в пустоту…» И ещё: «пилот только решает, куда кликнуть, и ничего из его вычислений не попадает в модель».

Этот запуск тоже был скриптовым, тоже по моему выбору: двенадцать сессий в классике и в дополнении, на всех трёх сложностях. Телепорт, Nova после каждого второго прыжка, Energy Shield примерно раз в две минуты начиная с четвёртой сессии, зелье, когда жизни меньше 70%. Две сессии на Hell закончились смертью персонажа, обе до того, как в сборку добавили Energy Shield.

Особые монстры в двенадцати сессиях, по типу игры

  • группы чемпионов
  • группы уникальных
  • суперуникальные
Дополнение, Hell 52
Дополнение, Nightmare 12
Дополнение, Normal 6
Классика, Hell 4
Классика, Nightmare 6
Классика, Normal 5

run:2026-10-14-mon-special. Все 13 модификаторов уникальных встретились и на Nightmare, и на Hell.

Показать числа
группы чемпионовгруппы уникальныхсуперуникальные всего
Дополнение, Hell 11383 52
Дополнение, Nightmare 543 12
Дополнение, Normal 132 6
Классика, Hell 211 4
Классика, Nightmare 060 6
Классика, Normal 122 5

390 144 проверки, 0 расхождений. Совпали все 169 выборов модификаторов: список кандидатов, суммарный вес и взятое число. 606 вызовов обработчиков модификаторов на боссах и членах групп не изменили ни одного сида, кроме одного шага rndname. Одиннадцать суперуникальных, среди них Rakanishu, Griswold, Bishibosh и Beetleburst, построены так, как предсказала модель, вместе с числом миньонов. У Rakanishu было 8 миньонов на Normal, 9 на Nightmare и 10 на Hell — в пределах от MinGrp + сложность до MaxGrp + сложность.

Классы монстров за четырьмя суперуникальными из запуска

  • Fallen: маленький красный демон с топором и щитом Fallen, класс Rakanishu
  • Griswold, огромный кузнец-мертвец Griswold
  • Скарабей: большое тёмное насекомое с длинными когтями Скарабей, класс Beetleburst
  • Fallen Shaman с изогнутым посохом Fallen Shaman, класс Bishibosh

Собрано из слоёв спрайтов самой игры инструментом проекта tools/cof.py: первый кадр спокойной позы, без смены цвета, которую суперуникальный получает в игре. Графика Diablo II © Blizzard Entertainment.

Здесь сообщество было право наполовину. На Arreat Summit написано, что уникальные монстры получают одну дополнительную способность на Nightmare и две на Hell, и это так: уникальный получает 1 + сложность модификаторов. На одной странице вики на fandom то же самое сказано про суперуникальных, вместе с «3–6 миньонами». Для суперуникальных это неверно. Они получают фиксированные модификаторы из своей записи плюс сложность дополнительных выборов, а в запуске у них было от 3 до 10 миньонов. В находке предполагается, что эти числа относились к случайным уникальным: «числа подходят к случайным уникальным … скорее всего, имелись в виду они».

Ещё один сюрприз был в именах. Имя случайного уникального монстра — это префикс и суффикс из двух строковых таблиц, иногда с прозвищем после них, и собирается оно на клиенте по сиду имени. Сервер передаёт этот сид в 16 битах. Сначала в коде прочитали только серверную часть, поэтому в находке было написано, что имя выбирается по 16 битам. Первый же бросок имени на клиенте в отладочной сессии с этим не совпал, зато совпал с младшим байтом. Клиент читает все 16 бит, а потом оставляет только младший байт. Теперь в находке написано: «возможных имён у случайных уникальных всего 256». Все 94 броска имени на клиенте в этом запуске шли по младшему байту.

Уникальные и сетовые: цель в 450 сессиях

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

flowchart TD
q["бросок качества: уникальный (7) или сетовый (5)"] --> seed["сид предмета ставится в фиксированное начало, {S, 666}"]
seed --> u["сборщик уникальных: отбор строк, одно взвешенное число"]
seed --> st["сборщик сетовых: отбор строк, одно взвешенное число"]
u -->|"нет кандидата или уже создан в этой игре"| du["прочность x3"]
st -->|нет кандидата| ds["прочность x2"]
du --> r["сид снова {S, 666}, собрать редкий"]
ds --> m["сид снова {S, 666}, собрать магический"]
r -->|неудача| m
m -->|неудача| sup["превосходный"]
sup -->|неудача| n["обычный"]

По плану в запусках нужно было набрать хотя бы 30 сборок уникальных и 30 сетовых, чтобы проявились все фильтры и откаты. Агент собрал UniqNova — волшебницу 80-го уровня с magic find от стандартных уникальных вещей и чармов — и фармил Eldritch the Rectifier. Это суперуникальный монстр, который в каждой игре дополнения стоит рядом с вейпоинтом Frigid Highlands. Вейпоинт, пара телепортов, Nova, собрать дроп, новая игра, следующий сид.

Тридцать сессий на Hell дали 304 предмета. Один из них был уникальным и три сетовыми, и все три сетовых не собрались и откатились в магические. С такой скоростью до цели понадобилось бы около 450 сессий. Расчёт есть в ADR-0010, и там же сказано, что дело не в magic find: из-за убывающей отдачи MF 458, который был у персонажа почти весь обычный запуск, даёт к шансу уникального примерно 161%, а при 608 было бы всего около 177%.

Magic find и его прибавка к шансу

Те же кривые, что в посте про второй и третий дни. Точки — MF 458 и 608.

  • уникальные, C = 250
  • сетовые, C = 500
  • редкие, C = 600
0 0 100 100 200 200 300 300 400 400 500 500 600 600 700 700 magic find персонажа без убывающей отдачи прибавка к шансу, % уникальные 184 MF 458: +161% MF 608: +177% сетовые 291 MF 458: +239% MF 608: +274% редкие 323 MF 458: +259% MF 608: +301%
Показать числа
MF уникальныесетовыередкие
10 +10%+10%+10%
50 +41%+45%+46%
100 +71%+83%+85%
200 +111%+142%+150%
300 +136%+187%+200%
458 +161%+239%+259%
608 +177%+274%+301%

Поэтому архитектор написал новое правило, ADR-0010, а я его одобрил: запуск с подменённой таблицей. Бросок качества читает одну таблицу, ItemRatio, чтобы решить, станет ли предмет уникальным. Сборщики её не читают никогда. Они читают UniqueItems, SetItems, базовые предметы и сид предмета. Поэтому в таком запуске подменяется только ItemRatio, одним отдельным файлом в копии каталога игры, а все остальные таблицы остаются стандартными. В основном варианте базовые шансы обнулены, а столбцы минимума понижены, так что шанс стоит на этом новом минимуме, и magic find уже ни на что не влияет. Во втором варианте любой предмет, дошедший до проверки на уникальность, её проходит. В записи перечислена каждая изменённая ячейка. Если прогнать те же логи со стандартной ItemRatio, 177 бросков не сходятся, так что игра действительно загрузила подменённую таблицу.

ADR строго ограничивает, что такой запуск может доказать. Он подтверждает сам путь: отбор, взятые числа, откаты. Но ничего не говорит о том, как часто этот путь срабатывает: «частоты, соотношения и утверждения „как часто“ никогда не могут опираться на такой запуск». И такой запуск никогда не идёт в одиночку: запуск на стандартных таблицах тоже должен воспроизвестись.

Сборки уникальных и сетовых: обычный запуск и запуск с подменой

  • стандартные таблицы, 30 сессий
  • подменённая ItemRatio, 54 сессии
Сборки уникальных 1 115
Сборки сетовых 3 72

run:2026-10-14-uniques и run:2026-10-14-uniques-forced.

Показать числа
стандартные таблицы, 30 сессийподменённая ItemRatio, 54 сессии
Сборки уникальных 1115
Сборки сетовых 372

Чем закончились сборки в запуске с подменой

  • выбрана строка уникального
  • уникальный не собрался, стал редким
  • выбрана строка сетового
  • сетовый не собрался, стал магическим
Дополнение, Hell 51
Дополнение, Nightmare 62
Дополнение, Normal 23
Классика, Nightmare 51
Показать числа
выбрана строка уникальногоуникальный не собрался, стал редкимвыбрана строка сетовогосетовый не собрался, стал магическим всего
Дополнение, Hell 1771215 51
Дополнение, Nightmare 2812913 62
Дополнение, Normal 5936 23
Классика, Nightmare 2611410 51

1 519 созданий воспроизведены, 0 неудач: 115 сборок уникальных и 72 сетовых на трёх сложностях и в классике. Что из этого вышло:

  • Один раз за игру. Уникальный предмет, собранный в игре, ставит бит в наборе из 4 096 бит, и если позже выбор снова падает на ту же строку, сборка не удаётся, без повторного выбора. В одной классической игре на Nightmare выбор пришёлся на уникальный нагрудник, уже собранный в этой игре. Сборка не удалась, прочность утроилась, 34 из 50 стали 102 из 150, и предмет вышел редким. Так выходит любой несобранный уникальный: он становится редким, а если у базы есть прочность, она утраивается.
  • Ладдерные уникальные в одиночной игре. Djinnslayer и Leviathan, строки только для ладдера, выпали в одиночной игре на Hell. Флаг ладдера не учитывается, если один байт в структуре игры не равен нулю, а в одиночной игре там 3.
  • Сброс сида. Каждый шаг отката возвращает сид предмета к тому же фиксированному началу, {S, 666}, с которого начинал неудавшийся сборщик, так что числа, взятые этим сборщиком, просто пропадают.
  • У сетовых нет проверки «один раз за игру». Нет ни проверки enabled, ни проверки ладдера.

Два ладдерных уникальных из запуска с подменой и база случая с повторной сборкой

  • Djinnslayer: изогнутый меч в инвентаре, кадры падения на землю и персонаж с ним в руке Djinnslayer
  • Leviathan: зелёный доспех в инвентаре, кадры падения на землю и персонаж в нём Leviathan
  • Нагрудник в инвентаре, кадры падения на землю и персонаж в нём Breast Plate — база уникального, собранного дважды

На каждом листе: картинка в инвентаре, кадры падения предмета на землю и предмет на персонаже. Отрисовано инструментом проекта tools/item_art.py. Графика Diablo II © Blizzard Entertainment.

Фарм после обеда

Почти всё послеобеденное время ушло на этот фарм, и пока он шёл, руководство по игре на харнессе пополнялось правилом за правилом. Я смотрел, как играет агент, и руководство само говорит, откуда эти правила: «они появились, пока владелец смотрел запуски». Вот некоторые:

  • «Убивать через Nova». Nova не нужно наводить: встань рядом с группой и колдуй. «Никаких Fire Wall, Blizzard и прочего, что ставится на клетку: скрипт промахивается, и сессия пропадает».
  • «Пропускать то, что Nova не убьёт». Перед боем посчитать сопротивление цели молнии на этой сложности, вместе с тем, что добавляют её модификаторы, и телепортироваться мимо группы с иммунитетом.
  • «Выбирать навыки горячими клавишами (F-клавишами), а не переключением комплекта оружия». Сначала Energy Shield кастовали через смену комплекта оружия. Я увидел, как переключение застряло и вместо Nova стоял Energy Shield, и теперь правило говорит: «если игра пропустит переключение, персонаж останется не на том комплекте, и каждая „Nova“ после этого превращается в Energy Shield».
  • «Не фармить Мефисто: путь через Durance of Hate для скриптовой игры оказался слишком длинным и хрупким». Три сессии в Durance of Hate остановил я, и в запись эксперимента они не вошли.
  • «Не оставлять запущенную сессию без дела». «Чаще всего „зависание“ — вовсе не зависание: игра в порядке, просто её никто не ведёт, пока агент между прыжками правит код».

И одно правило про меня: «десять минут игры владельца однажды дали больше суперуникальных, улучшений и сундуков, чем час скриптовой охоты; если покрытие застряло, попросите его сыграть, а не гриндите».

Аффиксы, свойства и «от 2 до 6» с Arreat Summit

Вечером реверсер дочитал создание предмета до конца: аффиксы и свойства. Это пока только находки, по чтению кода, всё [I]: выведено, но ещё не подтверждено запуском. Запуски для них будут следующими. Но кое-что из прочитанного расходится с тем, что пишет сообщество, и рассудят это запуски.

Уровень аффиксов, alvl, определяет, какие префиксы и суффиксы может получить предмет:

i = max(ilvl, qlvl);
if (magic_lvl) alvl = i + magic_lvl;
else if (i < 99 - qlvl / 2) alvl = i - qlvl / 2;
else alvl = 2 * i - 99;
alvl = clamp(alvl, 1, 99);

На Arreat Summit написано, что игра случайно выбирает из всех аффиксов с qlvl <= ilvl + 2, одинаково для магических и редких предметов. В 1.14d такое правило есть только в коде для версии 0, до которого не доходит ни один создаваемый предмет. Рабочий код сравнивает с alvl и выбирает с весом из столбца frequency.

Теперь число аффиксов у редкого предмета. По Arreat Summit у редких предметов от 2 до 6 магических свойств. Код делает один шаг сида предмета и по трём младшим битам берёт значение из таблицы на восемь записей:

Число слотов аффиксов у редкого предмета по таблице в коде

{3, 4, 4, 5, 5, 5, 6, 6}[low & 7]; у джевелов вместо этого 3 или 4.

2 аффикса 0 из 8
3 аффикса 1 из 8
4 аффикса 2 из 8
5 аффиксов 3 из 8
6 аффиксов 2 из 8

items/0005, по чтению кода, [I].

Показать числа
шанс
2 аффикса 0 из 8
3 аффикса 1 из 8
4 аффикса 2 из 8
5 аффиксов 3 из 8
6 аффиксов 2 из 8

Выходит, слотов от 3 до 6, и чаще всего пять. Меньше трёх получается, только если закончились кандидаты в префиксы или суффиксы. Запуски должны показать это распределение, а заодно редкий предмет меньше чем с тремя аффиксами, если такой когда-нибудь выпадет.

Из находки о свойствах:

  • Эфирность — это одно число Range(100) после сборщика качества: предмет эфирный, если выпало меньше 5. Только в дополнении, только оружие и броня с прочностью, не низкого качества и не сетовые.
  • Гнёзда у обычных и превосходных предметов — тоже одно Range(100): гнёзда есть, если выпало меньше 33. А вот число гнёзд не бросается: это начальный сид предмета по модулю предела плюс один.
  • У превосходных свойства тоже бросаются. В справочнике Blizzard сказано, что базовая функция свойства «ставит значению максимум, если предмет превосходного качества». Код бросает значение в любом режиме, а диапазон «от +1 до +3» к точности у превосходного оружия на Arreat Summit с кодом совпадает.
  • Ни одно свойство не пользуется языком формул. В игре есть маленький байткод для некоторых формул. Прочитаны все обработчики свойств, и ни один его не вычисляет, так что свойствам предметов не нужно ждать эту строку карты.

Карта по одному числу

В первый день я написал, что воспроизведение сида комнаты — это момент, когда «проект впервые соберёт кусок карты Diablo II только по спецификации и одному числу». На третий день это были комната и имя файла, в виде чисел. Сегодня в 20:48 пришла находка о генераторе открытой местности, а с ней проверка прочитанного: не новый запуск, а скрипт, который по уже записанным логам заново проигрывает всё построение уровня на открытой местности по его сиду. Он моделирует раскладку акта, границу, скалы и реки, дороги, области пресетов, вейпоинт и святилища, а потом сравнивает каждый поставленный пресет и каждый прямоугольник комнаты с тем, что записала игра.

370 построений уровней на открытой местности из логов, по всем пяти актам (серверные и клиентские копии считаются отдельно), 0 расхождений. Генератор лабиринтов прошёл такую же проверку: 12 построений лабиринтов из логов, 0 расхождений.

Карту по этой модели сам проект ещё не рисовал. Для этого поста скрипт прогнали на четырёх записанных играх, и ниже нарисовано то, что он строит для Blood Moor — уровня 2, первой области за Rogue Encampment. Для каждой из них все пресеты и все прямоугольники комнат, которые поставила модель, совпали с логом игры на том же сиде.

Blood Moor на четырёх сидах карты

Каждый квадрат — одна клетка уровня размером 8×8 тайлов. Построено tools/outdoor_replay.py по сиду уровня, который выводится из сида карты.

  • открытая местность
  • граница или скала
  • река
  • мост
  • вход в Den of Evil
  • другой пресет (дома, камни, пруд)
  • проход на соседний уровень
  • дорога
  • святилище

сид карты 12345
сид уровня 015b2e79

сид карты 777
сид уровня 14ad6889

сид карты 1
сид уровня 6ac69361

сид карты 2026
сид уровня 0775b9ae

На сидах 12345 и 777 уровень широкий, на 1 и 2026 высокий: направление выбирает раскладка акта. Вход в Den of Evil, дома и пруд есть на всех четырёх, каждый раз в другом месте. На сиде 777 через реку перекинут мост.

Квадраты показывают только то, какой кусок стоит в клетке, а не как выглядят его тайлы. Сид уровня выводится из сида карты, набранного в командной строке: сид карты 12345 даёт сид уровня 015b2e79, и по нему река идёт вдоль восточного края, вход в Den of Evil стоит правее середины, а проходы на соседние уровни открываются на западном краю и внизу.

Порядок построения, по находке:

flowchart TB
ms["сид карты"] --> act["сид акта: прямоугольники уровней и связи"]
ms --> ls["сид уровня"]
act --> ring["контур уровня и куски границы"]
ring --> place["скалы, река, мост, пещеры"]
ls --> place
place --> roads["дороги между проходами"]
roads --> fill["вейпоинт, святилища, области пресетов: перемешать внутренние клетки, взять первую подходящую"]
fill --> rooms["по комнате 8x8 на каждую свободную клетку"]

Чаще всего за всё построение игра берёт число, чтобы выбрать файл. Когда тип пресета используется впервые, игра берёт число, чтобы выбрать один из его файлов, даже если файл всего один. Чтобы поставить кусок в случайное место, игра перемешивает все внутренние клетки, по два числа на клетку, и берёт первую, куда он помещается. Если не помещается никуда, ничего не ставится, и если вызывающий код не проверит, этого никто не заметит.

Слоты

В 20:38 пришло ещё одно изменение, и оно про то, сколько всего может работать одновременно: слоты харнесса, ADR-0011. До сегодняшнего дня у запусков был один харнесс, на дисплее :7, и каждый запуск каждого плана ждал его. Вторая копия из третьего дня могла только подключиться к той же игре.

flowchart TB
game["общий каталог игры: закреплённый Game.exe, стандартные архивы"]
game --> g0["слот 0: дисплей :7, wineprefix"]
game --> g1["слот 1: дисплей :8, wineprefix-1"]
game --> g2["слот 2: дисплей :9, wineprefix-2"]
g0 --> l0["игра + gdb + логгер, свои сохранения"]
g1 --> l1["игра + gdb + логгер, свои сохранения"]
g2 --> l2["игра + gdb + логгер, свои сохранения"]

Теперь у каждого слота свой дисплей и свой wine-префикс — копия базового с пустым каталогом сохранений. Все инструменты харнесса принимают --slot N, а --display или --prefix, которые ему противоречат, отклоняются. Каждая роль получает свои слоты в промпте и никогда не трогает чужие. На этой машине одновременно можно держать три-четыре слота, примерно по 770 МБ префикса на каждый.

Изменилось правило уборки. Пока игра была одна, убить все процессы по шаблону через pkill -f было нормальной уборкой. Когда игр несколько, «так убиваются сессии других ролей».

Цифры

Где проект сейчас, в сравнении с прошлыми двумя постами

Спецификации после ревью (из 28 строк)

3 день 1
7 день 3
12 день 4

[C] подтверждённые утверждения в находках

48 день 1
240 день 3
446 день 4

[I] выведенные утверждения в находках

175 день 1
455 день 3
721 день 4

Именованные адреса в Game.exe

278 день 1
626 день 3
1 143 день 4

Инструменты на Python

26 день 1
38 день 3
56 день 4

Закрытые планы

5 день 1
14 день 3
19 день 4

У каждого показателя своя шкала. Источники: карта исследования, docs/findings, re/symbols.csv, tools/ и docs/plans/done к каждой отсечке.

Именованных адресов в Game.exe за день стало почти вдвое больше, с 626 до 1 143. Большинство из них — функции, которые получили имена этим вечером при чтении аффиксов, лабиринтов, переходов и открытой местности.

И теги со временем:

Подтверждённые и выведенные утверждения в находках

[C] — прочитано в коде и подтверждено чем-то независимым, обычно запуском. [I] — только прочитано в коде. Ночи вырезаны.

  • [C] подтверждено
  • [I] выведено
0 200 400 600 800 11 окт. 12 окт. 13 окт. 14 окт. [C] 446 [I] 721

Провалы [I] — это в основном утверждения, перешедшие в [C].

Показать числа
Время[C][I]
11 окт. 10:31 0 0
11 окт. 13:35 7 15
11 окт. 15:05 21 58
11 окт. 16:00 28 59
11 окт. 16:25 42 62
11 окт. 17:57 45 104
11 окт. 19:06 48 148
11 окт. 20:11 48 175
11 окт. 22:12 57 176
11 окт. 22:23 57 176
12 окт. 08:04 80 173
12 окт. 08:41 103 183
12 окт. 09:02 104 184
12 окт. 09:31 108 217
12 окт. 09:58 109 228
12 окт. 10:16 117 278
12 окт. 10:42 135 295
12 окт. 11:51 139 295
12 окт. 12:21 159 291
12 окт. 13:41 161 292
12 окт. 14:10 168 295
12 окт. 15:19 168 320
12 окт. 15:46 184 354
12 окт. 15:58 184 354
13 окт. 08:05 196 348
13 окт. 08:26 204 374
13 окт. 08:33 205 422
13 окт. 10:11 206 448
13 окт. 13:43 223 449
13 окт. 15:59 225 452
13 окт. 18:56 229 453
13 окт. 19:21 236 452
13 окт. 19:28 240 455
13 окт. 21:15 279 429
13 окт. 22:02 279 442
14 окт. 08:11 279 442
14 окт. 08:49 282 443
14 окт. 09:21 282 493
14 окт. 09:36 282 495
14 окт. 09:49 290 536
14 окт. 10:00 291 537
14 окт. 10:09 293 586
14 окт. 10:20 293 587
14 окт. 11:05 332 567
14 окт. 13:10 356 562
14 окт. 17:35 376 560
14 окт. 19:22 376 611
14 окт. 19:56 395 651
14 окт. 20:36 402 691
14 окт. 20:50 446 721

В первый день подтверждённой была примерно пятая часть. К концу третьего — примерно треть. Сейчас 446 против 721, то есть около 38%. В спецификациях, которые и есть результат проекта, 999 подтверждённых против 801 выведенного. На графике по утрам прочитанное переходит в [C], а по вечерам добавляются новые [I] для утренних запусков. С 17:35 до 20:50 число [I] выросло с 560 до 721, почти всё — из находок об аффиксах, лабиринтах, переходах, свойствах и открытой местности.

Что открыто

Все строки ядра начаты, так что теперь осталась в основном вторая половина работы по каждой:

  • Запуски для сегодняшних вечерних находок. Аффиксы, свойства, лабиринты, переходы и открытая местность прочитаны и проверены прогоном там, где были логи, и всем им нужны запуски, теперь на отдельных слотах.
  • Объекты. Святилища, сундуки и бочки: таблицы описаны сегодня, находок пока нет.
  • Характеристики монстров. Здоровье, урон и опыт в зависимости от сложности и числа игроков. Туда же уходит один открытый вопрос сегодняшнего дня: в классике на Nightmare два уникальных монстра с аурой взяли уровень ауры 50, а у уникальных в дополнении это их собственный уровень плюс 3.
  • Пути создания. Торговцы, Cube, рунные слова, наёмники: всё ещё не начаты и всё ещё ждут ядро.

И карта. Сегодня это сетка квадратов для одного уровня, нарисованная вне проекта по его собственной модели. Карту с настоящими тайлами по сиду ещё предстоит нарисовать.