Diablo II, день четвёртый
Пост про второй и третий дни остановился 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 расхождений.
В находках было две ошибки, и обе нашлись при сверке со справочником 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.
390 144 проверки, 0 расхождений. Совпали все 169 выборов модификаторов: список кандидатов, суммарный вес и взятое число. 606 вызовов обработчиков модификаторов на боссах и членах групп не изменили ни одного сида, кроме одного шага rndname. Одиннадцать суперуникальных, среди них Rakanishu, Griswold, Bishibosh и Beetleburst, построены так, как предсказала модель, вместе с числом миньонов. У Rakanishu было 8 миньонов на Normal, 9 на Nightmare и 10 на Hell — в пределах от MinGrp + сложность до MaxGrp + сложность.
Классы монстров за четырьмя суперуникальными из запуска
-
Fallen, класс Rakanishu
-
Griswold
-
Скарабей, класс Beetleburst
-
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
Показать числа
| 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 строго ограничивает, что такой запуск может доказать. Он подтверждает сам путь: отбор, взятые числа, откаты. Но ничего не говорит о том, как часто этот путь срабатывает: «частоты, соотношения и утверждения „как часто“ никогда не могут опираться на такой запуск». И такой запуск никогда не идёт в одиночку: запуск на стандартных таблицах тоже должен воспроизвестись.
1 519 созданий воспроизведены, 0 неудач: 115 сборок уникальных и 72 сетовых на трёх сложностях и в классике. Что из этого вышло:
- Один раз за игру. Уникальный предмет, собранный в игре, ставит бит в наборе из 4 096 бит, и если позже выбор снова падает на ту же строку, сборка не удаётся, без повторного выбора. В одной классической игре на Nightmare выбор пришёлся на уникальный нагрудник, уже собранный в этой игре. Сборка не удалась, прочность утроилась, 34 из 50 стали 102 из 150, и предмет вышел редким. Так выходит любой несобранный уникальный: он становится редким, а если у базы есть прочность, она утраивается.
- Ладдерные уникальные в одиночной игре. Djinnslayer и Leviathan, строки только для ладдера, выпали в одиночной игре на Hell. Флаг ладдера не учитывается, если один байт в структуре игры не равен нулю, а в одиночной игре там 3.
- Сброс сида. Каждый шаг отката возвращает сид предмета к тому же фиксированному началу,
{S, 666}, с которого начинал неудавшийся сборщик, так что числа, взятые этим сборщиком, просто пропадают. - У сетовых нет проверки «один раз за игру». Нет ни проверки
enabled, ни проверки ладдера.
Два ладдерных уникальных из запуска с подменой и база случая с повторной сборкой
-
Djinnslayer
-
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 до 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 строк)
[C] подтверждённые утверждения в находках
[I] выведенные утверждения в находках
Именованные адреса в Game.exe
Инструменты на Python
Закрытые планы
У каждого показателя своя шкала. Источники: карта исследования, docs/findings, re/symbols.csv, tools/ и docs/plans/done к каждой отсечке.
Именованных адресов в Game.exe за день стало почти вдвое больше, с 626 до 1 143. Большинство из них — функции, которые получили имена этим вечером при чтении аффиксов, лабиринтов, переходов и открытой местности.
И теги со временем:
Подтверждённые и выведенные утверждения в находках
[C] — прочитано в коде и подтверждено чем-то независимым, обычно запуском. [I] — только прочитано в коде. Ночи вырезаны.
- [C] подтверждено
- [I] выведено
Провалы [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, рунные слова, наёмники: всё ещё не начаты и всё ещё ждут ядро.
И карта. Сегодня это сетка квадратов для одного уровня, нарисованная вне проекта по его собственной модели. Карту с настоящими тайлами по сиду ещё предстоит нарисовать.