Beispiel #1
0
 def __init__(self, description="A curved line graph.", **kwargs):
     super().__init__(
         description=description,
         points=tunable.TunableList(
             description="The points that make up this curved line.",
             tunable=TunableCurvePoint()),
         **kwargs)
Beispiel #2
0
class ByTypeRegistrationInformation(RegistrationInformation):
    FACTORY_TUNABLES = {
        "Types":
        tunable.TunableList(
            description=
            "The types of game object that the interaction should be attached to. A corresponding type determiner python function needs to also exist and be setup for object registration to occur.",
            tunable=tunable.Tunable(tunable_type=str, default="Everything"))
    }

    Types: typing.Tuple[str, ...]

    def GetRelevantObjectTypes(self) -> typing.Set[str]:
        """
		Get a list of the types of game object that the interaction should be attached to.
		"""

        return set(self.Types)
Beispiel #3
0
class ByDefinitionInstanceIDRegistrationInformation(RegistrationInformation):
    FACTORY_TUNABLES = {
        "InstanceIDs":
        tunable.TunableList(
            description=
            "The instance id of any game object definition that the interaction should be attached to. The interaction would be attached to the definition's game object, it may be simpler to just designate the game object its self.",
            tunable=tunable.Tunable(tunable_type=int, default=0))
    }

    InstanceIDs: typing.Tuple[int, ...]

    def GetRelevantDefinitionInstanceIDs(self) -> typing.Set[int]:
        """
		Get a list of instances ids that point to the game object definitions that the interaction should be attached to.
		"""

        return set(self.InstanceIDs)
Beispiel #4
0
class ByObjectInstanceIDRegistrationInformation(RegistrationInformation):
    FACTORY_TUNABLES = {
        "InstanceIDs":
        tunable.TunableList(
            description=
            "The instance id of any game objects that this interaction should be attached to.",
            tunable=tunable.Tunable(tunable_type=int, default=0))
    }

    InstanceIDs: typing.Tuple[int, ...]

    def GetRelevantObjectInstanceIDs(self) -> typing.Set[int]:
        """
		Get a list of instances ids that point to the game objects that the interaction should be attached to.
		"""

        return set(self.InstanceIDs)
Beispiel #5
0
class UseReductionOptions(UseReductionBase, tunable.HasTunableSingletonFactory, tunable.AutoFactoryInit):
	FACTORY_TUNABLES = {
		"Options": tunable.TunableList(
			tunable = tunable.TunableVariant(
				inventoryStatistic = UseReductionInventoryStatistic.TunableFactory(),
				inventoryObject = UseReductionInventoryObject.TunableFactory(),
			)
		)
	}

	Options: typing.Tuple[UseReductionBase]

	def GetUseCount (self, targetSimInfo: sim_info.SimInfo) -> int:
		"""
		Get the number of uses of this woohoo safety method the target sim has left.
		:param targetSimInfo: The sim who we are getting the use count of this safety method for.
		:type targetSimInfo: sim_info.SimInfo
		:return: The number of uses of this woohoo safety method the target sim has left.
		:rtype: bool
		"""

		if not isinstance(targetSimInfo, sim_info.SimInfo):
			raise Exceptions.IncorrectTypeException(targetSimInfo, "targetSimInfo", (sim_info.SimInfo,))

		useCount = 0  # type: int

		for option in self.Options:  # type: UseReductionBase
			useCount += option.GetUseCount(targetSimInfo)

		return useCount

	def RemoveUse (self, targetSimInfo: sim_info.SimInfo) -> bool:
		"""
		Remove one or more uses from this woohoo safety method.
		:param targetSimInfo: The sim who we are removing a use of this safety method for.
		:type targetSimInfo: sim_info.SimInfo
		:return: Whether or not we could remove a use from the target sim.
		:rtype: bool
		"""
		for option in self.Options:  # type: UseReductionBase
			if option.RemoveUse(targetSimInfo):
				return True

		return False
