Среда: Омега-день | Глава 7

image

Предлагаю вниманию читателей GT седьмую главу фантастического романа «Среда: Омега-день».

О чем эта книга?

Каждый день жители Алакосо задают друг другу одни и те же вопросы: куда исчезла Большая земля? придет ли конец их заточению на острове? какая сила загоняет их в ситуации, достойные самых жутких сновидений? Но никто из островитян даже не догадывается, что происходит с ними и Алакосо на самом деле.

Краткий гайд по персонажам

Александр Нобби — математик, программист;
Оливье Пирсон — бывший хозяин отеля;
Ила Пирсон — жена бывшего хозяина отеля;
Хелен Пирсон — дочь бывшего хозяина отеля;
Раламбу — капитан рыбацкой команды «Джон»;
Мамфо — старшая по хозяйству, жена Раламбу;
Джошуа — сын Раламбу и Мамфо;
Робин Фриз — участник рыбацкой команды «Пол»;
Юджин — комендант;
Симо («Колдун») — старший по рыболовству;
Венди — старшая по кухне;
Янус Орэ — врач;
Катя Лебедева — летчица, племянница Януса Орэ;
Адриан Зибко («Коп») — старший по безопасности;
Энтони Морн («Очкарик») — бывший полицейский-стажер, участник рыбацкой команды «Джон».

Несколько слов от автора

Эту историю я писал под впечатлением от таких научно-технических достижений, как Интернет вещей, искусственный интеллект, дополненная реальность, «умные города» и Big Data.
По жанровой принадлежности я бы отнес «Омега-день» к киберфантастике с элементами постапа и психологического триллера.

Заранее благодарю за любые отклики и желаю приятного чтения!

Текст — под катом.

Глава 7
Зибко

54

4 года 8 месяцев и 3 суток с Омега-дня

Вокруг лески по поверхности воды лениво расползались концентрические окружности, возмущая отражения облаков, Зенитного затемнения и порхавших повсюду черных бабочек. Двумя метрами ниже упругая нить растворялась в зеленоватом полумраке.

Прижавшись к борту лодки, Катя Лебедева вглядывалась в пахнувшую водорослями пучину. Там, в глубине, двигались неясные тени — по широким спиралям они медленно приближались к леске. Увлеченная наблюдением, Катя все ниже склонялась к воде. Негромкие всплески, прилетевшие из-за кормы, она пропустила мимо ушей.

— А рыбка-то с другой стороны! — внезапно нарушил тишину голос Фриза.
Лебедева вскрикнула и даже подпрыгнула от неожиданности, сильно качнув лодку. Чтобы успокоиться, Катя с шумом выдохнула сквозь сжатые губы.
— Рыбка! — саркастично удивилась она. — Да ты говорящая!
Робин Фриз, висевший на руках с внешней стороны кормы, показал в улыбке желтоватые зубы. Длинные мокрые волосы облепили его осунувшееся лицо с первыми глубокими морщинами.
Катя брызнула на Фриза забортной водой.

— Напугал меня! — шутливо проворчала она.
Затем, о чем-то вспомнив, Лебедева с опаской оглядела берег Алакосо, оставшийся в ста метрах за кормой. Робин в этом время вскарабкался на лодку (от чего нос суденышка на время взмыл в воздух) и сел напротив Лебедевой. Вода стекала с рельефного тела Фриза на деревянное дно, где блестели чешуей несколько рыбин.
Порывшись под сиденьем, Катя вытащила оттуда бумажный сверток и протянула его фризу.

Робин с удовлетворением развернул бумагу:
— М-м-м, неужели с рыбой? Наконец-то!
— Своего теленка я зарежу для тебя в следующий раз! — улыбнулась Катя.
— Бедный Нобби! — воскликнул Фриз. — Он этого не заслужил!
Притворный ужас Робина рассмешил Лебедеву.
Фриз жадно впился зубами в лепешку с рыбной начинкой, но тут же прервал пиршество, чтобы вытащить изо рта кость и бросить ее за борт.

Катя запрокинула голову, словно в небе имелось нечто, заслуживавшее ее внимания.
— Растет? — поинтересовался Фриз с набитым ртом.
— Прямо на глазах… — ответила Катя. — Что будет, когда оно заполнит все небо?
— Скоро узнаем. Почему скоро?
Робин проглотил остатки лепешки и достал из свертка следующую.

— Я располагаю кое-какой информацией из надежных источников, — сказал он.
— Вот как? — недоверчиво произнесла Катя.
— Именно. Но главное произойдет еще раньше. И я знаю, что эти перемены будут к лучшему.
— Ты меня заинтриговал, — не без иронии призналась Лебедева.
— На чем держится моя вера в лучшее? Как говорил Раламбу, сама способность надеяться доказывает, что все небезнадежно. Я жду перемен с радостью: небольшая революция не повредит Алакосо.

Лебедева приподняла острую бровь, а потом вытянула ноги, уперев берцы в соседнее сиденье.
— Я собираю людей, — посерьезнев, заявил Фриз. Все рецепты успеха сводятся к единственной заповеди: «действуй!». И мы последуем этой заповеди.
— Мы? Сколько человек входит в это «мы»?
Фриз отвернулся от Кати.
— Пока немного. Но это не главное.
— А что главное?
— Главное, мы не одни.

55

Морн вышел на пляж, который в утренние часы был романтично безлюден. Энтони остановился в тени кустов на границе с рощей и, расположив ноги на ширине плеч, начал сосредоточенно вращать кистями рук. Вслед за этим Очкарик принялся совершать круговые махи вытянутыми руками — сначала вперед, затем — в обратном направлении.
Морн улыбался, подставив лицо утреннему бризу и глубоко вдыхая свежий океанский воздух. Небесная вспышка отразилась в его очках.

Поставив руки на пояс, Морн начал вращать головой, разминая шейные мышцы. Потом Энтони перешел к приседаниям. Где-то на пятом цикле упражнения в плечо Очкарика ударился камешек. Однако Морн не заметил этого и продолжал, покряхтывая, приседать. Лишь второй камень, угодивший Энтони в ухо, заставил его остановиться и с удивлением поглядеть на кусты.
— Да-да, это я, — опередил Робин Фриз вопрос Морна. — Что я тут делаю? Хочу поговорить с вами.
Морн беспокойно огляделся.

— Фриз! — приглушенно воскликнул он. — Вам необходимо срочно выйти на контакт с руководством! Я могу…
— Тише, Морн, — перебил Фриз. — Я всего лишь прошу о коротком разговоре.
— Боюсь, вы от меня не узнаете ничего полезного, — затараторил Морн.
— Я ударю тебя, если ты не заткнешься и не выслушаешь меня, — холодно предупредил Фриз, почесывая кулак.
Морн пожал плечами.

— Я слушаю, — вздохнул он и вернулся к приседаниям, встав к Фризу боком.
— Я собираю единомышленников, — сказал Робин. — Кто они? Те, кому не нравится, что происходит на острове. Как полисмен, вы не можете не понимать меня. Пора здесь кое-что поменять.
Энтони с сопением присаживался на корточки и снова вставал, никак не реагируя на слова Фриза.
— На моей стороне уже несколько человек, — продолжил Робин. — Мы готовы действовать. Пришла пора и вам сделать свой выбор, Энтони.

Морн выпрямился и сделал скучное лицо:
— Вы о чем, Фриз? Какой выбор? Какие действия? Что вы собрались менять?
— Я собираюсь менять руководство колонии, — объявил Робин.
Морн стащил с носа очки и стал протирать их носовым платком.
— Ох, Фриз, — усмехнулся он, — вы старше и опытнее меня… Впрочем, подобные ошибки допускают даже умнейшие люди… Скажу кратко: Робин, вы не сможете ничего сделать.

— Как говорил Раламбу, победу рождает жажда, — возразил Фриз.
— Увы, не все роды заканчиваются благополучно, — ответил Морн. — Хотя, если уж говорить о жажде, вам для начала было бы неплохо понять, чего вы действительно хотите. Думаете, ваше желание — свалить Юджина? Нет, это не так. Вы просто хотите быть счастливым. Думаете, революция — это мост, ведущий в страну свободы? Выбросьте это подростковую блажь из головы! Глупо бороться за счастье и свободу, ведь они и так уже есть у всех — внутри у каждого.

— Внутри? — рассмеялся Фриз. — Вы серьезно? Хотите сказать, что все вокруг счастливы и свободны?
— Именно. Только упорно не хотят осознать это, — кивнул Морн.
— Я вам нарисую другую схему, Энтони. Без борьбы человеческая жизнь — это прогулка по узкому, как трещина в скале коридору, на одной стене которого написано «страх», а на другой — «тоска». А в конце этого коридора — черная пропасть. Ни счастьем, ни свободой там не пахнет.
— Каждый видит то, что хочет видеть, — отмахнулся Энтони. — Мне жаль вас, Фриз. Вы — раб иллюзии, и это делает вас несчастным. Я бы хотел вывести вас из этого тупика.

Морн упер в бока кулаки и стал выполнять полуобороты туловища.
— Слушаю вас с огромным интересом, — сказал Фриз, усевшись на землю.
— Сначала я бы хотел покончить с темой революции, — заговорил Энтони. — Вы заявили, что хотите убрать Юджина. А вы не задавались вопросом, кто бы смог его заменить? И что мы все выиграем от этой замены? Нельзя не признать, что после Омега-дня мы фактически откатились к первобытному обществу. Известно, что во главе подобного коллектива, как правило, стоит самый сильный мужчина — это сплачивает группу и дает всем ее членам лучшие шансы на выживание.
— То есть Юджин, по-вашему, необходим? По-вашему, он лучший из возможных? — уточнил Фриз.

Морн перешел к наклонам туловища в стороны.
— Идеальных нет, — ответил он, глубоко вдохнув. — Ошибки допускают любые руководители. Без этого никуда. Идеалы — продукт неокрепших мозгов. Вы можете сменить Юджина на кого-то другого, но сообщество все равно вернется в естественное для него «энергетическое» состояние, только пертурбации приведут к лишним проблемам и жертвам. Фриз, рано или поздно вы поймете, что попытки поменять мир — лишь плохой способ справиться с внутренними противоречиями, которые гложут вас уже много лет. Но даже если мир изменится, вы все равно останетесь недовольным, поскольку недовольство — это фундаментальное свойство вашей личности.

Морн лег на песок и преступил к ритмичным отжиманиям. В такт движениям он продолжал:
— Человек, осознавший свою внутреннюю свободу, справившийся с комплексами, сможет преуспеть в любой среде, в любом коллективе. Он понимает и разделяет ценности общества, в котором живет. В сущности, эти ценности универсальны — трудолюбие, честность и уверенность в себе. И если вы культивируете в себе эти качества, если становитесь полезны обществу, оно справедливо вознаграждает вас. Все очень просто, Фриз.
Морн поднялся и улыбнулся Фризу, добродушно склонив голову набок.

— Вы закончили, Энтони? — спросил Робин.
— Я могу привести еще немало доводов, чтобы образумить вас и убедить вернуться к работе.
Фриз устало помассировал лоб.
— Вот что я вам скажу, Морн, — сказал он. — Существует простой способ, позволяющий отличить ценную мысль от схоластики. Знаете какой? Чушь универсальна, а правда имеет крайне узкую область применения. Если доводы уместны в любом споре, значит грошь им цена.
Морн непонимающе насупился.

— Энтони, вы все-таки подумайте над моим предложением, — добавил Фриз.
— Так вы не пойдете к руководству? — удивился Морн.
Робин лишь ухмыльнулся:
— Вы еще можете присоединится к нам.
Бунтарь махнул на прощание рукой и отправился назад в рощу. Морн долго смотрел ему вслед, взявшись за подбородок.

— Подождите, Фриз, — воскликнул он, наконец.
Робин оглянулся.
— Я хотел бы хорошенько подумать, а потом еще раз поговорить с вами, — сказал Очкарик. — Наши взгляды не вполне совпадают, но я думаю, мы можем быть друг другу полезны. Приходите сегодня часов в шесть на берег Тамунто.
Фриз кивнул и исчез в зарослях.

56

Около десяти утра заспанная Мамфо, потягиваясь, вышла из своего номера и направилась к лестнице. Упавшие на лоб спиральные пряди, мятая баскетбольная майка с большой цифрой «5» — все это добавляло ей сонного очарования.

На полпути к лестнице Мамфо остановилась и прислушалась. Из номера Юджина доносились знакомые голоса. Старшая по хозяйству на цыпочках вернулась к двери коменданта и присела возле нее. Ловким (и единственным) пальцем правой руки женщина прочистила замочную скважину от сора, а затем прильнула к двери.

— А если понадобиться еще что-то? — спросил Юджин.
— О чем ты? — откликнулся дребезжащий тенор доктора Орэ.
— О другом лекарстве. Которого нет на острове.
— Не беспокойся по этому поводу, Юджин. Того, что есть, хватит. Ты, главное, соблюдай режим. Ты соблюдаешь? Молодец. И если, например, почувствуешь себя нехорошо между приемами, принимай дополнительную таблетку. Проблема в другом, на самом деле.

— В чем? — растерялся, судя по интонации, комендант.
— Да в том, что ты, поросенок, совсем не учитываешь риски! И мое старческое сердце обливается за тебя кровью.
— Не учитываю риски?
— Ты закрываешься в номере? — с упреком осведомился доктор.
Мамфо ненадолго оторвалась от замочной скважины, чтобы почесать в ухе.
— Иногда закрываюсь, — ответил комендант врачу.

— У тебя хорошая динамика, — беспокойно произнес Янус. — Однако вероятность повторения приступов пока существует. Представь, что будет, если тебе станет плохо, а номер окажется заперт! На то, чтобы сломать дверь, уйдут драгоценные минуты, и…
— Тебе нужен ключ? — перебил доктора Юджин.
— Думаю, будет лучше, если у меня будет доступ в твой номер. Будет лучше для тебя в первую очередь.

— Хорошо, — согласился комендант. — В случае ЧП, иди в подвал. Там, во втором помещении от входа будет ниша в стене. Ниша заставлена коробками. За одной из коробок — сейф. Код доступа: 2908. В сейфе найдешь связку ключей. Там есть дубликат ключа от моего номера. На нем красная ленточка.
— 2908? — переспросил Янус.
— Угу. Таблетки я держу за пазухой или здесь, под статуей рукастого мужика.
— Это Вишну, — усмехнулся доктор. — А вообще, ты правильно делаешь, что не оставляешь пузырек на видном месте.

— Ты что-то знаешь?
Послышался скрипучий смех Орэ:
— А чего это ты так напрягся, Юджин? Боишься за свою жизнь? А зря. Я вот смерти совсем не боюсь. В ней нет ничего страшного: я, например, умирал уже двадцать семь тысяч раз — каждый вечер в теплой постели. Грань между жизнью и смертью столь же неуловима, как мгновение между сном и бодрствованием. А стоит ли бояться того, чего не почувствуешь?
Орэ снова засмеялся.

