evePhotosvc.py (42850B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\services\evePhotosvc.py 2 import datetime 3 import numbers 4 import os 5 import rfc822 6 import shutil 7 import sys 8 import time 9 import logging 10 from carbon.common.script.sys.service import Service 11 from collections import defaultdict 12 import blue 13 import evetypes 14 import inventorycommon.const as const 15 import inventorycommon.typeHelpers 16 import localization 17 import requests 18 import trinity 19 import uthread 20 from carbon.common.script.util import commonutils, logUtil as log 21 from carbon.common.script.sys.serviceConst import ROLE_PROGRAMMER 22 from carbonui.control.browser import browserutil 23 from carbonui.uicore import uicore 24 from carbonui.util.various_unsorted import GetBuffersize 25 from eve.client.script.ui.shared.maps.map2D import Map2D 26 from eve.common.script.sys import idCheckers 27 from evephotosvc.const import NONE_PATH 28 from fsdBuiltData.common import graphicIDs 29 from iconrendering2.const import IconRenderFormat, IconStyle, STYLE_STRINGS, BACKGROUND_PATHS 30 from iconrendering2.factory.recipe import GetBlueprintBackgroundAndOverlay 31 from iconrendering2.renderers.renderer_base import IconRenderInfo 32 from iconrendering2.renderers.renderer_fromfile import FromFileIconRenderer 33 from iconrendering2.utils import GetIconFileFromSheet 34 from industry.const import REACTION_GROUPS 35 from evephotosvc.scenePortraitQueue import ScenePortraitQueue 36 from globalConfig import GetImageServerMissingImageRetryDelay, IMAGE_SERVER_MISSING_IMAGE_RETRY_DELAY_IN_SECONDS_DEFAULT 37 from monolithconfig import add_global_config_callback 38 logger = logging.getLogger(__name__) 39 NOT_AVAILABLE_PATH = 'res:/UI/Texture/notavailable.dds' 40 MAX_PORTRAIT_THREADS = 5 41 MAX_CACHE_AGE = 60 * 60 * 1 42 DEFAULT_PORTRAIT_SIZE = 512 43 DEFAULT_PORTRAIT_SAVE_SIZE = 1024 44 PORTRAIT_SIZES = (32, 45 64, 46 128, 47 256, 48 512) 49 CORP_ALLIANCE_SIZES = (64, 128, 512) 50 DEFAULT_MARKETING_TEST_IMAGE_SERVER = 'http://cdn1.eveonline.com/marketing/InGameVirtualGoodsStore/TestServersImages/' 51 52 def GetPictureFileName(typeID, graphicID, size): 53 shaderModel = trinity.GetShaderModel() 54 name = '%s_%s_%s_%s_%s.dds' % (shaderModel, 55 graphicID or 0, 56 evetypes.GetIconID(typeID) or 0, 57 evetypes.GetRaceID(typeID) or 0, 58 size) 59 return name 60 61 62 def GetCachePath(typeID, graphicID, size, itemID, blueprint = None): 63 if blueprint == IconStyle.BP_COPY: 64 return u'cache:/Pictures/Blueprints/bpc_%s' % GetPictureFileName(typeID, graphicID, size) 65 elif blueprint == IconStyle.BP_RELIC: 66 return u'cache:/Pictures/Blueprints/relic_%s' % GetPictureFileName(typeID, graphicID, size) 67 elif blueprint == IconStyle.BP_ORIGINAL: 68 return u'cache:/Pictures/Blueprints/bp_%s' % GetPictureFileName(typeID, graphicID, size) 69 elif blueprint == IconStyle.BP_DUST: 70 return u'cache:/Pictures/Blueprints/dust_%s_%s_%s.dds' % (trinity.GetShaderModel(), typeID, size) 71 else: 72 return u'cache:/Pictures/Gids/' + GetPictureFileName(typeID, graphicID, size) 73 74 75 def GetRenderPath(graphicID, size, blueprint): 76 if size > 64: 77 size = 128 78 else: 79 size = 64 80 if blueprint == IconStyle.BP_COPY: 81 fileName = '%s_%s_%s.png' % (graphicID, 64, STYLE_STRINGS[blueprint].upper()) 82 elif blueprint == IconStyle.BP_ORIGINAL: 83 fileName = '%s_%s_%s.png' % (graphicID, 64, STYLE_STRINGS[blueprint].upper()) 84 else: 85 fileName = '%s_%s.png' % (graphicID, size) 86 iconFolder = graphicIDs.GetIconFolder(graphicID) 87 if iconFolder is not None: 88 return iconFolder + '/' + fileName 89 return fileName 90 91 92 def DoLogIt(path): 93 if not commonutils.IsFullLogging() and 'portrait' in path.lower(): 94 return False 95 return True 96 97 98 class EvePhoto(Service): 99 __guid__ = 'svc.photo' 100 __exportedcalls__ = {'GetPortrait': [], 101 'AddPortrait': [], 102 'GetAllianceLogo': [], 103 'GetCorporationLogo': [], 104 'OrderByTypeID': [], 105 'CheckAvail': [], 106 'GetTextureFromURL': [], 107 'CheckDates': [], 108 'GetIconByType': [], 109 'SavePortraits': []} 110 __startupdependencies__ = ['settings', 'machoNet'] 111 __notifyevents__ = ['OnSessionChanged'] 112 113 def __init__(self): 114 super(EvePhoto, self).__init__() 115 self.notavail = {} 116 self.imageServerQueue = defaultdict(list) 117 self.currentlyFetching = defaultdict(list) 118 self.fetchingFromImageServer = 0 119 self.portraitregistrations = {} 120 self.byTypeIDQue = [] 121 self.byTypeID_IsRunning = 0 122 self.types = {} 123 self.gidque = [] 124 self.gidrunning = 0 125 self.gidcount = 0 126 self.faceoptions = {} 127 self.chappcache = None 128 self.photoscene = None 129 self.CheckDates(blue.paths.ResolvePathForWriting(u'cache:/Browser/Img')) 130 self.CheckDates(blue.paths.ResolvePathForWriting(u'cache:/Pictures/Portraits')) 131 self.CheckDates(blue.paths.ResolvePathForWriting(u'cache:/Pictures/Gids')) 132 self.urlloading = {} 133 self.pendingPI = {} 134 self.portraits = None 135 self.allianceLogos = None 136 self.corpLogos = None 137 self.storeBannerImages = None 138 self.pendingPortraitGeneration = set() 139 self._scenePortraitQueue = ScenePortraitQueue() 140 self.on_scene_portrait_rendered = self._scenePortraitQueue.on_portrait_rendered 141 self.defaultImageServerForUser = None 142 self.defaultMarketingImages = None 143 if not blue.pyos.packaged: 144 username = os.environ.get('USERNAME') 145 if username is None: 146 username = os.environ.get('USER') 147 if username is not None: 148 self.defaultImageServerForUser = 'http://%s.dev.image/' % username.replace('.', '_').lower() 149 self.LogInfo('Guessing ImageServer url as we are not in a build client: ', self.defaultImageServerForUser) 150 self.defaultMarketingImages = DEFAULT_MARKETING_TEST_IMAGE_SERVER 151 152 def OnSessionChanged(self, isRemote, session, change): 153 if 'charid' or 'corpid' in change: 154 if session.corpid: 155 for size in CORP_ALLIANCE_SIZES: 156 self.GetCorporationLogo(session.corpid, size) 157 158 if 'charid' or 'allianceid' in change: 159 if session.allianceid: 160 for size in CORP_ALLIANCE_SIZES: 161 self.GetAllianceLogo(session.allianceid, size) 162 163 def CheckAvail(self, path): 164 pathLog = path if DoLogIt(path) else '' 165 self.LogInfo('CheckAvail ', pathLog) 166 if path in self.notavail: 167 self.LogInfo('CheckAvail ', pathLog, ' is in notavail') 168 return None 169 elif blue.paths.exists(path): 170 self.LogInfo('CheckAvail ', pathLog, ' exists') 171 return path 172 else: 173 self.LogInfo('CheckAvail ', pathLog, " doesn't exist, and has been added to notavail") 174 self.notavail[path] = 1 175 return None 176 177 def GetValidBlueprintPath(self, sprite, typeID, size = 64, blueprint = None): 178 graphicID = evetypes.GetGraphicID(typeID) 179 cachePath = GetCachePath(typeID, graphicID, size, None, blueprint) 180 resFile = blue.ResFile() 181 try: 182 if resFile.Open(cachePath): 183 self.LogInfo('GetValidBlueprintPath ', cachePath, ' exists') 184 return cachePath 185 finally: 186 del resFile 187 188 graphic = inventorycommon.typeHelpers.GetIcon(typeID) 189 if graphic and graphic.iconFile: 190 iconPath = graphic.iconFile.strip() 191 iconPath = GetIconFileFromSheet(iconPath) 192 else: 193 iconPath = sprite.texture.resPath 194 background, overlay = GetBlueprintBackgroundAndOverlay(blueprint if blueprint else IconStyle.BP_ORIGINAL) 195 uthread.new(self.RenderBlueprint, sprite, cachePath, size, iconPath, background, overlay) 196 self._RemovePathFromNotAvailList(cachePath) 197 return cachePath 198 199 def RenderBlueprint(self, sprite, cachePath, size, iconPath, background, overlay): 200 renderInfo = IconRenderInfo(os.path.dirname(cachePath), os.path.basename(cachePath), size, IconRenderFormat.PNG) 201 renderInfo.background = BACKGROUND_PATHS.get(background, None) 202 renderInfo.overlay = overlay 203 renderInfo.inputIcon = iconPath 204 renderer = FromFileIconRenderer() 205 renderer.useAsyncSave = False 206 renderer.AddRenderInfo(renderInfo) 207 renderer.Render() 208 sprite.ReloadTexture() 209 210 def DoBlueprint(self, sprite, typeID, size = 64, blueprint = None): 211 if not sprite.texture: 212 return 213 texture = sprite.texture.resPath 214 bpPath = self.GetValidBlueprintPath(sprite, typeID, size, blueprint) 215 if sprite.texture.resPath == texture: 216 sprite.texture.resPath = bpPath 217 sprite.rectLeft = 0 218 sprite.rectWidth = 0 219 sprite.rectTop = 0 220 sprite.rectHeight = 0 221 222 def GetTextureFromURL(self, path, currentURL = None, ignoreCache = 0, dontcache = 0, fromWhere = None, sizeonly = 0, retry = 1): 223 if path.endswith('.blue'): 224 return self.GetPic_blue(path) 225 fullPath = browserutil.ParseURL(path, currentURL)[0] 226 if path.startswith('res:'): 227 try: 228 surface = trinity.Tr2HostBitmap() 229 surface.CreateFromFile(path) 230 w, h = surface.width, surface.height 231 bw, bh = GetBuffersize(w), GetBuffersize(h) 232 if sizeonly: 233 return (path, 234 w, 235 h, 236 bw, 237 bh) 238 return self.ReturnTexture(path, w, h, bw, bh) 239 except: 240 self.LogError('Failed to load image', path) 241 if fullPath in self.urlloading: 242 del self.urlloading[fullPath] 243 sys.exc_clear() 244 return self.ErrorPic(sizeonly) 245 246 if ignoreCache: 247 sm.GetService('imageCache').InvalidateImage(fullPath) 248 while fullPath in self.urlloading: 249 blue.pyos.BeNice() 250 251 if not dontcache: 252 cacheData = sm.GetService('imageCache').GetFromCache(fullPath) 253 if cacheData and os.path.exists(cacheData[0].replace('cache:/', blue.paths.ResolvePath(u'cache:/'))): 254 if sizeonly: 255 return cacheData 256 return self.ReturnTexture(*cacheData) 257 try: 258 self.urlloading[fullPath] = 1 259 ret = requests.get(fullPath) 260 cacheID = int(str(blue.os.GetWallclockTime()) + str(uthread.uniqueId() or uthread.uniqueId())) 261 imagestream = ret.content 262 ret.headers = {key.lower():value for key, value in ret.headers.iteritems()} 263 ext = None 264 if 'content-type' in ret.headers.keys() and ret.headers['content-type'].startswith('image/'): 265 ext = ret.headers['content-type'][6:] 266 if ext == None or ext == 'png': 267 header = imagestream[:16] 268 for sig, sext in [('PNG', 'PNG'), 269 ('GIF', 'GIF'), 270 ('JFI', 'JPEG'), 271 ('BM8', 'BMP')]: 272 for i in xrange(0, 12): 273 if header[i:i + 3] == sig: 274 ext = sext 275 break 276 277 if not ext: 278 header = imagestream[-16:] 279 for sig, sext in [('XFILE', 'TGA')]: 280 for i in xrange(0, 10): 281 if header[i:i + 5] == sig: 282 ext = sext 283 break 284 285 if ext: 286 filename = '%sBrowser/Img/%s.%s' % (blue.paths.ResolvePath(u'cache:/'), cacheID, ext) 287 resfile = blue.classes.CreateInstance('blue.ResFile') 288 if not resfile.Open(filename, 0): 289 resfile.Create(filename) 290 resfile.Write(imagestream) 291 resfile.Close() 292 surface = trinity.Tr2HostBitmap() 293 surface.CreateFromFile(filename) 294 w, h = surface.width, surface.height 295 bw, bh = GetBuffersize(w), GetBuffersize(h) 296 cachePath = 'cache:/Browser/Img/%s.%s' % (cacheID, ext) 297 if 'pragma' not in ret.headers.keys() or ret.headers['Pragma'].find('no-cache') == -1: 298 sm.GetService('imageCache').Cache(fullPath, (cachePath, 299 w, 300 h, 301 bw, 302 bh)) 303 del self.urlloading[fullPath] 304 if sizeonly: 305 return (cachePath, 306 w, 307 h, 308 bw, 309 bh) 310 return self.ReturnTexture(cachePath, w, h, bw, bh) 311 del self.urlloading[fullPath] 312 return self.ErrorPic(sizeonly) 313 except Exception as e: 314 if retry: 315 sys.exc_clear() 316 if fullPath in self.urlloading: 317 del self.urlloading[fullPath] 318 return self.GetTextureFromURL(path, currentURL, ignoreCache, dontcache, fromWhere, sizeonly, 0) 319 self.LogError(e, 'Failed to load image', repr(path)) 320 if fullPath in self.urlloading: 321 del self.urlloading[fullPath] 322 sys.exc_clear() 323 return self.ErrorPic(sizeonly) 324 325 def ErrorPic(self, sizeonly = 0): 326 if sizeonly: 327 return (NONE_PATH, 328 32, 329 32, 330 32, 331 32) 332 tex = trinity.Tr2Sprite2dTexture() 333 tex.resPath = NONE_PATH 334 return (tex, 32, 32) 335 336 def CleanExt(self, ext): 337 validNot3Letters = ('jpeg',) 338 for _ext in validNot3Letters: 339 if ext.lower().startswith(_ext): 340 return _ext 341 342 return ext[:3] 343 344 def ReturnTexture(self, path, width, height, bufferwidth, bufferheight): 345 tex = trinity.Tr2Sprite2dTexture() 346 tex.resPath = path 347 return (tex, width, height) 348 349 def CheckDates(self, path): 350 now = long(time.time()) 351 rem = [] 352 for fileName in os.listdir(path): 353 if fileName.split('.')[-1] in ('blue', 'dat', 'txt'): 354 continue 355 try: 356 lastRead = os.path.getatime(path + '/' + fileName) 357 except OSError as e: 358 continue 359 360 age = now - lastRead 361 if age / 2592000: 362 rem.append(fileName) 363 364 for each in rem: 365 try: 366 os.remove(path + '/' + each) 367 except OSError as e: 368 continue 369 370 def InitializePortraits(self): 371 if self.portraits is None: 372 imageServer = self.GetImageServerURL('imageserverurl', self.defaultImageServerForUser) 373 self.portraits = RemoteImageCacher('Character', self, '.jpg', imageServer, self.machoNet) 374 self.pendingPortraitGeneration = settings.user.ui.Get('pendingPortraitGeneration', set()) 375 for charID in self.pendingPortraitGeneration: 376 if charID is None: 377 continue 378 self.LogInfo('Character', charID, 'is marked as newly customized.') 379 self.portraits.AddToWatchList(charID, self.PortraitDownloaded) 380 for size in PORTRAIT_SIZES: 381 if size == DEFAULT_PORTRAIT_SIZE: 382 continue 383 self.portraits.RemoveFromCache(charID, size) 384 385 def PortraitDownloaded(self, charID): 386 self.pendingPortraitGeneration = settings.user.ui.Get('pendingPortraitGeneration', set()) 387 if charID in self.pendingPortraitGeneration: 388 self.pendingPortraitGeneration.discard(charID) 389 settings.user.ui.Set('pendingPortraitGeneration', self.pendingPortraitGeneration) 390 391 def GetPortrait(self, charID, size, sprite = None, orderIfMissing = True, callback = False, allowServerTrip = False, blocking = False, fullPoseSetup = True): 392 self.InitializePortraits() 393 size = self.FindBestPortraitSize(size) 394 defaultIcon = self.GetPortraitDefaultIcon(size) 395 if charID in self.pendingPortraitGeneration: 396 size = DEFAULT_PORTRAIT_SIZE 397 callback = 'OnPortraitCreated' if callback else None 398 path = self.GetImage(charID, size, self.portraits, sprite, orderIfMissing, callback, defaultIcon) 399 if path == defaultIcon: 400 if session.role & ROLE_PROGRAMMER: 401 cachedPath = self._GetCachedScenePortrait(charID, size) 402 if cachedPath: 403 if sprite is not None: 404 sprite.LoadTexture(cachedPath) 405 return cachedPath 406 if blocking: 407 path = self.RenderPortraitFromScene(charID, size, fullPoseSetup=True) 408 if path: 409 if sprite is not None: 410 sprite.LoadTexture(path) 411 return path 412 else: 413 self._QueueScenePortrait(charID, size, sprite, callback) 414 return 415 else: 416 return path 417 418 def _GetCachedScenePortrait(self, charID, size): 419 return self._scenePortraitQueue.GetCachedPortrait(charID, size) 420 421 def _QueueScenePortrait(self, charID, size, sprite, callback): 422 self._scenePortraitQueue.Enqueue(charID, size, sprite) 423 424 def GetScenePortraitCacheDir(self): 425 return self._scenePortraitQueue.GetCacheDir() 426 427 def InvalidateScenePortrait(self, charID): 428 self._scenePortraitQueue.InvalidatePortrait(charID, PORTRAIT_SIZES) 429 430 def ClearScenePortraitCache(self): 431 self._scenePortraitQueue.ClearCache() 432 433 def GetPortraitDefaultIcon(self, size): 434 if size > 64: 435 defaultIcon = 'res:/UI/Texture/silhouette.png' 436 else: 437 defaultIcon = 'res:/UI/Texture/silhouette_64.png' 438 return defaultIcon 439 440 def SavePortraits(self, charIDs): 441 if type(charIDs) != list: 442 charIDs = [charIDs] 443 uthread.new(self._SavePortraits, charIDs) 444 445 def GetPortraitSaveSize(self): 446 portraitSize = sm.GetService('machoNet').GetGlobalConfig().get('defaultPortraitSaveSize') 447 if portraitSize is None: 448 portraitSize = DEFAULT_PORTRAIT_SAVE_SIZE 449 self.LogInfo('Portrait will be saved at', portraitSize) 450 return int(portraitSize) 451 452 def _SavePortraits(self, charIDs): 453 length = len(charIDs) 454 sm.GetService('loading').ProgressWnd(localization.GetByLabel('UI/Shared/GeneratingPicture'), '', 0, length) 455 portraitSaveSize = self.GetPortraitSaveSize() 456 for i, charID in enumerate(charIDs): 457 imagePath = self.portraits.GetImage(charID, portraitSaveSize, forceUpdate=True) 458 if imagePath: 459 cacheFile = blue.paths.ResolvePath(imagePath) 460 try: 461 shutil.copy2(cacheFile, blue.sysinfo.GetUserDocumentsDirectory() + '/EVE/capture/Portraits/%s.jpg' % charID) 462 except WindowsError as e: 463 log.LogTraceback('Failed to copy character image') 464 self.LogError(e, 'Failed to copy character image') 465 466 sm.GetService('loading').ProgressWnd(localization.GetByLabel('UI/Shared/GeneratingPicture'), '', i + 1, length) 467 468 sm.GetService('loading').ProgressWnd(localization.GetByLabel('UI/Shared/GeneratingPicture'), '', length, length) 469 470 def GetAllianceLogo(self, allianceID, size, sprite = None, orderIfMissing = True, callback = False): 471 if self.allianceLogos is None: 472 imageServer = self.GetImageServerURL('imageserverurl', self.defaultImageServerForUser) 473 self.allianceLogos = RemoteImageCacher('Alliance', self, '.png', imageServer, self.machoNet) 474 callback = 'OnAllianceLogoReady' if callback else None 475 img = self.GetImage(allianceID, size, self.allianceLogos, sprite, orderIfMissing, callback, 'res:/UI/Texture/defaultAlliance.dds', isCorpOrAlliance=True) 476 return img 477 478 def GetCorporationLogo(self, corpID, size, sprite = None, orderIfMissing = True, callback = False): 479 if self.corpLogos is None: 480 imageServer = self.GetImageServerURL('imageserverurl', self.defaultImageServerForUser) 481 self.corpLogos = RemoteImageCacher('Corporation', self, '.png', imageServer, self.machoNet) 482 callback = 'OnCorpLogoReady' if callback else None 483 img = self.GetImage(corpID, size, self.corpLogos, sprite, orderIfMissing, callback, 'res:/UI/Texture/defaultCorporation.dds', isCorpOrAlliance=True) 484 return img 485 486 def GetImage(self, itemID, size, handler, sprite = None, orderIfMissing = True, callback = None, defaultIcon = 'res:/UI/Texture/notavailable.dds', isCorpOrAlliance = False): 487 if uicore.desktop.dpiScaling > 1.0 and not isCorpOrAlliance: 488 size = size * 2 489 if not isinstance(itemID, numbers.Integral): 490 return defaultIcon 491 path, isFresh = handler.GetCachedImage(itemID, size) 492 if sprite is not None: 493 sprite.LoadTexture(path or defaultIcon) 494 if not isFresh and orderIfMissing and not handler.MissingFromServer(itemID, size): 495 if (itemID, size, handler) in self.currentlyFetching: 496 self.currentlyFetching[itemID, size, handler].append([sprite, callback]) 497 else: 498 self.imageServerQueue[itemID, size, handler].append([sprite, callback]) 499 if len(self.imageServerQueue) > self.fetchingFromImageServer and self.fetchingFromImageServer < MAX_PORTRAIT_THREADS: 500 self.fetchingFromImageServer += 1 501 uthread.pool('photo::FetchRemoteImages', self.__FetchFromImageServer) 502 elif handler.MissingFromServer(itemID, size) and path is None: 503 return defaultIcon 504 if isFresh: 505 return path 506 if path is not None: 507 resolvedPath = blue.paths.ResolvePath(handler.GetCachePath(itemID, size)) 508 if os.path.exists(resolvedPath): 509 return path 510 return defaultIcon 511 512 def __FetchFromImageServer(self): 513 self.LogInfo('Starting a image server thread') 514 try: 515 while len(self.imageServerQueue) > 0: 516 (itemID, size, handler), orders = self.imageServerQueue.popitem() 517 image = handler.GetImage(itemID, size) 518 self.currentlyFetching[itemID, size, handler] = [] 519 if image is None: 520 continue 521 orders += self.currentlyFetching.pop((itemID, size, handler)) 522 for sprite, callback in orders: 523 if callback: 524 sm.ScatterEvent(callback, itemID, size) 525 elif sprite is not None: 526 try: 527 sprite.LoadTexture(image) 528 except: 529 log.LogException('Error adding portrait to sprite') 530 sys.exc_clear() 531 532 finally: 533 self.fetchingFromImageServer -= 1 534 535 def AddPortrait(self, portraitPath, charID): 536 self.LogInfo('Adding portrait of', charID, 'from path', portraitPath) 537 try: 538 shutil.copy2(portraitPath, blue.paths.ResolvePathForWriting(u'cache:/Pictures/Characters/%s_%s.jpg' % (charID, DEFAULT_PORTRAIT_SIZE))) 539 except WindowsError as e: 540 log.LogTraceback('Failed to copy default size character image') 541 self.LogError(e, 'Failed to copy default size character image') 542 543 try: 544 shutil.copy2(portraitPath, blue.paths.ResolvePathForWriting(u'cache:/Pictures/Characters/%s_%s.jpg' % (charID, 256))) 545 except WindowsError as e: 546 log.LogTraceback('Failed to copy 256 px character image') 547 self.LogError(e, 'Failed to copy 256 px character image') 548 549 portraitCachePath = 'cache:/Pictures/Characters/%s_%s.jpg' % (charID, DEFAULT_PORTRAIT_SIZE) 550 res = blue.resMan.GetResource(portraitCachePath, 'atlas') 551 res.Reload() 552 portraitCachePath_256 = 'cache:/Pictures/Characters/%s_%s.jpg' % (charID, 256) 553 res = blue.resMan.GetResource(portraitCachePath_256, 'atlas') 554 res.Reload() 555 self.pendingPortraitGeneration = settings.user.ui.Get('pendingPortraitGeneration', set()) 556 self.pendingPortraitGeneration.add(charID) 557 settings.user.ui.Set('pendingPortraitGeneration', self.pendingPortraitGeneration) 558 if self.portraits is not None: 559 self.portraits.missingImages.pop((charID, DEFAULT_PORTRAIT_SIZE), None) 560 self.portraits.missingImages.pop((charID, 256), None) 561 self.portraits.AddToWatchList(charID, self.PortraitDownloaded) 562 563 def _RemovePathFromNotAvailList(self, cachename): 564 try: 565 del self.notavail[cachename] 566 self.LogInfo('Deleted ', cachename, ' from not avail list') 567 except: 568 self.LogInfo(cachename, ' was not in the not avail list') 569 570 def Do2DMap(self, sprite, ids, idLevel, drawLevel, size = 256): 571 ssmap = Map2D() 572 ssmap.Draw(ids, idLevel, drawLevel, size, sprite) 573 574 def GetIconByType(self, sprite, typeID, itemID = None, size = None, ignoreSize = False, isCopy = False): 575 if typeID is None or not evetypes.Exists(typeID): 576 return 577 actSize = size or 64 578 categoryID = evetypes.GetCategoryID(typeID) 579 groupID = evetypes.GetGroupID(typeID) 580 blueprint = None 581 if categoryID == const.categoryBlueprint: 582 if groupID in REACTION_GROUPS: 583 blueprint = IconStyle.BP_REACTION 584 elif isCopy: 585 blueprint = IconStyle.BP_COPY 586 else: 587 blueprint = IconStyle.BP_ORIGINAL 588 elif categoryID == const.categoryInfantry: 589 blueprint = IconStyle.BP_DUST 590 elif categoryID == const.categoryAncientRelic: 591 blueprint = IconStyle.BP_RELIC 592 if itemID and groupID in (const.groupRegion, const.groupConstellation, const.groupSolarSystem): 593 if not (idCheckers.IsSolarSystem(itemID) or idCheckers.IsConstellation(itemID) or idCheckers.IsRegion(itemID)): 594 log.LogError('Not valid itemID for 2D map, itemID: ', itemID, ', typeID: ', typeID) 595 log.LogTraceback() 596 level = [const.groupRegion, const.groupConstellation, const.groupSolarSystem].index(groupID) + 1 597 return self.Do2DMap(sprite, [itemID], level, level + 1, actSize) 598 if evetypes.IsRenderable(typeID): 599 if groupID == const.groupCharacter: 600 return self.GetPortrait(itemID, actSize, sprite) 601 else: 602 return self.OrderByTypeID([[typeID, 603 sprite, 604 actSize, 605 itemID, 606 blueprint]]) 607 elif categoryID == const.categoryShipSkin: 608 self.DoSkinLicenseIcon(typeID, sprite) 609 else: 610 iconInfo = inventorycommon.typeHelpers.GetIcon(typeID) 611 if iconInfo and iconInfo.iconFile: 612 icon = iconInfo.iconFile.strip() 613 if icon: 614 if blueprint: 615 return self.DoBlueprint(sprite, typeID, size=actSize, blueprint=blueprint) 616 else: 617 return sprite.LoadIcon(icon, ignoreSize=ignoreSize) 618 619 def DoSkinLicenseIcon(self, typeID, sprite): 620 if not sprite.texture: 621 return 622 try: 623 skin = sm.GetService('cosmeticsSvc').GetSkinByLicenseType(typeID) 624 sprite.texture.resPath = skin.iconTexturePath 625 except KeyError: 626 sprite.texture.resPath = 'res:/UI/Texture/classes/skins/icons/default.png' 627 628 def OrderByTypeID(self, orderlist): 629 for each in orderlist: 630 if len(each) == 4: 631 typeID, wnd, size, itemID = each 632 blueprint = None 633 else: 634 typeID, wnd, size, itemID, blueprint = each 635 if uicore.desktop.dpiScaling > 1.0: 636 size = size * 2 637 if wnd is None or wnd.destroyed: 638 orderlist.remove(each) 639 continue 640 foundPath = self.ExistsInCacheOrRenders(typeID, size, itemID, blueprint) 641 if foundPath: 642 wnd.LoadTexture(foundPath) 643 orderlist.remove(each) 644 continue 645 wnd.LoadTexture('res:/UI/Texture/notavailable.dds') 646 647 for each in orderlist: 648 self.byTypeIDQue.append(each) 649 650 if not self.byTypeID_IsRunning: 651 uthread.pool('photo::OrderByTypeID', self.ProduceTypeIDs) 652 self.byTypeID_IsRunning = 1 653 654 def ExistsInCacheOrRenders(self, typeID, size, itemID, blueprint): 655 graphicID = evetypes.GetGraphicID(typeID) 656 if evetypes.GetGroupID(typeID) == const.groupStation: 657 npcStation = cfg.mapSolarSystemContentCache.npcStations.get(itemID, None) 658 if npcStation: 659 graphicID = npcStation.graphicID 660 renderPath = GetRenderPath(graphicID, size, blueprint) 661 if blue.paths.exists(renderPath): 662 return renderPath 663 cachePath = GetCachePath(typeID, graphicID, size, itemID, blueprint) 664 if self.CheckAvail(cachePath) is not None: 665 return cachePath 666 667 def ProduceTypeIDs(self): 668 while self.byTypeIDQue: 669 for order in self.byTypeIDQue: 670 if len(order) == 4: 671 typeID, wnd, size, itemID = order 672 blueprint = None 673 else: 674 typeID, wnd, size, itemID, blueprint = order 675 if wnd is None or wnd.destroyed: 676 self.byTypeIDQue.remove(order) 677 continue 678 if uicore.desktop.dpiScaling > 1.0: 679 size = size * 2 680 foundPath = self.ExistsInCacheOrRenders(typeID, size, itemID, blueprint) 681 if foundPath: 682 wnd.LoadTexture(foundPath) 683 self.byTypeIDQue.remove(order) 684 continue 685 else: 686 self.LogError('The type', typeID, 'is missing a pre rendered icon!') 687 photopath = NOT_AVAILABLE_PATH 688 if wnd is None or wnd.destroyed: 689 self.byTypeIDQue.remove(order) 690 continue 691 if wnd.texture and wnd.texture.resPath != NOT_AVAILABLE_PATH: 692 self.byTypeIDQue.remove(order) 693 continue 694 wnd.LoadTexture(photopath) 695 if photopath == NOT_AVAILABLE_PATH: 696 self.byTypeIDQue.remove(order) 697 continue 698 self.byTypeIDQue.remove(order) 699 blue.pyos.synchro.Yield() 700 701 self.byTypeID_IsRunning = 0 702 703 def GetPlanetScene(self): 704 scenepath = sm.GetService('sceneManager').GetScene() 705 scene = trinity.Load(scenepath) 706 scene.backgroundEffect = trinity.Load('res:/dx9/scene/starfield/starfieldNebula.red') 707 if scene.backgroundEffect is not None: 708 for node in scene.backgroundEffect.resources.Find('trinity.TriTextureParameter'): 709 if node.name == 'NebulaMap': 710 node.resourcePath = scene.envMap1ResPath 711 712 scene.backgroundRenderingEnabled = True 713 return scene 714 715 def GetImageServerURL(self, clientCfgValue = 'imageserverurl', defaultServer = None): 716 imageServer = sm.GetService('machoNet').GetGlobalConfig().get(clientCfgValue) 717 if imageServer is None: 718 imageServer = defaultServer 719 return imageServer 720 721 def FindBestPortraitSize(self, size): 722 for available_size in PORTRAIT_SIZES: 723 if available_size >= size: 724 return available_size 725 726 return PORTRAIT_SIZES[-1] 727 728 def RenderPortraitFromScene(self, charID, size = DEFAULT_PORTRAIT_SIZE, charInfo = None, dna = None, portraitSettings = None, fullPoseSetup = False): 729 cachePath = self._scenePortraitQueue.GetCachePath(charID, size) 730 return self._scenePortraitQueue.renderer.Render(charID, size, charInfo=charInfo, dna=dna, portraitSettings=portraitSettings, fullPoseSetup=fullPoseSetup, cachePath=cachePath) 731 732 def RenderPortrait(self, charID, size = DEFAULT_PORTRAIT_SIZE, height = None, sprite = None, fullPoseSetup = True): 733 cachePath = self._scenePortraitQueue.GetCachePath(charID, size, height) 734 path = self._scenePortraitQueue.renderer.Render(charID, size, fullPoseSetup=fullPoseSetup, height=height, cachePath=cachePath) 735 if path and sprite is not None and not sprite.destroyed: 736 sprite.LoadTexture(path) 737 return path 738 739 740 NOT_MODIFIED = 304 741 TEMP_REDIRECT = 302 742 MAX_ATTEMPTS = 4 743 REQUEST_TIMEOUT_SEC = 10 744 745 class RemoteImageCacher(object): 746 747 def __init__(self, cacheItem, logger, suffix, imageServer, machoNet): 748 self.initialized = False 749 self.LogInfo = logger.LogInfo 750 self.LogWarn = logger.LogWarn 751 self.LogError = logger.LogError 752 self.LogInfo('Initializing Remote Image service for', cacheItem) 753 self._machoNet = machoNet 754 self._missingImagesRetryDelay = IMAGE_SERVER_MISSING_IMAGE_RETRY_DELAY_IN_SECONDS_DEFAULT 755 add_global_config_callback(self._OnRefreshGlobalConfig) 756 self.requestSession = requests.session() 757 self.cacheItem = cacheItem 758 self.cacheBasePath = 'cache:/Pictures/%ss/' % self.cacheItem 759 self.cacheFile = '%s_%s' + suffix 760 self.missingImages = {} 761 self.watchList = {} 762 if imageServer == '': 763 imageServer = 'http://xxx.dev.image/' 764 if imageServer is None: 765 self.LogError('RemoteImageCacher can not operate without a server URL!', imageServer) 766 return 767 imageServer = imageServer.strip().encode('ascii') 768 self.imageUri = imageServer 769 if not imageServer.endswith('/'): 770 self.imageUri += '/' 771 self.imageUri += cacheItem + '/%s_%s' + suffix 772 self.LogInfo('RemoteImageCacher initialized with imageUri', self.imageUri) 773 self.initialized = True 774 775 def Initialized(self): 776 return self.initialized 777 778 def _OnRefreshGlobalConfig(self): 779 self._missingImagesRetryDelay = GetImageServerMissingImageRetryDelay(machoNet=self._machoNet) 780 781 def AddToWatchList(self, itemID, callback): 782 self.watchList[itemID] = callback 783 784 def GetCachePath(self, itemID, size, createPath = False): 785 basePath = self.cacheBasePath 786 if self.cacheItem == 'Character' and size in (32, 64): 787 basePath = '%sChat/%s/' % (self.cacheBasePath, itemID % 100) 788 if createPath: 789 pathToFile = blue.paths.ResolvePath(basePath) 790 if not os.path.exists(pathToFile): 791 os.makedirs(pathToFile) 792 return basePath + self.cacheFile % (itemID, size) 793 794 def GetCachedImage(self, itemID, size): 795 if not self.Initialized(): 796 return (None, False) 797 cachePath = self.GetCachePath(itemID, size) 798 fileSystemPath = blue.paths.ResolvePath(cachePath) 799 if os.path.exists(fileSystemPath): 800 return (cachePath, self.__IsFresh(cachePath)) 801 return (None, False) 802 803 def MissingFromServer(self, itemID, size): 804 lastTry = self.missingImages.get((itemID, size), None) 805 if lastTry is not None: 806 if time.time() - lastTry > self._missingImagesRetryDelay: 807 return False 808 return True 809 return False 810 811 def GetImage(self, itemID, size, forceUpdate = False): 812 if not self.Initialized() or self.MissingFromServer(itemID, size) and not forceUpdate: 813 return 814 lockID = str(itemID) + '_' + str(size) 815 uthread.Lock(self, lockID) 816 try: 817 try: 818 cachePath = self.GetCachePath(itemID, size, createPath=True) 819 except Exception as e: 820 self.LogError('Failed to get image cache folder', repr(e)) 821 self.initialized = False 822 return 823 824 cacheFile = blue.paths.ResolvePath(cachePath) 825 lastModified = self.GetLastModified(cachePath) 826 if forceUpdate or not self.__IsFresh(cachePath): 827 self.LogInfo('Get image for', itemID, 'is fetching/refreshing image. Forced = ', forceUpdate) 828 image, headerLastModifiedTime = self.__GetImageFromUrl(itemID, size, lastModified) 829 if image is None: 830 self.LogInfo('No image found for', itemID, 'adding to missing images') 831 if self._missingImagesRetryDelay > 0: 832 self.missingImages[itemID, size] = time.time() 833 return 834 if (itemID, size) in self.missingImages: 835 self.missingImages.pop((itemID, size), None) 836 if image == NOT_MODIFIED: 837 self.LogInfo('Image has not been modified, updating cached image') 838 self.UpdateLastCheckedTime(cacheFile) 839 else: 840 resfile = blue.classes.CreateInstance('blue.ResFile') 841 try: 842 if not resfile.Open(cachePath, 0): 843 try: 844 resfile.Create(cachePath) 845 except Exception as e: 846 self.LogError('Failed to get image cache folder', repr(e)) 847 self.initialized = False 848 return 849 850 resfile.Write(image) 851 resfile.Close() 852 self.InvalidateResManagerForResource(cachePath) 853 self.FetchedCacheFile(cachePath, headerLastModifiedTime) 854 except Exception as e: 855 self.LogError('Failed to update cached image', repr(e)) 856 return 857 858 if itemID in self.watchList: 859 self.LogInfo('RemoteImageCached removed item ', itemID, 'was an item in the watchlist.') 860 self.watchList[itemID](itemID) 861 del self.watchList[itemID] 862 finally: 863 uthread.UnLock(self, lockID) 864 865 return cachePath 866 867 def __GetImageFromUrl(self, itemID, size, lastModified): 868 if not self.Initialized(): 869 return (None, None) 870 url = (self.imageUri % (itemID, size)).strip().encode('ascii') 871 headers = {} 872 if lastModified: 873 cacheTime = datetime.datetime.utcfromtimestamp(lastModified) 874 cacheStamp = cacheTime.strftime('%a, %d %b %Y %H:%M:%S GMT') 875 headers['If-Modified-Since'] = cacheStamp 876 self.LogInfo('adding If-Modified-Since header for', cacheStamp) 877 response = None 878 exc = None 879 image = None 880 headerLastModifiedTime = None 881 for attempt in range(MAX_ATTEMPTS): 882 try: 883 response = self.requestSession.get(url, headers=headers, timeout=REQUEST_TIMEOUT_SEC, allow_redirects=False) 884 break 885 except Exception as e: 886 exc = e 887 if attempt + 1 < MAX_ATTEMPTS: 888 backoffSeconds = 2 ** attempt 889 logger.warn('Retrying requesting image for url: %s. Sleeping for %s seconds. Attempts done so far: (%s/%s). Exception: %s', url, backoffSeconds, attempt + 1, MAX_ATTEMPTS, exc) 890 blue.pyos.synchro.SleepWallclock(backoffSeconds * 1000) 891 892 else: 893 if blue.pyos.packaged: 894 logger.exception('Retries exhausted for requesting an image for url: %s. Total attempts: %s. Last ex: %s.', url, MAX_ATTEMPTS, exc) 895 else: 896 logger.warn('Retries exhausted for requesting an image for url: %s. Total attempts: %s. Last ex: %s.', url, MAX_ATTEMPTS, exc) 897 898 if response: 899 contentType = response.headers.get('Content-Type', None) 900 if response.status_code == NOT_MODIFIED: 901 image = NOT_MODIFIED 902 self.LogInfo('Not Modified', url, 'since', lastModified, time.ctime(lastModified)) 903 elif response.status_code == TEMP_REDIRECT: 904 self.LogInfo('Temp Redirect while getting image', url) 905 elif not contentType or not contentType.startswith('image/'): 906 logger.exception('Did not receive image content type for url: %s. Response status code: %s' % (url, response.status_code)) 907 else: 908 image = response.content 909 rLastModified = response.headers.get('Last-Modified', None) 910 if rLastModified: 911 headerLastModifiedTime = self.__FormatLastModifiedHeader(rLastModified) 912 if not headerLastModifiedTime: 913 headerLastModifiedTime = time.time() 914 return (image, headerLastModifiedTime) 915 916 @staticmethod 917 def __FormatLastModifiedHeader(lastModified): 918 try: 919 time_tuple = rfc822.parsedate(lastModified) 920 return time.mktime(time_tuple) 921 except Exception as e: 922 log.LogException('Photo - Exception formatting last modified header', str(e)) 923 924 def GetLastModified(self, cachePath): 925 filepath = blue.paths.ResolvePath(cachePath) 926 if os.path.exists(filepath): 927 return os.path.getmtime(filepath) 928 929 def GetLastChecked(self, cachePath): 930 return self.GetLastModified(cachePath) 931 932 def InvalidateResManagerForResource(self, cacheFile): 933 try: 934 self.LogInfo('Reloading blue res', cacheFile) 935 res = blue.resMan.GetResource(str(cacheFile), 'atlas') 936 if res: 937 res.Reload() 938 except SystemError: 939 sys.exc_clear() 940 941 def FetchedCacheFile(self, cacheFile, headerLastModifiedTime): 942 pass 943 944 def UpdateLastCheckedTime(self, cacheFile): 945 self.UpdateFileTimeStamp(cacheFile, None) 946 947 def UpdateFileTimeStamp(self, cacheFile, timeStamp): 948 try: 949 with file(cacheFile, 'a'): 950 os.utime(cacheFile, timeStamp) 951 except Exception as e: 952 pass 953 954 def __IsFresh(self, cachePath): 955 lastModified = self.GetLastChecked(cachePath) 956 if lastModified is None: 957 return False 958 delta = time.time() - lastModified 959 return delta < MAX_CACHE_AGE 960 961 def RemoveFromCache(self, itemID, size): 962 cachePath = self.GetCachePath(itemID, size) 963 filepath = blue.paths.ResolvePath(cachePath) 964 if os.path.exists(filepath): 965 os.remove(filepath)