OSDN Git Service

RAD-75: Inventory needs copy/cut/paste for moving items
[radegast/radegast.git] / Radegast / Core / RadegastInstance.cs
1 // \r
2 // Radegast Metaverse Client\r
3 // Copyright (c) 2009, Radegast Development Team\r
4 // All rights reserved.\r
5 // \r
6 // Redistribution and use in source and binary forms, with or without\r
7 // modification, are permitted provided that the following conditions are met:\r
8 // \r
9 //     * Redistributions of source code must retain the above copyright notice,\r
10 //       this list of conditions and the following disclaimer.\r
11 //     * Redistributions in binary form must reproduce the above copyright\r
12 //       notice, this list of conditions and the following disclaimer in the\r
13 //       documentation and/or other materials provided with the distribution.\r
14 //     * Neither the name of the application "Radegast", nor the names of its\r
15 //       contributors may be used to endorse or promote products derived from\r
16 //       this software without specific prior written permission.\r
17 // \r
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"\r
19 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\r
20 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\r
21 // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\r
22 // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\r
23 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\r
24 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\r
25 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\r
26 // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\r
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
28 //\r
29 // $Id$\r
30 //\r
31 using System;\r
32 using System.Collections.Generic;\r
33 using System.IO;\r
34 using System.Reflection;\r
35 using System.Threading;\r
36 using System.Windows.Forms;\r
37 using Radegast.Commands;\r
38 using Radegast.Netcom;\r
39 using Radegast.Media;\r
40 using OpenMetaverse;\r
41 \r
42 namespace Radegast\r
43 {\r
44     public class RadegastInstance\r
45     {\r
46         #region OnRadegastFormCreated\r
47         public event Action<RadegastForm> RadegastFormCreated;\r
48         /// <summary>\r
49         /// Triggers the RadegastFormCreated event.\r
50         /// </summary>\r
51         public virtual void OnRadegastFormCreated(RadegastForm radForm)\r
52         {\r
53             if (RadegastFormCreated != null) RadegastFormCreated(radForm);\r
54         }\r
55         #endregion        \r
56         private GridClient client;\r
57         private RadegastNetcom netcom;\r
58 \r
59         private StateManager state;\r
60 \r
61         private frmMain mainForm;\r
62 \r
63         // Singleton, there can be only one instance\r
64         private static RadegastInstance globalInstance = null;\r
65         public static RadegastInstance GlobalInstance\r
66         {\r
67             get\r
68             {\r
69                 if (globalInstance == null)\r
70                 {\r
71                     globalInstance = new RadegastInstance(new GridClient());\r
72                 }\r
73                 return globalInstance;\r
74             }\r
75         }\r
76 \r
77         private string userDir;\r
78         /// <summary>\r
79         /// System (not grid!) user's dir\r
80         /// </summary>\r
81         public string UserDir { get { return userDir; } }\r
82 \r
83         /// <summary>\r
84         /// Grid client's user dir for settings and logs\r
85         /// </summary>\r
86         public string ClientDir\r
87         {\r
88             get\r
89             {\r
90                 if (client != null && client.Self != null && !string.IsNullOrEmpty(client.Self.Name))\r
91                 {\r
92                     return Path.Combine(userDir, client.Self.Name);\r
93                 }\r
94                 else\r
95                 {\r
96                     return Environment.CurrentDirectory;\r
97                 }\r
98             }\r
99         }\r
100 \r
101         public string InventoryCacheFileName { get { return Path.Combine(ClientDir, "inventory.cache"); } }\r
102 \r
103         private string globalLogFile;\r
104         public string GlobalLogFile { get { return globalLogFile; } }\r
105 \r
106         private bool monoRuntime;\r
107         public bool MonoRuntime { get { return monoRuntime; } }\r
108 \r
109         private Dictionary<UUID, Group> groups = new Dictionary<UUID, Group>();\r
110         public Dictionary<UUID, Group> Groups { get { return groups; } }\r
111 \r
112         private Settings globalSettings;\r
113         /// <summary>\r
114         /// Global settings for the entire application\r
115         /// </summary>\r
116         public Settings GlobalSettings { get { return globalSettings; } }\r
117 \r
118         private Settings clientSettings;\r
119         /// <summary>\r
120         /// Per client settings\r
121         /// </summary>\r
122         public Settings ClientSettings { get { return clientSettings; } }\r
123 \r
124         public Dictionary<UUID, string> nameCache = new Dictionary<UUID, string>();\r
125 \r
126         public const string INCOMPLETE_NAME = "Loading...";\r
127 \r
128         public readonly bool advancedDebugging = false;\r
129 \r
130         public readonly List<IRadegastPlugin> PluginsLoaded = new List<IRadegastPlugin>();\r
131 \r
132         private MediaManager mediaManager;\r
133         /// <summary>\r
134         /// Radegast media manager for playing streams and in world sounds\r
135         /// </summary>\r
136         public MediaManager MediaManager { get { return mediaManager; } }\r
137 \r
138 \r
139         private CommandsManager commandsManager;\r
140         /// <summary>\r
141         /// Radegast command manager for executing textual console commands\r
142         /// </summary>\r
143         public CommandsManager CommandsManager { get { return commandsManager; } }\r
144 \r
145         /// <summary>\r
146         /// Radegast ContextAction manager for context sensitive actions\r
147         /// </summary>\r
148         public ContextActionsManager ContextActionManager { get; private set; }\r
149 \r
150         private RadegastMovement movement;\r
151         /// <summary>\r
152         /// Allows key emulation for moving avatar around\r
153         /// </summary>\r
154         public RadegastMovement Movement { get { return movement; } }\r
155 \r
156         /// <summary>\r
157         /// The last item that was cut or copied in the inventory, used for pasting\r
158         /// in a different place on the inventory, or other places like profile\r
159         /// that allow sending copied inventory items\r
160         /// </summary>\r
161         public InventoryClipboard InventoryClipboard;\r
162 \r
163         #region Events\r
164         /// <summary>The event subscribers, null of no subscribers</summary>\r
165         private EventHandler<ClientChangedEventArgs> m_ClientChanged;\r
166 \r
167         ///<summary>Raises the ClientChanged Event</summary>\r
168         /// <param name="e">A ClientChangedEventArgs object containing\r
169         /// the old and the new client</param>\r
170         protected virtual void OnClientChanged(ClientChangedEventArgs e)\r
171         {\r
172             EventHandler<ClientChangedEventArgs> handler = m_ClientChanged;\r
173             if (handler != null)\r
174                 handler(this, e);\r
175         }\r
176 \r
177         /// <summary>Thread sync lock object</summary>\r
178         private readonly object m_ClientChangedLock = new object();\r
179 \r
180         /// <summary>Raised when the GridClient object in the main Radegast instance is changed</summary>\r
181         public event EventHandler<ClientChangedEventArgs> ClientChanged\r
182         {\r
183             add { lock (m_ClientChangedLock) { m_ClientChanged += value; } }\r
184             remove { lock (m_ClientChangedLock) { m_ClientChanged -= value; } }\r
185         }\r
186         #endregion Events\r
187 \r
188         public RadegastInstance(GridClient client0)\r
189         {\r
190             // incase something else calls GlobalInstance while we are loading\r
191             globalInstance = this;\r
192 \r
193 #if !DEBUG\r
194             Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);\r
195             Application.ThreadException += HandleThreadException;\r
196 #endif\r
197 \r
198             client = client0;\r
199 \r
200             // Are we running mono?\r
201             monoRuntime = Type.GetType("Mono.Runtime") != null;\r
202 \r
203             netcom = new RadegastNetcom(this);\r
204             state = new StateManager(this);\r
205             mediaManager = new MediaManager(this);\r
206             commandsManager = new CommandsManager(this);\r
207             ContextActionManager = new ContextActionsManager(this);\r
208             movement = new RadegastMovement(this);\r
209 \r
210             InitializeLoggingAndConfig();\r
211             InitializeClient(client);\r
212 \r
213             mainForm = new frmMain(this);\r
214             mainForm.InitializeControls();\r
215 \r
216             mainForm.Load += new EventHandler(mainForm_Load);\r
217             ScanAndLoadPlugins();\r
218         }\r
219 \r
220         private void InitializeClient(GridClient client)\r
221         {\r
222             client.Settings.MULTIPLE_SIMS = true;\r
223 \r
224             client.Settings.USE_INTERPOLATION_TIMER = false;\r
225             client.Settings.ALWAYS_REQUEST_OBJECTS = true;\r
226             client.Settings.ALWAYS_DECODE_OBJECTS = true;\r
227             client.Settings.OBJECT_TRACKING = true;\r
228             client.Settings.ENABLE_SIMSTATS = true;\r
229             client.Settings.FETCH_MISSING_INVENTORY = true;\r
230             client.Settings.SEND_AGENT_THROTTLE = true;\r
231             client.Settings.SEND_AGENT_UPDATES = true;\r
232 \r
233             client.Settings.USE_ASSET_CACHE = true;\r
234             client.Settings.ASSET_CACHE_DIR = Path.Combine(userDir, "cache");\r
235             client.Assets.Cache.AutoPruneEnabled = false;\r
236 \r
237             client.Throttle.Total = 5000000f;\r
238             client.Settings.THROTTLE_OUTGOING_PACKETS = true;\r
239             client.Settings.LOGIN_TIMEOUT = 120 * 1000;\r
240             client.Settings.SIMULATOR_TIMEOUT = 120 * 1000;\r
241             client.Settings.MAX_CONCURRENT_TEXTURE_DOWNLOADS = 20;\r
242 \r
243             RegisterClientEvents(client);\r
244         }\r
245 \r
246         private void RegisterClientEvents(GridClient client)\r
247         {\r
248             client.Groups.CurrentGroups += new EventHandler<CurrentGroupsEventArgs>(Groups_CurrentGroups);\r
249             client.Groups.GroupLeaveReply += new EventHandler<GroupOperationEventArgs>(Groups_GroupsChanged);\r
250             client.Groups.GroupDropped += new EventHandler<GroupDroppedEventArgs>(Groups_GroupsChanged);\r
251             client.Groups.GroupJoinedReply += new EventHandler<GroupOperationEventArgs>(Groups_GroupsChanged);\r
252             client.Avatars.UUIDNameReply += new EventHandler<UUIDNameReplyEventArgs>(Avatars_UUIDNameReply);\r
253             netcom.ClientConnected += new EventHandler<EventArgs>(netcom_ClientConnected);\r
254        }\r
255 \r
256         private void UnregisterClientEvents(GridClient client)\r
257         {\r
258             client.Groups.CurrentGroups -= new EventHandler<CurrentGroupsEventArgs>(Groups_CurrentGroups);\r
259             client.Groups.GroupLeaveReply -= new EventHandler<GroupOperationEventArgs>(Groups_GroupsChanged);\r
260             client.Groups.GroupDropped -= new EventHandler<GroupDroppedEventArgs>(Groups_GroupsChanged);\r
261             client.Groups.GroupJoinedReply -= new EventHandler<GroupOperationEventArgs>(Groups_GroupsChanged);\r
262             client.Avatars.UUIDNameReply -= new EventHandler<UUIDNameReplyEventArgs>(Avatars_UUIDNameReply);\r
263             netcom.ClientConnected -= new EventHandler<EventArgs>(netcom_ClientConnected);\r
264         }\r
265 \r
266         public void Reconnect()\r
267         {\r
268             TabConsole.DisplayNotificationInChat("Attempting to reconnect...", ChatBufferTextStyle.StatusDarkBlue);\r
269             Logger.Log("Attemting to reconnect", Helpers.LogLevel.Info, client);\r
270             GridClient oldClient = client;\r
271             client = new GridClient();\r
272             UnregisterClientEvents(oldClient);\r
273             InitializeClient(client);\r
274             OnClientChanged(new ClientChangedEventArgs(oldClient, client));\r
275             netcom.Login();\r
276         }\r
277 \r
278         public void CleanUp()\r
279         {\r
280             if (client != null)\r
281             {\r
282                 UnregisterClientEvents(client);\r
283             }\r
284 \r
285             lock (PluginsLoaded)\r
286             {\r
287                 List<IRadegastPlugin> unload = new List<IRadegastPlugin>(PluginsLoaded);\r
288                 unload.ForEach(plug =>\r
289                {\r
290                    PluginsLoaded.Remove(plug);\r
291                    try\r
292                    {\r
293                        plug.StopPlugin(this);\r
294                    }\r
295                    catch (Exception ex)\r
296                    {\r
297                        Logger.Log("ERROR in Shutdown Plugin: " + plug + " because " + ex, Helpers.LogLevel.Debug, ex);\r
298                    }\r
299                });\r
300             }\r
301 \r
302             if (movement != null)\r
303             {\r
304                 movement.Dispose();\r
305                 movement = null;\r
306             }\r
307             if (commandsManager != null)\r
308             {\r
309                 commandsManager.Dispose();\r
310                 commandsManager = null;\r
311             }\r
312             if (ContextActionManager != null)\r
313             {\r
314                 ContextActionManager.Dispose();\r
315                 ContextActionManager = null;\r
316             }\r
317             if (mediaManager != null)\r
318             {\r
319                 mediaManager.Dispose();\r
320                 mediaManager = null;\r
321             }\r
322             if (state != null)\r
323             {\r
324                 state.Dispose();\r
325                 state = null;\r
326             }\r
327             if (netcom != null)\r
328             {\r
329                 netcom.Dispose();\r
330                 netcom = null;\r
331             }\r
332             if (mainForm != null)\r
333             {\r
334                 mainForm.Load -= new EventHandler(mainForm_Load);\r
335             }\r
336             Logger.Log("RadegastInstance finished cleaning up.", Helpers.LogLevel.Debug);\r
337         }\r
338 \r
339         void mainForm_Load(object sender, EventArgs e)\r
340         {\r
341             StartPlugins();\r
342         }\r
343 \r
344         private void StartPlugins()\r
345         {\r
346             lock (PluginsLoaded)\r
347             {\r
348                 foreach (IRadegastPlugin plug in PluginsLoaded)\r
349                 {\r
350                     try\r
351                     {\r
352                         plug.StartPlugin(this);\r
353                     }\r
354                     catch (Exception ex)\r
355                     {\r
356                         Logger.Log("ERROR in Starting Radegast Plugin: " + plug + " because " + ex, Helpers.LogLevel.Debug);\r
357                     }\r
358                 }\r
359             }\r
360         }\r
361 \r
362         private void ScanAndLoadPlugins()\r
363         {\r
364             string dirName = Application.StartupPath;\r
365 \r
366             if (!Directory.Exists(dirName)) return;\r
367 \r
368             foreach (string loadfilename in Directory.GetFiles(dirName))\r
369             {\r
370                 if (loadfilename.ToLower().EndsWith(".dll") || loadfilename.ToLower().EndsWith(".exe"))\r
371                 {\r
372                     try\r
373                     {\r
374                         Assembly assembly = Assembly.LoadFile(loadfilename);\r
375                         LoadAssembly(loadfilename, assembly);\r
376                     }\r
377                     catch (BadImageFormatException)\r
378                     {\r
379                         // non .NET .dlls\r
380                     }\r
381                     catch (ReflectionTypeLoadException)\r
382                     {\r
383                         // Out of date or dlls missing sub dependencies\r
384                     }\r
385                     catch (TypeLoadException)\r
386                     {\r
387                         // Another version of: Out of date or dlls missing sub dependencies\r
388                     }\r
389                     catch (Exception ex)\r
390                     {\r
391                         Logger.Log("ERROR in Radegast Plugin: " + loadfilename + " because " + ex, Helpers.LogLevel.Debug);\r
392                     }\r
393                 }\r
394             }\r
395         }\r
396 \r
397         public void LoadAssembly(string loadfilename, Assembly assembly)\r
398         {\r
399             foreach (Type type in assembly.GetTypes())\r
400             {\r
401                 if (typeof(IRadegastPlugin).IsAssignableFrom(type))\r
402                 {\r
403                     if  (type.IsInterface) continue;\r
404                     try\r
405                     {\r
406                         IRadegastPlugin plug;\r
407                         ConstructorInfo constructorInfo = type.GetConstructor(new Type[] {typeof (RadegastInstance)});\r
408                         if (constructorInfo != null)\r
409                             plug = (IRadegastPlugin) constructorInfo.Invoke(new[] {this});\r
410                         else\r
411                         {\r
412                             constructorInfo = type.GetConstructor(new Type[] {});\r
413                             if (constructorInfo != null)\r
414                                 plug = (IRadegastPlugin) constructorInfo.Invoke(new object[0]);\r
415                             else\r
416                             {\r
417                                 Logger.Log("ERROR Constructing Radegast Plugin: " + loadfilename + " because "+type+ " has no usable constructor.",Helpers.LogLevel.Debug);\r
418                                 continue;\r
419                             }\r
420                         }\r
421                         lock (PluginsLoaded) PluginsLoaded.Add(plug);\r
422                     }\r
423                     catch (Exception ex)\r
424                     {\r
425                         Logger.Log("ERROR Constructing Radegast Plugin: " + loadfilename + " because " + ex,\r
426                                    Helpers.LogLevel.Debug);\r
427                     }\r
428                 }\r
429                 else\r
430                 {\r
431                     try\r
432                     {\r
433                         commandsManager.LoadType(type);\r
434                     }\r
435                     catch (Exception ex)\r
436                     {\r
437                         Logger.Log("ERROR in Radegast Plugin: " + loadfilename + " Command: " + type +\r
438                                    " because " + ex.Message + " " + ex.StackTrace, Helpers.LogLevel.Debug);\r
439                     }\r
440                 }\r
441             }\r
442         }\r
443 \r
444         void netcom_ClientConnected(object sender, EventArgs e)\r
445         {\r
446             try\r
447             {\r
448                 if (!Directory.Exists(ClientDir))\r
449                     Directory.CreateDirectory(ClientDir);\r
450             }\r
451             catch (Exception ex)\r
452             {\r
453                 Logger.Log("Failed to create client directory", Helpers.LogLevel.Warning, ex);\r
454             }\r
455 \r
456             clientSettings = new Settings(Path.Combine(ClientDir, "client_settings.xml"));\r
457         }\r
458 \r
459 \r
460         void Avatars_UUIDNameReply(object sender, UUIDNameReplyEventArgs e)\r
461         {\r
462             lock (nameCache)\r
463             {\r
464                 foreach (KeyValuePair<UUID, string> av in e.Names)\r
465                 {\r
466                     if (!nameCache.ContainsKey(av.Key))\r
467                     {\r
468                         nameCache.Add(av.Key, av.Value);\r
469                     }\r
470                 }\r
471             }\r
472         }\r
473 \r
474         public string getAvatarName(UUID key)\r
475         {\r
476             lock (nameCache)\r
477             {\r
478                 if (key == UUID.Zero)\r
479                 {\r
480                     return "(???) (???)";\r
481                 }\r
482                 if (nameCache.ContainsKey(key))\r
483                 {\r
484                     return nameCache[key];\r
485                 }\r
486                 else\r
487                 {\r
488                     client.Avatars.RequestAvatarName(key);\r
489                     return INCOMPLETE_NAME;\r
490                 }\r
491             }\r
492         }\r
493 \r
494         public void getAvatarNames(List<UUID> keys)\r
495         {\r
496             lock (nameCache)\r
497             {\r
498                 List<UUID> newNames = new List<UUID>();\r
499                 foreach (UUID key in keys)\r
500                 {\r
501                     if (!nameCache.ContainsKey(key))\r
502                     {\r
503                         newNames.Add(key);\r
504                     }\r
505                 }\r
506                 if (newNames.Count > 0)\r
507                 {\r
508                     client.Avatars.RequestAvatarNames(newNames);\r
509                 }\r
510             }\r
511         }\r
512 \r
513         public bool haveAvatarName(UUID key)\r
514         {\r
515             lock (nameCache)\r
516             {\r
517                 if (nameCache.ContainsKey(key))\r
518                     return true;\r
519                 else\r
520                     return false;\r
521             }\r
522         }\r
523 \r
524         void Groups_GroupsChanged(object sender, EventArgs e)\r
525         {\r
526             client.Groups.RequestCurrentGroups();\r
527         }\r
528 \r
529         public static string SafeFileName(string fileName)\r
530         {\r
531             foreach (char lDisallowed in Path.GetInvalidFileNameChars())\r
532             {\r
533                 fileName = fileName.Replace(lDisallowed.ToString(), "_");\r
534             }\r
535 \r
536             return fileName;\r
537         }\r
538 \r
539         public void LogClientMessage(string fileName, string message)\r
540         {\r
541             lock (this)\r
542             {\r
543                 try\r
544                 {\r
545                     foreach (char lDisallowed in System.IO.Path.GetInvalidFileNameChars())\r
546                     {\r
547                         fileName = fileName.Replace(lDisallowed.ToString(), "_");\r
548                     }\r
549 \r
550                     File.AppendAllText(Path.Combine(ClientDir, fileName),\r
551                         DateTime.Now.ToString("yyyy-MM-dd [HH:mm:ss] ") + message + Environment.NewLine);\r
552                 }\r
553                 catch (Exception) { }\r
554             }\r
555         }\r
556 \r
557         void Groups_CurrentGroups(object sender, CurrentGroupsEventArgs e)\r
558         {\r
559             this.groups = e.Groups;\r
560         }\r
561 \r
562         private void InitializeLoggingAndConfig()\r
563         {\r
564             try\r
565             {\r
566                 userDir = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), Properties.Resources.ProgramName);\r
567                 if (!Directory.Exists(userDir))\r
568                 {\r
569                     Directory.CreateDirectory(userDir);\r
570                 }\r
571             }\r
572             catch (Exception)\r
573             {\r
574                 userDir = System.Environment.CurrentDirectory;\r
575             };\r
576 \r
577             globalLogFile = Path.Combine(userDir, Properties.Resources.ProgramName + ".log");\r
578             globalSettings = new Settings(Path.Combine(userDir, "settings.xml"));\r
579         }\r
580 \r
581         public GridClient Client\r
582         {\r
583             get { return client; }\r
584         }\r
585 \r
586         public RadegastNetcom Netcom\r
587         {\r
588             get { return netcom; }\r
589         }\r
590 \r
591         public StateManager State\r
592         {\r
593             get { return state; }\r
594         }\r
595 \r
596         public frmMain MainForm\r
597         {\r
598             get { return mainForm; }\r
599         }\r
600 \r
601         public TabsConsole TabConsole\r
602         {\r
603             get { return mainForm.TabConsole; }\r
604         }\r
605 \r
606         public void HandleThreadException(object sender, ThreadExceptionEventArgs e)\r
607         {\r
608             Logger.Log("Unhandled Thread Exception: " \r
609                 + e.Exception.Message + Environment.NewLine\r
610                 + e.Exception.StackTrace + Environment.NewLine,\r
611                 Helpers.LogLevel.Error,\r
612                 client);\r
613 #if !DEBUG\r
614             Application.Exit();\r
615 #endif\r
616         }\r
617     }\r
618 \r
619     #region Event classes\r
620     public class ClientChangedEventArgs : EventArgs\r
621     {\r
622         private GridClient m_OldClient;\r
623         private GridClient m_Client;\r
624 \r
625         public GridClient OldClient { get { return m_OldClient; } }\r
626         public GridClient Client { get { return m_Client; } }\r
627 \r
628         public ClientChangedEventArgs(GridClient OldClient, GridClient Client)\r
629         {\r
630             m_OldClient = OldClient;\r
631             m_Client = Client;\r
632         }\r
633     }\r
634     #endregion Event classes\r
635 }\r