Beispiel #6
0
class WoohooSafetyMethod(tunable.HasTunableSingletonFactory, tunable.AutoFactoryInit):
	class Requirement:
		def RequirementMet (self, targetSim: sim.Sim) -> bool:
			"""
			Whether or not the target sim meets the requirements.
			"""

			raise NotImplementedError()

	class ObjectRequirement(Requirement, tunable.HasTunableSingletonFactory, tunable.AutoFactoryInit):
		FACTORY_TUNABLES = {
			"RequiredObject": tunable.TunableReference(description = "A sim must have this object in their inventory in order to meet this requirement.", manager = services.get_instance_manager(resources.Types.OBJECT), pack_safe = True),
			"RequiredObjectTests": tests.TunableTestSet(description = "A set of tests the object must pass in order to this requirement to be meet.")
		}

		RequiredObject: typing.Optional[definition.Definition]
		RequiredObjectTests: tests.CompoundTestList

		def RequirementMet (self, targetSim: sim.Sim) -> bool:
			"""
			Whether or not the target sim meets the requirements.
			"""

			if not isinstance(targetSim, sim.Sim):
				raise Exceptions.IncorrectTypeException(targetSim, "targetSim", (sim.Sim,))

			if self.RequiredObject is None:
				return False

			inventoryComponent = targetSim.get_component(ComponentsTypes.INVENTORY_COMPONENT)  # type: ComponentsInventory.InventoryComponent

			if not inventoryComponent.has_item_with_definition(self.RequiredObject):
				return False
			else:
				if len(self.RequiredObjectTests) == 0:
					return True

			for matchingObject in inventoryComponent.get_items_with_definition_gen(self.RequiredObject):  # type: game_object.GameObject
				requiredObjectResolver = resolver.SingleObjectResolver(matchingObject)
				testResults = self.RequiredObjectTests.run_tests(requiredObjectResolver)  # type: typing.Optional[TestingUnit.TestResult]

				if not testResults:
					return False

			return True

	class PerformanceTypeInfo(tunable.HasTunableSingletonFactory, tunable.AutoFactoryInit):
		FACTORY_TUNABLES = {
			"ArrivingPercentage": tunable.TunableVariant(
				description = "The options to select the percentage of sperm that will arrive for this performance type. These values should be from 0 to 1.",
				default = "fixed",
				fixed = tunable.Tunable(description = "A fixed arriving percentage for this performance type.", tunable_type = float, default = 0.5),
				random = tunable.TunableInterval(description = "An arriving percentage, between the upper and lower bounds, will be randomly selected.", tunable_type = float, default_lower = 0.5, default_upper = 0.5, minimum = 0, maximum = 1),
				normalDistribution = Distribution.TunableNormalDistribution(description = "An arriving percentage will be randomly selected based on a normal distribution.", meanDefault = 0.5, standardDeviationDefault = 0),
			)
		}

		ArrivingPercentage: typing.Union[float, tunable.TunableInterval, Distribution.NormalDistribution]

		def GenerateSpermArrivingPercentage (self, seed: typing.Optional[int] = None) -> float:
			"""
			Get the percentage of sperm that should get past this woohoo device and make it from one sim to another. This will return a number from 0 to 1.
			"""

			if seed is None:
				seed = random.randint(-1000000000, 1000000000)  # type: int

			if not isinstance(seed, int):
				raise Exceptions.IncorrectTypeException(seed, "seed", (int, None))

			if isinstance(self.ArrivingPercentage, tunable.TunedInterval):
				arrivingPercentage = self.ArrivingPercentage.random_float(seed = seed + -712214524)  # type: float
			elif isinstance(self.ArrivingPercentage, Distribution.NormalDistribution):
				arrivingPercentage = self.ArrivingPercentage.GenerateValue(seed = seed + 38190718, minimum = 0, maximum = 1)  # type: float
			else:
				arrivingPercentage = self.ArrivingPercentage

			return arrivingPercentage

	FACTORY_TUNABLES = {
		"Requirements": tunable.TunableList(
			description = "A list of requirements to determine if a sim can use this method. Requirements may be split into groups, only one group needs to be valid for a sim to met the requirements. If there are no requirements we will assume this method can always be used.",
			tunable = tunable.TunableList(
				description = "An individual group for requirements. For this method to be usable, every criteria in at least one of these groups must be met.",
				tunable = tunable.TunableVariant(
					objectRequirement = ObjectRequirement.TunableFactory(),
				)
			)
		),
		"UsingByDefault": tunable.Tunable(description = "Whether or not sims will use this method if its available by default.", tunable_type = bool, default = True),

		"PerformanceTypes": tunable.TunableMapping(
			description = "A set of performance types that denote how effective this safety method was. A performance type will be randomly selected for a woohoo using the 'PerformanceTypeChances' value.",
			key_type = tunable.Tunable(tunable_type = str, default = "UNKNOWN_PERFORMANCE_TYPE"),
			value_type = PerformanceTypeInfo.TunableFactory()
		),
		"PerformanceTypeChances": Probability.TunableProbability(description = "The chances that each performance type will be selected. Each option's identifier should correspond with a performance type."),

		"CompoundingMethod": tunable.Tunable(description = "Whether or not this method may be used at the same time as another method. If more than one non-compound method are to be used, we will select the first method found.", tunable_type = bool, default = True),

		"HandleUseCommand": tunable.OptionalTunable(description = "A console command that needs to be called after this safety method is used. This command should take the woohoo safety method guid, the inseminated sim id, the source sim id, and the performance type, in that order. The method GUID, inseminated sim id, and source sim id parameters may not actually get valid ids.", tunable = tunable.Tunable(tunable_type = str, default = None))
	}

	Requirements: typing.Tuple[typing.Tuple[Requirement, ...], ...]
	UsingByDefault: bool

	PerformanceTypes: typing.Dict[str, PerformanceTypeInfo]
	PerformanceTypeChances: Probability.Probability

	CompoundingMethod: bool

	HandleUseCommand: typing.Optional[str]

	GUID = None  # type: typing.Optional[int]

	@property
	def HasRequirement (self) -> bool:
		"""
		Whether or not this safety method has requirements.
		"""

		if len(self.Requirements) == 0:
			return False

		for requirementGroup in self.Requirements:  # type: typing.Tuple[WoohooSafetyMethod.Requirement, ...]
			if len(requirementGroup) != 0:
				return True

		return False

	def GetUniqueSeed (self) -> int:
		"""
		Get a unique randomization seed for this woohoo safety method.
		"""

		random.seed(self.GUID)
		return random.randint(-1000000000, 1000000000)  # type: int

	def IsAvailable (self, targetSimInfo: sim_info.SimInfo) -> bool:
		"""
		Get whether or not the target sim can use this safety method. This may incorrectly return false for non-instanced sims; we cannot read the inventory
		of sims not instanced.
		"""

		if not isinstance(targetSimInfo, sim_info.SimInfo):
			raise Exceptions.IncorrectTypeException(targetSimInfo, "targetSimInfo", (sim_info.SimInfo,))

		if not self.HasRequirement:
			return True

		if not targetSimInfo.is_instanced():
			return False

		targetSim = targetSimInfo.get_sim_instance()  # type: sim.Sim

		for requirementGroup in self.Requirements:  # type: typing.Tuple[WoohooSafetyMethod.Requirement, ...]
			if len(requirementGroup) == 0:
				continue

			groupRequirementsMet = True  # type: bool

			for requirement in requirementGroup:  # type: WoohooSafetyMethod.Requirement
				if not requirement.RequirementMet(targetSim):
					groupRequirementsMet = False
					break

			if groupRequirementsMet:
				return True

		return False

	def SelectPerformanceType (self, seed: typing.Optional[int] = None) -> typing.Tuple[str, PerformanceTypeInfo]:
		"""
		Randomly select one of the performance type in this woohoo safety method.
		"""

		if seed is None:
			seed = random.randint(-1000000000, 1000000000)  # type: int

		if not isinstance(seed, int):
			raise Exceptions.IncorrectTypeException(seed, "seed", (int, None))

		if len(self.PerformanceTypes) == 0:
			raise Exception("Could not find any safety method performance type to select.\nGUID: %s" % self.GUID)

		if len(self.PerformanceTypeChances.Options) == 0:
			raise Exception("Could not select a performance type 'PerformanceTypeChances' has no options.\nGUID: %s" % self.GUID)

		performanceType = self.PerformanceTypeChances.ChooseOption(seed = seed + -443757754).Identifier  # type: str
		performanceTypeInfo = self.PerformanceTypes.get(performanceType, None)  # type: WoohooSafetyMethod.PerformanceTypeInfo

		if performanceTypeInfo is None:
			Debug.Log("Randomly selected performance type that doesn't exist.\nGUID: %s, Performance Type: %s" % (str(self.GUID), performanceType), This.Mod.Namespace, Debug.LogLevels.Error, group = This.Mod.Namespace, owner = __name__)
			return random.choice(self.PerformanceTypes)

		return performanceType, performanceTypeInfo

	def GenerateSpermArrivingPercentage (self, seed: typing.Optional[int] = None) -> float:
		"""
		Get the percentage of sperm that should get past this woohoo device and make it from one sim to another. This will return a number from 0 to 1.
		"""

		if seed is None:
			seed = random.randint(-1000000000, 1000000000)  # type: int

		if not isinstance(seed, int):
			raise Exceptions.IncorrectTypeException(seed, "seed", (int, None))

		performanceType, performanceTypeInfo = self.SelectPerformanceType(seed = seed + -443757754)  # type: str, WoohooSafetyMethod.PerformanceTypeInfo
		arrivingPercentage = performanceTypeInfo.GenerateSpermArrivingPercentage(seed + -16160599)  # type: float

		if arrivingPercentage < 0:
			Debug.Log("Safety method performance type generated an arriving percentage is less than 0.\nGUID: %s, Performance Type: %s" % (str(self.GUID), performanceType), This.Mod.Namespace, Debug.LogLevels.Error, group = This.Mod.Namespace, owner = __name__)
			arrivingPercentage = 0

		if arrivingPercentage > 1:
			Debug.Log("Safety method performance type generated an arriving percentage is greater than 1.\nGUID: %s, Performance Type: %s" % (str(self.GUID), performanceType), This.Mod.Namespace, Debug.LogLevels.Error, group = This.Mod.Namespace, owner = __name__)
			arrivingPercentage = 1

		return arrivingPercentage

	def HandlePostUse (self, inseminatedSimInfo: typing.Optional[sim_info.SimInfo], sourceSimInfo: typing.Optional[sim_info.SimInfo], performanceType: str) -> None:
		"""
		Handle the after effects of using this woohoo safety method.
		"""

		if not isinstance(inseminatedSimInfo, sim_info.SimInfo) and inseminatedSimInfo is not None:
			raise Exceptions.IncorrectTypeException(inseminatedSimInfo, "inseminatedSimInfo", (sim_info.SimInfo,))

		if not isinstance(inseminatedSimInfo, sim_info.SimInfo) and inseminatedSimInfo is not None:
			raise Exceptions.IncorrectTypeException(inseminatedSimInfo, "inseminatedSimInfo", (sim_info.SimInfo,))

		if not isinstance(performanceType, str):
			raise Exceptions.IncorrectTypeException(performanceType, "performanceType", (str,))

		if self.HandleUseCommand is not None:
			methodGUID = self.GUID if self.GUID is not None else 0  # type: int
			inseminatedSimID = inseminatedSimInfo.id if inseminatedSimInfo is not None else 0
			sourceSimID = sourceSimInfo.id if sourceSimInfo is not None else 0

			try:
				consoleCommand = " ".join((self.HandleUseCommand, str(methodGUID), str(inseminatedSimID), str(sourceSimID), performanceType))
				commands.execute(consoleCommand, None)
			except:
				Debug.Log("Failed to call handle post use command for woohoo safety method '%s'." % str(self.GUID), This.Mod.Namespace, Debug.LogLevels.Exception, group = This.Mod.Namespace, owner = __name__)
