Добавлен , опубликован
Раздел:
Основы

Игровые ходы

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

Сетевые команды

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

Сетевые метки

У каждого активного агента есть сетевой идентификатор одинаковый на всех компьютерах.
Ссылаясь на него, можно объяснить другим клиентам, кого именно ты выделил или на кого использовал способность. Если поля равны "-1", то объект отсутствует.
TNetTag = record
	Presence: Dword;
	Birth: Dword;
end;

Команды выделения

Модификация выделения
NET_COMMAND_UNIT_SELECTION_MODIFY = 0x16
РазмерИмяКоментарий
1 байтActionТип действия
2 байтаCountКоличество юнитов в списке
Count раз TNetTagUnitsСписок целевых юнитов
Содержит список юнитов которых нужно добавить в/убрать из выделения.
Контрольные группы и подгруппы
NET_COMMAND_UNIT_DEFINE_CONTROL_GROUP = 0x17
РазмерИмяКоментарий
1 байтGroupIndexНомер группы
2 байтаCountКоличество юнитов
Count раз TNetTagUnitsЮниты для добавления в группу
Добавляет юнитов в группу для быстрого выделения.
Используется хоткеями "Ctrl+цифра".
NET_COMMAND_UNIT_SELECT_CONTROL_GROUP = 0x18
РазмерИмяКоментарий
1 байтGroupIndexНомер группы
1 байтаUnknownКто знает
Выделяет группу, прежде назначеную предыдущей командой.
NET_COMMAND_UNIT_SELECT_SUB_GROUP = 0x19
РазмерИмяКоментарий
4 байтаUnitTypeКод с типом юнита
NetTagUnitСам юнит
Меняет выбранную подгруппу юнитов в выделении.
NET_COMMAND_UNIT_REFRESH_SUB_GROUP = 0x1A
Параметры отсутствуют.
Лень изучать.
Создания события
NET_COMMAND_UNIT_SELECTION_EVENT = 0x1B
РазмерИмяКоментарий
1 байтActionТип действия
TNetTagUnitЦелевой юнит
Создает событие выделения в JASS-скрипте.
Выделение прочих объектов
NET_COMMAND_SELECTABLE_SELECTION_MODIFY = 0x1C
РазмерИмяКоментарий
1 байтActionВероятно, тип действия, как и в прочих командах.
TNetTagSelectableЦелевой объект
Используется для выделения предметов и разрушаемых объектов.
Список возможных действий при выделении:
ИмяЗначениеОписание
Add1Добавить юнита в выделение.
Remove2Убрать юнита из выделения.
Set3Вроде как сначала очищает выделение, а затем добавляет юнита. Наверное, используется при одиночном клике на юнита.

Команды кэша

Пролог
Все команды кэша имеют одинаковое начало:
РазмерИмяКоментарий
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-го уровня.

Решение 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". В файле настроек укажите путь к файлу, куда будут записывать команды. Если указать вместо пути минус "-", то запись будет вестись в консоль (если она не будет обнаружена, то программа крашнется).
На каждую исходящую команду создается строчка с записаными в виде шестнадцатеричных чисел байтами из которых она состоит.
Когда игра отсылает буфер с командами хосту, добавляется строчка "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").
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
0
15
11 месяцев назад
0
Сколько байт буфера занимает каждое выделение юнита за один момент времени?
0
27
11 месяцев назад
0
Спасибо. интересная информация.
1
14
11 месяцев назад
1
Всё же, информация о том, что игра может отказаться слать команду из-за переполнения буфера оказалась ложной. Если это происходит, то игра сначала сбрасывает буфер хосту и затем снова проверяет, поместится ли команда и только лишь в противном случае отбрасывает её.
IDA HexRays псевдокод функции отправки команды
void __fastcall NetSendCommandStore(CDataStore *stream, int SessionId)
{
  CNetData *NetData; // esi
  CPlayerWar3 *player; // eax
  DWORD length; // [esp+Ch] [ebp-Ch] MAPDST BYREF
  void *buffer; // [esp+10h] [ebp-8h] BYREF

  NetData = (CNetData *)*((_DWORD *)GetWc3Prop(PROP_SYNDATA)[4] + SYNDATA_NETDATA);
  if ( SessionId == NetData->ActiveSessionId_
    && *(_DWORD *)&NetData->Sessions.buffer_field1EC[0x304 * SessionId + 0x7C] >= GAME_STATE_LOADING )
  {
    stream->vtable->GetValues(stream, &buffer, &length, 0);
    if ( length )
    {
      if ( SessionId
        || !g_GameWar3
        || (player = CGameWar3::GetPlayer(g_GameWar3, g_GameWar3->LocalPlayerId),
            !CPlayerWar3::IsActionLimitReached(player, *(_BYTE *)buffer)) )
      {
        if ( length + NetData->ActionsStream.Length - NetData->ActionsStreamOverhead >= 1024 )
          CNetData::SendCommands(NetData); // после этого вызова буфер очищается
        if ( length + NetData->ActionsStream.Length - NetData->ActionsStreamOverhead >= 1024 )
          debug_log("NetSendCommandStore: rejected, %u bytes\n", length);
        else
          Stream::Write(&NetData->ActionsStream, buffer, length);
      }
    }
  }
}
1
14
11 месяцев назад
Отредактирован IceFog
1
Последние новости!
Обновил статью:
  • Уточнил лимит буфера команд.
  • Убрал связанные с выдуманной потерей данных части.
  • Добавил информацию о командах выделения и синхронизации кэша.
  • Сделал мод для логирования команд.
Сколько байт буфера занимает каждое выделение юнита за один момент времени?
Теперь ты можешь проверить это и сам, но всё же отвечу:
При добавлении одного юнита в выделение, шлется 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 байт).
Следовательно, пошлется до 36 байт.
0
14
11 месяцев назад
Отредактирован host_pi
0
а еще есть такие "нативки":
которые позволяют сихрить в 5 строк все координаты
native JNSetSyncDelay takes integer delay returns nothing
native JNGetSyncDelay takes nothing returns integer
native DzGetTriggerSyncPlayer takes nothing returns player
native DzGetTriggerSyncData takes nothing returns string
native DzSyncData takes string prefix,string data returns nothing
native DzTriggerRegisterSyncData takes trigger trig,string prefix,boolean server returns nothing



пример кода, приводящего к потере контроля
а где пример кода по синхронизации чужих кликов мышки или положения камеры?
или там опять на 2000 строк?
0
14
11 месяцев назад
0
Перезалил архив с модом. Добавил недостающую библиотеку "MinHook_x86.dll".
Надо будет разобраться, как её статически скомпоновать с библиотекой на FreePascal.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.