— Шутки шутками, — продолжил Янус, — но имей в виду, Юджин: если лекарство попадет в руки кого бы то ни было, кроме тебя или меня, жди неприятностей. Заполучив таблетки, любой сможет полностью контролировать тебя, и Truvelo в этом случае не поможет. Так что, будь осторожен, комендант.
— Спасибо, доктор.
— Все будет хорошо, не волнуйся, — подбодрил собеседника Орэ.

Заслышав приближавшиеся изнутри шаги, Мамфо отпрянула от двери. Женщина поспешила прочь, но ступала при этом как можно тише.
Дверь коменданта скрипнула, когда старшая по хозяйству была уже достаточно далеко. Расслабленной походкой Мамфо прошла к лестнице и скрылась внизу.

На самом деле Мурена могла и не торопиться, покидая коридор — из номера Юджина так никто и не вышел. Если бы Мамфо узнала это, держу пари, она бы сильно удивилась и постаралась бы лучше разобраться в том, что происходит. Впрочем, для этого ей потребовались бы совершенно фантастические способности, в частности, умение проходить сквозь закрытые двери. Но что бы она в этом случае увидела внутри номера? Должен признаться, что знаю ответ и, пожалуй, поделюсь им с вами.

Воображение не знает ограничений, поэтому мы можем отправиться в тот самый день и, обернувшись бесплотным духом, влететь в номер Юджина прямо сквозь закрытую дверь.
Для продления интриги, положим, что мы влетаем в номер спиной вперед. Тогда первым делом мы увидим внутреннюю сторону двери, в которой, впрочем, нет ничего особенного — желтоватая древесина с узорами годичных колец.

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

Рассматривая все эти вещи, мы неожиданно понимаем, что дальше отлетать некуда — позади стена с окном. И тогда к нам приходит осознание странного факта: несмотря на то, что в обстановке номера нет ничего настораживающего, и все предметы находятся на своих обычных местах, в помещении явно не хватает чего-то важного…
В помещении не хватает Юджина и доктора Орэ!
Комната абсолютно безлюдна.

57

Лучи Солнца, проскользнувшие под Зенитным затемнением, окрашивали воды Тамунто в ржавый цвет. Стоял полный штиль, и деревья по берегам озера застыли в вечерней тишине, точно нарисованные на холсте.

В роще, шагах в двадцати от западного берега, притаились несколько мужчин: вооруженные деревянными кольями Симо, Зибко, Тани, Изельди, Тутла, а также Юджин с неизменным Truvelo.
С места, где прятались старшие и рыбаки, хорошо просматривался берег Тамунто с сидевшей на пне фигурой, в которой по сутулым плечам и напряженной спине угадывался Энтони Морн.

— Запаздывает наш революционер… — кашлянув, сказал Зибко.
— Заткнись, — вполголоса откликнулся Симо, который отрешенно созерцал озеро, словно жертва гипноза.
Комендант поднес к лицу запястье с наручными часами и включил подсветку циферблата.
— Половина седьмого, — произнес он в полный голос. — Тварь не придет.
— Может, Очкарик ошибся со временем? — предположил Симо. — Умники часто путаются в своих собственных извилинах.

— Как ты — в сетях Мамфо? — съязвил Зибко.
Колдун устало отмахнулся, а остальные мужчины отреагировали на реплику Копа дружным хохотом. Морн оглянулся на их смех.
— Поди сюда, Очкарик! — позвал его Юджин.
Энтони послушно затрусил к зарослям.
— Ты украл у меня час времени! — огорошил Очкарика комендант. — Как возместишь?
Морн теребил джинсовые шорты, не зная, что ответить Массажисту.

— Юджин… прости, — промямлил Энтони. — Он действительно обещал быть здесь в шесть. Я не знаю, что случилось.
Симо, Зибко и рыбаки с насмешливым любопытством наблюдали за нервными ужимками Морна. Комендант дал им еще немного насладиться превосходством над Очкариком, а потом сжалился и потрепал Энтони по щеке:
— Прощаю! — сказал он и гыкнул. — Уходим, ребята.

Все семеро отправились в сторону отеля. Никому из них не пришло в голову взглянуть наверх, хотя если бы они и сделали это, то вряд ли заметили бы Фриза среди густой листвы. Робин лежал на толстом суку, обхватив его ногами, и следил за удалявшимися мужчинами. На раскрытой ладони бунтаря сидела черная бабочка.

58

Номер Адриана Зибко освещала тусклая лампочка, которая свисала с потолка на длинном проводе. Вокруг единственного источника света медленно кружили черные бабочки, отбрасывая на стены мистические тени.

На подоконнике стояла старая стереомагнитолла. Из ее динамиков тараторил неутомимый рэпер.
Стол был укрыт газетами. На них высилась кучка сушеных соцветий и ворох бумаг — исходные материалы для производства сплифов. В углу комнаты распахнул широкий зев мешок с сухими листьями.

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

Прогремел взрыв. Меньше, чем через секунду влажные веки Адриана разомкнулись. Зибко вскочил с кровати и стал напряженно вслушиваться. Громыхнуло так, словно на воздух взлетела половина отеля. В какой-то момент Зибко с подозрением уставился на магнитоллу. Однако громкость устройства стояла на минимуме, и вряд ли оно могло выдать столь оглушительный залп.

Старший по безопасности все еще ловил ртом воздух, но постепенно начал успокаиваться. В конце концов, грохот мог просто почудиться ему во сне. Однако в этот момент раздался второй взрыв, от которого Зибко буквально подпрыгнул на месте. От воздействия взрывной волны задребезжали стекла в окне номера. Теперь стало ясно, что грохотало где-то снаружи здания.
Зибко, покашливая, направился к окну. Судя по выражению лица Адриана, вид из номера произвел на него сильное впечатление. Старший по безопасности в изумлении провел пальцами по усам и бородке.

Окно выходило на юг, благодаря чему из него можно было наблюдать озеро Тамунто и его окрестности. И Зибко отчетливо видел множество дрожавших огней на берегу водоема. Огоньки находились в постоянном движении, как рой светящихся насекомых. Разглядеть подробности оказалось Адриану не под силу, как бы он ни щурился и ни напрягал глаз. Тогда старший по безопасности развернулся и решительно зашагал из номера.

59

Адриан Зибко, примчавшийся к Тамунто, завороженно наблюдал за открывшимся ему действом. В первый момент Коп вряд ли увидел у воды что-то, кроме хаотично двигавшихся огненных цветков. Но Адриан, скорее всего, быстро осознал, что свободную от деревьев площадку на западном берегу озера озаряют десятки подвижных факелов. Многочисленные отражения в водной глади удваивали количество источников света.

Рыбаки с факелами в руках беспрестанно расхаживали по берегу Тамунто, описывая сложные петляющие траектории. Участники этого броуновского движения не останавливались ни на секунду, но ухитрялись при этом не сталкиваться друг с другом. Рыбаки бубнили что-то нечленораздельное, и над берегом стоял гул полусотни голосов.
Набрав в легкие побольше воздуха, Зибко вышел из-за деревьев на освещенное факелами пространство.

— Отставить! — заорал он. — Именем коменданта! Прекратить безобразие!
Рыбаки смолкли и все как один встали лицом к Зибко. Лица их постепенно приобретали осмысленные выражения, словно участники странного ритуала выходили из транса.
Коп собирался обратиться к присутствовавшим и уже раскрыл было рот, но его опередил чей-то крик, донесшийся из толпы рыбаков:
— Слава Зибко!!!

Не успел Адриан как-то отреагировать на услышанное, как один за другим рыбаки стали падать ниц, склоняя головы в направлении Копа. Факелы в руках молившихся бросали рыжие отсветы на ряды округлых спин.
— Слава Зибко! Слава Зибко! Слава Зибко! — надрывался кто-то.

60

— Ты должен помнить главное, — мягко сказала Мамфо, — ты всегда останешься самым дорогим и близким для меня человеком.
Джошуа сидел на противоположной от матери стороне кровати, надув щеки и сложив на груди руки. Всем своим видом он показывал, что слова Мамфо ему глубоко безразличны.
Согнувшись и поставив локти на колени, старшая по хозяйству рассматривала длинные суставчатые пальцы ног, которыми волнообразно перебирала.

— Не разговаривай со мной, как с ребенком, мама, — сухо проговорил Джошуа.
— Если ты у меня взрослый, то должен был понять все без дополнительных обсуждений, — тут же нашлась Мамфо.
— А я все прекрасно понимаю!
Джошуа выпятил нижнюю губу и что-то смахнул со щеки.
Мурена с интересом придвинулась к сыну:
— И что ты понимаешь?

Мальчик злобно сжал зубы, а затем выпалил:
— Я понимаю, что ты предала отца! Он погиб в море, выполняя свой долг, а ты даже не горюешь по нему!
Мамфо сжалась, будто ее окатили ледяной водой.
— Что за ерунда, — пробормотала она, со вздохом закрыв лицо ладонями. — Знаешь, пройдут годы и ты пожалеешь о своих словах, — добавила она холодно. — Рано или поздно тебе все станет понятно. А возможно, ты и сам окажешься в подобной ситуации.
— Я никогда не окажусь в подобной ситуации! — воскликнул Джошуа.
— Почему ты так уверен?
— Потому что я не предатель!

— Ты не предатель, — согласилась Мамфо. — Ты — эгоист. Маленький, вредный эгоист. И эти слова об отце… Ты просто не любишь Симо. В этом истинная причина твоего недовольства.
— А почему я должен его любить? — возмутился Джошуа. — Он — сумасшедший! Я хочу, чтобы он отстал от тебя, от нашей семьи! Скажи ему или это сделаю я!

Мурена резко поднялась с кровати и встала перед сыном:
— Значит так, — стальным тоном заговорила она, — ничего ты никому не скажешь. Ты не имеешь права лезть в дела взрослых. Понял? Я хотела поговорить с тобой, как с разумным человеком, на равных, но ты, похоже, слишком мал для таких разговоров.
— Ну мал, так мал, — развел руками мальчик и, спрыгнув с кровати, заторопился к выходу.
— Джошуа! — властно окликнула его мать.

Ответом ей был лишь грохот захлопнувшейся двери. Мамфо направилась вдогонку, но, сделав пару шагов, остановилась и поднесла руку ко лбу. После недолгой заминки старшая по хозяйству подошла к окну и, опершись на подоконник, задумчиво закусила нижнюю губу. Вид ночного пляжа не вызвал интереса у Мурены, поэтому она присмотрелась к стеллажу, который возвышался слева от нее. По соседству с книгами Жюля Верна, Стивенсона, Герберта Уэллса, Уильяма Голдинга и Алекса Гарленда располагалась миниатюрная модель шхуны «Hispaniola», а также один из рисунков Джошуа, который мальчик поместил в фоторамку. Это был определенно портрет Раламбу, хотя узнать отца Джошуа можно было скорее по коралловым бусам и необъятным синим шортам, чем по чертам лица. Мамфо взяла рисунок с полки и смахнула пальцами пылинки, осевшие на стекле.

За дверью послышались торопливые шаги, затем — скрип, и вот на пороге появился Джошуа, выглядивший так, словно только что столкнулся с белым медведем.
— Что стряслось? — тревожно спросила Мамфо, возвращая фоторамку на место.
— На острове что-то происходит, — ответил мальчик севшим от волнения голосом.

61

— Мне нужно все твое внимание, — сказала Мамфо Джошуа, сжав его плечи. — Прямо сейчас ты пулей полетишь к Венди. Беги по Западной аллее, через рощу. Это важно, слышишь? Не вздумай отправиться в Поселок через озеро! По пути не останавливайся и ни с кем не разговаривай. У Венди останешься до тех пор, пока я за тобой не приду.
Мурена с подозрением огляделась — в коридоре третьего этажа кроме них с Джошуа никого не было.

— Ты понял меня? — тряхнула Мамфо сына. — Могу я на тебя положиться?
— Да, — кивнул мальчик.
— Еще раз!
— Да, мам!
— Пошел!
Мамфо развернула сына и слегка подтолкнула его. Двинувшись к лестнице, Джошуа хмуро оглянулся на мать через плечо.
— Живее! — крикнула Мамфо вдогонку сыну.

Убедившись, что Джошуа начал спускаться, старшая по хозяйству направилась к номеру Юджина. Без лишних церемоний Мамфо распахнула дверь и вошла в логово коменданта.
Юджин лежал в постели, уткнувшись лицом в подушку. Его правая рука свешивалась с кровати, так что зажатый в ней пистолет-пулемет упирался стволом в пол. Верхом на пояснице коменданта восседала обнаженная Тамби, которая молотила ребрами ладоней по холке господина. При виде Мамфо наложница скромно потупилась.

— Сгинь! — вполголоса приказала ей Мурена.
Тамби спрыгнула с Юджина и умчалась из номера, прихватив со стула халат.
— Заменить ее хочешь? — пробубнил в подушку комендант.
— Ты должен кое-что увидеть, — объявила Мамфо, подойдя к кровати.

62

Юджин и Мамфо, притаившись среди ветвей, наблюдали за тем, что происходит на берегу Тамунто. У озера собрались рыбаки. Их факелы бросали отсветы на лица двух шпионов.
— Люди от природы хотят быть верными и полезными, — тихо проговорила Мамфо, — но мир всегда вознаграждает их за отказ от этих планов.
Мурена поправила съехавшую бретельку.

— Что за черт? — пробормотал Юджин.
В центре свободного от деревьев участка, примыкавшего к Тамунто, стоял старший по безопасности Адриан Зибко. Вокруг него по часовой стрелке шагали рыбаки-факелоносцы. Взгляды их были обращены к Зибко, а губы находились в постоянном движении, будто они читали молитвы. Время от времени рыбаки возносили руки к ночному небу, исчерченному зелеными меридианами.

— Зибко в коменданты! — выкрикнул кто-то из факелоносцев.
— Зибко в коменданты! — повторили два голоса хором.
— Зибко в коменданты! — подхватила вся толпа.

Старший по безопасности переходил от одного рыбака к другому, обмениваясь с ними короткими эмоциональными (судя по мимике) репликами. Поравнявшись с Мду, Коп положил руку на плечо голого рыбака. До Юджина и Мамфо не долетели слова Зибко, но реакция на них Мду оказалась странной. Голый рыбак закружился на месте, барабаня ступнями по пыльной земле.

Вечно мерзнувший Ив также был в числе собравшихся на берегу. Как обычно, он был укутан в цветастый платок и держал на руках кота. Зибко, судя по всему, что-то не нравилось в поведении Ива, поскольку Коп почти сразу замахнулся на него цепочкой, которую до того крутил на пальце. Мерзляк отпрянул от Копа, но не покинул площадку, а продолжил обход вокруг ее центра.
Некоторые рыбаки, например Дла-Дла, вели себя почти пугающе. Переговорив с Зибко, толстяк упал на колени и стал кланяться старшему по безопасности, прикладываясь к земле лбом. Зибко без видимого удовольствия наблюдал за подобострастным рыбаком.

Еще немного побродив среди факелоносцев, Зибко влез на пень, который торчал посреди прибрежного участка. Адриан смотрел на рыбаков каким-то диковатым взглядом, что могло говорить о его нетрезвом состоянии. Наблюдая со своего постамента за толпой, он периодически почесывал сломанный нос и поправлял форменные брюки.