Beispiel #7
0
class RegistrationExtension:
    """
	Allows you to setup automatic registration of interactions to objects.
	"""

    RegisterAllObjects: bool

    # noinspection SpellCheckingInspection
    INSTANCE_TUNABLES = {
        "ObjectRegistrationInformation":
        tunable.TunableList(tunable=RegistrationInformationVariant()),
    }

    ObjectRegistrationInformation: typing.List[RegistrationInformation]

    _registeredObjects = dict(
    )  # type: typing.Dict[typing.Type[script_object.ScriptObject], typing.Set[str]]

    def __init_subclass__(cls, *args, **kwargs):
        super().__init_subclass__(*args, **kwargs)

        cls._registeredObjects = dict(
        )  # type: typing.Dict[typing.Type[script_object.ScriptObject], typing.Set[str]]

    @classmethod
    def RegisterToObjects(cls) -> None:
        """
		Add this interaction to the appropriate objects.
		"""

        definitionManager = services.definition_manager(
        )  # type: definition_manager.DefinitionManager

        objectTypeOrganizer = cls._GetObjectTypeOrganizer(
        )  # type: typing.Type[RegistrationTypes.ObjectTypeOrganizer]
        objectsByType = objectTypeOrganizer.GetObjectsByType(
        )  # type: typing.Dict[str, typing.Set[typing.Type[script_object.ScriptObject]]]

        for registrationInformation in cls.ObjectRegistrationInformation:  # type: RegistrationInformation
            relevantObjects = set(
            )  # type: typing.Set[typing.Type[script_object.ScriptObject]]

            for relevantObjectInstanceID in registrationInformation.GetRelevantObjectInstanceIDs(
            ):  # type: int
                relevantObjectKey = resources.get_resource_key(
                    relevantObjectInstanceID, resources.Types.OBJECT)
                matchingObject = definitionManager.types.get(
                    relevantObjectKey, None
                )  # type: typing.Optional[typing.Type[script_object.ScriptObject]]

                if matchingObject is None:
                    continue

                relevantObjects.add(matchingObject)

            for relevantDefinitionInstanceID in registrationInformation.GetRelevantDefinitionInstanceIDs(
            ):  # type: int
                matchingDefinition = definitionManager.get(
                    relevantDefinitionInstanceID
                )  # type: typing.Optional[definition.Definition]

                if matchingDefinition is None:
                    continue

                matchingObject = matchingDefinition.cls  # type: typing.Type[script_object.ScriptObject]

                relevantObjects.add(matchingObject)

            for relevantObjectType in registrationInformation.GetRelevantObjectTypes(
            ):  # type: str
                matchingObjects = objectsByType.get(
                    relevantObjectType, None
                )  # type: typing.Optional[typing.Set[typing.Type[script_object.ScriptObject]]]

                if matchingObjects is None:
                    continue

                relevantObjects.update(matchingObjects)

            for relevantObject in relevantObjects:  # type: typing.Type[script_object.ScriptObject]
                registeredInteractionLists = cls._registeredObjects.get(
                    relevantObject,
                    None)  # type: typing.Optional[typing.List[str]]

                if registeredInteractionLists is not None:
                    if registrationInformation.InteractionListAttribute in registeredInteractionLists:
                        continue

                registrationInformation.RegisterObject(relevantObject, cls)

                if registeredInteractionLists is not None:
                    registeredInteractionLists.append(
                        registrationInformation.InteractionListAttribute)
                else:
                    cls._registeredObjects[relevantObject] = {
                        registrationInformation.InteractionListAttribute
                    }

    @classmethod
    def _tuning_loaded_callback(cls):
        superObject = super()
        if hasattr(superObject, "_tuning_loaded_callback"):
            # noinspection PyProtectedMember
            superObject._tuning_loaded_callback()

        cls._RegisterRegistrationExtendedInteraction()

    @classmethod
    def _RegisterRegistrationExtendedInteraction(cls) -> None:
        _registrationExtensionInteractions.append(cls)

    @classmethod
    def _GetObjectTypeOrganizer(
            cls) -> typing.Type[RegistrationTypes.ObjectTypeOrganizer]:
        return RegistrationTypes.ObjectTypeOrganizer
