skillExtractorController.py (21413B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\skilltrading\skillExtractorController.py 2 import characterskills 3 import clonegrade 4 import collections 5 from eve.common.lib.appConst import SKILL_TRADING_BUCKET_SIZE 6 import signals 7 from omega.client.clone_grade_svc import get_clone_grade_svc 8 9 class SkillExtractorError(Exception): 10 pass 11 12 13 class NoSkillpointsError(SkillExtractorError): 14 pass 15 16 17 class SkillNotFoundError(SkillExtractorError): 18 pass 19 20 21 class SkillLockedError(SkillExtractorError): 22 pass 23 24 25 class NotEnoughExtractedError(SkillExtractorError): 26 pass 27 28 29 class GoalAlreadyReachedError(SkillExtractorError): 30 pass 31 32 33 class SkillNotExtractedError(SkillExtractorError): 34 pass 35 36 37 class SkillSourceInterface(object): 38 39 def GetInjectedSkills(self): 40 raise NotImplementedError() 41 42 def GetSkillQueue(self): 43 raise NotImplementedError() 44 45 def GetSkillRestrictions(self): 46 raise NotImplementedError() 47 48 def PerformExtraction(self, itemID, skills): 49 raise NotImplementedError() 50 51 52 SkillRestriction = collections.namedtuple('SkillRestriction', ['level', 'reason']) 53 RESTRICTION_JUMP_CLONE = 0 54 RESTRICTION_PI_UPGRADE = 1 55 RESTRICTION_PI_COLONY = 2 56 RESTRICTION_IMPLANT = 3 57 RESTRICTION_CLONE_STATE = 4 58 RESTRICTION_OVER_TRAINED = 5 59 60 class SkillExtractorControllerBase(SkillSourceInterface): 61 SKILL_POINT_GOAL = SKILL_TRADING_BUCKET_SIZE 62 63 def __init__(self, itemID): 64 super(SkillExtractorControllerBase, self).__init__() 65 self.itemID = itemID 66 self.isCompleted = False 67 self._skills = None 68 self._skillsByID = None 69 self._extracted = collections.defaultdict(list) 70 self.onUpdate = signals.Signal(signalName='onUpdate3') 71 self.onSkillListUpdate = signals.Signal(signalName='onSkillListUpdate') 72 self.onSkillListUpdate.connect(self.onUpdate) 73 74 @property 75 def extractedPoints(self): 76 return sum((sum(p) for p in self._extracted.itervalues()), 0) 77 78 @property 79 def progress(self): 80 return self.extractedPoints / float(self.SKILL_POINT_GOAL) 81 82 @property 83 def skills(self): 84 if self._skills is None: 85 self._skills = self.GetInjectedSkills() 86 for skill in self._skills: 87 self._UpdateSkillExtracted(skill) 88 self._UpdateSkillRequired(skill) 89 self._UpdateSkillQueued(skill) 90 self._UpdateSkillRestricted(skill) 91 92 return self._skills 93 94 @property 95 def skillsByID(self): 96 if self._skillsByID is None: 97 self._skillsByID = {s.typeID:s for s in self.skills} 98 return self._skillsByID 99 100 def ExtractSkill(self, skillID): 101 if self.extractedPoints >= self.SKILL_POINT_GOAL: 102 raise GoalAlreadyReachedError() 103 if skillID not in self.skillsByID: 104 raise SkillNotFoundError() 105 skill = self.skillsByID[skillID] 106 if skill.IsLocked(): 107 raise SkillLockedError() 108 clamped_points = self._GetClampedSkillPointAdjustment(skill) 109 self._extracted[skill.typeID].append(clamped_points) 110 self._UpdateSkillExtracted(skill) 111 self._UpdateSkillRequired(skill) 112 self._UpdateSkillRestricted(skill) 113 for requiredSkillID in skill.requirements: 114 if requiredSkillID not in self.skillsByID: 115 continue 116 s = self.skillsByID[requiredSkillID] 117 self._UpdateSkillRequired(s) 118 s.NotifyOnUpdateIfDirty() 119 120 skill.NotifyOnUpdateIfDirty() 121 self.onUpdate() 122 123 def Revert(self, skillID): 124 if skillID not in self._extracted: 125 raise SkillNotExtractedError() 126 self._RevertSingleLevel(skillID) 127 self._RevertRequirements(skillID) 128 skill = self.skillsByID[skillID] 129 self._UpdateSkillRestricted(skill) 130 for skill in self.skills: 131 skill.NotifyOnUpdateIfDirty() 132 133 self.onUpdate() 134 135 def _RevertRequirements(self, skillID): 136 skill = self.skillsByID[skillID] 137 for requiredSkillID, requiredLevel in skill.requirements.iteritems(): 138 if requiredSkillID not in self.skillsByID: 139 continue 140 requiredSkill = self.skillsByID[requiredSkillID] 141 while requiredSkill.level < requiredLevel and requiredSkillID in self._extracted: 142 self._RevertSingleLevel(requiredSkillID) 143 144 self._RevertRequirements(requiredSkillID) 145 self._UpdateSkillRequired(requiredSkill) 146 147 self._UpdateSkillRequired(skill) 148 149 def _RevertSingleLevel(self, skillID): 150 self._extracted[skillID].pop() 151 if len(self._extracted[skillID]) == 0: 152 del self._extracted[skillID] 153 skill = self.skillsByID[skillID] 154 self._UpdateSkillExtracted(skill) 155 156 def _UpdateSkillExtracted(self, skill): 157 extractedPoints = sum(self._extracted.get(skill.typeID, [])) 158 skill.points = skill.unmodifiedPoints - extractedPoints 159 160 def _UpdateSkillRequired(self, skill): 161 required = False 162 for dependentSkillID, level in skill.dependencies.iteritems(): 163 if dependentSkillID not in self.skillsByID: 164 continue 165 isDependentQueued = dependentSkillID in (s for s, _ in self.GetSkillQueue()) 166 dependentSkillPoints = self.skillsByID[dependentSkillID].points 167 if dependentSkillPoints == 0 and not isDependentQueued: 168 continue 169 requiredPoints = characterskills.GetSPForLevelRaw(skill.rank, level) 170 if skill.points <= requiredPoints: 171 required = True 172 break 173 174 skill.isRequired = required 175 176 def _UpdateSkillQueued(self, skill): 177 queuedSkills = set((skillID for skillID, _ in self.GetSkillQueue())) 178 skill.isQueued = skill.typeID in queuedSkills 179 180 def _GetClampedSkillPointAdjustment(self, skill): 181 adjustment = self._GetNextExtractionIncrement(skill) 182 return self._ClampToRemainingSkillPoints(adjustment) 183 184 def _GetNextExtractionIncrement(self, skill): 185 if skill.points == 0: 186 raise NoSkillpointsError() 187 current_level_absolute_points = characterskills.GetSPForLevelRaw(skill.rank, skill.level) 188 if current_level_absolute_points < skill.points: 189 adjust_to_level = skill.level 190 else: 191 adjust_to_level = skill.level - 1 192 return skill.points - characterskills.GetSPForLevelRaw(skill.rank, adjust_to_level) 193 194 def _ClampToRemainingSkillPoints(self, points): 195 remaining = self.SKILL_POINT_GOAL - self.extractedPoints 196 return min(remaining, max(0, points)) 197 198 def Commit(self): 199 if self.extractedPoints < self.SKILL_POINT_GOAL: 200 raise NotEnoughExtractedError() 201 skills = {sid:sum(p) for sid, p in self._extracted.iteritems()} 202 self.PerformExtraction(self.itemID, skills) 203 self.isCompleted = True 204 self.onUpdate() 205 206 def OnSkillTrained(self, skillID, points): 207 if skillID not in self.skillsByID: 208 self.Clear() 209 return 210 skill = self.skillsByID[skillID] 211 if points == -1: 212 self._CheckAndRemoveExtracted(skill) 213 self.Clear() 214 else: 215 skill.points = points 216 skill._unmodifiedPoints = points 217 wasExtracted = self._CheckAndRemoveExtracted(skill) 218 if wasExtracted: 219 self.onUpdate() 220 skill.NotifyOnUpdateIfDirty() 221 222 def Clear(self): 223 self._skills = None 224 self._skillsByID = None 225 self.onSkillListUpdate() 226 227 def OnSkillQueueUpdated(self): 228 extractedBefore = self.extractedPoints 229 for skill in self.skills: 230 self._UpdateSkillQueued(skill) 231 232 queuedSkills = [ self.skillsByID[sid] for sid, _ in self.GetSkillQueue() ] 233 for skill in queuedSkills: 234 self._CheckAndRemoveExtracted(skill) 235 self._RevertRequirements(skill.typeID) 236 237 for skill in self.skills: 238 self._UpdateSkillRequired(skill) 239 skill.NotifyOnUpdateIfDirty() 240 241 if extractedBefore != self.extractedPoints: 242 self.onUpdate() 243 244 def _CheckAndRemoveExtracted(self, skill): 245 isExtracted = skill.typeID in self._extracted 246 if isExtracted: 247 del self._extracted[skill.typeID] 248 self._UpdateSkillExtracted(skill) 249 self._UpdateSkillRestricted(skill) 250 return isExtracted 251 252 def Close(self): 253 self.onSkillListUpdate.clear() 254 self.onUpdate.clear() 255 if self._skills is not None: 256 for skill in self._skills: 257 skill.onUpdate.clear() 258 259 def OnSkillRestrictionsUpdated(self): 260 for skill in self.skills: 261 self._UpdateSkillRestricted(skill) 262 263 extractedBefore = self.extractedPoints 264 restrictions = self.GetSkillRestrictions() 265 for skillID, restriction in restrictions.iteritems(): 266 if skillID not in self.skillsByID: 267 continue 268 skill = self.skillsByID[skillID] 269 self._RevertToLevel(skill, restriction.level) 270 self._RevertRequirements(skillID) 271 skill.NotifyOnUpdateIfDirty() 272 273 for skill in self.skills: 274 skill.NotifyOnUpdateIfDirty() 275 276 if self.extractedPoints != extractedBefore: 277 self.onUpdate() 278 279 def _RevertToLevel(self, skill, level): 280 while skill.level < level and skill.typeID in self._extracted: 281 self._extracted[skill.typeID].pop() 282 if len(self._extracted[skill.typeID]) == 0: 283 del self._extracted[skill.typeID] 284 self._UpdateSkillExtracted(skill) 285 286 def _UpdateSkillRestricted(self, skill): 287 restrictions = self.GetSkillRestrictions() 288 if skill.typeID not in restrictions: 289 skill.isRestricted = False 290 else: 291 restrictedLevel = restrictions[skill.typeID].level 292 if restrictedLevel == characterskills.MAX_SKILL_LEVEL: 293 restrictedPoints = skill.points 294 else: 295 restrictedPoints = characterskills.GetSPForLevelRaw(skill.rank, restrictedLevel) 296 skill.isRestricted = skill.points <= restrictedPoints 297 skill.restrictionReason = restrictions[skill.typeID].reason 298 299 300 class SkillStaticDataInterface(object): 301 302 def GetRequirements(self): 303 raise NotImplementedError() 304 305 def GetDependencies(self): 306 raise NotImplementedError() 307 308 def GetRank(self): 309 raise NotImplementedError() 310 311 312 class SkillBase(SkillStaticDataInterface): 313 314 def __init__(self, typeID, points): 315 super(SkillBase, self).__init__() 316 self._typeID = typeID 317 self._points = points 318 self._unmodifiedPoints = points 319 self._isRequired = False 320 self._isQueued = False 321 self._isRestricted = False 322 self._restrictionReason = None 323 self._rank = None 324 self._level = None 325 self._dirty = False 326 self.onUpdate = signals.Signal(signalName='onUpdate4') 327 328 @property 329 def typeID(self): 330 return self._typeID 331 332 @property 333 def points(self): 334 return self._points 335 336 @points.setter 337 def points(self, points): 338 self._dirty |= points != self._points 339 self._points = points 340 self._level = None 341 342 @property 343 def unmodifiedPoints(self): 344 return self._unmodifiedPoints 345 346 @property 347 def progress(self): 348 return self._CalculateProgress(self.points) 349 350 @property 351 def unmodifiedProgress(self): 352 return self._CalculateProgress(self.unmodifiedPoints) 353 354 def _CalculateProgress(self, points): 355 level = characterskills.GetSkillLevelRaw(points, self.rank) 356 if level == 5: 357 return 0.0 358 last_level_points = characterskills.GetSPForLevelRaw(self.rank, level) 359 next_level_points = characterskills.GetSPForLevelRaw(self.rank, level + 1) 360 current_level_points = points - last_level_points 361 return current_level_points / float(next_level_points - last_level_points) 362 363 @property 364 def level(self): 365 if self._level is None: 366 self._level = characterskills.GetSkillLevelRaw(self.points, self.rank) 367 return self._level 368 369 @property 370 def unmodifiedLevel(self): 371 return characterskills.GetSkillLevelRaw(self.unmodifiedPoints, self.rank) 372 373 @property 374 def rank(self): 375 if self._rank is None: 376 self._rank = self.GetRank() 377 return self._rank 378 379 @property 380 def requirements(self): 381 return self.GetRequirements() 382 383 @property 384 def dependencies(self): 385 return self.GetDependencies() 386 387 @property 388 def isRequired(self): 389 return self._isRequired 390 391 @isRequired.setter 392 def isRequired(self, required): 393 self._dirty |= required != self._isRequired 394 self._isRequired = required 395 396 @property 397 def isQueued(self): 398 return self._isQueued 399 400 @isQueued.setter 401 def isQueued(self, queued): 402 self._dirty |= queued != self._isQueued 403 self._isQueued = queued 404 405 @property 406 def isRestricted(self): 407 return self._isRestricted 408 409 @isRestricted.setter 410 def isRestricted(self, restricted): 411 self._dirty |= restricted != self._isRestricted 412 self._isRestricted = restricted 413 414 @property 415 def restrictionReason(self): 416 return self._restrictionReason 417 418 @restrictionReason.setter 419 def restrictionReason(self, reason): 420 self._dirty |= reason != self._restrictionReason 421 self._restrictionReason = reason 422 423 def NotifyOnUpdateIfDirty(self): 424 if self._dirty: 425 self._dirty = False 426 self.onUpdate() 427 428 def IsLocked(self): 429 return self.isRequired or self.isQueued or self.isRestricted 430 431 432 class SkillServiceStaticDataInterface(SkillStaticDataInterface): 433 434 def GetRequirements(self): 435 return sm.GetService('skills').GetRequiredSkills(self.typeID) 436 437 def GetDependencies(self): 438 return sm.GetService('skills').GetDependentSkills(self.typeID) 439 440 def GetRank(self): 441 return sm.GetService('skills').GetSkillRank(self.typeID) 442 443 444 class SkillImpl(SkillBase, SkillServiceStaticDataInterface): 445 pass 446 447 448 class SkillServiceSkillSource(SkillSourceInterface): 449 450 def GetInjectedSkills(self): 451 raw_skills = sm.GetService('skills').GetSkillsIncludingLapsed().values() 452 return [ SkillImpl(typeID=s.typeID, points=s.trainedSkillPoints) for s in raw_skills if s.trainedSkillPoints is not None ] 453 454 def GetSkillQueue(self): 455 queue = sm.GetService('skillqueue').GetQueue() 456 return [ (x.trainingTypeID, x.trainingToLevel) for x in queue ] 457 458 def GetSkillRestrictions(self): 459 restrictions = {} 460 461 def updateRestrictions(new_restrictions): 462 for sid, r in new_restrictions.iteritems(): 463 if sid not in restrictions or r.level > restrictions[sid].level: 464 restrictions[sid] = r 465 466 updateRestrictions(self._GetRestrictedAlphaSkills()) 467 updateRestrictions(self._GetRestrictedPlanetSkills()) 468 updateRestrictions(self._GetRestrictedCloneSkills()) 469 updateRestrictions(self._GetRestrictedImplantSkills()) 470 updateRestrictions(self._GetOverTrainedSkills()) 471 return restrictions 472 473 def _GetRestrictedPlanetSkills(self): 474 restrictions = {} 475 colonies = sm.GetService('planetSvc').GetMyPlanets() 476 if len(colonies) > 0: 477 colonySkillLimit = max(len(colonies) - 1, 0) 478 restriction = SkillRestriction(colonySkillLimit, RESTRICTION_PI_COLONY) 479 restrictions[const.typeInterplanetaryConsolidation] = restriction 480 maxUpgradeLevel = 0 481 for planetID in (c.planetID for c in colonies): 482 planet = sm.GetService('planetSvc').GetPlanet(planetID) 483 colonyLevel = planet.GetCommandCenterLevel(session.charid) 484 maxUpgradeLevel = max(colonyLevel, maxUpgradeLevel) 485 486 if maxUpgradeLevel > 0: 487 restriction = SkillRestriction(maxUpgradeLevel, RESTRICTION_PI_UPGRADE) 488 restrictions[const.typeCommandCenterUpgrade] = restriction 489 return restrictions 490 491 def _GetRestrictedCloneSkills(self): 492 restrictions = {} 493 cloneCount = len(sm.GetService('clonejump').GetClones()) 494 if cloneCount > 0: 495 restriction = SkillRestriction(min(cloneCount, 5), RESTRICTION_JUMP_CLONE) 496 restrictions[const.typeInfomorphPsychology] = restriction 497 if cloneCount > 5: 498 restriction = SkillRestriction(cloneCount - 5, RESTRICTION_JUMP_CLONE) 499 restrictions[const.typeAdvancedInfomorphPsychology] = restriction 500 if cloneCount > 10: 501 restriction = SkillRestriction(cloneCount - 10, RESTRICTION_JUMP_CLONE) 502 restrictions[const.typeEliteInfomorphPsychology] = restriction 503 return restrictions 504 505 def _GetRestrictedImplantSkills(self): 506 restrictions = {} 507 implants = self._GetAllMyImplants() 508 for typeID in implants: 509 requirements = sm.GetService('skills').GetRequiredSkillsRecursive(typeID) 510 for requiredSkillID, requiredLevel in requirements.iteritems(): 511 if requiredSkillID in restrictions: 512 maxRequiredLevel = max(requiredLevel, restrictions[requiredSkillID].level) 513 else: 514 maxRequiredLevel = requiredLevel 515 restriction = SkillRestriction(maxRequiredLevel, RESTRICTION_IMPLANT) 516 restrictions[requiredSkillID] = restriction 517 518 return restrictions 519 520 def _GetRestrictedAlphaSkills(self): 521 if get_clone_grade_svc().is_omega_with_grace_period(): 522 return {} 523 restrictions = {} 524 alpha = clonegrade.CloneGradeStorage()[session.raceID] 525 for skillID, level in alpha.skills: 526 restrictions[skillID] = SkillRestriction(level, RESTRICTION_CLONE_STATE) 527 528 return restrictions 529 530 def _GetAllMyImplants(self): 531 implants = set() 532 currentImplants = sm.GetService('skills').GetImplants().values() 533 implants.update((i.typeID for i in currentImplants)) 534 jumpCloneImplants = sm.GetService('clonejump').GetCloneImplants() 535 implants.update((i.typeID for i in jumpCloneImplants or [])) 536 return implants 537 538 def _GetOverTrainedSkills(self): 539 restrictions = {} 540 skills = sm.GetService('skills').GetSkillsIncludingLapsed().values() 541 for skill in skills: 542 maxPoints = characterskills.GetSPForLevelRaw(skill.skillRank, characterskills.MAX_SKILL_LEVEL) 543 if skill.trainedSkillPoints > maxPoints: 544 restrictions[skill.typeID] = SkillRestriction(characterskills.MAX_SKILL_LEVEL, RESTRICTION_OVER_TRAINED) 545 546 return restrictions 547 548 def PerformExtraction(self, itemID, skills): 549 sm.GetService('skills').ExtractSkills(skills, itemID) 550 551 552 class SkillExtractorController(SkillExtractorControllerBase, SkillServiceSkillSource): 553 __notifyevents__ = ['OnCloneJumpUpdate', 554 'OnImplantsChanged', 555 'OnNewSkillQueueSaved', 556 'OnPlanetChangesSubmitted', 557 'OnPlanetCommandCenterDeployedOrRemoved', 558 'OnSkillsChanged'] 559 560 def __init__(self, *args, **kwargs): 561 super(SkillExtractorController, self).__init__(*args, **kwargs) 562 self._cached_queue = None 563 self._cached_restrictions = None 564 sm.RegisterNotify(self) 565 566 def Close(self): 567 super(SkillExtractorController, self).Close() 568 sm.UnregisterNotify(self) 569 570 def GetSkillQueue(self): 571 if self._cached_queue is None: 572 self._cached_queue = super(SkillExtractorController, self).GetSkillQueue() 573 return self._cached_queue 574 575 def GetSkillRestrictions(self): 576 if self._cached_restrictions is None: 577 self._cached_restrictions = super(SkillExtractorController, self).GetSkillRestrictions() 578 return self._cached_restrictions 579 580 def PerformExtraction(self, itemID, skills): 581 super(SkillExtractorController, self).PerformExtraction(itemID, skills) 582 sm.UnregisterNotify(self) 583 584 def OnCloneJumpUpdate(self): 585 self._cached_restrictions = None 586 self.OnSkillRestrictionsUpdated() 587 588 def OnImplantsChanged(self): 589 self._cached_restrictions = None 590 self.OnSkillRestrictionsUpdated() 591 592 def OnNewSkillQueueSaved(self, newQueue): 593 self._cached_queue = None 594 self.OnSkillQueueUpdated() 595 596 def OnPlanetChangesSubmitted(self, planetID): 597 self._cached_restrictions = None 598 self.OnSkillRestrictionsUpdated() 599 600 def OnPlanetCommandCenterDeployedOrRemoved(self): 601 self._cached_restrictions = None 602 self.OnSkillRestrictionsUpdated() 603 604 def OnSkillsChanged(self, skillInfo): 605 for skillID, info in skillInfo.iteritems(): 606 self.OnSkillTrained(skillID, info.trainedSkillPoints) 607 608 609 def __SakeReloadHook(): 610 import log 611 try: 612 from eve.client.script.ui.skilltrading.test import test_skillExtractorController 613 import unittest 614 import sys 615 suite = unittest.TestLoader().loadTestsFromModule(test_skillExtractorController) 616 unittest.TextTestRunner(stream=sys.stdout, verbosity=1).run(suite) 617 except: 618 log.LogException()