— Вы все поняли, что нужно делать? — внезапно спросил он во весь голос. — Вопросы остались?
— Поняли, сэр! — хором откликнулись рыбаки.
Услышав это, Юджин сразу же вскинул пистолет-пулемет. Но Мамфо опустила его руку, мягко надавив на нее сверху.
— Тише, дурачок! — прошептала она. — Ты всегда успеешь пустить в ход свою игрушку.
Комендант повернулся на месте к своей спутнице и вопросительно посмотрел на нее. Мамфо наклонилась к груди Юджина, к тому месту, где его футболку украшал большой желтый смайлик. Женщина пошевелила ноздрями и поморщилась.

— Ты давно был в душе? — поинтересовалась она.
Ее вопрос был скорее риторическим, и Мурена не стала дожидаться ответа. Вместо этого она сразу же вернулась к основной теме:
— Я думаю, тебе надо проверить тайник.
Неожиданно Мамфо вскрикнула, а затем выдала крепкое ругательство. Она брезгливо поежилась и стряхнула с запястья черную бабочку.

— Ненавижу насекомых! — пояснила Мурена и продолжила: — Не исключено, что Зибко, разрази его страшным пробоем, нашел твой схрон.
Юджин провел ладонью по ежику на голове.
— Зибко в коменданты! Зибко в коменданты! Зибко в Коменданты! — донеслось с берега Тамунто.
— Ты теряешь время, — поторопила Юджина Мамфо.

63

Юджин бежал по берегу ручья Тамунти: в правой руке — Truvelo, в левой — фонарь, под мышкой — небольшая лопата. Из-под ботинок коменданта во все стороны летели брызги и мокрый песок. Луч фонаря прыгал по мелким камням и корягам, на десять метров опережая Юджина. Время от времени сквозь световой конус пролетали черные бабочки.
Комендант остановился и осмотрелся: по зарослям кустарника, деревьям и невысоким скалам скользнул круг ярко-желтого света. Юджин побежал дальше — остановка оказалась преждевременной. Не теряя скорости, Массажист перескочил через лежавшее поперек ручья дерево.

Неожиданно под луч фонаря попал мелкий зверек, который пил воду из ручья. Животное сверкнуло желтыми глазами-отражателями и скрылось в зарослях.
В месте, где песок оказался особенно мокрым, ботинок Юджина ушел в грунт почти полностью. Комендант не заметил этого, и при следующем шаге вытащил ногу из застрявшей обуви. Почувствовав под ступней песок, Массажист разразился громким проклятьем.
Юджин ополоснул правую ногу в ручье, а прыжками на левой добрался до застрявшего ботинка. Осторожным движением комендант всунул ногу в обувь и продолжил кросс.

Следующую остановку Массажист сделал через пару минут. Лучом фонаря он обшаривал окружающее пространство, пока не остановился на темной каменной глыбе. Скала была расколота надвое: через всю ее толщу проходила широкая трещина.
Юджин посветил в противоположную сторону, и в световой конус попало высохшее деревце. Комендант что-то удовлетворенно пробурчал себе под нос, а затем встал посередине отрезка между расколотой глыбой и руслом ручья. Здесь он и воткнул в грунт лопатку.

Для фиксации источника света Юджин воспользовался трещиной в скале — комендант вставил туда рукоятку фонаря. Луч света теперь был направлен туда, где из песка торчала лопатка. Для пистолета-пулемета нашлось сухое место на камне в паре шагов от ручья.
Пришло время поработать землекопом. Однако не успел Юджин сделать и пары копков, как замер и огляделся по сторонам. Комендант прислушался, а потом даже посмотрел на небо, где светились зеленые меридианы. Но тревога оказалась напрасной — вокруг было тихо.

Юджин вернулся к работе, и рядом с ним начала быстро расти куча влажного песка. Комендант громко пыхтел — копать с обездвиженной шеей было нелегко.
Глубина ямы увеличилась до тридцати сантиметров, когда лезвие лопаты впервые шаркнуло по чему-то твердому. Юджин наклонился над ямой и нащупал металлические ручки. Потянув за них, он не без усилий извлек из углубления ящик. На его гладкой поверхности были заметны черные буквы и цифры, которые когда-то нанесли с помощью трафарета.

Поставив ящик на песок, Юджин снял с пояса связку ключей и отыскал среди них нужный. Комендант снял с висячего замка полиэтиленовый пакет, а затем без труда повернул в скважине ключ. С легким скрипом крышка ящика откинулась, открыв глазам Массажиста пару винтовок, пистолет, а также несколько упаковок патронов.

Комендант пошарил рукой в ящике, гремя оружием, а потом выругался и сбегал к треснувшей глыбе за фонарем. Направив его луч в ящик, Юджин тщательно проверил целостность комплекта. Массажист несколько раз приподнимал винтовки, чтобы убедиться в сохранности лежавших под ними патронов. В конце концов Юджин успокоился и на некоторое время задумался.
— Хм…, — буркнул он и захлопнул крышку.

64

Завершив небольшой обход поселка, Мамфо постучала в дверь Венди. В ожидании ответа Мурена напряженно поглядывала на соседние домики.
Туча не открывала — за дверью стояла тишина. Выждав четверть минуты, Мамфо снова постучала. И опять безрезультатно. Когда Мурена в третий раз занесла кулак для удара по двери, изнутри, наконец, послышались шаги и скрип половиц.

— Венди, это я, — вполголоса проговорила Мурена.
Дверь приоткрылась, и на пороге показалась старшая по кухне. Спросонья Венди щурилась и недовольно кривила губы.
— Что стряслось? — спросила она.
— Я пока еще не поняла, — ответила Мамфо и кратко оглянулась на улицу за своей спиной. — Венди, ты сегодня не замечала ничего странного здесь, в поселке?
— Странного?
Венди открыла дверь шире.

— В прочем, неважно, — махнула однопалой рукой Мамфо. — Вы уже легли?
— Мы?
— Пусть Джошуа побудет у тебя, пока все не утрясется, хорошо? — с ноткой извинения попросила Мурена.
— Ну конечно! — улыбнулась Туча. — А когда ты его приведешь?
Венди долго не могла понять, почему ее вопрос так ошарашил Мамфо, и куда вдруг помчалась Мурена.

65

Юджин вошел в безлюдный, плохо освещенный холл отеля, который патрулировали неутомимые черные бабочки. Стараясь производить как можно меньше шума, комендант проследовал мимо регистрационной стойки и обтянутых красной кожей диванов.
На ботинках и джинсах Массажиста оставалось еще много песка, принесенного с берегов ручья Тамунти. При каждом шаге песчинки осыпались на пол и похрустывали под подошвами коменданта.

Юджин крутил торсом и выставленным перед собой truvelo, напоминая танк, водящий из стороны в сторону орудийной башней. Так Массажист добрался до главной лестницы и, преодолев ее первый пролет, оказался на межэтажной площадке.
Скрипнувшая дверь парадного входа заставила коменданта развернуться. Довольно долго он держался наготове, поглаживая пальцем спусковой крючок, но холл оставался пустынным — по-видимому, дверью играл ветер.

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

Переведя дух, комендант продолжил подъем. Новых встреч на лестнице не произошло, и до третьего этажа Юджину удалось добраться, не истратив ни одного патрона.
Комендант не торопился поворачивать в коридор, где располагались номера Золотого квартета. Он склонил набок туловище и выставил из-за угла половину лица, чтобы оценить обстановку. На этаже было тихо, лишь слабо гудела вентиляционная система. В конце коридора темнела дверь, ведущая в номер Адриана Зибко.

Юджин бесшумно поставил на пол коридора ногу, а затем притянул к ней все остальное. Скользящими шагами он стал продвигаться к двери Зибко, держа ее на мушке Truvelo. Когда до цели оставалось не больше пяти шагов, стали слышны мерные удары басов — в номере Зибко играла музыка.
Юджин, готовый в любой момент открыть огонь на поражение, продвинулся еще на пару метров. В этот момент раздался предательски громкий писк. Комендант не сразу понял, что сигнал подают часы на его запястье. Разобравшись, наконец, с источником звука, Юджин нажал кнопку на корпусе гаджета, и писк прекратился.

Комендант посмотрел на дисплей часов и тревожно вздохнул. Его намерения сразу же изменились: Юджин вернулся к двери в свой номер и, достав из кармана ключ, вставил его в замок. Перед тем как зайти в комнату, Юджин еще раз навел «орудийную башню» на дверь Зибко: изнутри номера по-прежнему доносилась музыка.
Еще ни разу комендант не заходил в свою комнату с такой осторожностью. Впрочем, осмотр помещения не выявил источников опасности. Смятая постель, заставленный грязной посудой стол, куча одежды на полу — ничего в номере не изменилось с того времени, когда Юджин был здесь в последний раз.

Комендант подошел к пианино и поднял с его верхней крышки статуэтку Вишну. С одной из четырнадцати рук божества слетела потревоженная черная бабочка.
Судя по виду коменданта, под основанием статуэтки скрывалось нечто ужасное. Ужасное настолько, что Юджин тут же схватился за левую часть груди, частично прикрыв ладонью желтую рожицу, изображенную на футболке.
Вряд ли существовал на Алакосо предмет, который смог бы напугать коменданта сильнее, чем пустота, которую Юджин обнаружил под статуэткой Вишну.

66

Дверь в номер Зибко по-прежнему оставалась заперта, под ней горела желтая полоска света. Из номера доносились приглушенные биты и речитатив. Комендант шагнул к двери старшего по безопасности. Под подошвами захрустели сухие крошки — похоже, Джошуа недавно бегал тут с печеньем или бутербродом.

Продвинувшись к комнате Зибко всего на один шаг, комендант опять остановился и прислушался, крепко сдавив пальцами магазин пистолета-пулемета. Свободной рукой Юджин почесал живот, а затем приблизился к цели еще на пару метров.

Висевшая под потолком люстра светила коменданту в спину, и от ног Юджина протянулась длинная тень, конец которой доставал до полоски света под дверью Зибко. Комендант нахмурился — с этого расстояния уже хорошо слышались странные шаркающие звуки, доносящиеся из номера старшего по безопасности.

Лицо Зибко, то и дело исчезавшее в клубах дыма, блестело от пота. В зубах Адриана тлел мятый сплиф. Толстая извилистая, как русло реки вена часто пульсировала на шее Копа.
В центре номера стоял перевернутый стол с торчавшими кверху тремя ножками. Четвертая была в руках у Зибко — сидя на краю кровати, Адриан строгал оторванную деталь ножом. В руках Копа ножка постепенно превращалась в копье, а у ног Адриана росла горка стружки.
В щели под дверью показалась тень. При виде нее Адриан выронил из задрожавших губ сплиф и вскочил с кровати. Бесшумно переступая с ноги на ногу, Зибко пошел к выходу из номера, держа «копье» наготове. У самой двери он остановился и отвел рукой волосы от уха. Из коридора послышался легкий шорох — гость был уже у самой двери.

Комендант не спешил входить или стучаться. С отсутствующим видом он чесал в мизинцем в ушной раковине. Впрочем, его невозмутимости внезапно пришел конец: комендант вдруг помрачнел, закатил глаза и схватился за левую часть груди. Дыхание Юджина участилось, воздух свистел, проходя сквозь стиснутые зубы. Ноздри коменданта задергались, у Юджина выступили слезы. С усердием автомата он принялся массировать себе грудь.

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

Комендант громко сопел, постукивая кулаком по области сердца. Подойдя к двери почти вплотную, он передернул затвор Truvelo. Медленным движением Юджин навел оружие на замок. Палец Массажиста нащупал спусковой крючок. Как раз в это мгновение щелкнула задвижка, и дверь в номер распахнулась.
Перед комендантом предстал старший по безопасности с поношенным ботинком в руках. У босых ног Копа валялась заточенная ножка стола. Не дожидаясь реакции Юджина на увиденное, Зибко поднял ботинок над головой, словно чудом добытую реликвию.
Без лишних вопросов комендант прицелился в лицо Зибко.

— Далеко не только! — многозначительно заявил Коп. — Далеко не только, Юджин!
— Чего? — прищурился комендант.
Зибко, щека которого нервически подергивалась, глянул на пистолет-пулемет.
— Ты можешь всадить пулю в мои мозги, комендант, — сказал он, — но в этом случае ты никогда не узнаешь, что творится на этом проклятом острове.
Обдумывая услышанное, Юджин поглаживал спусковой крючок подушечкой пальца. Зибко сделал глотательное движение, и с его носа сорвалась дрожащая капля.

— Слышишь? — продолжил Коп. — Любому смертнику полагается последнее слово.
— Попробуй, — согласился Юджин.
— Он почти успел. Почти добился своей цели, — усмехнулся Зибко. — Завербовал почти всех рыбаков и готов был бросить их на отель. Знаешь, комендант, я действительно дал маху. До сегодняшнего дня я не замечал деятельности этого негодяя, хотя он плел свои сети у всех на виду.

Зибко громко прочистил горло и отбросил со лба тяжелые афрокосички:
— Сам знаешь, комендант: если движение нельзя остановить, его нужно возглавить. Когда я узнал, что этот подонок собирает своих людей на берегу Тамунто, мне ничего не оставалось, кроме как явиться на их сбор и объявить о своей солидарности с этими псами. Так я смог не только взять дело под свой контроль, но и выяснить, кто руководит бунтом.

— Кто? — спросил Юджин, не опуская пистолета-пулемета.
Зибко поднял указательный палец, чтобы призвать коменданта к терпению. Коп плавно присел, поставил ботинок рядом с «копьем», а затем снова выпрямился с поднятыми руками. Ствол Truvelo в руках коменданта отслеживал все движения Зибко.
— Дла-Дла, — наконец, ответил Коп. — Мерзкий предатель, который привык притворяться безобидным жирдяем. Кто бы мог подумать!
Юджин озадаченно скривил губы.

— Так что, убери пушку, комендант, — попросил Зибко. — Опусти ствол, и я расскажу тебе подробности.
С полминуты Юджин стоял неподвижно, а потом выполнил просьбу Копа. Зибко облегченно вздохнул и улыбнулся:
— Так-то лучше.

Как раз в это время из-за спины коменданта послышался частый топот. Старший по безопасности с удивлением отклонился вправо, чтобы выглянуть в коридор.
— Стойте! — послышался крик Джошуа. — Стойте! Стойте, комендант! Вы не знаете!
Запыхавшийся мальчик со взмокшей головой и забрызганными грязью брюками вбежал в номер.
— Не стреляйте, комендант! — твердил Джошуа. — Мистер Зибко… старший по безопасности… Он не виноват! Он ничего не терял! Все на месте!
— На месте? — не понял Юджин.
Джошуа пытался привести дыхание в норму.

— Под… подождите! — поднял он ладони с растопыренными пальцами.
Мальчик промчался через комнату и присел возле кровати. Просунув руку под койку, Джошуа достал оттуда желтую баночку и потряс ей в воздухе, чтобы привлечь внимание Юджина.
— Вот! Все на месте! Не ругайте мистера Зибко! Он ничего не потерял!

