aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Region/Caches/AssetCache.cs
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Region/Caches/AssetCache.cs')
-rw-r--r--OpenSim/Region/Caches/AssetCache.cs670
1 files changed, 670 insertions, 0 deletions
diff --git a/OpenSim/Region/Caches/AssetCache.cs b/OpenSim/Region/Caches/AssetCache.cs
new file mode 100644
index 0000000..d0cc370
--- /dev/null
+++ b/OpenSim/Region/Caches/AssetCache.cs
@@ -0,0 +1,670 @@
1/*
2* Copyright (c) Contributors, http://www.openmetaverse.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
29using System;
30using System.Collections.Generic;
31using System.Threading;
32using System.Reflection;
33using libsecondlife;
34using libsecondlife.Packets;
35using OpenSim.Framework.Interfaces;
36using OpenSim.Framework.Types;
37using OpenSim.Framework.Utilities;
38
39namespace OpenSim.Caches
40{
41 public delegate void DownloadComplete(AssetCache.TextureSender sender);
42
43 /// <summary>
44 /// Manages local cache of assets and their sending to viewers.
45 /// </summary>
46 public class AssetCache : IAssetReceiver
47 {
48 public Dictionary<libsecondlife.LLUUID, AssetInfo> Assets;
49 public Dictionary<libsecondlife.LLUUID, TextureImage> Textures;
50
51 public List<AssetRequest> AssetRequests = new List<AssetRequest>(); //assets ready to be sent to viewers
52 public List<AssetRequest> TextureRequests = new List<AssetRequest>(); //textures ready to be sent
53
54 public Dictionary<LLUUID, AssetRequest> RequestedAssets = new Dictionary<LLUUID, AssetRequest>(); //Assets requested from the asset server
55 public Dictionary<LLUUID, AssetRequest> RequestedTextures = new Dictionary<LLUUID, AssetRequest>(); //Textures requested from the asset server
56
57 public Dictionary<LLUUID, TextureSender> SendingTextures = new Dictionary<LLUUID, TextureSender>();
58 private IAssetServer _assetServer;
59 private Thread _assetCacheThread;
60 private LLUUID[] textureList = new LLUUID[5];
61
62 /// <summary>
63 ///
64 /// </summary>
65 public AssetCache(IAssetServer assetServer)
66 {
67 Console.WriteLine("Creating Asset cache");
68 _assetServer = assetServer;
69 _assetServer.SetReceiver(this);
70 Assets = new Dictionary<libsecondlife.LLUUID, AssetInfo>();
71 Textures = new Dictionary<libsecondlife.LLUUID, TextureImage>();
72 this._assetCacheThread = new Thread(new ThreadStart(RunAssetManager));
73 this._assetCacheThread.IsBackground = true;
74 this._assetCacheThread.Start();
75
76 }
77
78 public AssetCache(string assetServerDLLName, string assetServerURL, string assetServerKey)
79 {
80 Console.WriteLine("Creating Asset cache");
81 _assetServer = this.LoadAssetDll(assetServerDLLName);
82 _assetServer.SetServerInfo(assetServerURL, assetServerKey);
83 _assetServer.SetReceiver(this);
84 Assets = new Dictionary<libsecondlife.LLUUID, AssetInfo>();
85 Textures = new Dictionary<libsecondlife.LLUUID, TextureImage>();
86 this._assetCacheThread = new Thread(new ThreadStart(RunAssetManager));
87 this._assetCacheThread.IsBackground = true;
88 this._assetCacheThread.Start();
89
90 }
91
92 /// <summary>
93 ///
94 /// </summary>
95 public void RunAssetManager()
96 {
97 while (true)
98 {
99 try
100 {
101 //Console.WriteLine("Asset cache loop");
102 this.ProcessAssetQueue();
103 this.ProcessTextureQueue();
104 Thread.Sleep(500);
105 }
106 catch (Exception e)
107 {
108 Console.WriteLine(e.Message);
109 }
110 }
111 }
112
113 public void LoadDefaultTextureSet()
114 {
115 //hack: so we can give each user a set of textures
116 textureList[0] = new LLUUID("00000000-0000-0000-9999-000000000001");
117 textureList[1] = new LLUUID("00000000-0000-0000-9999-000000000002");
118 textureList[2] = new LLUUID("00000000-0000-0000-9999-000000000003");
119 textureList[3] = new LLUUID("00000000-0000-0000-9999-000000000004");
120 textureList[4] = new LLUUID("00000000-0000-0000-9999-000000000005");
121
122 for (int i = 0; i < textureList.Length; i++)
123 {
124 this._assetServer.RequestAsset(textureList[i], true);
125 }
126
127 }
128
129 public AssetBase[] CreateNewInventorySet(LLUUID agentID)
130 {
131 AssetBase[] inventorySet = new AssetBase[this.textureList.Length];
132 for (int i = 0; i < textureList.Length; i++)
133 {
134 if (this.Textures.ContainsKey(textureList[i]))
135 {
136 inventorySet[i] = this.CloneImage(agentID, this.Textures[textureList[i]]);
137 TextureImage image = new TextureImage(inventorySet[i]);
138 this.Textures.Add(image.FullID, image);
139 this._assetServer.UploadNewAsset(image); //save the asset to the asset server
140 }
141 }
142 return inventorySet;
143 }
144
145 public AssetBase GetAsset(LLUUID assetID)
146 {
147 AssetBase asset = null;
148 if (this.Textures.ContainsKey(assetID))
149 {
150 asset = this.Textures[assetID];
151 }
152 else if (this.Assets.ContainsKey(assetID))
153 {
154 asset = this.Assets[assetID];
155 }
156 return asset;
157 }
158
159 public void AddAsset(AssetBase asset)
160 {
161 // Console.WriteLine("adding asset " + asset.FullID.ToStringHyphenated());
162 if (asset.Type == 0)
163 {
164 //Console.WriteLine("which is a texture");
165 if (!this.Textures.ContainsKey(asset.FullID))
166 { //texture
167 TextureImage textur = new TextureImage(asset);
168 this.Textures.Add(textur.FullID, textur);
169 this._assetServer.UploadNewAsset(asset);
170 }
171 }
172 else
173 {
174 if (!this.Assets.ContainsKey(asset.FullID))
175 {
176 AssetInfo assetInf = new AssetInfo(asset);
177 this.Assets.Add(assetInf.FullID, assetInf);
178 this._assetServer.UploadNewAsset(asset);
179 }
180 }
181 }
182
183 /// <summary>
184 ///
185 /// </summary>
186 private void ProcessTextureQueue()
187 {
188 if (this.TextureRequests.Count == 0)
189 {
190 //no requests waiting
191 return;
192 }
193 int num;
194 num = this.TextureRequests.Count;
195
196 AssetRequest req;
197 for (int i = 0; i < num; i++)
198 {
199 req = (AssetRequest)this.TextureRequests[i];
200 if (!this.SendingTextures.ContainsKey(req.ImageInfo.FullID))
201 {
202 TextureSender sender = new TextureSender(req);
203 sender.OnComplete += this.TextureSent;
204 lock (this.SendingTextures)
205 {
206 this.SendingTextures.Add(req.ImageInfo.FullID, sender);
207 }
208 }
209
210 }
211
212 this.TextureRequests.Clear();
213 }
214
215 /// <summary>
216 /// Event handler, called by a TextureSender object to say that texture has been sent
217 /// </summary>
218 /// <param name="sender"></param>
219 public void TextureSent(AssetCache.TextureSender sender)
220 {
221 if (this.SendingTextures.ContainsKey(sender.request.ImageInfo.FullID))
222 {
223 lock (this.SendingTextures)
224 {
225 this.SendingTextures.Remove(sender.request.ImageInfo.FullID);
226 }
227 }
228 }
229
230 public void AssetReceived(AssetBase asset, bool IsTexture)
231 {
232 if (asset.FullID != LLUUID.Zero) // if it is set to zero then the asset wasn't found by the server
233 {
234 //check if it is a texture or not
235 //then add to the correct cache list
236 //then check for waiting requests for this asset/texture (in the Requested lists)
237 //and move those requests into the Requests list.
238 if (IsTexture)
239 {
240 TextureImage image = new TextureImage(asset);
241 this.Textures.Add(image.FullID, image);
242 if (this.RequestedTextures.ContainsKey(image.FullID))
243 {
244 AssetRequest req = this.RequestedTextures[image.FullID];
245 req.ImageInfo = image;
246 if (image.Data.LongLength > 600)
247 {
248 //over 600 bytes so split up file
249 req.NumPackets = 1 + (int)(image.Data.Length - 600 + 999) / 1000;
250 }
251 else
252 {
253 req.NumPackets = 1;
254 }
255 this.RequestedTextures.Remove(image.FullID);
256 this.TextureRequests.Add(req);
257 }
258 }
259 else
260 {
261 AssetInfo assetInf = new AssetInfo(asset);
262 this.Assets.Add(assetInf.FullID, assetInf);
263 if (this.RequestedAssets.ContainsKey(assetInf.FullID))
264 {
265 AssetRequest req = this.RequestedAssets[assetInf.FullID];
266 req.AssetInf = assetInf;
267 if (assetInf.Data.LongLength > 600)
268 {
269 //over 600 bytes so split up file
270 req.NumPackets = 1 + (int)(assetInf.Data.Length - 600 + 999) / 1000;
271 }
272 else
273 {
274 req.NumPackets = 1;
275 }
276 this.RequestedAssets.Remove(assetInf.FullID);
277 this.AssetRequests.Add(req);
278 }
279 }
280 }
281 }
282
283 public void AssetNotFound(AssetBase asset)
284 {
285 //the asset server had no knowledge of requested asset
286
287 }
288
289 #region Assets
290 /// <summary>
291 ///
292 /// </summary>
293 /// <param name="userInfo"></param>
294 /// <param name="transferRequest"></param>
295 public void AddAssetRequest(IClientAPI userInfo, TransferRequestPacket transferRequest)
296 {
297 LLUUID requestID = new LLUUID(transferRequest.TransferInfo.Params, 0);
298 //check to see if asset is in local cache, if not we need to request it from asset server.
299
300 if (!this.Assets.ContainsKey(requestID))
301 {
302 //not found asset
303 // so request from asset server
304 if (!this.RequestedAssets.ContainsKey(requestID))
305 {
306 AssetRequest request = new AssetRequest();
307 request.RequestUser = userInfo;
308 request.RequestAssetID = requestID;
309 request.TransferRequestID = transferRequest.TransferInfo.TransferID;
310 this.RequestedAssets.Add(requestID, request);
311 this._assetServer.RequestAsset(requestID, false);
312 }
313 return;
314 }
315 //it is in our cache
316 AssetInfo asset = this.Assets[requestID];
317
318 //work out how many packets it should be sent in
319 // and add to the AssetRequests list
320 AssetRequest req = new AssetRequest();
321 req.RequestUser = userInfo;
322 req.RequestAssetID = requestID;
323 req.TransferRequestID = transferRequest.TransferInfo.TransferID;
324 req.AssetInf = asset;
325
326 if (asset.Data.LongLength > 600)
327 {
328 //over 600 bytes so split up file
329 req.NumPackets = 1 + (int)(asset.Data.Length - 600 + 999) / 1000;
330 }
331 else
332 {
333 req.NumPackets = 1;
334 }
335
336 this.AssetRequests.Add(req);
337 }
338
339 /// <summary>
340 ///
341 /// </summary>
342 private void ProcessAssetQueue()
343 {
344 if (this.AssetRequests.Count == 0)
345 {
346 //no requests waiting
347 return;
348 }
349 int num;
350
351 if (this.AssetRequests.Count < 5)
352 {
353 //lower than 5 so do all of them
354 num = this.AssetRequests.Count;
355 }
356 else
357 {
358 num = 5;
359 }
360 AssetRequest req;
361 for (int i = 0; i < num; i++)
362 {
363 req = (AssetRequest)this.AssetRequests[i];
364
365 TransferInfoPacket Transfer = new TransferInfoPacket();
366 Transfer.TransferInfo.ChannelType = 2;
367 Transfer.TransferInfo.Status = 0;
368 Transfer.TransferInfo.TargetType = 0;
369 Transfer.TransferInfo.Params = req.RequestAssetID.GetBytes();
370 Transfer.TransferInfo.Size = (int)req.AssetInf.Data.Length;
371 Transfer.TransferInfo.TransferID = req.TransferRequestID;
372 req.RequestUser.OutPacket(Transfer);
373
374 if (req.NumPackets == 1)
375 {
376 TransferPacketPacket TransferPacket = new TransferPacketPacket();
377 TransferPacket.TransferData.Packet = 0;
378 TransferPacket.TransferData.ChannelType = 2;
379 TransferPacket.TransferData.TransferID = req.TransferRequestID;
380 TransferPacket.TransferData.Data = req.AssetInf.Data;
381 TransferPacket.TransferData.Status = 1;
382 req.RequestUser.OutPacket(TransferPacket);
383 }
384 else
385 {
386 //more than one packet so split file up , for now it can't be bigger than 2000 bytes
387 TransferPacketPacket TransferPacket = new TransferPacketPacket();
388 TransferPacket.TransferData.Packet = 0;
389 TransferPacket.TransferData.ChannelType = 2;
390 TransferPacket.TransferData.TransferID = req.TransferRequestID;
391 byte[] chunk = new byte[1000];
392 Array.Copy(req.AssetInf.Data, chunk, 1000);
393 TransferPacket.TransferData.Data = chunk;
394 TransferPacket.TransferData.Status = 0;
395 req.RequestUser.OutPacket(TransferPacket);
396
397 TransferPacket = new TransferPacketPacket();
398 TransferPacket.TransferData.Packet = 1;
399 TransferPacket.TransferData.ChannelType = 2;
400 TransferPacket.TransferData.TransferID = req.TransferRequestID;
401 byte[] chunk1 = new byte[(req.AssetInf.Data.Length - 1000)];
402 Array.Copy(req.AssetInf.Data, 1000, chunk1, 0, chunk1.Length);
403 TransferPacket.TransferData.Data = chunk1;
404 TransferPacket.TransferData.Status = 1;
405 req.RequestUser.OutPacket(TransferPacket);
406 }
407
408 }
409
410 //remove requests that have been completed
411 for (int i = 0; i < num; i++)
412 {
413 this.AssetRequests.RemoveAt(0);
414 }
415
416 }
417
418 public AssetInfo CloneAsset(LLUUID newOwner, AssetInfo sourceAsset)
419 {
420 AssetInfo newAsset = new AssetInfo();
421 newAsset.Data = new byte[sourceAsset.Data.Length];
422 Array.Copy(sourceAsset.Data, newAsset.Data, sourceAsset.Data.Length);
423 newAsset.FullID = LLUUID.Random();
424 newAsset.Type = sourceAsset.Type;
425 newAsset.InvType = sourceAsset.InvType;
426 return (newAsset);
427 }
428 #endregion
429
430 #region Textures
431 /// <summary>
432 ///
433 /// </summary>
434 /// <param name="userInfo"></param>
435 /// <param name="imageID"></param>
436 public void AddTextureRequest(IClientAPI userInfo, LLUUID imageID)
437 {
438 //Console.WriteLine("texture request for " + imageID.ToStringHyphenated());
439 //check to see if texture is in local cache, if not request from asset server
440 if (!this.Textures.ContainsKey(imageID))
441 {
442 if (!this.RequestedTextures.ContainsKey(imageID))
443 {
444 //not is cache so request from asset server
445 AssetRequest request = new AssetRequest();
446 request.RequestUser = userInfo;
447 request.RequestAssetID = imageID;
448 request.IsTextureRequest = true;
449 this.RequestedTextures.Add(imageID, request);
450 this._assetServer.RequestAsset(imageID, true);
451 }
452 return;
453 }
454
455 //Console.WriteLine("texture already in cache");
456 TextureImage imag = this.Textures[imageID];
457 AssetRequest req = new AssetRequest();
458 req.RequestUser = userInfo;
459 req.RequestAssetID = imageID;
460 req.IsTextureRequest = true;
461 req.ImageInfo = imag;
462
463 if (imag.Data.LongLength > 600)
464 {
465 //over 600 bytes so split up file
466 req.NumPackets = 1 + (int)(imag.Data.Length - 600 + 999) / 1000;
467 }
468 else
469 {
470 req.NumPackets = 1;
471 }
472 this.TextureRequests.Add(req);
473 }
474
475 public TextureImage CloneImage(LLUUID newOwner, TextureImage source)
476 {
477 TextureImage newImage = new TextureImage();
478 newImage.Data = new byte[source.Data.Length];
479 Array.Copy(source.Data, newImage.Data, source.Data.Length);
480 //newImage.filename = source.filename;
481 newImage.FullID = LLUUID.Random();
482 newImage.Name = source.Name;
483 return (newImage);
484 }
485 #endregion
486
487 private IAssetServer LoadAssetDll(string dllName)
488 {
489 Assembly pluginAssembly = Assembly.LoadFrom(dllName);
490 IAssetServer server = null;
491
492 foreach (Type pluginType in pluginAssembly.GetTypes())
493 {
494 if (pluginType.IsPublic)
495 {
496 if (!pluginType.IsAbstract)
497 {
498 Type typeInterface = pluginType.GetInterface("IAssetPlugin", true);
499
500 if (typeInterface != null)
501 {
502 IAssetPlugin plug = (IAssetPlugin)Activator.CreateInstance(pluginAssembly.GetType(pluginType.ToString()));
503 server = plug.GetAssetServer();
504 break;
505 }
506
507 typeInterface = null;
508 }
509 }
510 }
511 pluginAssembly = null;
512 return server;
513 }
514
515 public class AssetRequest
516 {
517 public IClientAPI RequestUser;
518 public LLUUID RequestAssetID;
519 public AssetInfo AssetInf;
520 public TextureImage ImageInfo;
521 public LLUUID TransferRequestID;
522 public long DataPointer = 0;
523 public int NumPackets = 0;
524 public int PacketCounter = 0;
525 public bool IsTextureRequest;
526 //public bool AssetInCache;
527 //public int TimeRequested;
528
529 public AssetRequest()
530 {
531
532 }
533 }
534
535 public class AssetInfo : AssetBase
536 {
537 public AssetInfo()
538 {
539
540 }
541
542 public AssetInfo(AssetBase aBase)
543 {
544 Data = aBase.Data;
545 FullID = aBase.FullID;
546 Type = aBase.Type;
547 InvType = aBase.InvType;
548 Name = aBase.Name;
549 Description = aBase.Description;
550 }
551 }
552
553 public class TextureImage : AssetBase
554 {
555 public TextureImage()
556 {
557
558 }
559
560 public TextureImage(AssetBase aBase)
561 {
562 Data = aBase.Data;
563 FullID = aBase.FullID;
564 Type = aBase.Type;
565 InvType = aBase.InvType;
566 Name = aBase.Name;
567 Description = aBase.Description;
568 }
569 }
570
571 public class TextureSender
572 {
573 public AssetRequest request;
574 public event DownloadComplete OnComplete;
575 Thread m_thread;
576 public TextureSender(AssetRequest req)
577 {
578 request = req;
579 //Console.WriteLine("creating worker thread for texture " + req.ImageInfo.FullID.ToStringHyphenated());
580 //Console.WriteLine("texture data length is " + req.ImageInfo.Data.Length);
581 // Console.WriteLine("in " + req.NumPackets + " packets");
582 //ThreadPool.QueueUserWorkItem(new WaitCallback(SendTexture), new object());
583
584 //need some sort of custom threadpool here, as using the .net one, overloads it and stops the handling of incoming packets etc
585 //but don't really want to create a thread for every texture download
586 m_thread = new Thread(new ThreadStart(SendTexture));
587 m_thread.IsBackground = true;
588 m_thread.Start();
589 }
590
591 public void SendTexture()
592 {
593 //Console.WriteLine("starting to send sending texture " + request.ImageInfo.FullID.ToStringHyphenated());
594 while (request.PacketCounter != request.NumPackets)
595 {
596 SendPacket();
597 Thread.Sleep(500);
598 }
599
600 //Console.WriteLine("finished sending texture " + request.ImageInfo.FullID.ToStringHyphenated());
601 if (OnComplete != null)
602 {
603 OnComplete(this);
604 }
605 }
606
607 public void SendPacket()
608 {
609 AssetRequest req = request;
610 // Console.WriteLine("sending " + req.ImageInfo.FullID);
611
612 // if (req.ImageInfo.FullID == new LLUUID("00000000-0000-0000-5005-000000000005"))
613 if (req.PacketCounter == 0)
614 {
615 //first time for this request so send imagedata packet
616 if (req.NumPackets == 1)
617 {
618 //only one packet so send whole file
619 ImageDataPacket im = new ImageDataPacket();
620 im.ImageID.Packets = 1;
621 im.ImageID.ID = req.ImageInfo.FullID;
622 im.ImageID.Size = (uint)req.ImageInfo.Data.Length;
623 im.ImageData.Data = req.ImageInfo.Data;
624 im.ImageID.Codec = 2;
625 req.RequestUser.OutPacket(im);
626 req.PacketCounter++;
627 //req.ImageInfo.l= time;
628 //System.Console.WriteLine("sent texture: " + req.ImageInfo.FullID);
629 // Console.WriteLine("sending packet 1 for " + req.ImageInfo.FullID.ToStringHyphenated());
630 }
631 else
632 {
633 //more than one packet so split file up
634 ImageDataPacket im = new ImageDataPacket();
635 im.ImageID.Packets = (ushort)req.NumPackets;
636 im.ImageID.ID = req.ImageInfo.FullID;
637 im.ImageID.Size = (uint)req.ImageInfo.Data.Length;
638 im.ImageData.Data = new byte[600];
639 Array.Copy(req.ImageInfo.Data, 0, im.ImageData.Data, 0, 600);
640 im.ImageID.Codec = 2;
641 req.RequestUser.OutPacket(im);
642 req.PacketCounter++;
643 //req.ImageInfo.last_used = time;
644 //System.Console.WriteLine("sent first packet of texture:
645 // Console.WriteLine("sending packet 1 for " + req.ImageInfo.FullID.ToStringHyphenated());
646 }
647 }
648 else
649 {
650 //Console.WriteLine("sending packet" + req.PacketCounter + "for " + req.ImageInfo.FullID.ToStringHyphenated());
651 //send imagepacket
652 //more than one packet so split file up
653 ImagePacketPacket im = new ImagePacketPacket();
654 im.ImageID.Packet = (ushort)req.PacketCounter;
655 im.ImageID.ID = req.ImageInfo.FullID;
656 int size = req.ImageInfo.Data.Length - 600 - 1000 * (req.PacketCounter - 1);
657 if (size > 1000) size = 1000;
658 im.ImageData.Data = new byte[size];
659 Array.Copy(req.ImageInfo.Data, 600 + 1000 * (req.PacketCounter - 1), im.ImageData.Data, 0, size);
660 req.RequestUser.OutPacket(im);
661 req.PacketCounter++;
662 //req.ImageInfo.last_used = time;
663 //System.Console.WriteLine("sent a packet of texture: "+req.image_info.FullID);
664 }
665
666 }
667 }
668 }
669}
670