Дорогие, друзья!
К сожалению мой проект не стал опенсурсом. Не стал не потому, что я "зажал" код, а потому что труды мои никто в конце концов не оценит. Да, да, даже ника не вспомните. Поэтому я пилил эмулятор в привате. Сегодня хочу сообщить вам, что разработка эмулятора почти прекращена. Причина очевидная - разработчики изменяют сетевую часть игры каждый вторник, среду. Новые пакеты, новые опкоды. Идти по пути обновлений означает тратить свое время на сбор новых опкодов и разбор структуры пакетов. Не имею желания больше заниматься этим говном. Не потому что игра плохая, игра отличная, но потому что разработчики сами не определились со своей игрой. На этом сопельки можно закончить.
Что я планирую сделать в этом топике? Нет, я не выложу вам сурсы и свои наработки, поскольку это означало бы дать вам рыбу, сожрав которую, вы пришли бы в поисках очередной. Я дам вам удачку, ловите сами.
Большая часть наработок принадлежит разработчикам Атомикса, ребятам, которые поделились со мной своими наработками. Вопрос о доступности кода мы обсуждали, и они не против, если я покажу вам отрывки кода snippets.
Я покажу структуры серверов, покажу опкоды, пакеты, крипты и всякое навроде этого постепенно и шаг за шагом. Вы сможете сами запилить себе эмулятор. Как вы поступите со своими наработками мне не важно. Расшарите или продадите, но начальная информация будет общедоступной на этом форуме.
Язык программирования будет C# и все примеры будут включать в себя принципы C#. Но поскольку я не планирую отдать вам рабочий сурс ввиде "просто копипейст энд мейк билд", вы сможете переписать под любой вам нужный язык.
Данный топик, пожалуй посвящен людям с опытом в кодинге, нубы и нубские вопросы не приветствуются, т.к. обучать кого-либо я не собираюсь. Нуб? Проходи мимо. Также не приветствуются в топике "Одминистраторы фришек" с вопросами "когда уже выложат рабочий сервер, чтобы на школьниках можно было рубить бабло?". Никто вам ничего не выложит, и если у вас есть деньги но нету мозгов, вам тоже никто не собирается помогать, в частности я.
Поэтому не пишите мне в ЛС с предложениями.
Ну а теперь перейдем к технической части топика. Начнем естественно с логин сервера.
Формируется пакет так:
[SRC="c++"]public class BNSLoginPacket : Packet<LoginPacketOpcode>
{
public string Command { get; set; }
public int Serial { get; set; }
public byte WorldID { get; set; }
public string Content { get; set; }
protected XmlDocument ReadLoginPacket()
{
Serial = GetInt(2);
WorldID = GetByte();
Content = Encoding.UTF8.GetString(GetBytes((ushort)(Length - 7), 7));
XmlDocument xml = new XmlDocument();
xml.LoadXml(Content);
return xml;
}
public void WritePacket()
{
string res = Serial != 0 ?
string.Format("{0}\r\ns:{1}R\r\nl:{2}\r\n\r\n{3}", Command, Serial, Encoding.UTF8.GetByteCount(Content), Content) :
string.Format("{0}\r\nl:{1}\r\n\r\n{2}", Command, Encoding.UTF8.GetByteCount(Content), Content);
byte[] buf = Encoding.UTF8.GetBytes(res);
PutBytes(buf, 2);
}
public override string ToString()
{
var ser = GetInt(2);
var world = GetByte();
var content = Encoding.UTF8.GetString(GetBytes((ushort)(Length - 7), 7));
return string.Format("{0}\n", content);
}
}[/SRC]
ЛогинСервер включает в себя следующие пакеты:
CM_STS_CONNECT //просто соединение
CM_AUTH_LOGIN_START //запрашивает данные логина
CM_AUTH_KEY_DATA //запрашивает обмен ключами
CM_AUTH_LOGIN_FINISH //завершает авторизацию
CM_AUTH_GAME_TOKEN //передает ключ сессии
CM_AUTH_TOKEN //хандшейк между клиентом и логином
CM_ACCOUNT_LIST //кол-во аккаунтов
CM_WORLD_LIST //кол-во серверов
CM_CHAR_LIST //кол-во персонажей
CM_CHAR_SLOT_REQUEST //запрашиваем кол-во слотов
CM_CHAR_CREATE //создаем персонажа
CM_CHAR_DELETE //удаляем персонажа
CM_SLOT_LIST //кол-во слотов
CM_USER_INFO //данные о персонаже
CM_ERROR_RETURN //заглушка для необработанных пакетов
CM_PING //просто пинг
Есть два типа пакетов. Первый вызывает обработчик, второй формирует данные.
CM_ACCOUNT_LIST
[SRC="c++"]public CM_ACCOUNT_LIST()
{
this.ID = LoginPacketOpcode.CM_ACCOUNT_LIST;
}
public override Packet<LoginPacketOpcode> New()
{
return new CM_ACCOUNT_LIST();
}
public override void OnProcess(Session<LoginPacketOpcode> client)
{
((LoginSession)client).OnAccountList(GetInt(2));
}[/SRC]
Такие все, за исключением следующих:
CM_AUTH_KEY_DATA
[SRC="c++"]public CM_AUTH_KEY_DATA()
{
this.ID = LoginPacketOpcode.CM_AUTH_KEY_DATA;
}
public override Packet<LoginPacketOpcode> New()
{
return new CM_AUTH_KEY_DATA();
}
public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
byte[] keyData = null;
root = xml["Request"];
list = root.ChildNodes;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "keydata":
keyData = Convert.FromBase64String(i.InnerText);
break;
}
}
xml = null;
System.IO.MemoryStream ms = new System.IO.MemoryStream(keyData);
System.IO.BinaryReader br = new System.IO.BinaryReader(ms);
byte[] exchangeKey = br.ReadBytes(br.ReadInt32());
byte[] hash = br.ReadBytes(br.ReadInt32());
string checkHash = Convert.ToBase64String(hash);
((LoginSession)client).OnAuthKeyData(exchangeKey, checkHash, Serial);
}
}[/SRC]
CM_AUTH_LOGIN_START:
[SRC="c++"]public CM_AUTH_LOGIN_START()
{
this.ID = LoginPacketOpcode.CM_AUTH_LOGIN_START;
}
public override Packet<LoginPacketOpcode> New()
{
return new CM_AUTH_LOGIN_START();
}
public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
string loginName = "";
root = xml["Request"];
list = root.ChildNodes;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "loginname":
loginName = i.InnerText;
break;
}
}
xml = null;
((LoginSession)client).OnAuthLoginStart(loginName, Serial);
}[/SRC]
CM_CHAR_CREATE:
[SRC="c++"]public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
byte[] keyData = null;
root = xml["bns"];
list = root.ChildNodes;
byte slotID = 0;
string charName = "";
byte[] charData = null;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "slotid":
Guid id = Guid.Parse(i.InnerText);
for (slotID = 0; slotID < 5; slotID++)
{
if (((uint)slotID).ToGUID() == id)
break;
}
break;
case "charname":
charName = i.InnerText;
break;
case "chardata":
charData = Convert.FromBase64String(i.InnerText);
break;
}
}
xml = null;
((LoginSession)client).OnCharCreate(GetInt(2), WorldID, slotID, charName, charData);
}[/SRC]
CM_CHAR_DELETE:
[SRC="c++"]public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
root = xml["bns"];
list = root.ChildNodes;
byte slotID = 0;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "slotid":
Guid id = Guid.Parse(i.InnerText);
for (slotID = 0; slotID < 5; slotID++)
{
if (((uint)slotID).ToGUID() == id)
break;
}
break;
}
}
xml = null;
((LoginSession)client).OnCharDelete(GetInt(2), slotID);
}[/SRC]
CM_CHAR_SLOT_REQUEST:
[SRC="c++"]public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
string loginName = "";
root = xml["Request"];
list = root.ChildNodes;
byte slotID = 0;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "slotid":
Guid id = Guid.Parse(i.InnerText);
for (slotID = 0; slotID < 5; slotID++)
{
if (((uint)slotID).ToGUID() == id)
break;
}
break;
}
}
xml = null;
((LoginSession)client).OnCharSlotRequest(GetInt(2), slotID);
}[/SRC]
При входе:
[SRC="c++"]public partial class LoginSession : Session<LoginPacketOpcode>
{
int loginSerial;
string checkHash;
Account account;
AccountLoginResult lastLoginRes = AccountLoginResult.INVALID_PASSWORD;
public Account Account { get { return account; } }
public void OnAuthLoginStart(string loginName, int serial)
{
loginSerial = serial;
AccountSession.Instance.RequestAccountInfo(loginName.Split('@')[0], this);
Logger.ShowInfo(loginName + " попытка соединения");
}
public void OnAccountInfo(AccountLoginResult res, Account account)
{
if (res == AccountLoginResult.NO_SUCH_ACCOUNT)
Logger.ShowInfo("Результат соединения:" + res.ToString());
switch (res)
{
case AccountLoginResult.OK:
{
this.account = account;
this.Network.Crypt.KeyExchange.MakePrivateKey();
((BNSKeyExchange)this.Network.Crypt.KeyExchange).Username = account.UserName;
((BNSKeyExchange)this.Network.Crypt.KeyExchange).Password = account.Password;
byte[] exchange = this.Network.Crypt.KeyExchange.GetKeyExchangeBytes(Mode.Server);
byte[] session = ((BNSKeyExchange)this.Network.Crypt.KeyExchange).Session.getBytes();
System.IO.MemoryStream ms = new System.IO.MemoryStream();
System.IO.BinaryWriter bw = new System.IO.BinaryWriter(ms);
bw.Write(session.Length);
bw.Write(session);
bw.Write(exchange.Length);
bw.Write(exchange);
string key = string.Format("<Reply>\n<KeyData>{0}</KeyData>\n</Reply>\n", Convert.ToBase64String(ms.ToArray()));
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = loginSerial;
p.Content = key;
p.WritePacket();
this.Network.SendPacket(p);
}
break;
case AccountLoginResult.NO_SUCH_ACCOUNT:
case AccountLoginResult.DB_ERROR:
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 400 ErrAccountNotFound";
p.Serial = loginSerial;
p.Content = "<Error code=\"3002\" server=\"1008\" module=\"1\" line=\"458\"/>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
break;
}
}
public void OnAuthKeyData(byte[] exchangeKey, string checkHash, int serial)
{
this.Network.Crypt.KeyExchange.MakeKey(Mode.Server, exchangeKey);
string hash = ((BNSKeyExchange)this.Network.Crypt.KeyExchange).Authentication;
if (checkHash == hash.Split(',')[0])
{
this.checkHash = hash.Split(',')[1];
loginSerial = serial;
AccountSession.Instance.AccountLogin(account.AccountID, this);
}
else
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 400 ErrBadPasswd";
p.Serial = serial;
p.Content = "<Error code=\"11\" server=\"1012\" module=\"1\" line=\"1683\"/>\n";
p.Encrypt = false;
p.WritePacket();
this.Network.SendPacket(p);
Logger.ShowInfo("Login result for " + account.UserName + ": BadPassword");
}
}
public void OnAccountLoginResult(AccountLoginResult result)
{
lastLoginRes = result;
if (result == AccountLoginResult.OK)
{
account.LastLoginTime = DateTime.Now;
account.LastLoginIP = Network.Socket.RemoteEndPoint.ToString().Split(':')[0];
System.IO.MemoryStream ms = new System.IO.MemoryStream();
System.IO.BinaryWriter bw = new System.IO.BinaryWriter(ms);
bw.Write(32);
bw.Write(Convert.FromBase64String(checkHash));
string res = Convert.ToBase64String(ms.ToArray());
string key = string.Format("<Reply>\n<KeyData>{0}</KeyData>\n</Reply>\n", res);
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = loginSerial;
p.Content = key;
p.Encrypt = false;
p.WritePacket();
this.Network.SendPacket(p);
Logger.ShowInfo(account.UserName + " login successful!");
}
else
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 400 ErrBadPasswd";//should be already log in
p.Serial = loginSerial;
p.Content = "<Error code=\"11\" server=\"1012\" module=\"1\" line=\"1683\"/>\n";
p.Encrypt = false;
p.WritePacket();
this.Network.SendPacket(p);
Logger.ShowInfo("Login result for " + account.UserName + ": ALREADY_LOG_IN");
AccountSession.Instance.AccountLogout(account.AccountID, this);
}
}
public void OnAuthLoginFinish(int serial)
{
Guid accountGuid = account.AccountID.ToGUID();
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "POST /Presence/UserInfo STS/1.0";
p.Serial = 0;
p.Content = string.Format("<Message>\n<UserId>{0}</UserId>\n<UserCenter>1</UserCenter>\n<UserName>{1}</UserName>\n<Status>online</Status>\n<Aliases type=\"array\">\n<Alias>{1}</Alias>\n<Alias>bns:{1}</Alias>\n</Aliases>\n</Message>\n", accountGuid.ToString().ToUpper(), account.UserName);
p.WritePacket();
this.Network.SendPacket(p);
p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply>\n<UserId>{0}</UserId>\n<UserCenter>1</UserCenter>\n<Roles type=\"array\"/>\n<LocationId>32B0E246-1D5F-4AC1-AC30-E462AAF4C870</LocationId>\n<AccessMask>1073741823</AccessMask>\n<UserName>{1}</UserName>\n</Reply>\n", accountGuid.ToString().ToUpper(), account.UserName);
Logger.ShowInfo("account:" + accountGuid.ToString());
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnUserInfo(int serial)
{
Guid accountGuid = account.AccountID.ToGUID();
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply>\n<UserId>{0}</UserId>\n<UserCenter>1</UserCenter>\n<UserName>{1}</UserName>\n<LoginName>{1}@plaync.co.kr</LoginName>\n<UserStatus>1</UserStatus>\n</Reply>\n", accountGuid.ToString().ToUpper(), account.UserName);
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnAuthGameToken(int serial)
{
account.LoginToken = ((uint)Global.Random.Next()).ToGUID();
Logger.ShowInfo("gametoken:" + account.LoginToken.ToString().ToUpper());
account.TokenExpireTime = DateTime.Now.AddMinutes(10);
AccountSession.Instance.AccountSave(account, this);
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply>\n<Token>{0}</Token>\n</Reply>\n", account.LoginToken.ToString().ToUpper());
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnAuthToken(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string token = account.AccountID.ToGUID().ToString().ToUpper() + ":" + ((uint)Global.Random.Next()).ToGUID().ToString().ToUpper();
Logger.ShowInfo("Auth Token: " + token);
p.Content = string.Format("<Reply>\n<AuthnToken>{0}</AuthnToken>\n</Reply>\n", Convert.ToBase64String(Encoding.Default.GetBytes(token)));
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnError(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = serial == 8 ? "STS/1.0 400 ErrSecondPasswordNotFound" : "STS/1.0 400 ErrPermission";
p.Serial = serial;
//<Error code="3333" server="107" module="2" line="811"/>
p.Content = serial == 8 ? "<Error code=\"3333\" server=\"107\" module=\"2\" line=\"811\"/>\n" : "<Error code=\"49\" server=\"1001\" module=\"8000\" line=\"1086\"/>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnAccountList(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply type=\"array\">\n<GameAccount>\n<Alias>{0}</Alias>\n<Created>2013-06-08T17:45:58.689+09:00</Created>\n</GameAccount>\n</Reply>\n", account.AccountID.ToGUID().ToString().ToUpper());
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnWorldList(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string worlds = "";
int idx = 1;
foreach (WorldInfo i in Configuration.Instance.Worlds)
{
worlds += string.Format("<World>\n<WorldCode>{0}</WorldCode>\n<WorldName>{1}</WorldName>\n<PublicNetAddress>{2}</PublicNetAddress>\n<Property>\n<EnableCharacterCreate>true</EnableCharacterCreate>\n<WorldScore>0</WorldScore>\n</Property>\n<UserCounts>\n<PlayingUsers>426</PlayingUsers>\n<WaitingUsers>0</WaitingUsers>\n<MaxUsers>4000</MaxUsers>\n</UserCounts>\n</World>\n",
idx++, i.Name, i.Address);
}
p.Content = string.Format("<Reply type=\"array\">\n{0}\n</Reply>\n", worlds);
p.WritePacket();
this.Network.SendPacket(p);
}
}[/SRC]
При чтении данных персонажа:
[SRC="c++"]public partial class LoginSession : Session<LoginPacketOpcode>
{
int characterSerial = 0, removeSerial = 0;
Dictionary<byte, ActorPC> chars;
int currentCharIndex;
ActorPC createdPC, removePC;
public void OnCharList(int serial)
{
characterSerial = serial;
CharacterSession.Instance.RequestCharList(account.AccountID, this);
}
public void OnGotCharList(List<ActorPC> chars)
{
this.chars=new Dictionary<byte,ActorPC>();
foreach (ActorPC i in chars)
this.chars[i.SlotID] = i;
currentCharIndex = 0;
LoadInventory();
}
public void SendSlotList(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string content = "<Reply type=\"array\">\n";
for (byte i = 0; i < (3 + this.account.ExtraSlots); i++)
{
content += string.Format("<Slot>\n<SlotId>{0}</SlotId>\n<AppGroupId>2</AppGroupId>\n<SlotType>char</SlotType>\n<SystemSlot>1</SystemSlot>\n<Changed>2012-04-25T05:52:22Z</Changed>\n<Registered>2012-04-25T05:52:22Z</Registered>\n</Slot>\n",
((uint)i).ToGUID().ToString().ToUpper());
}
p.Content = content + "</Reply>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
public void SendCharList()
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = characterSerial;
string content = "<Reply>\n";
for (byte i = 0; i < 5; i++)
{
if (chars.ContainsKey(i))
{
ActorPC pc = chars[i];
System.IO.MemoryStream ms = new System.IO.MemoryStream();
pc.AppearenceToSteam(ms);
content += string.Format("<CharSlot>\n<SlotId>{0}</SlotId>\n<CharId>{1}</CharId>\n<WorldCode>{4}</WorldCode>\n<CharName>{2}</CharName>\n<CharData><bns><pcdbid>{1}</pcdbid><showcase>{3}</showcase></bns></CharData>\n</CharSlot>\n",
((uint)pc.SlotID).ToGUID(), pc.CharID, pc.Name, Convert.ToBase64String(ms.ToArray()), pc.WorldID);
ms.Close();
ms = null;
}
else
content += string.Format("<CharSlot><SlotId>{0}</SlotId>\n</CharSlot>\n", ((uint)i).ToGUID());
}
p.Content = content + "</Reply>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnCharSlotRequest(int serial, byte slotID)
{
if (chars.ContainsKey(slotID))
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string content = "<Reply>\n";
ActorPC pc = chars[slotID];
System.IO.MemoryStream ms = new System.IO.MemoryStream();
pc.AppearenceToSteam(ms);
content += string.Format("<SlotId>{0}</SlotId>\n<CharId>{1}</CharId>\n<WorldCode>{4}</WorldCode>\n<CharName>{2}</CharName>\n<CharData><bns><pcdbid>{1}</pcdbid><showcase>{3}</showcase></bns></CharData>\n",
((uint)pc.SlotID).ToGUID(), pc.CharID, pc.Name, Convert.ToBase64String(ms.ToArray()), pc.WorldID);
ms.Close();
ms = null;
p.Content = content + "</Reply>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
}
public void OnCharDelete(int serial, byte slotID)
{
if (chars.ContainsKey(slotID))
{
removePC = chars[slotID];
removeSerial = serial;
CharacterSession.Instance.DeleteChar(removePC.CharID, this);
}
}
public void OnCharDeleteResult(SM_CHAR_DELETE_RESULT.Results Result)
{
if (Result == SM_CHAR_DELETE_RESULT.Results.OK && removePC != null)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = removeSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>DeletePc</command>\n<result>OK</result>\n<slotid>{0}</slotid>\n</bns>\n",
((uint)removePC.SlotID).ToGUID(), removePC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
if (chars.ContainsKey(removePC.SlotID))
chars.Remove(removePC.SlotID);
}
else
{
removePC = null;
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = removeSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>DeletePc</command>\n<result>Fail</result>\n<slotid>{0}</slotid>\n<reason>202</reason></bns>\n",
((uint)removePC.SlotID).ToGUID(), removePC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
}
}
public void OnCharCreate(int serial, byte worldID, byte slotID, string charName, byte[] charData)
{
System.IO.MemoryStream ms = new System.IO.MemoryStream(charData);
System.IO.BinaryReader br = new System.IO.BinaryReader(ms);
ms.Position = 2;
byte[] display1 = Conversions.HexStr2Bytes("6363634E63737364777777636464776464646464646464");
br.ReadBytes(br.ReadInt16()).CopyTo(display1, 0);
ActorPC pc = new ActorPC();
pc.Level = 1;
pc.AccountID = account.AccountID;
pc.Name = charName;
pc.SlotID = slotID;
pc.WorldID = worldID;
pc.Appearence1 = display1;
ms.Position += 2;
pc.Race = (Race)br.ReadByte();
ms.Position += 2;
pc.Gender = (Gender)br.ReadByte();
ms.Position += 2;
pc.Job = (Job)br.ReadByte();
ms.Position += 2;
pc.Appearence2 = br.ReadBytes(br.ReadInt16());
pc.MapID = 1101;
pc.X = -3177;
pc.Y = 9243;
pc.Z = 599;
pc.Dir = 45;
pc.UISettings = "";
pc.InventorySize = 32;
switch (pc.Job)
{
case Job.Assassin:
pc.HP = 66;
pc.MP = 0;
pc.MaxHP = 66;
pc.MaxMP = 10;
break;
case Job.ForceMaster:
pc.HP = 61;
pc.MP = 10;
pc.MaxHP = 61;
pc.MaxMP = 10;
break;
case Job.KungfuMaster:
pc.HP = 109;
pc.MaxHP = 109;
pc.MP = 0;
pc.MaxMP = 10;
break;
case Job.BladeMaster:
default:
pc.HP = 99;
pc.MP = 0;
pc.MaxHP = 99;
pc.MaxMP = 10;
break;
}
characterSerial = serial;
createdPC = pc;
CharacterSession.Instance.CreateChar(pc, this);
}
public void OnCharCreateResult(uint charID,SM_CHAR_CREATE_RESULT.Results Result)
{
if (Result == SM_CHAR_CREATE_RESULT.Results.OK)
{
createdPC.CharID = charID;
Base.Quests.Quest q = new Base.Quests.Quest();
q.QuestID = 250;
q.Step = 1;
q.NextStep = 1;
createdPC.Quests[q.QuestID] = q;
CharacterSession.Instance.CharacterSave(createdPC);
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = characterSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>CreatePc</command>\n<result>OK</result>\n<slotid>{0}</slotid>\n</bns>\n",
((uint)createdPC.SlotID).ToGUID(), createdPC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
if (!chars.ContainsKey(createdPC.SlotID))
chars[createdPC.SlotID] = createdPC;
}
else
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = characterSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>CreatePc</command>\n<result>Fail</result>\n<slotid>{0}</slotid>\n<reason>202</reason>\n</bns>\n",
((uint)createdPC.SlotID).ToGUID(), createdPC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
}
}
}[/SRC]
[SRC="c++"]public class BNSLoginNetwork<T> : Network<T>
{
new string lastContent = "";
public override Network<T> CreateNewInstance(System.Net.Sockets.Socket sock, Dictionary<T, Packet<T>> commandTable, Session<T> client)
{
BNSLoginNetwork<T> instance = new BNSLoginNetwork<T>();
CreateNewInstance(instance, sock, commandTable, client);
return instance;
}
protected override void OnReceivePacket(byte[] buffer)
{
if (Crypt.KeyExchange.IsReady)
Crypt.Decrypt(buffer, 0, buffer.Length);
string content = lastContent + Encoding.UTF8.GetString(buffer);
try
{
ParseContent(content);
}
catch (Exception ex)
{
Logger.ShowError(ex);
}
}
void ParseContent(string content)
{
System.IO.StringReader sr = new System.IO.StringReader(content);
string header = sr.ReadLine();
int length = int.Parse(sr.ReadLine().Split(':')[1]);
string tmp = sr.ReadLine();
string serial="";
if (tmp != "")
{
serial = tmp;
do
{
tmp = sr.ReadLine();
} while (tmp != "" && sr.Peek() != -1);
}
byte worldID;
LoginPacketOpcode opcode = ParseHeader(header, out worldID);
if (opcode == LoginPacketOpcode.Unknown)
{
Logger.ShowError("Неизвестный пакет:");
Logger.ShowError(content);
}
byte[] buf = Encoding.UTF8.GetBytes(sr.ReadToEnd());
if (buf.Length >= length)
{
byte[] tmp2 = new byte[length];
Array.Copy(buf, 0, tmp2, 0, length);
Packet<T> p = new Packet<T>();
p.ID = (T)(object)(int)opcode;
p.PutInt(serial != "" ? int.Parse(serial.Split(':')[1]) : 0, 2);
p.PutByte(worldID);
p.PutBytes(tmp2, 7);
ProcessPacket(p);
if (length < buf.Length)
ParseContent(Encoding.UTF8.GetString(buf, length, buf.Length - length));
else
lastContent = "";
}
else
lastContent = content;
}
LoginPacketOpcode ParseHeader(string header ,out byte worldID)
{
string[] token = header.Split(' ');
return GetOpcode(token[1], out worldID);
}
public static LoginPacketOpcode GetOpcode(string header,out byte worldID)
{
worldID = 1;
if (header.Contains("/Game.bns."))
{
string[] token = header.Split('/');
worldID = byte.Parse(token[1].Substring(9));
token[1] = token[1].Substring(0, 8);
header = "/" + token[1] + "/" + token[2];
}
switch (header)
{
case "/Sts/Connect":
return LoginPacketOpcode.CM_STS_CONNECT;
case "/Auth/LoginStart":
return LoginPacketOpcode.CM_AUTH_LOGIN_START;
case "/Auth/KeyData":
return LoginPacketOpcode.CM_AUTH_KEY_DATA;
case "/Auth/LoginFinish":
return LoginPacketOpcode.CM_AUTH_LOGIN_FINISH;
case "/Auth/RequestToken":
return LoginPacketOpcode.CM_AUTH_TOKEN;
case "/Auth/RequestGameToken":
return LoginPacketOpcode.CM_AUTH_GAME_TOKEN;
case "/GameAccount/ListMyAccounts":
return LoginPacketOpcode.CM_ACCOUNT_LIST;
case "/World/ListWorlds":
return LoginPacketOpcode.CM_WORLD_LIST;
case "/Slot/ListCharSlots":
return LoginPacketOpcode.CM_CHAR_LIST;
case "/Slot/GetCharSlot":
return LoginPacketOpcode.CM_CHAR_SLOT_REQUEST;
case "/Game.bns/CreatePC":
return LoginPacketOpcode.CM_CHAR_CREATE;
case "/Game.bns/DeletePC":
return LoginPacketOpcode.CM_CHAR_DELETE;
case "/Sts/Ping":
return LoginPacketOpcode.CM_PING;
case "/Slot/ListSlots":
return LoginPacketOpcode.CM_SLOT_LIST;
case "/Auth/GetMyUserInfo":
return LoginPacketOpcode.CM_USER_INFO;
case "/SecondPassword/GetStatus":
case "/Grade.bns/GetGameGrade":
case "/Friend/GetUserInfo":
case "/VirtualCurrency/GetBalance":
case "/Friend/PageRecvProposals":
case "/Goods/ListCategoryIds":
return LoginPacketOpcode.CM_ERROR_RETURN;
}
return LoginPacketOpcode.Unknown;
}
public override void SendPacket(Packet<T> p, bool noWarper)
{
throw new NotImplementedException();
}
public override void SendPacket(Packet<T> p)
{
byte[] buf = p.GetBytes((ushort)(p.Length - 2), 2);
if (Crypt.KeyExchange.IsReady && p.Encrypt)
Crypt.Encrypt(buf, 0, buf.Length);
SendPacketRaw(buf, 0, buf.Length);
}
}[/SRC]
Этой информации достаточно, чтобы написать рабочий логин сервер. Да, вы не нашли здесь систем создания аккаунтов, соединение с БД и т.д. Я намеренно не буду выкладывать такие вещи, поскольку каждый кто будет писать свой эмулятор, напишет эти модули под себя и под свою выбранную БД.
Продолжение следует.
Топик буду обновлять по мере ваших успехов и продвижений в разработке.
К сожалению мой проект не стал опенсурсом. Не стал не потому, что я "зажал" код, а потому что труды мои никто в конце концов не оценит. Да, да, даже ника не вспомните. Поэтому я пилил эмулятор в привате. Сегодня хочу сообщить вам, что разработка эмулятора почти прекращена. Причина очевидная - разработчики изменяют сетевую часть игры каждый вторник, среду. Новые пакеты, новые опкоды. Идти по пути обновлений означает тратить свое время на сбор новых опкодов и разбор структуры пакетов. Не имею желания больше заниматься этим говном. Не потому что игра плохая, игра отличная, но потому что разработчики сами не определились со своей игрой. На этом сопельки можно закончить.
Что я планирую сделать в этом топике? Нет, я не выложу вам сурсы и свои наработки, поскольку это означало бы дать вам рыбу, сожрав которую, вы пришли бы в поисках очередной. Я дам вам удачку, ловите сами.
Большая часть наработок принадлежит разработчикам Атомикса, ребятам, которые поделились со мной своими наработками. Вопрос о доступности кода мы обсуждали, и они не против, если я покажу вам отрывки кода snippets.
Я покажу структуры серверов, покажу опкоды, пакеты, крипты и всякое навроде этого постепенно и шаг за шагом. Вы сможете сами запилить себе эмулятор. Как вы поступите со своими наработками мне не важно. Расшарите или продадите, но начальная информация будет общедоступной на этом форуме.
Язык программирования будет C# и все примеры будут включать в себя принципы C#. Но поскольку я не планирую отдать вам рабочий сурс ввиде "просто копипейст энд мейк билд", вы сможете переписать под любой вам нужный язык.
Данный топик, пожалуй посвящен людям с опытом в кодинге, нубы и нубские вопросы не приветствуются, т.к. обучать кого-либо я не собираюсь. Нуб? Проходи мимо. Также не приветствуются в топике "Одминистраторы фришек" с вопросами "когда уже выложат рабочий сервер, чтобы на школьниках можно было рубить бабло?". Никто вам ничего не выложит, и если у вас есть деньги но нету мозгов, вам тоже никто не собирается помогать, в частности я.
Поэтому не пишите мне в ЛС с предложениями.
Ну а теперь перейдем к технической части топика. Начнем естественно с логин сервера.
Относительно логин сервера / Concerning LoginServer
Пакеты
Формируется пакет так:
[SRC="c++"]public class BNSLoginPacket : Packet<LoginPacketOpcode>
{
public string Command { get; set; }
public int Serial { get; set; }
public byte WorldID { get; set; }
public string Content { get; set; }
protected XmlDocument ReadLoginPacket()
{
Serial = GetInt(2);
WorldID = GetByte();
Content = Encoding.UTF8.GetString(GetBytes((ushort)(Length - 7), 7));
XmlDocument xml = new XmlDocument();
xml.LoadXml(Content);
return xml;
}
public void WritePacket()
{
string res = Serial != 0 ?
string.Format("{0}\r\ns:{1}R\r\nl:{2}\r\n\r\n{3}", Command, Serial, Encoding.UTF8.GetByteCount(Content), Content) :
string.Format("{0}\r\nl:{1}\r\n\r\n{2}", Command, Encoding.UTF8.GetByteCount(Content), Content);
byte[] buf = Encoding.UTF8.GetBytes(res);
PutBytes(buf, 2);
}
public override string ToString()
{
var ser = GetInt(2);
var world = GetByte();
var content = Encoding.UTF8.GetString(GetBytes((ushort)(Length - 7), 7));
return string.Format("{0}\n", content);
}
}[/SRC]
ЛогинСервер включает в себя следующие пакеты:
CM_STS_CONNECT //просто соединение
CM_AUTH_LOGIN_START //запрашивает данные логина
CM_AUTH_KEY_DATA //запрашивает обмен ключами
CM_AUTH_LOGIN_FINISH //завершает авторизацию
CM_AUTH_GAME_TOKEN //передает ключ сессии
CM_AUTH_TOKEN //хандшейк между клиентом и логином
CM_ACCOUNT_LIST //кол-во аккаунтов
CM_WORLD_LIST //кол-во серверов
CM_CHAR_LIST //кол-во персонажей
CM_CHAR_SLOT_REQUEST //запрашиваем кол-во слотов
CM_CHAR_CREATE //создаем персонажа
CM_CHAR_DELETE //удаляем персонажа
CM_SLOT_LIST //кол-во слотов
CM_USER_INFO //данные о персонаже
CM_ERROR_RETURN //заглушка для необработанных пакетов
CM_PING //просто пинг
Есть два типа пакетов. Первый вызывает обработчик, второй формирует данные.
CM_ACCOUNT_LIST
[SRC="c++"]public CM_ACCOUNT_LIST()
{
this.ID = LoginPacketOpcode.CM_ACCOUNT_LIST;
}
public override Packet<LoginPacketOpcode> New()
{
return new CM_ACCOUNT_LIST();
}
public override void OnProcess(Session<LoginPacketOpcode> client)
{
((LoginSession)client).OnAccountList(GetInt(2));
}[/SRC]
Такие все, за исключением следующих:
CM_AUTH_KEY_DATA
[SRC="c++"]public CM_AUTH_KEY_DATA()
{
this.ID = LoginPacketOpcode.CM_AUTH_KEY_DATA;
}
public override Packet<LoginPacketOpcode> New()
{
return new CM_AUTH_KEY_DATA();
}
public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
byte[] keyData = null;
root = xml["Request"];
list = root.ChildNodes;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "keydata":
keyData = Convert.FromBase64String(i.InnerText);
break;
}
}
xml = null;
System.IO.MemoryStream ms = new System.IO.MemoryStream(keyData);
System.IO.BinaryReader br = new System.IO.BinaryReader(ms);
byte[] exchangeKey = br.ReadBytes(br.ReadInt32());
byte[] hash = br.ReadBytes(br.ReadInt32());
string checkHash = Convert.ToBase64String(hash);
((LoginSession)client).OnAuthKeyData(exchangeKey, checkHash, Serial);
}
}[/SRC]
CM_AUTH_LOGIN_START:
[SRC="c++"]public CM_AUTH_LOGIN_START()
{
this.ID = LoginPacketOpcode.CM_AUTH_LOGIN_START;
}
public override Packet<LoginPacketOpcode> New()
{
return new CM_AUTH_LOGIN_START();
}
public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
string loginName = "";
root = xml["Request"];
list = root.ChildNodes;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "loginname":
loginName = i.InnerText;
break;
}
}
xml = null;
((LoginSession)client).OnAuthLoginStart(loginName, Serial);
}[/SRC]
CM_CHAR_CREATE:
[SRC="c++"]public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
byte[] keyData = null;
root = xml["bns"];
list = root.ChildNodes;
byte slotID = 0;
string charName = "";
byte[] charData = null;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "slotid":
Guid id = Guid.Parse(i.InnerText);
for (slotID = 0; slotID < 5; slotID++)
{
if (((uint)slotID).ToGUID() == id)
break;
}
break;
case "charname":
charName = i.InnerText;
break;
case "chardata":
charData = Convert.FromBase64String(i.InnerText);
break;
}
}
xml = null;
((LoginSession)client).OnCharCreate(GetInt(2), WorldID, slotID, charName, charData);
}[/SRC]
CM_CHAR_DELETE:
[SRC="c++"]public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
root = xml["bns"];
list = root.ChildNodes;
byte slotID = 0;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "slotid":
Guid id = Guid.Parse(i.InnerText);
for (slotID = 0; slotID < 5; slotID++)
{
if (((uint)slotID).ToGUID() == id)
break;
}
break;
}
}
xml = null;
((LoginSession)client).OnCharDelete(GetInt(2), slotID);
}[/SRC]
CM_CHAR_SLOT_REQUEST:
[SRC="c++"]public override void OnProcess(Session<LoginPacketOpcode> client)
{
XmlDocument xml = ReadLoginPacket();
XmlElement root;
XmlNodeList list;
string loginName = "";
root = xml["Request"];
list = root.ChildNodes;
byte slotID = 0;
foreach (object j in list)
{
XmlElement i;
if (j.GetType() != typeof(XmlElement)) continue;
i = (XmlElement)j;
switch (i.Name.ToLower())
{
case "slotid":
Guid id = Guid.Parse(i.InnerText);
for (slotID = 0; slotID < 5; slotID++)
{
if (((uint)slotID).ToGUID() == id)
break;
}
break;
}
}
xml = null;
((LoginSession)client).OnCharSlotRequest(GetInt(2), slotID);
}[/SRC]
Крипт / crypto
Состоит из двух частей:
Обмен ключами:
[SRC="c++"]public unsafe class BNSKeyExchange : EncryptionKeyExchange
{
public static BigInteger N = new BigInteger("E306EBC02F1DC69F5B437683FE3851FD9AAA6E97F4CBD42FC06C72053CBCED68EC570E6666F529C58518CF7B299B5582495DB169ADF48ECEB6D65461B4D7C75DD1DA89601D5C498EE48BB950E2D8D5E0E0C692D613483B38D381EA9674DF74D67665259C4C31A29E0B3CFF7587617260E8C58FFA0AF8339CD68DB3ADB90AAFEE");
public static BigInteger P = new BigInteger("7A39FF57BCBFAA521DCE9C7DEFAB520640AC493E1B6024B95A28390E8F05787D");
public static byte[] staticKey = Conversions.HexStr2Bytes("AC34F3070DC0E52302C2E8DA0E3F7B3E63223697555DF54E7122A14DBC99A3E8");
public static BigInteger Two = new BigInteger(2);
BigInteger privateKey;
BigInteger exchangeKey = Two;
BigInteger exchangeKeyServer;
SHA256 sha = SHA256.Create();
BigInteger session = new BigInteger((ulong)Global.Random.Next() << 32 | (uint)Global.Random.Next());
byte[] passwordHash, usernameHash;
string user, password, authentication;
public string Username { get { return user; } set { user = value; } }
public string Password { get { return password; } set { password = value; } }
public BigInteger Session { get { return session; } set { session = value; } }
public override EncryptionKeyExchange CreateNewInstance()
{
return new BNSKeyExchange();
}
public override void MakePrivateKey()
{
privateKey = new BigInteger(sha.ComputeHash(Encoding.ASCII.GetBytes(DateTime.Now.Ticks.ToString())));
}
BigInteger GetKeyExchange()
{
if (exchangeKey == Two)
exchangeKey = Two.modPow(privateKey, N);
return exchangeKey;
}
public BigInteger GetKeyExchangeClient()
{
return GetKeyExchange();
}
BigInteger GetKeyExchangeServer()
{
if (exchangeKey == Two)
{
try
{
exchangeKey = Two.modPow(privateKey, N);
string username = user + "@plaync.co.kr";
passwordHash = sha.ComputeHash(Encoding.ASCII.GetBytes(username + ":" + password));
usernameHash = sha.ComputeHash(Encoding.ASCII.GetBytes(username));
BigInteger hash2 = SHA256Hash2ArrayInverse(session.getBytes(), passwordHash);
BigInteger v25 = Two.modPow(hash2, N);
v25 = (v25 * P) % N;
exchangeKeyServer = (exchangeKey + v25) % N;
}
catch
{
}
}
return exchangeKeyServer;
}
public override byte[] GetKeyExchangeBytes(Mode mode)
{
switch (mode)
{
case Mode.Client:
return GetKeyExchangeClient().getBytes();
case Mode.Server:
return GetKeyExchangeServer().getBytes();
}
return null;
}
public byte[][] GenerateKeyClient(BigInteger exchangeKey)
{
string username = user + "@plaync.co.kr";
byte[] passwordHash = sha.ComputeHash(Encoding.ASCII.GetBytes(username + ":" + password));
BigInteger hash1 = SHA256Hash2ArrayInverse(this.GetKeyExchange().getBytes(), exchangeKey.getBytes());
BigInteger hash2 = SHA256Hash2ArrayInverse(session.getBytes(), passwordHash);
BigInteger v27 = new BigInteger(exchangeKey.getBytes());
BigInteger v25 = Two.modPow(hash2, N);
v25 = (v25 * P) % N;
while (v27 < v25)
v27 += N;
v27 -= v25;
BigInteger v24 = ((hash1 * hash2) + privateKey) % N;
BigInteger v21 = v27.modPow(v24, N);
key = GenerateEncryptionKeyRoot(v21.getBytes());
byte[] chash1 = sha.ComputeHash(CombineBuffers(staticKey, sha.ComputeHash(Encoding.ASCII.GetBytes(username)), session.getBytes(), this.GetKeyExchange().getBytes(), exchangeKey.getBytes(), key));
byte[] chash2 = sha.ComputeHash(CombineBuffers(this.GetKeyExchange().getBytes(), chash1, key));
key = Generate256BytesKey(key);
return new byte[][] { chash1, chash2 };
}
public byte[][] GenerateKeyServer(BigInteger exchangeKey)
{
BigInteger hash1 = SHA256Hash2ArrayInverse(exchangeKey.getBytes(), this.GetKeyExchangeServer().getBytes());
BigInteger hash2 = SHA256Hash2ArrayInverse(session.getBytes(), passwordHash);
BigInteger v27 = new BigInteger(exchangeKey.getBytes());
BigInteger v21 = (this.GetKeyExchange().modPow((hash1 * hash2), N) * v27.modPow(privateKey, N)) % N;
key = GenerateEncryptionKeyRoot(v21.getBytes());
byte[] chash1 = sha.ComputeHash(CombineBuffers(staticKey, usernameHash, session.getBytes(), exchangeKey.getBytes(), GetKeyExchangeServer().getBytes(), key));
byte[] chash2 = sha.ComputeHash(CombineBuffers(exchangeKey.getBytes(), chash1, key));
key = Generate256BytesKey(key);
return new byte[][] { chash1, chash2 };
}
public override void MakeKey(Mode mode, byte[] keyExchangeBytes)
{
byte[][] checkHash = null;
BigInteger exchange = new BigInteger(keyExchangeBytes);
switch (mode)
{
case Mode.Client:
checkHash = GenerateKeyClient(exchange);
break;
case Mode.Server:
checkHash = GenerateKeyServer(exchange);
break;
}
authentication = Convert.ToBase64String(checkHash[0]) + "," + Convert.ToBase64String(checkHash[1]);
}
public override bool IsReady
{
get { return key != null; }
}
BigInteger SHA256Hash2ArrayInverse(byte[] tmp1, byte[] tmp2)
{
BigInteger hash;
byte[] combine = new byte[tmp1.Length + tmp2.Length];
tmp1.CopyTo(combine, 0);
tmp2.CopyTo(combine, tmp1.Length);
byte[] buf = sha.ComputeHash(combine);
byte[] res = IntegerReverse(buf);
hash = new BigInteger(res);
return hash;
}
byte[] IntegerReverse(byte[] buf)
{
byte[] res = new byte[buf.Length];
for (int i = 0; i < res.Length / 4; i++)
{
fixed (byte* ptr = buf)
{
fixed (byte* ptr2 = res)
{
int* src = (int*)ptr;
int* dst = (int*)ptr2;
dst[i] = src[res.Length / 4 - 1 - i];
}
}
}
return res;
}
byte[] GenerateEncryptionKeyRoot(byte[] src)
{
int firstSize = src.Length;
int startIndex = 0;
byte[] half;
byte[] dst = new byte[64];
if (src.Length > 4)
{
do
{
if (src[startIndex] == 0)
break;
firstSize--;
startIndex++;
} while (firstSize > 4);
}
int size = firstSize >> 1;
half = new byte[size];
if (size > 0)
{
int index = startIndex + firstSize - 1;
for (int i = 0; i < size; i++)
{
half[i] = src[index];
index -= 2;
}
}
byte[] hash = sha.ComputeHash(half, 0, size);
for (int i = 0; i < 32; i++)
{
dst[2 * i] = hash[i];
}
if (size > 0)
{
int index = startIndex + firstSize - 2;
for (int i = 0; i < size; i++)
{
half[i] = src[index];
index -= 2;
}
}
hash = sha.ComputeHash(half, 0, size);
for (int i = 0; i < 32; i++)
{
dst[2 * i + 1] = hash[i];
}
return dst;
}
byte[] CombineBuffers(params byte[][] buffers)
{
int len = 0;
foreach (byte[] i in buffers)
{
len += i.Length;
}
byte[] res = new byte[len];
int index = 0;
foreach (byte[] i in buffers)
{
i.CopyTo(res, index);
index += i.Length;
}
return res;
}
byte[] Generate256BytesKey(byte[] src)
{
int v7 = 1;
byte[] res = new byte[256];
for (int i = 0; i < 256; i++)
res[i] = (byte)i;
int v6 = 0;
int counter = 0;
for (int i = 64; i > 0; i--)
{
int v9 = (v6 + src[counter] + res[v7 - 1]) & 0xFF;
int v10 = res[v7 - 1];
res[v7 - 1] = res[v9];
int v8 = counter + 1;
res[v9] = (byte)v10;
if (v8 == src.Length)
v8 = 0;
int v13 = v9 + src[v8];
int v11 = v8 + 1;
int v14 = v13 + res[v7];
v13 = res[v7];
int v12 = (byte)v14;
res[v7] = res[v12];
res[v12] = (byte)v13;
if (v11 == src.Length)
v11 = 0;
int v16 = (v12 + src[v11] + res[v7 + 1]) & 0xFF;
int v17 = res[v7 + 1];
res[v7 + 1] = res[v16];
int v15 = v11 + 1;
res[v16] = (byte)v17;
if (v15 == src.Length)
v15 = 0;
int v18 = v16 + src[v15];
int v19 = res[v7 + 2];
v6 = (v18 + res[v7 + 2]) & 0xFF;
counter = v15 + 1;
res[v7 + 2] = res[v6];
res[v6] = (byte)v19;
if (counter == src.Length)
counter = 0;
v7 += 4;
}
return res;
}
}[/SRC]
Ответный ключ:
[SRC="c++"]public abstract class EncryptionKeyExchange
{
public static BigInteger Module = new BigInteger(Conversions.HexStr2Bytes("f488fd584e49dbcd20b49de49107366b336c380d451d0f7c88b31c7c5b2d8ef6f3c923c043f0a55b188d8ebb558cb85d38d334fd7c175743a31d186cde33212cb52aff3ce1b1294018118d7c84a70a72d686c40319c807297aca950cd9969fabd00a509b0246d3083d66a45d419f9c7cbd894b221926baaba25ec355e92f78c7"));
protected byte[] key;
public byte[] Key { get { return key; } }
public abstract EncryptionKeyExchange CreateNewInstance();
public abstract void MakePrivateKey();
public abstract byte[] GetKeyExchangeBytes(Mode mode);
public abstract void MakeKey(Mode mode, byte[] keyExchangeBytes);
public abstract bool IsReady
{
get;
}
}[/SRC]
Обмен ключами:
[SRC="c++"]public unsafe class BNSKeyExchange : EncryptionKeyExchange
{
public static BigInteger N = new BigInteger("E306EBC02F1DC69F5B437683FE3851FD9AAA6E97F4CBD42FC06C72053CBCED68EC570E6666F529C58518CF7B299B5582495DB169ADF48ECEB6D65461B4D7C75DD1DA89601D5C498EE48BB950E2D8D5E0E0C692D613483B38D381EA9674DF74D67665259C4C31A29E0B3CFF7587617260E8C58FFA0AF8339CD68DB3ADB90AAFEE");
public static BigInteger P = new BigInteger("7A39FF57BCBFAA521DCE9C7DEFAB520640AC493E1B6024B95A28390E8F05787D");
public static byte[] staticKey = Conversions.HexStr2Bytes("AC34F3070DC0E52302C2E8DA0E3F7B3E63223697555DF54E7122A14DBC99A3E8");
public static BigInteger Two = new BigInteger(2);
BigInteger privateKey;
BigInteger exchangeKey = Two;
BigInteger exchangeKeyServer;
SHA256 sha = SHA256.Create();
BigInteger session = new BigInteger((ulong)Global.Random.Next() << 32 | (uint)Global.Random.Next());
byte[] passwordHash, usernameHash;
string user, password, authentication;
public string Username { get { return user; } set { user = value; } }
public string Password { get { return password; } set { password = value; } }
public BigInteger Session { get { return session; } set { session = value; } }
public override EncryptionKeyExchange CreateNewInstance()
{
return new BNSKeyExchange();
}
public override void MakePrivateKey()
{
privateKey = new BigInteger(sha.ComputeHash(Encoding.ASCII.GetBytes(DateTime.Now.Ticks.ToString())));
}
BigInteger GetKeyExchange()
{
if (exchangeKey == Two)
exchangeKey = Two.modPow(privateKey, N);
return exchangeKey;
}
public BigInteger GetKeyExchangeClient()
{
return GetKeyExchange();
}
BigInteger GetKeyExchangeServer()
{
if (exchangeKey == Two)
{
try
{
exchangeKey = Two.modPow(privateKey, N);
string username = user + "@plaync.co.kr";
passwordHash = sha.ComputeHash(Encoding.ASCII.GetBytes(username + ":" + password));
usernameHash = sha.ComputeHash(Encoding.ASCII.GetBytes(username));
BigInteger hash2 = SHA256Hash2ArrayInverse(session.getBytes(), passwordHash);
BigInteger v25 = Two.modPow(hash2, N);
v25 = (v25 * P) % N;
exchangeKeyServer = (exchangeKey + v25) % N;
}
catch
{
}
}
return exchangeKeyServer;
}
public override byte[] GetKeyExchangeBytes(Mode mode)
{
switch (mode)
{
case Mode.Client:
return GetKeyExchangeClient().getBytes();
case Mode.Server:
return GetKeyExchangeServer().getBytes();
}
return null;
}
public byte[][] GenerateKeyClient(BigInteger exchangeKey)
{
string username = user + "@plaync.co.kr";
byte[] passwordHash = sha.ComputeHash(Encoding.ASCII.GetBytes(username + ":" + password));
BigInteger hash1 = SHA256Hash2ArrayInverse(this.GetKeyExchange().getBytes(), exchangeKey.getBytes());
BigInteger hash2 = SHA256Hash2ArrayInverse(session.getBytes(), passwordHash);
BigInteger v27 = new BigInteger(exchangeKey.getBytes());
BigInteger v25 = Two.modPow(hash2, N);
v25 = (v25 * P) % N;
while (v27 < v25)
v27 += N;
v27 -= v25;
BigInteger v24 = ((hash1 * hash2) + privateKey) % N;
BigInteger v21 = v27.modPow(v24, N);
key = GenerateEncryptionKeyRoot(v21.getBytes());
byte[] chash1 = sha.ComputeHash(CombineBuffers(staticKey, sha.ComputeHash(Encoding.ASCII.GetBytes(username)), session.getBytes(), this.GetKeyExchange().getBytes(), exchangeKey.getBytes(), key));
byte[] chash2 = sha.ComputeHash(CombineBuffers(this.GetKeyExchange().getBytes(), chash1, key));
key = Generate256BytesKey(key);
return new byte[][] { chash1, chash2 };
}
public byte[][] GenerateKeyServer(BigInteger exchangeKey)
{
BigInteger hash1 = SHA256Hash2ArrayInverse(exchangeKey.getBytes(), this.GetKeyExchangeServer().getBytes());
BigInteger hash2 = SHA256Hash2ArrayInverse(session.getBytes(), passwordHash);
BigInteger v27 = new BigInteger(exchangeKey.getBytes());
BigInteger v21 = (this.GetKeyExchange().modPow((hash1 * hash2), N) * v27.modPow(privateKey, N)) % N;
key = GenerateEncryptionKeyRoot(v21.getBytes());
byte[] chash1 = sha.ComputeHash(CombineBuffers(staticKey, usernameHash, session.getBytes(), exchangeKey.getBytes(), GetKeyExchangeServer().getBytes(), key));
byte[] chash2 = sha.ComputeHash(CombineBuffers(exchangeKey.getBytes(), chash1, key));
key = Generate256BytesKey(key);
return new byte[][] { chash1, chash2 };
}
public override void MakeKey(Mode mode, byte[] keyExchangeBytes)
{
byte[][] checkHash = null;
BigInteger exchange = new BigInteger(keyExchangeBytes);
switch (mode)
{
case Mode.Client:
checkHash = GenerateKeyClient(exchange);
break;
case Mode.Server:
checkHash = GenerateKeyServer(exchange);
break;
}
authentication = Convert.ToBase64String(checkHash[0]) + "," + Convert.ToBase64String(checkHash[1]);
}
public override bool IsReady
{
get { return key != null; }
}
BigInteger SHA256Hash2ArrayInverse(byte[] tmp1, byte[] tmp2)
{
BigInteger hash;
byte[] combine = new byte[tmp1.Length + tmp2.Length];
tmp1.CopyTo(combine, 0);
tmp2.CopyTo(combine, tmp1.Length);
byte[] buf = sha.ComputeHash(combine);
byte[] res = IntegerReverse(buf);
hash = new BigInteger(res);
return hash;
}
byte[] IntegerReverse(byte[] buf)
{
byte[] res = new byte[buf.Length];
for (int i = 0; i < res.Length / 4; i++)
{
fixed (byte* ptr = buf)
{
fixed (byte* ptr2 = res)
{
int* src = (int*)ptr;
int* dst = (int*)ptr2;
dst[i] = src[res.Length / 4 - 1 - i];
}
}
}
return res;
}
byte[] GenerateEncryptionKeyRoot(byte[] src)
{
int firstSize = src.Length;
int startIndex = 0;
byte[] half;
byte[] dst = new byte[64];
if (src.Length > 4)
{
do
{
if (src[startIndex] == 0)
break;
firstSize--;
startIndex++;
} while (firstSize > 4);
}
int size = firstSize >> 1;
half = new byte[size];
if (size > 0)
{
int index = startIndex + firstSize - 1;
for (int i = 0; i < size; i++)
{
half[i] = src[index];
index -= 2;
}
}
byte[] hash = sha.ComputeHash(half, 0, size);
for (int i = 0; i < 32; i++)
{
dst[2 * i] = hash[i];
}
if (size > 0)
{
int index = startIndex + firstSize - 2;
for (int i = 0; i < size; i++)
{
half[i] = src[index];
index -= 2;
}
}
hash = sha.ComputeHash(half, 0, size);
for (int i = 0; i < 32; i++)
{
dst[2 * i + 1] = hash[i];
}
return dst;
}
byte[] CombineBuffers(params byte[][] buffers)
{
int len = 0;
foreach (byte[] i in buffers)
{
len += i.Length;
}
byte[] res = new byte[len];
int index = 0;
foreach (byte[] i in buffers)
{
i.CopyTo(res, index);
index += i.Length;
}
return res;
}
byte[] Generate256BytesKey(byte[] src)
{
int v7 = 1;
byte[] res = new byte[256];
for (int i = 0; i < 256; i++)
res[i] = (byte)i;
int v6 = 0;
int counter = 0;
for (int i = 64; i > 0; i--)
{
int v9 = (v6 + src[counter] + res[v7 - 1]) & 0xFF;
int v10 = res[v7 - 1];
res[v7 - 1] = res[v9];
int v8 = counter + 1;
res[v9] = (byte)v10;
if (v8 == src.Length)
v8 = 0;
int v13 = v9 + src[v8];
int v11 = v8 + 1;
int v14 = v13 + res[v7];
v13 = res[v7];
int v12 = (byte)v14;
res[v7] = res[v12];
res[v12] = (byte)v13;
if (v11 == src.Length)
v11 = 0;
int v16 = (v12 + src[v11] + res[v7 + 1]) & 0xFF;
int v17 = res[v7 + 1];
res[v7 + 1] = res[v16];
int v15 = v11 + 1;
res[v16] = (byte)v17;
if (v15 == src.Length)
v15 = 0;
int v18 = v16 + src[v15];
int v19 = res[v7 + 2];
v6 = (v18 + res[v7 + 2]) & 0xFF;
counter = v15 + 1;
res[v7 + 2] = res[v6];
res[v6] = (byte)v19;
if (counter == src.Length)
counter = 0;
v7 += 4;
}
return res;
}
}[/SRC]
Ответный ключ:
[SRC="c++"]public abstract class EncryptionKeyExchange
{
public static BigInteger Module = new BigInteger(Conversions.HexStr2Bytes("f488fd584e49dbcd20b49de49107366b336c380d451d0f7c88b31c7c5b2d8ef6f3c923c043f0a55b188d8ebb558cb85d38d334fd7c175743a31d186cde33212cb52aff3ce1b1294018118d7c84a70a72d686c40319c807297aca950cd9969fabd00a509b0246d3083d66a45d419f9c7cbd894b221926baaba25ec355e92f78c7"));
protected byte[] key;
public byte[] Key { get { return key; } }
public abstract EncryptionKeyExchange CreateNewInstance();
public abstract void MakePrivateKey();
public abstract byte[] GetKeyExchangeBytes(Mode mode);
public abstract void MakeKey(Mode mode, byte[] keyExchangeBytes);
public abstract bool IsReady
{
get;
}
}[/SRC]
Обработчики
При входе:
[SRC="c++"]public partial class LoginSession : Session<LoginPacketOpcode>
{
int loginSerial;
string checkHash;
Account account;
AccountLoginResult lastLoginRes = AccountLoginResult.INVALID_PASSWORD;
public Account Account { get { return account; } }
public void OnAuthLoginStart(string loginName, int serial)
{
loginSerial = serial;
AccountSession.Instance.RequestAccountInfo(loginName.Split('@')[0], this);
Logger.ShowInfo(loginName + " попытка соединения");
}
public void OnAccountInfo(AccountLoginResult res, Account account)
{
if (res == AccountLoginResult.NO_SUCH_ACCOUNT)
Logger.ShowInfo("Результат соединения:" + res.ToString());
switch (res)
{
case AccountLoginResult.OK:
{
this.account = account;
this.Network.Crypt.KeyExchange.MakePrivateKey();
((BNSKeyExchange)this.Network.Crypt.KeyExchange).Username = account.UserName;
((BNSKeyExchange)this.Network.Crypt.KeyExchange).Password = account.Password;
byte[] exchange = this.Network.Crypt.KeyExchange.GetKeyExchangeBytes(Mode.Server);
byte[] session = ((BNSKeyExchange)this.Network.Crypt.KeyExchange).Session.getBytes();
System.IO.MemoryStream ms = new System.IO.MemoryStream();
System.IO.BinaryWriter bw = new System.IO.BinaryWriter(ms);
bw.Write(session.Length);
bw.Write(session);
bw.Write(exchange.Length);
bw.Write(exchange);
string key = string.Format("<Reply>\n<KeyData>{0}</KeyData>\n</Reply>\n", Convert.ToBase64String(ms.ToArray()));
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = loginSerial;
p.Content = key;
p.WritePacket();
this.Network.SendPacket(p);
}
break;
case AccountLoginResult.NO_SUCH_ACCOUNT:
case AccountLoginResult.DB_ERROR:
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 400 ErrAccountNotFound";
p.Serial = loginSerial;
p.Content = "<Error code=\"3002\" server=\"1008\" module=\"1\" line=\"458\"/>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
break;
}
}
public void OnAuthKeyData(byte[] exchangeKey, string checkHash, int serial)
{
this.Network.Crypt.KeyExchange.MakeKey(Mode.Server, exchangeKey);
string hash = ((BNSKeyExchange)this.Network.Crypt.KeyExchange).Authentication;
if (checkHash == hash.Split(',')[0])
{
this.checkHash = hash.Split(',')[1];
loginSerial = serial;
AccountSession.Instance.AccountLogin(account.AccountID, this);
}
else
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 400 ErrBadPasswd";
p.Serial = serial;
p.Content = "<Error code=\"11\" server=\"1012\" module=\"1\" line=\"1683\"/>\n";
p.Encrypt = false;
p.WritePacket();
this.Network.SendPacket(p);
Logger.ShowInfo("Login result for " + account.UserName + ": BadPassword");
}
}
public void OnAccountLoginResult(AccountLoginResult result)
{
lastLoginRes = result;
if (result == AccountLoginResult.OK)
{
account.LastLoginTime = DateTime.Now;
account.LastLoginIP = Network.Socket.RemoteEndPoint.ToString().Split(':')[0];
System.IO.MemoryStream ms = new System.IO.MemoryStream();
System.IO.BinaryWriter bw = new System.IO.BinaryWriter(ms);
bw.Write(32);
bw.Write(Convert.FromBase64String(checkHash));
string res = Convert.ToBase64String(ms.ToArray());
string key = string.Format("<Reply>\n<KeyData>{0}</KeyData>\n</Reply>\n", res);
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = loginSerial;
p.Content = key;
p.Encrypt = false;
p.WritePacket();
this.Network.SendPacket(p);
Logger.ShowInfo(account.UserName + " login successful!");
}
else
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 400 ErrBadPasswd";//should be already log in
p.Serial = loginSerial;
p.Content = "<Error code=\"11\" server=\"1012\" module=\"1\" line=\"1683\"/>\n";
p.Encrypt = false;
p.WritePacket();
this.Network.SendPacket(p);
Logger.ShowInfo("Login result for " + account.UserName + ": ALREADY_LOG_IN");
AccountSession.Instance.AccountLogout(account.AccountID, this);
}
}
public void OnAuthLoginFinish(int serial)
{
Guid accountGuid = account.AccountID.ToGUID();
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "POST /Presence/UserInfo STS/1.0";
p.Serial = 0;
p.Content = string.Format("<Message>\n<UserId>{0}</UserId>\n<UserCenter>1</UserCenter>\n<UserName>{1}</UserName>\n<Status>online</Status>\n<Aliases type=\"array\">\n<Alias>{1}</Alias>\n<Alias>bns:{1}</Alias>\n</Aliases>\n</Message>\n", accountGuid.ToString().ToUpper(), account.UserName);
p.WritePacket();
this.Network.SendPacket(p);
p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply>\n<UserId>{0}</UserId>\n<UserCenter>1</UserCenter>\n<Roles type=\"array\"/>\n<LocationId>32B0E246-1D5F-4AC1-AC30-E462AAF4C870</LocationId>\n<AccessMask>1073741823</AccessMask>\n<UserName>{1}</UserName>\n</Reply>\n", accountGuid.ToString().ToUpper(), account.UserName);
Logger.ShowInfo("account:" + accountGuid.ToString());
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnUserInfo(int serial)
{
Guid accountGuid = account.AccountID.ToGUID();
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply>\n<UserId>{0}</UserId>\n<UserCenter>1</UserCenter>\n<UserName>{1}</UserName>\n<LoginName>{1}@plaync.co.kr</LoginName>\n<UserStatus>1</UserStatus>\n</Reply>\n", accountGuid.ToString().ToUpper(), account.UserName);
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnAuthGameToken(int serial)
{
account.LoginToken = ((uint)Global.Random.Next()).ToGUID();
Logger.ShowInfo("gametoken:" + account.LoginToken.ToString().ToUpper());
account.TokenExpireTime = DateTime.Now.AddMinutes(10);
AccountSession.Instance.AccountSave(account, this);
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply>\n<Token>{0}</Token>\n</Reply>\n", account.LoginToken.ToString().ToUpper());
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnAuthToken(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string token = account.AccountID.ToGUID().ToString().ToUpper() + ":" + ((uint)Global.Random.Next()).ToGUID().ToString().ToUpper();
Logger.ShowInfo("Auth Token: " + token);
p.Content = string.Format("<Reply>\n<AuthnToken>{0}</AuthnToken>\n</Reply>\n", Convert.ToBase64String(Encoding.Default.GetBytes(token)));
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnError(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = serial == 8 ? "STS/1.0 400 ErrSecondPasswordNotFound" : "STS/1.0 400 ErrPermission";
p.Serial = serial;
//<Error code="3333" server="107" module="2" line="811"/>
p.Content = serial == 8 ? "<Error code=\"3333\" server=\"107\" module=\"2\" line=\"811\"/>\n" : "<Error code=\"49\" server=\"1001\" module=\"8000\" line=\"1086\"/>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnAccountList(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
p.Content = string.Format("<Reply type=\"array\">\n<GameAccount>\n<Alias>{0}</Alias>\n<Created>2013-06-08T17:45:58.689+09:00</Created>\n</GameAccount>\n</Reply>\n", account.AccountID.ToGUID().ToString().ToUpper());
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnWorldList(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string worlds = "";
int idx = 1;
foreach (WorldInfo i in Configuration.Instance.Worlds)
{
worlds += string.Format("<World>\n<WorldCode>{0}</WorldCode>\n<WorldName>{1}</WorldName>\n<PublicNetAddress>{2}</PublicNetAddress>\n<Property>\n<EnableCharacterCreate>true</EnableCharacterCreate>\n<WorldScore>0</WorldScore>\n</Property>\n<UserCounts>\n<PlayingUsers>426</PlayingUsers>\n<WaitingUsers>0</WaitingUsers>\n<MaxUsers>4000</MaxUsers>\n</UserCounts>\n</World>\n",
idx++, i.Name, i.Address);
}
p.Content = string.Format("<Reply type=\"array\">\n{0}\n</Reply>\n", worlds);
p.WritePacket();
this.Network.SendPacket(p);
}
}[/SRC]
При чтении данных персонажа:
[SRC="c++"]public partial class LoginSession : Session<LoginPacketOpcode>
{
int characterSerial = 0, removeSerial = 0;
Dictionary<byte, ActorPC> chars;
int currentCharIndex;
ActorPC createdPC, removePC;
public void OnCharList(int serial)
{
characterSerial = serial;
CharacterSession.Instance.RequestCharList(account.AccountID, this);
}
public void OnGotCharList(List<ActorPC> chars)
{
this.chars=new Dictionary<byte,ActorPC>();
foreach (ActorPC i in chars)
this.chars[i.SlotID] = i;
currentCharIndex = 0;
LoadInventory();
}
public void SendSlotList(int serial)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string content = "<Reply type=\"array\">\n";
for (byte i = 0; i < (3 + this.account.ExtraSlots); i++)
{
content += string.Format("<Slot>\n<SlotId>{0}</SlotId>\n<AppGroupId>2</AppGroupId>\n<SlotType>char</SlotType>\n<SystemSlot>1</SystemSlot>\n<Changed>2012-04-25T05:52:22Z</Changed>\n<Registered>2012-04-25T05:52:22Z</Registered>\n</Slot>\n",
((uint)i).ToGUID().ToString().ToUpper());
}
p.Content = content + "</Reply>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
public void SendCharList()
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = characterSerial;
string content = "<Reply>\n";
for (byte i = 0; i < 5; i++)
{
if (chars.ContainsKey(i))
{
ActorPC pc = chars[i];
System.IO.MemoryStream ms = new System.IO.MemoryStream();
pc.AppearenceToSteam(ms);
content += string.Format("<CharSlot>\n<SlotId>{0}</SlotId>\n<CharId>{1}</CharId>\n<WorldCode>{4}</WorldCode>\n<CharName>{2}</CharName>\n<CharData><bns><pcdbid>{1}</pcdbid><showcase>{3}</showcase></bns></CharData>\n</CharSlot>\n",
((uint)pc.SlotID).ToGUID(), pc.CharID, pc.Name, Convert.ToBase64String(ms.ToArray()), pc.WorldID);
ms.Close();
ms = null;
}
else
content += string.Format("<CharSlot><SlotId>{0}</SlotId>\n</CharSlot>\n", ((uint)i).ToGUID());
}
p.Content = content + "</Reply>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
public void OnCharSlotRequest(int serial, byte slotID)
{
if (chars.ContainsKey(slotID))
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = serial;
string content = "<Reply>\n";
ActorPC pc = chars[slotID];
System.IO.MemoryStream ms = new System.IO.MemoryStream();
pc.AppearenceToSteam(ms);
content += string.Format("<SlotId>{0}</SlotId>\n<CharId>{1}</CharId>\n<WorldCode>{4}</WorldCode>\n<CharName>{2}</CharName>\n<CharData><bns><pcdbid>{1}</pcdbid><showcase>{3}</showcase></bns></CharData>\n",
((uint)pc.SlotID).ToGUID(), pc.CharID, pc.Name, Convert.ToBase64String(ms.ToArray()), pc.WorldID);
ms.Close();
ms = null;
p.Content = content + "</Reply>\n";
p.WritePacket();
this.Network.SendPacket(p);
}
}
public void OnCharDelete(int serial, byte slotID)
{
if (chars.ContainsKey(slotID))
{
removePC = chars[slotID];
removeSerial = serial;
CharacterSession.Instance.DeleteChar(removePC.CharID, this);
}
}
public void OnCharDeleteResult(SM_CHAR_DELETE_RESULT.Results Result)
{
if (Result == SM_CHAR_DELETE_RESULT.Results.OK && removePC != null)
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = removeSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>DeletePc</command>\n<result>OK</result>\n<slotid>{0}</slotid>\n</bns>\n",
((uint)removePC.SlotID).ToGUID(), removePC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
if (chars.ContainsKey(removePC.SlotID))
chars.Remove(removePC.SlotID);
}
else
{
removePC = null;
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = removeSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>DeletePc</command>\n<result>Fail</result>\n<slotid>{0}</slotid>\n<reason>202</reason></bns>\n",
((uint)removePC.SlotID).ToGUID(), removePC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
}
}
public void OnCharCreate(int serial, byte worldID, byte slotID, string charName, byte[] charData)
{
System.IO.MemoryStream ms = new System.IO.MemoryStream(charData);
System.IO.BinaryReader br = new System.IO.BinaryReader(ms);
ms.Position = 2;
byte[] display1 = Conversions.HexStr2Bytes("6363634E63737364777777636464776464646464646464");
br.ReadBytes(br.ReadInt16()).CopyTo(display1, 0);
ActorPC pc = new ActorPC();
pc.Level = 1;
pc.AccountID = account.AccountID;
pc.Name = charName;
pc.SlotID = slotID;
pc.WorldID = worldID;
pc.Appearence1 = display1;
ms.Position += 2;
pc.Race = (Race)br.ReadByte();
ms.Position += 2;
pc.Gender = (Gender)br.ReadByte();
ms.Position += 2;
pc.Job = (Job)br.ReadByte();
ms.Position += 2;
pc.Appearence2 = br.ReadBytes(br.ReadInt16());
pc.MapID = 1101;
pc.X = -3177;
pc.Y = 9243;
pc.Z = 599;
pc.Dir = 45;
pc.UISettings = "";
pc.InventorySize = 32;
switch (pc.Job)
{
case Job.Assassin:
pc.HP = 66;
pc.MP = 0;
pc.MaxHP = 66;
pc.MaxMP = 10;
break;
case Job.ForceMaster:
pc.HP = 61;
pc.MP = 10;
pc.MaxHP = 61;
pc.MaxMP = 10;
break;
case Job.KungfuMaster:
pc.HP = 109;
pc.MaxHP = 109;
pc.MP = 0;
pc.MaxMP = 10;
break;
case Job.BladeMaster:
default:
pc.HP = 99;
pc.MP = 0;
pc.MaxHP = 99;
pc.MaxMP = 10;
break;
}
characterSerial = serial;
createdPC = pc;
CharacterSession.Instance.CreateChar(pc, this);
}
public void OnCharCreateResult(uint charID,SM_CHAR_CREATE_RESULT.Results Result)
{
if (Result == SM_CHAR_CREATE_RESULT.Results.OK)
{
createdPC.CharID = charID;
Base.Quests.Quest q = new Base.Quests.Quest();
q.QuestID = 250;
q.Step = 1;
q.NextStep = 1;
createdPC.Quests[q.QuestID] = q;
CharacterSession.Instance.CharacterSave(createdPC);
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = characterSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>CreatePc</command>\n<result>OK</result>\n<slotid>{0}</slotid>\n</bns>\n",
((uint)createdPC.SlotID).ToGUID(), createdPC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
if (!chars.ContainsKey(createdPC.SlotID))
chars[createdPC.SlotID] = createdPC;
}
else
{
BNSLoginPacket p = new BNSLoginPacket();
p.Command = "STS/1.0 200 OK";
p.Serial = characterSerial;
p.Content = string.Format("<bns>\n<protocol>Game.bns.{1}</protocol>\n<command>CreatePc</command>\n<result>Fail</result>\n<slotid>{0}</slotid>\n<reason>202</reason>\n</bns>\n",
((uint)createdPC.SlotID).ToGUID(), createdPC.WorldID);
p.WritePacket();
this.Network.SendPacket(p);
}
}
}[/SRC]
Сеть
[SRC="c++"]public class BNSLoginNetwork<T> : Network<T>
{
new string lastContent = "";
public override Network<T> CreateNewInstance(System.Net.Sockets.Socket sock, Dictionary<T, Packet<T>> commandTable, Session<T> client)
{
BNSLoginNetwork<T> instance = new BNSLoginNetwork<T>();
CreateNewInstance(instance, sock, commandTable, client);
return instance;
}
protected override void OnReceivePacket(byte[] buffer)
{
if (Crypt.KeyExchange.IsReady)
Crypt.Decrypt(buffer, 0, buffer.Length);
string content = lastContent + Encoding.UTF8.GetString(buffer);
try
{
ParseContent(content);
}
catch (Exception ex)
{
Logger.ShowError(ex);
}
}
void ParseContent(string content)
{
System.IO.StringReader sr = new System.IO.StringReader(content);
string header = sr.ReadLine();
int length = int.Parse(sr.ReadLine().Split(':')[1]);
string tmp = sr.ReadLine();
string serial="";
if (tmp != "")
{
serial = tmp;
do
{
tmp = sr.ReadLine();
} while (tmp != "" && sr.Peek() != -1);
}
byte worldID;
LoginPacketOpcode opcode = ParseHeader(header, out worldID);
if (opcode == LoginPacketOpcode.Unknown)
{
Logger.ShowError("Неизвестный пакет:");
Logger.ShowError(content);
}
byte[] buf = Encoding.UTF8.GetBytes(sr.ReadToEnd());
if (buf.Length >= length)
{
byte[] tmp2 = new byte[length];
Array.Copy(buf, 0, tmp2, 0, length);
Packet<T> p = new Packet<T>();
p.ID = (T)(object)(int)opcode;
p.PutInt(serial != "" ? int.Parse(serial.Split(':')[1]) : 0, 2);
p.PutByte(worldID);
p.PutBytes(tmp2, 7);
ProcessPacket(p);
if (length < buf.Length)
ParseContent(Encoding.UTF8.GetString(buf, length, buf.Length - length));
else
lastContent = "";
}
else
lastContent = content;
}
LoginPacketOpcode ParseHeader(string header ,out byte worldID)
{
string[] token = header.Split(' ');
return GetOpcode(token[1], out worldID);
}
public static LoginPacketOpcode GetOpcode(string header,out byte worldID)
{
worldID = 1;
if (header.Contains("/Game.bns."))
{
string[] token = header.Split('/');
worldID = byte.Parse(token[1].Substring(9));
token[1] = token[1].Substring(0, 8);
header = "/" + token[1] + "/" + token[2];
}
switch (header)
{
case "/Sts/Connect":
return LoginPacketOpcode.CM_STS_CONNECT;
case "/Auth/LoginStart":
return LoginPacketOpcode.CM_AUTH_LOGIN_START;
case "/Auth/KeyData":
return LoginPacketOpcode.CM_AUTH_KEY_DATA;
case "/Auth/LoginFinish":
return LoginPacketOpcode.CM_AUTH_LOGIN_FINISH;
case "/Auth/RequestToken":
return LoginPacketOpcode.CM_AUTH_TOKEN;
case "/Auth/RequestGameToken":
return LoginPacketOpcode.CM_AUTH_GAME_TOKEN;
case "/GameAccount/ListMyAccounts":
return LoginPacketOpcode.CM_ACCOUNT_LIST;
case "/World/ListWorlds":
return LoginPacketOpcode.CM_WORLD_LIST;
case "/Slot/ListCharSlots":
return LoginPacketOpcode.CM_CHAR_LIST;
case "/Slot/GetCharSlot":
return LoginPacketOpcode.CM_CHAR_SLOT_REQUEST;
case "/Game.bns/CreatePC":
return LoginPacketOpcode.CM_CHAR_CREATE;
case "/Game.bns/DeletePC":
return LoginPacketOpcode.CM_CHAR_DELETE;
case "/Sts/Ping":
return LoginPacketOpcode.CM_PING;
case "/Slot/ListSlots":
return LoginPacketOpcode.CM_SLOT_LIST;
case "/Auth/GetMyUserInfo":
return LoginPacketOpcode.CM_USER_INFO;
case "/SecondPassword/GetStatus":
case "/Grade.bns/GetGameGrade":
case "/Friend/GetUserInfo":
case "/VirtualCurrency/GetBalance":
case "/Friend/PageRecvProposals":
case "/Goods/ListCategoryIds":
return LoginPacketOpcode.CM_ERROR_RETURN;
}
return LoginPacketOpcode.Unknown;
}
public override void SendPacket(Packet<T> p, bool noWarper)
{
throw new NotImplementedException();
}
public override void SendPacket(Packet<T> p)
{
byte[] buf = p.GetBytes((ushort)(p.Length - 2), 2);
if (Crypt.KeyExchange.IsReady && p.Encrypt)
Crypt.Encrypt(buf, 0, buf.Length);
SendPacketRaw(buf, 0, buf.Length);
}
}[/SRC]
Этой информации достаточно, чтобы написать рабочий логин сервер. Да, вы не нашли здесь систем создания аккаунтов, соединение с БД и т.д. Я намеренно не буду выкладывать такие вещи, поскольку каждый кто будет писать свой эмулятор, напишет эти модули под себя и под свою выбранную БД.
Продолжение следует.
Топик буду обновлять по мере ваших успехов и продвижений в разработке.