Коп растерянно хлопал ресницами, глядя то на баночку, то на Юджина.
— Я не… — начал было он, но комендант не стал дожидаться объяснений. Он огрел Копа основанием кулака, в котором сжимал рукоятку Truvelo. Зибко рухнул на пол как подкошенный. Комендант сел рядом с Копом, взвалил его себе на плечо и понес из номера.
— Иди к себе, — не оглядываясь, приказал он Джошуа.

Подвальное помещение освещал слабый светильник, закрепленный на стене под самым потолком. Мамфо и Юджин вполголоса переговаривались, стоя у лестницы.
— Ты проверил схрон? — спросила Мурена.
— Все на месте, — пробурчал Юджин.
— А? — не расслышала Мамфо.

Женщина с раздражением обернулась на громко мычащего и сучащего ногами Зибко.
— А нельзя ли потише? — воскликнула она.
Зибко висел на веревке, многократно обвитой вокруг его талии и вытянутых по бокам рук. Ноги старшего по безопасности болтались над бочкой, доверху заполненной водой. Всплески и волны на поверхности выдавали присутствие в бочке чего-то подвижного и многочисленного.
Наверное, Зибко многое бы высказал коменданту и старшей по хозяйству, если бы не грушевидная лампа накаливания, патрон которой выглядывал из его кровоточивших губ. В сложившейся ситуации старшему по безопасности оставалось лишь надсадно мычать.
Веревка, на которой висел Зибко, была перекинута через установленную под потолком балку и привязана к ржавой трубе, выныривавшей из подвальной стены.

По разбитому лицу Копа стекали кровь, сопли и слюни. Он так неистово извивался и голосил, что его глазные яблоки норовили вывалиться из отведенных им в голове мест.
Мамфо положила руку на плечо Юджину:
— Продолжай.
— Схрон цел, — повторил Юджин, перекрикивая Зибко.
— Значит, до оружия они не добрались… — рассудила Мамфо и поморщилась, так как Коп не унимался. — Надо подумать. Время у нас есть. Без главаря они действовать не решатся. Нужно завтра же провести смотр и выявить самых неблагонадежных. Без Симо будет не обойтись.
Зибко перешел на рык и скулеж.

— Да замолчишь ты?! — вскричала Мамфо.
При этих словах труба, к которой была привязана веревка, оторвалась от стены и устремилась вверх под действием веса Зибко. С отчаянным воплем старший по безопасности плюхнулся в бочку. Смешанные со слизью брызги окатили стены подвального помещения. Над поверхностью осталась лишь истерически мычащая голова Адриана.

Промокшая от брызг Мамфо прыснула со смеху. Не в силах говорить, она лишь водила указательным пальцем, повторяя движение веревки. Она бы еще больше развеселилась, если бы труба, оторвавшаяся от стены, перемахнула через балку и обрушилась на голову Зибко. Но, к счастью, ржавая железяка застряла наверху, зацепившись за балку.
— Она сама… честно! — хохотала Мурена.

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

______________________________________________________________________________

Друзья! Всех, кого не оставила равнодушными эта история (искренне надеюсь, что такие существуют), я приглашаю в группу VK, посвященную «Среде». Если вы захотите в теплой, неформальной обстановке обсудить последние события на Алакосо или у вас появились вопросы по содержанию романа — милости прошу! Кроме того, буду благодарен за указания на орфографические ашыпки, а также за конструктивную (как, впрочем, и деструктивную) критику. В группе я буду публиковать не только анонсы новых глав, но и различные заметки, касающиеся работы над книгой. Не обойдется и без сюрпризов (но это не точно).

Короче говоря, добро пожаловать!
ссылка на оригинал статьи https://geektimes.ru/post/289523/

Три цикла в аттракторе Лоренца

image

Изучая иностранную литературу, на днях наткнулся на работы [1, 2] профессора Мичиганского университета Дивакара Вишваната (Divakar Viswanath) об итерационном алгоритме вычисления периодических орбит динамических систем, основанном на методе Линдштедта-Пуанкаре (ЛП) (для ознакомления с ним рекомендую книгу [3, с. 408-411]). Преимуществом данного метода является то, что он не требует численного интегрирования дифференциального уравнения, поэтому может быть применён к построению и неустойчивых циклов.

На сегодняшний день в математике одно из популярных направлений исследований — это теория динамического хаоса. Самым известным объектом здесь является система Лоренца, введённая в 60-е годы 20-ого века. Отмечу, что с того времени появилось много нелинейных математических моделей, где имеет место хаотическое поведение решений, в различных областях науки. Несколько лет назад получили популярность хаотические системы без положений равновесия, применяемые для шифрования сигналов (см., например, [4]). Тем, кто только начинает заниматься теорией хаоса, советую посмотреть математический фильм ХАОС, состоящий из девяти глав.

Уорвик Такер (Warwick Tucker) в работе [5] доказал существование периодических решений в аттракторе Лоренца, но убедительных доказательств найденных циклов в численных экспериментах авторов различных статей мне не удавалось найти.

Пусть x(t), y(t) и z(t) — фазовые координаты системы Лоренца. В 2004 году Вишванатом в работе [2] были найдены три цикла ЛП-методом. Он привёл значения начальных условий и периода:

где T — период.

На мой взгляд, это значительный прорыв в исследовании аттрактора Лоренца.

Может возникнуть вопрос — зачем 99 знаков в дробной части чисел? Дело в том, что периодические орбиты являются неустойчивыми (и эти числа, скорее всего, являются иррациональными), и чтобы их построить численными методами на периоде с приемлемой точностью, нужно располагать большим количеством знаков после запятой.

Я решил проверить и построить циклы Вишваната в программе, приведённой в топике [6] (численная схема также описана в работе [7]). Для этого была взята точность 1e-110 по степенному ряду, количество бит под мантиссу вещественного числа — 390 (машинный эпсилон при этом равен 7.93107e-118), проход по времени — только вперёд. Осуществлялась проверка следующих равенств:

x(0) = x(T),
y(0) = y(T),
z(0) = z(T).

Для первого цикла совпали все знаки в дробной части у всех координат, кроме последнего знака для y(T) (у меня там получилась цифра 6), для второго — 80 знаков, для третьего — 38 знаков.

Далее приведены рисунки циклов Вишваната.

image
image
image

И анимация, загруженная на YouTube.

Для второго рисунка меня заинтересовало, насколько близко траектория системы проходит к началу координат O(0;0;0). В численном эксперименте найдена точка с координатами

Поскольку базовым методом для численной схемы является метод степенный рядов, для третьего цикла определена максимальная степень аппроксимирующего полинома на переменных шагах интегрирования, где решение раскладывается в ряды, — 78, приближенное максимальное значение шага интегрирования при этом = 0.0120621. Так как значение T здесь достаточно велико, время вычислений на моём компьютере составило примерно 6.2 мин.

Литература

1. Viswanath D. The Lindstedt-Poincare Technique as an Algorithm for Computing Periodic Orbits // SIAM Review. — 2001. — Vol. 43, Iss. 3, pp. 478-495.
2. Viswanath D. The Fractal Property of the Lorenz Attractor // Physica D: Nonlinear Phenomena. — 2004. — Vol. 190, Iss. 1-2, pp. 115-128.
3. Федорюк М.В. Обыкновенные дифференциальные уравнения. — М.: Наука, 1985. — 448 с.
4. Wang Z., Akgul A., Pham V.-T., Jafari S. Chaos-Based Application of a Novel No-Equilibrium Chaotic System with Coexisting Attractors // Nonlinear Dynamics. — 2017, pp. 1-11.
5. Tucker W. A Rigorous ODE Solver and Smale’s 14th Problem // Foundations of Computational Mathematics. — 2002. — Vol. 2, Iss. 1, pp. 53-117.
6. Пчелинцев А. Динамическая система Лоренца и вычислительный эксперимент, 2014. Хабрахабр. https://habrahabr.ru/post/229959/
7. Пчелинцев А.Н. Численное и физическое моделирование динамики системы Лоренца // Сибирский журнал вычислительной математики. — 2014. — Т. 17, №2. — С. 191-201.
ссылка на оригинал статьи https://habrahabr.ru/post/329578/

Реверс-инжиниринг игры Lost Vikings

После интересной обратной разработки игрового движка Comprehend (см. Recomprehend) я подбирал новый проект для реверс-инжиниринга игры под DOS. За долгие годы разные люди реверсировали множество старых популярных игр и опубликовали для них спецификации и инструменты. Например, на сайте shikadi.net есть куча информации об играх, в которые я играл в детстве.

Я обнаружил, что для реверс-инжиниринга игры The Lost Vikings компании Blizzard (тогда она называлась Silicon and Synapse), похоже, не предпринималось никаких серьёзных попыток. Игра была выпущена в 1993 году, на закате эры DOS, и очень нравилась мне в юности. The Lost Vikings — это головоломка-платформер, в которой игрок управляет тремя викингами, каждый из которых имеет собственные умения. Викингам нужно объединить свои силы для решения загадок и прохождения уровней с различной тематикой: космический корабль, доисторический мир, Древний Египет. На изображении ниже показан первый уровень игры (источник: Strategy Wiki):

image

Казалось, что эту игру разобрать будет довольно просто. Уровни основаны на тайловых картах и содержат простые загадки: кнопки, включающие и отключающие объекты, передвижные ящики и поднимающий предметы кран. И на самом деле, бóльшая часть проекта по обратной разработке была достаточно прямолинейной. У игры есть один пакетный файл данных, содержащий сжатые блоки файлов. Блоки кодируют различные ресурсы игры, такие как спрайты, карты, звуки и т.д. Я написал несколько утилит, которые можно использовать для просмотра ресурсов игры: The Lost Vikings Tools.

Виртуальная машина

Интересный аспект работы движка заключается в том, что объекты в игре используют шаблонную систему классов. Для каждого мира в файле данных есть блок, определяющий набор классов объектов. Например, на показанном выше первом уровне обе двери аварийного выхода имеют тип класса 0x4f. Реверс-инжиниринг кода классов объектов привёл к следующей функции:

image

Пропуская пока альтернативный путь к адресу 0x142a2, этот код использует si как индекс в структуре массива классов объектов (каждый шаблон класса состоит из 0x15 байт). Слово в смещении 0x3 — это структура, используемая в качестве адреса в сегменте ES. Сегмент ES содержит все данные блока шаблона класса объекта. Затем код переходит в цикл по адресу 0x142a6. Этот цикл получает следующий байт из сегмента ES и использует его как индекс таблицы функций. На каждом шаге цикла bx содержит адрес в сегменте ES, а si содержит текущий код операции (опкод).

Интересно заметить, что в цикле используется безусловный переход, и поэтому он никогда не завершается. По крайней мере, при нормальном выполнении программы. Похоже, что у каждого класса объекта есть какая-то связанная с ним программа, основанная на опкодах, которая интерпретируется этой функцией. Первоначально я просто исследовал некоторые из функций в таблице вызовов. Вот как выглядит первая в режиме графов IDA:

image

Анализ стека IDA здесь выполнить не удаётся, и на то есть причины. pop ax в качестве первой команды функции — это довольно странное поведение. Команда вызова x86 загружает указатель команды (ip) в стек, а команда ret извлекает его, чтобы вернуть вызывающей функции. Команда pop здесь фактически отбрасывает адрес возврата. Это значит, что следующая команда ret перейдёт назад на два кадра стека вместо одного. Этот опкод выполняет выход из бесконечного цикла интерпретатора.

Чтобы понять, как на самом деле реализован опкод, нам нужно переключиться в линейный режим IDA:

image

Я прокомментировал имена функций их соответствующими номерами опкодов, чтобы эта часть кода была понятней. Здесь довольно умно выполняется повторное использование кода. Легче всего начать с изучения опкода 0x03. Вспомним, что в обрабатывающем опкоды цикле есть данные блоков классов объектов, загруженных в сегмент ES, а bx используется в качестве указателя команды виртуальной машины. Таким образом, опкод 0x03 — это команда безусловного перехода. Она загружает слово по адресу текущего указателя команд, устанавливает на него указатель команд, а затем возвращается к циклу обработки опкодов.

Работая в обратном порядке, опкод 0x05 пропускает следующее слово-операнд и сохраняет следующий адрес в массив. Реверс других функций показывает, что word_28522 — это индекс текущего экземпляра объекта, то есть этот опкод хранит один адрес для каждого объекта на уровне. Затем он восстанавливает значение bx и проходит в коде до опкода 0x03 (переход). Поэтому этот опкод, похоже, является очень ограниченной командой вызова со всего одним уровнем стека.

Опкод 0x00 сохраняет текущий указатель функции в глобальный массив. Заметьте, что он отличается от опкода 0x05, который сохраняет адрес после операнда адреса вызова. Затем он переходит к обработчику опкодов вызовов. Однако, команда не будет выполнена из-за первой команды pop. Вместо этого вызывающий цикл выполнит выход. Адрес, сохраняемый этой командой, используется как альтернативный путь входа к циклу вызова функций, который я не стал рассматривать выше. Здесь опкод 0x00 используется как что-то вроде выхода из сопрограммы (coroutine). Он сохраняет текущее положение в программе и выходит из цикла обработки опкодов, позволяя игровому движку выполнить другие задачи: обновить графику, получить данные ввода пользователя и т.д., возвращаясь к тому, откуда выполнила выход программа виртуальной машины. Это позволяет программе виртуальной машины выполнять сложные задачи без простоя остальной части игрового движка.

Дизассемблинг опкодов

На этом этапе у меня уже было общее представление о том, как работает обработчик опкодов виртуальной машины. Изучив таблицу вызовов функций, я нашёл там примерно 215 реализованных опкодов, поэтому вместо их непосредственной обратной разработки по порядку я начал писать простой скрипт, чтобы декомпилировать программу для нужного класса объектов. Таким образом я мог сосредоточиться только на опкодах, вызываемых объектами на первом уровне.

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

Вот пример простого опкода:

image

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

image

Этот опкод сохраняет текущее значение регистра общего назначения по адресу в сегменте данных (DS), указанному операндом-словом. Программы в файле данных игры используют этот опкод для записи в определённые части игровых данных, таких как слоты инвентаря викингов. Несколько смещений DS также используются как дополнительные временные значения, когда их требуется больше одного. Например, смещения 0x0206 и 0x0208 часто используются как временные координаты x и y объекта.

Этот опкод и противоположный ему опкод 0x53, считывающий из DS в регистр общего назначения, интересны тем, что они могут считывать и записывать в DS любые игровые данные, а не только те, которые назначены создателями оригинальной игры. Это даёт нам интересные перспективы расширения оригинальной игры.

Более сложным является опкод 0x14:

image

На первый взгляд, его реверсирование не должно вызвать проблем. Он получает байтовый операнд и вызывает пару подфункций. Затем он получает ещё один байтовый операнд и снова вызывает те же подфункции. Но если взглянуть на первую подфункцию (sub_15473), то дело немного усложняется:

image