Beispiel #8
0
	def __init__ (self, description = "An object to randomly select from a set of options.", **kwargs):  # TODO add the spacings the ea's tunables use to descriptions?
		super().__init__(
			description = description,
			options = tunable.TunableList(description = "The set of options for the probability object.", tunable = TunableOption()),
			**kwargs)
Beispiel #9
0
class CyclePhaseTest(tunable.HasTunableSingletonFactory,
                     tunable.AutoFactoryInit):
    class PhaseState(tunable.HasTunableSingletonFactory,
                     tunable.AutoFactoryInit):
        class MatchTypes(enum_lib.IntEnum):
            Whitelist = 1  # type: CyclePhaseTest.PhaseState.MatchTypes
            Blacklist = 2  # type: CyclePhaseTest.PhaseState.MatchTypes

        FACTORY_TUNABLES = {
            "Phase":
            ToolsTunable.TunablePythonEnumEntry(
                description="The targeted phase of a menstrual cycle.",
                enumType=CycleShared.MenstrualCyclePhases,
                default=CycleShared.MenstrualCyclePhases.Menstruation),
            "MatchType":
            ToolsTunable.TunablePythonEnumEntry(
                description=
                "Whether the specified phase is whitelisted or blacklisted. If there are no whitelisted states we will assume that all states are valid unless told otherwise by blacklisted states.",
                enumType=MatchTypes,
                default=MatchTypes.Whitelist),
            "StartCompletion":
            tunable.OptionalTunable(tunable=tunable.TunableRange(
                description=
                "The completion percentage for the targeted phase at which this test will start matching.",
                tunable_type=float,
                default=0,
                minimum=0,
                maximum=1)),
            "EndCompletion":
            tunable.OptionalTunable(tunable=tunable.TunableRange(
                description=
                "The completion percentage for the targeted phase at which this test will stop matching.",
                tunable_type=float,
                default=0,
                minimum=0,
                maximum=1))
        }

        Phase: CycleShared.MenstrualCyclePhases
        MatchType: MatchTypes
        StartCompletion: typing.Optional[float]
        EndCompletion: typing.Optional[float]

    FACTORY_TUNABLES = {
        "PhaseStates":
        tunable.TunableList(tunable=PhaseState.TunableFactory(
            description=
            "A list of phase states that the menstrual cycle must either match or not match. Letting this be empty indicates that the menstrual cycle will always be valid."
        ))
    }

    PhaseStates: typing.Tuple[PhaseState, ...]

    def InValidPhase(self,
                     testingCycle: CycleMenstrual.MenstrualCycle) -> bool:
        """
		Get whether or not this cycle is in a valid phase for this test.
		"""

        if not isinstance(testingCycle, CycleMenstrual.MenstrualCycle):
            raise Exceptions.IncorrectTypeException(
                testingCycle, "testingCycle",
                (CycleMenstrual.MenstrualCycle, ))

        if len(self.PhaseStates) == 0:
            return True

        validState = True  # type: bool

        hasWhitelistedPhase = False  # type: bool
        inWhitelistedPhase = False  # type: bool

        for phaseState in self.PhaseStates:  # type: CyclePhaseTest.PhaseState
            if phaseState.MatchType == CyclePhaseTest.PhaseState.MatchTypes.Whitelist:
                hasWhitelistedPhase = True

            if not testingCycle.GetPhaseIsActive(phaseState.Phase):
                continue

            phaseCompletion = testingCycle.GetPhaseCompletionPercentage(
                phaseState.Phase)  # type: typing.Optional[float]

            if phaseCompletion is None:
                continue

            if phaseState.StartCompletion is not None and phaseState.StartCompletion > phaseCompletion:
                continue

            if phaseState.EndCompletion is not None and phaseState.EndCompletion < phaseCompletion:
                continue

            if phaseState.MatchType == CyclePhaseTest.PhaseState.MatchTypes.Whitelist:
                inWhitelistedPhase = True
                continue
            elif phaseState.MatchType == CyclePhaseTest.PhaseState.MatchTypes.Blacklist:
                validState = False
                break

        if hasWhitelistedPhase and not inWhitelistedPhase:
            validState = False

        return validState

    def TicksUntilValidPhase(
            self, testingCycle: CycleMenstrual.MenstrualCycle,
            reproductiveTimeMultiplier: float) -> typing.Optional[int]:
        """
		Get the number of game ticks until the input cycle reaches a valid phase. This will return none if the cycle will never enter a valid phase.
		:param testingCycle: The menstrual cycle to find valid phases for.
		:type testingCycle: CycleMenstrual.MenstrualCycle
		:param reproductiveTimeMultiplier: The reproductive time multiplier used to simulate the testing cycle
		:type reproductiveTimeMultiplier: float
		"""

        if not isinstance(testingCycle, CycleMenstrual.MenstrualCycle):
            raise Exceptions.IncorrectTypeException(
                testingCycle, "testingCycle",
                (CycleMenstrual.MenstrualCycle, ))

        if len(self.PhaseStates) == 0:
            return 0

        hasWhitelistedPhase = False  # type: bool

        whitelistedTimes = list(
        )  # type: typing.List[typing.Tuple[float, float]]
        blacklistedTimes = list(
        )  # type: typing.List[typing.Tuple[float, float]]

        for phaseState in self.PhaseStates:
            if phaseState.MatchType == CyclePhaseTest.PhaseState.MatchTypes.Whitelist:
                hasWhitelistedPhase = True

            if testingCycle.GetPhaseCompleted(phaseState.Phase):
                continue

            timeUntilPhaseStart = testingCycle.GetTimeUntilPhaseStarts(
                phaseState.Phase)  # type: float
            timeUntilPhaseEnd = testingCycle.GetTimeUntilPhaseEnds(
                phaseState.Phase)  # type: float

            if timeUntilPhaseEnd < 0:
                Debug.Log(
                    "A menstrual cycle indicated a phase was not complete, but its end time was %s minutes ago. Phase: %s"
                    % (str(timeUntilPhaseEnd), str(phaseState.Phase)),
                    This.Mod.Namespace,
                    Debug.LogLevels.Warning,
                    group=This.Mod.Namespace,
                    owner=__name__,
                    lockIdentifier=__name__ + ":" +
                    str(Python.GetLineNumber()))
                continue

            phaseLength = timeUntilPhaseEnd - timeUntilPhaseStart  # type: float

            if phaseLength <= 0:
                Debug.Log(
                    "Calculated a menstrual cycle phase length as less than or equal to 0. Phase: %s"
                    % str(phaseState.Phase),
                    This.Mod.Namespace,
                    Debug.LogLevels.Warning,
                    group=This.Mod.Namespace,
                    owner=__name__,
                    lockIdentifier=__name__ + ":" +
                    str(Python.GetLineNumber()))
                continue

            if phaseState.StartCompletion is None:
                listedStartTime = timeUntilPhaseStart
            else:
                listedStartTime = timeUntilPhaseStart + phaseLength * phaseState.StartCompletion

            if phaseState.EndCompletion is None:
                listedEndTime = timeUntilPhaseEnd
            else:
                listedEndTime = timeUntilPhaseStart + phaseLength * phaseState.EndCompletion

            if phaseState.MatchType == CyclePhaseTest.PhaseState.MatchTypes.Whitelist:
                whitelistedTimes.append((listedStartTime, listedEndTime))
            elif phaseState.MatchType == CyclePhaseTest.PhaseState.MatchTypes.Blacklist:
                blacklistedTimes.append((listedStartTime, listedEndTime))

        soonestValidTime = None  # type: typing.Optional[float]

        def setTimeIfSooner(testingValidTime: float) -> None:
            nonlocal soonestValidTime

            if soonestValidTime is None:
                soonestValidTime = testingValidTime

            if soonestValidTime < testingValidTime:
                soonestValidTime = testingValidTime

        if hasWhitelistedPhase:
            for whitelistedStartTime, whitelistedEndTime in whitelistedTimes:  # type: float, float
                if soonestValidTime is not None and whitelistedStartTime >= soonestValidTime:
                    continue

                validTime = whitelistedStartTime  # type: float

                intervalBlacklisted = False  # type: bool
                for blackListedStartTime, blackListedEndTime in blacklistedTimes:  # type: float, float
                    if blackListedStartTime <= validTime and blackListedEndTime >= whitelistedEndTime:
                        intervalBlacklisted = True
                        break

                    if blackListedStartTime <= validTime:
                        validTime = blackListedEndTime

                if not intervalBlacklisted:
                    setTimeIfSooner(max(validTime, 0.0))

                if soonestValidTime == 0:
                    break
        else:
            for blackListedStartTime, blackListedEndTime in blacklistedTimes:  # type: float, float
                if soonestValidTime is not None and blackListedEndTime >= soonestValidTime:
                    continue

                blacklistContinues = False  # type: bool
                for otherBlackListedStartTime, otherBlackListedEndTime in blacklistedTimes:  # type: float, float
                    if otherBlackListedStartTime <= blackListedEndTime < otherBlackListedEndTime:
                        blacklistContinues = True
                        pass

                if not blacklistContinues:
                    setTimeIfSooner(max(blackListedEndTime, 0.0))

                if soonestValidTime == 0:
                    break

        if soonestValidTime is None:
            return None
        else:
            return ReproductionShared.ReproductiveMinutesToTicks(
                soonestValidTime, reproductiveTimeMultiplier)