Немного общих слов.
Этот вопрос был решен написанием внешнего приложения, внедряющегося в адресное пространство процесса trainz.exe и обменивающимся данными со скриптом через участок его памяти. Намеренно не стану сообщать, на каком языке она написана, так как тут же появятся [strike]проповедники языка истинного[/strike] «вумные» [strike]сионисты и пасквилянты[/strike] программисты , которым никак [strike]неймется[/strike] не удается [strike]не облить грязью всех вокруг и[/strike] свой [strike]фанатизм[/strike] богатый запас знаний [strike]засунуть в одно место[/strike] донести до [strike]«быдлокодеров»[/strike] невежественной толпы. Не буду вдаваться и в подробности реализации, скажу лишь самое основное. Участок памяти представляет собой строковый массив, единожды размещаемый скриптом в памяти. При инициализации в этот массив помещается особая сигнатура (к слову, всегда одна и та же), которую впоследствии и отыскивает в памяти процесса программа среди океана байт. Считывание-запись данных осуществляется по очереди то скриптом, то программой (определяется состоянием первого байта массива). Размер участка памяти строго фиксирован (пока 10 Кбайт) и в процессе игры не изменяется (впоследствии размер будет задаваться в правилах сессии).
Кстати говоря, следует сказать пару слов по поводу следующего высказывания
ну ну Гении - влезть в чужой бинарник своими руками и без вылетов - посмотрим
Не спорю, ковыряться в чужом адресном пространстве не безопасно, но за время тестирования (более 50 запусков) не произошло ни одного "вылета" (до тех пор, пока не занялся голосовым чатом и не начал развешивать глобальный хук на клавиатуру ).
Обмен данными между экземплярами программ-клиентов по сети осуществляется через DirectPlay. Что до вумных программистов, считающих интерфейсами зла все кроме сокетов, [strike]то пусть продолжают заниматься с winsock многопоточным онанизмом[/strike] желаю им приятного времяпровождения при конструировании давно изобретенных велосипедов. Так как на моих глючных F{пи-и-ик}YouBill’ьных окошках DirectX версии 8 и более новые частично не работает (зато отлично работает то, что криво работало в «чистой» системе ), пришлось довольствоваться DP из состава DirectX 7. Данные по сети передаются в сжатом с помощью библиотеки zlib виде.
На настоящий момент реализован также голосовой чат. Чтобы можно было устанавливать соединения с кем-либо, нужно назначить во вкладке «голосовой чат» соответствующую горячую клавишу. Управление чатом следующее: нажимаем на клавишу «Alt», затем не отпуская его жмем и отпускаем назначенную данному игроку клавишу (Alt не отпускаем) – всё, «исходящий канал открыт». Для закрытия канала нужно всего-навсего отпустить «Alt».
Голосовые сообщения отправляются по частям (1 часть – 0,2 мсек) также в сжатом виде. Для сжатия используется кодек speex (ох, и запыхался же с ним! ). Битрейт, однако же, получается просто неблагопристойно большой – 15-20 Кбайт/сек на сообщение (несмотря на то, что кодеку назначено сжимать до 15 Кбит/сек). Поддерживается только одно одновременное исходящее соединение (при этом оно может быть широковещательным и транслироваться сразу всем) и теоретически неограниченное количество входящих.
Пожалуй, довольно лелеять функционал программы-клиента. Тем более, что сама программа ещё до конца не отлажена, и то ли из-за несовместимости глобальных клавиатурных хуков с отладчиком, то ли из-за неправильного использования CopyMemory (никак не получается сыскать источник-рассадник багов), все вместе с отладчиком систематически накрывается медным тазом, сопровождаясь победоносным восклицанием Билла: «память не может быть “read”!». (знаете, порой так хочется открыть kernel32.exe в каком-нибудь PELord’е, откуда родом эта CopyMemory, и в константе подменить «read» на какое-нибудь непристойное выражение… )
Протокол
Полагаю, что всех уже утомил своими пустыми разглагольствованиями, поэтому этот раздел постараюсь написать коротко и ясно. За передачу данных в скрипте от игры до игры отвечает предназначенный для этой цели статический класс Modem. Данные передаются вот такими пакетами:
- Код: Выделить всё
class Packet {
public string major; //первое поле данных, полностью в вашем распоряжении
public string minor; //второе поле данных, также полностью в вашем распоряжении
public string type;//тип пакета (об этом далее)
public int DestPlayerNum;//номер игрока, кому сообщение должно быть доставлено (нумерация с нуля) (ставьте -2 для отправки широковещательного сообщения)
public int SrcPlayerNum;//номер текущего игрока
}
Пара слов о типе. Много разных дополнений могут одновременно посылать и получать пакеты. Чтобы различные объекты не путали пакеты, и таким образом не воцарился хаос, каждому пакету назначен тип, определяющий, какой объект его породил. При отправке пакета в поле type нужно ОБЯЗАТЕЛЬНО занести kuid вашего объекта. Можно, конечно, указать чужой kuid, но во избежание путаницы этого лучше не делать.
В поле DestPlayerNum может принимать не только значения номеров игроков получателя, но и дополнительные 2 специальных значения:
-1 – отправка служебного сообщения программе (для отправки специальных сообщений, например, управляющих голосовым чатом)
-2 – отправка широковещательного сообщения (сообщения, отправляемого всем игрокам одновременно)
Поле SrcPlayerNum можно не указывать: оно будет назначено при отправке сообщения.
Так как фрагментация пакетов не поддерживается, длина пакета ограничена размером выходного буфера, и вам следует следить, чтобы она не была превышена (иначе пакет не будет помещен в очередь). Для этого у класса Modem есть метод public bool VeryBigPacket(Packet p), возвращающий истину, если пакет не умещается в буфере отправки.
У класса Modem есть много различных public-методов, но в вашем распоряжении есть только следующие (остальные, что называется «системные»):
- public int MaxPacketSize() – получение максимально возможной длины исходящего пакета
- public bool ConnectionStatus() - возвращает истину, если соединение установлено
- public int GetCurrentPlayerNum() – получение порядкового номера текущего игрока (нумерация начинается с нуля) на данный момент
- public int GetTotalPlayers() – метод для получения общего количества игроков в игре на данный момент
- public bool VeryBigPacket(Packet p) – метод, проверяющий, уместится ли пакет в буфере отправки
- public void PushPacket(Packet p) – метод для отправки пакета. Пакет помещается в конец очереди отправки пакетов.
- public void AddHandler(Handler h) – добавление обработчика сообщений (об этом ниже)
- public void RemoveHandler(Handler h) – удаление обработчика сообщений
- public KUID TypeOfIncomingPackets
- public void RecievePacket(Packet p)
- public void ConnectionOptionsChanged()
В поле TypeOfIncomingPackets ОБЯЗАТЕЛЬНО должен быть установлен куид пакетов, которые обрабатываются этим обработчиком. Никакие другие пакеты в данный обработчик поступать не будут (в принципе можно указать любой куид, не обязательно, чтобы он совпадал с куидом вашего объекта).
При поступлении каждого сообщения с указанным куидом вызвается метод RecievePacket (блин, только сейчас узнал, что правильно пишется Receive ). Метод ConnectionOptionsChanged вызывается при каких-либо изменениях состояния соединения (установлено/разорвано соединение, подключился новый/отключился существующий игрок, …).
Естественно, просто так обработчик сообщений исполнять своих функций не будет. Чтобы он начал обрабатывать сообщения, ссылку на него нужно передать модему, для чего у последнего есть процедура AddHandler. Она помещает ссылку на обработчик в особую иерархическую структуру – список обработчиков, после чего пакеты с соответствующим куидом начнут обрабатываться. На один и тот же куид может быть назначено несколько обработчиков. Можно и удалить обработчик из списка с помощью метода RemoveHandler.
Существует статический класс LocalPlayerOwnership для описания объектов, доступ к которым локальному игроку открыт. Можно использовать его методы, чтобы определить, управляет ли некоторым объектом локальный игрок:
- public bool TrainUnderControl(Train Tr) – функция проверяет, управляется ли поезд текущим игроком
- public bool JunctionUnderControl(string JName) – функция проверяет, управляется ли стрелка текущим игроком
- public bool JunctionIsShared(string JName) – функция проверяет, является ли стрелка «общедоступной»
- public bool JunctionIsMy(string JName) – функция проверяет, находится ли стрелка в «вотчине» текущего игрока
- public bool JunctionIsOnlyMy(string JName) – функция проверяет, управляется ли стрелка текущим игроком и никем больше
- Код: Выделить всё
include “Buildable.gs”
include “MultiplayerClasses.gs”
class MyObject isclass Buildable, Handler { //можно и не buildable, хоть DriverCharacter
public void RecievePacket(Packet p) {
if (p.major==”Paket1”)
Interface.Print(“Prishol paket 1 takogo soderjanuja : ” + p.minor);
if (p.major==”Paket2”)
Interface.Print(“Prishol paket 2 takogo soderjanuja : ” + p.minor);
}
public void ConnectionOptionsChanged()
{
if (Modem.ConnectionStatus()) {
Packet p=new Packet();
p.type = TypeOfIncomingPackets.GetLogString();
p.major = “Packet1”;
p.minor = “YaYa!!!”;
p.DstPlayerNum = -2; //всем-всем …
Modem.PushPacket(p);
}
}
public void Init(void)
{
inherited();
TypeOfIncomingPackets=GetAsset().GetKUID();
Modem.AddHandler(me);
}
};