Анализ стека IDA выполнить снова не удаётся, и он показывает четыре выхода из этого блока, который я обрезал, потому что все они неверны. На самом деле происходит вот что: первый байтовый операнд передаётся в эту функцию в ax, который затем используется как индекс таблицы переходов. Хотя для ax выполняется операция AND с 7, на самом деле в таблице переходов есть всего четыре действительных значения. Взглянув на тип 0 в таблице переходов, мы увидим следующее:

image

Здесь загружается непосредственный операнд-слово. Этот фрагмент функции выполняет retn, который возвращается из обработчика опкода 0x14 верхнего уровня. Нетрудно заметить, почему у IDA возникают проблемы с правильным дизассемблированием этого кода.

Другие записи в таблице переходов различными способами загружают значения, а затем каждая из них снова делает retn. Тип 1 берёт байтовый операнд, используемый в качестве индекса поля объекта. Объект имеет поля для таких значений, как смещения x и y, флаги и т.д. Тип 2 получает операнд-слово и загружает значение из DS (как опкод 0x53 из примера выше). Тип 3 получает байтовый операнд и загружает поле целевого объекта для выполнения действий, например, для распознавания столкновений.

Вернёмся обратно к обработчику опкода 0x14 верхнего уровня. Здесь следующей вызывается sub_15470. Она всего на три байта впереди первой вызванной подфункции. И в этом снова есть есть умная оптимизация кода. Эта подфункция просто сдвигает ax вправо на 3, а затем переходит к коду, который мы только что реверсировали выше. Итак, первый байтовый операнд опкода 0x14 используется для кодирования того, как получаются два последующих аргумента. Реверсирование обработки второго байтового операнда показывает, что он работает таким же образом.

После получения всех его операндов (переменной длины) обработчик опкода 0x14 вызывает sub_13809. Я не буду вставлять сюда код, потому что функция довольно большая и вызывает множество других подфункций, каждая из которых тоже объёмна. Это один из тех случаев, когда я не заморачивался реверсированием действий функции, потому что позже они станут понятны из контекста.

Виртуальная машина CISC

Команды в виртуальной машине The Lost Vikings имеют переменную длину. В случае опкода 0x14 и других похожих опкодов определение длины требует декодирования некоторых операндов. Наличие команд переменной длины означает, что программу нужно дизассемблировать итеративно, как минимум определяя количество операндов для каждого нового опкода в программе. Если бы все опкоды имели фиксированную длину, то можно было бы пропускать неизвестные опкоды.

Существуют также команды, выполняющие стандартные операции, используемые многими программами. Например, многие программы выполняют суммирование или вычитание из текущего положения объекта на основании его направления. Это можно закодировать как несколько команд, но опкод 0x91 получает единственный операнд-слово как смещение в DS, а затем прибавляет или вычитает из него текущее значение временного регистра на основании текущего горизонтального направления объекта (флаг 0x0040). Следующий псевдокод демонстрирует работу опкода 0x90.

if (this.flags & 0x0040)     DS[operand] -= var; else     DS[operand] += var;

Благодаря этому на кодирование требуется гораздо меньше байтов, особенно потому, что эта операция выполняется несколькими программами. Во времена DOS, когда пространство на диске и вычислительная мощь были в дефиците, это имело большой смысл. Для современного реверс-разработчика это просто добавляет несколько новых опкодов, в которых нужно разобраться.

Дизассемблер

Я написал простой скрипт на Python, который может дизассемблировать программы виртуальной машины в то, что смутно напоминает C. Он линейно декодирует команды программы, сохраняя адреса переходов и вызовов. Когда он натыкается на функцию, останавливающую или перенаправляющую выполнение кода, например, возврат или безусловный переход, он останавливается и проверяет другие непосещённые адреса в программе.

В программах виртуальной машины использовали хитрости оптимизации, похожие на применённые в двоичном коде игры. Например, некоторые пути выхода из подпроцедур в программах виртуальной машины используются повторно с помощью перехода к ним из других подпроцедур. Дизассемблер справляется с этим, декодируя команды дважды, поэтому две подпроцедуры могут иметь код с одинаковыми адресами.

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

var = 0x1000; obj->field_10 = var;

Дизассемблер реорганизует их следующим образом:

obj->field_10 = 0x1000;

Дизассемблер не пытается перестроить блоки кода в такие конструкции, как if или while, поэтому получающиеся программы выглядят как спагетти-код. Некоторые большие программы сами по себе можно сделать полноценными проектами для обратной разработки.

Полная программа

Я начал с программы для башни с пушкой в верхней левой комнате первого уровня (класс объекта 0x04). Мне показалось, что это должна быть довольно простая программа. Башня с пушкой стреляет снарядами с определённым интервалом, и викинги не могут её уничтожить. Ниже показана программа, полученная после обработки моим дизассемблером. Над каждым опкодом есть комментарий, показывающий в квадратных скобках адрес, в кавычках — опкод, после чего идут операнды.

Программа башни выглядит вот так:

