ZoneService.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  1. using CommonAI;
  2. using CommonAI.Zone.Instance;
  3. using CommonAI.ZoneServer.JSGModule;
  4. using CommonLang;
  5. using CommonLang.Concurrent;
  6. using CommonLang.Log;
  7. using Pomelo;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Configuration;
  11. using System.Diagnostics;
  12. using System.Dynamic;
  13. using System.Threading;
  14. using System.Web.Helpers;
  15. using XmdsCommon.Plugin;
  16. using XmdsCommonServer.Plugin;
  17. using XmdsServerNode.Node;
  18. using XmdsServerNode.Node.R2bNotify;
  19. using static CommonAI.ZoneServer.JSGModule.JSGServerProfile;
  20. namespace XmdsServerEdgeJS.Zone
  21. {
  22. public abstract class ZoneService : IZone
  23. {
  24. //战斗服版本号
  25. public static int s_bsVersion = 1;
  26. public static ZoneService Instance { get; private set; }
  27. protected ZoneService()
  28. {
  29. Instance = this;
  30. updatePrivateMemory(null);
  31. }
  32. //----------------------------------------------------------------------------------------------
  33. protected Logger log = LoggerFactory.GetLogger(typeof(ZoneService).Name);
  34. protected Logger monitorLog = LoggerFactory.GetLogger("monitor");
  35. private ZoneNodeManager zoneNodeManager;
  36. private BattleCodec codec;
  37. private AtomicLong privateMemoryMB = new AtomicLong(0);
  38. private Timer systemUpdateTimer;
  39. private Timer logUpdateTimer;
  40. protected XmdsServerProxy b2r_proxy = new XmdsServerProxy();
  41. /// <summary>
  42. /// 副本列表
  43. /// </summary>
  44. private HashMap<string, XmdsZoneNode> nodes = new HashMap<string, XmdsZoneNode>();
  45. /// <summary>
  46. /// 玩家列表
  47. /// </summary>
  48. private HashMap<string, XmdsPlayer> players = new HashMap<string, XmdsPlayer>();
  49. //----------------------------------------------------------------------------------------------
  50. public int PlayerCount
  51. {
  52. get
  53. {
  54. lock (players)
  55. {
  56. return players.Count;
  57. }
  58. }
  59. }
  60. public int ZoneNodeCount
  61. {
  62. get
  63. {
  64. lock (nodes)
  65. {
  66. return nodes.Count;
  67. }
  68. }
  69. }
  70. public long ProcessPrivateMemoryMB
  71. {
  72. get { return privateMemoryMB.Value; }
  73. }
  74. //----------------------------------------------------------------------------------------------
  75. #region Internal
  76. public XmdsPlayer getPlayer(string playerID)
  77. {
  78. lock (players)
  79. {
  80. return players.Get(playerID);
  81. }
  82. }
  83. protected XmdsZoneNode getZoneNode(string instanceID)
  84. {
  85. lock (nodes)
  86. {
  87. return nodes.Get(instanceID);//[instanceID];
  88. }
  89. }
  90. //internal void sendToGameServer(string name, Object param)
  91. //{
  92. // try
  93. // {
  94. // //转化为json
  95. // string json = Json.Encode(param);
  96. // IceManager.instance().eventNotify(name, json);
  97. // }
  98. // catch (Exception err)
  99. // {
  100. // log.Error(err.Message, err);
  101. // }
  102. //}
  103. protected void notifyBattleServer(string instanceId, R2BNotifyMessage param)
  104. {
  105. var node = getZoneNode(instanceId);
  106. if (node != null)
  107. {
  108. //b2r_proxy.SendToBattleServer(node.Node.Zone, param);
  109. param.OnHandle(node.Node.Zone);
  110. }
  111. }
  112. private void updatePrivateMemory(object state)
  113. {
  114. long private_memory_mb = Process.GetCurrentProcess().PrivateMemorySize64 / 1024 / 1024;
  115. privateMemoryMB.Value = private_memory_mb;
  116. }
  117. private void updateLog(object state)
  118. {
  119. dynamic eventStat = new ExpandoObject();
  120. eventStat.Type = "monitor";
  121. eventStat.Object = InstanceZoneObject.ActiveObjectCount + "/" + InstanceZoneObject.AllocObjectCount;
  122. eventStat.Zone = InstanceZone.ActiveZoneCount + "/" + InstanceZone.AllocZoneCount;
  123. eventStat.Node = ZoneNodeCount;
  124. eventStat.Player = PlayerCount;
  125. eventStat.Memory_MB = ProcessPrivateMemoryMB;
  126. eventStat.Pool = ObjectPoolStatus.TotalActive + "/" + ObjectPoolStatus.TotalCount;
  127. monitorLog.Debug(eventStat);
  128. // 输出统计日志
  129. if (JSGServerProfile.CheckPrintAllData())
  130. {
  131. this.PrintAllSceneInfo();
  132. }
  133. }
  134. private void PrintAllSceneInfo()
  135. {
  136. lock (nodes)
  137. {
  138. foreach (var p in new List<XmdsZoneNode>(nodes.Values))
  139. {
  140. try
  141. {
  142. string ext = p.Node.Zone.IsObjectMapNull() ? ", 场景已销毁:" : ", units=" + p.Node.Zone.AllUnitsCount
  143. + ", spells=" + p.Node.Zone.AllSpellsCount + ", items=" + p.Node.Zone.AllItemsCount + ", players: " + p.Node.Zone.AllPlayersCount;
  144. log.Info("PrintAllSceneInfo-ID:" + p.InstanceID + ", " + p.Node.GetBindGameSrvId() + ", 场景ID: " + p.Node.SceneID + ext);
  145. }
  146. catch (Exception e)
  147. {
  148. log.Error("PrintAllSceneInfo: " + p.InstanceID + ", e: " + e);
  149. }
  150. }
  151. }
  152. }
  153. #endregion
  154. //----------------------------------------------------------------------------------------------
  155. public void Start(ZoneConfig zoneConfig)
  156. {
  157. lock (this)
  158. {
  159. try
  160. {
  161. log.Info("startPath:" + zoneConfig.startPath);
  162. log.Info("assetPath:" + zoneConfig.assetPath);
  163. JSGServerProfile.init();
  164. systemUpdateTimer = new Timer(updatePrivateMemory, this, 0, 10000);
  165. zoneNodeManager = new ZoneNodeManager();
  166. zoneNodeManager.Init(zoneConfig.startPath, b2r_proxy, zoneConfig.assetPath);
  167. codec = new BattleCodec(ZoneNodeManager.Templates.Templates);
  168. //日志定时器
  169. logUpdateTimer = new Timer(updateLog, this, 60000, 60000);
  170. }
  171. catch (Exception err)
  172. {
  173. //log.Error("初始化失败");
  174. //log.Error(err.Message, err);
  175. //Environment.Exit(0);
  176. throw err;
  177. }
  178. }
  179. }
  180. public void Stop()
  181. {
  182. this.ClearAllPlayers((e, c) => { });
  183. this.DestoryAllZones((e, c) => { });
  184. lock (this)
  185. {
  186. if (zoneNodeManager != null)
  187. {
  188. b2r_proxy.Dispose();
  189. zoneNodeManager.Shutdown();
  190. systemUpdateTimer.Dispose();
  191. logUpdateTimer.Dispose();
  192. zoneNodeManager = null;
  193. codec = null;
  194. }
  195. }
  196. }
  197. public void SetCallBack(string gameSrvId)
  198. {
  199. JSGMountainKingModule.Init(gameSrvId);
  200. }
  201. public void CreateZone(string playerId, string gameServerId, int mapTemplateId, string instanceId, bool forceCreate,
  202. string data, Action<int> cb, Action<Exception> err)
  203. {
  204. long lstart = TimeUtil.GetTimestampMS();
  205. int resCode = 0;
  206. if (!forceCreate && playerId != null && playerId.Length > 0)
  207. {
  208. var player = getPlayer(playerId);
  209. if (player != null && player.BindingActor.IsActive && player.BindingActor.Virtual.IsInPVP())
  210. {
  211. resCode = 1;
  212. XmdsVirtual playerVirtual = player.BindingActor.Virtual as XmdsVirtual;
  213. log.Warn("PVP状态下传送2:" + playerVirtual.mUnit.PlayerUUID + ", 场景ID: " + playerVirtual.mUnit.Parent.GetSceneID() + ",hateInfo: "
  214. + playerVirtual.GetHateSystem().GetHatePlayerInfo() + ", 触发PVP玩家:" + playerVirtual.mPvpTriggerPlayerId);
  215. }
  216. }
  217. if (resCode == 0)
  218. {
  219. long private_memory_mb = ProcessPrivateMemoryMB;
  220. if (private_memory_mb > ZoneNodeManager.NodeConfig.SYSTEM_MAX_HEAP_SIZE_MB)
  221. {
  222. string msg = string.Format("CreateZone failed : Private memory {0}MB out of range {1}MB", private_memory_mb, ZoneNodeManager.NodeConfig.SYSTEM_MAX_HEAP_SIZE_MB);
  223. log.Error(msg);
  224. throw new OutOfMemoryException(msg);
  225. }
  226. XmdsZoneNode node = new XmdsZoneNode(this, instanceId, gameServerId);
  227. node.Node.Callback = IceManager.instance().getCallback(gameServerId);
  228. lock (nodes)
  229. {
  230. if (nodes.ContainsKey(instanceId))
  231. {
  232. throw new Exception(string.Format("node instance id ({0}) already exist!", instanceId));
  233. }
  234. nodes.Add(node.InstanceID, node);
  235. }
  236. {
  237. node.Start(mapTemplateId, data, (z) =>
  238. {
  239. //监控日志
  240. dynamic logEvent = new ExpandoObject();
  241. logEvent.type = "createZone";
  242. logEvent.mapTemplateId = mapTemplateId;
  243. logEvent.instanceId = instanceId;
  244. monitorLog.Debug(logEvent);
  245. });
  246. }
  247. JSGServerProfile.RecordZoneCreate(mapTemplateId, (int)(TimeUtil.GetTimestampMS() - lstart));
  248. }
  249. else
  250. {
  251. log.Warn("玩家pvp状态创建场景:" + playerId + ", " + mapTemplateId + ", " + data);
  252. }
  253. cb(resCode);
  254. }
  255. public void DestroyZone(string instanceId, Action<Exception, object> cb)
  256. {
  257. XmdsZoneNode node;
  258. lock (nodes)
  259. {
  260. node = this.nodes.RemoveByKey(instanceId);
  261. }
  262. if (node != null)
  263. {
  264. //删除场景实例的所有玩家
  265. lock (players)
  266. {
  267. node.Node.ForEachPlayers((client) =>
  268. {
  269. var player = players.Get(client.PlayerUUID);
  270. if (player != null)
  271. {
  272. if (client.Node == player.Node)
  273. {
  274. players.RemoveByKey(client.PlayerUUID);
  275. }
  276. //else
  277. //{
  278. // log.Warn(client.Node + " destory zone player " + client.PlayerUUID + " in " + player.Node);
  279. //}
  280. }
  281. });
  282. }
  283. node.Stop((z) =>
  284. {
  285. //监控日志
  286. dynamic logEvent = new ExpandoObject();
  287. logEvent.type = "destroyZone";
  288. logEvent.mapTemplateId = z.Node.SceneID;
  289. logEvent.instanceId = instanceId;
  290. monitorLog.Debug(logEvent);
  291. });
  292. }
  293. cb(null, "done");
  294. }
  295. public void DestoryAllZones(Action<Exception, object> cb)
  296. {
  297. lock (nodes)
  298. {
  299. foreach (var p in new List<XmdsZoneNode>(nodes.Values))
  300. {
  301. try
  302. {
  303. this.DestroyZone(p.InstanceID, (e, c) => { });
  304. }
  305. catch (Exception e)
  306. {
  307. log.Error(e);
  308. }
  309. }
  310. }
  311. cb(null, "done");
  312. }
  313. public void PlayerEnter(string playerId, string instanceId, string input, Action<Exception, object> cb)
  314. {
  315. var node = getZoneNode(instanceId);
  316. if (node == null)
  317. {
  318. throw new InstanceNotExistException(instanceId);
  319. }
  320. long private_memory_mb = ProcessPrivateMemoryMB;
  321. if (private_memory_mb > ZoneNodeManager.NodeConfig.SYSTEM_MAX_HEAP_SIZE_MB)
  322. {
  323. string msg = string.Format("PlayerEnter failed : Private memory {0}MB out of range {1}MB", private_memory_mb, ZoneNodeManager.NodeConfig.SYSTEM_MAX_HEAP_SIZE_MB);
  324. log.Error(msg);
  325. throw new OutOfMemoryException(msg);
  326. }
  327. dynamic data = Json.Decode(input);
  328. string connectServerId = (string)data.connectServerId;
  329. var session = FastStream.instance().GetSessionByID(connectServerId);
  330. if (session == null)
  331. {
  332. throw new Exception("ConnectServerId not exist : " + connectServerId);
  333. }
  334. string uid = (string)data.uid;
  335. bool robot = data.robot != null ? (bool)data.robot : false;
  336. XmdsPlayerEnter enter = null;
  337. if (data["tempData"] != null)
  338. {
  339. int posX = 0;
  340. int posY = 0;
  341. float direction = 0;
  342. string flagName = null;
  343. flagName = data.tempData.flag;
  344. if (data.tempData["x"] != null)
  345. {
  346. posX = (int)data.tempData.x;
  347. }
  348. if (data.tempData["y"] != null)
  349. {
  350. posY = (int)data.tempData.y;
  351. }
  352. if (data.tempData["direction"] != null)
  353. {
  354. direction = (float)data.tempData.direction;
  355. }
  356. int unitTemplateID = (int)data.unitTemplateID;
  357. XmdsUnitProperties unitprop = new XmdsUnitProperties();
  358. XmdsUnitData unitData = XmdsPlayerUtil.instance().createXmdsUnitData(data, playerId);
  359. unitprop.ServerData = unitData;
  360. enter = new XmdsPlayerEnter();
  361. enter.Pos = new CommonLang.Vector.Vector2(posX, posY);
  362. enter.direction = direction;
  363. enter.FlagName = flagName;
  364. enter.UnitData = new CommonAI.ZoneServer.CreateUnitInfoR2B();
  365. enter.UnitData.Force = unitData.BaseInfo.force;
  366. enter.UnitData.alliesForce = unitData.BaseInfo.alliesForce;
  367. enter.UnitData.StartFlag = null;
  368. enter.UnitData.UnitTemplateID = unitTemplateID;
  369. enter.UnitData.UnitPropData = unitprop;
  370. foreach (dynamic obj in data.tasks)
  371. {
  372. int taskId = obj.QuestID;
  373. int state = obj.State;
  374. XmdsQuestData qData = new XmdsQuestData();
  375. qData.TaskID = taskId.ToString();
  376. qData.TaskState = state.ToString();
  377. foreach (dynamic attr in obj.Attributes)
  378. {
  379. qData.AddAttribute(attr.key, attr.value);
  380. }
  381. unitData.Tasks.Add(qData);
  382. }
  383. foreach (dynamic obj in data.flags)
  384. {
  385. XmdsQuestFlag flag = new XmdsQuestFlag();
  386. flag.FlagName = obj[0];
  387. flag.FlagValue = obj[1];
  388. unitData.QuestFlags.Add(flag);
  389. }
  390. unitData.PlayerEntered = data.playerEntered;
  391. }
  392. XmdsPlayer player = new XmdsPlayer(this, playerId, uid, session, node);
  393. lock (players)
  394. {
  395. if (players.ContainsKey(playerId))
  396. {
  397. this.players[playerId] = player;
  398. //string oldInstanceId = players.Get(playerId).InstanceId;
  399. //PlayerLeave(playerId, oldInstanceId, false, (err, ret) =>
  400. //{
  401. // if (err != null)
  402. // {
  403. // log.Error(err);
  404. // }
  405. //});
  406. }
  407. else
  408. {
  409. this.players.Add(playerId, player);
  410. }
  411. node.Node.OnPlayerEnter(player, enter,
  412. (c) =>
  413. {
  414. player.BindingActor.IsRobot = robot;
  415. //监控日志
  416. dynamic logEvent = new ExpandoObject();
  417. logEvent.type = "playerEnter";
  418. logEvent.mapTemplateId = node.Node.SceneID;
  419. logEvent.instanceId = instanceId;
  420. logEvent.playerId = playerId;
  421. monitorLog.Debug(logEvent);
  422. //cb(null, "done");
  423. },
  424. (e) =>
  425. {
  426. log.Error(e.Message, e);
  427. lock (this.players) { this.players.RemoveByKey(playerId); }
  428. //cb(e, null);
  429. });
  430. }
  431. cb(null, "done");
  432. }
  433. public void PlayerLeave(string playerId, string instanceId, bool keepObject, Action<Exception, object> cb)
  434. {
  435. XmdsPlayer player = null;
  436. try
  437. {
  438. lock (players)
  439. {
  440. player = players.Get(playerId);
  441. if (player != null)
  442. {
  443. if (instanceId.Equals(player.InstanceId))
  444. {
  445. this.players.RemoveByKey(playerId);
  446. }
  447. else
  448. {
  449. log.Warn(instanceId + " leave player " + player.InstanceId + player.Node);
  450. }
  451. }
  452. }
  453. var node = getZoneNode(instanceId);
  454. if (node == null)
  455. {
  456. //throw new InstanceNotExistException(instanceId);
  457. cb(null, "done");
  458. return;
  459. }
  460. //玩家已经离开场景
  461. //if (player == null)
  462. //{
  463. // throw new PlayerNotExistException(playerId);
  464. //}
  465. node.Node.OnPlayerLeave(player != null ? player.BindingActor : node.Node.GetPlayer(playerId),
  466. (c) =>
  467. {
  468. //监控日志
  469. dynamic logEvent = new ExpandoObject();
  470. logEvent.type = "playerLeave";
  471. logEvent.mapTemplateId = node.Node.SceneID;
  472. logEvent.playerId = playerId;
  473. monitorLog.Debug(logEvent);
  474. //cb(null, "done");
  475. },
  476. (e) =>
  477. {
  478. //cb(e, null);
  479. log.Error(e.Message, e);
  480. },
  481. keepObject);
  482. cb(null, "done");
  483. }
  484. catch (Exception e)
  485. {
  486. log.Error(e.Message, e);
  487. cb(e, null);
  488. }
  489. finally
  490. {
  491. if (player != null) player.Dispose();
  492. }
  493. }
  494. public void PlayerNetStateChanged(string playerId, string state)
  495. {
  496. var player = getPlayer(playerId);
  497. if (player == null)
  498. {
  499. log.Error("player not exist :" + playerId + " PlayerNetStateChanged");
  500. return;
  501. }
  502. var node = player.Node;
  503. if (node == null)
  504. {
  505. log.Error("zone node not exist :" + player.InstanceId + " PlayerNetStateChanged");
  506. return;
  507. }
  508. log.Debug("player:" + playerId + " PlayerNetStateChanged " + state);
  509. node.OnPlayerNetStateChanged(player, state);
  510. }
  511. public void PlayerReceive(string playerId, byte[] msg)
  512. {
  513. var player = getPlayer(playerId);
  514. if (player == null)
  515. {
  516. log.Debug("fast stream receive not exist player : " + playerId);
  517. return;
  518. }
  519. var node = player.Node;
  520. if (node == null)
  521. {
  522. log.Error("zone node not exist : " + player.InstanceId + " fast stream receive");
  523. return;
  524. }
  525. //log.Debug("player:" + playerId + " socket.receive");
  526. node.OnPlayerReceivedMessage(player, msg);
  527. }
  528. public void GetAllPlayerCount(Action<Exception, int> cb)
  529. {
  530. int count = PlayerCount;
  531. cb(null, count);
  532. }
  533. public void ClearAllPlayers(Action<Exception, object> cb)
  534. {
  535. try
  536. {
  537. lock (players)
  538. {
  539. foreach (var p in new List<XmdsPlayer>(players.Values))
  540. {
  541. this.PlayerLeave(p.PlayerUUID, p.InstanceId, false, (e, c) => { });
  542. }
  543. }
  544. cb(null, "done");
  545. }
  546. catch (Exception e)
  547. {
  548. cb(e, null);
  549. }
  550. }
  551. public void GetAllPlayers(Action<Exception, object> cb)
  552. {
  553. try
  554. {
  555. var ret = new List<string>();
  556. lock (players)
  557. {
  558. foreach (var p in new List<XmdsPlayer>(players.Values))
  559. {
  560. ret.Add(p.PlayerUUID);
  561. }
  562. }
  563. cb(null, ret.ToArray());
  564. }
  565. catch (Exception e)
  566. {
  567. cb(e, null);
  568. }
  569. }
  570. public void GetServerState(string serverID, Action<Exception, object> cb)
  571. {
  572. try
  573. {
  574. int zc = ZoneNodeCount;
  575. int pc = PlayerCount;
  576. int ActiveObjectCount = InstanceZoneObject.ActiveObjectCount;
  577. int AllocObjectCount = InstanceZoneObject.AllocObjectCount;
  578. int ActiveZoneCount = InstanceZone.ActiveZoneCount;
  579. int AllocZoneCount = InstanceZone.AllocZoneCount;
  580. float w = zc * 1000 + pc;
  581. int flag = IceManager.instance().getCallback(serverID) == null ? 0 : 1;
  582. dynamic ret = new
  583. {
  584. weight = w,
  585. memory = ProcessPrivateMemoryMB,
  586. zone_count = zc,
  587. player_count = pc,
  588. ActiveObjectCount = ActiveObjectCount,
  589. AllocObjectCount = AllocObjectCount,
  590. ActiveZoneCount = ActiveZoneCount,
  591. AllocZoneCount = AllocZoneCount,
  592. flag = flag,
  593. };
  594. cb(null, ret);
  595. }
  596. catch (Exception e)
  597. {
  598. log.Error("GetServerState failed, error: " + e.Message, e);
  599. cb(e, e.Message);
  600. }
  601. }
  602. public void RegisterGameServer(int serverId, int crossId, Action<Exception, int> cb)
  603. {
  604. try
  605. {
  606. var str = ConfigurationManager.AppSettings["game.server.id"];
  607. if (!string.IsNullOrEmpty(str) && int.Parse(str) != serverId && int.Parse(str) != crossId)
  608. {
  609. cb(null, -1);
  610. return;
  611. }
  612. }
  613. catch (Exception e)
  614. {
  615. cb(e, -2);
  616. return;
  617. }
  618. cb(null, s_bsVersion);
  619. }
  620. //获取单位血量
  621. public void GetUnitHP(string instanceId, int objectId, Action<Exception, int> cb)
  622. {
  623. try
  624. {
  625. var node = getZoneNode(instanceId);
  626. if (node == null)
  627. {
  628. cb(null, -1);
  629. return;
  630. }
  631. InstanceUnit unit = node.Node.Zone.getUnit((uint)objectId);
  632. if (unit == null)
  633. {
  634. cb(null, -2);
  635. return;
  636. }
  637. cb(null, unit.CurrentHP);
  638. }
  639. catch (Exception e)
  640. {
  641. cb(e, -10);
  642. return;
  643. }
  644. }
  645. //----------------------------------------------------------------------------------------------
  646. }
  647. }