MapForm.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. using Server.MirDatabase;
  2. using Server.MirEnvir;
  3. namespace Server.MirForms
  4. {
  5. public static class ConvertMapInfo
  6. {
  7. public static Envir EditEnvir = null;
  8. public static string Path = string.Empty;
  9. private static List<String> errors = new List<String>();
  10. public static void Start(Envir envirToUpdate)
  11. {
  12. if (Path == string.Empty) return;
  13. EditEnvir = envirToUpdate;
  14. if (EditEnvir == null) return;
  15. var lines = File.ReadAllLines(Path);
  16. for (int i = 0; i < lines.Length; i++)
  17. {
  18. if (lines[i].StartsWith("[")) // Read map info
  19. {
  20. lines[i] = System.Text.RegularExpressions.Regex.Replace(lines[i], @"\s+", " "); // Clear white-space
  21. lines[i] = lines[i].Replace(" ;", ";"); // Remove space before semi-colon
  22. // Trim comment at the end of the line
  23. if (lines[i].Contains(';'))
  24. lines[i] = lines[i].Substring(0, lines[i].IndexOf(";", System.StringComparison.Ordinal));
  25. bool isNew = false;
  26. var a = lines[i].Split(']'); // Split map info into [0] = MapFile MapName 0 || [1] = Attributes
  27. string[] b = a[0].Split(' ');
  28. string fileName = b[0].TrimStart('[');// Assign MapFile from variable and trim leading '[' char
  29. MapInfo newMapInfo = null;
  30. if ((newMapInfo = EditEnvir.MapInfoList.FirstOrDefault(it => it.FileName == fileName)) == null)
  31. {
  32. newMapInfo = new MirDatabase.MapInfo { Index = ++EditEnvir.MapIndex, FileName = fileName };
  33. isNew=true;
  34. }
  35. newMapInfo.Title = b[1].Replace("*", " "); // Assign MapName from variable, replacing asterisk with space
  36. List<string> mapAttributes = new List<string>(); // Group of all attributes associated with that map
  37. mapAttributes.AddRange(a[1].Split(' '));
  38. newMapInfo.NoTeleport = mapAttributes.Any(s => s.Contains("NOTELEPORT".ToUpper()));
  39. newMapInfo.NoRandom = mapAttributes.Any(s => s.Contains("NORANDOMMOVE".ToUpper()));
  40. newMapInfo.NoEscape = mapAttributes.Any(s => s.Contains("NOESCAPE".ToUpper()));
  41. newMapInfo.NoRecall = mapAttributes.Any(s => s.Contains("NORECALL".ToUpper()));
  42. newMapInfo.NoDrug = mapAttributes.Any(s => s.Contains("NODRUG".ToUpper()));
  43. newMapInfo.NoPosition = mapAttributes.Any(s => s.Contains("NOPOSITIONMOVE".ToUpper()));
  44. newMapInfo.NoThrowItem = mapAttributes.Any(s => s.Contains("NOTHROWITEM".ToUpper()));
  45. newMapInfo.NoDropPlayer = mapAttributes.Any(s => s.Contains("NOPLAYERDROP".ToUpper()));
  46. newMapInfo.NoDropMonster = mapAttributes.Any(s => s.Contains("NOMONSTERDROP".ToUpper()));
  47. newMapInfo.NoNames = mapAttributes.Any(s => s.Contains("NONAMES".ToUpper()));
  48. newMapInfo.NoFight = mapAttributes.Any(s => s.Contains("NOFIGHT".ToUpper()));
  49. newMapInfo.NoMount = mapAttributes.Any(s => s.Contains("NOMOUNT".ToUpper()));
  50. newMapInfo.NeedBridle = mapAttributes.Any(s => s.Contains("NEEDBRIDLE".ToUpper()));
  51. newMapInfo.Fight = mapAttributes.Any(s => s.Contains("FIGHT".ToUpper()));
  52. newMapInfo.NoTownTeleport = mapAttributes.Any(s => s.Contains("NOTOWNTELEPORT".ToUpper()));
  53. newMapInfo.NoExperience = mapAttributes.Any(s => s.Contains("NOEXPERIENCE".ToUpper()));
  54. newMapInfo.NoGroup = mapAttributes.Any(s => s.Contains("NOGROUP".ToUpper()));
  55. newMapInfo.NoPets = mapAttributes.Any(s => s.Contains("NOPETS".ToUpper()));
  56. newMapInfo.NoHero = mapAttributes.Any(s => s.Contains("NOHEROES".ToUpper()));
  57. newMapInfo.NoIntelligentCreatures =
  58. mapAttributes.Any(s => s.Contains("NOINTELLIGENTCREATURES".ToUpper())) ||
  59. mapAttributes.Any(s => s.Contains("NOINTELLIGENTCREATURE".ToUpper()));
  60. // --- RequiredGroup (supports legacy flag and REQUIREDGROUP(n), plus typo REQUIREGROUP) ---
  61. newMapInfo.RequiredGroupSize = 0;
  62. // numeric form: REQUIREDGROUP(n) / REQUIREGROUP(n)
  63. bool hasReqSize =
  64. mapAttributes.Any(x => x.StartsWith("REQUIREDGROUP(".ToUpper())) ||
  65. mapAttributes.Any(x => x.StartsWith("REQUIREGROUP(".ToUpper()));
  66. newMapInfo.FireWallLimit = mapAttributes.Any(x => x.StartsWith("FIREWALL(", StringComparison.OrdinalIgnoreCase));
  67. if (newMapInfo.FireWallLimit)
  68. {
  69. int index = mapAttributes.FindIndex(x => x.StartsWith("FIREWALL(", StringComparison.OrdinalIgnoreCase));
  70. var token = mapAttributes[index].Trim();
  71. var inner = token.Substring("FIREWALL(".Length).TrimEnd(')');
  72. if (int.TryParse(inner, out var parsed) && parsed > 0)
  73. newMapInfo.FireWallCount = parsed;
  74. else
  75. newMapInfo.FireWallCount = 1; // sensible fallback
  76. }
  77. if (hasReqSize)
  78. {
  79. int idx = mapAttributes.FindIndex(x => x.StartsWith("REQUIREDGROUP(".ToUpper()));
  80. if (idx < 0) idx = mapAttributes.FindIndex(x => x.StartsWith("REQUIREGROUP(".ToUpper()));
  81. string token = mapAttributes[idx];
  82. int l = token.IndexOf('(');
  83. int r = token.LastIndexOf(')');
  84. if (l >= 0 && r > l)
  85. newMapInfo.RequiredGroupSize = Convert.ToInt32(token.Substring(l + 1, r - l - 1));
  86. }
  87. else if (mapAttributes.Any(s => s.Contains("REQUIREDGROUP".ToUpper())) ||
  88. mapAttributes.Any(s => s.Contains("REQUIREGROUP".ToUpper())))
  89. {
  90. // legacy bare flag -> default to 2
  91. newMapInfo.RequiredGroupSize = 2;
  92. }
  93. newMapInfo.Fire = mapAttributes.Any(x => x.StartsWith("FIRE(".ToUpper()));
  94. if (newMapInfo.Fire)
  95. {
  96. int index = mapAttributes.FindIndex(x => x.StartsWith("FIRE(".ToUpper()));
  97. newMapInfo.FireDamage = Convert.ToInt16(mapAttributes[index].TrimStart("FIRE(".ToCharArray()).TrimEnd(')'));
  98. }
  99. newMapInfo.Lightning = mapAttributes.Any(x => x.StartsWith("LIGHTNING(".ToUpper()));
  100. if (newMapInfo.Lightning)
  101. {
  102. int index = mapAttributes.FindIndex(x => x.StartsWith("LIGHTNING(".ToUpper()));
  103. newMapInfo.LightningDamage = Convert.ToInt16(mapAttributes[index].TrimStart("LIGHTNING(".ToCharArray()).TrimEnd(')'));
  104. }
  105. newMapInfo.NoReconnect = mapAttributes.Any(x => x.StartsWith("NORECONNECT(".ToUpper()));
  106. if (newMapInfo.NoReconnect)
  107. {
  108. int index = mapAttributes.FindIndex(x => x.StartsWith("NORECONNECT(".ToUpper()));
  109. newMapInfo.NoReconnectMap = mapAttributes[index].TrimStart("NORECONNECT(".ToCharArray()).TrimEnd(')');
  110. }
  111. if (mapAttributes.Any(x => x.StartsWith("MINIMAP(".ToUpper())))
  112. {
  113. int index = mapAttributes.FindIndex(x => x.StartsWith("MINIMAP(".ToUpper()));
  114. newMapInfo.MiniMap = Convert.ToUInt16(mapAttributes[index].TrimStart("MINIMAP(".ToCharArray()).TrimEnd(')'));
  115. }
  116. if (mapAttributes.Any(x => x.StartsWith("BIGMAP(".ToUpper())))
  117. {
  118. int index = mapAttributes.FindIndex(x => x.StartsWith("BIGMAP(".ToUpper()));
  119. newMapInfo.BigMap = Convert.ToUInt16(mapAttributes[index].TrimStart("BIGMAP(".ToCharArray()).TrimEnd(')'));
  120. }
  121. if (mapAttributes.Any(s => s.Contains("MINE(".ToUpper())))
  122. {
  123. int index = mapAttributes.FindIndex(x => x.StartsWith("MINE(".ToUpper()));
  124. newMapInfo.MineIndex = Convert.ToByte(mapAttributes[index].TrimStart("MINE(".ToCharArray()).TrimEnd(')'));
  125. }
  126. if (mapAttributes.Any(s => s.Contains("MAPLIGHT(".ToUpper())))
  127. {
  128. int index = mapAttributes.FindIndex(x => x.StartsWith("MAPLIGHT(".ToUpper()));
  129. newMapInfo.MapDarkLight = Convert.ToByte(mapAttributes[index].TrimStart("MAPLIGHT(".ToCharArray()).TrimEnd(')'));
  130. }
  131. if (mapAttributes.Any(s => s.Contains("MUSIC(".ToUpper())))
  132. {
  133. int index = mapAttributes.FindIndex(x => x.StartsWith("MUSIC(".ToUpper()));
  134. newMapInfo.Music = Convert.ToUInt16(mapAttributes[index].TrimStart("MUSIC(".ToCharArray()).TrimEnd(')'));
  135. }
  136. if (mapAttributes.Any(x => x.StartsWith("LIGHT(".ToUpper()))) // Check if there is a LIGHT attribute and get its value
  137. {
  138. int index = mapAttributes.FindIndex(x => x.StartsWith("LIGHT(".ToUpper()));
  139. switch (mapAttributes[index].TrimStart("LIGHT(".ToCharArray()).TrimEnd(')'))
  140. {
  141. case "Dawn":
  142. newMapInfo.Light = LightSetting.Dawn;
  143. break;
  144. case "Day":
  145. newMapInfo.Light = LightSetting.Day;
  146. break;
  147. case "Evening":
  148. newMapInfo.Light = LightSetting.Evening;
  149. break;
  150. case "Night":
  151. newMapInfo.Light = LightSetting.Night;
  152. break;
  153. case "Normal":
  154. newMapInfo.Light = LightSetting.Normal;
  155. break;
  156. default:
  157. newMapInfo.Light = LightSetting.Normal;
  158. break;
  159. }
  160. }
  161. else newMapInfo.Light = LightSetting.Normal;
  162. // Check for light type
  163. if (mapAttributes.Any(s => s.Contains("DAY".ToUpper()))) // DAY = Day
  164. newMapInfo.Light = LightSetting.Day;
  165. else if (mapAttributes.Any(s => s.Contains("DARK".ToUpper()))) // DARK = Night
  166. newMapInfo.Light = LightSetting.Night;
  167. if(isNew) EditEnvir.MapInfoList.Add(newMapInfo); // Add map to list
  168. }
  169. else if (lines[i].StartsWith(";")) continue;
  170. else
  171. continue;
  172. }
  173. for (int j = 0; j < EditEnvir.MapInfoList.Count; j++)
  174. {
  175. EditEnvir.MapInfoList[j].Movements.Clear();
  176. for (int k = 0; k < lines.Length; k++)
  177. {
  178. try
  179. {
  180. if (lines[k].StartsWith(EditEnvir.MapInfoList[j].FileName + " "))
  181. {
  182. MirDatabase.MovementInfo newMovement = new MirDatabase.MovementInfo();
  183. if (lines[k].Contains("NEEDHOLE"))
  184. {
  185. newMovement.NeedHole = true;
  186. lines[k] = lines[k].Replace("NEEDHOLE", "");
  187. }
  188. if (lines[k].Contains("NEEDMOVE"))
  189. {
  190. newMovement.NeedMove = true;
  191. lines[k] = lines[k].Replace("NEEDMOVE", "");
  192. }
  193. if (lines[k].Contains("NEEDCONQUEST"))
  194. {
  195. int conqLocation = lines[k].IndexOf(" NEEDCONQUEST");
  196. string conq = lines[k].Substring(conqLocation);
  197. int conqIndex = int.Parse(conq.Replace("NEEDCONQUEST(", "").Replace(")", "")); //get value
  198. newMovement.ConquestIndex = conqIndex;
  199. lines[k] = lines[k].Remove(conqLocation);
  200. }
  201. if (lines[k].Contains("SHOWONBIGMAP"))
  202. {
  203. newMovement.ShowOnBigMap = true;
  204. lines[k] = lines[k].Replace("SHOWONBIGMAP", "");
  205. }
  206. if (lines[k].Contains("BIGMAPICON"))
  207. {
  208. int iconLocation = lines[k].IndexOf(" BIGMAPICON");
  209. string icon = lines[k].Substring(iconLocation);
  210. int iconIndex = int.Parse(icon.Replace("BIGMAPICON(", "").Replace(")", "")); //get value
  211. newMovement.Icon = iconIndex;
  212. lines[k] = lines[k].Remove(iconLocation);
  213. }
  214. lines[k] = lines[k].Replace('.', ','); // Replace point with comma
  215. lines[k] = lines[k].Replace(":", ","); // Replace colon with comma
  216. lines[k] = lines[k].Replace(", ", ","); // Remove space after comma
  217. lines[k] = lines[k].Replace(" ,", ","); // Remove space before comma
  218. lines[k] = System.Text.RegularExpressions.Regex.Replace(lines[k], @"\s+", " "); // Clear whitespace
  219. lines[k] = lines[k].Replace(" ;", ";"); // Remove space before semi-colon
  220. // Trim comment at the end of the line
  221. if (lines[k].Contains(';'))
  222. lines[k] = lines[k].Substring(0, lines[k].IndexOf(";", System.StringComparison.Ordinal));
  223. var c = lines[k].Split(' ');
  224. // START - Get values from line
  225. if (c.Length == 7) // Every value has a space
  226. {
  227. c[1] = c[1] + "," + c[2];
  228. c[2] = c[5] + "," + c[6];
  229. c[3] = c[4];
  230. }
  231. else if (c.Length == 6) // One value has a space
  232. {
  233. if (c[2] == "->") // Space in to XY
  234. {
  235. c[2] = c[4] + "," + c[5];
  236. }
  237. else if (c[3] == "->") // Space in from XY
  238. {
  239. c[1] = c[1] + "," + c[2];
  240. c[2] = c[5];
  241. c[3] = c[4];
  242. }
  243. }
  244. else if (c.Length == 5) // Proper format
  245. {
  246. c[2] = c[4];
  247. }
  248. else // Unreadable value count
  249. {
  250. continue;
  251. }
  252. // END - Get values from line
  253. string[] d = c[1].Split(',');
  254. string[] e = c[2].Split(',');
  255. var toMapIndex = EditEnvir.MapInfoList.FindIndex(a => a.FileName == c[3]); //check existing maps for the connection info
  256. var toMap = -1;
  257. if (toMapIndex >= 0)
  258. {
  259. toMap = EditEnvir.MapInfoList[toMapIndex].Index; //get real index
  260. }
  261. if (toMap < 0)
  262. {
  263. toMapIndex = EditEnvir.MapInfoList.FindIndex(a => a.FileName.ToString() == c[3]);
  264. if (toMapIndex >= 0)
  265. {
  266. toMap = EditEnvir.MapInfoList[toMapIndex].Index;
  267. }
  268. }
  269. if (toMap < 0)
  270. {
  271. errors.Add("Destination Map Failed on line: " + lines[k] + "");
  272. continue;
  273. }
  274. newMovement.MapIndex = toMap;
  275. newMovement.Source = new Point(int.Parse(d[0]), int.Parse(d[1]));
  276. newMovement.Destination = new Point(int.Parse(e[0]), int.Parse(e[1]));
  277. //NeedHole
  278. //NeedMove
  279. //ConquestIndex
  280. EditEnvir.MapInfoList[j].Movements.Add(newMovement);
  281. }
  282. }
  283. catch (Exception)
  284. {
  285. continue;
  286. }
  287. }
  288. }
  289. for (int j = 0; j < EditEnvir.MapInfoList.Count; j++)
  290. {
  291. EditEnvir.MapInfoList[j].MineZones.Clear();
  292. for (int k = 0; k < lines.Length; k++)
  293. {
  294. if (!lines[k].StartsWith("MINEZONE")) continue;
  295. var line = lines[k].Split(' ');
  296. try
  297. {
  298. if (line[1] == EditEnvir.MapInfoList[j].FileName)
  299. {
  300. MineZone newMineInfo = new MineZone
  301. {
  302. Mine = Convert.ToByte(line[3]),
  303. Location = new Point(Convert.ToInt16(line[4]), Convert.ToInt16(line[5])),
  304. Size = Convert.ToUInt16(line[6])
  305. };
  306. EditEnvir.MapInfoList[j].MineZones.Add(newMineInfo);
  307. }
  308. }
  309. catch (Exception) { continue; }
  310. }
  311. }
  312. for (int j = 0; j < EditEnvir.MapInfoList.Count; j++)
  313. {
  314. EditEnvir.MapInfoList[j].SafeZones.Clear();
  315. for (int k = 0; k < lines.Length; k++)
  316. {
  317. //STARTZONE(0,150,150,50) || SAFEZONE(0,150,150,50)
  318. if (!lines[k].StartsWith("SAFEZONE") && !lines[k].StartsWith("STARTZONE")) continue;
  319. var line = lines[k].Replace(")", "").Split(','); // STARTZONE(0,150,150,50) -> STARTZONE(0 || 150 || 150 || 50
  320. var head = line[0].Split('('); // STARTZONE(0 -> STARTZONE || 0
  321. try
  322. {
  323. if (head[1] == EditEnvir.MapInfoList[j].FileName)
  324. {
  325. MirDatabase.SafeZoneInfo newSafeZone = new MirDatabase.SafeZoneInfo
  326. {
  327. Info = EditEnvir.MapInfoList[j],
  328. StartPoint = head[0].Equals("STARTZONE"),
  329. Location = new Point(Convert.ToInt16(line[2]), Convert.ToInt16(line[3])),
  330. Size = Convert.ToUInt16(line[1])
  331. };
  332. if (!EditEnvir.MapInfoList[j].SafeZones.Exists(sz => sz.StartPoint == newSafeZone.StartPoint && sz.Location.X == newSafeZone.Location.X && sz.Location.Y == newSafeZone.Location.Y))
  333. EditEnvir.MapInfoList[j].SafeZones.Add(newSafeZone);
  334. }
  335. }
  336. catch (Exception) { continue; }
  337. }
  338. }
  339. }
  340. public static void End()
  341. {
  342. SMain.Enqueue(String.Join("MapInfo Import Report:", errors.Count > 0 ? "" : "No Errors"));
  343. foreach (String error in errors)
  344. SMain.Enqueue(error);
  345. }
  346. }
  347. public static class ConvertNPCInfo
  348. {
  349. public static List<NPCInfo> NPCInfoList = new List<NPCInfo>();
  350. public static void Start()
  351. {
  352. string Path = string.Empty;
  353. OpenFileDialog ofd = new OpenFileDialog();
  354. ofd.Filter = "Text File|*.txt";
  355. ofd.ShowDialog();
  356. if (ofd.FileName == string.Empty) return;
  357. Path = ofd.FileName;
  358. var NPCList = File.ReadAllLines(Path);
  359. for (int i = 0; i < NPCList.Length; i++)
  360. {
  361. if (NPCList[i].Contains(';'))
  362. NPCList[i] = NPCList[i].Substring(0, NPCList[i].IndexOf(";", System.StringComparison.Ordinal));
  363. var Line = System.Text.RegularExpressions.Regex.Replace(NPCList[i], @"\s+", " ").Split(' ');
  364. if (Line.Length < 6) continue;
  365. try
  366. {
  367. NPCInfo NPC = new NPCInfo
  368. {
  369. FileName = Line[0],
  370. Map = Line[1],
  371. X = Convert.ToInt16(Line[2]),
  372. Y = Convert.ToInt16(Line[3]),
  373. Title = Line[4],
  374. Image = (Line.Length >= 8) ? Convert.ToInt16(Line[6]) : Convert.ToInt16(Line[5])
  375. };
  376. NPCInfoList.Add(NPC);
  377. }
  378. catch (Exception)
  379. {
  380. continue;
  381. }
  382. }
  383. }
  384. public static void Stop()
  385. {
  386. NPCInfoList.Clear();
  387. }
  388. }
  389. public class NPCInfo
  390. {
  391. public string
  392. FileName = string.Empty,
  393. Map = "0",
  394. Title = string.Empty;
  395. public int
  396. Index = 0,
  397. X = 0,
  398. Y = 0,
  399. Image = 0,
  400. Rate = 100;
  401. }
  402. public static class ConvertMonGenInfo
  403. {
  404. public static List<MonGenInfo> monGenList = new List<MonGenInfo>();
  405. public static void Start()
  406. {
  407. OpenFileDialog ofd = new OpenFileDialog();
  408. ofd.Filter = "Text File|*.txt|Gen File|*.gen|All|*.*";
  409. ofd.Multiselect = true;
  410. ofd.ShowDialog();
  411. if (ofd.FileNames.Length == 0) return;
  412. for (int i = 0; i < ofd.FileNames.Length; i++)
  413. {
  414. var MonGen = File.ReadAllLines(ofd.FileNames[i]);
  415. for (int j = 0; j < MonGen.Length; j++)
  416. {
  417. if (MonGen[j].Contains(';'))
  418. MonGen[j] = MonGen[j].Substring(0, MonGen[j].IndexOf(";", System.StringComparison.Ordinal));
  419. var Line = System.Text.RegularExpressions.Regex.Replace(MonGen[j], @"\s+", " ").Split(',');
  420. if (Line.Length < 7) continue;
  421. try
  422. {
  423. MonGenInfo MonGenItem = new MonGenInfo
  424. {
  425. Map = Line[0],
  426. X = Convert.ToInt16(Line[1]),
  427. Y = Convert.ToInt16(Line[2]),
  428. Name = Line[3],
  429. Range = Convert.ToInt16(Line[4]),
  430. Count = Convert.ToInt16(Line[5]),
  431. Delay = Convert.ToInt16(Line[6]),
  432. Direction = (Line.Length >= 8) ? Convert.ToInt16(Line[7]) : 0,
  433. RoutePath = (Line.Length >= 9) ? Line[8] : string.Empty
  434. };
  435. monGenList.Add(MonGenItem);
  436. }
  437. catch (Exception) { continue; }
  438. }
  439. }
  440. }
  441. public static void Stop()
  442. {
  443. monGenList.Clear();
  444. }
  445. }
  446. public class MonGenInfo
  447. {
  448. public string
  449. Map = string.Empty,
  450. Name = string.Empty,
  451. RoutePath = string.Empty;
  452. public int
  453. X = 0,
  454. Y = 0,
  455. Range = 0,
  456. Count = 0,
  457. Delay = 0,
  458. Direction = 0;
  459. }
  460. }