main_374d: {     // [374d] (19) 37bc     this.field_21 = 0x37bc;     this.field_22 = 0x0001;      // [3750] (97) 00 01     if (0x0001 & (1 << 0))         var = 0x0001;      // [3753] (00)     this.save_state();      // [3754] (9c) 18 08     if (!(var))         this.db_flags &= ~(1 << 12);  label_3757:     // [3757] (2f)     vm_func_2f();      // [3758] (00)     this.save_state();      // [3759] (01)     nop();      // [375a] (51) 0000     // [375d] (8c) 30 3798     if (this.field_30 != 0x0000)         call sub_3798;      // [3761] (52) 1c     // [3763] (77) 0000 3757     if (this.update_time != 0x0000)         jump label_3757;      // [3768] (19) 37c8     this.field_21 = 0x37c8;     this.field_22 = 0x0001;      // [376b] (52) 1e     // [376d] (57) 0206     g_tmp_a = this.xoff;      // [3770] (51) 0004     // [3773] (91) 0206     if (this.flags & 0x0040)         g_tmp_a -= 0x0004;     else         g_tmp_a += 0x0004;      // [3776] (52) 20     // [3778] (57) 0208     g_tmp_b = this.yoff;      // [377b] (51) fff8     // [377e] (5a) 0208     g_tmp_b += 0xfff8;      // [3781] (51) 001e     // [3784] (56) 1c     this.update_time = 0x001e;      // [3786] (02) 3030     vm_func_02(0x3030);      // [3789] (14) 12 0206 0208 00 0000 0000 05     new.x = g_tmp_a;     new.y = g_tmp_b;     new.unknown_a = 0x0000;     new.unknown_b = 0x0000;     new.unknown_c &= 0x801;     new.type = 0x05;     spawn_obj(new);      // [3795] (03) 3757     jump label_3757;  }  sub_3798: {     // [3798] (51) 000a     // [379b] (73) 30 37a5     if (this.field_30 == 0x000a)         jump label_37a5;      // [379f] (51) 0000     // [37a2] (56) 30     this.field_30 = 0x0000;      // [37a4] (06)     return;  label_37a5:     // [37a5] (52) 32     // [37a7] (69) 0a 37ba     if (this.argument < this.field_32)         jump label_37ba;      // [37ab] (52) 32     // [37ad] (5c) 0a     this.argument -= this.field_32;      // [37af] (51) 0000     // [37b2] (56) 30     this.field_30 = 0x0000;      // [37b4] (51) 0000     // [37b7] (56) 32     this.field_32 = 0x0000;      // [37b9] (06)     return;  label_37ba:     // [37ba] (0e)     vm_func_0e();      // [37bb] (10)     this.update(); }

Здесь можно заметить следующее:

  • Функция this.save_state() — это опкод 0x00. Она используется как результат, позволяющий программе сохранить место, в котором она находилась, и вернуться к коду игрового движка.
  • Мой дизассемблер присваивает некоторым полям объекта придуманные мной имена. В этой программе видны имена xoff, yoff, flags и update_time.
  • Также даны названия известным смещениям DS. g_tmp_a и g_tmp_b являются смещениями DS 0x0206 и 0x0208.
  • Здесь используется опкод 0x14. Я дал ему имя spawn_obj(), потому что из контекста его применения понятно, что он используется для создания нового объекта пули при каждом выстреле башни.
  • Опкод 0x19 присваивает значения полям 0x21 и 0x22 объекта. Примечательно, что полю 0x21 присваивается значение, напоминающее адрес программы. Читая код и экспериментируя, я выяснил, что этот адрес на самом деле является смещением для команд в маленькой вторичной виртуальной машине, которая управляет отображением графики объектов. Пока я не реверсировал этот код.

Рефакторинг программы помогает понять, что в ней происходит:

main_374d: {     /* Инициализация башни */     this.set_graphics_prog(0x37bc);      if (0x0001 & (1 << 0))         var = 0x0001;      this.save_state();      if (!(var))         this.db_flags &= ~(1 << 12);  label_3757:     /*      * Основной цикл башни. Возвращает результат в основной      * движок игры при каждой итерации.      */     while (1) {         vm_func_2f();         this.save_state();          /*          * Предназначение этой части неизвестно. Полю this.argument          * можно присвоить экземпляр объекта в заголовке          * уровня. Возможно, она используется для небольшого          * изменения поведения некоторых башен.          */         if (this.field_30 != 0x0000) {             if (this.field_30 != 0x000a) {                 this.field_30 = 0x0000;             } else {                 if (this.argument < this.field_32) {                      vm_func_0e();                      this.update();                 } else {                     this.argument -= this.field_32;                     this.field_30 = 0x0000;                     this.field_32 = 0x0000;                 }             }         }          /* Если время обновления update_time достигает нуля, то выпускается пуля */         if (this.update_time == 0x0000) {             this.set_graphics_prog(0x37c8);                           /* Сброс времени обновления */             this.update_time = 0x001e;                      vm_func_02(0x3030);                                        /*              * Создание объекта пули в четырёх пикселях от передней части              * башни и в восьми пикселях над центральной линией.              */             if (this.flags & OBJ_FLAG_FLIP_HORIZONTAL)                 new.x -= 4;             else                 new.x += 4;             new->y = this.yoff - 8;             new->unknown_a = 0x0000;             new->unknown_b = 0x0000;             new->unknown_c &= 0x801;             new->type_d = 0x05;             spawn_obj(new);         }     } }

Стоит заметить, что в программе всё ещё есть части и опкоды, предназначение которых я не определил, но поведение башни достаточно понятно. Поле update_time объекта используется для кодирования скорости стрельбы башни. Программа сама не занимается уменьшением этого значения (по крайней мере, очевидным образом). Возможно, эту задачу выполняет один из неизвестных опкодов или какая-нибудь часть основного игрового движка.

Взлом игры

Разумеется, возможность изучить исходные программы, используемые игрой, интересна сама по себе. Но самое любопытное в способе реализации виртуальной машиной поведения объекта заключается в том, что мы можем изменять его или создавать новые программы. Это было бы гораздо проще, если написать компилятор (о нём я расскажу ниже), но пока мы можем лишь менять значения в hex-редакторе вручную.

Повторюсь — я выбрал для модификации программу башни с пушкой, потому что она довольно проста. Сначала я решил изменить тип объекта, которым стреляет башня. Успех оказался переменным. Попытки реализовать стрельбу одними объектами, например, зелёным инопланетянином, приводили к «вываливанию» или зависанию игры. Выбор других объектов приводил к тому, что башня ничем не стреляла. Мне удалось заставить её стрелять огненными стрелами (в обычной игре это бонус, который может использовать один из викингов). Также довольно просто увеличить темп стрельбы пушки уменьшением значения поля update_time.

Вторая сделанная мной модификация стала немного более амбициозной: я заставил башню вместо стрельбы двигаться. Опкод (0x14) для создания пули довольно длинный:

14 12 0206 0208 00 0000 0000 05

Виртуальная машина предоставляет возможность использования однобайтовой команды nop (0x01) (что довольно благородно с её стороны), которую можно использовать для замены этой команды. Команды выше уже установили DS[0x0206] в текущее положение по оси x башни плюс-минус 4, в зависимости от её направления. Поэтому мне просто нужно добавить команды для присвоения этого значения текущему положению x. Для этого требуются всего две команды:

53 0206   // var = DS[0x0206]; 56 1e     // this.x = var;

Благодаря этому изменению башня может двигаться, но её ничего не останавливает. Я пробовал реализовать распознавание столкновений и поворот обратно, но правильной работы добиться не удалось. Требуется более глубокая обратная разработка.

Я собрал небольшое видео моих модификаций:

Также я выложил в открытый доступ свой дизассемблер:

https://github.com/RyanMallon/TheLostVikingsTools/blob/master/dissassembler/lv_vm_disasm.py

Рекомпиляция игры

В предыдущем разделе мы говорили о реверс-инжиниринге виртуальной машины, используемой для реализации объектов в Lost Vikings.

Раньше я тестировал некоторые из опкодов эмпирически, ручным патчингом исходных блоков данных в шестнадцатеричном редакторе. Такой подход работал, но был очень монотонным и не очень хорошо масштабировался для экспериментов с большими программами или с программами, содержащими циклы и переходы. Выше я предположил, что создание простого языка и компилятора будет полезно для дальнейшей обратной разработки виртуальной машины. Поэтому я его написал.

Построение компилятора

Для помощи в обратной разработке виртуальной машины и удобства создания новых программ я решил написать компилятор. Чтобы не усложнять, я выбрал однопроходный метод рекурсивного спуска.

Дизайн компилятора в основном позаимстован у компиляторов типа PL/0, которые часто изучают на университетских курсах о компиляторах (см. PL/0). Не буду вдаваться в подробности написания базового компилятора, потому что в Интернете достаточно информации об этом. В сущности, компилятор получает исходную программу, выполняет лексический анализ для создания потока токенов, а затем производит синтаксический анализ программы, генерируя код.

Lost Vikings C

Я создал очень простой C-подобный язык для реализации на нём программ объектов. Одно из преимуществ использования C-подобного языка заключается в том, что можно использовать существующий препроцессор C для поддержки определения констант и макросов. Язык не позволяет определять переменные, программист ограничен полями объектов и глобальными переменными, определяемыми виртуальной машиной. Встроенные функции обеспечивают поддержку генерации опкодов для операций получения результатов и создания объектов. Язык имеет незначительные отличия от C, просто чтобы сохранить простоту лексического и синтаксического анализаторов.

Снова возьмём для примера объект башни с пушкой из первого уровня, который мы модифицировали выше. Реализовать стреляющую стрелами башню на Lost Vikings C можно следующим образом:

#include "vikings.h"  #define timer      field_30 #define DELAY_TIME 40  function main {     call set_gfx_prog(0x37bc);      this.timer = 0;     while (this.timer != 0xffff) {         call update_obj();         call yield();          if (this.timer != 0) {             this.timer = this.timer - 1;          } else {             // Вычисление смещения снаряда на основании             // направления башни.             if (this.flags & OBJ_FLAG_FLIP_HORIZ) {                 g_tmp_a = this.x - 12;             } else {                 g_tmp_a = this.x + 12;             }             g_tmp_b = this.y - 12;              // Обновление анимации башни             call set_gfx_prog(0x37c8);              // Создание стрелы             call spawn_obj(g_tmp_a, g_tmp_b, 0, 0, 7);              // Задержка перед следующим выстрелом             this.timer = DELAY_TIME;         }     } }

Каждому объекту нужен основной цикл, который никогда не должен завершаться. Основной цикл должен вызывать функцию yield(), чтобы позволить выполнять выход циклу обработки виртуальной машины. В противном случае движок игры «зависнет». Вызов update_obj() делает то, что следует из его названия.

Использование многих полей объектов относятся к конкретному объекту. В нашем случае я использовал поле field_30 в качестве таймера для управления скоростью стрельбы башни. Когда таймер достигает нуля, башня стреляет, а затем переустанавливает таймер.

Единственное, что осталось — сгенерировать код для этой программы.

Генерирование кода

Виртуальная машина The Lost Vikings не является идеальной средой для простого компилятора. Компиляторы типа PL/0 обычно генерируют код для абстрактной стековой машины, поэтому простое выражение типа:

this.x = this.y + 1;

сгенерирует вот такой код:

push this.y    ; запись this.y в стек push 1         ; запись 1 в стек add            ; извлечение двух верхних значений, суммирование и запись результата pop this.x     ; извлечение результата в this.x

Однако у виртуальной машины The Lost Vikings нет стека. У неё есть простой временный регистр, поля объектов и глобальные переменные. Показанная выше программа транслируется в виртуальную машину Lost Vikings следующим образом:

52 20          ; var = this.y 56 1e          ; this.x = var 51 0001        ; var = 0x0001 59 1e          ; this.y += var

Для неё гораздо сложнее генерировать код при компилировании из языка общего назначения. Возможно, потребуется генерировать в первом проходе промежуточное представление, а затем во втором проходе изменять порядок команд и т.д. для генерирования конечного кода.

Абстрактная машина

Я пришёл к следующему решению: создание абстрактной стековой машины, для которой можно удобно компилировать поверх виртуальной машины Lost Vikings. Это возможно сделать благодаря опкодам для загрузки и хранения глобальных переменных. Эти опкоды получают в качестве своих операндов смещения в сегменте данных (DS) игры. Это позволяет использовать опкоды для загрузки и хранения любого произвольного адреса в DS.

Компилятор генерирует код, помещая фальшивый стек в верхнюю часть (0xf004) сегмента данных. Два адреса в основании фальшивого стека зарезервированы под специальный регистр: 0xf000 — это нулевой регистр, а 0xf002 — флаговый регистр, используемый для сравнения. Приведённое выше выражение теперь можно скомпилировать так:

52 20        ; var = this.x 57 f004      ; ds[f004] = var 51 0001      ; var = 0x0001 57 f006      ; ds[f006] = var 53 f006      ; var = ds[f006] 5a f004      ; ds[f004] += var 53 f004      ; var = ds[f004] 56 1e        ; this.y = var

Несмотря на не очень высокую производительность, этот код очень просто генерировать. Моя цель — создание компилятора для тестирования небольших программ, чтобы определить, что делают опкоды или их сочетания. Пока меня не очень волнует эффективность, с точки зрения и времени, и размера кода.

Вторая проблема в создании языка для оригинальной виртуальной машины заключается в том, что многие опкоды выполняют условный вызов функций. Например, опкод 0x1a проверяет, столкнулся ли текущий объект с викингом, и если это так, то он выполняет команду вызова. Желательнее, чтобы программист мог сам решать, нужно ли использовать вызов или переход (например, конструкцию с if).

Я реализовал это, сгенерировав код для стандартной вспомогательной функции, устанавливающей флаг и выполняющей возврат. При генерировании кода для опкода, выполняющего условный вызов, сначала сбрасывается флаговый регистр. Если опкод выполняет вызов, то флаговый регистр устанавливается. Затем можно проверять флаг для выполнения вызова перехода. Например, следующий код:

if (call collided_with_viking(0x01)) {     this.x = 1; }

генерирует вот такой код:

[c009] 51 0000      ; var = 0x0000 [c00c] 57 f002      ; ds[f002] = var, сброс флагового регистра [c00f] 1a 01 c0a5   ; if (collided_with_viking(0x01)) call c0a5 [c013] 53 f002      ; var = ds[f002] [c016] 74 f000 c026 ; if (var == ds[f000]) goto c026 [c01b] 51 0001      ; var = 0x0001 [c01e] 57 f004      ; ds[f004] = var [c021] 53 f004      ; var = ds[f004] [c024] 56 1e        ; this.field_x = var [c026] ...          ; следующая команда после блока if  ; Установка флаговой вспомогательной функции [c0a5] 51 0001      ; var = 0x0001 [c0a8] 57 f002      ; ds[f002] = var, установка флагового регистра [c0ab] 06           ; return

Нулевой регистр используется здесь для сравнения его со значением флага. В начале каждой программы компилятор генерирует команды для сброса нулевого регистра.

Патчинг программ

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

Всё это в теории работает, но на деле есть одна небольшая проблема. Игра хранит программы виртуальной машины в дополнительном сегменте (ES) в месте, указываемом с помощью API выделения памяти DOS. Функция выделения памяти выглядит так:

image

Вызов выделения памяти для виртуальной машины выглядит так:

image

То есть для программ выделяется 0xc000 байт (48 КБ). Проблема в том, что блок для программ мира космического корабля уже занимает 48972 байт, то есть в его конце остаётся всего 180 байт для патча в новой программе. Не так уж много, когда работаешь с компилятором, генерирующим очень избыточный код.

Я обнаружил эту проблему, когда пытался скомпилировать большую программу. Игра начала вести себя хаотично, она или «зависала», или случайным образом пропадала башня с пушкой. Такие ошибки довольно сложно отследить, потому что это может быть багом в моём компиляторе или сгенерированном коде, проблемой недопонимания принципа работы какого-то опкода, или багом/ограничением оригинальной игры.

Быстрым решением является патчинг двоичного кода игры для расширения размера памяти, выделяемого под программу. Увеличение выделенного размера до 0xd000 байт (52 КБ) не вызывает проблем и даёт достаточно места для экспериментов с простыми программами. Я добавил команды для этого в мой компилятор.

Эмпирическое реверсирование

Целью создания компилятора было упрощение обратной разработки виртуальной машины. Для тестирования нового опкода необходимо добавить запись в словарь функций в классе генератора кода и создать функцию генерирования команд для опкода. Например, одним из первых экспериментов была работа с 0x41, который используется в нескольких дизассемблированных программ, таких как объекты кнопок/значков с вопросительным знаком.

Его запись словаря в компиляторе изначально выглядела так:

"vm_func_41" : (False, 4, emit_builtin_vm_func_41),

Кортеж указывает, будет ли функция возвращать значение, количество передаваемых ей аргументов и функцию генератора кода. Функции помечаются как возвращающие значение, если опкод выполняет условный вызов или переход.

После изучения опкода в IDA я знал, что опкод 0x41 выполняется безусловно и получает четыре аргумента. Его аргументы используют кодирование типа переменной, который я описывал в начале статьи. Он генерирует следующую функцию:

def emit_builtin_vm_func_41(self, reg_list):     operands = self.pack_args(reg_list, 4)     self.emit(0x41, *operands)

Вспомогательная функция pack_args обеспечивает упаковку аргументов типа переменной.

Теперь её можно вызвать в короткой тестовой программе. Для тестирования я использую функцию collided_with_viking (опкод 0x1a), поэтому тестируемый опкод запускается только при касании викингом башни. Заметьте, что функция collided_with_viking получает единственный байтовый операнд, назначение которого я не знаю, но значение 0x01 вполне для него подходит. Также я использую как одноразовый триггер поле field_32 объекта, чтобы тестируемый мной опкод срабатывал только при первом касании викингом объекта.

Моя тестовая программа выглядит так:

function main {     call set_gfx_prog(0x37bc);      this.field_32 = 0;     while (this.field_32 != 0xffff) {         call update_obj();         call yield();          if (call collided_with_viking(0x01)) {             if (this.field_32 == 0) {                 call vm_func_41(1, 1, 1, 1);                 this.field_32 = 1;             }         }     } }

В результате в игре возникает вот такой графический «глитч»:

image

Похоже, что опкод используется для отображения диалогового окна, но непонятно, почему возникает эта графическая ошибка. Если посмотреть на некоторые дизассемблированные программы из оригинальной игры, то видно, что за опкодом 0x41 обычно следуют неизвестные опкоды 0xcb и 0x42, ни один из которых не получает мои аргументы. При добавлении встроенных программ для этих опкодов в компиляторе и повторном запуске игры отображается диалоговое окно, ожидающее нажатия кнопки, а затем окно закрывается. Итак, опкод 0x41 показывает диалоговое окно, опкод 0xcb дожидается нажатия кнопки, а опкод 0x42 удаляет окно.

Дальнейшие эксперименты с опкодом 0x41 показывают, что его первый аргумент — это индекс строки диалога. Строки хранятся в двоичном файле игры, что неудобно для моддинга. Второй аргумент по-прежнему неизвестен, а третий и четвёртый управляют смещением по x и y относительно объекта, рядом с которым открывается диалоговое окно.

Хорошим примером того, почему такое экспериментальное реверсирование полезно, может стать опкод 0xcb, рассмотренный в IDA:

image

Хотя эта функция и небольшая по размеру, и назначение её понятно (она изменяет глобальные переменные), совсем не очевидно, какое влияние она оказывает на игровой движок. Изучение перекрёстных ссылок для каждой из глобальных переменных в IDA тоже не особо помогает. Каждая из этих глобальных переменных используется во множестве мест, и ни у одного из них назначение не становится сразу очевидным. Я мог бы потратить кучу времени в IDA, пытаясь выяснить, что изменение этих глобальных переменных говорит игровому движку подождать нажатия кнопки в следующем игровом цикле. Эмпирическое тестирование позволило выяснить это очень быстро. Недостаток заключается в том, что я всё ещё не знаю точного назначения каждой глобальной переменной в функции.

Интересные поля

Одна из интересных особенностей, обнаруженных мной при экспериментах с компилятором, заключается в том, как объекты могут ссылаться друг на друга. Из первоначального анализа я уже знал, что поддержка ссылок в коде есть, но компилятор помог мне быстро разобраться в подробностях.

Поле 0x3c каждого объекта определяет текущий целевой объект. Некоторые опкоды, например collided_with_viking (0x1a), автоматически присваивают значение этому полю, но его также можно указать вручную. Викинги — это всегда объекты 0 (Балеог), 1 (Эрик) и 2 (Олаф). После назначения целевого объекта, им можно управлять через целевые поля. Каждый опкод имеет свои варианты изменения поля текущего объекта (целевого поля). Например, опкод 0x59 добавляет временный регистр к полю в текущем объекте, а опкод 0x5b добавляет временный регистр к полю в целевом объекте.

Другие объекты могут управлять полями в объектах викингов, чтобы контролировать их поведение. Например, поле 0x32 для викингов — это количество ожидаемого урона. Объект может наносить урон викингу, прибавляя значение к этому полю. Сами викинги частично реализованы программами в виртуальной машине. При следующем запуске программы викинга она проверяет поле ожидаемого урона и наносит соответствующий урон.

Интерес представляют также 0x12 и 0x14, которые являются скоростями для всех объектов по x и y. До написания компилятора я думал, что объекты перемещаются или добавлением/вычитанием из их полей смещения по x и y (0x1e и 0x20) или, возможно, использованием похожего на функцию опкода. Однако игра использует пару полей скорости. Некоторые объекты, например, викинги, автоматически изменяют свою скорость. Поэтому, например, если объект присваивает викингам скорость, поднимающую их вверх, то в последующие циклы игры викинг соответственно меняет собственную скорость, чтобы снова упасть вниз.

Похоже, что в объектах викингов не хватало полей для хранения всего, поэтому каждый из четырёх слотов инвентаря викингов является глобальной переменной. Объект может проверять, равна ли соответствующая глобальная переменная нулю, и давать викингу предмет, напрямую присваивая значение глобальной переменной слота инвентаря.

Расширенный взлом игры

Для экспериментов с виртуальной машиной и демонстрации её гибкости я написал немного более сложную программу для башни с пушкой на первом уровне. Башня проверяет, какой из викингов её коснулся, и реагирует по-разному. Если башни касается Эрик, она даёт ему предметы. Если касается Олаф, он отталкивается в воздух. Когда касается Балеог, башня меняет направление. Вот как это выглядит в действии:

Такой уровень гибкости в игре, созданной в начале 90-х, довольно сильно впечатляет. Исходный код этой программы я добавил в папку examples компилятора.

Работа на будущее

По-прежнему остаётся довольно много опкодов, полей и глобальных переменных, назначение которых я не понимаю. Компилятору недостаёт некоторых функций, например, пока он поддерживает только операторы сравнения == и !=, и в нём отсутствует большинство побитовых операторов. Он немного громоздок в работе, потому что требует отдельной распаковки блока оригинальной программы из файла данных игры, патчинга и повторной упаковки файла данных.

Все инструменты являются общественным достоянием (public domain), их открытый исходный код выложен на github: https://github.com/RyanMallon/TheLostVikingsTools.

Олдскульная Blizzard: спрайты, карты и палитры

Я занимался реверс-инжинирингом двух ранних игр Blizzard под DOS, The Lost Vikings и Blackthorne. Выше я писал о виртуальной машине, которая используется в The Lost Vikings для реализации игровых объектов. Blizzard выложила The Lost Vikings и Blackthorne в свободный доступ на Battle.net. Стоит заметить, что Blizzard создавала свои ранние игры, в том числе The Lost Vikings, под именем Silicon and Synapse. Blackthorne стала первой игрой, выпущенной под брендом Blizzard.

В этом разделе я расскажу о том, как в двух играх реализованы форматы спрайтов и тайловый движок. У этих двух игр очень похожие движки, причём в Blackthorne после The Lost Vikings внесены некоторые улучшения. В дальнейшем я буду называть игровой движок обеих игр «движком Blizzard».

Игры хранят все свои данные в одном пакетном файле данных, который называется DATA.DAT. Пакетный файл — это архив, в котором содержатся блоки спрайтов, уровней, звуков и т.д. Большинство блоков пакетного файла сжато разновидностью алгоритма сжатия LZSS.

Я создал несколько простых утилит, которые можно использовать для просмотра спрайтов, уровней и т.д. из The Lost Vikings и Blackthorne. Эти утилиты можно скачать с GitHub (ссылка выше). В этот раздел я добавил примеры кода. Все скриншоты в разделе сделаны с помощью этих утилит.

image

./sprite_view DATA.DAT -fraw -s -u -w344 -p 0x17b 0x17c ./level_view --blackthorne DATA.DAT 1 -h0x6e

Графический режим

В обеих играх используется популярный режим VGA Mode X. Mode X — это 256-цветный плоскостной графический режим с разрешением 320×240. «Плоскостной» означает, что вместо линейного расположения пикселей, как в режиме VGA Mode 13h с разрешением 320×200, они разделены на набор плоскостей. Плоскостная графика изначально была разработана для ускорения обработки графики. Она позволяет нескольким чипам памяти хранить отдельные плоскости и передавать их паралелльно. В этой статье на Shikadi.net есть хорошее объяснение работы плоскостной графики.

В режиме Mode X используется четыре плоскости. Первая плоскость хранит пиксели 0, 4, 8 и т.д. Вторая плоскость хранит пиксели 1, 5, 9 и т.д. Поэтому спрайт 8×2 хранится не линейно, как пиксели:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15

Плоскостной режим Mode X хранит их так:

0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15

В основном я выполнял обратную разработку форматов спрайтов, изучая данные в блоках пакетного файла, а не код рендеринга в IDA. Я обладаю очень рудиментарным пониманием программирования под DOS и VGA, а код рендеринга старых игр обычно содержит множество хитростей для оптимизации и умное применения ассемблера, которые трудно понять (мне).

Формат «сырых» спрайтов

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

for (plane = 0; plane < 4; plane++) {     for (i = 0; i < (sprite_width * sprite_height) / 4; i++) {         offset = (i * 4) + plane;         y = offset / sprite_width;         x = offset % sprite_width;          pixel = *sprite++;         dst[((dst_y + y) * dst_width) + (dst_x + x)] = pixel;     } }

Дополнительно можно выбрать индекс цвета, используемого в качестве прозрачного. «Сырые» спрайты одинаковой ширины и высоты всегда имеют одинаковый объём данных, но они не особо эффективно используют место. Это особенно справедливо для игрового движка Blizzard, потому что спрайты используют всего шестнадцать цветов (это всегда значения от 0x0 до 0xf), поэтому четыре байта на пиксель тратятся впустую. Шестнадцать цветов на спрайт используются потому, что это позволяет применить хитрые трюки с палитрой, которые я объясню ниже.

Формат распакованных спрайтов/спрайтов с маской

Второй формат спрайтов позволяет использовать прозрачность, не жертвуя индексом цвета, но ценой является более сложный алгоритм отрисовки и немного больший размер данных. В разработанных мной утилитах я назвал этот формат «распакованным» (unpacked), но, возможно, лучше назвать его «с маской».

В этом формате спрайтов каждому набору из восьми пикселей предшествует байт маски, указывающий, какие пиксели нужно отрисовывать. Прозрачные пиксели по-прежнему кодируются значением 0x0, но пропускаются при отрисовке. Это позволяет использовать индекс цвета 0x0 в качестве дополнительного цвета.

Алгоритм отрисовки таких спрайтов выглядит следующим образом:

for (plane = 0; plane < 4; plane++) {     x = plane;     y = 0;     for (i = 0; i < (sprite_width * sprite_height) / 4 / 8; i++) {         mask = *sprite++;         for (bit = 7; bit >= 0; bit--) {             pixel = *sprite++;              if (mask & (1 << bit))                 dst[((dst_y + y) * dst_width) + (dst_x + x)] = pixel;              x += 4;             if (x >= sprite_width) {                 y++;                 x = plane;             }         }     } }

Различные собираемые предметы The Lost Vikings являются распакованными спрайтами/спрайтами с маской 16×16. Их можно просмотреть с помощью:

./sprite_view DATA.DAT -l2 -funpacked -w16 -h16 0x12f

image

Упакованные спрайты 32×32

Последний формат спрайтов используется только в The Lost Vikings и оптимизирован для отрисовки спрайтов 32×32. Как и в распакованном формате, каждому набору пикселей предшествует байт маски, определяющий отрисовываемые пиксели. Однако упакованный формат не хранит прозрачные пиксели и упаковывает по два пикселя в каждый байт, потому что используется всего шестнадцать цветов. Если количество отрисовываемых пикселей нечётно, то последний полубайт равен нулю.

Этот формат использует место более эффективно по сравнению с двумя предыдущими, но имеет более сложный алгоритм отрисовки и переменную длину спрайтов. Блоки пакетного файла, содержащие спрайты упакованного формата, начинаются с заголовка из 16-битных значений, определяющих смещение каждого спрайта.

Алгоритм отрисовки упакованных спрайтов таков:

for (plane = 0; plane < 4; plane++) {     for (y = 0; y < 32; y++) {         num_pixels = 0;         mask = *sprite++;         for (bit = 7; bit >= 0; bit--) {             if (mask & (1 << bit)) {                 pixel = *sprite;                 if (num_pixels & 1)                     sprite++;                 else                     pixel >>= 4;                 pixel &= 0xf;                  x = ((7 - bit) * 4) + plane;                 dst[((dst_y + y) * dst_width) + (dst_x + x)] = pixel;                  num_pixels++;             }         }         if (num_pixels & 1)             sprite++;     } }

В процессе написания поста я обнаружил в заголовке второго уровня The Lost Vikings приведённый ниже спрайт. В нём используется упакованный формат 32×32, однако не помню, чтобы видел его в игре. Может быть кто-нибудь знает, возможно, это секретный или неиспользованный ресурс? Его можно просмотреть следующим образом:

./sprite_view DATA.DAT -l2 -fpacked32 0xec -b0x10

image

Зачем использовать несколько форматов спрайтов?

На этот вопрос у меня нет точного ответа. Я ни в коей мере не являюсь специалистом в тонкостях программирования для оптимизации VGA. Возможно, разные форматы используются для увеличения скорости рендеринга спрайтов, которые должны иметь разную частоту обновления. Тайлы для карт всегда имеют «сырой» формат. «Сырой» формат также используется для некоторых спрайтов интерфейса. Спрайты в упакованном формате 32×32 — это викинги и многие враги. Распакованный формат используется для некоторых слабо анимированных врагов типа башни с пушкой и других объектов, например, переключателей и лифтов.

Похоже, что в Blackthorne совсем не используется упакованный формат 32×32. Возможно, потому, что многие спрайты Blackthorne крупнее 32×32. Большинство спрайтов с подробной анимацией использует «сырой» формат. В Blackthorne нет уровней со скроллингом (как в оригинальной Prince of Persia), что ускоряет рендеринг.

Я не разобрался ещё в одном вопросе: как движок Blizzard понимает, какой формат и размер спрайтов используется для отрисовки каждого объекта? В заголовках уровней есть частичная соответствующая информация, но её недостаточно для правильной отрисовки всех объектов. Подозреваю, что эти части информации о рендеринге управляются виртуальной машиной.

Расположение спрайтов

Как я упомянул выше, в Blackthorne используются спрайты большего размера, чем в The Lost Vikings. Несмотря на то, что можно написать алгоритм рендеринга «сырых» спрайтов произвольного размера, в движке Blizzard применяется другой подход. Крупные спрайты рендерятся соединением нескольких мелких спрайтов по неизменному шаблону. Например, в главном персонаже Blackthorne используются спрайты 32×48, состоящие из двух спрайтов 16×16 для головы и одного спрайта 32×32 для тела:

image

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

./sprite_view --blackthorne DATA.DAT -fraw -w32 -h48 -l2 -b0x80 0x42

Возможно, такой подход был выбран потому, что разработчики Blizzard уже написали оптимизированные циклы рендеринга для спрайтов 16×16 и 32×32, и быстрее было рендерить крупные спрайты как набор мелких, а не как один большой.

Уровни из тайловых карт

И в The Lost Vikings, и в Blackthorne уровни создаются из тайловых карт с тайлами 16×16 (хотя мне кажется, что в обеих играх авторы хорошо постарались, чтобы такого ощущения не возникало). Используемые для тайлов спрайты на самом деле имеют размер 8×8. Каждый тайл содержит структуру, которую я называю «заготовкой». Она определяет, как создать набор спрайтов 8×8 из тайла 16×16. Каждую часть компонента можно горизонтально/вертикально перевернуть, что позволяет использовать компонентные тайлы в разных местах.

Например, в полном наборе тайлов (тайлсете) первого уровня The Lost Vikings есть несколько тайлов, таких как лестницы, которые имеют отзеркаленные версии, а также множество тайлов, которые несколько раз используют один угловой фрагмент, например, синие панели с заклёпками.

image

Просмотреть этот набор тайлов можно так:

./tileset_view DATA.DAT 1

Пакетный файл содержит для каждого набора тайлов блок, описывающий заготовки. Каждая заготовка имеет длину 8 байт, а каждый тайл закодирован как 16-битное значение. Для каждого 16-битного значения индекс спрайта кодируется как 10-битное значение, а остальные 6 бит используются как флаги. Флаги определяют вертикальное и/или горизонтальное отзеркаливание спрайта, а также то, где находится этот компонент тайла: спереди или на фоне. Кодирование бита передней/фоновой части в заготовке позволило движку Blizzard использовать общую тайловую карту для рендеринга обоих слоёв

Blackthone расширяет использование заготовок в движке двумя способами. Во-первых, три неиспользованных бита флагов применены в качестве цветовой основы. The Lost Vikings привязывает ко всем тайлам карты только 16 цветов. Благодаря добавлению битов цветовой основы Blackthorne может использовать для тайлов 128 цветов, однако каждый отдельный спрайт компонента всё равно ограничен 16 цветами.

Во-вторых, в Blackthorne добавили фоновый слой карты. Это позволяет игре иметь более подробные фоны и небо, просвечивающие сквозь пустоты в передних тайлах. Для простоты я буду называть вторичный фоновый слой слоем неба, а основную тайловую карту — передним и фоновым слоями. Слой неба не использует биты флагов передней части/фона, поэтому можно рассматривать его как единый слой.

На рисунке ниже показано, как собирается тайловая карта для первого уровня Blackthorne:

image
Слева направо, сверху вниз: небо/фон, передние тайлы, фоновые тайлы, все слои

Можно просмотреть тайловую карту следующим образом (заметьте, что уровень имеет номер 3, потому что уровни 1 и 2 — это начальная анимационная заставка и обучающий уровень):

./level_view --blackthorne DATA.DAT 3

Здесь можно заметить пару интересных моментов:

  • Некоторые части слоя неба полностью перекрыты передними слоями. Возможно, так получилось из-за процесса разработки или инструментов Blizzard.
  • Часть водопадов отрисовывается на слое неба, а другая часть — на фоне. Например, в правом нижнем углу основание водопада отрисовано на слое неба. Так получилось потому, что на верхнем слое есть камень, закрывающий водопад. Из-за формы камня его нельзя отрисовать с помощью одной карты с правильной обработкой передней части/фона.
  • Некоторые области карты как будто отсутствуют. Например, водопад посередине вверху внезапно обрывается. Это потому, что в Blackthorne используются статические комнаты, а не скроллинг. Пустые области карты в процессе игры никогда не видны. На некоторых уровнях это весьма затратно, потому что не используются целые экраны. Возможно, Blizzard решила реализовать экраны на одной тайловой карте даже несмотря на то, что в игре не используется скроллинг, чтобы снова задействовать уже имеющийся код движка The Lost Vikings.

Всё — это уровень

Blizzard сделала ещё один умный шаг: всё в игре — промежуточные экраны, анимированные заставки, меню — является уровнями. Без сомнения, это очень сэкономило время на разработку. Поскольку весь тяжкий труд по реализации кода для уровней игры с анимацией и подвижными объектами был уже сделан, то вполне логично снова использовать его для управления заставками и меню. Полагаю, в движке игры снова используется виртуальная машина для реализации анимации в уровнях анимационных заставок. Единственным заметным исключением стал начальный экран The Lost Vikings из начала этого раздела, который закодирован как большой «сырой» спрайт без сжатия.

Например, при запуске игры отображается несколько начальных экранов. Два из них закодированы как единый уровень (из двух комнат). В левой части изображения ниже показан набор тайлов для уровня, а справа — сам уровень. В игре сначала показывается силуэт персонажа (который светится, но подробнее об этом позже). Логотип Blackthorne отображается над главным меню.

image

Не знаю точно, зачем нужны эти две копии тайлов логотипа Blackthorne. Заметьте, что они имеют немного отличающуюся расцветку. Например, в верхнем наборе значок TM чёрный и находится внутри логотипа, а в нижнем — белый и находится снаружи. Возможно, этот набор тайлов ещё раз используется для другого экрана в конце игры?

Набор тайлов и уровень этого экрана можно посмотреть так:

./tileset_view --blackthorne DATA.DAT 1 -c 0x6e  ./level_view --blackthorne DATA.DAT 1 -h0x6e

Управление палитрой

Выше я говорил о том, что в каждом спрайте используется шестнадцать цветов, а значения всегда кодируются как 0x0 – 0xf. Это позволяет каждому уровню назначать свою собственную палитру для повторного использования спрайтов в разных уровнях.

В The Lost Vikings есть несколько миров, в том числе космический, доисторический и безумный конфетный миры. В каждом есть свои цветовая схема и враги. Управляемые игроком викинги используют только два набора палитр из 16 цветов (Олаф и Балеог оба используют одну зелёно-жёлтую цветовую схему, у Эрика — красно-синяя схема). В интерфейсе используется ещё один набор из 16 цветов, а ещё одна используется для предметов, таких как ключи и пополнение здоровья, которые есть во всех мирах. Благодаря этому остаётся приличная часть 256-цветной палитры, которая определяется уровнем. В большинстве уровней загружается базовая 128-цветовая палитна, а затем набор из восьми 16-цветных палитр.

Индивидуальные палитры уровней позволяют повторно использовать спрайты с другой цветовой схемой. Это был популярный трюк в эру 8-битного цвета и ограниченного дискового пространства. Как известно, в играх Mortal Kombat есть несколько палитр, меняющих внешний вид персонажей-ниндзя. На рисунке ниже показаны одни и те же спрайты динозавра с настройками палитры разных уровней:

image

Просмотреть две разные версии спрайта динозавра можно так:

./sprite_view DATA.DAT -fpacked32 -b0xc0 -l5  0xf8 ./sprite_view DATA.DAT -fpacked32 -b0xc0 -l10 0xf8

Заметьте, что программе просмотра спрайтов передаются аргументы номера уровня (-l) и смещения базовой палитры offset (-b). Номер уровня используется для определения того, какие блоки палитры нужно загружать при анализе блока заголовка уровня. Индекс базовой палитры я определил экспериментально. Повторюсь, в этой части движка Blizzard я разобрался не полностью. Снова подозреваю, что индекс базовой палитры указывается командами в программе объектов виртуальной машины.

Анимации палитр

Ещё один популярный трюк из эры DOS — анимация графики сменой цветов палитры. Анимация объекта перерисовкой пикселей довольно затратна, особенно для больших частей, которые нужно часто обновлять, например, для фоновых водопадов в Blackthorne. Вместо изменения самих пикселей гораздо экономнее изменять цвета палитры. При этом мгновенно обновляются все пиксели соответствующего цвета. Эта техника в основном полезна для простых цикличных анимаций, таких как водопады и мигающие огни в космических уровнях The Lost Vikings. Как сказано ранее, анимации палитры используются в Blackthorne для создания эффекта свечения силуэта на начальном экране.

Движок Blizzard реализует анимации палитры в заголовке уровня. Каждый аниматор палитры имеет 8-битное значение скорости и два 8-битных значения индекса цвета. Если два индекса не равны, то анимация циклически переключается между двумя этими индексами. Такой формат хорошо подходит для анимации движущихся по шаблону объектов.

Если два индекса равны, то анимация палитры используется для одного цвета и заголовок уровня указывает список 16-битных значений для анимации. Каждое из 16-битных значений кодирует значение цвета в формате RGB-555 (5 бит на цвет, то есть один бит теряется впустую). Обычная палитра VGA и движок Blizzard способны отображать 6 бит на цвет. Анимации палитры теряют младший значимый байт сдвигом каждого значения цвета на один влево. Этот формат анимации палитры полезен для анимации чего-то типа пульсирующего света.

Можно нажать в программе просмотра уровней «A», чтобы посмотреть анимации палитр.

Game Over

На этом пока всё:

./level_view DATA.DAT 48 ./level_view --blackthorne DATA.DAT 23

image

Можете читать меня в Twitter: @ryiron.
ссылка на оригинал статьи https://habrahabr.ru/post/329448/

Моделирование конструкций. Требования к моделлеру

В прошлой статье Понятие системы и конструкции. Их место в проектировании информационных систем, посвященной конструкциям, я вкратце затронул герменевтический круг – это один из способов нашего мышления, нацеленного на достижение понимания. Герменевтический круг состоит из двух направлений мышления: анализа и синтеза.

Анализ – это процесс, при котором мы представляем изучаемый объект в виде множество его частей (изучаем различные конструкции, на которые можно разложить изучаемый объект).

Синтез — это обратная «сборка» объекта.

Утверждается, что чувство понимания достигается, когда, сделав разборку объекта (анализ), а затем его сборку (синтез), — субъект получает непротиворечивый результат. В той же статье я отметил, что стандарты, как правило, нацелены на описание только одного направления мышления — анализа, но совершенно игнорируют второе направление – синтез.


Игнорирование процесса синтеза приводит к тому, что мы теряем способность делать проверку результатов анализа и начинаем мыслить шаблонами. Например, если нас спросить, из чего состоит велосипед, то довольно быстро найдется «правильный» ответ. Но если спросить: "Частью чего является велосипед?", – мы сильно затруднимся с ответом.

Множество шаблонов, которые мы заучиваем, касаются только одного направления движения – в сторону анализа и почти никогда – в сторону синтеза. Как только слышим вопросы: "Как устроено что-то?", "Как работает что-то?", – сразу в сознании возникает образ конструкции. Однако, также часто, как мы слышим вопрос: "Как устроен объект?", так же редко слышим вопрос: «В рамках какого контекста существует объект?», «Частью чего он является?» Поэтому нам легко даются ответы на одни вопросы, и с трудом на другие. Будь мы дисциплинированы в своем мышлении, мы бы также легко ответили на вопрос: "Частью чего является велосипед?", как и на вопрос: "Из чего состоит велосипед?" И самое главное — привели бы приблизительно одинаковое количество вариантов ответа как на один, так и на второй вопросы.

Где можно столкнуться с подобными вопросами на практике? Например, когда надо решить задачу описания предприятия в рамках какого-то контекста. Мы задаемся вопросом: "Частью чего является предприятие?" Ответов на этот вопрос много: предприятие может быть частью города, частью отрасли, частью холдинга. В конструкции каждого такого объекта предприятие играем свою уникальную роль и связано своими уникальными связями с различными контрагентами. Вы видели стандарты, в которых описан подход к моделированию различных конструкций, частью которых является исследуемый объект? Боюсь утверждать на все сто, но я, к своему стыду, таких стандартов не знаю. Их либо нет, либо они мало известны. Поэтому нет и фреймворков для работы с такого рода моделями. А потребность в такого рода инструментах есть, например, когда мы хотим анализировать связи наших контрагентов в рамках различных коллабораций.

Сформулируем требование к фреймворку, моделирующему конструкции:

Возможность один объект представить в виде различных конструкций, а также возможность один объект представить частью различных конструкций.

Следующее ограничение, которое нас сдерживает при моделировании конструкций, — это непонимание того, что такое множества объектов и как моделируется множество.

Множество – это одно из тех аксиоматических понятий, которое невозможно определить через другие понятия. Кантор сказал, что множество — это многое, мыслимое как целое.

Например, пусть есть множество объектов на космической станции, или множество живых организмов на Земле, или множество ракушек на пляже. Множество – это тоже объект учета, но специфический, отличный от объекта. Множество имеет состав. Что входит в состав множества? Ответ забавный: множество объектов. Получается какой-то порочный круг – множество объектов имеет состав, а состав состоит из множества объектов. Причина этого в том, что два разных понятия обозначены одним термином:

  • Множество – это многое, мыслимое нами как целое (мат).
  • Множество – синоним слова «много».

Получается, что множество (мат.) имеет состав, а в состав входит МНОГО объектов.

В разговорной речи мы никогда не употребляем термин множество в математическом смысле. Например, когда мы говорим, что на улице множество людей танцевало и пело, то, конечно, имеется ввиду множество как синоним слова много. Множество танцевать не может: на улице много людей танцевало и пело.

Другое дело сказать: Множество танцующих на улице людей имеет ненулевое пересечение со множеством поющих на улице людей. Понятно, что здесь идет речь о множестве во втором смысле этого слова. В разговорной речи такое не услышишь. При этом множество танцующих людей имеет состав. Этот состав включает в себя много танцующих людей.

Если в разговорной речи упоминают слово множество, имеют ввиду МНОГО объектов. Но МНОГО объектов, — это не множество. Множество в математическом смысле – это то, что мыслится как целое. Когда я попросил представить множество объектов на космической станции, большинство из вас представило разные объекты, которые можно там увидеть. Но это представление – не есть представление множества в математическом смысле. Это представление множества объектов в смысле МНОГО объектов. Множество в математическом смысле – это нечто иное. Это то, что представляется нами как единое целое. То, что вы представили много объектов, не дает вам представления о множестве. Вы представили состав множества. Теперь надо этот состав представить, как одно целое (для начала дать ему имя, например). Вот тут и кроется загадка множества – не всякий способен помыслить много объектов как одно целое, а создаваемый при этом образ не обязательно совпадет с образом, представленным другим субъектом. Именно поэтому так трудно объяснить, что такое множество. Именно поэтому проще ввести это понятие аксиоматически, без всякой опоры на здравый смысл.

Множество (так же, как и объект) необходимо в качестве инструмента для описания действительности. Понятие объект нам кажется понятным, только потому, что с этим понятием мы знакомимся в детстве, а множество – непонятным, потому что с ним мы знакомимся в институте, когда изучаем основы математики.

И тем не менее, понятие множества сидит в нас достаточно глубоко и известно каждому так же, как известно понятие объект, но не осознается нами так же ясно. Термин множество был введен не случайно, а как результат осознания этого факта.

Недавно я сделал попытку ввести понятие объекта так, как мы ввели бы его, будь оно нам совершенно неизвестно – при помощи классификации тех моделей, которые мы создаем в своем воображении, когда слышим упоминание об объекте. Эта попытка описана в статье: Строгое определение понятий: объект, состояние, событие, бизнес-операция и бизнес- функция. Попытка ввести понятие множества через модели, которые мы создаем в своем воображении, сделана мной в контексте моделирования конструкций в статье: Классификация конструкций: примеры и заблуждения. Эта статья вызвала бурную дискуссию, но, как мне показалось, обсуждали не классификацию, а вопросы, которые можно назвать скорее религиозными: например, должна ли конструкция обладать эмерджентностью, или может ли конструкция состоять из одного элемента? Понятно, что формальная теория систем не должна ограничивать себя каким-то узким кругом конструкций, иначе нельзя будет выполнять операции над системами. Из этого я сделал вывод, что рассуждения про множества довольно затруднительны и решил еще раз подробно остановиться на этой теме.

Когда мы говорили о конструкции, мы говорили о ней, как об:

  • объекте,
  • множестве объектов, связанными связями и мыслимыми как целое.

Вопрос: под множеством объектов в данном определении понимается что? Множество в смысле «математическое множество», или множество в смысле «много»? В прошлой статье я не акцентировал на этом внимание, теперь мы можем разобрать это определение более подробно.

Мы видим, что определение конструкции практически повторяет определение множества: конструкция — это много объектов, мыслимых как единое целое. Множество в математическом смысле и конструкция – это объекты одного сорта. Состав множества – это много объектов, а состав конструкции — это много объектов и много связей. При этом множество – не есть много объектов, и конструкция – не есть много объектов и связей. Если мы говорим, что конструкция состоит из множества объектов, мыслимых как целое, имеется в виду, что состав конструкции включает в себя много объектов, но конструкция при этом– одна. Именно поэтому я сказал, что понятие множества сидит в нашем сознании глубоко, но не осознается нами так, как осознается понятие объекта.

Можно сказать и так: конструкция – это множество (в математическом смысле) объектов и связей между ними. Тогда мы не должны говорить о том, что его необходимо мыслить его как целое, потому что это требование включено в определение термина множества (в математическом смысле) и получается тавтология.

Поэтому определение конструкции можно дать как первым способом, так и вторым.

Чтобы яснее представить себе аналогию, которая есть между понятием множества и понятием конструкции, нарисуем картинку.


Множеству соответствует конструкция. Множество имеет состав, и конструкция имеет состав. В состав множества входит много объектов и в состав конструкции входит много объектов (связи – тоже объекты).

Поскольку такая аналогия напрашивается, то на конструкциях можно построить такую же алгебру, как и на множествах: конструкции можно складывать, искать их пересечения, вычитать. Чтобы такая алгебра имела место быть, надо добавить нулевой элемент, аналог пустого множества, — пустую конструкцию, состоящую из пустого множества элементов. Кроме того, надо будет допустить существование конструкции из одного элемента или из одной связи, что кажется контринтуитивно, но без этого не получится проводить операции на конструкциях. То есть, для проведения операций над конструкциями (и системами) мы должны принять по отношению к термину конструкция ту же аксиоматику, что и по отношению ко множеству. Это позволит нам соединять конструкции, разъединять их, искать пересечения и проч. Что забавно, в математике множества появились в ответ на вопрос: как моделировать реальность, в том числе и конструкции, но, спустя век мы так и не научились этим инструментом пользоваться!

Часто можно слышать такие выражения как: «Конструкция производит электроэнергию». Поскольку множество ничего не «может» делать, не имеет размера, ничего не весит, то все эти свойства может иметь либо объект, входящий в состав множества, либо объект, синтезированный на основе этого множества, но не само множество. Получается, что, когда мы говорим, что конструкция производит электроэнергию, мы имеем в виду либо какой-то ее элемент, либо объект, синтезированный на ее основе, но ни в коем случае не имеем в виду множество объектов и связей, мыслимых как целое! Это важно помнить, и это нам понадобится в следующих статьях.

В информационных системах, с которыми мне приходится иметь дело, нет встроенного механизма для моделирования множеств. Моделирование множества (присвоение ему имени, атрибутов, возможность работать с ним как с обычным объектом, вызывая его по имени, обращаться к его атрибутам, проводить операции сложения, вычитания и пересечения) пока реализуются вручную. Из этого следует, что и конструкции приходится моделировать вручную. Следующее требование к фреймворку, который имеет цель моделировать конструкции:

Способность моделировать множества и производить операции над ними.

В следующей статье так же подробно рассмотрим понятия «тип» и «атрибут».
ссылка на оригинал статьи https://habrahabr.ru/post/329576/

Телекино

Родился я в 1981 году, в то время уже были вполне современные фотоаппараты типа Зенит, ФЭД, которые на 36-и миллиметровую пленку могли делать отличные черно-белые фотографии. Но вот видеосъемка была достаточно дорогим удовольствием и для большей части населения была доступна в формате 8мм или супер-8.
Данная технология говорит сама за себя-

Ширина пленки всего 8 миллиметров, размер кадра 4,4×3,25 мм, прародителем данного формата была пленка шириной 16 мм. Изначально по причине дороговизны кинопленки было придумано решение при котором съемка на 16-мм пленку производилось интересным способом при котором методом Бустрофедо́на размещались 4 кадра вместо одно — экономия в 4 раза — профит.
Чуть позже сделали перфорацию почаще и появилась возможность упростить механизм киноаппаратов и вместо перемещения кадрового окна слева направо, просто снимать сначала одну сторону пленки, потом вторую на обратном ходе — так называемый формат 2х8.
Существовали даже резаки для продольной резки пленки

тот что был у нас дома не сохранился, и даже его фотку я не нашел, выглядел он как свисток.
И триумфом любительской киносъемки стал формат супер-8. Пленка изначально шириной 8 мм и за счет уменьшения перфорации — увеличенный кадр, целых 5,36×4,01 мм, КПД использования пленки было увеличено с с 47 до 71%.
Мои родители скопив неизвестную сумму денег приобрели камеру Аврора

Питание от 4-х батареек формата АА и кассета с кинопленкой. Пара многоразовых кассет шли в комплекте. Пленку нужно было в темноте заправить в кассету, это было чуть сложнее чем заправить 36мм пленку в кассету для фотоаппарата, 8-мм пленка пропускалась с одной стороны кассеты через несколько роликов на другую сторону и наматывалась на приемный ролик.

Я примерно в 10 лет застал наличие пленки, камеры, кассет и отсутствие желания кого-либо этим заниматься (занимался отец но родители развелись). А мне было очень интересно и три пленки по 15 метров(именно столько входило в кассету) был заснято.Через пару лет удалось купить цветные пленки в одноразовых кассетах — тут уже очень просто, вставил кассету заснял, вставил другую. И начались лихие 90-е и закончилась эра любительской киносъемки.
Семейный просмотр периодически устраивался на кинопроекторе типа Русь

но банальная проблема — резиновый пассик оказалась трудноразрешимой и частый просмотр стал затруднителен.
Но так удивительно посмотреть на себя, бегающего с молотком в 3 года, в попытке починить велосипед. Смотришь на это и понимаешь, что гик — это с детства.
И вот я созрел для того чтобы это 8-мм богатство перевести в набор единичек и ноликов и скопировать на более современный носитель. Помониторил паутину и выяснилось, что оцифровка кинопленки 8мм в это довольно специфичная услуга, в Москве и Питере есть конторы обладающие импортными аппаратами разной степени совершенства, но вот в Екатериньбурге предлагают только одну, распространенную по планете технологию, дружба кинопроектора и кинокамеры или фотоаппарата — это когда фотоаппарат смотрит в объектив проектора.
При этом все вокруг описывают преимущество покадрового сканирования, что гораздо более развито для 36-мм пленки, на которой сняты почти все советские фильмы. Но оказалось, что цены на покадровое сканирование 8 мм пленки на фирменном аппарате совсем не любительские — около 250 рублей за минуту, итого на 100 метров набегает весьма неплохо.
Жаба довольно мотивирующее животное, под влиянием которого интернет был просканирован на предмет DIY сканера кинопленки. Изучение вопроса показало, что в России данный вопрос решается в основном способом дружбы проектора с камерой, а вот Америка имеет на своей земле несколько гиков создавших руками аппараты — Телекино, которые позволяют покадрово отснять пленку. Имея набор файлов-картинок преобразовать их в видео можно большим количество как платных так и бесплатных программ — технология используется для съемки анимации и поэтому довольно развита до сих пор.
Решено — будем делать Телекино.
ссылка на оригинал статьи https://geektimes.ru/post/289521/