def gather_death_data(species: PrimalDinoCharacter) -> DeathData: out = DeathData( dossierId=species.DeathGivesDossierIndex[0], baseXP=species.KillXPBase[0], ) engrams = species.get('DeathGiveEngramClasses', fallback=None) if engrams: # Output only valid engram references. for engram_ref in engrams: if engram_ref and engram_ref.value and engram_ref.value.value: out.engrams.append(decode_item_name(engram_ref)) # Export possible loot bag drop components (for use with wiki.drops). loot_chance = species.DeathInventoryChanceToUse[0] if loot_chance > 0: loot_templates = gather_inherited_struct_fields(species.get_source(), 'DeathInventoryTemplates', LOOT_INVENTORY_TEMPLATES_DEFAULTS) weights = loot_templates['Weights'] drop_components = loot_templates['AssociatedObjects'] if drop_components and drop_components.values: # Make sure there's no more weights than drop components if weights: weights = weights.values else: weights = [] weights = weights[:len(drop_components.values)] # Go through each drop component and gather the possible choices. choices = list() for ref, weight in zip_longest(drop_components, weights, fillvalue=1): # Skip invalid drop component references. if not ref or not ref.value or not ref.value.value: continue # Skip the component if its weight is lower than zero. if weight <= 0: continue # Needed due to a bug in the UE module which will be fixed in another commit. choices.append((weight, ref.format_for_json().format_for_json())) # Convert the choices list into chance weight_sum = sum(t[0] for t in choices) for weight, ref in choices: out.lootBags.append(ItemChancePair(chance=weight / weight_sum, item=ref)) # Clamp the primary chance for the bag between 0 and 1. # If there's no valid drop components, override it to zero. if out.lootBags: # Explicit float cast to avoid a type warning over missing overloads for int+object. out.lootBagChance = min(1, max(0, float(loot_chance))) else: out.lootBagChance = 0 return out
def gather_cloning_data(species: PrimalDinoCharacter) -> Optional[CloningData]: if not can_be_cloned(species): return None loader = species.get_source().asset.loader chamber_a = loader[CLONING_CHAMBER_C] assert chamber_a.default_export chamber = cast(TekCloningChamber, gather_properties(chamber_a.default_export)) cost_base = species.CloneBaseElementCost[ 0] * chamber.CloneBaseElementCostGlobalMultiplier[0] cost_level = species.CloneElementCostPerLevel[ 0] * chamber.CloneElementCostPerLevelGlobalMultiplier[ 0] # skipped: CharacterLevel time_base = chamber.CloningTimePerElementShard[ 0] * cost_base # skipped: BabyMatureSpeedMultiplier time_level = chamber.CloningTimePerElementShard[ 0] * cost_level # skipped: BabyMatureSpeedMultiplier, CharacterLevel if cost_base == 0: # Free cloning, skip for sanity, it probably can't be obtained naturally. return None return CloningData( costBase=round(cost_base, 1), costLevel=round(cost_level, 2), timeBase=round(time_base, 1), timeLevel=round(time_level, 2), )
def convert_level_data( species: PrimalDinoCharacter, dcsc: DinoCharacterStatusComponent) -> Optional[LevelData]: if bool(species.bIsBossDino[0]): return None pgd_asset = species.get_source().asset.loader[COREMEDIA_PGD_PKG] assert pgd_asset.default_export pgd: PropertyTable = pgd_asset.default_export.properties result = LevelData() # Max experience points max_xp_override = species.OverrideDinoMaxExperiencePoints[0] if max_xp_override > 0: result.maxExperience = max_xp_override else: result.maxExperience = dcsc.MaxExperiencePoints[0] # Export ramp type and find one from PGD ramp_type = dcsc.LevelExperienceRampType[0].get_enum_value_name() if ramp_type != LevelExperienceRampType.DinoEasy.name: result.ramp = ramp_type ramp_id = LevelExperienceRampType[ramp_type].value ramp_prop = pgd.get_property('LevelExperienceRamps', index=ramp_id, fallback=None) if not ramp_prop: # No ramp (MAX). result.maxLevels = 0 else: ramp = ramp_prop.values[0].value # Find highest level using a vanilla ramp for index, threshold in enumerate(ramp.values): if result.maxExperience >= threshold: result.maxLevels = index + 1 else: break # Level cap offset if species.has_override('DestroyTamesOverLevelClampOffset'): result.capOffset = species.DestroyTamesOverLevelClampOffset[0] return result
def _should_skip_species(species: PrimalDinoCharacter, overrides: OverrideSettings): if overrides.skip_export: return True if not species.has_override('DescriptiveName'): return True return False
def can_be_cloned(species: PrimalDinoCharacter) -> bool: """ Requirements for cloning are: - FLAGS_PREVENT_CLONE are all false - not DCSC->FreezeStatusValues - does not have a rider - clone base element cost higher or equal to 0 """ for flag in FLAGS_PREVENT_CLONE: if species.get(flag): return False return species.CloneBaseElementCost[ 0] >= 0 and species.AutoFadeOutAfterTameTime[0] == 0
def gather_damage_mults(char_props: PrimalDinoCharacter) -> Optional[Dict[str, float]]: damage_adjusters = char_props.get('BoneDamageAdjusters', 0, None) if not damage_adjusters: return None adjusters = damage_adjusters.values result = dict() for bone_info in adjusters: name = str(bone_info.get_property('BoneName')) mult = bone_info.get_property('DamageMultiplier') result[name] = mult return result
def extract_core(self, _: Path): '''Perform sanity tests on core species.''' # Count species by prefix (/Game/<part> or /Game/Mods/<id>) counter: Counter = Counter() for clsname in find_sub_classes(PrimalDinoCharacter.get_ue_type()): if not clsname.startswith('/Game'): continue parts = clsname.split('/') if clsname.startswith('/Game/Mods/'): modid = self.manager.loader.get_mod_id(clsname) assert modid parts[3] = modid parts = parts[:4] else: parts = parts[:3] counter.update(['/'.join(parts)]) # Check counts against configured limits overrides = get_overrides() reports = list() for path, min_count in overrides.sanity_checks.min_species.items(): count = counter.get(path, 0) logger.debug('%s = %d (need %d)', path, count, min_count) if count < min_count: reports.append((path, count, min_count)) # If anything failed, report and turn off Git push if not reports: return None # Disable git push to hopefully avoid damaging commits self.manager.config.git.SkipPush = False # Put together a report message header = "Species count sanity check failed! (git push disabled):\n" lines: List[str] = [] for path, count, min_count in reports: lines.append(f" {path} expected {min_count}, found {count}\n") # Log it logger.error(''.join([header] + lines)) # Send it to Discord send_to_discord(log=lines, header='Purlovia failed the species sanity check:')
def convert_level_data(species: PrimalDinoCharacter, dcsc: DinoCharacterStatusComponent) -> LevelData: pgd_asset = species.get_source().asset.loader[COREMEDIA_PGD_PKG] assert pgd_asset.default_export pgd: PropertyTable = pgd_asset.default_export.properties result = LevelData() # Max experience points max_xp_override = species.OverrideDinoMaxExperiencePoints[0] if max_xp_override > 0: result.maxExperience = max_xp_override else: result.maxExperience = dcsc.MaxExperiencePoints[0] # Export ramp type and find one from PGD ramp_type = dcsc.LevelExperienceRampType[0].get_enum_value_name() if ramp_type != LevelExperienceRampType.DinoEasy.name: result.xpRamp = ramp_type # Calculate max levels out of max XP. ramp_id = LevelExperienceRampType[ramp_type].value ramp_prop = pgd.get_property('LevelExperienceRamps', index=ramp_id, fallback=None) if not ramp_prop: # No ramp (MAX). Tame level ups not possible. result.maxTameLevels = 0 else: ramp = ramp_prop.values[0].value # Find highest level using a vanilla ramp for index, threshold in enumerate(ramp.values): if result.maxExperience >= threshold: result.maxTameLevels = index + 1 else: break # Official servers' level cap if species.has_override('DestroyTamesOverLevelClampOffset'): result.levelCapOffset = species.DestroyTamesOverLevelClampOffset[0] # Wild spawn levels base_level_override = species.AbsoluteBaseLevel[0] fixed_level = species.bUseFixedSpawnLevel[0] if base_level_override != 0 or fixed_level: # Forced base level. Does not scale with difficulty. result.wildForcedLevel = base_level_override or 1 result.ignoresLevelModifiers = fixed_level # If level modifiers are enabled, multiply the level by final level multiplier. if not fixed_level: result.wildForcedLevel *= species.FinalNPCLevelMultiplier[0] else: # Weighed level range table. level_weights = species.get('DinoBaseLevelWeightEntries', fallback=None) out_level_table = list() if level_weights and level_weights.values: weight_sum = 0 entries = list() final_mult = species.FinalNPCLevelMultiplier[0] # Gather the data into a temporary list, with weight and range. for entry in level_weights: d = entry.as_dict() entries.append( (d['EntryWeight'], d['BaseLevelMinRange'] * final_mult, d['BaseLevelMaxRange'] * final_mult)) weight_sum += d['EntryWeight'] # Pack gathered data into MinMaxChanceRanges with calculated chances. The properties cannot be modified through INI # configs. for weight, min_lvl, max_lvl in entries: out_level_table.append( MinMaxChanceRange(chance=clean_float(weight / weight_sum), min=clean_float(min_lvl), max=clean_float(max_lvl))) result.wildLevelTable = out_level_table # Calculate required amount of Mutagen needed to use it. # Formula extracted from PrimalItemConsumable_Mutagen_C's scripts # Vultures cannot use Mutagen as it can't enter their inventories. # IsRobot and IsVehicle banned from Mutagen in v329.51. if species.DinoNameTag[0] != 'Vulture' and not species.bIsRobot[ 0] and not species.bIsVehicle[0]: mutagen_base = species.MutagenBaseCost[0] if mutagen_base <= 0: mutagen_base = species.CloneBaseElementCost[0] if mutagen_base > 0: cost = 1 + 99 * mutagen_base # Round up 0.5 to conform with in-game calculations. if (cost % 1) >= 0.5: result.mutagenCost = ceil(cost) else: result.mutagenCost = round(cost) return result
def get_ue_type(self): return PrimalDinoCharacter.get_ue_type()
def _gather_flags(species: PrimalDinoCharacter) -> List[str]: result = [ _clean_flag_name(field) for field in OUTPUT_FLAGS if species.get(field, fallback=False) ] return result
def gather_breeding_data(char_props: PrimalDinoCharacter, loader: AssetLoader) -> Dict[str, Any]: data: Dict[str, Any] = dict(gestationTime=0, incubationTime=0) gestation_breeding = char_props.bUseBabyGestation[0] fert_eggs = char_props.get('FertilizedEggItemsToSpawn', 0, None) fert_egg_weights = char_props.get('FertilizedEggWeightsToSpawn', 0, None) if gestation_breeding: gestation_speed = char_props.BabyGestationSpeed[0].rounded_value extra_gestation_speed_m = char_props.ExtraBabyGestationSpeedMultiplier[ 0].rounded_value try: data['gestationTime'] = cd(1 / gestation_speed / extra_gestation_speed_m) except ZeroDivisionError: logger.warning( f"Species {char_props.get_source().asset.assetname} tried dividing by zero for its gestationTime" ) elif fert_eggs and fert_eggs.values: eggs: List[Tuple[ObjectProperty, float]] = [] for index, egg in enumerate(fert_eggs.values): weight = fert_egg_weights.values[index] if fert_egg_weights else 1 # Verify the egg is a valid Object and that weight isn't 0 if str(egg) == 'None' or weight == 0: continue eggs.append((egg, weight)) # Sort eggs by highest weighting eggs.sort(reverse=True, key=itemgetter(1)) if eggs: # We only provide the highest possibility egg to ASB fert_egg_asset = loader.load_related(eggs[0][0]) assert fert_egg_asset.default_export egg_props: PrimalItem = ue.gathering.gather_properties( fert_egg_asset.default_export) egg_decay = egg_props.EggLoseDurabilityPerSecond[0].rounded_value extra_egg_decay_m = egg_props.ExtraEggLoseDurabilityPerSecondMultiplier[ 0].rounded_value # 'incubationTime' = 100 / (Egg Lose Durability Per Second × Extra Egg Lose Durability Per Second Multiplier) try: data['incubationTime'] = cd(100 / egg_decay / extra_egg_decay_m) except ZeroDivisionError: logger.warning( f"Species {char_props.get_source().asset.assetname} tried dividing by zero for its incubationTime" ) data['eggTempMin'] = egg_props.EggMinTemperature[0] data['eggTempMax'] = egg_props.EggMaxTemperature[0] # 'maturationTime' = 1 / (Baby Age Speed × Extra Baby Age Speed Multiplier) baby_age_speed = char_props.BabyAgeSpeed[0].rounded_value extra_baby_age_speed_m = char_props.ExtraBabyAgeSpeedMultiplier[ 0].rounded_value try: data['maturationTime'] = cd(1 / baby_age_speed / extra_baby_age_speed_m) except ZeroDivisionError: logger.warning( f"Species {char_props.get_source().asset.assetname} tried dividing by zero for its maturationTime" ) data['matingCooldownMin'] = char_props.NewFemaleMinTimeBetweenMating[0] data['matingCooldownMax'] = char_props.NewFemaleMaxTimeBetweenMating[0] return data