В Героях Меча и Магии (и не только в третьей части) практикуется упаковка свойств объектов в двойное слово (DWORD, 32 бита), слово (WORD, 16 бит) или даже байт (BYTE, 8 бит). Например, состояние нейтральных монстров (количество и лояльность) не хранятся по отдельности и запакованы в 32-х битное число. То же самое и с обсуждаемым выше Учёным. Давайте посмотрим, как работать с такими запакованными данными: как их распаковывать и как запаковывать.
1. РаспаковкаЧтобы не увязнуть в общей теории, давайте рассмотрим, как игра запаковывает свойства Учёного в 32-х битное число. Для начала определим, какие свойства есть у Учёного. Он может:
0) повышать один из 4-х первичных навыков;
1) давать или повышать ступень развития одного из 28-ми (29-ти в HotA) вторичных навыков;
2) учить одному из 69-ти заклинаний (70 минус Titan's Lightning Bolt).
Здесь нас не будет интересовать, в каких случаях срабатывает каждый из этих вариантов, или есть ли ограничения на получаемые заклинания, т.к. к теме упаковки это не относится.Видим, что для хранения номера заклинания, а также номеров первичного и вторичного навыков будет хватать 3-х байтов + 1 байт на хранение самого типа награды (1 байт = 8 бит и может хранить в себе 2^8 = 256 состояний). Хорошо, можно хранить эту информацию в 2-х байтах: первый байт - тип награды, второй байт - ID награды (заклинание/первичный/вторичный навык), но программисты NWC решили "заморочиться" и запаковали всё это - внимание! - в 4 байта, причём - внимание! - используя битовые поля. Не будем сейчас рассуждать на тему, почему они так сделали (возможно, ради универсальности: у всех тайлов карты _MapItem_ есть 32-битное поле setup, но по-прежнему непонятно, почему бы не обойтись 2-мя младшими байтами этого поля и не "заморачиваться" с упаковкой/распаковкой). Но есть то, что есть. Поэтому далее.
Пронумеруем биты двойного слова:
- Код: Выделить всё
HEX 1F 1E 1D 1C 1B 1A 19 18 17 16 15 14 13 12 11 10 0F 0E 0D 0C 0B 0A 09 08 07 06 05 04 03 02 01 00
DEC 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X X
Биты справа (X, может принимать два значения: 0 или 1) называются
младшими, биты слева -
старшими.
Посчитаем, сколько бит потребуется для упаковки ID в каждом из трёх вышеупомянутых случаев:
0) 4 первичных навыка. Хватит 2-х бит. Например, Атаку можно закодировать последовательностью бит 00, Защиту - 01, Силу Магии - 10 и Знания - 11;
1) 28 (29 в HotA) вторичных навыка. Хватит 5-ти бит. Pathfinding - 00000, Archery - 00001, ..., First Aid - 11011 (Interference - 11100);
2) 70 заклинаний. Хватит 7-ми бит. Summon Boat - 0000000, Scuttle Boat - 0000001, ..., Air Elemental - 1000101 (здесь 7 бит хватает даже на заклинания существ, если что).
А ещё потребуется 2 бита на хранение типа награды: 00 - первичный навык, 01 - вторичный навык, 10 - заклинание.
Для тех кто не знает, как подобрать минимальное количество бит, способных сохранить N состояний: Вам следует подобрать такую степень двойки, чтобы она была не меньше числа состояний, а предыдущая степень - меньше. Например, для 70-ти заклинаний: 2^6 = 64 меньше, чем 70, а 2^7 = 128 уже больше. Поэтому для хранения всех состояний потребуется 7 бит.Итого нам потребуется 2 + 5 + 7 + 2 = 16 бит. Однако следует учесть, что по умолчанию арифметические и логические операции проводятся над целыми числами
со знаком, и на хранение этого знака нужен ещё 1 бит: 0 - знака нет, 1 - знак есть. Можно, конечно, обойтись и без знака, занимающего лишний бит, но тогда придётся работать с беззнаковыми типами: unsigned char, unsigned int и т.п., что есть прямой путь к возникновению различных багов. Программисты NWC знали это, поэтому просто примите: для каждого числа нужен ещё один бит под знак. Итого: 3 + 6 + 8 + 3 = 20 бит, которые отлично помещаются в двойное слово setup.
Давайте теперь посмотрим, как награды Учёного реально хранятся в этом двойном слове (32-х битах) и как их оттуда достать:
- Код: Выделить всё
HEX 1F 1E 1D 1C 1B 1A 19 18 17 16 15 14 13 12 11 10 0F 0E 0D 0C 0B 0A 09 08 07 06 05 04 03 02 01 00
DEC 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
X X X X X X X X X X X X X X X X X X X X X X X X X X X X X R R R
Три младших бита (R) занимает тип награды (см. выше). Чтобы получить это значение, применяют две операции побитого сдвига (shl - логический сдвиг влево и sar - арифметический сдвиг вправо; разница между логическим и арифметическим сдвигом в том, что второй сохраняет знак числа). В C++ для shl есть оператор
<<, а для shr/sar -
>> (про нюансы использования и отличия shr и sar почитайте сами, ибо уже лезем в джунгли).
Нас будет интересовать формула, позволяющая получить значение последовательности бит длиной L, начиная с бита с номером N. Например, чтобы достать тип награды (см. три бита R выше), нам потребуется извлечь последовательность бит длиной L = 3, начиная с бита с номером N = 0. Это можно сделать двумя сдвигами: влево до тех пор, пока левый бит R не упрётся в левую стенку (позиция 31), и вправо до тех пор, пока правый бит R не упрётся в правую стенку (позиция 0). Немного смекнув, понимаем, что двойное слово нужно сдвинуть влево на 32 (размер двойного слова) - 3 (длина последовательности бит) = 29 бит, а потом на столько же вправо. Отсюда и получается код (см. пост с кодом выше):
- Код: Выделить всё
int ScholarType = *(int*)c->ebx << 29 >> 29;
Аналогично можно извлечь и остальные значения (кто не умеет читать дизассемблированный/декомпилированный листинг, может легко догадаться после серии экспериментов, в каких именно битах эти значения хранятся):
0) Первичный навык (буквами "P" отмечена битовая последовательность, хранящая ID навыка):
- Код: Выделить всё
HEX 1F 1E 1D 1C 1B 1A 19 18 17 16 15 14 13 12 11 10 0F 0E 0D 0C 0B 0A 09 08 07 06 05 04 03 02 01 00
DEC 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
X X X X X X X X X X X X X X X X X X X X X X X X X X P P P R R R
Мы видим, что на хранение ID первичного навыка отводится 3 бита (2 бита на сам ID + 1 бит знаковый). Нам нужно извлечь последовательность бит длиной L = 3, начиная с бита с номером N = 3. Очевидно, что нужно подвинуть крайний левый бит "до упора" влево: 32 - (N + L) = 32 - (3 + 3) = 26 бит, а потом вправо на 32 - L = 32 - 3 = 29 бит. Итого код:
- Код: Выделить всё
int ScholarPrimarySkill = *(int*)c->ebx << 26 >> 29;
1) Вторичный навык (буквами "S" отмечена битовая последовательность, хранящая ID навыка):
- Код: Выделить всё
HEX 1F 1E 1D 1C 1B 1A 19 18 17 16 15 14 13 12 11 10 0F 0E 0D 0C 0B 0A 09 08 07 06 05 04 03 02 01 00
DEC 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
X X X X X X X X X X X X X X X X X X X S S S S S S S P P P R R R
На хранение номера вторичного навыка отводится 7 бит (6 бит + 1 знаковый). С запасом. Нам нужно извлечь последовательность бит длиной L = 7*, начиная с бита с номером N = 6. Двигаем крайний левый бит "до упора" влево: 32 - (N + L) = 32 - (6 + 7) = 19 бит, а потом вправо на 32 - L = 32 - 7 = 25 бит. Код:
- Код: Выделить всё
int ScholarSecondarySkill = *(int*)c->ebx << 19 >> 25;
* Вы не можете знать об избытке, но это не играет никакой роли, потому что в избыточных битах всё равно будут нули.
2) Заклинание (буквами "M" отмечена битовая последовательность, хранящая ID заклинания):
- Код: Выделить всё
HEX 1F 1E 1D 1C 1B 1A 19 18 17 16 15 14 13 12 11 10 0F 0E 0D 0C 0B 0A 09 08 07 06 05 04 03 02 01 00
DEC 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
X X X X X X X X X M M M M M M M M M M S S S S S S S P P P R R R
На хранение ID заклинания отведено целых 9 бит + 1 бит знаковый, т.е. с порядочным избытком. Нам нужно извлечь последовательность бит длиной L = 10*, начиная с бита с номером N = 13. Подвинем крайний левый бит "до упора" влево: 32 - (N + L) = 32 - 23 = 9 бит, а потом вправо на 32 - L = 32 - 10 = 22 бита. Код:
- Код: Выделить всё
int ScholarSpell = *(int*)c->ebx << 9 >> 22;
* Вы не можете знать об избытке, но это не играет никакой роли, потому что в избыточных битах всё равно будут нули.
2. ЗапаковкаЗдесь всё элементарно. Обнуляем двойное слово. Двигаем значение влево в нужное место приёмника (обычно приёмник - двойное слово DWORD) и логически складываем полученное значение с текущим значением приёмника. Например, пусть Учёный даёт заклинание Titan's Lightning Bolt (ID = 57):
- Код: Выделить всё
*(int*)c->ebx |= 57 << 13; // 13 - номер битовой позиции, с которой начинается ID заклинания
Если же требуется не инициализировать битовое поле, а
перезаписать его, то проще всего использовать подходящую структуру:
- Код: Выделить всё
struct Scholar {
int Reward : 3;
int PSkill : 3;
int SSkill : 7;
int Spell : 10;
};
...
Scholar* scholar = (Scholar*)c->ebx;
scholar->Spell = 57;