NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

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)