Добавлен IceFog,
опубликован
Раздел:
Основы
Игровые ходы
Всякий раз, когда игрок выделяет юнита, отдает ему приказ или когда сценарий синхронизирует ячейки кэша - всё это запускает один и тот же механизм.
Когда совершается действие, требующее синхронизации, в специальный буфер (ограниченый размером в 1023 байт) добавляется команда.
Содержимое этого буфера периодически сбрасывается хосту. Также это происходит при переполнении.
Содержимое этого буфера периодически сбрасывается хосту. Также это происходит при переполнении.
Как я понимаю, хост склеивает команды полученые ото всех игроков в один большой пакет и пересылает его всем.
Игроки исполняют команды, сдвигают игровое время вперед и шлют в ответ хэш-сумму игры и если она отличается от ожидаемой хостом, тот отключает десинхронизированного игрока.
Игроки исполняют команды, сдвигают игровое время вперед и шлют в ответ хэш-сумму игры и если она отличается от ожидаемой хостом, тот отключает десинхронизированного игрока.
Таким образом совершается один игровой ход.
Сетевые команды
Каждая команда начинается с байта содержащего её номер, а дальше идут параметры.
Сетевые метки
У каждого активного агента есть сетевой идентификатор одинаковый на всех компьютерах.
Ссылаясь на него, можно объяснить другим клиентам, кого именно ты выделил или на кого использовал способность. Если поля равны "-1", то объект отсутствует.
Ссылаясь на него, можно объяснить другим клиентам, кого именно ты выделил или на кого использовал способность. Если поля равны "-1", то объект отсутствует.
TNetTag = record
Presence: Dword;
Birth: Dword;
end;
Команды выделения
Модификация выделения
NET_COMMAND_UNIT_SELECTION_MODIFY = 0x16
Размер | Имя | Коментарий |
---|---|---|
1 байт | Action | Тип действия |
2 байта | Count | Количество юнитов в списке |
Count раз TNetTag | Units | Список целевых юнитов |
Содержит список юнитов которых нужно добавить в/убрать из выделения.
Контрольные группы и подгруппы
NET_COMMAND_UNIT_DEFINE_CONTROL_GROUP = 0x17
Размер | Имя | Коментарий |
---|---|---|
1 байт | GroupIndex | Номер группы |
2 байта | Count | Количество юнитов |
Count раз TNetTag | Units | Юниты для добавления в группу |
Добавляет юнитов в группу для быстрого выделения.
Используется хоткеями "Ctrl+цифра".
Используется хоткеями "Ctrl+цифра".
NET_COMMAND_UNIT_SELECT_CONTROL_GROUP = 0x18
Размер | Имя | Коментарий |
---|---|---|
1 байт | GroupIndex | Номер группы |
1 байта | Unknown | Кто знает |
Выделяет группу, прежде назначеную предыдущей командой.
NET_COMMAND_UNIT_SELECT_SUB_GROUP = 0x19
Размер | Имя | Коментарий |
---|---|---|
4 байта | UnitType | Код с типом юнита |
NetTag | Unit | Сам юнит |
Меняет выбранную подгруппу юнитов в выделении.
NET_COMMAND_UNIT_REFRESH_SUB_GROUP = 0x1A
Параметры отсутствуют.
Лень изучать.
Создания события
NET_COMMAND_UNIT_SELECTION_EVENT = 0x1B
Размер | Имя | Коментарий |
---|---|---|
1 байт | Action | Тип действия |
TNetTag | Unit | Целевой юнит |
Создает событие выделения в JASS-скрипте.
Выделение прочих объектов
NET_COMMAND_SELECTABLE_SELECTION_MODIFY = 0x1C
Размер | Имя | Коментарий |
---|---|---|
1 байт | Action | Вероятно, тип действия, как и в прочих командах. |
TNetTag | Selectable | Целевой объект |
Используется для выделения предметов и разрушаемых объектов.
Список возможных действий при выделении:
Имя | Значение | Описание |
---|---|---|
Add | 1 | Добавить юнита в выделение. |
Remove | 2 | Убрать юнита из выделения. |
Set | 3 | Вроде как сначала очищает выделение, а затем добавляет юнита. Наверное, используется при одиночном клике на юнита. |
Команды кэша
Пролог
Все команды кэша имеют одинаковое начало:
Размер | Имя | Коментарий |
---|---|---|
NTString[256] | CacheName | Имя кэша |
NTString[256] | Key1 | Первый ключ |
NTString[256] | Key2 | Второй ключ |
NTString означает нуль-терминированую строку не превышающую указаный в квадратных скобках размер.
Для уменьшения количества посылаемых данных, стоит использовать как можно более короткие строки для имени кэша и ключей, но учтите, что кэш с пустым именем не работает.
Команды записи
NET_COMMAND_SYNC_STORE_INTEGER = 0x6B
NET_COMMAND_SYNC_STORE_REAL = 0x6C
NET_COMMAND_SYNC_STORE_BOOLEAN = 0x6D
Размер | Имя | Коментарий |
---|---|---|
4 байта | Value | Значение для записи |
NET_COMMAND_SYNC_STORE_UNIT = 0x6E
Много всякого, лень разбирать.
Строки
NET_COMMAND_SYNC_STORE_STRING = 0x6F
NET_COMMAND_SYNC_CLEAR_STRING = 0x74
Синхронизация строк не реализована.
При попытке отправить или очистить строку, шлется только номер команды, без ключей кэша и самой строки. Приём же этой команды вовсе не реализиован - она воспринимается как неизвестная и игнорируется вместе со всем, что шло в буфере за ней.
При попытке отправить или очистить строку, шлется только номер команды, без ключей кэша и самой строки. Приём же этой команды вовсе не реализиован - она воспринимается как неизвестная и игнорируется вместе со всем, что шло в буфере за ней.
Команды очистки
NET_COMMAND_SYNC_CLEAR_INTEGER = 0x70
NET_COMMAND_SYNC_CLEAR_REAL = 0x71
NET_COMMAND_SYNC_CLEAR_BOOLEAN = 0x72
NET_COMMAND_SYNC_CLEAR_UNIT = 0x73
Очищает указанную ячейку кэша.
Синхронизация данных в скрипте
Игра не предоставляет события синхронизации данных, но известно, что выделение юнитов использует тот же буфер команд. Следовательно, если сначала послать команды синхронизации ячеек кэша и сразу после этого выделить юнита, то когда произойдет событие, можно будет не сомневаться в том, что предыдущие команды уже исполнились.
пример кода
gamecache sync_cache
unit sync_unit
void end_data() {
SelectUnit(sync_unit, true)
SelectUnit(sync_unit, false)
}
void send(player p, int value) {
if (GetLocalPlayer() == p) {
StoreInteger(sync_cache, "", "", 1)
SyncStoredInteger(sync_cache, "", "")
end_data()
}
}
void on_incoming_data() {
player sender = GetTriggerPlayer()
int data = LoadInteger(sync_cache, "", "")
BJDebugMsg("Received " + I2S(data) + " from player " + GetPlayerName(sender))
}
void init() {
sync_cache = InitGameCache("sync")
sync_unit = CreateUnit(...)
trigger t = CreateTrigger()
TriggerRegisterUnitEvent(t, EVENT_UNIT_SELECTED, u)
TriggerAddAction(t, function on_incoming_data)
}
Код можно оптимизировать, чтобы за один раз слалось не одно число, а произвольное количество данных.
Потеря контроля
Если послать слишком много данных, то игрок может потерять возможность отдавать приказы.
Происходит это потому, что размер буфера команд ограничен и количество обрабатываемых пакетов команд (ходов) в секунду ограничено. Вероятно, это число можно отрегулировать меняя задержку командой хост-ботов "!latency" и чем меньше оно будет, тем больше пропускная способность.
Предположим, можно совершить 8 ходов в секунду. Тогда, если заполнить командами 1000 буферов, то следующие команды игрока попадут в 1001-ый буфер, до которого дойдет очередь через 125 секунд, а до тех пор игра не будет реагировать на его приказы.
пример кода, приводящего к потере контроля
globals
timer test_timer
gamecache test_cache
endglobals
function test_tick takes nothing returns nothing
local integer i = 1
call StoreString(test_cache, "", "", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
loop
call SyncStoredString(test_cache, "", "")
set i = i + 1
exitwhen i > 10000
endloop
endfunction
function test_start takes nothing returns nothing
call TimerStart(test_timer, 0.05, true, function test_tick)
endfunction
function test_init takes nothing returns nothing
local trigger t = CreateTrigger()
call TriggerRegisterPlayerChatEvent(t, GetLocalPlayer(), "-start", true)
call TriggerAddAction(t, function test_start)
set test_cache = InitGameCache("a")
set test_timer = CreateTimer()
endfunction
Уязвимости
Событие синхронизации приходится отлавливать косвенно, посредством выделения специального юнита, но что если между командой с данными и командой выделения будут исполнены команды с данными от других игроков?
Например, если команда с данными попадет в первый буфер, а команда выделения уедет в следующем. Это создаст промежуток, в котором возможно исполнить лишние команды.
Например, если команда с данными попадет в первый буфер, а команда выделения уедет в следующем. Это создаст промежуток, в котором возможно исполнить лишние команды.
Представим себе следующую последовательность команд:
Ход 1
Игрок 1 (Красный)
Записать в ячейку кэша "cache_name" по ключам "a" и "b" число 42.
Игрок 2 (Синий)
Записать в ячейку кэша "cache_name" по ключам "a" и "b" число 1.
Ход 2
Игрок 1 (Красный)
Выделить юнита <sync_unit>.
Синий игрок перезаписал посланные красным данные.
Последствия от этого могут быть разные. Например, если карта считывает с диска сохраненного героя и отсылает его уровень (в данном случае 42), то синий игрок может испортить красному игру, так как тому придется играть слабаком 1-го уровня.
Последствия от этого могут быть разные. Например, если карта считывает с диска сохраненного героя и отсылает его уровень (в данном случае 42), то синий игрок может испортить красному игру, так как тому придется играть слабаком 1-го уровня.
Решение 1
Когда данные были синхронизированы, можно ожидать дополнительного подтверждения от игрока отправителя, после того как тот проверит целостность данных.
Это добавит дополнительную задержку, а взамен лишит других игроков возможности подменять данные.
Это добавит дополнительную задержку, а взамен лишит других игроков возможности подменять данные.
код с проверкой подлинности
gamecache sync_cache
unit sync_unit_data_chunk_event
unit sync_unit_data_check_passed
int value_to_sync
int[] synced_values
void end_data() {
SelectUnit(sync_unit_data_chunk_event, true)
SelectUnit(sync_unit_data_chunk_event, false)
}
void send(player sender, int value) {
if (GetLocalPlayer() == sender) {
value_to_sync = value
StoreInteger(sync_cache, "", "", value)
SyncStoredInteger(sync_cache, "", "")
end_data()
}
}
void data_checked() {
SelectUnit(sync_unit_data_check_passed, true)
SelectUnit(sync_unit_data_check_passed, false)
}
void on_sync_complete() {
player sender = GetTriggerPlayer()
int data = synced_values[GetPlayerId(sender)]
BJDebugMsg("Received " + I2S(data) + " from player " + GetPlayerName(sender))
}
void on_data_chunk() {
int data = LoadInteger(sync_cache, "", "")
synced_value[GetPlayerId(GetTriggerPlayer())] = data
if (GetLocalPlayer() == GetTriggerPlayer()) {
if (data == value_to_sync) {
data_checked()
}
}
}
void on_sync_event() {
unit u = GetTriggerUnit()
if (u == sync_unit_data_chunk_event) {
on_data_chunk()
} elseif (u == sync_unit_data_check_passed) {
on_sync_complete()
}
}
void init() {
sync_cache = InitGameCache("sync")
sync_unit_data_chunk_event = CreateUnit(...)
sync_unit_data_check_passed = CreateUnit(...)
trigger t = CreateTrigger()
TriggerRegisterUnitEvent(t, EVENT_UNIT_SELECTED, sync_unit_data_chunk_event)
TriggerRegisterUnitEvent(t, EVENT_UNIT_SELECTED, sync_unit_data_check_passed)
TriggerAddAction(t, function on_sync_event)
}
Этот способ позволяет обнаружить подмену данных, но не пречесь её. Остается возможность заблокировать исходящие от других игроков данные. Для того, чтобы избежать этого, нужно каким-то образом гарантировать, что команды с данными и команда выделения юнита будут находится в одном буфере команд.
Решение 2
Если при помощи мемхака добавить событие синхронизации ячейки кэша, то таких проблем можно будет избежать.
Заморозка спящих потоков
Может не надо?
Просмотр исходящих команд
Для тех, кому интересно посмотреть как выглядят команды, я сделал небольшой мод.
Для установки, киньте файл настроек "NetCommandsLog.cfg", а также "MinHook_x86.dll" и "NetCommandsLog.dll" в папку с игрой, не забыв сменить расширение последнего на "*.mix". В файле настроек укажите путь к файлу, куда будут записывать команды. Если указать вместо пути минус "-", то запись будет вестись в консоль (если она не будет обнаружена, то программа крашнется).
Для установки, киньте файл настроек "NetCommandsLog.cfg", а также "MinHook_x86.dll" и "NetCommandsLog.dll" в папку с игрой, не забыв сменить расширение последнего на "*.mix". В файле настроек укажите путь к файлу, куда будут записывать команды. Если указать вместо пути минус "-", то запись будет вестись в консоль (если она не будет обнаружена, то программа крашнется).
На каждую исходящую команду создается строчка с записаными в виде шестнадцатеричных чисел байтами из которых она состоит.
Когда игра отсылает буфер с командами хосту, добавляется строчка "Command buffer flushed (%d bytes)" где "%d" размер всех накопленых к этому моменту команд.
Когда игра отсылает буфер с командами хосту, добавляется строчка "Command buffer flushed (%d bytes)" где "%d" размер всех накопленых к этому моменту команд.
пример лога
16 01 05 00 A5 30 00 00 A5 30 00 00 BB 30 00 00 BB 30 00 00 D1 30 00 00 D1 30 00 00 E7 30 00 00 E7 30 00 00 FD 30 00 00 FD 30 00 00 (44 bytes)
1A (1 bytes)
19 61 65 70 68 A5 30 00 00 A5 30 00 00 (13 bytes)
Command buffer flushed (58 bytes).
12 08 00 03 00 0D 00 FF FF FF FF FF FF FF FF 03 93 F4 C5 80 08 03 45 FF FF FF FF FF FF FF FF (31 bytes)
Command buffer flushed (31 bytes).
01 (1 bytes)
Command buffer flushed (1 bytes).
02 (1 bytes)
Command buffer flushed (1 bytes).
Архив с модом прикреплен к ресурсу (исходники в папке "src").
`
ОЖИДАНИЕ РЕКЛАМЫ...
Чтобы оставить комментарий, пожалуйста, войдите на сайт.
Отредактирован IceFog
При добавлении одного юнита в выделение, шлется NET_COMMAND_UNIT_SELECTION_MODIFY (12 байт).
Каждый вызов SelectUnit всегда создает событие (если есть подписчики) и следовательно шлется NET_COMMAND_UNIT_SELECTION_EVENT (10 байт).
Также, могут слаться NET_COMMAND_UNIT_REFRESH_SUB_GROUP (1 байт) и NET_COMMAND_UNIT_SELECT_SUB_GROUP (13 байт).
Отредактирован host_pi
которые позволяют сихрить в 5 строк все координаты
или там опять на 2000 строк?
Надо будет разобраться, как её статически скомпоновать с библиотекой на FreePascal.