diff options
author | Dr Scofield | 2008-05-23 16:07:47 +0000 |
---|---|---|
committer | Dr Scofield | 2008-05-23 16:07:47 +0000 |
commit | 09188d5d10e279583c0e6074d38a1ac99abb7e43 (patch) | |
tree | f2bfcd205cac6657d219d9b20086d362915bf51d /OpenSim | |
parent | *Refactor of the LandManagementModule that allows OpenSim to run without it (diff) | |
download | opensim-SC-09188d5d10e279583c0e6074d38a1ac99abb7e43.zip opensim-SC-09188d5d10e279583c0e6074d38a1ac99abb7e43.tar.gz opensim-SC-09188d5d10e279583c0e6074d38a1ac99abb7e43.tar.bz2 opensim-SC-09188d5d10e279583c0e6074d38a1ac99abb7e43.tar.xz |
IRCBridgeModule (and ChatModule before the refactoring) didn't succeed in
finding out which region a new avatar was logging in to; the same problem
occurred when the client/avatar logged out. the reason was mani-fold:
- Scene.AddNewClient(...) would call SubscribeToClientEvents(client)
which would subscribe to all client events and then call
TriggerOnNewClient(...) BEFORE the ScenePresence object had even been
created and added. i've moved the TriggerOnNewClient() call to the
end of Scene.AddNewClient()
- Scene.AddNewClient(...) is called with child == true; a later call
to ScenePresence.MakeRootAgent() will turn child to false. When
OnNewClient is triggered, child is still true, causing IRCBridgeModule's
FindClientRegion to ignore the ScenePresence of the new avatar.
i've changed IRCBridgeModule to still use OnNewClient and also OnLogout
and OnConnectionClosed but only to signal that the avatar has logged on
(logged off respectively). to track whether an avatar has actually entered
a region i've added EventManager.OnMakeRootAgent (complementing
OnMakeChildAgent).
also, i've cleaned up the internal IRCModule code a bit. currently it
still uses IClientAPI.SendChatMessage() which replicates the code in
ChatModule, that needs to be changed to use TriggerOnChatFromWorld().
Diffstat (limited to 'OpenSim')
6 files changed, 884 insertions, 8 deletions
diff --git a/OpenSim/Region/ClientStack/LindenUDP/LLClientView.cs b/OpenSim/Region/ClientStack/LindenUDP/LLClientView.cs index 93ce29e..c1c4af2 100644 --- a/OpenSim/Region/ClientStack/LindenUDP/LLClientView.cs +++ b/OpenSim/Region/ClientStack/LindenUDP/LLClientView.cs | |||
@@ -930,12 +930,14 @@ namespace OpenSim.Region.ClientStack.LindenUDP | |||
930 | /// <param name="fromPos"></param> | 930 | /// <param name="fromPos"></param> |
931 | /// <param name="fromName"></param> | 931 | /// <param name="fromName"></param> |
932 | /// <param name="fromAgentID"></param> | 932 | /// <param name="fromAgentID"></param> |
933 | public void SendChatMessage(string message, byte type, LLVector3 fromPos, string fromName, LLUUID fromAgentID, byte source, byte audible) | 933 | public void SendChatMessage(string message, byte type, LLVector3 fromPos, string fromName, |
934 | LLUUID fromAgentID, byte source, byte audible) | ||
934 | { | 935 | { |
935 | SendChatMessage(Helpers.StringToField(message), type, fromPos, fromName, fromAgentID, source, audible); | 936 | SendChatMessage(Helpers.StringToField(message), type, fromPos, fromName, fromAgentID, source, audible); |
936 | } | 937 | } |
937 | 938 | ||
938 | public void SendChatMessage(byte[] message, byte type, LLVector3 fromPos, string fromName, LLUUID fromAgentID, byte source, byte audible) | 939 | public void SendChatMessage(byte[] message, byte type, LLVector3 fromPos, string fromName, |
940 | LLUUID fromAgentID, byte source, byte audible) | ||
939 | { | 941 | { |
940 | ChatFromSimulatorPacket reply = (ChatFromSimulatorPacket)PacketPool.Instance.GetPacket(PacketType.ChatFromSimulator); | 942 | ChatFromSimulatorPacket reply = (ChatFromSimulatorPacket)PacketPool.Instance.GetPacket(PacketType.ChatFromSimulator); |
941 | reply.ChatData.Audible = audible; | 943 | reply.ChatData.Audible = audible; |
diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/ChatModule.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/ChatModule.cs index 37cf328..8216222 100644 --- a/OpenSim/Region/Environment/Modules/Avatar/Chat/ChatModule.cs +++ b/OpenSim/Region/Environment/Modules/Avatar/Chat/ChatModule.cs | |||
@@ -65,7 +65,6 @@ namespace OpenSim.Region.Environment.Modules.Avatar.Chat | |||
65 | m_scenes.Add(scene); | 65 | m_scenes.Add(scene); |
66 | scene.EventManager.OnNewClient += NewClient; | 66 | scene.EventManager.OnNewClient += NewClient; |
67 | scene.EventManager.OnChatFromWorld += SimChat; | 67 | scene.EventManager.OnChatFromWorld += SimChat; |
68 | // scene.RegisterModuleInterface<ISimChat>(this); | ||
69 | } | 68 | } |
70 | 69 | ||
71 | // wrap this in a try block so that defaults will work if | 70 | // wrap this in a try block so that defaults will work if |
@@ -108,8 +107,6 @@ namespace OpenSim.Region.Environment.Modules.Avatar.Chat | |||
108 | public void SimChat(Object sender, ChatFromViewerArgs e) | 107 | public void SimChat(Object sender, ChatFromViewerArgs e) |
109 | { | 108 | { |
110 | ScenePresence avatar = null; | 109 | ScenePresence avatar = null; |
111 | |||
112 | //TODO: Move ForEachScenePresence and others into IScene. | ||
113 | Scene scene = (Scene) e.Scene; | 110 | Scene scene = (Scene) e.Scene; |
114 | 111 | ||
115 | //TODO: Remove the need for this check | 112 | //TODO: Remove the need for this check |
diff --git a/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs b/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs new file mode 100644 index 0000000..0b5f1b4 --- /dev/null +++ b/OpenSim/Region/Environment/Modules/Avatar/Chat/IRCBridgeModule.cs | |||
@@ -0,0 +1,860 @@ | |||
1 | /* | ||
2 | * Copyright (c) Contributors, http://opensimulator.org/ | ||
3 | * See CONTRIBUTORS.TXT for a full list of copyright holders. | ||
4 | * | ||
5 | * Redistribution and use in source and binary forms, with or without | ||
6 | * modification, are permitted provided that the following conditions are met: | ||
7 | * * Redistributions of source code must retain the above copyright | ||
8 | * notice, this list of conditions and the following disclaimer. | ||
9 | * * Redistributions in binary form must reproduce the above copyright | ||
10 | * notice, this list of conditions and the following disclaimer in the | ||
11 | * documentation and/or other materials provided with the distribution. | ||
12 | * * Neither the name of the OpenSim Project nor the | ||
13 | * names of its contributors may be used to endorse or promote products | ||
14 | * derived from this software without specific prior written permission. | ||
15 | * | ||
16 | * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY | ||
17 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
19 | * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY | ||
20 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||
23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
26 | */ | ||
27 | |||
28 | using System; | ||
29 | using System.Collections.Generic; | ||
30 | using System.IO; | ||
31 | using System.Net.Sockets; | ||
32 | using System.Reflection; | ||
33 | using System.Text.RegularExpressions; | ||
34 | using System.Threading; | ||
35 | using libsecondlife; | ||
36 | using log4net; | ||
37 | using Nini.Config; | ||
38 | using OpenSim.Framework; | ||
39 | using OpenSim.Region.Environment.Interfaces; | ||
40 | using OpenSim.Region.Environment.Scenes; | ||
41 | |||
42 | namespace OpenSim.Region.Environment.Modules.Avatar.Chat | ||
43 | { | ||
44 | public class IRCBridgeModule : IRegionModule, ISimChat | ||
45 | { | ||
46 | private static readonly ILog m_log = | ||
47 | LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | ||
48 | |||
49 | private const int DEBUG_CHANNEL = 2147483647; | ||
50 | |||
51 | private string m_defaultzone = null; | ||
52 | |||
53 | private IRCChatModule m_irc = null; | ||
54 | private Thread m_irc_connector = null; | ||
55 | |||
56 | private string m_last_leaving_user = null; | ||
57 | private string m_last_new_user = null; | ||
58 | private List<Scene> m_scenes = new List<Scene>(); | ||
59 | |||
60 | internal object m_syncInit = new object(); | ||
61 | internal object m_syncLogout = new object(); | ||
62 | |||
63 | #region IRegionModule Members | ||
64 | |||
65 | public void Initialise(Scene scene, IConfigSource config) | ||
66 | { | ||
67 | lock (m_syncInit) | ||
68 | { | ||
69 | if (!m_scenes.Contains(scene)) | ||
70 | { | ||
71 | m_scenes.Add(scene); | ||
72 | scene.EventManager.OnNewClient += NewClient; | ||
73 | scene.EventManager.OnChatFromWorld += SimChat; | ||
74 | scene.EventManager.OnMakeRootAgent += OnMakeRootAgent; | ||
75 | scene.EventManager.OnMakeChildAgent += OnMakeChildAgent; | ||
76 | } | ||
77 | |||
78 | try | ||
79 | { | ||
80 | m_defaultzone = config.Configs["IRC"].GetString("fallback_region", "Sim"); | ||
81 | } | ||
82 | catch (Exception) | ||
83 | { | ||
84 | } | ||
85 | |||
86 | // setup IRC Relay | ||
87 | if (m_irc == null) | ||
88 | { | ||
89 | m_irc = new IRCChatModule(config); | ||
90 | } | ||
91 | if (m_irc_connector == null) | ||
92 | { | ||
93 | m_irc_connector = new Thread(IRCConnectRun); | ||
94 | m_irc_connector.Name = "IRCConnectorThread"; | ||
95 | m_irc_connector.IsBackground = true; | ||
96 | } | ||
97 | m_log.InfoFormat("[IRC] initialized for {0}, nick: {1} ", scene.RegionInfo.RegionName, | ||
98 | m_defaultzone); | ||
99 | } | ||
100 | } | ||
101 | |||
102 | public void PostInitialise() | ||
103 | { | ||
104 | if (m_irc.Enabled) | ||
105 | { | ||
106 | try | ||
107 | { | ||
108 | //m_irc.Connect(m_scenes); | ||
109 | if (m_irc_connector == null) | ||
110 | { | ||
111 | m_irc_connector = new Thread(IRCConnectRun); | ||
112 | m_irc_connector.Name = "IRCConnectorThread"; | ||
113 | m_irc_connector.IsBackground = true; | ||
114 | } | ||
115 | if (!m_irc_connector.IsAlive) | ||
116 | { | ||
117 | m_irc_connector.Start(); | ||
118 | ThreadTracker.Add(m_irc_connector); | ||
119 | } | ||
120 | } | ||
121 | catch (Exception) | ||
122 | { | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | |||
127 | public void Close() | ||
128 | { | ||
129 | m_irc.Close(); | ||
130 | } | ||
131 | |||
132 | public string Name | ||
133 | { | ||
134 | get { return "IRCBridgeModule"; } | ||
135 | } | ||
136 | |||
137 | public bool IsSharedModule | ||
138 | { | ||
139 | get { return true; } | ||
140 | } | ||
141 | |||
142 | #endregion | ||
143 | |||
144 | #region ISimChat Members | ||
145 | |||
146 | public void SimChat(Object sender, ChatFromViewerArgs e) | ||
147 | { | ||
148 | ScenePresence avatar = null; | ||
149 | Scene scene = (Scene) e.Scene; | ||
150 | |||
151 | if (scene == null) | ||
152 | scene = m_scenes[0]; | ||
153 | |||
154 | // Filled in since it's easier than rewriting right now. | ||
155 | string fromName = e.From; | ||
156 | |||
157 | if (e.Sender != null) | ||
158 | { | ||
159 | avatar = scene.GetScenePresence(e.Sender.AgentId); | ||
160 | } | ||
161 | |||
162 | if (avatar != null) | ||
163 | { | ||
164 | fromName = avatar.Firstname + " " + avatar.Lastname; | ||
165 | } | ||
166 | |||
167 | // Try to reconnect to server if not connected | ||
168 | if (m_irc.Enabled && !m_irc.Connected) | ||
169 | { | ||
170 | // In a non-blocking way. Eventually the connector will get it started | ||
171 | try | ||
172 | { | ||
173 | if (m_irc_connector == null) | ||
174 | { | ||
175 | m_irc_connector = new Thread(IRCConnectRun); | ||
176 | m_irc_connector.Name = "IRCConnectorThread"; | ||
177 | m_irc_connector.IsBackground = true; | ||
178 | } | ||
179 | if (!m_irc_connector.IsAlive) | ||
180 | { | ||
181 | m_irc_connector.Start(); | ||
182 | ThreadTracker.Add(m_irc_connector); | ||
183 | } | ||
184 | } | ||
185 | catch (Exception) | ||
186 | { | ||
187 | } | ||
188 | } | ||
189 | |||
190 | |||
191 | // We only want to relay stuff on channel 0 | ||
192 | if (e.Channel == 0 || e.Channel == DEBUG_CHANNEL) | ||
193 | { | ||
194 | if (e.Channel == DEBUG_CHANNEL) | ||
195 | e.Type = ChatTypeEnum.DebugChannel; | ||
196 | |||
197 | // IRC stuff | ||
198 | if (e.Message.Length > 0 && e.Channel == 0) | ||
199 | { | ||
200 | if (m_irc.Connected && (avatar != null)) // this is to keep objects from talking to IRC | ||
201 | { | ||
202 | m_irc.PrivMsg(fromName, scene.RegionInfo.RegionName, e.Message); | ||
203 | } | ||
204 | } | ||
205 | } | ||
206 | } | ||
207 | |||
208 | #endregion | ||
209 | |||
210 | public void NewClient(IClientAPI client) | ||
211 | { | ||
212 | try | ||
213 | { | ||
214 | string clientName = String.Format("{0} {1}", client.FirstName, client.LastName); | ||
215 | |||
216 | client.OnChatFromViewer += SimChat; | ||
217 | client.OnLogout += ClientLoggedOut; | ||
218 | client.OnConnectionClosed += ClientLoggedOut; | ||
219 | |||
220 | if (clientName != m_last_new_user) | ||
221 | { | ||
222 | if ((m_irc.Enabled) && (m_irc.Connected)) | ||
223 | { | ||
224 | m_log.DebugFormat("[IRC] {0} logging on", clientName); | ||
225 | m_irc.PrivMsg(m_irc.Nick, "Sim", | ||
226 | String.Format("notices {0} logging on", clientName)); | ||
227 | } | ||
228 | m_last_new_user = clientName; | ||
229 | } | ||
230 | } | ||
231 | catch (Exception ex) | ||
232 | { | ||
233 | m_log.Error("[IRC]: NewClient exception trap:" + ex.ToString()); | ||
234 | } | ||
235 | } | ||
236 | |||
237 | public void OnMakeRootAgent(ScenePresence presence) | ||
238 | { | ||
239 | try | ||
240 | { | ||
241 | if ((m_irc.Enabled) && (m_irc.Connected)) | ||
242 | { | ||
243 | string regionName = presence.Scene.RegionInfo.RegionName; | ||
244 | string clientName = String.Format("{0} {1}", presence.Firstname, presence.Lastname); | ||
245 | m_log.DebugFormat("[IRC] noticing {0} in {1}", clientName, regionName); | ||
246 | m_irc.PrivMsg(m_irc.Nick, "Sim", String.Format("notices {0} in {1}", clientName, regionName)); | ||
247 | } | ||
248 | } | ||
249 | catch (Exception ex) | ||
250 | { | ||
251 | } | ||
252 | } | ||
253 | |||
254 | public void OnMakeChildAgent(ScenePresence presence) | ||
255 | { | ||
256 | try | ||
257 | { | ||
258 | if ((m_irc.Enabled) && (m_irc.Connected)) | ||
259 | { | ||
260 | string regionName = presence.Scene.RegionInfo.RegionName; | ||
261 | string clientName = String.Format("{0} {1}", presence.Firstname, presence.Lastname); | ||
262 | m_log.DebugFormat("[IRC] noticing {0} in {1}", clientName, regionName); | ||
263 | m_irc.PrivMsg(m_irc.Nick, "Sim", String.Format("notices {0} left {1}", clientName, regionName)); | ||
264 | } | ||
265 | } | ||
266 | catch (Exception ex) | ||
267 | { | ||
268 | } | ||
269 | } | ||
270 | |||
271 | |||
272 | public void ClientLoggedOut(IClientAPI client) | ||
273 | { | ||
274 | lock (m_syncLogout) | ||
275 | { | ||
276 | try | ||
277 | { | ||
278 | if ((m_irc.Enabled) && (m_irc.Connected)) | ||
279 | { | ||
280 | string clientName = String.Format("{0} {1}", client.FirstName, client.LastName); | ||
281 | // handles simple case. May not work for hundred connecting in per second. | ||
282 | // and the NewClients calles getting interleved | ||
283 | // but filters out multiple reports | ||
284 | if (clientName != m_last_leaving_user) | ||
285 | { | ||
286 | m_last_leaving_user = clientName; | ||
287 | m_irc.PrivMsg(m_irc.Nick, "Sim", String.Format("notices {0} logging out", clientName)); | ||
288 | m_log.InfoFormat("[IRC]: {0} logging out", clientName); | ||
289 | } | ||
290 | |||
291 | if (m_last_new_user == clientName) | ||
292 | m_last_new_user = null; | ||
293 | } | ||
294 | } | ||
295 | catch (Exception ex) | ||
296 | { | ||
297 | m_log.Error("[IRC]: ClientLoggedOut exception trap:" + ex.ToString()); | ||
298 | } | ||
299 | } | ||
300 | } | ||
301 | |||
302 | // if IRC is enabled then just keep trying using a monitor thread | ||
303 | public void IRCConnectRun() | ||
304 | { | ||
305 | while (true) | ||
306 | { | ||
307 | if ((m_irc.Enabled) && (!m_irc.Connected)) | ||
308 | { | ||
309 | m_irc.Connect(m_scenes); | ||
310 | } | ||
311 | Thread.Sleep(15000); | ||
312 | } | ||
313 | } | ||
314 | |||
315 | public string FindClientRegion(string firstName, string lastName, bool ignoreChilds) | ||
316 | { | ||
317 | string sourceRegion = null; | ||
318 | foreach (Scene s in m_scenes) | ||
319 | { | ||
320 | s.ForEachScenePresence(delegate(ScenePresence presence) | ||
321 | { | ||
322 | if (ignoreChilds | ||
323 | && (presence.Firstname == firstName) | ||
324 | && (presence.Lastname == lastName)) | ||
325 | { | ||
326 | sourceRegion = presence.Scene.RegionInfo.RegionName; | ||
327 | return; | ||
328 | } | ||
329 | |||
330 | if (!ignoreChilds && !presence.IsChildAgent | ||
331 | && (presence.Firstname == firstName) | ||
332 | && (presence.Lastname == lastName)) | ||
333 | { | ||
334 | sourceRegion = presence.Scene.RegionInfo.RegionName; | ||
335 | return; | ||
336 | } | ||
337 | }); | ||
338 | if (sourceRegion != null) return sourceRegion; | ||
339 | } | ||
340 | if (m_defaultzone == null) | ||
341 | { | ||
342 | m_defaultzone = "Sim"; | ||
343 | } | ||
344 | return m_defaultzone; | ||
345 | } | ||
346 | } | ||
347 | |||
348 | internal class IRCChatModule | ||
349 | { | ||
350 | #region ErrorReplies enum | ||
351 | |||
352 | public enum ErrorReplies | ||
353 | { | ||
354 | NotRegistered = 451, // ":You have not registered" | ||
355 | NicknameInUse = 433 // "<nick> :Nickname is already in use" | ||
356 | } | ||
357 | |||
358 | #endregion | ||
359 | |||
360 | #region Replies enum | ||
361 | |||
362 | public enum Replies | ||
363 | { | ||
364 | MotdStart = 375, // ":- <server> Message of the day - " | ||
365 | Motd = 372, // ":- <text>" | ||
366 | EndOfMotd = 376 // ":End of /MOTD command" | ||
367 | } | ||
368 | |||
369 | #endregion | ||
370 | |||
371 | private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); | ||
372 | private Thread listener; | ||
373 | |||
374 | private string m_basenick = null; | ||
375 | private string m_channel = null; | ||
376 | private bool m_connected = false; | ||
377 | private bool m_enabled = false; | ||
378 | private List<Scene> m_last_scenes = null; | ||
379 | private string m_nick = null; | ||
380 | private uint m_port = 6668; | ||
381 | private string m_privmsgformat = "PRIVMSG {0} :<{1} in {2}>: {3}"; | ||
382 | private StreamReader m_reader; | ||
383 | private List<Scene> m_scenes = null; | ||
384 | private string m_server = null; | ||
385 | |||
386 | private NetworkStream m_stream; | ||
387 | internal object m_syncConnect = new object(); | ||
388 | private TcpClient m_tcp; | ||
389 | private string m_user = "USER OpenSimBot 8 * :I'm an OpenSim to irc bot"; | ||
390 | private StreamWriter m_writer; | ||
391 | |||
392 | private Thread pingSender; | ||
393 | |||
394 | public IRCChatModule(IConfigSource config) | ||
395 | { | ||
396 | m_nick = "OSimBot" + Util.RandomClass.Next(1, 99); | ||
397 | m_tcp = null; | ||
398 | m_writer = null; | ||
399 | m_reader = null; | ||
400 | |||
401 | // configuration in OpenSim.ini | ||
402 | // [IRC] | ||
403 | // server = chat.freenode.net | ||
404 | // nick = OSimBot_mysim | ||
405 | // ;username = USER OpenSimBot 8 * :I'm a OpenSim to irc bot | ||
406 | // ; username is the IRC command line sent | ||
407 | // ; USER <irc_user> <visible=8,invisible=0> * : <IRC_realname> | ||
408 | // channel = #opensim-regions | ||
409 | // port = 6667 | ||
410 | // ;MSGformat fields : 0=botnick, 1=user, 2=region, 3=message | ||
411 | // ;for <bot>:<user in region> :<message> | ||
412 | // ;msgformat = "PRIVMSG {0} :<{1} in {2}>: {3}" | ||
413 | // ;for <bot>:<message> - <user of region> : | ||
414 | // ;msgformat = "PRIVMSG {0} : {3} - {1} of {2}" | ||
415 | // ;for <bot>:<message> - from <user> : | ||
416 | // ;msgformat = "PRIVMSG {0} : {3} - from {1}" | ||
417 | // Traps I/O disconnects so it does not crash the sim | ||
418 | // Trys to reconnect if disconnected and someone says something | ||
419 | // Tells IRC server "QUIT" when doing a close (just to be nice) | ||
420 | // Default port back to 6667 | ||
421 | |||
422 | try | ||
423 | { | ||
424 | m_server = config.Configs["IRC"].GetString("server"); | ||
425 | m_nick = config.Configs["IRC"].GetString("nick"); | ||
426 | m_basenick = m_nick; | ||
427 | m_channel = config.Configs["IRC"].GetString("channel"); | ||
428 | m_port = (uint) config.Configs["IRC"].GetInt("port", (int) m_port); | ||
429 | m_user = config.Configs["IRC"].GetString("username", m_user); | ||
430 | m_privmsgformat = config.Configs["IRC"].GetString("msgformat", m_privmsgformat); | ||
431 | if (m_server != null && m_nick != null && m_channel != null) | ||
432 | { | ||
433 | m_nick = m_nick + Util.RandomClass.Next(1, 99); | ||
434 | m_enabled = true; | ||
435 | } | ||
436 | } | ||
437 | catch (Exception) | ||
438 | { | ||
439 | m_log.Info("[CHAT]: No IRC config information, skipping IRC bridge configuration"); | ||
440 | } | ||
441 | } | ||
442 | |||
443 | public bool Enabled | ||
444 | { | ||
445 | get { return m_enabled; } | ||
446 | } | ||
447 | |||
448 | public bool Connected | ||
449 | { | ||
450 | get { return m_connected; } | ||
451 | } | ||
452 | |||
453 | public string Nick | ||
454 | { | ||
455 | get { return m_nick; } | ||
456 | } | ||
457 | |||
458 | public bool Connect(List<Scene> scenes) | ||
459 | { | ||
460 | lock (m_syncConnect) | ||
461 | { | ||
462 | try | ||
463 | { | ||
464 | if (m_connected) return true; | ||
465 | m_scenes = scenes; | ||
466 | if (m_last_scenes == null) | ||
467 | { | ||
468 | m_last_scenes = scenes; | ||
469 | } | ||
470 | |||
471 | m_tcp = new TcpClient(m_server, (int) m_port); | ||
472 | m_log.Info("[IRC]: Connecting..."); | ||
473 | m_stream = m_tcp.GetStream(); | ||
474 | m_log.Info("[IRC]: Connected to " + m_server); | ||
475 | m_reader = new StreamReader(m_stream); | ||
476 | m_writer = new StreamWriter(m_stream); | ||
477 | |||
478 | pingSender = new Thread(new ThreadStart(PingRun)); | ||
479 | pingSender.Name = "PingSenderThread"; | ||
480 | pingSender.IsBackground = true; | ||
481 | pingSender.Start(); | ||
482 | ThreadTracker.Add(pingSender); | ||
483 | |||
484 | listener = new Thread(new ThreadStart(ListenerRun)); | ||
485 | listener.Name = "IRCChatModuleListenerThread"; | ||
486 | listener.IsBackground = true; | ||
487 | listener.Start(); | ||
488 | ThreadTracker.Add(listener); | ||
489 | |||
490 | m_writer.WriteLine(m_user); | ||
491 | m_writer.Flush(); | ||
492 | m_writer.WriteLine(String.Format("NICK {0}", m_nick)); | ||
493 | m_writer.Flush(); | ||
494 | m_writer.WriteLine(String.Format("JOIN {0}", m_channel)); | ||
495 | m_writer.Flush(); | ||
496 | m_log.Info("[IRC]: Connection fully established"); | ||
497 | m_connected = true; | ||
498 | } | ||
499 | catch (Exception e) | ||
500 | { | ||
501 | Console.WriteLine(e.ToString()); | ||
502 | } | ||
503 | return m_connected; | ||
504 | } | ||
505 | } | ||
506 | |||
507 | public void Reconnect() | ||
508 | { | ||
509 | m_connected = false; | ||
510 | listener.Abort(); | ||
511 | pingSender.Abort(); | ||
512 | m_writer.Close(); | ||
513 | m_reader.Close(); | ||
514 | m_tcp.Close(); | ||
515 | if (m_enabled) | ||
516 | { | ||
517 | Connect(m_last_scenes); | ||
518 | } | ||
519 | } | ||
520 | |||
521 | public void PrivMsg(string from, string region, string msg) | ||
522 | { | ||
523 | // One message to the IRC server | ||
524 | try | ||
525 | { | ||
526 | m_writer.WriteLine(m_privmsgformat, m_channel, from, region, msg); | ||
527 | m_writer.Flush(); | ||
528 | m_log.InfoFormat("[IRC]: PrivMsg {0} in {1}: {2}", from, region, msg); | ||
529 | } | ||
530 | catch (IOException) | ||
531 | { | ||
532 | m_log.Error("[IRC]: Disconnected from IRC server.(PrivMsg)"); | ||
533 | Reconnect(); | ||
534 | } | ||
535 | catch (Exception ex) | ||
536 | { | ||
537 | m_log.ErrorFormat("[IRC]: PrivMsg exception trap: {0}", ex.ToString()); | ||
538 | } | ||
539 | } | ||
540 | |||
541 | private Dictionary<string, string> ExtractMsg(string input) | ||
542 | { | ||
543 | //examines IRC commands and extracts any private messages | ||
544 | // which will then be reboadcast in the Sim | ||
545 | |||
546 | m_log.Info("[IRC]: ExtractMsg: " + input); | ||
547 | Dictionary<string, string> result = null; | ||
548 | //string regex = @":(?<nick>\w*)!~(?<user>\S*) PRIVMSG (?<channel>\S+) :(?<msg>.*)"; | ||
549 | string regex = @":(?<nick>\w*)!(?<user>\S*) PRIVMSG (?<channel>\S+) :(?<msg>.*)"; | ||
550 | Regex RE = new Regex(regex, RegexOptions.Multiline); | ||
551 | MatchCollection matches = RE.Matches(input); | ||
552 | // Get some direct matches $1 $4 is a | ||
553 | if ((matches.Count == 1) && (matches[0].Groups.Count == 5)) | ||
554 | { | ||
555 | result = new Dictionary<string, string>(); | ||
556 | result.Add("nick", matches[0].Groups[1].Value); | ||
557 | result.Add("user", matches[0].Groups[2].Value); | ||
558 | result.Add("channel", matches[0].Groups[3].Value); | ||
559 | result.Add("msg", matches[0].Groups[4].Value); | ||
560 | } | ||
561 | else | ||
562 | { | ||
563 | m_log.Info("[IRC]: Number of matches: " + matches.Count); | ||
564 | if (matches.Count > 0) | ||
565 | { | ||
566 | m_log.Info("[IRC]: Number of groups: " + matches[0].Groups.Count); | ||
567 | } | ||
568 | } | ||
569 | return result; | ||
570 | } | ||
571 | |||
572 | public void PingRun() | ||
573 | { | ||
574 | // IRC keep alive thread | ||
575 | // send PING ever 15 seconds | ||
576 | while (true) | ||
577 | { | ||
578 | try | ||
579 | { | ||
580 | if (m_connected == true) | ||
581 | { | ||
582 | m_writer.WriteLine(String.Format("PING :{0}", m_server)); | ||
583 | m_writer.Flush(); | ||
584 | Thread.Sleep(15000); | ||
585 | } | ||
586 | } | ||
587 | catch (IOException) | ||
588 | { | ||
589 | m_log.Error("[IRC]: Disconnected from IRC server.(PingRun)"); | ||
590 | Reconnect(); | ||
591 | } | ||
592 | catch (Exception ex) | ||
593 | { | ||
594 | m_log.ErrorFormat("[IRC]: PingRun exception trap: {0}\n{1}", ex.ToString(), ex.StackTrace); | ||
595 | } | ||
596 | } | ||
597 | } | ||
598 | |||
599 | public void ListenerRun() | ||
600 | { | ||
601 | string inputLine; | ||
602 | LLVector3 pos = new LLVector3(128, 128, 20); | ||
603 | while (true) | ||
604 | { | ||
605 | try | ||
606 | { | ||
607 | while ((m_connected == true) && ((inputLine = m_reader.ReadLine()) != null)) | ||
608 | { | ||
609 | // Console.WriteLine(inputLine); | ||
610 | if (inputLine.Contains(m_channel)) | ||
611 | { | ||
612 | Dictionary<string, string> data = ExtractMsg(inputLine); | ||
613 | // Any chat ??? | ||
614 | if (data != null) | ||
615 | { | ||
616 | foreach (Scene m_scene in m_scenes) | ||
617 | { | ||
618 | m_scene.ForEachScenePresence(delegate(ScenePresence avatar) | ||
619 | { | ||
620 | if (!avatar.IsChildAgent) | ||
621 | { | ||
622 | avatar.ControllingClient.SendChatMessage( | ||
623 | Helpers.StringToField(data["msg"]), | ||
624 | 1, // 255, | ||
625 | pos, data["nick"], | ||
626 | LLUUID.Zero,(byte)ChatSourceType.Agent,(byte)ChatAudibleLevel.Fully); | ||
627 | } | ||
628 | }); | ||
629 | } | ||
630 | } | ||
631 | else | ||
632 | { | ||
633 | // Was an command from the IRC server | ||
634 | ProcessIRCCommand(inputLine); | ||
635 | } | ||
636 | } | ||
637 | else | ||
638 | { | ||
639 | // Was an command from the IRC server | ||
640 | ProcessIRCCommand(inputLine); | ||
641 | } | ||
642 | Thread.Sleep(150); | ||
643 | } | ||
644 | } | ||
645 | catch (IOException) | ||
646 | { | ||
647 | m_log.Error("[IRC]: ListenerRun IOException. Disconnected from IRC server ??? (ListenerRun)"); | ||
648 | Reconnect(); | ||
649 | } | ||
650 | catch (Exception ex) | ||
651 | { | ||
652 | m_log.ErrorFormat("[IRC]: ListenerRun exception trap: {0}\n{1}", ex.ToString(), ex.StackTrace); | ||
653 | } | ||
654 | } | ||
655 | } | ||
656 | |||
657 | public void BroadcastSim(string sender, string format, params string[] args) | ||
658 | { | ||
659 | LLVector3 pos = new LLVector3(128, 128, 20); | ||
660 | try | ||
661 | { | ||
662 | foreach (Scene m_scene in m_scenes) | ||
663 | { | ||
664 | m_scene.ForEachScenePresence(delegate(ScenePresence avatar) | ||
665 | { | ||
666 | if (!avatar.IsChildAgent) | ||
667 | { | ||
668 | avatar.ControllingClient.SendChatMessage( | ||
669 | Helpers.StringToField(String.Format(format, args)), | ||
670 | 1, //255, | ||
671 | pos, sender, LLUUID.Zero, | ||
672 | (byte)ChatSourceType.Object, | ||
673 | (byte)ChatAudibleLevel.Fully); | ||
674 | } | ||
675 | }); | ||
676 | } | ||
677 | } | ||
678 | catch (Exception ex) // IRC gate should not crash Sim | ||
679 | { | ||
680 | m_log.ErrorFormat("[IRC]: BroadcastSim Exception Trap: {0}\n{1}", ex.ToString(), ex.StackTrace); | ||
681 | } | ||
682 | } | ||
683 | |||
684 | public void ProcessIRCCommand(string command) | ||
685 | { | ||
686 | //m_log.Info("[IRC]: ProcessIRCCommand:" + command); | ||
687 | |||
688 | string[] commArgs = new string[command.Split(' ').Length]; | ||
689 | string c_server = m_server; | ||
690 | |||
691 | commArgs = command.Split(' '); | ||
692 | if (commArgs[0].Substring(0, 1) == ":") | ||
693 | { | ||
694 | commArgs[0] = commArgs[0].Remove(0, 1); | ||
695 | } | ||
696 | |||
697 | if (commArgs[1] == "002") | ||
698 | { | ||
699 | // fetch the correct servername | ||
700 | // ex: irc.freenode.net -> brown.freenode.net/kornbluth.freenode.net/... | ||
701 | // irc.bluewin.ch -> irc1.bluewin.ch/irc2.bluewin.ch | ||
702 | |||
703 | c_server = (commArgs[6].Split('['))[0]; | ||
704 | m_server = c_server; | ||
705 | } | ||
706 | |||
707 | if (commArgs[0] == "ERROR") | ||
708 | { | ||
709 | m_log.ErrorFormat("[IRC]: IRC SERVER ERROR: {0}", command); | ||
710 | } | ||
711 | |||
712 | if (commArgs[0] == "PING") | ||
713 | { | ||
714 | string p_reply = ""; | ||
715 | |||
716 | for (int i = 1; i < commArgs.Length; i++) | ||
717 | { | ||
718 | p_reply += commArgs[i] + " "; | ||
719 | } | ||
720 | |||
721 | m_writer.WriteLine(String.Format("PONG {0}", p_reply)); | ||
722 | m_writer.Flush(); | ||
723 | } | ||
724 | else if (commArgs[0] == c_server) | ||
725 | { | ||
726 | // server message | ||
727 | try | ||
728 | { | ||
729 | Int32 commandCode = Int32.Parse(commArgs[1]); | ||
730 | switch (commandCode) | ||
731 | { | ||
732 | case (int) ErrorReplies.NicknameInUse: | ||
733 | // Gen a new name | ||
734 | m_nick = m_basenick + Util.RandomClass.Next(1, 99); | ||
735 | m_log.ErrorFormat("[IRC]: IRC SERVER reports NicknameInUse, trying {0}", m_nick); | ||
736 | // Retry | ||
737 | m_writer.WriteLine(String.Format("NICK {0}", m_nick)); | ||
738 | m_writer.Flush(); | ||
739 | m_writer.WriteLine(String.Format("JOIN {0}", m_channel)); | ||
740 | m_writer.Flush(); | ||
741 | break; | ||
742 | case (int) ErrorReplies.NotRegistered: | ||
743 | break; | ||
744 | case (int) Replies.EndOfMotd: | ||
745 | break; | ||
746 | } | ||
747 | } | ||
748 | catch (Exception) | ||
749 | { | ||
750 | } | ||
751 | } | ||
752 | else | ||
753 | { | ||
754 | // Normal message | ||
755 | string commAct = commArgs[1]; | ||
756 | switch (commAct) | ||
757 | { | ||
758 | case "JOIN": | ||
759 | eventIrcJoin(commArgs); | ||
760 | break; | ||
761 | case "PART": | ||
762 | eventIrcPart(commArgs); | ||
763 | break; | ||
764 | case "MODE": | ||
765 | eventIrcMode(commArgs); | ||
766 | break; | ||
767 | case "NICK": | ||
768 | eventIrcNickChange(commArgs); | ||
769 | break; | ||
770 | case "KICK": | ||
771 | eventIrcKick(commArgs); | ||
772 | break; | ||
773 | case "QUIT": | ||
774 | eventIrcQuit(commArgs); | ||
775 | break; | ||
776 | case "PONG": | ||
777 | break; // that's nice | ||
778 | } | ||
779 | } | ||
780 | } | ||
781 | |||
782 | public void eventIrcJoin(string[] commArgs) | ||
783 | { | ||
784 | string IrcChannel = commArgs[2]; | ||
785 | string IrcUser = commArgs[0].Split('!')[0]; | ||
786 | BroadcastSim(m_nick, "{0} is joining {1}", IrcUser, IrcChannel); | ||
787 | } | ||
788 | |||
789 | public void eventIrcPart(string[] commArgs) | ||
790 | { | ||
791 | string IrcChannel = commArgs[2]; | ||
792 | string IrcUser = commArgs[0].Split('!')[0]; | ||
793 | BroadcastSim(m_nick, "{0} is parting {1}", IrcUser, IrcChannel); | ||
794 | } | ||
795 | |||
796 | public void eventIrcMode(string[] commArgs) | ||
797 | { | ||
798 | string IrcChannel = commArgs[2]; | ||
799 | string IrcUser = commArgs[0].Split('!')[0]; | ||
800 | string UserMode = ""; | ||
801 | for (int i = 3; i < commArgs.Length; i++) | ||
802 | { | ||
803 | UserMode += commArgs[i] + " "; | ||
804 | } | ||
805 | |||
806 | if (UserMode.Substring(0, 1) == ":") | ||
807 | { | ||
808 | UserMode = UserMode.Remove(0, 1); | ||
809 | } | ||
810 | } | ||
811 | |||
812 | public void eventIrcNickChange(string[] commArgs) | ||
813 | { | ||
814 | string UserOldNick = commArgs[0].Split('!')[0]; | ||
815 | string UserNewNick = commArgs[2].Remove(0, 1); | ||
816 | BroadcastSim(m_nick, "{0} changed their nick to {1}", UserOldNick, UserNewNick); | ||
817 | } | ||
818 | |||
819 | public void eventIrcKick(string[] commArgs) | ||
820 | { | ||
821 | string UserKicker = commArgs[0].Split('!')[0]; | ||
822 | string UserKicked = commArgs[3]; | ||
823 | string IrcChannel = commArgs[2]; | ||
824 | string KickMessage = ""; | ||
825 | for (int i = 4; i < commArgs.Length; i++) | ||
826 | { | ||
827 | KickMessage += commArgs[i] + " "; | ||
828 | } | ||
829 | BroadcastSim(m_nick, "{0} kicked {1} on {2} saying {3}", UserKicker, UserKicked, IrcChannel, KickMessage); | ||
830 | if (UserKicked == m_nick) | ||
831 | { | ||
832 | BroadcastSim(m_nick, "Hey, that was me!!!"); | ||
833 | } | ||
834 | } | ||
835 | |||
836 | public void eventIrcQuit(string[] commArgs) | ||
837 | { | ||
838 | string IrcUser = commArgs[0].Split('!')[0]; | ||
839 | string QuitMessage = ""; | ||
840 | |||
841 | for (int i = 2; i < commArgs.Length; i++) | ||
842 | { | ||
843 | QuitMessage += commArgs[i] + " "; | ||
844 | } | ||
845 | BroadcastSim(m_nick, "{0} quits saying {1}", IrcUser, QuitMessage); | ||
846 | } | ||
847 | |||
848 | public void Close() | ||
849 | { | ||
850 | m_connected = false; | ||
851 | m_writer.WriteLine(String.Format("QUIT :{0} to {1} wormhole with {2} closing", m_nick, m_channel, m_server)); | ||
852 | m_writer.Flush(); | ||
853 | listener.Abort(); | ||
854 | pingSender.Abort(); | ||
855 | m_writer.Close(); | ||
856 | m_reader.Close(); | ||
857 | m_tcp.Close(); | ||
858 | } | ||
859 | } | ||
860 | } \ No newline at end of file | ||
diff --git a/OpenSim/Region/Environment/Scenes/EventManager.cs b/OpenSim/Region/Environment/Scenes/EventManager.cs index f8eef82..6c714f8 100644 --- a/OpenSim/Region/Environment/Scenes/EventManager.cs +++ b/OpenSim/Region/Environment/Scenes/EventManager.cs | |||
@@ -156,7 +156,11 @@ namespace OpenSim.Region.Environment.Scenes | |||
156 | 156 | ||
157 | public event ScriptNotAtTargetEvent OnScriptNotAtTargetEvent; | 157 | public event ScriptNotAtTargetEvent OnScriptNotAtTargetEvent; |
158 | 158 | ||
159 | public event OnNewPresenceDelegate OnMakeChildAgent; | 159 | public delegate void OnMakeChildAgentDelegate(ScenePresence presence); |
160 | public event OnMakeChildAgentDelegate OnMakeChildAgent; | ||
161 | |||
162 | public delegate void OnMakeRootAgentDelegate(ScenePresence presence); | ||
163 | public event OnMakeRootAgentDelegate OnMakeRootAgent; | ||
160 | 164 | ||
161 | public delegate void NewInventoryItemUploadComplete(LLUUID avatarID, LLUUID assetID, string name, int userlevel); | 165 | public delegate void NewInventoryItemUploadComplete(LLUUID avatarID, LLUUID assetID, string name, int userlevel); |
162 | 166 | ||
@@ -307,7 +311,8 @@ namespace OpenSim.Region.Environment.Scenes | |||
307 | private NewGridInstantMessage handlerGridInstantMessageToIM = null; //OnGridInstantMessageToIMModule; | 311 | private NewGridInstantMessage handlerGridInstantMessageToIM = null; //OnGridInstantMessageToIMModule; |
308 | private NewGridInstantMessage handlerGridInstantMessageToFriends = null; //OnGridInstantMessageToFriendsModule; | 312 | private NewGridInstantMessage handlerGridInstantMessageToFriends = null; //OnGridInstantMessageToFriendsModule; |
309 | private ClientClosed handlerClientClosed = null; //OnClientClosed; | 313 | private ClientClosed handlerClientClosed = null; //OnClientClosed; |
310 | private OnNewPresenceDelegate handlerMakeChildAgent = null; //OnMakeChildAgent; | 314 | private OnMakeChildAgentDelegate handlerMakeChildAgent = null; //OnMakeChildAgent; |
315 | private OnMakeRootAgentDelegate handlerMakeRootAgent = null; //OnMakeRootAgent; | ||
311 | private OnTerrainTickDelegate handlerTerrainTick = null; // OnTerainTick; | 316 | private OnTerrainTickDelegate handlerTerrainTick = null; // OnTerainTick; |
312 | private RegisterCapsEvent handlerRegisterCaps = null; // OnRegisterCaps; | 317 | private RegisterCapsEvent handlerRegisterCaps = null; // OnRegisterCaps; |
313 | private DeregisterCapsEvent handlerDeregisterCaps = null; // OnDeregisterCaps; | 318 | private DeregisterCapsEvent handlerDeregisterCaps = null; // OnDeregisterCaps; |
@@ -574,6 +579,15 @@ namespace OpenSim.Region.Environment.Scenes | |||
574 | } | 579 | } |
575 | } | 580 | } |
576 | 581 | ||
582 | public void TriggerOnMakeRootAgent(ScenePresence presence) | ||
583 | { | ||
584 | handlerMakeRootAgent = OnMakeRootAgent; | ||
585 | if (handlerMakeRootAgent != null) | ||
586 | { | ||
587 | handlerMakeRootAgent(presence); | ||
588 | } | ||
589 | } | ||
590 | |||
577 | public void TriggerOnRegisterCaps(LLUUID agentID, Caps caps) | 591 | public void TriggerOnRegisterCaps(LLUUID agentID, Caps caps) |
578 | { | 592 | { |
579 | handlerRegisterCaps = OnRegisterCaps; | 593 | handlerRegisterCaps = OnRegisterCaps; |
diff --git a/OpenSim/Region/Environment/Scenes/Scene.cs b/OpenSim/Region/Environment/Scenes/Scene.cs index fe8cada..71c5b18 100644 --- a/OpenSim/Region/Environment/Scenes/Scene.cs +++ b/OpenSim/Region/Environment/Scenes/Scene.cs | |||
@@ -1582,6 +1582,7 @@ namespace OpenSim.Region.Environment.Scenes | |||
1582 | 1582 | ||
1583 | CommsManager.UserProfileCacheService.AddNewUser(client.AgentId); | 1583 | CommsManager.UserProfileCacheService.AddNewUser(client.AgentId); |
1584 | } | 1584 | } |
1585 | EventManager.TriggerOnNewClient(client); | ||
1585 | } | 1586 | } |
1586 | 1587 | ||
1587 | protected virtual void SubscribeToClientEvents(IClientAPI client) | 1588 | protected virtual void SubscribeToClientEvents(IClientAPI client) |
@@ -1660,7 +1661,7 @@ namespace OpenSim.Region.Environment.Scenes | |||
1660 | client.OnUndo += m_innerScene.HandleUndo; | 1661 | client.OnUndo += m_innerScene.HandleUndo; |
1661 | client.OnObjectGroupRequest += m_innerScene.HandleObjectGroupUpdate; | 1662 | client.OnObjectGroupRequest += m_innerScene.HandleObjectGroupUpdate; |
1662 | 1663 | ||
1663 | EventManager.TriggerOnNewClient(client); | 1664 | // EventManager.TriggerOnNewClient(client); |
1664 | } | 1665 | } |
1665 | 1666 | ||
1666 | public virtual void TeleportClientHome(LLUUID AgentId, IClientAPI client) | 1667 | public virtual void TeleportClientHome(LLUUID AgentId, IClientAPI client) |
diff --git a/OpenSim/Region/Environment/Scenes/ScenePresence.cs b/OpenSim/Region/Environment/Scenes/ScenePresence.cs index 103068c..a692b33 100644 --- a/OpenSim/Region/Environment/Scenes/ScenePresence.cs +++ b/OpenSim/Region/Environment/Scenes/ScenePresence.cs | |||
@@ -590,6 +590,8 @@ namespace OpenSim.Region.Environment.Scenes | |||
590 | //{ | 590 | //{ |
591 | m_scene.SendAllSceneObjectsToClient(this); | 591 | m_scene.SendAllSceneObjectsToClient(this); |
592 | 592 | ||
593 | m_scene.EventManager.TriggerOnMakeRootAgent(this); | ||
594 | |||
593 | //m_gotAllObjectsInScene = true; | 595 | //m_gotAllObjectsInScene = true; |
594 | //} | 596 | //} |
595 | } | 597 | } |