diff options
author | Justin Clark-Casey (justincc) | 2013-08-19 23:50:18 +0100 |
---|---|---|
committer | Justin Clark-Casey (justincc) | 2013-08-19 23:50:18 +0100 |
commit | 589b1a2eaf9058c3577b17ae76580a79ba855978 (patch) | |
tree | c0f9119ce19932e5b0651e66b0daee1d9479bb1b /OpenSim | |
parent | refactor: start bot connection thread within BotManager rather than externally (diff) | |
download | opensim-SC_OLD-589b1a2eaf9058c3577b17ae76580a79ba855978.zip opensim-SC_OLD-589b1a2eaf9058c3577b17ae76580a79ba855978.tar.gz opensim-SC_OLD-589b1a2eaf9058c3577b17ae76580a79ba855978.tar.bz2 opensim-SC_OLD-589b1a2eaf9058c3577b17ae76580a79ba855978.tar.xz |
Make it possible to reconnect pCampbots with the console command "connect [<n>]".
If no n is given then all available bots are connected
Diffstat (limited to 'OpenSim')
-rw-r--r-- | OpenSim/Tools/pCampBot/Bot.cs | 96 | ||||
-rw-r--r-- | OpenSim/Tools/pCampBot/BotManager.cs | 136 | ||||
-rw-r--r-- | OpenSim/Tools/pCampBot/pCampBot.cs | 3 |
3 files changed, 155 insertions, 80 deletions
diff --git a/OpenSim/Tools/pCampBot/Bot.cs b/OpenSim/Tools/pCampBot/Bot.cs index be7a5a1..f7af26e 100644 --- a/OpenSim/Tools/pCampBot/Bot.cs +++ b/OpenSim/Tools/pCampBot/Bot.cs | |||
@@ -158,8 +158,6 @@ namespace pCampBot | |||
158 | 158 | ||
159 | behaviours.ForEach(b => b.Initialize(this)); | 159 | behaviours.ForEach(b => b.Initialize(this)); |
160 | 160 | ||
161 | Client = new GridClient(); | ||
162 | |||
163 | Random = new Random(Environment.TickCount);// We do stuff randomly here | 161 | Random = new Random(Environment.TickCount);// We do stuff randomly here |
164 | FirstName = firstName; | 162 | FirstName = firstName; |
165 | LastName = lastName; | 163 | LastName = lastName; |
@@ -170,6 +168,59 @@ namespace pCampBot | |||
170 | 168 | ||
171 | Manager = bm; | 169 | Manager = bm; |
172 | Behaviours = behaviours; | 170 | Behaviours = behaviours; |
171 | |||
172 | // Only calling for use as a template. | ||
173 | CreateLibOmvClient(); | ||
174 | } | ||
175 | |||
176 | private void CreateLibOmvClient() | ||
177 | { | ||
178 | GridClient newClient = new GridClient(); | ||
179 | |||
180 | if (Client != null) | ||
181 | { | ||
182 | newClient.Settings.LOGIN_SERVER = Client.Settings.LOGIN_SERVER; | ||
183 | newClient.Settings.ALWAYS_DECODE_OBJECTS = Client.Settings.ALWAYS_DECODE_OBJECTS; | ||
184 | newClient.Settings.AVATAR_TRACKING = Client.Settings.AVATAR_TRACKING; | ||
185 | newClient.Settings.OBJECT_TRACKING = Client.Settings.OBJECT_TRACKING; | ||
186 | newClient.Settings.SEND_AGENT_THROTTLE = Client.Settings.SEND_AGENT_THROTTLE; | ||
187 | newClient.Settings.SEND_AGENT_UPDATES = Client.Settings.SEND_AGENT_UPDATES; | ||
188 | newClient.Settings.SEND_PINGS = Client.Settings.SEND_PINGS; | ||
189 | newClient.Settings.STORE_LAND_PATCHES = Client.Settings.STORE_LAND_PATCHES; | ||
190 | newClient.Settings.USE_ASSET_CACHE = Client.Settings.USE_ASSET_CACHE; | ||
191 | newClient.Settings.MULTIPLE_SIMS = Client.Settings.MULTIPLE_SIMS; | ||
192 | newClient.Throttle.Asset = Client.Throttle.Asset; | ||
193 | newClient.Throttle.Land = Client.Throttle.Land; | ||
194 | newClient.Throttle.Task = Client.Throttle.Task; | ||
195 | newClient.Throttle.Texture = Client.Throttle.Texture; | ||
196 | newClient.Throttle.Wind = Client.Throttle.Wind; | ||
197 | newClient.Throttle.Total = Client.Throttle.Total; | ||
198 | } | ||
199 | else | ||
200 | { | ||
201 | newClient.Settings.LOGIN_SERVER = LoginUri; | ||
202 | newClient.Settings.ALWAYS_DECODE_OBJECTS = false; | ||
203 | newClient.Settings.AVATAR_TRACKING = false; | ||
204 | newClient.Settings.OBJECT_TRACKING = false; | ||
205 | newClient.Settings.SEND_AGENT_THROTTLE = true; | ||
206 | newClient.Settings.SEND_PINGS = true; | ||
207 | newClient.Settings.STORE_LAND_PATCHES = false; | ||
208 | newClient.Settings.USE_ASSET_CACHE = false; | ||
209 | newClient.Settings.MULTIPLE_SIMS = true; | ||
210 | newClient.Throttle.Asset = 100000; | ||
211 | newClient.Throttle.Land = 100000; | ||
212 | newClient.Throttle.Task = 100000; | ||
213 | newClient.Throttle.Texture = 100000; | ||
214 | newClient.Throttle.Wind = 100000; | ||
215 | newClient.Throttle.Total = 400000; | ||
216 | } | ||
217 | |||
218 | newClient.Network.LoginProgress += this.Network_LoginProgress; | ||
219 | newClient.Network.SimConnected += this.Network_SimConnected; | ||
220 | newClient.Network.Disconnected += this.Network_OnDisconnected; | ||
221 | newClient.Objects.ObjectUpdate += Objects_NewPrim; | ||
222 | |||
223 | Client = newClient; | ||
173 | } | 224 | } |
174 | 225 | ||
175 | //We do our actions here. This is where one would | 226 | //We do our actions here. This is where one would |
@@ -192,7 +243,7 @@ namespace pCampBot | |||
192 | /// <summary> | 243 | /// <summary> |
193 | /// Tells LibSecondLife to logout and disconnect. Raises the disconnect events once it finishes. | 244 | /// Tells LibSecondLife to logout and disconnect. Raises the disconnect events once it finishes. |
194 | /// </summary> | 245 | /// </summary> |
195 | public void shutdown() | 246 | public void Disconnect() |
196 | { | 247 | { |
197 | ConnectionState = ConnectionState.Disconnecting; | 248 | ConnectionState = ConnectionState.Disconnecting; |
198 | 249 | ||
@@ -202,34 +253,27 @@ namespace pCampBot | |||
202 | Client.Network.Logout(); | 253 | Client.Network.Logout(); |
203 | } | 254 | } |
204 | 255 | ||
256 | public void Connect() | ||
257 | { | ||
258 | Thread connectThread = new Thread(ConnectInternal); | ||
259 | connectThread.Name = Name; | ||
260 | connectThread.IsBackground = true; | ||
261 | |||
262 | connectThread.Start(); | ||
263 | } | ||
264 | |||
205 | /// <summary> | 265 | /// <summary> |
206 | /// This is the bot startup loop. | 266 | /// This is the bot startup loop. |
207 | /// </summary> | 267 | /// </summary> |
208 | public void startup() | 268 | private void ConnectInternal() |
209 | { | 269 | { |
210 | Client.Settings.LOGIN_SERVER = LoginUri; | ||
211 | Client.Settings.ALWAYS_DECODE_OBJECTS = false; | ||
212 | Client.Settings.AVATAR_TRACKING = false; | ||
213 | Client.Settings.OBJECT_TRACKING = false; | ||
214 | Client.Settings.SEND_AGENT_THROTTLE = true; | ||
215 | Client.Settings.SEND_AGENT_UPDATES = false; | ||
216 | Client.Settings.SEND_PINGS = true; | ||
217 | Client.Settings.STORE_LAND_PATCHES = false; | ||
218 | Client.Settings.USE_ASSET_CACHE = false; | ||
219 | Client.Settings.MULTIPLE_SIMS = true; | ||
220 | Client.Throttle.Asset = 100000; | ||
221 | Client.Throttle.Land = 100000; | ||
222 | Client.Throttle.Task = 100000; | ||
223 | Client.Throttle.Texture = 100000; | ||
224 | Client.Throttle.Wind = 100000; | ||
225 | Client.Throttle.Total = 400000; | ||
226 | Client.Network.LoginProgress += this.Network_LoginProgress; | ||
227 | Client.Network.SimConnected += this.Network_SimConnected; | ||
228 | Client.Network.Disconnected += this.Network_OnDisconnected; | ||
229 | Client.Objects.ObjectUpdate += Objects_NewPrim; | ||
230 | |||
231 | ConnectionState = ConnectionState.Connecting; | 270 | ConnectionState = ConnectionState.Connecting; |
232 | 271 | ||
272 | // Current create a new client on each connect. libomv doesn't seem to process new sim | ||
273 | // information (e.g. EstablishAgentCommunication events) if connecting after a disceonnect with the same | ||
274 | // client | ||
275 | CreateLibOmvClient(); | ||
276 | |||
233 | if (Client.Network.Login(FirstName, LastName, Password, "pCampBot", StartLocation, "Your name")) | 277 | if (Client.Network.Login(FirstName, LastName, Password, "pCampBot", StartLocation, "Your name")) |
234 | { | 278 | { |
235 | ConnectionState = ConnectionState.Connected; | 279 | ConnectionState = ConnectionState.Connected; |
@@ -474,6 +518,8 @@ namespace pCampBot | |||
474 | // || args.Reason == NetworkManager.DisconnectType.NetworkTimeout) | 518 | // || args.Reason == NetworkManager.DisconnectType.NetworkTimeout) |
475 | // && OnDisconnected != null) | 519 | // && OnDisconnected != null) |
476 | 520 | ||
521 | |||
522 | |||
477 | if ( | 523 | if ( |
478 | (args.Reason == NetworkManager.DisconnectType.ClientInitiated | 524 | (args.Reason == NetworkManager.DisconnectType.ClientInitiated |
479 | || args.Reason == NetworkManager.DisconnectType.ServerInitiated | 525 | || args.Reason == NetworkManager.DisconnectType.ServerInitiated |
diff --git a/OpenSim/Tools/pCampBot/BotManager.cs b/OpenSim/Tools/pCampBot/BotManager.cs index fe6048a..f40a84d 100644 --- a/OpenSim/Tools/pCampBot/BotManager.cs +++ b/OpenSim/Tools/pCampBot/BotManager.cs | |||
@@ -182,6 +182,12 @@ namespace pCampBot | |||
182 | "bot", false, "quit", "quit", "Shutdown bots and exit", HandleShutdown); | 182 | "bot", false, "quit", "quit", "Shutdown bots and exit", HandleShutdown); |
183 | 183 | ||
184 | m_console.Commands.AddCommand( | 184 | m_console.Commands.AddCommand( |
185 | "bot", false, "connect", "connect [<n>]", "Connect bots", | ||
186 | "If an <n> is given, then the first <n> disconnected bots by postfix number are connected.\n" | ||
187 | + "If no <n> is given, then all currently disconnected bots are connected.", | ||
188 | HandleConnect); | ||
189 | |||
190 | m_console.Commands.AddCommand( | ||
185 | "bot", false, "disconnect", "disconnect [<n>]", "Disconnect bots", | 191 | "bot", false, "disconnect", "disconnect [<n>]", "Disconnect bots", |
186 | "Disconnecting bots will interupt any bot connection process, including connection on startup.\n" | 192 | "Disconnecting bots will interupt any bot connection process, including connection on startup.\n" |
187 | + "If an <n> is given, then the last <n> connected bots by postfix number are disconnected.\n" | 193 | + "If an <n> is given, then the last <n> connected bots by postfix number are disconnected.\n" |
@@ -206,7 +212,7 @@ namespace pCampBot | |||
206 | /// </summary> | 212 | /// </summary> |
207 | /// <param name="botcount">How many bots to start up</param> | 213 | /// <param name="botcount">How many bots to start up</param> |
208 | /// <param name="cs">The configuration for the bots to use</param> | 214 | /// <param name="cs">The configuration for the bots to use</param> |
209 | public void dobotStartup(int botcount, IConfig startupConfig) | 215 | public void CreateBots(int botcount, IConfig startupConfig) |
210 | { | 216 | { |
211 | m_firstName = startupConfig.GetString("firstname"); | 217 | m_firstName = startupConfig.GetString("firstname"); |
212 | m_lastNameStem = startupConfig.GetString("lastname"); | 218 | m_lastNameStem = startupConfig.GetString("lastname"); |
@@ -220,39 +226,55 @@ namespace pCampBot | |||
220 | Array.ForEach<string>( | 226 | Array.ForEach<string>( |
221 | startupConfig.GetString("behaviours", "p").Split(new char[] { ',' }), b => m_behaviourSwitches.Add(b)); | 227 | startupConfig.GetString("behaviours", "p").Split(new char[] { ',' }), b => m_behaviourSwitches.Add(b)); |
222 | 228 | ||
223 | ConnectBots( | 229 | for (int i = 0; i < botcount; i++) |
224 | botcount, m_firstName, m_lastNameStem, m_password, m_loginUri, m_startUri, m_fromBotNumber, m_wearSetting, m_behaviourSwitches); | 230 | { |
231 | lock (m_bots) | ||
232 | { | ||
233 | string lastName = string.Format("{0}_{1}", m_lastNameStem, i + m_fromBotNumber); | ||
234 | |||
235 | // We must give each bot its own list of instantiated behaviours since they store state. | ||
236 | List<IBehaviour> behaviours = new List<IBehaviour>(); | ||
237 | |||
238 | // Hard-coded for now | ||
239 | if (m_behaviourSwitches.Contains("c")) | ||
240 | behaviours.Add(new CrossBehaviour()); | ||
241 | |||
242 | if (m_behaviourSwitches.Contains("g")) | ||
243 | behaviours.Add(new GrabbingBehaviour()); | ||
244 | |||
245 | if (m_behaviourSwitches.Contains("n")) | ||
246 | behaviours.Add(new NoneBehaviour()); | ||
247 | |||
248 | if (m_behaviourSwitches.Contains("p")) | ||
249 | behaviours.Add(new PhysicsBehaviour()); | ||
250 | |||
251 | if (m_behaviourSwitches.Contains("t")) | ||
252 | behaviours.Add(new TeleportBehaviour()); | ||
253 | |||
254 | CreateBot(this, behaviours, m_firstName, lastName, m_password, m_loginUri, m_startUri, m_wearSetting); | ||
255 | } | ||
256 | } | ||
225 | } | 257 | } |
226 | 258 | ||
227 | private bool ConnectBots( | 259 | public void ConnectBots(int botcount) |
228 | int botcount, string firstName, string lastNameStem, string password, string loginUri, string startUri, int fromBotNumber, string wearSetting, | ||
229 | HashSet<string> behaviourSwitches) | ||
230 | { | 260 | { |
231 | ConnectingBots = true; | 261 | ConnectingBots = true; |
232 | 262 | ||
233 | Thread startBotThread | 263 | Thread connectBotThread = new Thread(o => ConnectBotsInternal(botcount)); |
234 | = new Thread( | ||
235 | o => ConnectBotsInternal( | ||
236 | botcount, firstName, lastNameStem, password, loginUri, startUri, fromBotNumber, wearSetting, | ||
237 | behaviourSwitches)); | ||
238 | |||
239 | startBotThread.Name = "Bots connection thread"; | ||
240 | startBotThread.Start(); | ||
241 | 264 | ||
242 | return true; | 265 | connectBotThread.Name = "Bots connection thread"; |
266 | connectBotThread.Start(); | ||
243 | } | 267 | } |
244 | 268 | ||
245 | private void ConnectBotsInternal( | 269 | private void ConnectBotsInternal(int botcount) |
246 | int botcount, string firstName, string lastNameStem, string password, string loginUri, string startUri, int fromBotNumber, string wearSetting, | ||
247 | HashSet<string> behaviourSwitches) | ||
248 | { | 270 | { |
249 | MainConsole.Instance.OutputFormat( | 271 | MainConsole.Instance.OutputFormat( |
250 | "[BOT MANAGER]: Starting {0} bots connecting to {1}, location {2}, named {3} {4}_<n>", | 272 | "[BOT MANAGER]: Starting {0} bots connecting to {1}, location {2}, named {3} {4}_<n>", |
251 | botcount, | 273 | botcount, |
252 | loginUri, | 274 | m_loginUri, |
253 | startUri, | 275 | m_startUri, |
254 | firstName, | 276 | m_firstName, |
255 | lastNameStem); | 277 | m_lastNameStem); |
256 | 278 | ||
257 | MainConsole.Instance.OutputFormat("[BOT MANAGER]: Delay between logins is {0}ms", LoginDelay); | 279 | MainConsole.Instance.OutputFormat("[BOT MANAGER]: Delay between logins is {0}ms", LoginDelay); |
258 | MainConsole.Instance.OutputFormat("[BOT MANAGER]: BotsSendAgentUpdates is {0}", InitBotSendAgentUpdates); | 280 | MainConsole.Instance.OutputFormat("[BOT MANAGER]: BotsSendAgentUpdates is {0}", InitBotSendAgentUpdates); |
@@ -269,28 +291,7 @@ namespace pCampBot | |||
269 | break; | 291 | break; |
270 | } | 292 | } |
271 | 293 | ||
272 | string lastName = string.Format("{0}_{1}", lastNameStem, i + fromBotNumber); | 294 | m_bots[i].Connect(); |
273 | |||
274 | // We must give each bot its own list of instantiated behaviours since they store state. | ||
275 | List<IBehaviour> behaviours = new List<IBehaviour>(); | ||
276 | |||
277 | // Hard-coded for now | ||
278 | if (behaviourSwitches.Contains("c")) | ||
279 | behaviours.Add(new CrossBehaviour()); | ||
280 | |||
281 | if (behaviourSwitches.Contains("g")) | ||
282 | behaviours.Add(new GrabbingBehaviour()); | ||
283 | |||
284 | if (behaviourSwitches.Contains("n")) | ||
285 | behaviours.Add(new NoneBehaviour()); | ||
286 | |||
287 | if (behaviourSwitches.Contains("p")) | ||
288 | behaviours.Add(new PhysicsBehaviour()); | ||
289 | |||
290 | if (behaviourSwitches.Contains("t")) | ||
291 | behaviours.Add(new TeleportBehaviour()); | ||
292 | |||
293 | StartBot(this, behaviours, firstName, lastName, password, loginUri, startUri, wearSetting); | ||
294 | } | 295 | } |
295 | 296 | ||
296 | // Stagger logins | 297 | // Stagger logins |
@@ -360,7 +361,7 @@ namespace pCampBot | |||
360 | // } | 361 | // } |
361 | 362 | ||
362 | /// <summary> | 363 | /// <summary> |
363 | /// This starts up the bot and stores the thread for the bot in the thread array | 364 | /// This creates a bot but does not start it. |
364 | /// </summary> | 365 | /// </summary> |
365 | /// <param name="bm"></param> | 366 | /// <param name="bm"></param> |
366 | /// <param name="behaviours">Behaviours for this bot to perform.</param> | 367 | /// <param name="behaviours">Behaviours for this bot to perform.</param> |
@@ -370,12 +371,12 @@ namespace pCampBot | |||
370 | /// <param name="loginUri">Login URI</param> | 371 | /// <param name="loginUri">Login URI</param> |
371 | /// <param name="startLocation">Location to start the bot. Can be "last", "home" or a specific sim name.</param> | 372 | /// <param name="startLocation">Location to start the bot. Can be "last", "home" or a specific sim name.</param> |
372 | /// <param name="wearSetting"></param> | 373 | /// <param name="wearSetting"></param> |
373 | public void StartBot( | 374 | public void CreateBot( |
374 | BotManager bm, List<IBehaviour> behaviours, | 375 | BotManager bm, List<IBehaviour> behaviours, |
375 | string firstName, string lastName, string password, string loginUri, string startLocation, string wearSetting) | 376 | string firstName, string lastName, string password, string loginUri, string startLocation, string wearSetting) |
376 | { | 377 | { |
377 | MainConsole.Instance.OutputFormat( | 378 | MainConsole.Instance.OutputFormat( |
378 | "[BOT MANAGER]: Starting bot {0} {1}, behaviours are {2}", | 379 | "[BOT MANAGER]: Creating bot {0} {1}, behaviours are {2}", |
379 | firstName, lastName, string.Join(",", behaviours.ConvertAll<string>(b => b.Name).ToArray())); | 380 | firstName, lastName, string.Join(",", behaviours.ConvertAll<string>(b => b.Name).ToArray())); |
380 | 381 | ||
381 | Bot pb = new Bot(bm, behaviours, firstName, lastName, password, startLocation, loginUri); | 382 | Bot pb = new Bot(bm, behaviours, firstName, lastName, password, startLocation, loginUri); |
@@ -387,12 +388,6 @@ namespace pCampBot | |||
387 | pb.OnDisconnected += handlebotEvent; | 388 | pb.OnDisconnected += handlebotEvent; |
388 | 389 | ||
389 | m_bots.Add(pb); | 390 | m_bots.Add(pb); |
390 | |||
391 | Thread pbThread = new Thread(pb.startup); | ||
392 | pbThread.Name = pb.Name; | ||
393 | pbThread.IsBackground = true; | ||
394 | |||
395 | pbThread.Start(); | ||
396 | } | 391 | } |
397 | 392 | ||
398 | /// <summary> | 393 | /// <summary> |
@@ -427,6 +422,37 @@ namespace pCampBot | |||
427 | return new LocalConsole("pCampbot"); | 422 | return new LocalConsole("pCampbot"); |
428 | } | 423 | } |
429 | 424 | ||
425 | private void HandleConnect(string module, string[] cmd) | ||
426 | { | ||
427 | if (ConnectingBots) | ||
428 | { | ||
429 | MainConsole.Instance.Output("Still connecting bots. Please wait for previous process to complete."); | ||
430 | return; | ||
431 | } | ||
432 | |||
433 | lock (m_bots) | ||
434 | { | ||
435 | int botsToConnect; | ||
436 | int disconnectedBots = m_bots.Count(b => b.ConnectionState == ConnectionState.Disconnected); | ||
437 | |||
438 | if (cmd.Length == 1) | ||
439 | { | ||
440 | botsToConnect = disconnectedBots; | ||
441 | } | ||
442 | else | ||
443 | { | ||
444 | if (!ConsoleUtil.TryParseConsoleNaturalInt(MainConsole.Instance, cmd[1], out botsToConnect)) | ||
445 | return; | ||
446 | |||
447 | botsToConnect = Math.Min(botsToConnect, disconnectedBots); | ||
448 | } | ||
449 | |||
450 | MainConsole.Instance.OutputFormat("Connecting {0} bots", botsToConnect); | ||
451 | |||
452 | ConnectBots(botsToConnect); | ||
453 | } | ||
454 | } | ||
455 | |||
430 | private void HandleDisconnect(string module, string[] cmd) | 456 | private void HandleDisconnect(string module, string[] cmd) |
431 | { | 457 | { |
432 | lock (m_bots) | 458 | lock (m_bots) |
@@ -461,10 +487,12 @@ namespace pCampBot | |||
461 | 487 | ||
462 | if (thisBot.ConnectionState == ConnectionState.Connected) | 488 | if (thisBot.ConnectionState == ConnectionState.Connected) |
463 | { | 489 | { |
464 | Util.FireAndForget(o => thisBot.shutdown()); | 490 | Util.FireAndForget(o => thisBot.Disconnect()); |
465 | disconnectedBots++; | 491 | disconnectedBots++; |
466 | } | 492 | } |
467 | } | 493 | } |
494 | |||
495 | DisconnectingBots = false; | ||
468 | } | 496 | } |
469 | } | 497 | } |
470 | 498 | ||
diff --git a/OpenSim/Tools/pCampBot/pCampBot.cs b/OpenSim/Tools/pCampBot/pCampBot.cs index e58b130..ada39ee 100644 --- a/OpenSim/Tools/pCampBot/pCampBot.cs +++ b/OpenSim/Tools/pCampBot/pCampBot.cs | |||
@@ -95,7 +95,8 @@ namespace pCampBot | |||
95 | 95 | ||
96 | int botcount = commandLineConfig.GetInt("botcount", 1); | 96 | int botcount = commandLineConfig.GetInt("botcount", 1); |
97 | 97 | ||
98 | bm.dobotStartup(botcount, commandLineConfig); | 98 | bm.CreateBots(botcount, commandLineConfig); |
99 | bm.ConnectBots(botcount); | ||
99 | 100 | ||
100 | while (true) | 101 | while (true) |
101 | { | 102 | { |