访问网站提示输入用户名密码,建立网站的流程多少钱,化妆品网站建设目的,天眼查企业信息查询平台官网专题介绍
该专题将会分析 LOMCN 基于韩版传奇 2#xff0c;使用 .NET 重写的传奇源码#xff08;服务端 客户端#xff09;#xff0c;分析数据交互、状态管理和客户端渲染等技术#xff0c;此外笔者还会分享将客户端部分移植到 Unity 和服务端用现代编程语言重写的全过…专题介绍
该专题将会分析 LOMCN 基于韩版传奇 2使用 .NET 重写的传奇源码服务端 客户端分析数据交互、状态管理和客户端渲染等技术此外笔者还会分享将客户端部分移植到 Unity 和服务端用现代编程语言重写的全过程。
概览
在这一篇文章中我们将从客户端入手分析从 TCP 连接建立、登录鉴权、角色选择、开始游戏到游戏内交互的全过程。
客户端启动
WinForm 入口 Program.cs
与服务端类似客户端也是一个 WinForm 应用程序在 Application 启动后会先跳转到 AMain 检查是否有热更新随后再跳转到 CMain 开启客户端主逻辑
// Program.cs
[STAThread]
private static void Main(string[] args)
{// ...Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false);if (Settings.P_Patcher) Application.Run(PForm new Launcher.AMain());else Application.Run(Form new CMain());// ...
}监听事件循环
在 CMain 的构造函数中我们监听了 Application Idle 事件作为事件循环
// CMain.cs
public CMain()
{InitializeComponent();Application.Idle Application_Idle;// ...
}在 Application_Idle 中我们通过 UpdateTime 更新客户端全局的时间戳通过 UpdateEnviroment 处理网络数据通过 RenderEnvironment 处理客户端渲染
private static void Application_Idle(object sender, EventArgs e)
{try{while (AppStillIdle){UpdateTime();UpdateEnviroment();RenderEnvironment();}}catch (Exception ex){SaveError(ex.ToString());}
}客户端场景划分
在用户登录之前UpdateEnviroment 发现连接实例为空不会做任何操作因此我们先跳过这个函数来看 RenderEnvironment 的处理过程这里实际上就是基于 Direct 3D 的客户端的渲染循环请大家注意 MirScene.ActiveScene.Draw 这个调用传奇通过 Scene 去区分不同的场景例如登录页面、角色选择页面和游戏页面每个页面都是一个独立的 Scene
private static void RenderEnvironment()
{try{if (DXManager.DeviceLost){DXManager.AttemptReset();Thread.Sleep(1);return;}DXManager.Device.Clear(ClearFlags.Target, Color.CornflowerBlue, 0, 0);DXManager.Device.BeginScene();DXManager.Sprite.Begin(SpriteFlags.AlphaBlend);DXManager.SetSurface(DXManager.MainSurface);// Note hereif (MirScene.ActiveScene ! null)MirScene.ActiveScene.Draw();DXManager.Sprite.End();DXManager.Device.EndScene();DXManager.Device.Present();}catch (Direct3D9Exception ex){DXManager.DeviceLost true;}catch (Exception ex){SaveError(ex.ToString());DXManager.AttemptRecovery();}
}那么当前的 ActiveScene 是在哪里设置的呢实际上在 MirScene 初始化时它会被指定为 LoginScene
public abstract class MirScene : MirControl
{public static MirScene ActiveScene new LoginScene();// ...
}因此上面的 Draw 方法其实会将登录页面绘制出来我们这里先跳过 GUI 相关的部分直接来看一下当用户输入完账号密码后是如何建立连接和发起登录的。
TCP 连接建立
传奇中的每个 Scene 都是继承自 MirControl 的 UI 对象MirControl 提供了 Shown 回调用于监听 UI 的展示在 LoginScene 展示时我们会开启 TCP 连接
public LoginScene()
{// ...Shown (sender, args) {Network.Connect();_connectBox.Show();};
}Network 是客户端的网络管理类在 Connect 方法中我们会创建一个 TcpClient 对象并发起连接服务端的信息通过配置获取
public static void Connect()
{if (_client ! null)Disconnect();ConnectAttempt;_client new TcpClient {NoDelay true};_client.BeginConnect(Settings.IPAddress, Settings.Port, Connection, null);
}与服务端的处理方式类似在 BeginConnect 的异步回调中我们会开启 receiveList 和 sendList 两个队列然后通过 BeginReceive 接收服务端数据、处理成 Packet 并加入 receiveList 等待处理。在客户端每帧 Process 的过程中我们会处理 receiveList 更改客户端状态同时根据用户输入产生数据包加入到 sendList 发送到服务端。
第一个数据包
服务端发送 S.Connected
通过上面的分析我们知道客户端启动的第一步是发起 TCP 连接请求服务端在对 Client 进行 Accept 时会创建 MirConnection 对象如果对此没有印象可以参考第一篇文章在 MirConnection 的构造方法中我们会向客户端发送 Connected 数据包这便是客户端与服务端交流的第一个数据包啦
public MirConnection(int sessionID, TcpClient client)
{// ..._receiveList new ConcurrentQueuePacket();_sendList new ConcurrentQueuePacket();_sendList.Enqueue(new S.Connected());_retryList new QueuePacket();Connected true;BeginReceive();
}客户端处理 S.Connected
前面我们提到在 TCP 连接建立之前基于 Application Idle 的事件循环对 UpdateEnviroment 的调用会被忽略而在连接建立之后这里会通过 Network.Process 处理服务端数据包和发送这一帧产生的数据包数据包会被路由到 ActiveScene 进行处理因此这里的 ProcessPacket 会调用到 LoginScene
public static void Process()
{// ...while (_receiveList ! null !_receiveList.IsEmpty){if (!_receiveList.TryDequeue(out Packet p) || p null) continue;MirScene.ActiveScene.ProcessPacket(p);}if (CMain.Time TimeOutTime _sendList ! null _sendList.IsEmpty)_sendList.Enqueue(new C.KeepAlive());if (_sendList null || _sendList.IsEmpty) return;TimeOutTime CMain.Time Settings.TimeOut; // 5000msListbyte data new Listbyte();while (!_sendList.IsEmpty){if (!_sendList.TryDequeue(out Packet p)) continue;data.AddRange(p.GetPacketBytes());}CMain.BytesSent data.Count;BeginSend(data);
}在 LoginScene 的 ProcessPacket 中包含了对客户端初始化和账户相关的数据处理由于当前数据包是 S.Connected 自然会进入到 ServerPacketIds.Connected 这个 case随后客户端通过 SendVersion 发送数据完整性检查请求这里会对 Executable 进行 hash
public override void ProcessPacket(Packet p)
{switch (p.Index){case (short)ServerPacketIds.Connected:Network.Connected true;SendVersion();break;case (short)ServerPacketIds.ClientVersion:ClientVersion((S.ClientVersion) p);break;// ...default:base.ProcessPacket(p);break;}
}数据完整性检查与 Connected 数据包类似首先客户端发送 hash 到服务端服务端校验后将结果返回到客户端这是一个初级的逆向对抗策略可通过修改发送的 hash 或忽略返回的错误跳过。
客户端登录过程
在上述检查通过以后客户端会展示账号密码输入页面用户输入账号密码后点击登录会调用 Login 方法发起登录请求
// LoginScene.cs
private void Login()
{OKButton.Enabled false;Network.Enqueue(new C.Login {AccountID AccountIDTextBox.Text, Password PasswordTextBox.Text});
}作为一款早期的游戏传奇的密码采用了明文传输囧服务端收到 C.Login 数据包后会尝试从 Account Database 中查询与之匹配的账户如果校验失败会发送 S.Login 返回登录失败的原因成功则发送 S.LoginSuccess
// Envir.cs
public void Login(ClientPackets.Login p, MirConnection c)
{// ...if (!AccountIDReg.IsMatch(p.AccountID)){c.Enqueue(new ServerPackets.Login { Result 1 });return;}if (!PasswordReg.IsMatch(p.Password)){c.Enqueue(new ServerPackets.Login { Result 2 });return;}var account GetAccount(p.AccountID);if (account null){c.Enqueue(new ServerPackets.Login { Result 3 });return;}// ...if (string.CompareOrdinal(account.Password, p.Password) ! 0){if (account.WrongPasswordCount 5){account.Banned true;account.BanReason Too many Wrong Login Attempts.;account.ExpiryDate DateTime.Now.AddMinutes(2);c.Enqueue(new ServerPackets.LoginBanned{Reason account.BanReason,ExpiryDate account.ExpiryDate});return;}c.Enqueue(new ServerPackets.Login { Result 4 });return;}account.WrongPasswordCount 0;lock (AccountLock){account.Connection?.SendDisconnect(1);account.Connection c;}c.Account account;c.Stage GameStage.Select;account.LastDate Now;account.LastIP c.IPAddress;MessageQueue.Enqueue(account.Connection.SessionID , account.Connection.IPAddress , User logged in.);c.Enqueue(new ServerPackets.LoginSuccess { Characters account.GetSelectInfo() });
}相应地在客户端侧也包含了对 Login 和 LoginSuccess 的处理
// LoginScene.cs
public override void ProcessPacket(Packet p)
{switch (p.Index){// ...case (short)ServerPacketIds.Login:Login((S.Login) p);break;case (short)ServerPacketIds.LoginSuccess:Login((S.LoginSuccess) p);break;default:base.ProcessPacket(p);break;}
}在登录失败时会调用到 private void Login(S.Login p) 这个重载方法展示登录失败原因事实上出于安全考虑登录失败的原因应当尽可能模糊
// LoginScene.cs
private void Login(S.Login p)
{_login.OKButton.Enabled true;switch (p.Result){case 0:MirMessageBox.Show(Logging in is currently disabled.);_login.Clear();break;case 1:MirMessageBox.Show(Your AccountID is not acceptable.);_login.AccountIDTextBox.SetFocus();break;case 2:MirMessageBox.Show(Your Password is not acceptable.);_login.PasswordTextBox.SetFocus();break;case 3:MirMessageBox.Show(GameLanguage.NoAccountID);_login.PasswordTextBox.SetFocus();break;case 4:MirMessageBox.Show(GameLanguage.IncorrectPasswordAccountID);_login.PasswordTextBox.Text string.Empty;_login.PasswordTextBox.SetFocus();break;}
}在登录成功时会调用到 private void Login(S.LoginSuccess p) 这个重载方法切换到角色选择 Scene 等待用户的下一步操作为了避免额外的数据交互服务端在登录成功后会返回角色列表
// LoginScene.cs
private void Login(S.LoginSuccess p)
{Enabled false;_login.Dispose();if(_ViewKey ! null !_ViewKey.IsDisposed) _ViewKey.Dispose();SoundManager.PlaySound(SoundList.LoginEffect);_background.Animated true;_background.AfterAnimation (o, e) {Dispose();ActiveScene new SelectScene(p.Characters);};
}开始游戏
服务端同步角色数据
在用户选择完角色点击开始游戏后客户端会发送包含角色选择信息的 C.StartGame 数据包到服务端
// SelectScene.cs
public void StartGame()
{// ...Network.Enqueue(new C.StartGame{CharacterIndex Characters[_selected].Index});
}服务端在接收到 C.StartGame 后会读从数据库读取角色数据随后新建一个 PlayerObject 调用 StartGame 方法
// MirConnection.cs
private void StartGame(C.StartGame p)
{// ...CharacterInfo info null;for (int i 0; i Account.Characters.Count; i){if (Account.Characters[i].Index ! p.CharacterIndex) continue;info Account.Characters[i];break;}if (info null){Enqueue(new S.StartGame { Result 2 });return;}// ...Player new PlayerObject(info, this);Player.StartGame();
}在 PlayerObject 的 StartGame 方法中服务端将角色添加到地图中随后发送游戏开始和玩家数据到客户端
// PlayerObject.cs
public void StartGame()
{Map temp Envir.GetMap(CurrentMapIndex);if (temp ! null temp.Info.NoReconnect){Map temp1 Envir.GetMapByNameAndInstance(temp.Info.NoReconnectMap);if (temp1 ! null){temp temp1;CurrentLocation GetRandomPoint(40, 0, temp);}}if (temp null || !temp.ValidPoint(CurrentLocation)){temp Envir.GetMap(BindMapIndex);if (temp null || !temp.ValidPoint(BindLocation)){SetBind();temp Envir.GetMap(BindMapIndex);if (temp null || !temp.ValidPoint(BindLocation)){StartGameFailed();return;}}CurrentMapIndex BindMapIndex;CurrentLocation BindLocation;}temp.AddObject(this);CurrentMap temp;Envir.Players.Add(this);StartGameSuccess();//Call Login NPCCallDefaultNPC(DefaultNPCType.Login);//Call Daily NPCif (Info.NewDay){CallDefaultNPC(DefaultNPCType.Daily);}
}随后在 StartGameSuccess 的调用中向客户端发送游戏开发和角色数据这里的每个 Get 方法的作用都是将地图和角色数据同步到客户端
// PlayerObject.cs
private void StartGameSuccess()
{Connection.Stage GameStage.Game;// ...Enqueue(new S.StartGame { Result 4, Resolution Settings.AllowedResolution });ReceiveChat(string.Format(GameLanguage.Welcome, GameLanguage.GameName), ChatType.Hint);// ...Spawned();SetLevelEffects();GetItemInfo();GetMapInfo();GetUserInfo();GetQuestInfo();GetRecipeInfo();GetCompletedQuests();GetMail();GetFriends();GetRelationship();if ((Info.Mentor ! 0) (Info.MentorDate.AddDays(Settings.MentorLength) DateTime.Now))MentorBreak();elseGetMentor();CheckConquest();GetGameShop();// ...
}private void GetUserInfo()
{string guildname MyGuild ! null ? MyGuild.Name : ;string guildrank MyGuild ! null ? MyGuildRank.Name : ;S.UserInformation packet new S.UserInformation{ObjectID ObjectID,RealId (uint)Info.Index,Name Name,GuildName guildname,GuildRank guildrank,NameColour GetNameColour(this),Class Class,Gender Gender,Level Level,Location CurrentLocation,Direction Direction,Hair Hair,HP HP,MP MP,Experience Experience,MaxExperience MaxExperience,LevelEffects LevelEffects,Inventory new UserItem[Info.Inventory.Length],Equipment new UserItem[Info.Equipment.Length],QuestInventory new UserItem[Info.QuestInventory.Length],Gold Account.Gold,Credit Account.Credit,HasExpandedStorage Account.ExpandedStorageExpiryDate Envir.Now ? true : false,ExpandedStorageExpiryTime Account.ExpandedStorageExpiryDate};Info.Inventory.CopyTo(packet.Inventory, 0);Info.Equipment.CopyTo(packet.Equipment, 0);Info.QuestInventory.CopyTo(packet.QuestInventory, 0);//IntelligentCreaturefor (int i 0; i Info.IntelligentCreatures.Count; i)packet.IntelligentCreatures.Add(Info.IntelligentCreatures[i].CreateClientIntelligentCreature());packet.SummonedCreatureType SummonedCreatureType;packet.CreatureSummoned CreatureSummoned;Enqueue(packet);
}客户端开始游戏
客户端目前处于 SelectScene在收到游戏启动成功的数据包 S.StartGame 后会根据返回数据调整分辨率并切换到 GameScene
public void StartGame(S.StartGame p)
{StartGameButton.Enabled true;switch (p.Result){case 0:MirMessageBox.Show(Starting the game is currently disabled.);break;case 1:MirMessageBox.Show(You are not logged in.);break;case 2:MirMessageBox.Show(Your character could not be found.);break;case 3:MirMessageBox.Show(No active map and/or start point found.);break;case 4:if (p.Resolution Settings.Resolution || Settings.Resolution 0) Settings.Resolution p.Resolution;switch (Settings.Resolution){default:case 1024:Settings.Resolution 1024;CMain.SetResolution(1024, 768);break;case 1280:CMain.SetResolution(1280, 800);break;case 1366:CMain.SetResolution(1366, 768);break;case 1920:CMain.SetResolution(1920, 1080);break;}ActiveScene new GameScene();Dispose();break;}
}在 GameScene 中客户端会处理来自服务端的角色信息、地图数据以及 NPC 和其他玩家数据等例如在收到游戏开始时服务端发送的 S.UserInformation 后会创建当前玩家的角色
// GameScene.cs
public override void ProcessPacket(Packet p)
{switch (p.Index){// ...case (short)ServerPacketIds.UserInformation:UserInformation((S.UserInformation)p);break;// ...}
}private void UserInformation(S.UserInformation p)
{User new UserObject(p.ObjectID);User.Load(p);MainDialog.PModeLabel.Visible User.Class MirClass.Wizard || User.Class MirClass.Taoist;Gold p.Gold;Credit p.Credit;InventoryDialog.RefreshInventory();foreach (SkillBarDialog Bar in SkillBarDialogs)Bar.Update();
}下一步
到这里整个客户端的启动流程就分析完了接下来的逻辑主要集中在服务端向客户端同步状态和客户端发送角色行为在接下来的文章中我们将深入分析这些交互的处理过程。