class SpawnPoint:
    __qualname__ = 'SpawnPoint'
    ARRIVAL_SPAWN_POINT_TAG = TunableEnumEntry(description='\n        The Tag associated with Spawn Points at the front of the lot.\n        ', tunable_type=Tag, default=Tag.INVALID)
    VISITOR_ARRIVAL_SPAWN_POINT_TAG = TunableEnumEntry(description='\n        The Tag associated with Spawn Points nearby the lot for visitors.\n        ', tunable_type=Tag, default=Tag.INVALID)

    def __init__(self, lot_id, zone_id, routing_surface=None):
        self.center = None
        self.lot_id = lot_id
        self._tags = set()
        if routing_surface is None:
            routing_surface = routing.SurfaceIdentifier(zone_id, 0, routing.SURFACETYPE_WORLD)
        self._routing_surface = routing_surface
        self._valid_slots = 0
        self._on_spawn_points_changed = CallableList()

    def __str__(self):
        return 'Name:{:20} Lot:{:15} Center:{:45} Tags:{}'.format(self.get_name(), self.lot_id, self.center, self.get_tags())

    @property
    def routing_surface(self):
        return self._routing_surface

    def get_name(self):
        raise NotImplementedError

    def get_tags(self):
        return self._tags

    def has_tag(self, tag):
        if tag is not None:
            return tag in self.get_tags()
        return False

    def next_spawn_spot(self):
        raise NotImplementedError

    def get_slot_pos(self, index=None):
        raise NotImplementedError

    @property
    def valid_slots(self):
        return self._valid_slots

    def reset_valid_slots(self):
        self._valid_slots = 0

    def register_spawn_point_changed_callback(self, callback):
        self._on_spawn_points_changed.append(callback)

    def unregister_spawn_point_changed_callback(self, callback):
        self._on_spawn_points_changed.remove(callback)

    def get_slot_positions(self):
        raise NotImplementedError
Exemple #2
0
class UiDialogBase:

	def __init__ (self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self.response = None
		self._listeners = CallableList()

	def add_listener (self, listener_callback):
		self._listeners.append(listener_callback)

	def distribute_dialog (self, dialog_type, dialog_msg, immediate = False):
		distributor = Distributor.instance()
		distributor.add_event(dialog_type, dialog_msg, immediate = immediate)

	def get_phone_ring_type (self):
		return PhoneRingType.NO_RING

	@property
	def responses (self):
		return tuple()

	def has_responses (self):
		return self.responses or self._additional_responses

	def on_response_received (self):
		pass

	def respond (self, response: int) -> bool:
		try:
			self.response = response
			self._listeners(self)
			return True
		finally:
			self.on_response_received()
		return False

	def show_dialog (self, on_response = None, **kwargs):
		if on_response is not None:
			self.add_listener(on_response)
		pythonutils.try_highwater_gc()
		services.ui_dialog_service().dialog_show(self, self.get_phone_ring_type(), **kwargs)

	def do_auto_respond (self, auto_response = DEFAULT):
		if auto_response is not DEFAULT:
			response = auto_response
		elif ButtonType.DIALOG_RESPONSE_CANCEL in self.responses:
			response = ButtonType.DIALOG_RESPONSE_CANCEL
		elif ButtonType.DIALOG_RESPONSE_OK in self.responses:
			response = ButtonType.DIALOG_RESPONSE_OK
		else:
			response = ButtonType.DIALOG_RESPONSE_CLOSED
		services.ui_dialog_service().dialog_respond(self.dialog_id, response)
Exemple #3
0
 def detach(self, *detaching_objects):
     global _nested_arb_detach_callbacks
     if self.master not in detaching_objects:
         for detaching_object in detaching_objects:
             self._attached_actors.remove(detaching_object)
         super().detach(*detaching_objects)
         return
     if _nested_arb_depth > 0:
         if _nested_arb_detach_callbacks is None:
             _nested_arb_detach_callbacks = CallableList()
         super_self = super()
         _nested_arb_detach_callbacks.append(
             lambda: super_self.detach(*self._attached_actors))
         return True
     if _nested_arb_detach_callbacks is not None:
         cl = _nested_arb_detach_callbacks
         _nested_arb_detach_callbacks = None
         cl()
     super().detach(*self._attached_actors)
Exemple #4
0
class ReservationMixin:
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._reservation_handlers = ()
        self._on_reservation_handlers_changed = None
        self._reservation_clobberers = None

    @property
    def in_use(self):
        if self._reservation_handlers:
            return True
        return False

    @property
    def self_or_part_in_use(self):
        if self._reservation_handlers:
            return True
        elif self.parts:
            return any(part.in_use for part in self.parts)
        return False

    def in_use_by(self, sim, owner=None):
        for handler in self._reservation_handlers:
            if handler.sim is not sim:
                continue
            if owner is not None and handler.reservation_interaction is not owner:
                continue
            return True
        return False

    def get_users(self, sims_only=False):
        users = set(handler.sim for handler in self._reservation_handlers
                    if not sims_only or handler.sim.is_sim)
        if self.parts:
            for part in self.parts:
                users |= part.get_users(sims_only=sims_only)
        return frozenset(users)

    def get_reservation_handler(self, sim, **kwargs):
        reservation_type = ReservationHandlerBasic if not self.parts else ReservationHandlerAllParts
        return reservation_type(sim, self, **kwargs)

    def get_use_list_handler(self, sim, **kwargs):
        return ReservationHandlerUseList(sim, self, **kwargs)

    def may_reserve(self,
                    reserver,
                    *_,
                    reservation_handler=None,
                    _from_reservation_call=False,
                    **kwargs):
        if reservation_handler is None:
            reservation_handler = self.get_reservation_handler(reserver)
        reserve_result = reservation_handler.may_reserve_internal(**kwargs)
        if gsi_handlers.sim_handlers_log.sim_reservation_archiver.enabled and reserver.is_sim:
            reserve_result_str = '{}: {}'.format(
                'reserve' if not _from_reservation_call else 'may_reserve',
                reserve_result)
            gsi_handlers.sim_handlers_log.archive_sim_reservation(
                reservation_handler, reserve_result_str)
        return reserve_result

    def add_reservation_handler(self, reservation_handler):
        if isinstance(self._reservation_handlers, tuple):
            self._reservation_handlers = WeakSet()
        self._reservation_handlers.add(reservation_handler)
        if self._on_reservation_handlers_changed:
            self._on_reservation_handlers_changed(user=reservation_handler.sim,
                                                  added=True)

    def get_reservation_handlers(self):
        return tuple(self._reservation_handlers)

    def remove_reservation_handler(self, reservation_handler):
        if not self._reservation_handlers:
            return
        self._reservation_handlers.discard(reservation_handler)
        if self._on_reservation_handlers_changed:
            self._on_reservation_handlers_changed(user=reservation_handler.sim,
                                                  added=False)

    def add_reservation_clobberer(self, reservation_holder,
                                  reservation_clobberer):
        if self._reservation_clobberers is None:
            self._reservation_clobberers = defaultdict(WeakSet)
        self._reservation_clobberers[reservation_holder].add(
            reservation_clobberer)

    def is_reservation_clobberer(self, reservation_holder,
                                 reservation_clobberer):
        if self._reservation_clobberers is None:
            return False
        if reservation_holder not in self._reservation_clobberers:
            return False
        return reservation_clobberer in self._reservation_clobberers[
            reservation_holder]

    def remove_reservation_clobberer(self, reservation_holder,
                                     reservation_clobberer):
        if self._reservation_clobberers is None:
            return
        if reservation_holder not in self._reservation_clobberers:
            return
        self._reservation_clobberers[reservation_holder].discard(
            reservation_clobberer)
        if not self._reservation_clobberers[reservation_holder]:
            del self._reservation_clobberers[reservation_holder]
        if not self._reservation_clobberers:
            self._reservation_clobberers = None

    def on_reset_get_interdependent_reset_records(self, reset_reason,
                                                  reset_records):
        super().on_reset_get_interdependent_reset_records(
            reset_reason, reset_records)
        relevant_sims = self.get_users(sims_only=True)
        for sim in relevant_sims:
            if self.reset_reason() == ResetReason.BEING_DESTROYED:
                reset_records.append(
                    ResetRecord(sim, ResetReason.RESET_EXPECTED, self,
                                'In use list of object being destroyed.'))
            else:
                body_target_part_owner = sim.posture_state.body.target
                if body_target_part_owner is not None:
                    if body_target_part_owner.is_part:
                        body_target_part_owner = body_target_part_owner.part_owner
                transition_controller = sim.queue.transition_controller
                if not body_target_part_owner is self:
                    if not transition_controller is None:
                        if not transition_controller.will_derail_if_given_object_is_reset(
                                self):
                            reset_records.append(
                                ResetRecord(sim, ResetReason.RESET_EXPECTED,
                                            self, 'Transitioning To or In.'))
                reset_records.append(
                    ResetRecord(sim, ResetReason.RESET_EXPECTED, self,
                                'Transitioning To or In.'))

    def usable_by_transition_controller(self, transition_controller):
        if transition_controller is None:
            return False
        required_sims = transition_controller.interaction.required_sims()
        targets = (self, ) + tuple(
            self.get_overlapping_parts()) if self.is_part else (self, )
        for reservation_handler in itertools.chain.from_iterable(
                target.get_reservation_handlers() for target in targets):
            if reservation_handler.sim in required_sims:
                continue
            reservation_interaction = reservation_handler.reservation_interaction
            if reservation_interaction is None:
                continue
            if reservation_interaction.priority >= transition_controller.interaction.priority:
                return False
            if transition_controller.interaction.priority <= Priority.Low:
                return False
        return True

    def register_on_use_list_changed(self, callback):
        if self._on_reservation_handlers_changed is None:
            self._on_reservation_handlers_changed = CallableList()
        self._on_reservation_handlers_changed.append(callback)

    def unregister_on_use_list_changed(self, callback):
        if callback in self._on_reservation_handlers_changed:
            self._on_reservation_handlers_changed.remove(callback)
            if not self._on_reservation_handlers_changed:
                self._on_reservation_handlers_changed = None
class InstanceManager:
    __qualname__ = 'InstanceManager'

    def __init__(self, path, type_enum, use_guid_for_ref=False):
        self._tuned_classes = {}
        self._callback_helper = {}
        self._verify_tunable_callback_helper = {}
        self._class_templates = []
        self.PATH = path
        self.TYPE = type_enum
        self._load_all_complete = False
        self._load_all_complete_callbacks = CallableList()
        self._use_guid_for_ref = use_guid_for_ref

    def add_on_load_complete(self, callback):
        if not self._load_all_complete:
            self._load_all_complete_callbacks.append(callback)
        else:
            callback(self)

    @property
    def all_instances_loaded(self):
        return self._load_all_complete

    def __str__(self):
        return 'InstanceManager_{}'.format(self.TYPE.name.lower())

    def get_changed_files(self):
        return sims4.core_services.file_change_manager().consume_set(self.TYPE)

    def reload_by_key(self, key):
        if not __debug__:
            raise RuntimeError('[manus] Reloading tuning is not supported for optimized python builds.')
        cls = self._tuned_classes.get(key)
        if cls is None:
            self.get(key)
            return
        try:
            sims4.tuning.serialization.restore_class_instance(cls)
            (tuning_callbacks, verify_callbacks) = sims4.tuning.serialization.load_from_xml(key, self.TYPE, cls, from_reload=True)
            if tuning_callbacks:
                for helper in tuning_callbacks:
                    helper.template.invoke_callback(cls, helper.name, helper.source, helper.value)
            if verify_callbacks:
                for helper in verify_callbacks:
                    helper.template.invoke_verify_tunable_callback(cls, helper.name, helper.source, helper.value)
            if hasattr(cls, TUNING_LOADED_CALLBACK):
                cls._tuning_loaded_callback()
            while hasattr(cls, VERIFY_TUNING_CALLBACK):
                cls._verify_tuning_callback()
        except:
            name = sims4.resources.get_name_from_key(key)
            logger.exception('Failed to reload tuning for {} (key:{}).', name, key, owner='manus')
            return
        return reload_dependencies_dict[key]

    @property
    def types(self):
        if not self.all_instances_loaded:
            logger.warn("Attempt to access instance types on '{}' before all instances are loaded", self)
        return self._tuned_classes

    def get_ordered_types(self, only_subclasses_of=object):

        def key(cls):
            result = tuple(x.__name__.lower() for x in reversed(cls.__mro__[:-1]))
            return (len(result), result)

        result = [c for c in self.types.values() if issubclass(c, only_subclasses_of)]
        result = sorted(result, key=key)
        return result

    @property
    def use_guid_for_ref(self):
        return self._use_guid_for_ref

    def register_class_template(self, template):
        self._class_templates.append(template)

    def register_tuned_class(self, instance, resource_key):
        if resource_key in self._tuned_classes:
            logger.info('Attempting to re-register class instance {} (Key:{}) with {}.', self._tuned_classes[resource_key], resource_key, self, owner='manus')
            return
        self._tuned_classes[resource_key] = instance
        instance.resource_key = resource_key
        if self.use_guid_for_ref:
            instance.guid64 = resource_key.instance

    def on_start(self):
        if sims4.core_services.SUPPORT_RELOADING_RESOURCES:
            file_change_manager = sims4.core_services.file_change_manager()
            if file_change_manager is not None:
                file_change_manager.create_set(self.TYPE, self.TYPE)

    def on_stop(self):
        if sims4.core_services.SUPPORT_RELOADING_RESOURCES:
            file_change_manager = sims4.core_services.file_change_manager()
            if file_change_manager is not None:
                file_change_manager.remove_set(self.TYPE)

    def create_class_instances(self):
        mtg = get_manager()
        res_id_list = mtg.get_all_res_ids(self.TYPE)
        logger.info('Creating {:4} tuning class instances managed by {}.', len(res_id_list), self, owner='manus')
        for (group_id, instance_id) in res_id_list:
            res_key = sims4.resources.Key(self.TYPE, instance_id, group_id)
            self._create_class_instance(res_key)

    def _create_class_instance(self, resource_key):
        cls = None
        try:
            registered_resource_key = sims4.resources.Key(self.TYPE, resource_key.instance)
            cls = sims4.tuning.serialization.create_class_instance(resource_key, self.TYPE)
            while cls is not None:
                self.register_tuned_class(cls, registered_resource_key)
        except Exception:
            if registered_resource_key in self._tuned_classes:
                del self._tuned_classes[registered_resource_key]
            logger.exception('An error occurred while attempting to create tuning instance: {}. Resource Key: {}.', cls, resource_key, owner='manus')

    def load_data_into_class_instances(self):
        logger.info('Loading {:4} tuning class instances managed by {}.', len(self._tuned_classes), self, owner='manus')
        for (key, cls) in self._tuned_classes.items():
            try:
                (tuning_callback_helpers, verify_tunable_callback_helpers) = sims4.tuning.serialization.load_from_xml(key, self.TYPE, cls)
                while tuning_callback_helpers:
                    self._callback_helper[cls] = tuning_callback_helpers
            except Exception:
                logger.exception('Exception while finalizing tuning for {}.', cls, owner='manus')

    def invoke_registered_callbacks_gen(self):
        logger.info('Invoking callbacks for {:4} tuning class instances managed by {}.', len(self._tuned_classes), self, owner='manus')
        invoke_verifications = False
        for cls in self._tuned_classes.values():
            self._invoke_tunable_callbacks(cls)
            if invoke_verifications:
                self._invoke_verify_tunable_callbacks(cls)
            try:
                if hasattr(cls, TUNING_LOADED_CALLBACK):
                    cls._tuning_loaded_callback()
                while invoke_verifications and hasattr(cls, VERIFY_TUNING_CALLBACK):
                    cls._verify_tuning_callback()
            except Exception:
                logger.exception('Exception in {}.{}.', cls, TUNING_LOADED_CALLBACK, owner='manus')
            yield False
        self._callback_helper = None
        self._verify_tunable_callback_helper = None
        self._load_all_complete = True
        self._load_all_complete_callbacks(self)

    def _invoke_tunable_callbacks(self, cls):
        tuning_callbacks = self._callback_helper.get(cls)
        if tuning_callbacks is None:
            return
        for helper in tuning_callbacks:
            try:
                helper.template.invoke_callback(cls, helper.name, helper.source, helper.value)
            except Exception:
                logger.exception('Exception in a tunable callback for variable {} in instance class {}.', helper.name, cls, owner='manus')

    def _invoke_verify_tunable_callbacks(self, cls):
        tuning_callbacks = self._verify_tunable_callback_helper.get(cls)
        if tuning_callbacks is None:
            return
        for helper in tuning_callbacks:
            try:
                helper.template.invoke_verify_tunable_callback(cls, helper.name, helper.source, helper.value)
            except Exception:
                logger.exception('Exception in a verify tunable callback for variable {} in instance class {}.', helper.name, cls, owner='manus')

    def get(self, name_or_id_or_key, pack_safe=False):
        key = sims4.resources.get_resource_key(name_or_id_or_key, self.TYPE)
        cls = self._tuned_classes.get(key)
        if cls is None:
            if not pack_safe:
                return
            raise UnavailablePackSafeResourceError
        return cls

    def _instantiate(self, target_type):
        return target_type()

    def export_descriptions(self, export_path, filter_fn=None):
        export_path = os.path.join(os.path.dirname(export_path), os.path.basename(self.PATH))
        export_path = os.path.join(export_path, 'Descriptions')
        to_export = {}
        for cls in sorted(self._class_templates, key=lambda cls: cls.__name__):
            cls_name = cls.__name__
            while filter_fn is None or filter_fn(cls):
                to_export[cls_name] = cls
        error_count = 0
        logger.info('TDESCs for {}: {}', self.TYPE.name, ', '.join([cls.__name__ for cls in to_export.values()]))
        for cls in to_export.values():
            result = sims4.tuning.serialization.export_class(cls, export_path, self.TYPE)
            while not result:
                error_count += 1
        return error_count

    def get_debug_statistics(self):
        result = []
        result.append(('TYPE', str(self.TYPE)))
        result.append(('PATH', str(self.PATH)))
        result.append(('UseGuidForReference', str(self._use_guid_for_ref)))
        result.append(('#TuningFiles', str(len(self._tuned_classes))))
        result.append(('#ClassTemplates', str(len(self._class_templates))))
        result.append(('LoadAllComplete', str(self._load_all_complete)))
        result.append(('#LoadAllCompelteCallbacks', str(len(self._load_all_complete_callbacks))))
        return result
Exemple #6
0
class UseListMixin:
    __qualname__ = 'UseListMixin'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._reservations = WeakKeyDictionary()
        self._reservations_multi = WeakKeyDictionary()
        self._use_list_changed_callbacks = CallableList()

    @property
    def using_sim(self):
        if not self._reservations:
            return
        return next(self._reservations.keys())

    @property
    def in_use(self):
        if self._reservations:
            return True
        return False

    def in_use_by(self, sim, owner=None):
        owners = self._reservations.get(sim)
        if not owners:
            return False
        return owner is None or owner in owners

    def _may_reserve_obj(self, sim, affordance, context):
        if self.in_use_by(sim):
            return True
        if self.children:
            for child in self.children:
                while not child.may_reserve(
                        sim, multi=False, affordance=affordance,
                        context=context):
                    return False
        return not self._reservations

    def _may_reserve_part(self,
                          sim,
                          affordance,
                          context,
                          check_overlapping_parts=True):
        part_list = self.part_owner.parts
        for part in part_list:
            if part is self:
                pass
            using_sim = part.using_sim
            while not using_sim is None:
                if using_sim is sim:
                    pass
                for si in using_sim.si_state:
                    reserve_object_tests = si.object_reservation_tests
                    while reserve_object_tests:
                        reserve_result = reserve_object_tests.run_tests(
                            si.get_resolver(target=sim))
                        if not reserve_result:
                            return reserve_result
                if using_sim.queue.transition_controller is not None:
                    transitioning_interaction = using_sim.queue.transition_controller.interaction
                    if transitioning_interaction.is_super:
                        reserve_object_tests = transitioning_interaction.object_reservation_tests
                        if reserve_object_tests:
                            target_sim = sim if transitioning_interaction.sim is not sim else using_sim
                            reserve_result = reserve_object_tests.run_tests(
                                transitioning_interaction.get_resolver(
                                    target=target_sim))
                            if not reserve_result:
                                return reserve_result
                while affordance is not None and affordance.is_super:
                    if context is None:
                        logger.error(
                            'Attempt to call may_reserve() with an affordance but no context!',
                            owner='maxr')
                    reserve_object_tests = affordance.object_reservation_tests
                    if reserve_object_tests:
                        reserve_result = reserve_object_tests.run_tests(
                            affordance.get_resolver(target=using_sim,
                                                    context=context))
                        if not reserve_result:
                            return reserve_result
        if affordance is not None and not self.supports_affordance(affordance):
            return TestResult(
                False, '{} does not support {}'.format(self, affordance))
        reserve_result = self._may_reserve_obj(sim,
                                               affordance=affordance,
                                               context=context)
        if not reserve_result:
            return reserve_result
        if check_overlapping_parts:
            for overlapping_part in self.get_overlapping_parts():
                if overlapping_part is self:
                    pass
                reserve_result = overlapping_part._may_reserve_part(
                    sim, None, None, check_overlapping_parts=False)
                while not reserve_result:
                    return reserve_result
        return TestResult(True, 'Passed all reservation tests.')

    def may_reserve(self, sim, multi=False, affordance=None, context=None):
        if self.parts:
            logger.callstack(
                'Cannot reserve an object that has parts: {}, for {} running {} - multi: {}',
                self,
                sim,
                affordance,
                multi,
                level=sims4.log.LEVEL_ERROR)
            return False
        if multi:
            return True
        if self.is_part:
            result = self._may_reserve_part(sim, affordance, context)
        else:
            result = self._may_reserve_obj(sim, affordance, context)
        return result

    def reserve(self, sim, owner, multi=False):
        use_list = self._reservations_multi if multi else self._reservations
        sim_list = setdefault_callable(use_list, sim, WeakSet)
        sim_list.add(owner)
        self._use_list_changed_callbacks(user=sim, added=True)

    def release(self, sim, owner, multi=False):
        use_list = self._reservations_multi if multi else self._reservations
        sim_list = use_list.get(sim)
        sim_list.remove(owner)
        if not sim_list:
            del use_list[sim]
        self._use_list_changed_callbacks(user=sim, added=False)
        self._destroy_if_necessary()

    def make_transient(self):
        self.transient = True
        self._destroy_if_necessary()

    def _destroy_if_necessary(self):
        if not self._reservations_multi and not self._reservations and self.transient:
            if self.is_part:
                self.part_owner.schedule_destroy_asap(
                    source=self, cause='Destroying unused transient part.')
            else:
                self.schedule_destroy_asap(
                    source=self, cause='Destroying unused transient object.')

    def usable_by_transition_controller(self, transition_controller):
        if transition_controller is not None:
            required_sims = transition_controller.interaction.required_sims()
            if self.is_part:
                all_overlapping_parts = self.get_overlapping_parts()
                all_overlapping_parts.append(self)
                for part in all_overlapping_parts:
                    for user in part.get_users():
                        while user not in required_sims:
                            return False
                return True
            for required_sim in required_sims:
                while self.in_use_by(required_sim):
                    return True
        return False

    def get_users(self, sims_only=False, include_multi=True):
        targets = (self, ) if not self.parts else self.parts
        if include_multi:
            return {
                sim
                for target in targets for sim in itertools.chain(
                    target._reservations, target._reservations_multi)
                if not sims_only or sim.is_sim
            }
        return {
            sim
            for target in targets for sim in target._reservations
            if not sims_only or sim.is_sim
        }

    def on_reset_get_interdependent_reset_records(self, reset_reason,
                                                  reset_records):
        super().on_reset_get_interdependent_reset_records(
            reset_reason, reset_records)
        relevant_sims = self.get_users(sims_only=True)
        for sim in relevant_sims:
            if self.reset_reason() == ResetReason.BEING_DESTROYED:
                reset_records.append(
                    ResetRecord(sim, ResetReason.RESET_EXPECTED, self,
                                'In use list of object being destroyed.'))
            body_target_part_owner = sim.posture_state.body.target
            if body_target_part_owner is not None and body_target_part_owner.is_part:
                body_target_part_owner = body_target_part_owner.part_owner
            transition_controller = sim.queue.transition_controller
            while body_target_part_owner is self or transition_controller is None or not transition_controller.will_derail_if_given_object_is_reset(
                    self):
                reset_records.append(
                    ResetRecord(sim, ResetReason.RESET_EXPECTED, self,
                                'Transitioning To or In.'))

    def register_on_use_list_changed(self, callback):
        self._use_list_changed_callbacks.append(callback)

    def unregister_on_use_list_changed(self, callback):
        if callback in self._use_list_changed_callbacks:
            self._use_list_changed_callbacks.remove(callback)

    def _print_in_use(self):
        if self._reservations:
            for sim in self._reservations:
                logger.debug('    Reservation: {}', sim)
        if self._reservations_multi:
            for sim in self._reservations_multi:
                logger.debug('    Reservation_Multi: {}', sim)
Exemple #7
0
class Client:
    _interaction_source = interactions.context.InteractionContext.SOURCE_PIE_MENU
    _interaction_priority = interactions.priority.Priority.High

    def __init__(self, session_id, account, household_id):
        self.id = session_id
        self.manager = None
        self._account = account
        self._household_id = household_id
        self._choice_menu = None
        self._interaction_parameters = {}
        self.active = True
        self.zone_id = services.current_zone_id()
        self._selectable_sims = SelectableSims(self)
        self._active_sim_info = None
        self._active_sim_changed = CallableList()
        self.ui_objects = weakref.WeakSet()
        self.primitives = ()
        self._live_drag_objects = []
        self._live_drag_start_system = LiveDragLocation.INVALID
        self._live_drag_is_stack = False
        self._live_drag_sell_dialog_active = False
        self.objects_moved_via_live_drag = WeakSet()

    def __repr__(self):
        return '<Client {0:#x}>'.format(self.id)

    @property
    def account(self):
        return self._account

    @distributor.fields.Field(op=distributor.ops.UpdateClientActiveSim)
    def active_sim_info(self):
        return self._active_sim_info

    resend_active_sim_info = active_sim_info.get_resend()

    @active_sim_info.setter
    def active_sim_info(self, sim_info):
        self._set_active_sim_without_field_distribution(sim_info)

    @property
    def active_sim(self):
        if self.active_sim_info is not None:
            return self.active_sim_info.get_sim_instance(
                allow_hidden_flags=ALL_HIDDEN_REASONS)

    @active_sim.setter
    def active_sim(self, sim):
        self.active_sim_info = sim.sim_info

    def _set_active_sim_without_field_distribution(self, sim_info):
        if self._active_sim_info is not None and self._active_sim_info is sim_info:
            return
        current_sim = self._active_sim_info.get_sim_instance(
        ) if self._active_sim_info is not None else None
        if sim_info is not None:
            self._active_sim_info = sim_info
            if sim_info.household is not None:
                sim_info.household.on_active_sim_changed(sim_info)
        else:
            self._active_sim_info = None
        self.notify_active_sim_changed(current_sim, new_sim_info=sim_info)

    @property
    def choice_menu(self):
        return self._choice_menu

    @property
    def interaction_source(self):
        return self._interaction_source

    @interaction_source.setter
    def interaction_source(self, value):
        if value is None:
            if self._interaction_source is not Client._interaction_source:
                del self._interaction_source
        else:
            self._interaction_source = value

    @property
    def interaction_priority(self):
        return self._interaction_priority

    @interaction_priority.setter
    def interaction_priority(self, value):
        if value is None:
            if self._interaction_priority is not Client._interaction_priority:
                del self._interaction_priority
        else:
            self._interaction_priority = value

    @property
    def household_id(self):
        return self._household_id

    @property
    def household(self):
        household_manager = services.household_manager()
        if household_manager is not None:
            return household_manager.get(self._household_id)

    @property
    def selectable_sims(self):
        return self._selectable_sims

    def create_interaction_context(self, sim, **kwargs):
        context = interactions.context.InteractionContext(
            sim,
            self.interaction_source,
            self.interaction_priority,
            client=self,
            **kwargs)
        return context

    @property
    def live_drag_objects(self):
        return self._live_drag_objects

    def get_interaction_parameters(self):
        return self._interaction_parameters

    def set_interaction_parameters(self, **kwargs):
        self._interaction_parameters = kwargs

    def set_choices(self, new_choices):
        self._choice_menu = new_choices

    def select_interaction(self, choice_id, revision):
        if self.choice_menu is not None:
            if revision == self.choice_menu.revision:
                choice_menu = self.choice_menu
                self._choice_menu = None
                self.set_interaction_parameters()
                try:
                    return choice_menu.select(choice_id)
                except:
                    if choice_menu.simref is not None:
                        sim = choice_menu.simref()
                        if sim is not None:
                            sim.reset(
                                ResetReason.RESET_ON_ERROR,
                                cause=
                                'Exception while selecting interaction from the pie menu.'
                            )
                    raise

    def get_create_op(self, *args, **kwargs):
        return distributor.ops.ClientCreate(self,
                                            *args,
                                            is_active=True,
                                            **kwargs)

    def get_delete_op(self):
        return distributor.ops.ClientDelete()

    def get_create_after_objs(self):
        active = self.active_sim
        if active is not None:
            yield active
        household = self.household
        if household is not None:
            yield household

    @property
    def valid_for_distribution(self):
        return True

    def refresh_achievement_data(self):
        active_sim_info = None
        if self.active_sim is not None:
            active_sim_info = self.active_sim.sim_info
        self.account.achievement_tracker.refresh_progress(active_sim_info)

    def send_message(self, msg_id, msg):
        if self.active:
            omega.send(self.id, msg_id, msg.SerializeToString())
        else:
            logger.warn(
                'Message sent to client {} after it has already disconnected.',
                self)

    def validate_selectable_sim(self):
        if self._active_sim_info is None or not self._active_sim_info.is_enabled_in_skewer:
            self.set_next_sim()

    def set_next_sim(self):
        sim_info = self._selectable_sims.get_next_selectable(
            self._active_sim_info)
        if sim_info is self.active_sim_info:
            self.resend_active_sim_info()
            return False
        return self.set_active_sim_info(sim_info)

    def set_next_sim_or_none(self, only_if_this_active_sim_info=None):
        if only_if_this_active_sim_info is not None and self._active_sim_info is not only_if_this_active_sim_info:
            return
        sim_info = self._selectable_sims.get_next_selectable(
            self._active_sim_info)
        if sim_info is None:
            return self.set_active_sim_info(None)
        if sim_info is self._active_sim_info:
            return self.set_active_sim_info(None)
        return self.set_active_sim_info(sim_info)

    def set_active_sim_by_id(self, sim_id):
        if self.active_sim_info is not None and self.active_sim_info.id == sim_id:
            return False
        for sim_info in self._selectable_sims:
            if sim_info.sim_id == sim_id:
                if not sim_info.is_enabled_in_skewer:
                    return False
                return self.set_active_sim_info(sim_info)
        return False

    def set_active_sim(self, sim):
        return self.set_active_sim_info(sim.sim_info)

    def set_active_sim_info(self, sim_info):
        with telemetry_helper.begin_hook(writer,
                                         TELEMETRY_HOOK_ACTIVE_SIM_CHANGED,
                                         sim_info=sim_info):
            pass
        self.active_sim_info = sim_info
        return self._active_sim_info is not None

    def add_selectable_sim_info(self, sim_info, send_relationship_update=True):
        self._selectable_sims.add_selectable_sim_info(
            sim_info, send_relationship_update=send_relationship_update)
        if self.active_sim_info is None:
            self.set_next_sim()
        self.household.refresh_aging_updates(sim_info)

    def add_selectable_sim_by_id(self, sim_id):
        sim_info = services.sim_info_manager().get(sim_id)
        if sim_info is not None:
            self.add_selectable_sim_info(sim_info)

    def remove_selectable_sim_info(self, sim_info):
        self._selectable_sims.remove_selectable_sim_info(sim_info)
        if self.active_sim_info is None:
            self.set_next_sim()
        self.household.refresh_aging_updates(sim_info)

    def remove_selectable_sim_by_id(self, sim_id):
        if len(self._selectable_sims) <= 1:
            return False
        sim_info = services.sim_info_manager().get(sim_id)
        if sim_info is not None:
            self.remove_selectable_sim_info(sim_info)
        return True

    def make_all_sims_selectable(self):
        self.clear_selectable_sims()
        for sim_info in services.sim_info_manager().objects:
            self._selectable_sims.add_selectable_sim_info(sim_info)
        self.set_next_sim()

    def clear_selectable_sims(self):
        self.active_sim_info = None
        self._selectable_sims.clear_selectable_sims()

    def register_active_sim_changed(self, callback):
        if callback not in self._active_sim_changed:
            self._active_sim_changed.append(callback)

    def unregister_active_sim_changed(self, callback):
        if callback in self._active_sim_changed:
            self._active_sim_changed.remove(callback)

    def on_sim_added_to_skewer(self, sim_info, send_relationship_update=True):
        if send_relationship_update:
            sim_info.relationship_tracker.send_relationship_info()
        is_zone_running = services.current_zone().is_zone_running
        sim_info.on_sim_added_to_skewer()
        if not is_zone_running:
            sim_info.commodity_tracker.start_low_level_simulation()
        else:
            services.active_household().distribute_household_data()
        sim_info.commodity_tracker.send_commodity_progress_update(
            from_add=True)
        sim_info.career_tracker.on_sim_added_to_skewer()
        if sim_info.degree_tracker is not None:
            sim_info.degree_tracker.on_sim_added_to_skewer()
        sim_info.send_whim_bucks_update(SetWhimBucks.LOAD)
        sim_info.resend_trait_ids()
        sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is not None:
            if is_zone_running:
                sim.inventory_component.visible_storage.allow_ui = True
                sim.inventory_component.publish_inventory_items()
            sim.ui_manager.refresh_ui_data()
            services.autonomy_service().logging_sims.add(sim)
            sim_info.start_aspiration_tracker_on_instantiation(
                force_ui_update=True)
            if sim_info.whim_tracker is not None:
                sim_info.whim_tracker.start_whims_tracker()
        zone_director = services.venue_service().get_zone_director()
        if zone_director is not None:
            zone_director.on_sim_added_to_skewer(sim_info)
        sim_info.trait_tracker.sort_and_send_commodity_list()

    def on_sim_removed_from_skewer(self, sim_info):
        sim = sim_info.get_sim_instance()
        if sim is not None:
            autonomy_service = services.autonomy_service()
            if autonomy_service is not None:
                autonomy_service.logging_sims.discard(sim)

    def clean_and_send_remaining_relationship_info(self):
        relationship_service = services.relationship_service()
        for sim_info in self.selectable_sims:
            sim_info.relationship_tracker.clean_and_send_remaining_relationship_info(
            )
            relationship_service.clean_and_send_remaining_object_relationships(
                sim_info.id)

    def cancel_live_drag_on_objects(self):
        for obj in self._live_drag_objects:
            obj.live_drag_component.cancel_live_dragging()
        self._live_drag_objects = []

    def _get_stack_items_from_drag_object(self,
                                          drag_object,
                                          remove=False,
                                          is_stack=False):
        if drag_object.inventoryitem_component is None:
            return (False, None)
        previous_inventory = drag_object.inventoryitem_component.get_inventory(
        )
        if previous_inventory is None:
            return (False, None)
        stack_id = drag_object.inventoryitem_component.get_stack_id()
        stack_items = previous_inventory.get_stack_items(stack_id)
        if remove:
            if is_stack:
                for item in stack_items:
                    success = previous_inventory.try_remove_object_by_id(
                        item.id, count=item.stack_count())
                else:
                    success = previous_inventory.try_remove_object_by_id(
                        drag_object.id, count=1)
                    stack_items = previous_inventory.get_stack_items(stack_id)
            else:
                success = previous_inventory.try_remove_object_by_id(
                    drag_object.id, count=1)
                stack_items = previous_inventory.get_stack_items(stack_id)
        else:
            success = True
        return (success, stack_items)

    def remove_drag_object_and_get_next_item(self, drag_object):
        next_object_id = None
        (success,
         stack_items) = self._get_stack_items_from_drag_object(drag_object,
                                                               remove=True)
        if success:
            if stack_items:
                next_object_id = stack_items[0].id
        return (success, next_object_id)

    def get_live_drag_object_value(self, drag_object, is_stack=False):
        (_, stack_items) = self._get_stack_items_from_drag_object(
            drag_object, remove=False, is_stack=is_stack)
        value = 0
        if is_stack and stack_items:
            for item in stack_items:
                value += item.current_value * item.stack_count()
        else:
            value = drag_object.current_value
        return value

    def start_live_drag(self,
                        live_drag_object,
                        start_system,
                        is_stack,
                        should_send_start_message: bool = True):
        self._live_drag_start_system = start_system
        success = True
        if is_stack:
            inventoryitem_component = live_drag_object.inventoryitem_component
            stack_id = inventoryitem_component.get_stack_id()
            current_inventory = inventoryitem_component.get_inventory()
            stack_items = current_inventory.get_stack_items(stack_id)
        else:
            stack_items = [live_drag_object]
        for item in stack_items:
            live_drag_component = live_drag_object.live_drag_component
            live_drag_component = item.live_drag_component
            if live_drag_component is None:
                logger_live_drag.error(
                    'Live Drag Start called on an object with no Live Drag Component. Object: {}'
                    .format(item))
                self.send_live_drag_cancel(live_drag_object.id)
                return
            if not ((not item.in_use or item.in_use_by(self))
                    and live_drag_component.can_live_drag):
                logger_live_drag.warn(
                    'Live Drag Start called on an object that is in use. Object: {}'
                    .format(item))
                self.send_live_drag_cancel(item.id)
                return
            success = live_drag_component.start_live_dragging(
                self, start_system)
            if not success:
                break
            self._live_drag_objects.append(item)
        if not success:
            self.cancel_live_drag_on_objects()
            self.send_live_drag_cancel(live_drag_object.id,
                                       LiveDragLocation.INVALID)
        self._live_drag_is_stack = is_stack
        if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled:
            gsi_handlers.live_drag_handlers.archive_live_drag(
                'Start',
                'Operation',
                LiveDragLocation.GAMEPLAY_SCRIPT,
                start_system,
                live_drag_object_id=live_drag_object.id)
        if live_drag_object.live_drag_component.active_household_has_sell_permission:
            sell_value = self.get_live_drag_object_value(
                live_drag_object, self._live_drag_is_stack
            ) if live_drag_object.definition.get_is_deletable() else -1
            for child_object in live_drag_object.get_all_children_gen():
                sell_value += self.get_live_drag_object_value(
                    child_object) if child_object.definition.get_is_deletable(
                    ) else 0
        else:
            sell_value = -1
        (valid_drop_object_ids,
         valid_stack_id) = live_drag_component.get_valid_drop_object_ids()
        icon_info = create_icon_info_msg(live_drag_object.get_icon_info_data())
        if should_send_start_message:
            op = distributor.ops.LiveDragStart(live_drag_object.id,
                                               start_system,
                                               valid_drop_object_ids,
                                               valid_stack_id, sell_value,
                                               icon_info)
            distributor_system = Distributor.instance()
            distributor_system.add_op_with_no_owner(op)

    def end_live_drag(self,
                      source_object,
                      target_object=None,
                      end_system=LiveDragLocation.INVALID,
                      location=None):
        live_drag_component = source_object.live_drag_component
        if live_drag_component is None:
            logger_live_drag.error(
                'Live Drag End called on an object with no Live Drag Component. Object: {}'
                .format(source_object))
            self.send_live_drag_cancel(source_object.id, end_system)
            return
        if source_object not in self._live_drag_objects:
            logger_live_drag.warn(
                'Live Drag End called on an object not being Live Dragged. Object: {}'
                .format(source_object))
            self.send_live_drag_cancel(source_object.id, end_system)
            return
        source_object_id = source_object.id
        if end_system == LiveDragLocation.BUILD_BUY:
            self.objects_moved_via_live_drag.add(source_object)
        else:
            self.objects_moved_via_live_drag.discard(source_object)
        self.cancel_live_drag_on_objects()
        next_object_id = None
        success = False
        inventory_item = source_object.inventoryitem_component
        if target_object is not None:
            live_drag_target_component = target_object.live_drag_target_component
            if live_drag_target_component is not None:
                (success, next_object_id
                 ) = live_drag_target_component.drop_live_drag_object(
                     source_object, self._live_drag_is_stack)
            elif source_object.parent_object() is target_object:
                success = True
            elif source_object.parent_object(
            ) is None and location is not None:
                inventory_item = source_object.inventoryitem_component
                if inventory_item is not None:
                    if inventory_item.is_in_inventory():
                        (success, next_object_id
                         ) = self.remove_drag_object_and_get_next_item(
                             source_object)
                source_object.set_location(location)
            else:
                logger_live_drag.error(
                    'Live Drag Target Component missing on object: {} and {} cannot be slotted into it.'
                    .format(target_object, source_object))
                success = False
        elif inventory_item is not None and inventory_item.is_in_inventory():
            if inventory_item.can_place_in_world or not inventory_item.inventory_only:
                if location is not None:
                    source_object.set_location(location)
                (success, next_object_id
                 ) = self.remove_drag_object_and_get_next_item(source_object)
        else:
            success = True
            if location is not None:
                source_object.set_location(location)
        if success:
            if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled:
                gsi_handlers.live_drag_handlers.archive_live_drag(
                    'End',
                    'Operation',
                    LiveDragLocation.GAMEPLAY_SCRIPT,
                    end_system,
                    live_drag_object_id=source_object_id,
                    live_drag_target=target_object)
            if not self._live_drag_is_stack:
                next_object_id = None
            op = distributor.ops.LiveDragEnd(source_object_id,
                                             self._live_drag_start_system,
                                             end_system, next_object_id)
            distributor_system = Distributor.instance()
            distributor_system.add_op_with_no_owner(op)
            self._live_drag_objects = []
            self._live_drag_start_system = LiveDragLocation.INVALID
            self._live_drag_is_stack = False
        else:
            self.send_live_drag_cancel(source_object_id, end_system)

    def cancel_live_drag(self,
                         live_drag_object,
                         end_system=LiveDragLocation.INVALID):
        live_drag_component = live_drag_object.live_drag_component
        if live_drag_component is None:
            logger_live_drag.warn(
                'Live Drag Cancel called on an object with no Live Drag Component. Object: {}'
                .format(live_drag_object))
            self.send_live_drag_cancel(live_drag_object.id)
            return
        if live_drag_component.live_drag_state == LiveDragState.NOT_LIVE_DRAGGING:
            logger_live_drag.warn(
                'Live Drag Cancel called on an object not being Live Dragged. Object: {}'
                .format(live_drag_object))
        else:
            self.cancel_live_drag_on_objects()
        self.send_live_drag_cancel(live_drag_object.id, end_system)

    def sell_live_drag_object(self,
                              live_drag_object,
                              end_system=LiveDragLocation.INVALID):
        live_drag_component = live_drag_object.live_drag_component
        if live_drag_component is None or not live_drag_object.definition.get_is_deletable(
        ):
            logger_live_drag.error(
                "Live Drag Sell called on object with no Live Drag Component or can't be deleted. Object: {}"
                .format(live_drag_object))
            self.send_live_drag_cancel(live_drag_object.id, end_system)
            return

        def sell_response(dialog):
            op = distributor.ops.LiveDragEnd(live_drag_object.id,
                                             self._live_drag_start_system,
                                             end_system,
                                             next_stack_object_id=None)
            distributor_system = Distributor.instance()
            distributor_system.add_op_with_no_owner(op)
            if not dialog.accepted:
                self.cancel_live_drag_on_objects()
                return
            value = int(
                self.get_live_drag_object_value(live_drag_object,
                                                self._live_drag_is_stack))
            for child_object in live_drag_object.get_all_children_gen():
                value += self.get_live_drag_object_value(
                    child_object) if child_object.definition.get_is_deletable(
                    ) else 0
            object_tags = set()
            if self._live_drag_is_stack:
                (_, stack_items) = self._get_stack_items_from_drag_object(
                    live_drag_object, remove=True, is_stack=True)
                for item in stack_items:
                    live_drag_component = item.live_drag_component
                    live_drag_component.cancel_live_dragging(
                        should_reset=False)
                    item.base_value = 0
                    item.set_stack_count(0)
                    object_tags.update(item.get_tags())
                    item.destroy(source=item,
                                 cause='Selling stack of live drag objects.')
            else:
                live_drag_object.live_drag_component.cancel_live_dragging(
                    should_reset=False)
                object_tags.update(live_drag_object.get_tags())
                if live_drag_object.is_in_inventory():
                    self.remove_drag_object_and_get_next_item(live_drag_object)
                else:
                    live_drag_object.remove_from_client()
                object_tags = frozenset(object_tags)
                live_drag_object.base_value = 0
                live_drag_object.destroy(source=live_drag_object,
                                         cause='Selling live drag object.')
            services.active_household().funds.add(
                value,
                Consts_pb2.TELEMETRY_OBJECT_SELL,
                self.active_sim,
                tags=object_tags)
            self._live_drag_objects = []
            self._live_drag_start_system = LiveDragLocation.INVALID
            self._live_drag_is_stack = False
            self._live_drag_sell_dialog_active = False

        favorites_tracker = self.active_sim_info.favorites_tracker
        if favorites_tracker and favorites_tracker.is_favorite_stack(
                live_drag_object):
            dialog = LiveDragTuning.LIVE_DRAG_SELL_FAVORITE_DIALOG(
                owner=live_drag_object)
        elif self._live_drag_is_stack:
            dialog = LiveDragTuning.LIVE_DRAG_SELL_STACK_DIALOG(
                owner=live_drag_object)
        else:
            dialog = LiveDragTuning.LIVE_DRAG_SELL_DIALOG(
                owner=live_drag_object)
        dialog.show_dialog(on_response=sell_response)
        self._live_drag_sell_dialog_active = True

    def send_live_drag_cancel(self,
                              live_drag_object_id,
                              live_drag_end_system=LiveDragLocation.INVALID):
        if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled:
            gsi_handlers.live_drag_handlers.archive_live_drag(
                'Cancel',
                'Operation',
                LiveDragLocation.GAMEPLAY_SCRIPT,
                live_drag_end_system,
                live_drag_object_id=live_drag_object_id)
        op = distributor.ops.LiveDragCancel(live_drag_object_id,
                                            self._live_drag_start_system,
                                            live_drag_end_system)
        distributor_system = Distributor.instance()
        distributor_system.add_op_with_no_owner(op)
        if not self._live_drag_sell_dialog_active:
            self._live_drag_objects = []
            self._live_drag_start_system = LiveDragLocation.INVALID
            self._live_drag_is_stack = False

    def on_add(self):
        if self._account is not None:
            self._account.register_client(self)
        for sim_info in self._selectable_sims:
            self.on_sim_added_to_skewer(sim_info)
        distributor = Distributor.instance()
        distributor.add_object(self)
        distributor.add_client(self)
        self.send_selectable_sims_update()
        self.selectable_sims.add_watcher(self,
                                         self.send_selectable_sims_update)

    def on_remove(self):
        if self.active_sim is not None:
            self._set_active_sim_without_field_distribution(None)
        if self._account is not None:
            self._account.unregister_client(self)
        for sim_info in self._selectable_sims:
            self.on_sim_removed_from_skewer(sim_info)
        self.selectable_sims.remove_watcher(self)
        distributor = Distributor.instance()
        distributor.remove_client(self)
        self._selectable_sims = None
        self.active = False

    def get_objects_in_view_gen(self):
        for manager in services.client_object_managers():
            for obj in manager.get_all():
                yield obj

    def notify_active_sim_changed(self, old_sim, new_sim_info=None):
        new_sim = new_sim_info.get_sim_instance(
        ) if new_sim_info is not None else None
        self._active_sim_changed(old_sim, new_sim)
        vfx_mask.notify_client_mask_update(new_sim_info)

    def _get_selector_visual_type(self, sim_info):
        if sim_info.is_baby:
            return (Sims_pb2.SimPB.BABY, None)
        if sim_info.is_toddler and services.daycare_service(
        ).is_sim_info_at_daycare(sim_info):
            return (Sims_pb2.SimPB.AT_DAYCARE, None)
        if sim_info.household.missing_pet_tracker.is_pet_missing(sim_info):
            return (Sims_pb2.SimPB.PET_MISSING, None)
        sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        for career in sim_info.careers.values():
            if career.currently_at_work:
                if career.is_at_active_event and sim is None:
                    return (Sims_pb2.SimPB.MISSING_ACTIVE_WORK,
                            career.career_category)
                return (Sims_pb2.SimPB.AT_WORK, career.career_category)
            if career.is_late:
                if not career.taking_day_off:
                    return (Sims_pb2.SimPB.LATE_FOR_WORK,
                            career.career_category)
        if services.get_rabbit_hole_service(
        ).should_override_selector_visual_type(sim_info.id):
            return (Sims_pb2.SimPB.OTHER, None)
        if sim is not None and sim.has_hidden_flags(
                HiddenReasonFlag.RABBIT_HOLE):
            return (Sims_pb2.SimPB.OTHER, None)
        if services.hidden_sim_service().is_hidden(sim_info.id):
            return (Sims_pb2.SimPB.OTHER, None)
        tutorial_service = services.get_tutorial_service()
        if tutorial_service is not None and tutorial_service.is_sim_unselectable(
                sim_info):
            return (Sims_pb2.SimPB.OTHER, None)
        return (Sims_pb2.SimPB.NORMAL, None)

    def send_selectable_sims_update(self):
        msg = Sims_pb2.UpdateSelectableSims()
        for sim_info in self._selectable_sims:
            with ProtocolBufferRollback(msg.sims) as new_sim:
                new_sim.id = sim_info.sim_id
                if sim_info.career_tracker is None:
                    logger.error(
                        'CareerTracker is None for selectable Sim {}'.format(
                            sim_info))
                else:
                    career = sim_info.career_tracker.get_currently_at_work_career(
                    )
                    new_sim.at_work = career is not None and not career.is_at_active_event
                new_sim.is_selectable = sim_info.is_enabled_in_skewer
                (selector_visual_type,
                 career_category) = self._get_selector_visual_type(sim_info)
                new_sim.selector_visual_type = selector_visual_type
                if career_category is not None:
                    new_sim.career_category = career_category
                new_sim.can_care_for_toddler_at_home = sim_info.can_care_for_toddler_at_home
                if not sim_info.is_instanced(
                        allow_hidden_flags=ALL_HIDDEN_REASONS):
                    new_sim.instance_info.zone_id = sim_info.zone_id
                    new_sim.instance_info.world_id = sim_info.world_id
                    new_sim.firstname = sim_info.first_name
                    new_sim.lastname = sim_info.last_name
                    zone_data_proto = services.get_persistence_service(
                    ).get_zone_proto_buff(sim_info.zone_id)
                    if zone_data_proto is not None:
                        new_sim.instance_info.zone_name = zone_data_proto.name
        distributor = Distributor.instance()
        distributor.add_op_with_no_owner(
            GenericProtocolBufferOp(Operation.SELECTABLE_SIMS_UPDATE, msg))

    @constproperty
    def is_sim():
        return False
class InstanceManager:

    def __init__(self, path, type_enum, use_guid_for_ref=False, base_game_only=False, require_reference=False):
        self._tuned_classes = {}
        self._remapped_keys = {}
        self._callback_helper = {}
        self._verify_tunable_callback_helper = {}
        self._class_templates = []
        self.PATH = path
        self.TYPE = type_enum
        self._load_all_complete = False
        self._load_all_complete_callbacks = CallableList()
        self._use_guid_for_ref = use_guid_for_ref
        self._base_game_only = base_game_only
        self._require_reference = require_reference

    def add_on_load_complete(self, callback):
        if not self._load_all_complete:
            self._load_all_complete_callbacks.append(callback)
        else:
            callback(self)

    @property
    def all_instances_loaded(self):
        return self._load_all_complete

    def __str__(self):
        return 'InstanceManager_{}'.format(self.TYPE.name.lower())

    def get_changed_files(self):
        return sims4.core_services.file_change_manager().consume_set(self.TYPE)

    def reload_by_key(self, key):
        raise RuntimeError('[manus] Reloading tuning is not supported for optimized python builds.')
        registered_resource_key = sims4.resources.Key(self.TYPE, key.instance)
        cls = self._tuned_classes.get(registered_resource_key)
        if cls is None:
            self.get(registered_resource_key)
            return
        try:
            sims4.tuning.serialization.restore_class_instance(cls)
            (tuning_callbacks, verify_callbacks) = sims4.tuning.serialization.load_from_xml(key, self.TYPE, cls, from_reload=True)
            if tuning_callbacks:
                for helper in tuning_callbacks:
                    helper.template.invoke_callback(cls, helper.name, helper.source, helper.value)
            if verify_callbacks:
                for helper in verify_callbacks:
                    helper.template.invoke_verify_tunable_callback(cls, helper.name, helper.source, helper.value)
            if hasattr(cls, TUNING_LOADED_CALLBACK):
                cls._tuning_loaded_callback()
            if hasattr(cls, VERIFY_TUNING_CALLBACK):
                cls._verify_tuning_callback()
        except:
            name = sims4.resources.get_name_from_key(key)
            logger.exception('Failed to reload tuning for {} (key:{}).', name, key, owner='manus')
            return
        return reload_dependencies_dict[key]

    @property
    def types(self):
        if not self.all_instances_loaded:
            logger.warn("Attempt to access instance types on '{}' before all instances are loaded", self)
        return self._tuned_classes

    def get_ordered_types(self, only_subclasses_of=object):

        def key(cls):
            result = tuple(x.__name__.lower() for x in reversed(cls.__mro__[:-1]))
            return (len(result), result)

        result = [c for c in self.types.values() if issubclass(c, only_subclasses_of)]
        result = sorted(result, key=key)
        return result

    @property
    def use_guid_for_ref(self):
        return self._use_guid_for_ref

    @property
    def base_game_only(self):
        return self._base_game_only

    @property
    def require_reference(self):
        return self._require_reference

    @property
    def remapped_keys(self):
        return self._remapped_keys

    def register_class_template(self, template):
        self._class_templates.append(template)

    def register_tuned_class(self, instance, resource_key):
        if resource_key in self._tuned_classes:
            logger.info('Attempting to re-register class instance {} (Key:{}) with {}.', self._tuned_classes[resource_key], resource_key, self, owner='manus')
            return
        self._tuned_classes[resource_key] = instance
        instance.resource_key = resource_key
        if self.use_guid_for_ref:
            instance.guid64 = resource_key.instance

    def on_start(self):
        if paths.SUPPORT_RELOADING_RESOURCES:
            file_change_manager = sims4.core_services.file_change_manager()
            if file_change_manager is not None:
                file_change_manager.create_set(self.TYPE, self.TYPE)

    def on_stop(self):
        if paths.SUPPORT_RELOADING_RESOURCES:
            file_change_manager = sims4.core_services.file_change_manager()
            if file_change_manager is not None:
                file_change_manager.remove_set(self.TYPE)

    def create_class_instances(self):
        mtg = get_manager()
        res_id_list = mtg.get_all_res_ids(self.TYPE)
        logger.info('Creating {:4} tuning class instances managed by {}.', len(res_id_list), self, owner='manus')
        for (group_id, instance_id) in res_id_list:
            res_key = sims4.resources.Key(self.TYPE, instance_id, group_id)
            self._create_class_instance(res_key)

    def _create_class_instance(self, resource_key):
        cls = None
        try:
            cls = sims4.tuning.serialization.create_class_instance(resource_key, self.TYPE)
            if cls is None:
                return
            registered_resource_key = sims4.resources.Key(self.TYPE, resource_key.instance)
            self.register_tuned_class(cls, registered_resource_key)
            if resource_key.group:
                self._remapped_keys[registered_resource_key] = resource_key
        except Exception:
            if registered_resource_key in self._tuned_classes:
                del self._tuned_classes[registered_resource_key]
            logger.exception('An error occurred while attempting to create tuning instance: {}. Resource Key: {}.', cls, resource_key, owner='manus')

    def load_data_into_class_instances(self):
        logger.info('Loading {:4} tuning class instances managed by {}.', len(self._tuned_classes), self, owner='manus')
        for (key, cls) in tuple(self._tuned_classes.items()):
            try:
                tuned_classes_key = key
                if key in self._remapped_keys:
                    key = self._remapped_keys[key]
                (tuning_callback_helpers, verify_tunable_callback_helpers) = sims4.tuning.serialization.load_from_xml(key, self.TYPE, cls)
                additional_pack = getattr(cls, 'additional_pack', None)
                if additional_pack is not None:
                    if not is_available_pack(cls.additional_pack):
                        del self._tuned_classes[tuned_classes_key]
                        continue
                self._callback_helper[cls] = tuning_callback_helpers
            except Exception:
                logger.exception('Exception while finalizing tuning for {}.', cls, owner='manus')
        self._remapped_keys = None

    def invoke_registered_callbacks_gen(self, registered_callbacks_timing_file=None):
        logger.info('Invoking callbacks for {:4} tuning class instances managed by {}.', len(self._tuned_classes), self, owner='manus')
        for cls in self._tuned_classes.values():
            start_time = time.time()
            callback_timing = self._invoke_tunable_callbacks(cls, registered_callbacks_timing_file is not None)
            invoke_time = time.time() - start_time
            try:
                if hasattr(cls, TUNING_LOADED_CALLBACK):
                    cls._tuning_loaded_callback()
            except Exception:
                logger.exception('Exception in {}.{}.', cls, TUNING_LOADED_CALLBACK, owner='manus')
            if registered_callbacks_timing_file is not None:
                registered_callbacks_timing_file.write('{},{},{},{}\n'.format(cls, time.time() - start_time, invoke_time, ','.join(callback_timing)))
            yield False
        self._callback_helper = None
        self._load_all_complete = True
        self._load_all_complete_callbacks(self)

    def _invoke_tunable_callbacks(self, cls, return_call_back_timing=False):
        callback_timing = []
        tuning_callbacks = self._callback_helper.get(cls)
        if tuning_callbacks is None:
            return callback_timing
        for helper in tuning_callbacks:
            start_time = time.time()
            try:
                helper.template.invoke_callback(cls, helper.name, helper.source, helper.value)
                if return_call_back_timing:
                    callback_timing.append('{}:{}'.format(helper.template.invoke_callback, time.time() - start_time))
            except Exception:
                logger.exception('Exception in a tunable callback for variable {} in instance class {}.', helper.name, cls, owner='manus')
        return callback_timing

    def invoke_verify_tuning_callback_gen(self):
        for cls in self._tuned_classes.values():
            self._invoke_verify_tunable_callbacks(cls)
            try:
                if hasattr(cls, VERIFY_TUNING_CALLBACK):
                    cls._verify_tuning_callback()
            except Exception:
                logger.exception('Exception in {}.{}.', cls, VERIFY_TUNING_CALLBACK, owner='manus')
            yield False
        self._verify_tunable_callback_helper = None

    def _invoke_verify_tunable_callbacks(self, cls):
        tuning_callbacks = self._verify_tunable_callback_helper.get(cls)
        if tuning_callbacks is None:
            return
        for helper in tuning_callbacks:
            try:
                helper.template.invoke_verify_tunable_callback(cls, helper.name, helper.source, helper.value)
            except Exception:
                logger.exception('Exception in a verify tunable callback for variable {} in instance class {}.', helper.name, cls, owner='manus')

    def get(self, name_or_id_or_key, pack_safe=False, get_fallback_definition_id=True):
        key = sims4.resources.get_resource_key(name_or_id_or_key, self.TYPE)
        cls = self._tuned_classes.get(key)
        if cls is None:
            if not pack_safe:
                return
            raise sims4.tuning.merged_tuning_manager.UnavailablePackSafeResourceError
        return cls

    def _instantiate(self, target_type):
        return target_type()

    def export_descriptions(self, export_path, filter_fn=None):
        export_path = os.path.join(os.path.dirname(export_path), os.path.basename(self.PATH))
        export_path = os.path.join(export_path, 'Descriptions')
        to_export = {}
        for cls in sorted(self._class_templates, key=lambda cls: cls.__name__):
            cls_name = cls.__name__
            if not filter_fn is None:
                if filter_fn(cls):
                    to_export[cls_name] = cls
            to_export[cls_name] = cls
        error_count = 0
        logger.info('TDESCs for {}: {}', self.TYPE.name, ', '.join([cls.__name__ for cls in to_export.values()]))
        for cls in to_export.values():
            result = sims4.tuning.serialization.export_class(cls, export_path, self.TYPE)
            if not result:
                error_count += 1
        return error_count

    def get_debug_statistics(self):
        result = []
        result.append(('TYPE', str(self.TYPE)))
        result.append(('PATH', str(self.PATH)))
        result.append(('UseGuidForReference', str(self._use_guid_for_ref)))
        result.append(('#TuningFiles', str(len(self._tuned_classes))))
        result.append(('#ClassTemplates', str(len(self._class_templates))))
        result.append(('LoadAllComplete', str(self._load_all_complete)))
        result.append(('#LoadAllCompelteCallbacks', str(len(self._load_all_complete_callbacks))))
        return result
Exemple #9
0
class ObjectManager(DistributableObjectManager, GameObjectManagerMixin,
                    AttractorManagerMixin):
    FIREMETER_DISPOSABLE_OBJECT_CAP = Tunable(
        int,
        5,
        description=
        'Number of disposable objects a lot can have at any given moment.')
    BED_TAGS = TunableTuple(
        description=
        '\n        Tags to check on an object to determine what type of bed an object is.\n        ',
        beds=TunableSet(
            description=
            '\n            Tags that consider an object as a bed other than double beds.\n            ',
            tunable=TunableEnumWithFilter(tunable_type=tag.Tag,
                                          default=tag.Tag.INVALID,
                                          filter_prefixes=BED_PREFIX_FILTER)),
        double_beds=TunableSet(
            description=
            '\n            Tags that consider an object as a double bed\n            ',
            tunable=TunableEnumWithFilter(tunable_type=tag.Tag,
                                          default=tag.Tag.INVALID,
                                          filter_prefixes=BED_PREFIX_FILTER)),
        kid_beds=TunableSet(
            description=
            '\n            Tags that consider an object as a kid bed\n            ',
            tunable=TunableEnumWithFilter(tunable_type=tag.Tag,
                                          default=tag.Tag.INVALID,
                                          filter_prefixes=BED_PREFIX_FILTER)),
        other_sleeping_spots=TunableSet(
            description=
            '\n            Tags that considered sleeping spots.\n            ',
            tunable=TunableEnumWithFilter(tunable_type=tag.Tag,
                                          default=tag.Tag.INVALID,
                                          filter_prefixes=BED_PREFIX_FILTER)))
    HOUSEHOLD_INVENTORY_OBJECT_TAGS = TunableTags(
        description=
        '\n        List of tags to apply to every household inventory proxy object.\n        '
    )
    INVALID_UNPARENTED_OBJECT_TAGS = TunableTags(
        description=
        '\n        Objects with these tags should not exist without a parent. An obvious\n        case is for transient objects. They should only exist as a carried object,\n        thus parented to a sim, when loading into a save game.\n        '
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._crafting_cache = CraftingObjectCache()
        self._sim_spawn_conditions = collections.defaultdict(set)
        self._water_terrain_object_cache = WaterTerrainObjectCache()
        self._client_connect_callbacks = CallableList()
        self._portal_cache = WeakSet()
        self._portal_added_callbacks = CallableList()
        self._portal_removed_callbacks = CallableList()
        self._front_door_candidates_changed_callback = CallableList()
        self._all_bed_tags = self.BED_TAGS.beds | self.BED_TAGS.double_beds | self.BED_TAGS.kid_beds | self.BED_TAGS.other_sleeping_spots
        self._tag_to_object_list = defaultdict(set)
        self._whim_set_cache = Counter()
        self._posture_providing_object_cache = None
        self._objects_to_ignore_portal_validation_cache = []

    @classproperty
    def save_error_code(cls):
        return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_OBJECT_MANAGER

    @property
    def crafting_cache(self):
        return self._crafting_cache

    @property
    def water_terrain_object_cache(self):
        return self._water_terrain_object_cache

    def portal_cache_gen(self):
        yield from self._portal_cache

    def on_client_connect(self, client):
        all_objects = list(self._objects.values())
        for game_object in all_objects:
            game_object.on_client_connect(client)

    def move_to_inventory(self, obj, inventory_manager):
        logger.assert_raise(
            isinstance(inventory_manager, InventoryManager),
            'Trying to move object to a non-inventory manager: {}',
            inventory_manager,
            owner='tingyul')
        logger.assert_raise(
            obj.id,
            'Attempting to move an object that was never added or has already been removed',
            owner='tingyul')
        logger.assert_raise(
            self._objects.get(obj.id) is obj,
            'Attempting to move an object {} that is not in this manager or not the same object {} in manager',
            obj,
            self._objects.get(obj.id),
            owner='tingyul')
        del self._objects[obj.id]
        obj.manager = inventory_manager
        inventory_manager._objects[obj.id] = obj
        self.remove_object_from_object_tags_cache(obj)
        self.remove_object_from_posture_providing_cache(obj)

    def add(self, obj, *args, **kwargs):
        super().add(obj, *args, **kwargs)
        self.add_object_to_object_tags_cache(obj)
        self.add_object_to_posture_providing_cache(obj)

    def remove(self, obj, *args, **kwargs):
        super().remove(obj, *args, **kwargs)
        current_zone = services.current_zone()
        if not current_zone.is_zone_shutting_down:
            self.remove_object_from_object_tags_cache(obj)
            self.remove_object_from_posture_providing_cache(obj)

    def add_object_to_object_tags_cache(self, obj):
        self.add_tags_and_object_to_cache(obj.get_tags(), obj)

    def add_tags_and_object_to_cache(self, tags, obj):
        if obj.id not in self:
            logger.error(
                "Trying to add object to tag cache when the object isn't in the manager: {}",
                obj,
                owner='tingyul')
            return
        for tag in tags:
            object_list = self._tag_to_object_list[tag]
            object_list.add(obj)

    def remove_object_from_object_tags_cache(self, obj):
        for tag in obj.get_tags():
            if tag not in self._tag_to_object_list:
                continue
            object_list = self._tag_to_object_list[tag]
            if obj not in object_list:
                continue
            object_list.remove(obj)
            if not object_list:
                del self._tag_to_object_list[tag]

    def _should_save_object_on_lot(self, obj):
        parent = obj.parent
        if parent is not None and parent.is_sim:
            inventory = parent.inventory_component
            if inventory.should_save_parented_item_to_inventory(obj):
                return False
            else:
                vehicle_component = obj.vehicle_component
                if vehicle_component is not None:
                    driver = vehicle_component.driver
                    if driver is not None and driver.is_sim:
                        inventory = driver.inventory_component
                        if inventory.should_save_parented_item_to_inventory(
                                obj):
                            return False
        else:
            vehicle_component = obj.vehicle_component
            if vehicle_component is not None:
                driver = vehicle_component.driver
                if driver is not None and driver.is_sim:
                    inventory = driver.inventory_component
                    if inventory.should_save_parented_item_to_inventory(obj):
                        return False
        return True

    def add_object_to_posture_providing_cache(self, obj):
        if not obj.provided_mobile_posture_affordances:
            return
        if self._posture_providing_object_cache is None:
            self._posture_providing_object_cache = set()
        self._posture_providing_object_cache.add(obj)
        posture_graph_service = services.posture_graph_service()
        if not posture_graph_service.has_built_for_zone_spin_up:
            posture_graph_service.on_mobile_posture_object_added_during_zone_spinup(
                obj)

    def remove_object_from_posture_providing_cache(self, obj):
        if not obj.provided_mobile_posture_affordances:
            return
        self._posture_providing_object_cache.remove(obj)
        if not self._posture_providing_object_cache:
            self._posture_providing_object_cache = None

    def get_posture_providing_objects(self):
        return self._posture_providing_object_cache or ()

    def rebuild_objects_to_ignore_portal_validation_cache(self):
        self._objects_to_ignore_portal_validation_cache.clear()
        for obj in self._objects.values():
            if not obj.routing_component is not None:
                if not obj.inventoryitem_component is not None:
                    if obj.live_drag_component is not None:
                        self._objects_to_ignore_portal_validation_cache.append(
                            obj.id)
            self._objects_to_ignore_portal_validation_cache.append(obj.id)

    def clear_objects_to_ignore_portal_validation_cache(self):
        self._objects_to_ignore_portal_validation_cache.clear()

    def get_objects_to_ignore_portal_validation_cache(self):
        return self._objects_to_ignore_portal_validation_cache

    def clear_caches_on_teardown(self):
        self._tag_to_object_list.clear()
        self._water_terrain_object_cache.clear()
        if self._posture_providing_object_cache is not None:
            self._posture_providing_object_cache.clear()
        self.clear_objects_to_ignore_portal_validation_cache()
        build_buy.unregister_build_buy_exit_callback(
            self._water_terrain_object_cache.refresh)

    def pre_save(self):
        all_objects = list(self._objects.values())
        lot = services.current_zone().lot
        for (_, inventory) in lot.get_all_object_inventories_gen(
                shared_only=True):
            for game_object in inventory:
                all_objects.append(game_object)
        for game_object in all_objects:
            game_object.update_all_commodities()

    @staticmethod
    def save_game_object(game_object, object_list, open_street_objects):
        save_result = None
        if game_object.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT:
            save_result = game_object.save_object(object_list.objects,
                                                  ItemLocation.ON_LOT, 0)
        else:
            if game_object.item_location == ItemLocation.ON_LOT or game_object.item_location == ItemLocation.INVALID_LOCATION:
                item_location = ItemLocation.FROM_OPEN_STREET
            else:
                item_location = game_object.item_location
            save_result = game_object.save_object(open_street_objects.objects,
                                                  item_location, 0)
        return save_result

    def save(self,
             object_list=None,
             zone_data=None,
             open_street_data=None,
             store_travel_group_placed_objects=False,
             **kwargs):
        if object_list is None:
            return
        open_street_objects = file_serialization.ObjectList()
        total_beds = 0
        double_bed_exist = False
        kid_bed_exist = False
        alternative_sleeping_spots = 0
        university_roommate_beds = 0
        if store_travel_group_placed_objects:
            objects_to_save_for_clean_up = []
        roommate_bed_tags = set()
        roommate_service = services.get_roommate_service()
        if roommate_service is not None:
            roommate_bed_tags = roommate_service.BED_TAGS
        for game_object in self._objects.values():
            if self._should_save_object_on_lot(game_object):
                save_result = ObjectManager.save_game_object(
                    game_object, object_list, open_street_objects)
                if not save_result:
                    continue
                if zone_data is None:
                    continue
                if store_travel_group_placed_objects and save_result.owner_id != 0:
                    placement_flags = build_buy.get_object_placement_flags(
                        game_object.definition.id)
                    if build_buy.PlacementFlags.NON_INVENTORYABLE not in placement_flags:
                        objects_to_save_for_clean_up.append(save_result)
                if not game_object.definition.has_build_buy_tag(
                        *self._all_bed_tags):
                    continue
                if game_object.definition.has_build_buy_tag(
                        *self.BED_TAGS.double_beds):
                    double_bed_exist = True
                    total_beds += 1
                elif game_object.definition.has_build_buy_tag(
                        *self.BED_TAGS.kid_beds):
                    total_beds += 1
                    kid_bed_exist = True
                elif game_object.definition.has_build_buy_tag(
                        *self.BED_TAGS.other_sleeping_spots):
                    alternative_sleeping_spots += 1
                elif game_object.definition.has_build_buy_tag(
                        *self.BED_TAGS.beds):
                    total_beds += 1
                if len(roommate_bed_tags) > 0:
                    if game_object.definition.has_build_buy_tag(
                            *roommate_bed_tags):
                        university_roommate_beds += 1
        if open_street_data is not None:
            open_street_data.objects = open_street_objects
        if zone_data is not None:
            bed_info_data = gameplay_serialization.ZoneBedInfoData()
            bed_info_data.num_beds = total_beds
            bed_info_data.double_bed_exist = double_bed_exist
            bed_info_data.kid_bed_exist = kid_bed_exist
            bed_info_data.alternative_sleeping_spots = alternative_sleeping_spots
            if roommate_service is not None:
                household_and_roommate_cap = roommate_service.HOUSEHOLD_AND_ROOMMATE_CAP
                bed_info_data.university_roommate_beds = min(
                    household_and_roommate_cap, university_roommate_beds)
            zone_data.gameplay_zone_data.bed_info_data = bed_info_data
            if store_travel_group_placed_objects:
                current_zone = services.current_zone()
                save_game_protocol_buffer = services.get_persistence_service(
                ).get_save_game_data_proto()
                self._clear_clean_up_data_for_zone(current_zone,
                                                   save_game_protocol_buffer)
                self._save_clean_up_destination_data(
                    current_zone, objects_to_save_for_clean_up,
                    save_game_protocol_buffer)
        lot = services.current_zone().lot
        for (inventory_type, inventory) in lot.get_all_object_inventories_gen(
                shared_only=True):
            for game_object in inventory:
                game_object.save_object(object_list.objects,
                                        ItemLocation.OBJECT_INVENTORY,
                                        inventory_type)

    def _clear_clean_up_data_for_zone(self, current_zone,
                                      save_game_protocol_buffer):
        current_zone_id = current_zone.id
        current_open_street_id = current_zone.open_street_id
        destination_clean_up_data = save_game_protocol_buffer.destination_clean_up_data
        for clean_up_save_data in destination_clean_up_data:
            indexes_to_clean_up = []
            for (index, old_object_clean_up_data) in enumerate(
                    clean_up_save_data.object_clean_up_data_list):
                if not old_object_clean_up_data.zone_id == current_zone_id:
                    if old_object_clean_up_data.world_id == current_open_street_id:
                        indexes_to_clean_up.append(index)
                indexes_to_clean_up.append(index)
            if len(indexes_to_clean_up) == len(
                    clean_up_save_data.object_clean_up_data_list):
                clean_up_save_data.ClearField('object_clean_up_data_list')
            else:
                for index in reversed(indexes_to_clean_up):
                    del clean_up_save_data.object_clean_up_data_list[index]

    def _save_clean_up_destination_data(self, current_zone,
                                        objects_to_save_for_clean_up,
                                        save_game_protocol_buffer):
        household_manager = services.household_manager()
        travel_group_manager = services.travel_group_manager()
        clean_up_save_data = None
        for object_data in sorted(objects_to_save_for_clean_up,
                                  key=lambda x: x.owner_id):
            owner_id = object_data.owner_id
            if clean_up_save_data is None or clean_up_save_data.household_id != owner_id:
                household = household_manager.get(owner_id)
                travel_group = None
                if household is not None:
                    travel_group = household.get_travel_group()
                for clean_up_save_data in save_game_protocol_buffer.destination_clean_up_data:
                    if clean_up_save_data.household_id != owner_id:
                        continue
                    if travel_group is not None:
                        if travel_group.id == clean_up_save_data.travel_group_id:
                            break
                    if clean_up_save_data.travel_group_id in travel_group_manager:
                        continue
                    break
            with ProtocolBufferRollback(
                    clean_up_save_data.object_clean_up_data_list
            ) as object_clean_up_data:
                if object_data.loc_type == ItemLocation.ON_LOT:
                    object_clean_up_data.zone_id = current_zone.id
                else:
                    object_clean_up_data.world_id = current_zone.open_street_id
                object_clean_up_data.object_data = object_data

    def add_sim_spawn_condition(self, sim_id, callback):
        for sim in services.sim_info_manager().instanced_sims_gen():
            if sim.id == sim_id:
                logger.error(
                    'Sim {} is already in the world, cannot add the spawn condition',
                    sim)
                return
        self._sim_spawn_conditions[sim_id].add(callback)

    def remove_sim_spawn_condition(self, sim_id, callback):
        if callback not in self._sim_spawn_conditions.get(sim_id, ()):
            logger.error(
                'Trying to remove sim spawn condition with invalid id-callback pair ({}-{}).',
                sim_id, callback)
            return
        self._sim_spawn_conditions[sim_id].remove(callback)

    def trigger_sim_spawn_condition(self, sim_id):
        if sim_id in self._sim_spawn_conditions:
            for callback in self._sim_spawn_conditions[sim_id]:
                callback()
            del self._sim_spawn_conditions[sim_id]

    def add_portal_lock(self, sim, callback):
        self.register_portal_added_callback(callback)
        for portal in self.portal_cache_gen():
            portal.lock_sim(sim)

    def register_portal_added_callback(self, callback):
        if callback not in self._portal_added_callbacks:
            self._portal_added_callbacks.append(callback)

    def unregister_portal_added_callback(self, callback):
        if callback in self._portal_added_callbacks:
            self._portal_added_callbacks.remove(callback)

    def register_portal_removed_callback(self, callback):
        if callback not in self._portal_removed_callbacks:
            self._portal_removed_callbacks.append(callback)

    def unregister_portal_removed_callback(self, callback):
        if callback in self._portal_removed_callbacks:
            self._portal_removed_callbacks.remove(callback)

    def _is_valid_portal_object(self, portal):
        portal_component = portal.get_component(PORTAL_COMPONENT)
        if portal_component is None:
            return False
        return portal.has_portals()

    def add_portal_to_cache(self, portal):
        if portal not in self._portal_cache and self._is_valid_portal_object(
                portal):
            self._portal_cache.add(portal)
            self._portal_added_callbacks(portal)

    def remove_portal_from_cache(self, portal):
        if portal in self._portal_cache:
            self._portal_cache.remove(portal)
            self._portal_removed_callbacks(portal)

    def register_front_door_candidates_changed_callback(self, callback):
        if callback not in self._front_door_candidates_changed_callback:
            self._front_door_candidates_changed_callback.append(callback)

    def unregister_front_door_candidates_changed_callback(self, callback):
        if callback in self._front_door_candidates_changed_callback:
            self._front_door_candidates_changed_callback.remove(callback)

    def on_front_door_candidates_changed(self):
        self._front_door_candidates_changed_callback()

    def cleanup_build_buy_transient_objects(self):
        household_inventory_proxy_objects = self.get_objects_matching_tags(
            self.HOUSEHOLD_INVENTORY_OBJECT_TAGS)
        for obj in household_inventory_proxy_objects:
            self.remove(obj)

    def get_objects_matching_tags(self, tags: set, match_any=False):
        matching_objects = None
        for tag in tags:
            objs = self._tag_to_object_list[
                tag] if tag in self._tag_to_object_list else set()
            if matching_objects is None:
                matching_objects = objs
            elif match_any:
                matching_objects |= objs
            else:
                matching_objects &= objs
                if not matching_objects:
                    break
        if matching_objects:
            return frozenset(matching_objects)
        return EMPTY_SET

    def get_num_objects_matching_tags(self, tags: set, match_any=False):
        matching_objects = self.get_objects_matching_tags(tags, match_any)
        return len(matching_objects)

    @contextmanager
    def batch_commodity_flags_update(self):
        default_fn = self.clear_commodity_flags_for_objs_with_affordance
        try:
            affordances = set()
            self.clear_commodity_flags_for_objs_with_affordance = affordances.update
            yield None
        finally:
            self.clear_commodity_flags_for_objs_with_affordance = default_fn
            self.clear_commodity_flags_for_objs_with_affordance(affordances)

    def clear_commodity_flags_for_objs_with_affordance(self, affordances):
        for obj in self.valid_objects():
            if not obj.has_updated_commodity_flags():
                continue
            if any(affordance in affordances
                   for affordance in obj.super_affordances()):
                obj.clear_commodity_flags()

    def get_all_objects_with_component_gen(self, component_definition):
        if component_definition is None:
            return
        for obj in self.valid_objects():
            if obj.has_component(component_definition):
                yield obj

    def get_objects_with_tag_gen(self, tag):
        yield from self.get_objects_matching_tags((tag, ))

    def get_objects_with_tags_gen(self, *tags):
        yield from self.get_objects_matching_tags(tags, match_any=True)

    def on_location_changed(self, obj):
        self._registered_callbacks[CallbackTypes.ON_OBJECT_LOCATION_CHANGED](
            obj)

    def process_invalid_unparented_objects(self):
        invalid_objects = self.get_objects_matching_tags(
            self.INVALID_UNPARENTED_OBJECT_TAGS, match_any=True)
        for invalid_object in invalid_objects:
            if invalid_object.parent is None:
                logger.error(
                    'Invalid unparented object {} existed in game. Cleaning up.',
                    invalid_object)
                invalid_object.destroy(
                    source=invalid_object,
                    cause='Invalid unparented object found on zone spin up.')

    @classproperty
    def supports_parenting(self):
        return True

    def add_active_whim_set(self, whim_set):
        self._whim_set_cache[whim_set] += 1

    def remove_active_whim_set(self, whim_set):
        self._whim_set_cache[whim_set] -= 1
        if self._whim_set_cache[whim_set] <= 0:
            del self._whim_set_cache[whim_set]

    @property
    def active_whim_sets(self):
        return set(self._whim_set_cache.keys())
class AwayActionTracker:
    __qualname__ = 'AwayActionTracker'

    def __init__(self, sim_info):
        self._sim_info = sim_info
        self._current_away_action = None
        self._on_away_action_started = CallableList()
        self._on_away_action_ended = CallableList()
        self.add_on_away_action_started_callback(self._resend_away_action)
        self.add_on_away_action_ended_callback(self._resend_away_action)

    @property
    def sim_info(self):
        return self._sim_info

    @property
    def current_away_action(self):
        return self._current_away_action

    def _resend_away_action(self, _):
        self.sim_info.resend_current_away_action()

    def is_sim_info_valid_to_run_away_actions(self):
        if not self._sim_info.is_selectable:
            return False
        if self._sim_info.is_dead:
            return False
        if self._sim_info.is_baby:
            return False
        return True

    def _run_current_away_action(self):
        self._current_away_action.run(self._away_action_exit_condition_callback)
        self._on_away_action_started(self._current_away_action)

    def _find_away_action_from_load(self):
        away_actions_manager = services.get_instance_manager(sims4.resources.Types.AWAY_ACTION)
        for away_action_cls in away_actions_manager.types.values():
            while away_action_cls.should_run_on_load(self.sim_info):
                return away_action_cls

    def start(self, on_travel_away=False):
        if not self.is_sim_info_valid_to_run_away_actions():
            logger.error('Attempting to start away action tracker on invalid sim info {}.', self._sim_info, owner='jjacobson')
            return
        if self._sim_info.is_instanced(allow_hidden_flags=ALL_HIDDEN_REASONS) and not on_travel_away:
            if self._current_away_action is not None:
                if not self._current_away_action.available_when_instanced:
                    self._current_away_action = None
                    return
                    return
            else:
                return
        if self._current_away_action is not None:
            self._run_current_away_action()
            return
        away_action_cls = self._find_away_action_from_load()
        if away_action_cls is not None:
            self.create_and_apply_away_action(away_action_cls)
            return
        self.reset_to_default_away_action(on_travel_away=on_travel_away)

    def stop(self):
        if self._current_away_action is not None:
            if self._current_away_action.is_running:
                self._current_away_action.stop()
                self._on_away_action_ended(self._current_away_action)
            self._current_away_action = None

    def clean_up(self):
        self.remove_on_away_action_started_callback(self._resend_away_action)
        self.remove_on_away_action_ended_callback(self._resend_away_action)
        self.stop()

    def refresh(self, on_travel_away=False):
        if not self.is_sim_info_valid_to_run_away_actions():
            return
        current_zone = services.current_zone()
        if not current_zone.is_zone_running and not current_zone.are_sims_hitting_their_marks:
            return
        if self._sim_info.zone_id == services.current_zone_id():
            self.stop()
            self._current_away_action = None
        else:
            self.start(on_travel_away=on_travel_away)

    def create_and_apply_away_action(self, away_action_cls, target=None):
        if not self.is_sim_info_valid_to_run_away_actions():
            logger.warn('Attempting to apply away action on invalid sim info {}.', self._sim_info, owner='jjacobson')
            return
        self.stop()
        self._current_away_action = away_action_cls(self, target=target)
        self._run_current_away_action()

    def _away_action_exit_condition_callback(self, _):
        self.reset_to_default_away_action()

    def reset_to_default_away_action(self, on_travel_away=False):
        default_away_action = self.sim_info.get_default_away_action(on_travel_away=on_travel_away)
        if default_away_action is None:
            self.stop()
            return
        self.create_and_apply_away_action(default_away_action)

    def save_away_action_info_to_proto(self, away_action_tracker_proto):
        if self._current_away_action is not None:
            away_action_tracker_proto.away_action.away_action_id = self._current_away_action.guid64
            target = self._current_away_action.target
            if target is not None:
                away_action_tracker_proto.away_action.target_sim_id = target.id

    def load_away_action_info_from_proto(self, away_action_tracker_proto):
        if away_action_tracker_proto.HasField('away_action'):
            away_action_cls = services.get_instance_manager(sims4.resources.Types.AWAY_ACTION).get(away_action_tracker_proto.away_action.away_action_id)
            if away_action_tracker_proto.away_action.HasField('target_sim_id'):
                target = services.sim_info_manager().get(away_action_tracker_proto.away_action.target_sim_id)
            else:
                target = None
            self._current_away_action = away_action_cls(self, target=target)

    def add_on_away_action_started_callback(self, callback):
        self._on_away_action_started.append(callback)

    def remove_on_away_action_started_callback(self, callback):
        self._on_away_action_started.remove(callback)

    def add_on_away_action_ended_callback(self, callback):
        self._on_away_action_ended.append(callback)

    def remove_on_away_action_ended_callback(self, callback):
        self._on_away_action_ended.remove(callback)
class BroadcasterService(Service):
    __qualname__ = 'BroadcasterService'
    INTERVAL = TunableRealSecond(description='\n        The time between broadcaster pulses. A lower number will impact\n        performance.\n        ', default=5)
    DEFAULT_QUADTREE_RADIUS = 0.1

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._alarm_handle = None
        self._processing_task = None
        self._on_update_callbacks = CallableList()
        self._pending_broadcasters = []
        self._active_broadcasters = []
        self._cluster_requests = {}
        self._object_cache = None
        self._pending_update = False
        self._quadtrees = defaultdict(sims4.geometry.QuadTree)

    def start(self):
        self._alarm_handle = add_alarm_real_time(self, interval_in_real_seconds(self.INTERVAL), self._on_update, repeating=True, use_sleep_time=False)
        object_manager = services.object_manager()
        object_manager.register_callback(CallbackTypes.ON_OBJECT_LOCATION_CHANGED, self._update_object_cache)
        object_manager.register_callback(CallbackTypes.ON_OBJECT_ADD, self._update_object_cache)
        services.current_zone().wall_contour_update_callbacks.append(self._update_object_cache)

    def stop(self):
        if self._alarm_handle is not None:
            cancel_alarm(self._alarm_handle)
            self._alarm_handle = None
        if self._processing_task is not None:
            self._processing_task.stop()
            self._processing_task = None
        object_manager = services.object_manager()
        object_manager.unregister_callback(CallbackTypes.ON_OBJECT_LOCATION_CHANGED, self._update_object_cache)
        object_manager.unregister_callback(CallbackTypes.ON_OBJECT_ADD, self._update_object_cache)
        services.current_zone().wall_contour_update_callbacks.remove(self._update_object_cache)

    def add_broadcaster(self, broadcaster):
        if broadcaster not in self._pending_broadcasters:
            self._pending_broadcasters.append(broadcaster)
            self._on_update_callbacks()

    def remove_broadcaster(self, broadcaster):
        if broadcaster in self._pending_broadcasters:
            self._pending_broadcasters.remove(broadcaster)
        if broadcaster in self._active_broadcasters:
            self._remove_from_cluster_request(broadcaster)
            self._remove_broadcaster_from_quadtree(broadcaster)
            self._active_broadcasters.remove(broadcaster)
        broadcaster.on_removed()
        self._on_update_callbacks()

    def _activate_pending_broadcasters(self):
        for broadcaster in self._pending_broadcasters:
            self._active_broadcasters.append(broadcaster)
            self.update_cluster_request(broadcaster)
            self._update_object_cache()
        self._pending_broadcasters.clear()

    def _add_broadcaster_to_quadtree(self, broadcaster):
        self._remove_broadcaster_from_quadtree(broadcaster)
        broadcaster_quadtree = self._quadtrees[broadcaster.routing_surface.secondary_id]
        broadcaster_bounds = sims4.geometry.QtCircle(sims4.math.Vector2(broadcaster.position.x, broadcaster.position.z), self.DEFAULT_QUADTREE_RADIUS)
        broadcaster_quadtree.insert(broadcaster, broadcaster_bounds)
        return broadcaster_quadtree

    def _remove_broadcaster_from_quadtree(self, broadcaster):
        broadcaster_quadtree = broadcaster.quadtree
        if broadcaster_quadtree is not None:
            broadcaster_quadtree.remove(broadcaster)

    def update_cluster_request(self, broadcaster):
        if broadcaster not in self._active_broadcasters:
            return
        clustering_request = broadcaster.get_clustering()
        if clustering_request is None:
            return
        self._remove_from_cluster_request(broadcaster)
        cluster_request_key = (type(broadcaster), broadcaster.routing_surface.secondary_id)
        if cluster_request_key in self._cluster_requests:
            cluster_request = self._cluster_requests[cluster_request_key]
            cluster_request.set_object_dirty(broadcaster)
        else:
            cluster_quadtree = self._quadtrees[broadcaster.routing_surface.secondary_id]
            cluster_request = clustering_request(lambda : self._get_broadcasters_for_cluster_request_gen(*cluster_request_key), quadtree=cluster_quadtree)
            self._cluster_requests[cluster_request_key] = cluster_request
        quadtree = self._add_broadcaster_to_quadtree(broadcaster)
        broadcaster.on_added_to_quadtree_and_cluster_request(quadtree, cluster_request)

    def _remove_from_cluster_request(self, broadcaster):
        cluster_request = broadcaster.cluster_request
        if cluster_request is not None:
            cluster_request.set_object_dirty(broadcaster)

    def _update_object_cache(self, obj=None):
        if obj is None:
            self._object_cache = None
            return
        if self._object_cache is not None:
            self._object_cache.add(obj)

    def _is_valid_broadcaster(self, broadcaster):
        broadcasting_object = broadcaster.broadcasting_object
        if broadcasting_object is None:
            return False
        if broadcasting_object.is_in_inventory():
            return False
        if broadcasting_object.parent is not None and broadcasting_object.parent.is_sim:
            return False
        return True

    def _get_broadcasters_for_cluster_request_gen(self, broadcaster_type, broadcaster_level):
        for broadcaster in self._active_broadcasters:
            while broadcaster.guid == broadcaster_type.guid:
                if broadcaster.should_cluster() and broadcaster.routing_surface.secondary_id == broadcaster_level:
                    yield broadcaster

    def get_broadcasters_gen(self, inspect_only=False):
        for (cluster_request_key, cluster_request) in self._cluster_requests.items():
            is_cluster_dirty = cluster_request.is_dirty()
            if is_cluster_dirty:
                for broadcaster in self._get_broadcasters_for_cluster_request_gen(*cluster_request_key):
                    broadcaster.regenerate_constraint()
            while not is_cluster_dirty or not inspect_only:
                while True:
                    for cluster in cluster_request.get_clusters_gen():
                        broadcaster_iter = cluster.objects_gen()
                        master_broadcaster = next(broadcaster_iter)
                        master_broadcaster.set_linked_broadcasters(list(broadcaster_iter))
                        yield master_broadcaster
        for broadcaster in self._active_broadcasters:
            while not broadcaster.should_cluster():
                if self._is_valid_broadcaster(broadcaster):
                    yield broadcaster

    def get_pending_broadcasters_gen(self):
        yield self._pending_broadcasters

    def _get_all_objects_gen(self):
        if any(broadcaster.allow_objects for broadcaster in self._active_broadcasters):
            if self._object_cache is None:
                self._object_cache = WeakSet(services.object_manager().valid_objects())
            yield list(self._object_cache)
        else:
            self._object_cache = None
            yield services.sim_info_manager().instanced_sims_gen()

    def register_callback(self, callback):
        if callback not in self._on_update_callbacks:
            self._on_update_callbacks.append(callback)

    def unregister_callback(self, callback):
        if callback in self._on_update_callbacks:
            self._on_update_callbacks.remove(callback)

    def _on_update(self, _):
        self._pending_update = True

    def update(self):
        if self._pending_update:
            self._pending_update = False
            self._update()

    def _update(self):
        try:
            self._activate_pending_broadcasters()
            current_broadcasters = set(self.get_broadcasters_gen())
            for obj in self._get_all_objects_gen():
                is_affected = False
                for broadcaster in current_broadcasters:
                    while broadcaster.can_affect(obj):
                        constraint = broadcaster.get_constraint()
                        if not constraint.valid:
                            pass
                        if constraint.geometry is None or constraint.geometry.contains_point(obj.position) and constraint.routing_surface == obj.routing_surface:
                            broadcaster.apply_broadcaster_effect(obj)
                            is_affected = True
                while not is_affected:
                    if self._object_cache is not None:
                        self._object_cache.remove(obj)
            for broadcaster in current_broadcasters:
                broadcaster.on_processed()
        finally:
            self._on_update_callbacks()
class UiMultiPicker(UiDialogOkCancel):
    FACTORY_TUNABLES = {'pickers': TunableList(description='\n            A list of picker interactions to use to build pickers.\n            ', tunable=TunableTuple(description='\n            \n                ', picker_interaction=TunableReference(description='\n                    The interaction that will be used to generate a picker.\n                    ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION), pack_safe=True), disabled_tooltip=TunableLocalizedString(description='\n                    The string that will be displayed when an item in the \n                    associated picker is not available.\n                    ')), tuning_group=GroupNames.PICKERTUNING), 'text_input': OptionalTunable(description='\n            If enabled then this dialog will also support a text input box.\n            ', tunable=UiTextInput.TunableFactory(description='\n                The tuning for the Text Input part of the dialog.\n                ', locked_args={'sort_order': 1}))}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._picker_dialogs = {}
        self._text_listeners = CallableList()
        self.existing_text = None
        self.ingredient_check = None
        self._changes_made = False

    def add_text_listener(self, listener):
        self._text_listeners.append(listener)

    def set_target_sim(self, target_sim):
        self.target_sim = target_sim

    def set_target(self, target):
        self.target = target

    def build_msg(self, **kwargs):
        message = super().build_msg(**kwargs)
        message.dialog_type = Dialog_pb2.UiDialogMessage.MULTI_PICKER
        if self.text_input is not None:
            text_input_overrides = {}
            (text_name, text) = self.existing_text
            text_input_overrides = {}
            if text:
                text_input_overrides[text_name] = lambda *_, **__: LocalizationHelperTuning.get_raw_text(text)
            self.text_input.build_msg(self, message, name=text_name, text_input_overrides=text_input_overrides if text_input_overrides else None)
        context = InteractionContext(self._owner(), InteractionSource.SCRIPT, Priority.Low)
        multi_picker_msg = Dialog_pb2.UiDialogMultiPicker()
        for picker_data in self.pickers:
            aop = AffordanceObjectPair(picker_data.picker_interaction, None, picker_data.picker_interaction, None)
            result = aop.interaction_factory(context)
            if not result:
                continue
            interaction = result.interaction
            picker_tuning = picker_data.picker_interaction.picker_dialog
            if picker_tuning.title is None:
                title = lambda *_, **__: interaction.get_name(apply_name_modifiers=False)
            else:
                title = self.picker_dialog.title
            dialog = picker_tuning(self._owner(), title=title, resolver=interaction.get_resolver(context=context))
            interaction._setup_dialog(dialog)
            dialog.add_listener(interaction._on_picker_selected)
            self._picker_dialogs[dialog.dialog_id] = dialog
            new_message = dialog.build_msg()
            multi_picker_item = multi_picker_msg.multi_picker_items.add()
            multi_picker_item.picker_data = new_message.picker_data
            multi_picker_item.picker_id = new_message.dialog_id
            multi_picker_item.disabled_tooltip = picker_data.disabled_tooltip
        message.multi_picker_data = multi_picker_msg
        return message

    def on_text_input(self, text_input_name='', text_input=''):
        self._text_listeners(self, text_input_name, text_input)
        return True

    @property
    def multi_select(self):
        return False

    def multi_picker_result(self, response_proto):
        for picker_result in response_proto.picker_responses:
            if picker_result.picker_id in self._picker_dialogs:
                dialog = self._picker_dialogs[picker_result.picker_id]
                if not self._changes_made:
                    if self._check_for_changes(dialog, picker_result.choices):
                        self._changes_made = True
                dialog.pick_results(picker_result.choices)
                dialog.respond(ButtonType.DIALOG_RESPONSE_OK)
        if self.existing_text[1] != response_proto.text_input:
            self._changes_made = True
        self.on_text_input(text_input=response_proto.text_input)

    def _check_for_changes(self, dialog, choices):
        for (index, row) in enumerate(dialog.picker_rows):
            if row.is_selected is not (index in choices):
                return True
        return False

    def get_single_result_tag(self):
        if self.response == ButtonType.DIALOG_RESPONSE_OK and self._changes_made:
            return True
        return False

    def get_result_tags(self):
        if self.response == ButtonType.DIALOG_RESPONSE_OK and self._changes_made:
            return True
        return False
Exemple #13
0
class BuffComponent(objects.components.Component, component_name=objects.components.types.BUFF_COMPONENT):
    __qualname__ = 'BuffComponent'
    DEFAULT_MOOD = TunableReference(services.mood_manager(), description='The default initial mood.')
    UPDATE_INTENSITY_BUFFER = TunableRange(description="\n        A buffer that prevents a mood from becoming active unless its intensity\n        is greater than the current active mood's intensity plus this amount.\n        \n        For example, if this tunable is 1, and the Sim is in a Flirty mood with\n        intensity 2, then a different mood would become the active mood only if\n        its intensity is 3+.\n        \n        If the predominant mood has an intensity that is less than the active\n        mood's intensity, that mood will become the active mood.\n        ", tunable_type=int, default=1, minimum=0)
    EXCLUSIVE_SET = TunableList(description='\n        A list of buff groups to determine which buffs are exclusive from each\n        other within the same group.  A buff cannot exist in more than one exclusive group.\n        \n        The following rule of exclusivity for a group:\n        1. Higher weight will always be added and remove any lower weight buffs\n        2. Lower weight buff will not be added if a higher weight already exist in component\n        3. Same weight buff will always be added and remove any buff with same weight.\n        \n        Example: Group 1:\n                    Buff1 with weight of 5 \n                    Buff2 with weight of 1\n                    Buff3 with weight of 1\n                 Group 2:\n                    Buff4 with weight of 6\n        \n        If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n        If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n        If sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n        If sim has Buff4, trying to add Buff1, Buff2, or Buff3 will be added and Buff4 will stay \n                          on component \n        ', tunable=TunableList(tunable=TunableTuple(buff_type=TunableReference(description='\n                    Buff in exclusive group\n                    ', manager=services.get_instance_manager(sims4.resources.Types.BUFF)), weight=Tunable(description='\n                    weight to determine if this buff should be added and\n                    remove other buffs in the exclusive group or not added at all.\n                    \n                    Example: Buff1 with weight of 5 \n                             Buff2 with weight of 1\n                             Buff3 with weight of 1\n                    \n                    If sim has Buff1, trying to add Buff2 or Buff3 will not be added.\n                    If sim has Buff2, trying to add Buff3 will remove Buff2 and add Buff3\n                    if sim has Buff2, trying to add Buff1 will remove Buff 2 and add Buff3\n                    ', tunable_type=int, default=1))))

    def __init__(self, owner):
        super().__init__(owner)
        self._active_buffs = {}
        self._get_next_handle_id = UniqueIdGenerator()
        self._success_chance_modification = 0
        self._active_mood = self.DEFAULT_MOOD
        self._active_mood_intensity = 0
        self._active_mood_buff_handle = None
        self.on_mood_changed = CallableList()
        self.on_mood_changed.append(self._publish_mood_update)
        self.on_mood_changed.append(self._send_mood_changed_event)
        self.load_in_progress = False
        self.on_buff_added = CallableList()
        self.on_buff_removed = CallableList()
        self.buff_update_alarms = {}
        if self._active_mood is None:
            logger.error('No default mood tuned in buff_component.py')
        elif self._active_mood.buffs:
            initial_buff_ref = self._active_mood.buffs[0]
            if initial_buff_ref and initial_buff_ref.buff_type:
                self._active_mood_buff_handle = self.add_buff(initial_buff_ref.buff_type)

    def __iter__(self):
        return self._active_buffs.values().__iter__()

    def __len__(self):
        return len(self._active_buffs)

    def on_sim_ready_to_simulate(self):
        for buff in self:
            buff.on_sim_ready_to_simulate()
        self._publish_mood_update()

    def on_sim_removed(self, *args, **kwargs):
        for buff in self:
            buff.on_sim_removed(*args, **kwargs)

    def clean_up(self):
        for (buff_type, buff_entry) in tuple(self._active_buffs.items()):
            self.remove_auto_update(buff_type)
            buff_entry.clean_up()
        self._active_buffs.clear()
        self.on_mood_changed.clear()
        self.on_buff_added.clear()
        self.on_buff_removed.clear()

    @objects.components.componentmethod
    def add_buff_from_op(self, buff_type, buff_reason=None):
        (can_add, _) = self._can_add_buff_type(buff_type)
        if not can_add:
            return False
        buff_commodity = buff_type.commodity
        if buff_commodity is not None:
            if not buff_type.refresh_on_add and self.has_buff(buff_type):
                return False
            tracker = self.owner.get_tracker(buff_commodity)
            if buff_commodity.convergence_value == buff_commodity.max_value:
                tracker.set_min(buff_commodity)
            else:
                tracker.set_max(buff_commodity)
            self.set_buff_reason(buff_type, buff_reason, use_replacement=True)
        else:
            self.add_buff(buff_type, buff_reason=buff_reason)
        return True

    @objects.components.componentmethod
    def add_buff(self, buff_type, buff_reason=None, update_mood=True, commodity_guid=None, replacing_buff=None, timeout_string=None, transition_into_buff_id=0, change_rate=None, immediate=False):
        replacement_buff_type = self._get_replacement_buff_type(buff_type)
        if replacement_buff_type is not None:
            return self.owner.add_buff(replacement_buff_type, buff_reason=buff_reason, update_mood=update_mood, commodity_guid=commodity_guid, replacing_buff=buff_type, timeout_string=timeout_string, transition_into_buff_id=transition_into_buff_id, change_rate=change_rate, immediate=immediate)
        (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type)
        if not can_add:
            return
        buff = self._active_buffs.get(buff_type)
        if buff is None:
            buff = buff_type(self.owner, commodity_guid, replacing_buff, transition_into_buff_id)
            self._active_buffs[buff_type] = buff
            buff.on_add(self.load_in_progress)
            self._update_chance_modifier()
            if update_mood:
                self._update_current_mood()
            if self.owner.household is not None:
                services.get_event_manager().process_event(test_events.TestEvent.BuffBeganEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type)
                self.register_auto_update(self.owner, buff_type)
            self.on_buff_added(buff_type)
        handle_id = self._get_next_handle_id()
        buff.add_handle(handle_id, buff_reason=buff_reason)
        self.send_buff_update_msg(buff, True, change_rate=change_rate, immediate=immediate)
        if conflicting_buff_type is not None:
            self.remove_buff_by_type(conflicting_buff_type)
        return handle_id

    def _get_replacement_buff_type(self, buff_type):
        if buff_type.trait_replacement_buffs is not None:
            trait_tracker = self.owner.trait_tracker
            for (trait, replacement_buff_type) in buff_type.trait_replacement_buffs.items():
                while trait_tracker.has_trait(trait):
                    return replacement_buff_type

    def register_auto_update(self, sim_info_in, buff_type_in):
        if buff_type_in in self.buff_update_alarms:
            self.remove_auto_update(buff_type_in)
        if sim_info_in.is_selectable and buff_type_in.visible:
            self.buff_update_alarms[buff_type_in] = alarms.add_alarm(self, create_time_span(minutes=15), lambda _, sim_info=sim_info_in, buff_type=buff_type_in: services.get_event_manager().process_event(test_events.TestEvent.BuffUpdateEvent, sim_info=sim_info, sim_id=sim_info.sim_id, buff=buff_type), True)

    def remove_auto_update(self, buff_type):
        if buff_type in self.buff_update_alarms:
            alarms.cancel_alarm(self.buff_update_alarms[buff_type])
            del self.buff_update_alarms[buff_type]

    @objects.components.componentmethod
    def remove_buff(self, handle_id, update_mood=True, immediate=False, on_destroy=False):
        for (buff_type, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                should_remove = buff_entry.remove_handle(handle_id)
                if should_remove:
                    del self._active_buffs[buff_type]
                    buff_entry.on_remove(not self.load_in_progress and not on_destroy)
                    if not on_destroy:
                        if update_mood:
                            self._update_current_mood()
                        self._update_chance_modifier()
                        self.send_buff_update_msg(buff_entry, False, immediate=immediate)
                        services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, sim_id=self.owner.sim_id, buff=buff_type)
                    if buff_type in self.buff_update_alarms:
                        self.remove_auto_update(buff_type)
                    self.on_buff_removed(buff_type)
                break

    @objects.components.componentmethod
    def get_buff_type(self, handle_id):
        for (buff_type, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                return buff_type

    @objects.components.componentmethod
    def has_buff(self, buff_type):
        return buff_type in self._active_buffs

    @objects.components.componentmethod
    def get_active_buff_types(self):
        return self._active_buffs.keys()

    @objects.components.componentmethod
    def get_buff_reason(self, handle_id):
        for buff_entry in self._active_buffs.values():
            while handle_id in buff_entry.handle_ids:
                return buff_entry.buff_reason

    @objects.components.componentmethod
    def debug_add_buff_by_type(self, buff_type):
        (can_add, conflicting_buff_type) = self._can_add_buff_type(buff_type)
        if not can_add:
            return False
        if buff_type.commodity is not None:
            tracker = self.owner.get_tracker(buff_type.commodity)
            state_index = buff_type.commodity.get_state_index_matches_buff_type(buff_type)
            if state_index is not None:
                index = state_index + 1
                if index < len(buff_type.commodity.commodity_states):
                    commodity_to_value = buff_type.commodity.commodity_states[index].value - 1
                else:
                    commodity_to_value = buff_type.commodity.max_value
                tracker.set_value(buff_type.commodity, commodity_to_value)
            else:
                logger.error('commodity ({}) has no states with buff ({}), Buff will not be added.', buff_type.commodity, buff_type)
                return False
        else:
            self.add_buff(buff_type)
        if conflicting_buff_type is not None:
            self.remove_buff_by_type(conflicting_buff_type)
        return True

    @objects.components.componentmethod
    def remove_buff_by_type(self, buff_type, on_destroy=False):
        buff_entry = self._active_buffs.get(buff_type)
        self.remove_buff_entry(buff_entry, on_destroy=on_destroy)

    @objects.components.componentmethod
    def remove_buff_entry(self, buff_entry, on_destroy=False):
        if buff_entry is not None:
            if buff_entry.commodity is not None:
                tracker = self.owner.get_tracker(buff_entry.commodity)
                commodity_inst = tracker.get_statistic(buff_entry.commodity)
                if commodity_inst is not None and commodity_inst.core:
                    if not on_destroy:
                        logger.callstack('Attempting to explicitly remove the buff {}, which is given by a core commodity.                                           This would result in the removal of a core commodity and will be ignored.', buff_entry, owner='tastle', level=sims4.log.LEVEL_ERROR)
                    return
                tracker.remove_statistic(buff_entry.commodity, on_destroy=on_destroy)
            elif buff_entry.buff_type in self._active_buffs:
                buff_entry.on_remove(on_destroy)
                del self._active_buffs[buff_entry.buff_type]
                if not on_destroy:
                    self._update_chance_modifier()
                    self._update_current_mood()
                    self.send_buff_update_msg(buff_entry, False)
                    services.get_event_manager().process_event(test_events.TestEvent.BuffEndedEvent, sim_info=self.owner, buff=type(buff_entry), sim_id=self.owner.id)

    @objects.components.componentmethod
    def set_buff_reason(self, buff_type, buff_reason, use_replacement=False):
        if use_replacement:
            replacement_buff_type = self._get_replacement_buff_type(buff_type)
            if replacement_buff_type is not None:
                buff_type = replacement_buff_type
        buff_entry = self._active_buffs.get(buff_type)
        if buff_entry is not None and buff_reason is not None:
            buff_entry.buff_reason = buff_reason
            self.send_buff_update_msg(buff_entry, True)

    @objects.components.componentmethod
    def buff_commodity_changed(self, handle_id, change_rate=None):
        for (_, buff_entry) in self._active_buffs.items():
            while handle_id in buff_entry.handle_ids:
                if buff_entry.show_timeout:
                    self.send_buff_update_msg(buff_entry, True, change_rate=change_rate)
                break

    @objects.components.componentmethod
    def get_success_chance_modifier(self):
        return self._success_chance_modification

    @objects.components.componentmethod
    def get_actor_scoring_modifier(self, affordance):
        total = 0
        for buff_entry in self._active_buffs.values():
            total += buff_entry.effect_modification.get_affordance_scoring_modifier(affordance)
        return total

    @objects.components.componentmethod
    def get_actor_success_modifier(self, affordance):
        total = 0
        for buff_entry in self._active_buffs.values():
            total += buff_entry.effect_modification.get_affordance_success_modifier(affordance)
        return total

    @objects.components.componentmethod
    def get_mood(self):
        return self._active_mood

    @objects.components.componentmethod
    def get_mood_animation_param_name(self):
        param_name = self._active_mood.asm_param_name
        if param_name is not None:
            return param_name
        (mood, _, _) = self._get_largest_mood(predicate=lambda mood: return True if mood.asm_param_name else False)
        return mood.asm_param_name

    @objects.components.componentmethod
    def get_mood_intensity(self):
        return self._active_mood_intensity

    @objects.components.componentmethod
    def get_effective_skill_level(self, skill):
        if skill.stat_type == skill:
            skill = self.owner.get_stat_instance(skill)
            if skill is None:
                return 0
        modifier = 0
        for buff_entry in self._active_buffs.values():
            modifier += buff_entry.effect_modification.get_effective_skill_modifier(skill)
        return skill.get_user_value() + modifier

    @objects.components.componentmethod
    def effective_skill_modified_buff_gen(self, skill):
        if skill.stat_type == skill:
            skill = self.owner.get_stat_instance(skill)
        for buff_entry in self._active_buffs.values():
            modifier = buff_entry.effect_modification.get_effective_skill_modifier(skill)
            while modifier != 0:
                yield (buff_entry, modifier)

    @objects.components.componentmethod
    def is_appropriate(self, tags):
        final_appropriateness = Appropriateness.DONT_CARE
        for buff in self._active_buffs:
            appropriateness = buff.get_appropriateness(tags)
            while appropriateness > final_appropriateness:
                final_appropriateness = appropriateness
        if final_appropriateness == Appropriateness.NOT_ALLOWED:
            return False
        return True

    def get_additional_create_ops_gen(self):
        yield GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg())
        for buff in self:
            while buff.visible:
                yield GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, self._create_buff_update_msg(buff, True))

    def _publish_mood_update(self):
        if self.owner.valid_for_distribution and self.owner.visible_to_client == True:
            Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_MOOD_UPDATE, self._create_mood_update_msg()))

    def _send_mood_changed_event(self):
        if not self.owner.is_npc:
            self.owner.whim_tracker.refresh_emotion_whim()
        services.get_event_manager().process_event(test_events.TestEvent.MoodChange, sim_info=self.owner)

    def _create_mood_update_msg(self):
        mood_msg = Commodities_pb2.MoodUpdate()
        mood_msg.sim_id = self.owner.id
        mood_msg.mood_key = self._active_mood.guid64
        mood_msg.mood_intensity = self._active_mood_intensity
        return mood_msg

    def _create_buff_update_msg(self, buff, equipped, change_rate=None):
        buff_msg = Sims_pb2.BuffUpdate()
        buff_msg.buff_id = buff.guid64
        buff_msg.sim_id = self.owner.id
        buff_msg.equipped = equipped
        if buff.buff_reason is not None:
            buff_msg.reason = buff.buff_reason
        if equipped and buff.show_timeout:
            (timeout, rate_multiplier) = buff.get_timeout_time()
            buff_msg.timeout = timeout
            buff_msg.rate_multiplier = rate_multiplier
            if change_rate is not None:
                if change_rate == 0:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_NONE
                elif change_rate > 0:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_UP if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_DOWN
                else:
                    progress_arrow = Sims_pb2.BUFF_PROGRESS_DOWN if not buff.flip_arrow_for_progress_update else Sims_pb2.BUFF_PROGRESS_UP
                buff_msg.buff_progress = progress_arrow
        buff_msg.is_mood_buff = buff.is_mood_buff
        buff_msg.commodity_guid = buff.commodity_guid or 0
        if buff.mood_override is not None:
            buff_msg.mood_type_override = buff.mood_override.guid64
        buff_msg.transition_into_buff_id = buff.transition_into_buff_id
        return buff_msg

    def send_buff_update_msg(self, buff, equipped, change_rate=None, immediate=False):
        if not buff.visible:
            return
        if self.owner.valid_for_distribution and self.owner.is_sim and self.owner.is_selectable:
            buff_msg = self._create_buff_update_msg(buff, equipped, change_rate=change_rate)
            if gsi_handlers.buff_handlers.sim_buff_log_archiver.enabled:
                gsi_handlers.buff_handlers.archive_buff_message(buff_msg, equipped, change_rate)
            Distributor.instance().add_op(self.owner, GenericProtocolBufferOp(Operation.SIM_BUFF_UPDATE, buff_msg))

    def _can_add_buff_type(self, buff_type):
        if not buff_type.can_add(self.owner):
            return (False, None)
        mood = buff_type.mood_type
        if mood is not None and mood.excluding_traits is not None and self.owner.trait_tracker.has_any_trait(mood.excluding_traits):
            return (False, None)
        if buff_type.exclusive_index is None:
            return (True, None)
        for conflicting_buff_type in self._active_buffs:
            while conflicting_buff_type.exclusive_index == buff_type.exclusive_index:
                if buff_type.exclusive_weight < conflicting_buff_type.exclusive_weight:
                    return (False, None)
                return (True, conflicting_buff_type)
        return (True, None)

    def _update_chance_modifier(self):
        positive_success_buff_delta = 0
        negative_success_buff_delta = 1
        for buff_entry in self._active_buffs.values():
            if buff_entry.success_modifier > 0:
                positive_success_buff_delta += buff_entry.get_success_modifier
            else:
                negative_success_buff_delta *= 1 + buff_entry.get_success_modifier
        self._success_chance_modification = positive_success_buff_delta - (1 - negative_success_buff_delta)

    def _get_largest_mood(self, predicate=None, buffs_to_ignore=()):
        weights = {}
        polarity_to_changeable_buffs = collections.defaultdict(list)
        polarity_to_largest_mood_and_weight = {}
        for buff_entry in self._active_buffs.values():
            current_mood = buff_entry.mood_type
            current_weight = buff_entry.mood_weight
            while not current_mood is None:
                if current_weight == 0:
                    pass
                if not (predicate is not None and predicate(current_mood)):
                    pass
                if buff_entry in buffs_to_ignore:
                    pass
                current_polarity = current_mood.buff_polarity
                if buff_entry.is_changeable:
                    polarity_to_changeable_buffs[current_polarity].append(buff_entry)
                total_current_weight = weights.get(current_mood, 0)
                total_current_weight += current_weight
                weights[current_mood] = total_current_weight
                (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(current_polarity, (None, None))
                if largest_mood is None:
                    polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight)
                else:
                    while total_current_weight > largest_weight:
                        polarity_to_largest_mood_and_weight[current_polarity] = (current_mood, total_current_weight)
        all_changeable_buffs = []
        for (buff_polarity, changeable_buffs) in polarity_to_changeable_buffs.items():
            (largest_mood, largest_weight) = polarity_to_largest_mood_and_weight.get(buff_polarity, (None, None))
            if largest_mood is not None:
                for buff_entry in changeable_buffs:
                    if buff_entry.mood_override is not largest_mood:
                        all_changeable_buffs.append((buff_entry, largest_mood))
                    largest_weight += buff_entry.mood_weight
                polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight)
            else:
                weights = {}
                largest_weight = 0
                for buff_entry in changeable_buffs:
                    if buff_entry.mood_override is not None:
                        all_changeable_buffs.append((buff_entry, None))
                    current_mood = buff_entry.mood_type
                    current_weight = buff_entry.mood_weight
                    total_current_weight = weights.get(current_mood, 0)
                    total_current_weight += current_weight
                    weights[current_mood] = total_current_weight
                    while total_current_weight > largest_weight:
                        largest_weight = total_current_weight
                        largest_mood = current_mood
                while largest_mood is not None and largest_weight != 0:
                    polarity_to_largest_mood_and_weight[buff_polarity] = (largest_mood, largest_weight)
        largest_weight = 0
        largest_mood = self.DEFAULT_MOOD
        active_mood = self._active_mood
        if polarity_to_largest_mood_and_weight:
            (mood, weight) = max(polarity_to_largest_mood_and_weight.values(), key=operator.itemgetter(1))
            if weight > largest_weight or weight == largest_weight and mood is active_mood:
                largest_weight = weight
                largest_mood = mood
        return (largest_mood, largest_weight, all_changeable_buffs)

    def _update_current_mood(self):
        (largest_mood, largest_weight, changeable_buffs) = self._get_largest_mood()
        if largest_mood is not None:
            intensity = self._get_intensity_from_mood(largest_mood, largest_weight)
            if self._should_update_mood(largest_mood, intensity, changeable_buffs):
                if self._active_mood_buff_handle is not None:
                    active_mood_buff_handle = self._active_mood_buff_handle
                    self.remove_buff(active_mood_buff_handle, update_mood=False)
                    if active_mood_buff_handle == self._active_mood_buff_handle:
                        self._active_mood_buff_handle = None
                    else:
                        return
                self._active_mood = largest_mood
                self._active_mood_intensity = intensity
                if len(largest_mood.buffs) >= intensity:
                    tuned_buff = largest_mood.buffs[intensity]
                    if tuned_buff is not None and tuned_buff.buff_type is not None:
                        self._active_mood_buff_handle = self.add_buff(tuned_buff.buff_type, update_mood=False)
                if gsi_handlers.buff_handlers.sim_mood_log_archiver.enabled and self.owner.valid_for_distribution and self.owner.visible_to_client == True:
                    gsi_handlers.buff_handlers.archive_mood_message(self.owner.id, self._active_mood, self._active_mood_intensity, self._active_buffs, changeable_buffs)
                caches.clear_all_caches()
                self.on_mood_changed()
        for (changeable_buff, mood_override) in changeable_buffs:
            changeable_buff.mood_override = mood_override
            self.send_buff_update_msg(changeable_buff, True)

    def _get_intensity_from_mood(self, mood, weight):
        intensity = 0
        for threshold in mood.intensity_thresholds:
            if weight >= threshold:
                intensity += 1
            else:
                break
        return intensity

    def _should_update_mood(self, mood, intensity, changeable_buffs):
        active_mood = self._active_mood
        active_mood_intensity = self._active_mood_intensity
        if mood is active_mood:
            return intensity != active_mood_intensity
        total_weight = sum(buff_entry.mood_weight for buff_entry in self._active_buffs.values() if buff_entry.mood_type is active_mood)
        active_mood_intensity = self._get_intensity_from_mood(active_mood, total_weight)
        if changeable_buffs and not self._active_mood.is_changeable:
            buffs_to_ignore = [changeable_buff for (changeable_buff, _) in changeable_buffs]
            (largest_mood, largest_weight, _) = self._get_largest_mood(buffs_to_ignore=buffs_to_ignore)
            new_intensity = self._get_intensity_from_mood(largest_mood, largest_weight)
            if self._should_update_mood(largest_mood, new_intensity, None):
                active_mood = largest_mood
                active_mood_intensity = new_intensity
        if active_mood.is_changeable and mood.buff_polarity == active_mood.buff_polarity:
            return True
        if not intensity or intensity < active_mood_intensity:
            return True
        if intensity >= active_mood_intensity + self.UPDATE_INTENSITY_BUFFER:
            return True
        if mood is self.DEFAULT_MOOD or active_mood is self.DEFAULT_MOOD:
            return True
        return False
Exemple #14
0
class CurfewService(Service):
    ALLOWED_CURFEW_TIMES = TunableList(
        description=
        '\n        A list of times (in military time) that are allowed to be set as curfew\n        times.\n        \n        NOTE: Many objects will have curfew components and will only support\n        a visual of certain values. Changing these values without making sure\n        the art supports the value will not work properly. Please only change\n        these values if you know for sure they need to be changed and are \n        getting support from modelling to make the change.\n        ',
        tunable=TunableRange(
            description=
            '\n            The hour for which the curfew will be set to.\n            ',
            tunable_type=int,
            default=0,
            minimum=0,
            maximum=23))
    CURFEW_END_TIME = TunableRange(
        description=
        '\n        The time when the curfew is considered to be over and the Sims are \n        no longer subject to it.\n        \n        This should probably be set to some time in the morning. 6am perhaps.\n        ',
        tunable_type=int,
        default=0,
        minimum=0,
        maximum=23)
    MINUTES_BEFORE_CURFEW_WARNING = TunableSimMinute(
        description=
        '\n        The minutes before the curfew starts that a Sim should receive a \n        warning about the curfew being about to start.\n        ',
        default=30)
    BREAK_CURFEW_WARNING = TunableLocalizedStringFactory(
        description=
        '\n        The string that is used to warn the player that a pie menu action will\n        cause the Sim to break curfew. This will wrap around the name of the \n        interaction so should be tuned to something like [Warning] {0.String}.\n        '
    )
    CURFEW_WARNING_TEXT_MESSAGE_DIALOG = UiDialogOk.TunableFactory(
        description=
        '\n        The dialog to display as a text message when warning a Sim that their\n        curfew is about to expire.\n        '
    )
    CURFEW_WARNING_SIM_TESTS = TunableTestSet(
        description=
        '\n        Tests to run on each of the Sims to determine if they should receive\n        the curfew warning text message or not.\n        '
    )
    BREAK_CURFEW_BUFF = TunablePackSafeReference(
        description=
        "\n        The buff that get's added to a Sim that breaks curfew. This buff will\n        enable the Sim to be disciplined for their behavior.\n        ",
        manager=services.buff_manager())
    INTERACTION_BLACKLIST_TAGS = TunableSet(
        description=
        '\n        A list of all the tags that blacklist interactions from causing Sims to\n        break curfew.\n        ',
        tunable=TunableEnumEntry(
            description=
            '\n            A tag that when tagged on the interaction will allow the Sim to run\n            the interaction and not break curfew.\n            ',
            tunable_type=Tag,
            default=Tag.INVALID,
            pack_safe=True))
    CURFEW_BEGIN_LOOT = TunablePackSafeReference(
        description=
        '\n        The loot to apply to all Sims in the family when curfew begins. This\n        will allow us to give buffs that affect the behavior of the Sims if\n        they pass certain tests.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.ACTION))
    CURFEW_END_LOOT = TunablePackSafeReference(
        description=
        '\n        The loot to apply to all Sims in the family when curfew ends. This will\n        allow us to remove buffs that affect the behavior of the Sims.\n        ',
        manager=services.get_instance_manager(sims4.resources.Types.ACTION))
    UNSET = -1

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._zone_curfew_data = {}
        self._curfew_warning_alarm_handle = None
        self._curfew_started_alarm_handle = None
        self._curfew_ended_alarm_handle = None
        self._curfew_message_alarm_handle = None
        self._curfew_warning_callback = CallableList()
        self._curfew_started_callback = CallableList()
        self._curfew_ended_callback = CallableList()
        self._time_set_callback = CallableList()

    def get_zone_curfew(self, zone_id):
        curfew_setting = self._zone_curfew_data.get(zone_id, self.UNSET)
        return curfew_setting

    def set_zone_curfew(self, zone_id, curfew_setting):
        if self._zone_curfew_data.get(zone_id, None) == curfew_setting:
            return
        if curfew_setting not in CurfewService.ALLOWED_CURFEW_TIMES and curfew_setting != CurfewService.UNSET:
            return
        self._zone_curfew_data[zone_id] = curfew_setting
        self._update_curfew_settings(zone_id, curfew_setting)
        self._setup_curfew_text_message()

    def _update_curfew_settings(self, current_zone_id, current_setting):
        self._create_alarm_handles(current_zone_id)
        self._time_set_callback(current_setting)

    def _create_alarm_handles(self, zone_id):
        for alarm in (self._curfew_warning_alarm_handle,
                      self._curfew_started_alarm_handle,
                      self._curfew_ended_alarm_handle):
            if alarm is not None:
                alarms.cancel_alarm(alarm)
        time = self._zone_curfew_data.get(zone_id, self.UNSET)
        now = services.time_service().sim_now
        self._create_warning_callback(now, time)
        self._create_curfew_callback(now, time)
        self._create_curfew_ended_callback(now, time)

    def _create_warning_callback(self, now, time):
        if time is not CurfewService.UNSET:
            alarm_time = date_and_time.create_date_and_time(hours=time - 1)
            warning_span = now.time_till_next_day_time(alarm_time)
            if warning_span.in_ticks() == 0:
                warning_span += TimeSpan(date_and_time.sim_ticks_per_day())
            self._curfew_warning_alarm_handle = alarms.add_alarm(
                self, warning_span, self._handle_warning_callback, False)

    def _handle_warning_callback(self, handle):
        self._curfew_warning_callback()
        now = services.time_service().sim_now
        time = self._zone_curfew_data.get(services.current_zone_id(),
                                          CurfewService.UNSET)
        self._create_warning_callback(now, time)

    def _create_curfew_callback(self, now, time):
        if time is not self.UNSET:
            alarm_time = date_and_time.create_date_and_time(hours=time)
            curfew_span = now.time_till_next_day_time(alarm_time)
            if curfew_span.in_ticks() == 0:
                curfew_span += TimeSpan(date_and_time.sim_ticks_per_day())
            self._curfew_started_alarm_handle = alarms.add_alarm(
                self, curfew_span, self._handle_curfew_callback, False)

    def _handle_curfew_callback(self, handle):
        self._curfew_started_callback()
        now = services.time_service().sim_now
        time = self._zone_curfew_data.get(services.current_zone_id(),
                                          CurfewService.UNSET)
        self.apply_curfew_loots()
        self._create_curfew_callback(now, time)

    def _create_curfew_ended_callback(self, now, time):
        if time is not CurfewService.UNSET:
            alarm_time = date_and_time.create_date_and_time(
                hours=CurfewService.CURFEW_END_TIME)
            curfew_span = now.time_till_next_day_time(alarm_time)
            if curfew_span.in_ticks() == 0:
                curfew_span += TimeSpan(date_and_time.sim_ticks_per_day())
            self._curfew_ended_alarm_handle = alarms.add_alarm(
                self, curfew_span, self._handle_curfew_ended_callback, False)

    def _handle_curfew_ended_callback(self, handle):
        self._curfew_ended_callback()
        now = services.time_service().sim_now
        time = CurfewService.CURFEW_END_TIME
        self.remove_curfew_loots()
        self._create_curfew_ended_callback(now, time)

    def register_for_alarm_callbacks(self, warning_callback, curfew_callback,
                                     curfew_over_callback, time_set_callback):
        self._curfew_warning_callback.append(warning_callback)
        self._curfew_started_callback.append(curfew_callback)
        self._curfew_ended_callback.append(curfew_over_callback)
        self._time_set_callback.append(time_set_callback)

    def unregister_for_alarm_callbacks(self, warning_callback, curfew_callback,
                                       curfew_over_callback,
                                       time_set_callback):
        if warning_callback in self._curfew_warning_callback:
            self._curfew_warning_callback.remove(warning_callback)
        if curfew_callback in self._curfew_started_callback:
            self._curfew_started_callback.remove(curfew_callback)
        if curfew_over_callback in self._curfew_ended_callback:
            self._curfew_ended_callback.remove(curfew_over_callback)
        if time_set_callback in self._time_set_callback:
            self._time_set_callback.remove(time_set_callback)

    def sim_breaking_curfew(self, sim, target, interaction=None):
        if interaction is not None and self.interaction_blacklisted(
                interaction):
            return False
        if sim.sim_info.is_in_travel_group():
            return False
        situation_manager = services.get_zone_situation_manager()
        sim_situations = situation_manager.get_situations_sim_is_in(sim)
        if any(situation.disallows_curfew_violation
               for situation in sim_situations):
            return False
        active_household = services.active_household()
        if active_household is None:
            return False
        home_zone_id = active_household.home_zone_id
        curfew_setting = self._zone_curfew_data.get(home_zone_id,
                                                    CurfewService.UNSET)
        if sim.sim_info not in active_household:
            return False
        if curfew_setting is not CurfewService.UNSET:
            if sim.sim_info.is_young_adult_or_older:
                return False
            elif self.past_curfew(curfew_setting):
                if not services.current_zone_id() == home_zone_id:
                    ensemble_service = services.ensemble_service()
                    ensemble = ensemble_service.get_visible_ensemble_for_sim(
                        sim)
                    if ensemble is not None and any(
                            sim.sim_info.is_young_adult_or_older
                            and sim.sim_info in active_household
                            for sim in ensemble):
                        return False
                    return True
                if target is not None and not target.is_in_inventory(
                ) and not services.active_lot().is_position_on_lot(
                        target.position):
                    return True
                elif target is None and not services.active_lot(
                ).is_position_on_lot(sim.position):
                    return True
            return True
            if target is not None and not target.is_in_inventory(
            ) and not services.active_lot().is_position_on_lot(
                    target.position):
                return True
            elif target is None and not services.active_lot(
            ).is_position_on_lot(sim.position):
                return True
        return False

    def interaction_blacklisted(self, interaction):
        interaction_tags = interaction.get_category_tags()
        for tag in CurfewService.INTERACTION_BLACKLIST_TAGS:
            if tag in interaction_tags:
                return True
        return False

    def past_curfew(self, curfew_setting):
        now = services.time_service().sim_now
        if now.hour() >= curfew_setting or now.hour(
        ) < CurfewService.CURFEW_END_TIME:
            return True
        return False

    def _setup_curfew_text_message(self):
        if self._curfew_message_alarm_handle is not None:
            self._curfew_message_alarm_handle.cancel()
            self._curfew_message_alarm_handle = None
        current_household = services.active_household()
        if current_household is None:
            return
        home_zone_id = current_household.home_zone_id
        curfew_setting = self._zone_curfew_data.get(home_zone_id,
                                                    CurfewService.UNSET)
        if curfew_setting is CurfewService.UNSET:
            return
        now = services.time_service().sim_now
        alarm_time = date_and_time.create_date_and_time(hours=curfew_setting)
        time_till_alarm = now.time_till_next_day_time(alarm_time)
        span = date_and_time.create_time_span(
            minutes=CurfewService.MINUTES_BEFORE_CURFEW_WARNING)
        time_till_alarm -= span
        self._curfew_message_alarm_handle = alarms.add_alarm(
            self, time_till_alarm, self._handle_curfew_message_callback, False)

    def _handle_curfew_message_callback(self, handle):
        active_lot = services.active_lot()
        if active_lot.lot_id != services.active_household_lot_id():
            from_sim = None
            for sim_info in services.active_household():
                if sim_info.is_young_adult_or_older:
                    if not sim_info.is_instanced():
                        from_sim = sim_info
                        break
            if from_sim is None:
                return
            for sim_info in services.active_household():
                if sim_info.get_sim_instance() is None:
                    continue
                resolver = DoubleSimResolver(sim_info, from_sim)
                if not CurfewService.CURFEW_WARNING_SIM_TESTS.run_tests(
                        resolver):
                    continue
                dialog = self.CURFEW_WARNING_TEXT_MESSAGE_DIALOG(
                    sim_info, target_sim_id=from_sim.id, resolver=resolver)
                dialog.show_dialog()

    def add_broke_curfew_buff(self, sim):
        if not sim.has_buff(CurfewService.BREAK_CURFEW_BUFF):
            sim.add_buff(CurfewService.BREAK_CURFEW_BUFF)

    def remove_broke_curfew_buff(self, sim):
        if sim.has_buff(CurfewService.BREAK_CURFEW_BUFF):
            sim.remove_buff_by_type(CurfewService.BREAK_CURFEW_BUFF)

    def is_curfew_active_on_lot_id(self, lot_id):
        curfew_setting = self._zone_curfew_data.get(lot_id,
                                                    CurfewService.UNSET)
        if curfew_setting == CurfewService.UNSET:
            return False
        return self.past_curfew(curfew_setting)

    def apply_curfew_loots(self):
        for sim_info in services.active_household():
            resolver = SingleSimResolver(sim_info)
            CurfewService.CURFEW_BEGIN_LOOT.apply_to_resolver(resolver)

    def remove_curfew_loots(self):
        for sim_info in services.active_household():
            resolver = SingleSimResolver(sim_info)
            CurfewService.CURFEW_END_LOOT.apply_to_resolver(resolver)

    @classproperty
    def save_error_code(cls):
        return persistence_error_types.ErrorCodes.SERVICE_SAVE_FAILED_CURFEW_SERVICE

    def save(self,
             object_list=None,
             zone_data=None,
             open_street_data=None,
             store_travel_group_placed_objects=False,
             save_slot_data=None):
        persistence_service = services.get_persistence_service()
        for save_zone_data in persistence_service.zone_proto_buffs_gen():
            setting = self._zone_curfew_data.get(save_zone_data.zone_id,
                                                 CurfewService.UNSET)
            save_zone_data.gameplay_zone_data.curfew_setting = setting

    def load(self, zone_data=None):
        persistence_service = services.get_persistence_service()
        for zone_data in persistence_service.zone_proto_buffs_gen():
            self._zone_curfew_data[
                zone_data.
                zone_id] = zone_data.gameplay_zone_data.curfew_setting

    def on_zone_load(self):
        current_zone_id = services.current_zone_id()
        self._setup_curfew_text_message()
        self._create_alarm_handles(current_zone_id)
        venue_manager = services.get_instance_manager(
            sims4.resources.Types.VENUE)
        current_venue_tuning = venue_manager.get(
            build_buy.get_current_venue(current_zone_id))
        if current_venue_tuning.is_residential or current_venue_tuning.is_university_housing:
            current_setting = self._zone_curfew_data.get(
                current_zone_id, CurfewService.UNSET)
            self._update_curfew_settings(current_zone_id, current_setting)
        else:
            self._update_curfew_settings(current_zone_id, CurfewService.UNSET)
class LocatorManager(Service):
    SPAWN_TAG_PREFIX = 'spawn_'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._locators = defaultdict(list)
        self._spawn_point_definitions = set()
        self._locators_changed_callbacks = CallableList()

    def __contains__(self, key):
        if not isinstance(key, int):
            raise TypeError('LocatorManager keys must be integers.')
        return key in self._locators

    def __getitem__(self, key):
        if not isinstance(key, int):
            raise TypeError('LocatorManager keys must be integers.')
        return self._locators[key]

    def __iter__(self):
        return iter(self._locators)

    def __len__(self):
        return len(self._locators)

    def __bool__(self):
        if self._locators:
            return True
        return False

    def get(self, key):
        return self._locators.get(key, ())

    def keys(self):
        return self._locators.keys()

    def values(self):
        return self._locators.values()

    def items(self):
        return self._locators.items()

    def register_locators_changed_callback(self, callback):
        self._locators_changed_callbacks.append(callback)

    def unregister_locators_changed_callback(self, callback):
        if callback in self._locators_changed_callbacks:
            self._locators_changed_callbacks.remove(callback)

    def _is_locator_for_spawn_point(self, locator):
        tags = locator.get_tags()
        if locator.obj_def_guid in self._spawn_point_definitions:
            return True
        if locator.obj_def_guid in self._locators:
            return False
        for tag_val in tags:
            if self.SPAWN_TAG_PREFIX in str(
                    tag.Tag.value_to_name[tag_val]).lower():
                self._spawn_point_definitions.add(locator.obj_def_guid)
                return True
        return False

    def set_up_locators(self, locator_data_array):
        spawn_point_locators = []
        for locator_data in locator_data_array:
            locator = LocatorObject(*locator_data)
            if self._is_locator_for_spawn_point(locator):
                spawn_point_locators.append(locator)
            else:
                self._locators[locator.obj_def_guid].append(locator)
        services.current_zone().set_up_world_spawn_points(spawn_point_locators)
Exemple #16
0
class SimInfoBaseWrapper(OutfitTrackerMixin):
    def __init__(self,
                 *args,
                 sim_id: int = 0,
                 gender: Gender = Gender.MALE,
                 age: Age = Age.ADULT,
                 species: SpeciesExtended = SpeciesExtended.HUMAN,
                 first_name: str = '',
                 last_name: str = '',
                 breed_name: str = '',
                 full_name_key=0,
                 breed_name_key=0,
                 physique: str = '',
                 skin_tone=1,
                 **kwargs):
        super().__init__(*args, **kwargs)
        self.sim_id = sim_id or id_generator.generate_object_id()
        self.primitives = distributor.ops.DistributionSet(self)
        self.manager = None
        self._base = BaseSimInfo(sim_id, first_name, last_name, breed_name,
                                 full_name_key, breed_name_key, age, gender,
                                 species, skin_tone, physique)
        self._base.voice_pitch = 0.0
        self._base.voice_actor = 0
        self._base.voice_effect = 0
        self._base.facial_attributes = ''
        self._base.custom_texture = 0
        self._base.pelt_layers = ''
        self._species = SpeciesExtended.get_species(species)
        self.on_base_characteristic_changed = CallableList()
        self.on_outfit_changed = CallableList()
        self.on_outfit_generated = CallableList()
        self._set_current_outfit_without_distribution(
            (OutfitCategory.EVERYDAY, 0))
        self._previous_outfit = (OutfitCategory.EVERYDAY, 0)
        self._preload_outfit_list = []
        self.on_preload_outfits_changed = CallableList()
        self.appearance_tracker = AppearanceTracker(self)
        self.visible_to_client = False

    @property
    def id(self):
        return self.sim_id

    @id.setter
    def id(self, value):
        pass

    def ref(self, callback=None):
        return weakref.ref(self, protected_callback(callback))

    def get_create_op(self, *args, **kwargs):
        additional_ops = list(self.get_additional_create_ops_gen())
        return distributor.ops.SimInfoCreate(self,
                                             *args,
                                             additional_ops=additional_ops,
                                             **kwargs)

    def get_additional_create_ops_gen(self):
        pass

    def get_delete_op(self):
        return distributor.ops.SimInfoDelete()

    def get_create_after_objs(self):
        return ()

    @property
    def valid_for_distribution(self):
        return self.manager is not None and self.id is not None

    @property
    def icon_info(self):
        return (self.id, self.manager.id)

    @staticmethod
    def copy_base_attributes(sim_info_a, sim_info_b):
        sim_info_a.age = sim_info_b.age
        sim_info_a.gender = sim_info_b.gender
        sim_info_a.extended_species = sim_info_b.extended_species

    @staticmethod
    def copy_physical_attributes(sim_info_a, sim_info_b):
        sim_info_a.physique = sim_info_b.physique
        sim_info_a.facial_attributes = sim_info_b.facial_attributes
        sim_info_a.voice_pitch = sim_info_b.voice_pitch
        sim_info_a.voice_actor = sim_info_b.voice_actor
        sim_info_a.voice_effect = sim_info_b.voice_effect
        sim_info_a.skin_tone = sim_info_b.skin_tone
        sim_info_a.flags = sim_info_b.flags
        if hasattr(sim_info_a, 'pelt_layers'):
            if hasattr(sim_info_b, 'pelt_layers'):
                sim_info_a.pelt_layers = sim_info_b.pelt_layers
        if hasattr(sim_info_a, 'base_trait_ids'):
            if hasattr(sim_info_b, 'base_trait_ids'):
                sim_info_a.base_trait_ids = list(sim_info_b.base_trait_ids)
        SimInfoBaseWrapper.copy_genetic_data(sim_info_a, sim_info_b)

    @staticmethod
    def copy_genetic_data(sim_info_a, sim_info_b):
        genetic_data_b = sim_info_b.genetic_data
        if hasattr(genetic_data_b, 'SerializeToString'):
            genetic_data_b = genetic_data_b.SerializeToString()
        if hasattr(sim_info_a.genetic_data, 'MergeFromString'):
            sim_info_a.genetic_data.MergeFromString(genetic_data_b)
        else:
            sim_info_a.genetic_data = genetic_data_b

    def get_icon_info_data(self):
        return IconInfoData(obj_instance=self)

    def get_icon_override(self):
        pass

    def populate_icon_canvas_texture_info(self, _):
        pass

    def populate_localization_token(self, token):
        token.type = LocalizedStringToken.SIM
        token.first_name = self.first_name
        token.last_name = self.last_name
        token.full_name_key = self.full_name_key
        token.is_female = self.is_female

    def save_sim_info(self, sim_info_msg):
        sim_info_msg.gender = self.gender
        sim_info_msg.age = self.age
        sim_info_msg.extended_species = self.extended_species
        sim_info_msg.physique = self.physique
        sim_info_msg.facial_attributes = self.facial_attributes
        sim_info_msg.skin_tone = self.skin_tone
        sim_info_msg.outfits = self.save_outfits()
        (current_outfit_type, current_outfit_index) = self.get_current_outfit()
        sim_info_msg.current_outfit_type = current_outfit_type
        sim_info_msg.current_outfit_index = current_outfit_index
        (previous_outfit_type,
         previous_outfit_index) = self.get_previous_outfit()
        sim_info_msg.previous_outfit_type = previous_outfit_type
        sim_info_msg.previous_outfit_index = previous_outfit_index

    def load_sim_info(self, sim_info_msg):
        self.gender = sim_info_msg.gender
        self.age = sim_info_msg.age
        self.extended_species = sim_info_msg.extended_species
        self.physique = sim_info_msg.physique
        self.facial_attributes = sim_info_msg.facial_attributes
        self.skin_tone = sim_info_msg.skin_tone
        self.load_outfits(sim_info_msg.outfits)
        self.set_current_outfit((sim_info_msg.current_outfit_type,
                                 sim_info_msg.current_outfit_index))
        self._previous_outfit = (sim_info_msg.previous_outfit_type,
                                 sim_info_msg.previous_outfit_index)

    def get_traits(self):
        return ()

    def set_trait_ids_on_base(self, trait_ids_override=None):
        trait_ids = trait_ids_override if trait_ids_override is not None else self.trait_ids
        self._base.base_trait_ids = trait_ids

    def update_gender_for_traits(self,
                                 gender_override=None,
                                 trait_ids_override=None):
        gender = gender_override if gender_override is not None else self.gender
        trait_ids = trait_ids_override if trait_ids_override is not None else self.trait_ids
        self._base.update_gender_for_traits(gender, trait_ids)

    def get_sim_instance(self, *args, **kwargs):
        pass

    def _get_trait_ids(self):
        return list(self._base.base_trait_ids)

    @distributor.fields.Field(op=distributor.ops.SetTraits,
                              priority=distributor.fields.Field.Priority.LOW)
    def trait_ids(self):
        return self._get_trait_ids()

    resend_trait_ids = trait_ids.get_resend()

    @property
    def base_trait_ids(self):
        return self._base.base_trait_ids

    @base_trait_ids.setter
    def base_trait_ids(self, value):
        self._base.base_trait_ids = value
        self.resend_trait_ids()

    def resend_physical_attributes(self):
        self.resend_skin_tone()
        self.resend_pelt_layers()
        self.resend_custom_texture()
        self.resend_facial_attributes()
        self.resend_physique()
        self.resend_outfits()
        self.resend_voice_pitch()
        self.resend_voice_actor()
        self.resend_voice_effect()
        self.resend_trait_ids()

    def load_from_resource(self,
                           resource_key,
                           age=None,
                           resend_physical_attributes=True):
        success = self._base.load_from_resource(resource_key, age)
        if not success:
            return
        if resend_physical_attributes:
            self.resend_physical_attributes()

    def apply_genetics(self, parent_a, parent_b, **kwargs):
        generate_offspring(parent_a._base, parent_b._base, self._base,
                           **kwargs)

    def add_random_variation_to_modifiers(self,
                                          variation_scale=None,
                                          resend_physical_attributes=True):
        if variation_scale is None:
            variation_scale = random.uniform(-1, 1)
        if not self._base.add_random_variation_to_modifiers(variation_scale):
            return
        if resend_physical_attributes:
            self.resend_physical_attributes()

    def generate_club_outfit(self,
                             tag_list=[],
                             outfit_category_and_index=(0, 0),
                             single_or_all_random=0):
        cas_parts = self._base.generate_club_outfit(
            tag_list,
            int(outfit_category_and_index[0]), outfit_category_and_index[1],
            int(OutfitCategory.SPECIAL), 0, single_or_all_random)
        return (cas_parts[0], cas_parts[1])

    def generate_outfit(self,
                        outfit_category,
                        outfit_index=0,
                        tag_list=(),
                        filter_flag=DEFAULT,
                        **kwargs):
        filter_flag = OutfitFilterFlag.USE_EXISTING_IF_APPROPRIATE if filter_flag is DEFAULT else filter_flag
        if not self._base.generate_outfit(outfit_category,
                                          outfit_index,
                                          tag_list,
                                          filter_flag=filter_flag,
                                          **kwargs):
            return False
        self.resend_outfits()
        self.on_outfit_generated(outfit_category, outfit_index)
        self.set_outfit_dirty(outfit_category)
        return True

    def generate_merged_outfit(self,
                               source_sim_info,
                               destination_outfit,
                               template_outfit,
                               source_outfit,
                               preserve_outfit_flags=False):
        if preserve_outfit_flags:
            source_outfit_data = source_sim_info.get_outfit(*source_outfit)
        generate_merged_outfit(self._base, source_sim_info._base,
                               destination_outfit[0], destination_outfit[1],
                               template_outfit[0], template_outfit[1],
                               source_outfit[0], source_outfit[1])
        if preserve_outfit_flags:
            self.set_outfit_flags(destination_outfit[0], destination_outfit[1],
                                  source_outfit_data.outfit_flags)
        self.resend_outfits()

    @contextmanager
    def set_temporary_outfit_flags(self, outfit_category, outfit_index,
                                   outfit_flags):
        if outfit_flags != DEFAULT:
            outfit_data = self.get_outfit(outfit_category, outfit_index)
            restore_flags = outfit_data.outfit_flags
            self.set_outfit_flags(outfit_category, outfit_index, outfit_flags)
        try:
            yield None
        finally:
            if outfit_flags != DEFAULT:
                self.set_outfit_flags(outfit_category, outfit_index,
                                      restore_flags)

    @contextmanager
    def set_temporary_outfit_flags_on_all_outfits(self, outfit_flags):
        with ExitStack() as stack:
            for (outfit_category,
                 outfit_index) in self.get_all_outfit_entries():
                stack.enter_context(
                    self.set_temporary_outfit_flags(outfit_category,
                                                    outfit_index,
                                                    outfit_flags))
            yield None

    def generate_merged_outfits_for_category(
            self,
            source_sim_info,
            outfit_category,
            outfit_flags=DEFAULT,
            fallback_outfit_category=OutfitCategory.EVERYDAY,
            **kwargs):
        current_outfit = self.get_current_outfit()
        for (outfit_index, _) in enumerate(
                source_sim_info.get_outfits_in_category(outfit_category)):
            merged_outfit = (outfit_category, outfit_index)
            if self.has_outfit(merged_outfit):
                template_outfit = merged_outfit
            elif self.has_outfit((outfit_category, 0)):
                template_outfit = (outfit_category, 0)
            else:
                template_outfit = current_outfit
            with source_sim_info.set_temporary_outfit_flags(
                    outfit_category, outfit_index, outfit_flags):
                self.generate_merged_outfit(source_sim_info, merged_outfit,
                                            template_outfit, merged_outfit,
                                            **kwargs)
        while len(self.get_outfits_in_category(outfit_category)) > len(
                source_sim_info.get_outfits_in_category(outfit_category)):
            self.remove_outfit(outfit_category)
        if self.has_outfit(current_outfit):
            self.set_current_outfit(current_outfit)
        elif self.has_outfit((outfit_category, 0)):
            self.set_current_outfit((outfit_category, 0))
        elif self.has_outfit((fallback_outfit_category, 0)):
            self.set_current_outfit((fallback_outfit_category, 0))
        elif self.has_outfit((OutfitCategory.BATHING, 0)):
            self.set_current_outfit((OutfitCategory.BATHING, 0))

    def is_duplicate_outfit(self, source_sim_info, template_outfit,
                            source_outfit):
        return is_duplicate_merged_outfit(self._base, source_sim_info._base,
                                          template_outfit[0],
                                          template_outfit[1], source_outfit[0],
                                          source_outfit[1])

    def is_generated_outfit_duplicate_in_category(self, source_sim_info,
                                                  source_outfit):
        return any(
            is_duplicate_merged_outfit(self._base, source_sim_info._base,
                                       source_outfit[0], outfit_index,
                                       source_outfit[0], source_outfit[1])
            for outfit_index in range(
                len(self.get_outfits_in_category(source_outfit[0]))))

    def get_outfits(self):
        return self

    @distributor.fields.Field(op=distributor.ops.SetSimOutfits)
    def _client_outfits(self):
        override = self.appearance_tracker.appearance_override_sim_info
        if override is not None:
            return override
        return self

    resend_outfits = _client_outfits.get_resend()

    @distributor.fields.Field(op=distributor.ops.PreloadSimOutfit)
    def preload_outfit_list(self):
        return tuple(self._preload_outfit_list)

    resend_preload_outfit_list = preload_outfit_list.get_resend()

    def add_preload_outfit(self, outfit):
        self._preload_outfit_list.append(outfit)
        self.resend_preload_outfit_list()
        self.on_preload_outfits_changed()

    def set_preload_outfits(self, outfit_list):
        self._preload_outfit_list = list(outfit_list)
        self.resend_preload_outfit_list()
        self.on_preload_outfits_changed()

    @distributor.fields.Field(op=distributor.ops.ChangeSimOutfit,
                              priority=distributor.fields.Field.Priority.LOW)
    def _current_outfit(self):
        return self._base.outfit_type_and_index

    resend_current_outfit = _current_outfit.get_resend()

    @_current_outfit.setter
    def _current_outfit(self, value):
        self._set_current_outfit_without_distribution(value)

    def _set_current_outfit_without_distribution(self, value):
        self._base.outfit_type_and_index = value
        self.on_outfit_changed(self, value)

    def get_current_outfit(self):
        return self._current_outfit

    def get_previous_outfit(self):
        return self._previous_outfit

    def set_current_outfit(self, outfit_category_and_index) -> bool:
        if self._current_outfit == outfit_category_and_index and not self.outfit_is_dirty(
                self._current_outfit[0]):
            return False
        if not self.has_outfit(outfit_category_and_index):
            outfit_category_and_index = (
                self.occult_tracker.get_fallback_outfit_category(
                    self.current_occult_types), 0)
            logger.warn('Trying to set outfit to a possible occult sim.',
                        owner='amohananey')
        if not self.has_outfit(outfit_category_and_index):
            error_msg = 'Trying to set outfit on Sim missing it. Sim: {}, Category: {}, Index: {}, {}'
            error_args = [
                self, outfit_category_and_index[0],
                outfit_category_and_index[1],
                StackVar(('interaction', ))
            ]
            career_tracker = getattr(self, 'career_tracker', None)
            if career_tracker is None:
                error_msg += '\n\tNo career tracker'
            else:
                error_msg += '\n\tCareers:'
                for career in career_tracker.careers.values():
                    error_msg += '\n\t\t{}: {}'
                    error_args.append(str(career))
                    error_args.append(str(career.current_level_tuning))
            logger.callstack(error_msg,
                             *error_args,
                             level=sims4.log.LEVEL_ERROR,
                             owner='tingyul')
        self.set_previous_outfit(new_outfit=outfit_category_and_index)
        self._current_outfit = outfit_category_and_index
        self.clear_outfit_dirty(self._current_outfit[0])
        return True

    def set_current_outfit_for_category(self, outfit_category):
        number_of_outfits = self.get_number_of_outfits_in_category(
            outfit_category)
        outfit_index = random.randrange(number_of_outfits)
        self.set_current_outfit((outfit_category, outfit_index))

    def set_previous_outfit(self, new_outfit, force=False):
        if not force and new_outfit == self._current_outfit:
            return
        self._previous_outfit = self._current_outfit

    def register_for_outfit_changed_callback(self, callback):
        if callback not in self.on_outfit_changed:
            self.on_outfit_changed.append(callback)

    def unregister_for_outfit_changed_callback(self, callback):
        if callback in self.on_outfit_changed:
            self.on_outfit_changed.remove(callback)

    @property
    def is_male(self):
        return Gender(self._base.gender) == Gender.MALE

    @property
    def is_female(self):
        return Gender(self._base.gender) == Gender.FEMALE

    @distributor.fields.Field(op=distributor.ops.SetFirstName)
    def first_name(self):
        return self._base.first_name

    @first_name.setter
    def first_name(self, value):
        if self._base.first_name != value:
            self._base.first_name = value

    @distributor.fields.Field(op=distributor.ops.SetLastName)
    def last_name(self):
        return self._base.last_name

    @last_name.setter
    def last_name(self, value):
        if self._base.last_name != value:
            self._base.last_name = value

    @property
    def first_name_key(self):
        return self._base.first_name_key

    @first_name_key.setter
    def first_name_key(self, value):
        if self._base.first_name_key != value:
            self._base.first_name_key = value

    @property
    def last_name_key(self):
        return self._base.last_name_key

    @last_name_key.setter
    def last_name_key(self, value):
        if self._base.last_name_key != value:
            self._base.last_name_key = value

    @distributor.fields.Field(op=distributor.ops.SetFullNameKey)
    def full_name_key(self):
        return self._base.full_name_key

    @full_name_key.setter
    def full_name_key(self, value):
        if self._base.full_name_key != value:
            self._base.full_name_key = value

    @distributor.fields.Field(op=distributor.ops.SetBreedName)
    def breed_name(self):
        return self._base.breed_name

    @breed_name.setter
    def breed_name(self, value):
        if self._base.breed_name != value:
            self._base.breed_name = value

    @distributor.fields.Field(op=distributor.ops.SetBreedNameKey)
    def breed_name_key(self):
        return self._base.breed_name_key

    @breed_name_key.setter
    def breed_name_key(self, value):
        if self._base.breed_name_key != value:
            self._base.breed_name_key = value

    @property
    def full_name(self):
        return ''

    @distributor.fields.Field(op=distributor.ops.SetGender,
                              priority=distributor.fields.Field.Priority.HIGH)
    def gender(self):
        return Gender(self._base.gender)

    @gender.setter
    def gender(self, value):
        self._base.gender = Gender(value)
        self.on_base_characteristic_changed()

    @distributor.fields.Field(op=distributor.ops.SetAge,
                              priority=distributor.fields.Field.Priority.HIGH)
    def age(self):
        return Age(self._base.age)

    @age.setter
    def age(self, value):
        self._base.age = value

    resend_age = age.get_resend()

    def apply_age(self, age):
        self._base.update_for_age(age)
        self.age = age
        self.resend_physical_attributes()

    @property
    def species(self):
        return self._species

    @species.setter
    def species(self, value):
        self.extended_species = value

    @distributor.fields.Field(op=distributor.ops.SetSpecies,
                              priority=distributor.fields.Field.Priority.HIGH)
    def extended_species(self):
        return self._base.species

    @extended_species.setter
    def extended_species(self, value):
        self._base.species = value
        self._species = SpeciesExtended.get_species(value)
        self.on_base_characteristic_changed()

    resend_extended_species = extended_species.get_resend()

    @property
    def is_human(self):
        return self.species == Species.HUMAN

    @property
    def is_pet(self):
        return self.species != Species.HUMAN

    @property
    def is_baby(self):
        return self.age == Age.BABY

    @property
    def is_toddler(self):
        return self.age == Age.TODDLER

    @property
    def rig_key(self):
        return self._base.rig_key

    @distributor.fields.Field(op=distributor.ops.SetSkinTone,
                              priority=distributor.fields.Field.Priority.HIGH)
    def skin_tone(self):
        override = self.appearance_tracker.appearance_override_sim_info
        if override is not None:
            return override._base.skin_tone
        return self._base.skin_tone

    _resend_skin_tone = skin_tone.get_resend()

    def resend_skin_tone(self, *args, **kwargs):
        self._resend_skin_tone(*args, **kwargs)
        if self.is_baby:
            self.resend_baby_skin_tone(*args, **kwargs)

    @skin_tone.setter
    def skin_tone(self, value):
        self._base.skin_tone = value
        if self.is_baby:
            self.resend_baby_skin_tone()

    @distributor.fields.Field(op=distributor.ops.SetBabySkinTone)
    def baby_skin_tone(self):
        return BabyTuning.get_baby_skin_tone_enum(self)

    resend_baby_skin_tone = baby_skin_tone.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetPeltLayers)
    def pelt_layers(self):
        return self._base.pelt_layers

    resend_pelt_layers = pelt_layers.get_resend()

    @pelt_layers.setter
    def pelt_layers(self, value):
        self._base.pelt_layers = value

    @distributor.fields.Field(op=distributor.ops.SetCustomTexture)
    def custom_texture(self):
        return self._base.custom_texture

    resend_custom_texture = custom_texture.get_resend()

    @custom_texture.setter
    def custom_texture(self, value):
        self._base.custom_texture = value

    @distributor.fields.Field(op=distributor.ops.SetVoicePitch)
    def voice_pitch(self):
        return self._base.voice_pitch

    resend_voice_pitch = voice_pitch.get_resend()

    @voice_pitch.setter
    def voice_pitch(self, value):
        self._base.voice_pitch = value

    @distributor.fields.Field(op=distributor.ops.SetVoiceActor)
    def voice_actor(self):
        return self._base.voice_actor

    resend_voice_actor = voice_actor.get_resend()

    @voice_actor.setter
    def voice_actor(self, value):
        self._base.voice_actor = value

    @distributor.fields.Field(op=distributor.ops.SetVoiceEffect)
    def voice_effect(self):
        return self._base.voice_effect

    @voice_effect.setter
    def voice_effect(self, value):
        if value is None:
            value = 0
        self._base.voice_effect = value

    resend_voice_effect = voice_effect.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetPhysique)
    def physique(self):
        return self._base.physique

    resend_physique = physique.get_resend()

    @physique.setter
    def physique(self, value):
        self._base.physique = value

    @distributor.fields.Field(op=distributor.ops.SetFacialAttributes)
    def facial_attributes(self):
        return self._base.facial_attributes

    @facial_attributes.setter
    def facial_attributes(self, value):
        self._base.facial_attributes = value

    resend_facial_attributes = facial_attributes.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetGeneticData)
    def genetic_data(self):
        return self._base.genetic_data

    @genetic_data.setter
    def genetic_data(self, value):
        self._base.genetic_data = value

    resend_genetic_data = genetic_data.get_resend()

    @property
    def flags(self):
        return self._base.flags

    @flags.setter
    def flags(self, value):
        self._base.flags = value
Exemple #17
0
class SpawnPoint:
    __qualname__ = 'SpawnPoint'
    ARRIVAL_SPAWN_POINT_TAG = TunableEnumEntry(
        description=
        '\n        The Tag associated with Spawn Points at the front of the lot.\n        ',
        tunable_type=Tag,
        default=Tag.INVALID)
    VISITOR_ARRIVAL_SPAWN_POINT_TAG = TunableEnumEntry(
        description=
        '\n        The Tag associated with Spawn Points nearby the lot for visitors.\n        ',
        tunable_type=Tag,
        default=Tag.INVALID)

    def __init__(self, lot_id, zone_id, routing_surface=None):
        self.center = None
        self.lot_id = lot_id
        self._tags = set()
        if routing_surface is None:
            routing_surface = routing.SurfaceIdentifier(
                zone_id, 0, routing.SURFACETYPE_WORLD)
        self._routing_surface = routing_surface
        self._valid_slots = 0
        self._on_spawn_points_changed = CallableList()

    def __str__(self):
        return 'Name:{:20} Lot:{:15} Center:{:45} Tags:{}'.format(
            self.get_name(), self.lot_id, self.center, self.get_tags())

    @property
    def routing_surface(self):
        return self._routing_surface

    def get_name(self):
        raise NotImplementedError

    def get_tags(self):
        return self._tags

    def has_tag(self, tag):
        if tag is not None:
            return tag in self.get_tags()
        return False

    def next_spawn_spot(self):
        raise NotImplementedError

    def get_slot_pos(self, index=None):
        raise NotImplementedError

    @property
    def valid_slots(self):
        return self._valid_slots

    def reset_valid_slots(self):
        self._valid_slots = 0

    def register_spawn_point_changed_callback(self, callback):
        self._on_spawn_points_changed.append(callback)

    def unregister_spawn_point_changed_callback(self, callback):
        self._on_spawn_points_changed.remove(callback)

    def get_slot_positions(self):
        raise NotImplementedError
Exemple #18
0
class SituationGoal(metaclass=HashedTunedInstanceMetaclass,
                    manager=services.get_instance_manager(
                        sims4.resources.Types.SITUATION_GOAL)):
    __qualname__ = 'SituationGoal'
    INSTANCE_SUBCLASSES_ONLY = True
    IS_TARGETED = False
    INSTANCE_TUNABLES = {
        '_display_name':
        TunableLocalizedStringFactory(
            description=
            '\n                Display name for the Situation Goal. It takes one token, the\n                target (0) of this situation goal.\n                ',
            tuning_group=GroupNames.UI),
        '_icon':
        TunableResourceKey(
            'PNG:missing_image',
            description=
            '\n                Icon to be displayed for the Situation Goal.\n                ',
            needs_tuning=True,
            resource_types=sims4.resources.CompoundTypes.IMAGE),
        '_pre_tests':
        TunableSituationGoalPreTestSet(
            description=
            '\n                A set of tests on the player sim and environment that all must\n                pass for the goal to be given to the player. e.g. Player Sim\n                has cooking skill level 7.\n                ',
            tuning_group=GroupNames.TESTS),
        '_post_tests':
        TunableSituationGoalPostTestSet(
            description=
            '\n                A set of tests that must all pass when the player satisfies the\n                goal_test for the goal to be consider completed. e.g. Player\n                has Drunk Buff when Kissing another sim at Night.\n                ',
            tuning_group=GroupNames.TESTS),
        '_environment_pre_tests':
        TunableSituationGoalEnvironmentPreTestSet(
            description=
            '\n                A set of sim independent pre tests.\n                e.g. There are five desks.\n                ',
            tuning_group=GroupNames.TESTS),
        'role_tags':
        TunableSet(
            TunableEnumEntry(Tag, Tag.INVALID),
            description=
            '\n                This goal will only be given to Sims in SituationJobs or Role\n                States marked with one of these tags.\n                '
        ),
        '_cooldown':
        TunableSimMinute(
            description=
            '\n                The cooldown of this situation goal.  Goals that have been\n                completed will not be chosen again for the amount of time that\n                is tuned.\n                ',
            default=600,
            minimum=0),
        '_iterations':
        Tunable(
            description=
            '\n                     Number of times the player must perform the action to complete the goal\n                     ',
            tunable_type=int,
            default=1),
        '_score':
        Tunable(
            description=
            '\n                    The number of points received for completing the goal.\n                    ',
            tunable_type=int,
            default=10),
        '_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is completed.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        '_tooltip':
        TunableLocalizedStringFactory(
            description=
            '\n            Tooltip for this situation goal. It takes one token, the\n            actor (0) of this situation goal.'
        ),
        'noncancelable':
        Tunable(
            description=
            '\n            Checking this box will prevent the player from canceling this goal in the whim system.',
            tunable_type=bool,
            needs_tuning=True,
            default=False),
        'time_limit':
        Tunable(
            description=
            '\n            Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n            time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.',
            tunable_type=int,
            default=0),
        'penalty_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to the Sim who fails\n            to complete this goal within the tuned time limit.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'goal_completion_notification':
        OptionalTunable(tunable=UiDialogNotification.TunableFactory(
            description=
            '\n                A TNS that will fire when this situation goal is completed.\n                '
        )),
        'audio_sting_on_complete':
        TunableResourceKey(
            description=
            '\n            The sound to play when this goal is completed.\n            ',
            default=None,
            resource_types=(sims4.resources.Types.PROPX, ),
            tuning_group=GroupNames.AUDIO),
        'goal_completion_modal_dialog':
        OptionalTunable(tunable=UiDialogOk.TunableFactory(
            description=
            '\n                A modal dialog that will fire when this situation goal is\n                completed.\n                '
        ))
    }

    @classmethod
    def can_be_given_as_goal(cls, actor, situation, **kwargs):
        if actor is not None:
            resolver = event_testing.resolver.DataResolver(
                actor.sim_info, None)
            result = cls._pre_tests.run_tests(resolver)
            if not result:
                return result
        environment_test_result = cls._environment_pre_tests.run_tests(
            Resolver())
        if not environment_test_result:
            return environment_test_result
        return TestResult.TRUE

    def __init__(self,
                 sim_info=None,
                 situation=None,
                 goal_id=0,
                 count=0,
                 **kwargs):
        self._sim_info = sim_info
        self._situation = situation
        self.id = goal_id
        self._on_goal_completed_callbacks = CallableList()
        self._completed_time = None
        self._count = count

    def destroy(self):
        self.decommision()
        self._sim_info = None
        self._situation = None

    def decommision(self):
        self._on_goal_completed_callbacks.clear()

    def create_seedling(self):
        actor_id = 0 if self._sim_info is None else self._sim_info.sim_id
        seedling = situations.situation_serialization.GoalSeedling(
            type(self), actor_id, self._count)
        return seedling

    def register_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.append(listener)

    def unregister_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.remove(listener)

    def get_gsi_name(self):
        if self._iterations <= 1:
            return self.__class__.__name__
        return '{} {}/{}'.format(self.__class__.__name__, self._count,
                                 self._iterations)

    def _on_goal_completed(self):
        self._completed_time = services.time_service().sim_now
        for loots in self._goal_loot_list:
            for loot in loots.goal_loot_actions:
                for sim in self._situation.all_sims_in_situation_gen():
                    loot.apply_to_resolver(sim.get_resolver())
        client = services.client_manager().get_first_client()
        if client is not None:
            active_sim = client.active_sim
            resolver = SingleSimResolver(active_sim)
            if self.goal_completion_notification is not None:
                notification = self.goal_completion_notification(
                    active_sim, resolver=resolver)
                notification.show_dialog()
            if self.goal_completion_modal_dialog is not None:
                dialog = self.goal_completion_modal_dialog(active_sim,
                                                           resolver=resolver)
                dialog.show_dialog()
        self._on_goal_completed_callbacks(self, True)

    def _on_iteration_completed(self):
        self._on_goal_completed_callbacks(self, False)

    def debug_force_complete(self, target_sim):
        self._count = self._iterations
        self._on_goal_completed()

    def handle_event(self, sim_info, event, resolver):
        if self._sim_info is not None and self._sim_info is not sim_info:
            return
        if self._run_goal_completion_tests(sim_info, event, resolver):
            if self._count >= self._iterations:
                self._on_goal_completed()
            else:
                self._on_iteration_completed()

    def _run_goal_completion_tests(self, sim_info, event, resolver):
        return self._post_tests.run_tests(resolver)

    def _get_actual_target_sim_info(self):
        pass

    def get_required_target_sim_info(self):
        pass

    @property
    def created_time(self):
        pass

    @property
    def completed_time(self):
        return self._completed_time

    def is_on_cooldown(self):
        if self._completed_time is None:
            return False
        time_since_last_completion = services.time_service(
        ).sim_now - self._completed_time
        return time_since_last_completion < interval_in_sim_minutes(
            self._cooldown)

    def get_localization_tokens(self):
        if self._sim_info is None:
            return (self._numerical_token, )
        required_tgt_sim = self.get_required_target_sim_info()
        if required_tgt_sim is None:
            return (self._numerical_token, self._sim_info)
        return (self._numerical_token, self._sim_info, required_tgt_sim)

    @property
    def display_name(self):
        return self._display_name(*self.get_localization_tokens())

    @property
    def tooltip(self):
        return self._tooltip(*self.get_localization_tokens())

    @property
    def score(self):
        return self._score

    @property
    def completed_iterations(self):
        return self._count

    @property
    def max_iterations(self):
        return self._iterations

    @property
    def _numerical_token(self):
        return self.max_iterations
Exemple #19
0
class BroadcasterService(Service):
    __qualname__ = 'BroadcasterService'
    INTERVAL = TunableRealSecond(
        description=
        '\n        The time between broadcaster pulses. A lower number will impact\n        performance.\n        ',
        default=5)
    DEFAULT_QUADTREE_RADIUS = 0.1

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._alarm_handle = None
        self._processing_task = None
        self._on_update_callbacks = CallableList()
        self._pending_broadcasters = []
        self._active_broadcasters = []
        self._cluster_requests = {}
        self._object_cache = None
        self._pending_update = False
        self._quadtrees = defaultdict(sims4.geometry.QuadTree)

    def start(self):
        self._alarm_handle = add_alarm_real_time(self,
                                                 interval_in_real_seconds(
                                                     self.INTERVAL),
                                                 self._on_update,
                                                 repeating=True,
                                                 use_sleep_time=False)
        object_manager = services.object_manager()
        object_manager.register_callback(
            CallbackTypes.ON_OBJECT_LOCATION_CHANGED,
            self._update_object_cache)
        object_manager.register_callback(CallbackTypes.ON_OBJECT_ADD,
                                         self._update_object_cache)
        services.current_zone().wall_contour_update_callbacks.append(
            self._update_object_cache)

    def stop(self):
        if self._alarm_handle is not None:
            cancel_alarm(self._alarm_handle)
            self._alarm_handle = None
        if self._processing_task is not None:
            self._processing_task.stop()
            self._processing_task = None
        object_manager = services.object_manager()
        object_manager.unregister_callback(
            CallbackTypes.ON_OBJECT_LOCATION_CHANGED,
            self._update_object_cache)
        object_manager.unregister_callback(CallbackTypes.ON_OBJECT_ADD,
                                           self._update_object_cache)
        services.current_zone().wall_contour_update_callbacks.remove(
            self._update_object_cache)

    def add_broadcaster(self, broadcaster):
        if broadcaster not in self._pending_broadcasters:
            self._pending_broadcasters.append(broadcaster)
            self._on_update_callbacks()

    def remove_broadcaster(self, broadcaster):
        if broadcaster in self._pending_broadcasters:
            self._pending_broadcasters.remove(broadcaster)
        if broadcaster in self._active_broadcasters:
            self._remove_from_cluster_request(broadcaster)
            self._remove_broadcaster_from_quadtree(broadcaster)
            self._active_broadcasters.remove(broadcaster)
        broadcaster.on_removed()
        self._on_update_callbacks()

    def _activate_pending_broadcasters(self):
        for broadcaster in self._pending_broadcasters:
            self._active_broadcasters.append(broadcaster)
            self.update_cluster_request(broadcaster)
            self._update_object_cache()
        self._pending_broadcasters.clear()

    def _add_broadcaster_to_quadtree(self, broadcaster):
        self._remove_broadcaster_from_quadtree(broadcaster)
        broadcaster_quadtree = self._quadtrees[
            broadcaster.routing_surface.secondary_id]
        broadcaster_bounds = sims4.geometry.QtCircle(
            sims4.math.Vector2(broadcaster.position.x, broadcaster.position.z),
            self.DEFAULT_QUADTREE_RADIUS)
        broadcaster_quadtree.insert(broadcaster, broadcaster_bounds)
        return broadcaster_quadtree

    def _remove_broadcaster_from_quadtree(self, broadcaster):
        broadcaster_quadtree = broadcaster.quadtree
        if broadcaster_quadtree is not None:
            broadcaster_quadtree.remove(broadcaster)

    def update_cluster_request(self, broadcaster):
        if broadcaster not in self._active_broadcasters:
            return
        clustering_request = broadcaster.get_clustering()
        if clustering_request is None:
            return
        self._remove_from_cluster_request(broadcaster)
        cluster_request_key = (type(broadcaster),
                               broadcaster.routing_surface.secondary_id)
        if cluster_request_key in self._cluster_requests:
            cluster_request = self._cluster_requests[cluster_request_key]
            cluster_request.set_object_dirty(broadcaster)
        else:
            cluster_quadtree = self._quadtrees[
                broadcaster.routing_surface.secondary_id]
            cluster_request = clustering_request(
                lambda: self._get_broadcasters_for_cluster_request_gen(
                    *cluster_request_key),
                quadtree=cluster_quadtree)
            self._cluster_requests[cluster_request_key] = cluster_request
        quadtree = self._add_broadcaster_to_quadtree(broadcaster)
        broadcaster.on_added_to_quadtree_and_cluster_request(
            quadtree, cluster_request)

    def _remove_from_cluster_request(self, broadcaster):
        cluster_request = broadcaster.cluster_request
        if cluster_request is not None:
            cluster_request.set_object_dirty(broadcaster)

    def _update_object_cache(self, obj=None):
        if obj is None:
            self._object_cache = None
            return
        if self._object_cache is not None:
            self._object_cache.add(obj)

    def _is_valid_broadcaster(self, broadcaster):
        broadcasting_object = broadcaster.broadcasting_object
        if broadcasting_object is None:
            return False
        if broadcasting_object.is_in_inventory():
            return False
        if broadcasting_object.parent is not None and broadcasting_object.parent.is_sim:
            return False
        return True

    def _get_broadcasters_for_cluster_request_gen(self, broadcaster_type,
                                                  broadcaster_level):
        for broadcaster in self._active_broadcasters:
            while broadcaster.guid == broadcaster_type.guid:
                if broadcaster.should_cluster(
                ) and broadcaster.routing_surface.secondary_id == broadcaster_level:
                    yield broadcaster

    def get_broadcasters_gen(self, inspect_only=False):
        for (cluster_request_key,
             cluster_request) in self._cluster_requests.items():
            is_cluster_dirty = cluster_request.is_dirty()
            if is_cluster_dirty:
                for broadcaster in self._get_broadcasters_for_cluster_request_gen(
                        *cluster_request_key):
                    broadcaster.regenerate_constraint()
            while not is_cluster_dirty or not inspect_only:
                while True:
                    for cluster in cluster_request.get_clusters_gen():
                        broadcaster_iter = cluster.objects_gen()
                        master_broadcaster = next(broadcaster_iter)
                        master_broadcaster.set_linked_broadcasters(
                            list(broadcaster_iter))
                        yield master_broadcaster
        for broadcaster in self._active_broadcasters:
            while not broadcaster.should_cluster():
                if self._is_valid_broadcaster(broadcaster):
                    yield broadcaster

    def get_pending_broadcasters_gen(self):
        yield self._pending_broadcasters

    def _get_all_objects_gen(self):
        if any(broadcaster.allow_objects
               for broadcaster in self._active_broadcasters):
            if self._object_cache is None:
                self._object_cache = WeakSet(
                    services.object_manager().valid_objects())
            yield list(self._object_cache)
        else:
            self._object_cache = None
            yield services.sim_info_manager().instanced_sims_gen()

    def register_callback(self, callback):
        if callback not in self._on_update_callbacks:
            self._on_update_callbacks.append(callback)

    def unregister_callback(self, callback):
        if callback in self._on_update_callbacks:
            self._on_update_callbacks.remove(callback)

    def _on_update(self, _):
        self._pending_update = True

    def update(self):
        if self._pending_update:
            self._pending_update = False
            self._update()

    def _update(self):
        try:
            self._activate_pending_broadcasters()
            current_broadcasters = set(self.get_broadcasters_gen())
            for obj in self._get_all_objects_gen():
                is_affected = False
                for broadcaster in current_broadcasters:
                    while broadcaster.can_affect(obj):
                        constraint = broadcaster.get_constraint()
                        if not constraint.valid:
                            pass
                        if constraint.geometry is None or constraint.geometry.contains_point(
                                obj.position
                        ) and constraint.routing_surface == obj.routing_surface:
                            broadcaster.apply_broadcaster_effect(obj)
                            is_affected = True
                while not is_affected:
                    if self._object_cache is not None:
                        self._object_cache.remove(obj)
            for broadcaster in current_broadcasters:
                broadcaster.on_processed()
        finally:
            self._on_update_callbacks()
Exemple #20
0
class RoutingComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.ROUTING_COMPONENT):
    _pathplan_context = None
    FACTORY_TUNABLES = {'plan_context_data': TunableTuple(description='\n            Data used to populate fields on the path plan context.\n            ', default_context=PathPlanContextWrapper.TunableFactory(description="\n                If no age override is specified, the default path plan data to\n                use for this agent's path planning.\n                "), context_age_species_overrides=TunableList(description='\n                List of age-species path plan context overrides for a specific\n                routing agent.\n                ', tunable=TunableTuple(description='\n                    Overrides to the path plan context of the agent defined by a\n                    combination of age and species.\n                    ', age=TunableEnumEntry(description='\n                        The age this override applies to.\n                        ', tunable_type=Age, default=Age.ADULT), species=TunableEnumEntry(description='\n                        The species this override applies to.\n                        ', tunable_type=SpeciesExtended, default=SpeciesExtended.HUMAN, invalid_enums=(SpeciesExtended.INVALID,)), context_override=PathPlanContextWrapper.TunableFactory()))), 'walkstyle_behavior': WalksStyleBehavior.TunableFactory(description='\n            Define the walkstyle behavior for owners of this component.\n            '), 'object_routing_component': OptionalTunable(description='\n            If enabled, this object will have an Object Routing component, which\n            controls an object routing behavior based on triggered states.\n            ', tunable=ObjectRoutingComponent.TunableFactory())}

    def __init__(self, owner, **kwargs):
        super().__init__(owner, **kwargs)
        self.owner = owner
        walkstyle_behavior = self.get_walkstyle_behavior()
        self._walkstyle_requests = [WalkStyleRequest(self.owner, walkstyle=walkstyle_behavior.default_walkstyle, priority=-1)]
        self._walk_style_handles = {}
        self.wading_buff_handle = None
        self.last_route_has_wading_nodes = False
        self._routing_stage_event_callbacks = defaultdict(CallableList)
        if owner.is_sim:
            owner.remove_component(objects.components.types.FOOTPRINT_COMPONENT)
        self._path_plan_context_map = {}
        self.on_slot = None
        self.stand_slot_reservation_removed_callbacks = CallableList()
        self._active_follow_path_weakref = None
        self.on_follow_path = CallableList()
        self.on_plan_path = CallableList()
        self.on_intended_location_changed = CallableList()
        self._current_path = None
        self._routing_slave_data = []
        self._routing_master_ref = None
        self._default_agent_radius = None
        self._route_event_context = RouteEventContext()
        self._route_interaction = None
        self._animation_context = None
        self._initial_carry_targets = None
        self._route_event_provider_requests = None

    def get_subcomponents_gen(self):
        yield from super().get_subcomponents_gen()
        if self.object_routing_component is not None:
            object_routing_component = self.object_routing_component(self.owner)
            yield from object_routing_component.get_subcomponents_gen()

    @property
    def current_path(self):
        return self._current_path

    @property
    def object_radius(self):
        return self._pathplan_context.agent_radius

    @property
    def route_event_context(self):
        return self._route_event_context

    @componentmethod
    def register_routing_stage_event(self, routing_stage_event, callback):
        self._routing_stage_event_callbacks[routing_stage_event].register(callback)

    def _on_routing_stage_event(self, routing_stage_event, **kwargs):
        callbacks = self._routing_stage_event_callbacks.get(routing_stage_event)
        if callbacks is not None:
            callbacks(self.owner, routing_stage_event, **kwargs)

    @contextmanager
    def temporary_walkstyle_request(self, walkstyle_request_factory):
        try:
            request = walkstyle_request_factory(self.owner)
            request.start()
            yield None
        finally:
            request.stop()

    @componentmethod
    def unregister_routing_stage_event(self, routing_stage_event, callback):
        self._routing_stage_event_callbacks[routing_stage_event].unregister(callback)

    @componentmethod
    def get_walkstyle_behavior(self):
        return self.walkstyle_behavior

    @componentmethod
    def get_cost_valid_walkstyle(self, cost_tuning):
        for walkstyle in self._walkstyle_requests:
            if walkstyle.priority != WalkStylePriority.COMBO:
                walkstyle_cost = cost_tuning.get(walkstyle.walkstyle, None)
                if walkstyle_cost is None:
                    return walkstyle.walkstyle
                current_value = self.owner.get_stat_value(walkstyle_cost.walkstyle_cost_statistic)
                if current_value - walkstyle_cost.cost > walkstyle_cost.walkstyle_cost_statistic.min_value:
                    return walkstyle.walkstyle
        walkstyle_behavior = self.get_walkstyle_behavior()
        return walkstyle_behavior.default_walkstyle

    @componentmethod
    def get_default_walkstyle(self):
        return self.walkstyle_behavior.get_default_walkstyle(self.owner)

    @componentmethod
    def get_walkstyle(self):
        for walkstyle in self._walkstyle_requests:
            if walkstyle.priority != WalkStylePriority.COMBO:
                return walkstyle.walkstyle
        walkstyle_behavior = self.get_walkstyle_behavior()
        return walkstyle_behavior.default_walkstyle

    @componentmethod
    def get_walkstyle_for_path(self, path):
        return self.walkstyle_behavior.get_walkstyle_for_path(self.owner, path)

    @componentmethod
    def get_walkstyle_list(self):
        return tuple(request.walkstyle for request in self._walkstyle_requests)

    @componentmethod
    def get_walkstyle_requests(self):
        return self._walkstyle_requests

    def _get_walkstyle_key(self):
        if self.owner.is_sim:
            return (self.owner.age, self.owner.gender, self.owner.extended_species)
        return (Age.ADULT, Gender.MALE, SpeciesExtended.HUMAN)

    @componentmethod
    def request_walkstyle(self, walkstyle_request, uid):
        if walkstyle_request.priority != WalkStylePriority.COMBO and not has_walkstyle_info(walkstyle_request.walkstyle, *self._get_walkstyle_key()):
            return
        self._walkstyle_requests.append(walkstyle_request)
        self._walkstyle_requests.sort(reverse=True, key=operator.attrgetter('priority'))
        self._walk_style_handles[uid] = RegistryHandle(lambda : self._unrequest_walkstyle(walkstyle_request))
        self._update_walkstyle()

    @componentmethod
    def remove_walkstyle(self, uid):
        if uid in self._walk_style_handles:
            self._walk_style_handles[uid].release()
            del self._walk_style_handles[uid]

    def _unrequest_walkstyle(self, walkstyle_request):
        self._walkstyle_requests.remove(walkstyle_request)
        self._update_walkstyle()

    def _update_walkstyle(self):
        for primitive in self.owner.primitives:
            try:
                primitive.request_walkstyle_update()
            except AttributeError:
                pass

    @componentmethod
    def get_additional_scoring_for_surface(self, surface_type):
        return self._pathplan_context.surface_preference_scoring.get(surface_type, 0)

    @componentmethod
    def add_location_to_quadtree(self, *args, **kwargs):
        path_plan_context = self._pathplan_context
        return path_plan_context.add_location_to_quadtree(*args, **kwargs)

    @componentmethod
    def remove_location_from_quadtree(self, *args, **kwargs):
        path_plan_context = self._pathplan_context
        return path_plan_context.remove_location_from_quadtree(*args, **kwargs)

    @property
    def _pathplan_context(self):
        age_key = getattr(self.owner, 'age', DEFAULT)
        species_key = getattr(self.owner, 'extended_species', DEFAULT)
        combined_override_key = (age_key, species_key)
        if combined_override_key in self._path_plan_context_map:
            return self._path_plan_context_map[combined_override_key]
        for override in self.plan_context_data.context_age_species_overrides:
            if override.age == age_key:
                if override.species == species_key:
                    self._path_plan_context_map[combined_override_key] = override.context_override(self.owner)
                    return self._path_plan_context_map[combined_override_key]
        self._path_plan_context_map.clear()
        self._path_plan_context_map[combined_override_key] = self.plan_context_data.default_context(self.owner)
        return self._path_plan_context_map[combined_override_key]

    @componentmethod
    def get_routing_context(self):
        return self.pathplan_context

    def on_sim_added(self):
        self._update_quadtree_location()

    def on_sim_removed(self):
        self.on_slot = None
        self._routing_master_ref = None
        self.clear_routing_slaves()

    def on_reset_internal_state(self, reset_reason):
        self.clear_routing_slaves()

    def add_callbacks(self):
        self.owner.register_on_location_changed(self._update_quadtree_location)
        self.owner.register_on_location_changed(self._check_violations)
        if self.get_walkstyle_behavior().supports_wading_walkstyle_buff(self.owner):
            self.owner.register_on_location_changed(self.get_walkstyle_behavior().check_for_wading)
        self.on_plan_path.append(self._on_update_goals)
        self.on_intended_location_changed.append(self.owner.refresh_los_constraint)
        self.on_intended_location_changed.append(self.owner._update_social_geometry_on_location_changed)
        self.on_intended_location_changed.append(lambda *_, **__: self.owner.two_person_social_transforms.clear())
        self.on_intended_location_changed.append(self.owner.update_intended_position_on_active_lot)

    def remove_callbacks(self):
        self.on_intended_location_changed.clear()
        if self._on_update_goals in self.on_plan_path:
            self.on_plan_path.remove(self._on_update_goals)
        if self.owner._on_location_changed_callbacks is not None and self._check_violations in self.owner._on_location_changed_callbacks:
            self.owner.unregister_on_location_changed(self._check_violations)
        if self.owner._on_location_changed_callbacks is not None and self._update_quadtree_location in self.owner._on_location_changed_callbacks:
            self.owner.unregister_on_location_changed(self._update_quadtree_location)

    @property
    def pathplan_context(self):
        return self._pathplan_context

    def get_or_create_routing_context(self):
        return self._pathplan_context

    @property
    def routing_context(self):
        return self._pathplan_context

    @property
    def connectivity_handles(self):
        return self._pathplan_context.connectivity_handles

    @property
    def is_moving(self):
        if self.owner.is_sim:
            return not self.owner.location.almost_equal(self.owner.intended_location)
        else:
            return self.current_path is not None

    @property
    def routing_master(self):
        if self._routing_master_ref is not None:
            return self._routing_master_ref()

    @routing_master.setter
    def routing_master(self, value):
        self._routing_master_ref = value.ref() if value is not None else None

    @property
    def route_interaction(self):
        return self._route_interaction

    @property
    def animation_context(self):
        if self._route_interaction is not None:
            return self._route_interaction.animation_context
        return self._animation_context

    SLAVE_RADIUS_MODIFIER = 0.5
    MAX_ALLOWED_AGENT_RADIUS = 0.25

    def _update_agent_radius(self):
        agent_radius_datas = [slave_data for slave_data in self._routing_slave_data if slave_data.should_increase_master_agent_radius]
        if not agent_radius_datas:
            return
        max_x = max(abs(slave_data.offset[0]) for slave_data in agent_radius_datas)
        max_x = max(max_x*self.SLAVE_RADIUS_MODIFIER, self._default_agent_radius)
        self._pathplan_context.agent_radius = min(self.MAX_ALLOWED_AGENT_RADIUS, max_x)

    def add_routing_slave(self, slave_data):
        if len(self._routing_slave_data) == 0:
            self._default_agent_radius = self._pathplan_context.agent_radius
        slave_data.slave.routing_master = self.owner
        self._routing_slave_data.append(slave_data)
        slave_data.on_add()
        self._update_agent_radius()

    def clear_routing_slaves(self):
        for slave_data in self._routing_slave_data:
            slave_data.slave.routing_master = None
            slave_data.on_release()
        self._routing_slave_data.clear()
        self._restore_agent_radius()
        self._update_agent_radius()

    @componentmethod
    def get_routing_slave_data(self):
        return self._routing_slave_data

    @componentmethod
    def get_formation_data_for_slave(self, obj):
        for slave_data in self._routing_slave_data:
            if slave_data.slave is obj:
                return slave_data

    @componentmethod
    def get_all_routing_slave_data_gen(self):
        yield from self._routing_slave_data

    @componentmethod
    def get_routing_slave_data_count(self, formation_type):
        return sum(1 for slave_data in self._routing_slave_data if slave_data.formation_type is formation_type)

    def clear_slave(self, slave):
        for slave_data in self._routing_slave_data:
            if slave_data.slave is slave:
                self._routing_slave_data.remove(slave_data)
                slave_data.on_release()
                break
        self._restore_agent_radius()
        slave.routing_master = None
        self._update_agent_radius()

    @componentmethod
    def write_slave_data_msg(self, route_msg, path=None):
        transitioning_sims = ()
        actor = self.owner
        if actor.is_sim:
            if actor.transition_controller is not None:
                transitioning_sims = actor.transition_controller.get_transitioning_sims()
        for slave_data in self.get_routing_slave_data():
            if slave_data.should_slave_for_path(path):
                if slave_data.slave in transitioning_sims:
                    continue
                (slave_actor, slave_msg) = slave_data.add_routing_slave_to_pb(route_msg, path=path)
                slave_actor.write_slave_data_msg(slave_msg, path=path)
        for slave_actor in actor.children:
            if not slave_actor.is_sim:
                continue
            carry_walkstyle_behavior = slave_actor.get_walkstyle_behavior().carry_walkstyle_behavior
            if carry_walkstyle_behavior is None:
                continue
            with ProtocolBufferRollback(route_msg.slaves) as slave_msg:
                slave_msg.id = slave_actor.id
                slave_msg.type = Routing_pb2.SlaveData.SLAVE_PAIRED_CHILD
                walkstyle_override_msg = slave_msg.walkstyle_overrides.add()
                walkstyle_override_msg.from_walkstyle = 0
                walkstyle_override_msg.to_walkstyle = carry_walkstyle_behavior.default_carry_walkstyle
                for (walkstyle, carry_walkstyle) in carry_walkstyle_behavior.carry_walkstyle_overrides.items():
                    walkstyle_override_msg = slave_msg.walkstyle_overrides.add()
                    walkstyle_override_msg.from_walkstyle = walkstyle
                    walkstyle_override_msg.to_walkstyle = carry_walkstyle
                slave_actor.write_slave_data_msg(slave_msg, path=path)

    def _restore_agent_radius(self):
        if self._default_agent_radius is not None:
            if len(self._routing_slave_data) == 0:
                self._pathplan_context.agent_radius = self._default_agent_radius
                self._default_agent_radius = None

    def contains_slave(self, slave):
        return any(slave.id == slave_data.slave.id for slave_data in self._routing_slave_data)

    def _on_update_goals(self, goal_list, starting):
        NUM_GOALS_TO_RESERVE = 2
        for (index, goal) in enumerate(goal_list, start=1):
            if index > NUM_GOALS_TO_RESERVE:
                break
            if starting:
                self.add_location_to_quadtree(placement.ItemType.SIM_INTENDED_POSITION, position=goal.position, orientation=goal.orientation, routing_surface=goal.routing_surface_id, index=index)
            else:
                self.remove_location_from_quadtree(placement.ItemType.SIM_INTENDED_POSITION, index=index)

    def set_portal_mask_flag(self, flag):
        self._pathplan_context.set_portal_key_mask(self._pathplan_context.get_portal_key_mask() | flag)

    def clear_portal_mask_flag(self, flag):
        self._pathplan_context.set_portal_key_mask(self._pathplan_context.get_portal_key_mask() & ~flag)

    def set_portal_discouragement_mask_flag(self, flag):
        self._pathplan_context.set_portal_discourage_key_mask(self._pathplan_context.get_portal_discourage_key_mask() | flag)

    def clear_portal_discouragement_mask_flag(self, flag):
        self._pathplan_context.set_portal_discourage_key_mask(self._pathplan_context.get_portal_discourage_key_mask() & ~flag)

    @componentmethod
    def update_portal_locks(self):
        sim = self.owner
        sim.manager.add_portal_lock(sim, sim._portal_added_callback)

    def _update_quadtree_location(self, *_, **__):
        self.add_location_to_quadtree(placement.ItemType.SIM_POSITION)

    def add_stand_slot_reservation(self, interaction, position, routing_surface, excluded_sims):
        interaction.add_liability(STAND_SLOT_LIABILITY, StandSlotReservationLiability(self.owner, interaction))
        excluded_sims.add(self.owner)
        self._stand_slot_reservation = position
        self.add_location_to_quadtree(placement.ItemType.ROUTE_GOAL_SUPPRESSOR, position=position, routing_surface=routing_surface)
        pathplan_context = self._pathplan_context
        reservation_radius = pathplan_context.agent_radius*2
        polygon = sims4.geometry.generate_circle_constraint(6, position, reservation_radius)
        self.on_slot = (position, polygon, routing_surface)
        UserFootprintHelper.force_move_sims_in_polygon(polygon, routing_surface, exclude=excluded_sims)

    def remove_stand_slot_reservation(self, interaction):
        self.remove_location_from_quadtree(placement.ItemType.ROUTE_GOAL_SUPPRESSOR)
        self.on_slot = None
        self.stand_slot_reservation_removed_callbacks(sim=self)

    def get_stand_slot_reservation_violators(self, excluded_sims=()):
        if not self.on_slot:
            return
        (_, polygon, routing_surface) = self.on_slot
        violators = []
        excluded_sims = {sim for sim in itertools.chain((self.owner,), excluded_sims)}
        for sim_nearby in placement.get_nearby_sims_gen(polygon.centroid(), routing_surface, radius=polygon.radius(), exclude=excluded_sims):
            if sims4.geometry.test_point_in_polygon(sim_nearby.position, polygon):
                if not sim_nearby.ignore_blocking_near_destination:
                    violators.append(sim_nearby)
        return violators

    def _check_violations(self, *_, **__):
        if services.privacy_service().check_for_late_violators(self.owner):
            return
        for reaction_trigger in self.owner.reaction_triggers.values():
            reaction_trigger.intersect_and_execute(self.owner)

    def create_route_interaction(self):
        if self.owner.is_sim:
            aop = AffordanceObjectPair(AnimationInteraction, None, AnimationInteraction, None, hide_unrelated_held_props=False)
            context = InteractionContext(self.owner, InteractionContext.SOURCE_SCRIPT, Priority.High)
            self._route_interaction = aop.interaction_factory(context).interaction
        else:
            self._animation_context = AnimationContext()
            self._animation_context.add_ref(self._current_path)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.create_route_interaction()

    def cancel_route_interaction(self):
        if self._route_interaction is not None:
            self._route_interaction.cancel(FinishingType.AUTO_EXIT, 'Route Ended.')
            self._route_interaction.on_removed_from_queue()
            self._route_interaction = None
        if self._animation_context is not None:
            self._animation_context.release_ref(self._current_path)
            self._animation_context = None
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.cancel_route_interaction()

    def set_follow_path(self, follow_path):
        self._active_follow_path_weakref = weakref.ref(follow_path)

    def clear_follow_path(self):
        self._active_follow_path_weakref = None

    def _get_active_follow_path(self):
        if self._active_follow_path_weakref is not None:
            return self._active_follow_path_weakref()

    def get_approximate_cancel_location(self):
        follow_path = self._get_active_follow_path()
        if follow_path is not None:
            ret = follow_path.get_approximate_cancel_location()
            if ret is not None:
                return ret
        return (self.owner.intended_transform, self.owner.intended_routing_surface)

    @componentmethod
    def set_routing_path(self, path):
        if path is None:
            if gsi_handlers.route_event_handlers.archiver.enabled:
                gsi_handlers.route_event_handlers.archive_route_events(self._current_path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_FINISHED, clear=True)
            self._on_routing_stage_event(RoutingStageEvent.ROUTE_END, path=self._current_path)
            self.cancel_route_interaction()
            self._current_path = None
            if self.owner.is_sim and self.owner.transition_controller is not None and self._initial_carry_targets != self.owner.posture_state.carry_targets:
                self.owner.transition_controller.derail(DerailReason.CONSTRAINTS_CHANGED, self.owner)
            self._initial_carry_targets = None
            return
        if self.owner.is_sim:
            self._initial_carry_targets = self.owner.posture_state.carry_targets
        self._current_path = path
        if self.pathplan_context.disable_fake_portals:
            self._current_path.remove_fake_portals()
        self.create_route_interaction()
        self._on_routing_stage_event(RoutingStageEvent.ROUTE_START, path=path)
        walkstyle = self.walkstyle_behavior.apply_walkstyle_to_path(self.owner, self._current_path)
        origin_q = (float(self.owner.orientation.x), float(self.owner.orientation.y), float(self.owner.orientation.z), float(self.owner.orientation.w))
        origin_t = (float(self.owner.position.x), float(self.owner.position.y), float(self.owner.position.z))
        (age, gender, species) = self._get_walkstyle_key()
        self._current_path.nodes.apply_initial_timing(origin_q, origin_t, walkstyle, age, gender, species, int(services.time_service().sim_now), services.current_zone_id())

    @componentmethod
    def update_routing_path(self, time_offset):
        if self._current_path is None:
            return
        walkstyle = self.walkstyle_behavior.apply_walkstyle_to_path(self.owner, self._current_path, time_offset=time_offset)
        (age, gender, species) = self._get_walkstyle_key()
        self._current_path.nodes.update_timing(walkstyle, age, gender, species, time_offset, services.current_zone_id())

    @componentmethod
    def update_slave_positions_for_path(self, path, transform, orientation, routing_surface, distribute=True, canceled=False):
        transitioning_sims = ()
        if self.owner.is_sim:
            if self.owner.transition_controller is not None:
                transitioning_sims = self.owner.transition_controller.get_transitioning_sims()
        for slave_data in self.get_routing_slave_data():
            if slave_data.slave in transitioning_sims:
                continue
            slave_data.update_slave_position(transform, orientation, routing_surface, distribute=distribute, path=path, canceled=canceled)

    def add_route_event_provider(self, request):
        if self._route_event_provider_requests is None:
            self._route_event_provider_requests = []
        self._route_event_provider_requests.append(request)

    def remove_route_event_provider(self, request):
        if self._route_event_provider_requests is not None and request in self._route_event_provider_requests:
            self._route_event_provider_requests.remove(request)
        if not self._route_event_provider_requests:
            self._route_event_provider_requests = None

    def route_event_executed(self, event_id):
        if self._route_event_context is None:
            return
        self._route_event_context.handle_route_event_executed(event_id, self.owner, path=self._current_path)

    def route_event_skipped(self, event_id):
        if self._route_event_context is None:
            return
        self._route_event_context.handle_route_event_skipped(event_id, self.owner, path=self._current_path)

    def remove_route_event_by_data(self, event_data):
        if self._route_event_context is None:
            return
        self._route_event_context.remove_route_event_by_data(event_data)

    @componentmethod
    def route_finished(self, path_id):
        for primitive in self.owner.primitives:
            if hasattr(primitive, 'route_finished'):
                primitive.route_finished(path_id)

    @componentmethod
    def route_time_update(self, path_id, current_time):
        for primitive in self.owner.primitives:
            if hasattr(primitive, 'route_time_update'):
                primitive.route_time_update(path_id, current_time)

    def _gather_route_events(self, path, **kwargs):
        owner = self.owner
        if owner.is_sim:
            interaction = owner.transition_controller.interaction if owner.transition_controller is not None else None
            if interaction is not None and interaction.is_super:
                interaction.provide_route_events(self._route_event_context, owner, path, **kwargs)
            owner.Buffs.provide_route_events_from_buffs(self._route_event_context, owner, path, **kwargs)
        broadcaster_service = services.current_zone().broadcaster_service
        broadcaster_service.provide_route_events(self._route_event_context, owner, path, **kwargs)
        if owner.weather_aware_component is not None:
            owner.weather_aware_component.provide_route_events(self._route_event_context, owner, path, **kwargs)
        if self._route_event_provider_requests is not None:
            for request in self._route_event_provider_requests:
                request.provide_route_events(self._route_event_context, owner, path, **kwargs)
        object_manager = services.object_manager(owner.zone_id)
        if object_manager is not None:
            for node in path.nodes:
                if node.portal_object_id != 0:
                    portal_object = object_manager.get(node.portal_object_id)
                    if portal_object is not None:
                        portal_object.provide_route_events(node.portal_id, self._route_event_context, owner, path, node=node, **kwargs)

    def clear_route_events(self, *args, **kwargs):
        if self._route_event_context is None:
            return
        self._route_event_context.clear_route_events()
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.clear_route_events()

    def schedule_and_process_route_events_for_new_path(self, path):
        if self._route_event_context is None:
            return
        if self.owner.is_sim and not self.owner.posture.mobile:
            return
        self.clear_route_events()
        start_time = RouteEventContext.ROUTE_TRIM_START
        end_time = min(start_time + ROUTE_EVENT_WINDOW_DURATION, path.duration())
        self._gather_route_events(path, start_time=start_time, end_time=end_time)
        self._route_event_context.schedule_route_events(self.owner, path)
        self._route_event_context.process_route_events(self.owner)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.schedule_and_process_route_events_for_new_path(path)
        if gsi_handlers.route_event_handlers.archiver.enabled:
            gsi_handlers.route_event_handlers.archive_route_events(path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_INITIAL)

    def append_route_events_to_route_msg(self, route_msg):
        if self._route_event_context is None:
            return
        self._route_event_context.append_route_events_to_route_msg(route_msg)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.append_route_events_to_route_msg(route_msg)

    def update_route_events_for_current_path(self, path, current_time, time_offset):
        (failed_events, failed_types) = self._route_event_context.prune_stale_events_and_get_failed_types(self.owner, path, current_time)
        start_time = current_time
        window_duration = ROUTE_EVENT_WINDOW_DURATION
        if time_offset < 0:
            window_duration -= time_offset
        end_time = min(start_time + window_duration, path.duration())
        self._gather_route_events(path, failed_types=failed_types, start_time=start_time, end_time=end_time)
        self._route_event_context.schedule_route_events(self.owner, path, start_time=start_time)
        should_update = True if failed_events else False or self._route_event_context.has_pending_events_to_process()
        for slave_data in self.get_routing_slave_data():
            should_update |= slave_data.slave.routing_component.update_route_events_for_current_path(path, current_time, time_offset)
        if gsi_handlers.route_event_handlers.archiver.enabled and gsi_handlers.route_event_handlers.update_log_enabled:
            gsi_handlers.route_event_handlers.archive_route_events(path, self.owner, gsi_handlers.route_event_handlers.PATH_TYPE_UPDATE)
        return should_update

    def process_updated_route_events(self):
        self._route_event_context.process_route_events(self.owner)
        for slave_data in self.get_routing_slave_data():
            slave_data.slave.routing_component.process_updated_route_events()

    @componentmethod
    def should_route_instantly(self):
        zone = services.current_zone()
        if zone.force_route_instantly:
            return True
        if self.owner.is_sim:
            if not (zone.are_sims_hitting_their_marks and self.owner._allow_route_instantly_when_hitting_marks):
                return False
            else:
                return not services.sim_spawner_service().sim_is_leaving(self.owner)
        return False
class SituationGoal(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.SITUATION_GOAL)):
    __qualname__ = 'SituationGoal'
    INSTANCE_SUBCLASSES_ONLY = True
    IS_TARGETED = False
    INSTANCE_TUNABLES = {'_display_name': TunableLocalizedStringFactory(description='\n                Display name for the Situation Goal. It takes one token, the\n                target (0) of this situation goal.\n                ', tuning_group=GroupNames.UI), '_icon': TunableResourceKey('PNG:missing_image', description='\n                Icon to be displayed for the Situation Goal.\n                ', needs_tuning=True, resource_types=sims4.resources.CompoundTypes.IMAGE), '_pre_tests': TunableSituationGoalPreTestSet(description='\n                A set of tests on the player sim and environment that all must\n                pass for the goal to be given to the player. e.g. Player Sim\n                has cooking skill level 7.\n                ', tuning_group=GroupNames.TESTS), '_post_tests': TunableSituationGoalPostTestSet(description='\n                A set of tests that must all pass when the player satisfies the\n                goal_test for the goal to be consider completed. e.g. Player\n                has Drunk Buff when Kissing another sim at Night.\n                ', tuning_group=GroupNames.TESTS), '_environment_pre_tests': TunableSituationGoalEnvironmentPreTestSet(description='\n                A set of sim independent pre tests.\n                e.g. There are five desks.\n                ', tuning_group=GroupNames.TESTS), 'role_tags': TunableSet(TunableEnumEntry(Tag, Tag.INVALID), description='\n                This goal will only be given to Sims in SituationJobs or Role\n                States marked with one of these tags.\n                '), '_cooldown': TunableSimMinute(description='\n                The cooldown of this situation goal.  Goals that have been\n                completed will not be chosen again for the amount of time that\n                is tuned.\n                ', default=600, minimum=0), '_iterations': Tunable(description='\n                     Number of times the player must perform the action to complete the goal\n                     ', tunable_type=int, default=1), '_score': Tunable(description='\n                    The number of points received for completing the goal.\n                    ', tunable_type=int, default=10), '_goal_loot_list': TunableList(description='\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is completed.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ', tunable=SituationGoalLootActions.TunableReference()), '_tooltip': TunableLocalizedStringFactory(description='\n            Tooltip for this situation goal. It takes one token, the\n            actor (0) of this situation goal.'), 'noncancelable': Tunable(description='\n            Checking this box will prevent the player from canceling this goal in the whim system.', tunable_type=bool, needs_tuning=True, default=False), 'time_limit': Tunable(description='\n            Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n            time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.', tunable_type=int, default=0), 'penalty_loot_list': TunableList(description='\n            A list of pre-defined loot actions that will applied to the Sim who fails\n            to complete this goal within the tuned time limit.\n            ', tunable=SituationGoalLootActions.TunableReference()), 'goal_completion_notification': OptionalTunable(tunable=UiDialogNotification.TunableFactory(description='\n                A TNS that will fire when this situation goal is completed.\n                ')), 'audio_sting_on_complete': TunableResourceKey(description='\n            The sound to play when this goal is completed.\n            ', default=None, resource_types=(sims4.resources.Types.PROPX,), tuning_group=GroupNames.AUDIO), 'goal_completion_modal_dialog': OptionalTunable(tunable=UiDialogOk.TunableFactory(description='\n                A modal dialog that will fire when this situation goal is\n                completed.\n                '))}

    @classmethod
    def can_be_given_as_goal(cls, actor, situation, **kwargs):
        if actor is not None:
            resolver = event_testing.resolver.DataResolver(actor.sim_info, None)
            result = cls._pre_tests.run_tests(resolver)
            if not result:
                return result
        environment_test_result = cls._environment_pre_tests.run_tests(Resolver())
        if not environment_test_result:
            return environment_test_result
        return TestResult.TRUE

    def __init__(self, sim_info=None, situation=None, goal_id=0, count=0, **kwargs):
        self._sim_info = sim_info
        self._situation = situation
        self.id = goal_id
        self._on_goal_completed_callbacks = CallableList()
        self._completed_time = None
        self._count = count

    def destroy(self):
        self.decommision()
        self._sim_info = None
        self._situation = None

    def decommision(self):
        self._on_goal_completed_callbacks.clear()

    def create_seedling(self):
        actor_id = 0 if self._sim_info is None else self._sim_info.sim_id
        seedling = situations.situation_serialization.GoalSeedling(type(self), actor_id, self._count)
        return seedling

    def register_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.append(listener)

    def unregister_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.remove(listener)

    def get_gsi_name(self):
        if self._iterations <= 1:
            return self.__class__.__name__
        return '{} {}/{}'.format(self.__class__.__name__, self._count, self._iterations)

    def _on_goal_completed(self):
        self._completed_time = services.time_service().sim_now
        for loots in self._goal_loot_list:
            for loot in loots.goal_loot_actions:
                for sim in self._situation.all_sims_in_situation_gen():
                    loot.apply_to_resolver(sim.get_resolver())
        client = services.client_manager().get_first_client()
        if client is not None:
            active_sim = client.active_sim
            resolver = SingleSimResolver(active_sim)
            if self.goal_completion_notification is not None:
                notification = self.goal_completion_notification(active_sim, resolver=resolver)
                notification.show_dialog()
            if self.goal_completion_modal_dialog is not None:
                dialog = self.goal_completion_modal_dialog(active_sim, resolver=resolver)
                dialog.show_dialog()
        self._on_goal_completed_callbacks(self, True)

    def _on_iteration_completed(self):
        self._on_goal_completed_callbacks(self, False)

    def debug_force_complete(self, target_sim):
        self._count = self._iterations
        self._on_goal_completed()

    def handle_event(self, sim_info, event, resolver):
        if self._sim_info is not None and self._sim_info is not sim_info:
            return
        if self._run_goal_completion_tests(sim_info, event, resolver):
            if self._count >= self._iterations:
                self._on_goal_completed()
            else:
                self._on_iteration_completed()

    def _run_goal_completion_tests(self, sim_info, event, resolver):
        return self._post_tests.run_tests(resolver)

    def _get_actual_target_sim_info(self):
        pass

    def get_required_target_sim_info(self):
        pass

    @property
    def created_time(self):
        pass

    @property
    def completed_time(self):
        return self._completed_time

    def is_on_cooldown(self):
        if self._completed_time is None:
            return False
        time_since_last_completion = services.time_service().sim_now - self._completed_time
        return time_since_last_completion < interval_in_sim_minutes(self._cooldown)

    def get_localization_tokens(self):
        if self._sim_info is None:
            return (self._numerical_token,)
        required_tgt_sim = self.get_required_target_sim_info()
        if required_tgt_sim is None:
            return (self._numerical_token, self._sim_info)
        return (self._numerical_token, self._sim_info, required_tgt_sim)

    @property
    def display_name(self):
        return self._display_name(*self.get_localization_tokens())

    @property
    def tooltip(self):
        return self._tooltip(*self.get_localization_tokens())

    @property
    def score(self):
        return self._score

    @property
    def completed_iterations(self):
        return self._count

    @property
    def max_iterations(self):
        return self._iterations

    @property
    def _numerical_token(self):
        return self.max_iterations
Exemple #22
0
class ClientObjectMixin:
    INITIAL_DEPRECIATION = TunablePercent(
        20,
        description=
        'Amount (0%%-100%%) of depreciation to apply to an object after purchase. An item worth 10 in the catalog if tuned at 20%% will be worth 8 after purchase.'
    )
    FADE_DURATION = TunableSimMinute(
        1.2, description='Default fade time (in sim minutes) for objects.')
    VISIBLE_TO_AUTOMATION = True
    _get_next_ui_metadata_handle = uid.UniqueIdGenerator(min_uid=1)
    HOVERTIP_HANDLE = 0
    ui_metadata = distributor.sparse.SparseField(
        ui_protocols.UiObjectMetadata, distributor.ops.SetUiObjectMetadata)
    _generic_ui_metadata_setters = {}
    FORWARD_OFFSET = 0.04

    def __init__(self, definition, **kwargs):
        super().__init__(definition, **kwargs)
        if definition is not None:
            self.apply_definition(definition, **kwargs)
        self._ui_metadata_stack = None
        self._ui_metadata_handles = None
        self._ui_metadata_cache = None
        self.primitives = distributor.ops.DistributionSet(self)
        zone_id = services.current_zone_id()
        self._location = sims4.math.Location(
            sims4.math.Transform(),
            routing.SurfaceIdentifier(zone_id, 0,
                                      routing.SurfaceType.SURFACETYPE_WORLD))
        self._children_objects = None
        self._scale = 1
        self._parent_type = ObjectParentType.PARENT_NONE
        self._parent_location = 0
        self._build_buy_lockout = False
        self._build_buy_lockout_alarm_handler = None
        self._tint = None
        self._opacity = None
        self._censor_state = None
        self._geometry_state = None
        self._geometry_state_overrides = None
        self._standin_model = None
        self._visibility = None
        self._visibility_flags = None
        self._material_state = None
        self._reference_arb = None
        self._audio_effects = None
        self._video_playlist = None
        self._painting_state = None
        self.custom_name = None
        self.custom_description = None
        self._multicolor = None
        self._display_number = None
        self._awareness_scores = None
        self._scratched = False
        self._base_value = definition.price
        self._needs_post_bb_fixup = False
        self._needs_depreciation = False
        self._swapping_to_parent = None
        self._swapping_from_parent = None
        self._on_children_changed = None
        self.allow_opacity_change = True
        self._wind_speed_effect = None

    def get_create_op(self, *args, **kwargs):
        additional_ops = list(self.get_additional_create_ops_gen())
        return distributor.ops.ObjectCreate(self,
                                            *args,
                                            additional_ops=additional_ops,
                                            **kwargs)

    @forward_to_components_gen
    def get_additional_create_ops_gen(self):
        pass

    def get_create_after_objs(self):
        parent = self.parent_object(child_type=ChildrenType.BB_ONLY)
        if parent is not None:
            return (parent, )
        return ()

    def get_delete_op(self, fade_duration=0):
        return distributor.ops.ObjectDelete(fade_duration=fade_duration)

    @forward_to_components
    def apply_definition(self, definition, obj_state=0):
        if not isinstance(definition, objects.definition.Definition):
            definition = services.definition_manager().get(definition)
        self._model = definition.get_model(obj_state)
        self._material_variant = definition.material_variant
        self._rig = definition.get_rig(obj_state)
        self._slot = definition.get_slot(obj_state)
        self._slots_resource = definition.get_slots_resource(obj_state)
        self._state_index = obj_state

    def set_definition(self, definition_id, ignore_rig_footprint=False):
        new_definition = services.definition_manager().get(definition_id)
        (result, error) = self.definition.is_similar(
            new_definition, ignore_rig_footprint=ignore_rig_footprint)
        if not result:
            logger.error(
                'Trying to set the definition {} to an incompatible definition {}.\n {}',
                self.definition.id,
                definition_id,
                error,
                owner='nbaker')
            return False
        services.definition_manager().unregister_definition(
            self.definition.id, self)
        self.apply_definition(new_definition, self._state_index)
        self.definition = new_definition
        services.definition_manager().register_definition(
            new_definition.id, self)
        self.resend_model_with_material_variant()
        self.resend_slot()
        self.resend_state_index()
        op = distributor.ops.SetObjectDefinitionId(definition_id)
        distributor.system.Distributor.instance().add_op(self, op)
        return True

    @property
    def hover_tip(self):
        if self._ui_metadata_stack is None or ClientObjectMixin.HOVERTIP_HANDLE not in self._ui_metadata_handles:
            return
        (_, _,
         value) = self._ui_metadata_handles[ClientObjectMixin.HOVERTIP_HANDLE]
        return value

    @hover_tip.setter
    def hover_tip(self, value):
        if value is not None:
            if self._ui_metadata_stack is None:
                self._ui_metadata_stack = []
                self._ui_metadata_handles = {}
                self._ui_metadata_cache = {}
            data = self._ui_metadata_handles.get(
                ClientObjectMixin.HOVERTIP_HANDLE)
            if data is not None:
                self._ui_metadata_stack.remove(data)
            data = (ClientObjectMixin.HOVERTIP_HANDLE, 'hover_tip', value)
            self._ui_metadata_stack.append(data)
            self._ui_metadata_handles[ClientObjectMixin.HOVERTIP_HANDLE] = data

    def add_ui_metadata(self, name, value):
        if self._ui_metadata_stack is None:
            self._ui_metadata_stack = []
            self._ui_metadata_handles = {}
            self._ui_metadata_cache = {}
        if name not in self._ui_metadata_cache:
            default_value = type(self).ui_metadata.generic_getter(name)(self)
            self._ui_metadata_cache[name] = default_value
        handle = self._get_next_ui_metadata_handle()
        data = (handle, name, value)
        self._ui_metadata_stack.append(data)
        self._ui_metadata_handles[handle] = data
        return handle

    def get_ui_metadata(self, handle):
        return self._ui_metadata_handles[handle]

    def remove_ui_metadata(self, handle):
        if self._ui_metadata_stack is not None:
            self._ui_metadata_stack.remove(self._ui_metadata_handles[handle])

    def update_ui_metadata(self, use_cache=True):
        if self._ui_metadata_stack is None:
            return
        ui_metadata = {}
        for (_, name, value) in self._ui_metadata_stack:
            ui_metadata[name] = value
        for (name, value) in ui_metadata.items():
            if name in self._ui_metadata_cache and self._ui_metadata_cache[
                    name] == value and use_cache:
                continue
            if name in self._generic_ui_metadata_setters:
                setter = self._generic_ui_metadata_setters[name]
            else:
                setter = type(self).ui_metadata.generic_setter(name,
                                                               auto_reset=True)
                self._generic_ui_metadata_setters[name] = setter
            try:
                setter(self, value)
            except (ValueError, TypeError):
                logger.error(
                    'Error trying to set field {} to value {} in object {}.',
                    name,
                    value,
                    self,
                    owner='camilogarcia')
        for name in self._ui_metadata_cache.keys() - ui_metadata.keys():
            try:
                if name in self._generic_ui_metadata_setters:
                    self._generic_ui_metadata_setters[name](self, None)
            except (ValueError, TypeError):
                logger.error(
                    'Error trying to set field {} to default in object {}.',
                    name,
                    self,
                    owner='nabaker')
        self._ui_metadata_cache = ui_metadata

    @property
    def swapping_to_parent(self):
        return self._swapping_to_parent

    @property
    def swapping_from_parent(self):
        return self._swapping_from_parent

    @contextmanager
    def _swapping_parents(self, old_parent, new_parent):
        self._swapping_from_parent = old_parent
        self._swapping_to_parent = new_parent
        try:
            yield None
        finally:
            self._swapping_from_parent = None
            self._swapping_to_parent = None

    @property
    def location(self):
        return self._location

    @distributor.fields.Field(op=distributor.ops.SetLocation)
    def _location_field_internal(self):
        return self

    resend_location = _location_field_internal.get_resend()

    @location.setter
    def location(self, new_location):
        self.set_location_without_distribution(new_location)
        self.resend_location()

    def set_location_without_distribution(self, new_location):
        if not isinstance(new_location, sims4.math.Location):
            raise TypeError()
        if not (new_location == self._location and
                (self.parts is not None and new_location.parent is not None)
                and new_location.parent.parts is not None):
            return
        old_location = self._location
        events = [(self, old_location)]
        for child in self.get_all_children_recursive_gen():
            events.append((child, child._location))
        if new_location.parent != old_location.parent:
            self.pre_parent_change(new_location.parent)
            with self._swapping_parents(old_location.parent,
                                        new_location.parent):
                if old_location.parent is not None:
                    old_location.parent._remove_child(
                        self, new_parent=new_location.parent)
                if new_location.parent is not None:
                    new_location.parent._add_child(self, new_location)
            visibility_state = self.visibility or VisibilityState()
            if new_location.parent is not None and new_location.parent._disable_child_footprint_and_shadow:
                visibility_state.enable_drop_shadow = False
            else:
                visibility_state.enable_drop_shadow = True
            self.visibility = visibility_state
        if new_location.parent is not None:
            current_inventory = self.get_inventory()
            if current_inventory is not None and not current_inventory.try_remove_object_by_id(
                    self.id):
                raise RuntimeError(
                    'Unable to remove object: {} from the inventory: {}, parenting request will be ignored.'
                    .format(self, current_inventory))
        posture_graph_service = services.current_zone().posture_graph_service
        with posture_graph_service.object_moving(self):
            self._location = new_location
            if self.parts:
                for part in self.parts:
                    part.on_owner_location_changed()
        if new_location.parent != old_location.parent:
            self.on_parent_change(new_location.parent)
        for (obj, old_value) in events:
            if obj is not self:
                new_location = obj.location.clone()
                obj._location = new_location
            obj.on_location_changed(old_value)

    def set_location(self, location):
        self.location = location

    def move_to(self, **overrides):
        self.location = self._location.clone(**overrides)

    @distributor.fields.Field(op=distributor.ops.SetAudioEffects)
    def audio_effects(self):
        return self._audio_effects

    resend_audio_effects = audio_effects.get_resend()

    def append_audio_effect(self, key, audio_effect_data):
        if self._audio_effects is None:
            self._audio_effects = {}
        self._audio_effects[key] = audio_effect_data
        self.resend_audio_effects()

    def remove_audio_effect(self, key):
        if self._audio_effects is None:
            logger.error(
                'Found audio effects is None while trying to remove audio effect with key {} on {}',
                key,
                self,
                owner='jdimailig')
            return
        if key in self._audio_effects:
            del self._audio_effects[key]
        if not self._audio_effects:
            self._audio_effects = None
        self.resend_audio_effects()

    @forward_to_components
    def on_location_changed(self, old_location):
        pass

    @property
    def transform(self):
        return self._location.world_transform

    @transform.setter
    def transform(self, transform):
        if self.parent is not None:
            self.move_to(transform=transform,
                         parent=None,
                         routing_surface=self.parent.routing_surface)
            return
        self.move_to(transform=transform)

    @property
    def position(self):
        return self.transform.translation

    @property
    def position_with_forward_offset(self):
        return self.position + self.forward * ClientObjectMixin.FORWARD_OFFSET

    @property
    def intended_position_with_forward_offset(self):
        return self.intended_position + self.intended_forward * ClientObjectMixin.FORWARD_OFFSET

    @property
    def orientation(self):
        return self.transform.orientation

    @property
    def forward(self):
        return self.orientation.transform_vector(
            self.forward_direction_for_picking)

    @property
    def routing_surface(self):
        return self._location.world_routing_surface

    @property
    def level(self):
        routing_surface = self.routing_surface
        if routing_surface is None:
            return
        return routing_surface.secondary_id

    @property
    def routing_location(self):
        return self.get_routing_location_for_transform(self.transform)

    def get_routing_location_for_transform(self,
                                           transform,
                                           routing_surface=DEFAULT):
        routing_surface = self.routing_surface if routing_surface is DEFAULT else routing_surface
        return routing.Location(transform.translation, transform.orientation,
                                routing_surface)

    @property
    def intended_transform(self):
        return self.transform

    @property
    def intended_position(self):
        return self.intended_transform.translation

    @property
    def intended_forward(self):
        return self.intended_transform.orientation.transform_vector(
            self.forward_direction_for_picking)

    @property
    def intended_routing_surface(self):
        return self.routing_surface

    @property
    def parent(self):
        parent = self._location.parent
        parent = self.attempt_to_remap_parent(parent)
        if parent is not None:
            children = parent.children
            if not (self not in children and self.is_part
                    and self.part_owner in children):
                return
        return parent

    @property
    def bb_parent(self):
        return self._location.parent

    def attempt_to_remap_parent(self, parent):
        if self.is_part and parent is not None and parent.parts is not None:
            distance = None
            found_part = None
            for part in parent.parts:
                dot = sims4.math.vector_dot(self.forward, part.forward)
                if dot < -0.98:
                    new_distance = (part.position -
                                    self.position).magnitude_squared()
                    if not distance is None:
                        if new_distance <= distance:
                            if new_distance == distance and found_part.subroot_index is not None:
                                continue
                            distance = new_distance
                            found_part = part
                    if new_distance == distance and found_part.subroot_index is not None:
                        continue
                    distance = new_distance
                    found_part = part
            return found_part
        return parent

    def parent_object(self, child_type=ChildrenType.DEFAULT):
        if child_type is ChildrenType.BB_ONLY:
            parent = self.bb_parent
        else:
            parent = self.parent
        if parent is not None:
            if parent.is_part:
                parent = parent.part_owner
        return parent

    @property
    def parent_slot(self):
        parent = self._location.parent
        if parent is None:
            return
        bone_name_hash = self._location.joint_name_or_hash or self._location.slot_hash
        result = None
        for runtime_slot in parent.get_runtime_slots_gen(
                bone_name_hash=bone_name_hash):
            assert not result is not None
            result = runtime_slot
        if result is None:
            result = RuntimeSlot(parent, bone_name_hash, frozenset())
        return result

    def get_parenting_root(self):
        result = self
        next_parent = result.parent
        while next_parent is not None:
            result = next_parent
            next_parent = result.parent
        return result

    @property
    def children(self):
        if self._children_objects is not None:
            return self._children_objects[ChildrenType.DEFAULT]
        return ()

    def children_recursive_gen(self, include_self=False):
        if include_self:
            yield self
        for child in self.children:
            yield child
            for grandchild in child.children_recursive_gen():
                yield grandchild

    @assertions.hot_path
    def _children_recursive_fast_gen(self):
        yield self
        for child in self.children:
            yield from child._children_recursive_fast_gen()

    def get_all_children_gen(self):
        if self._children_objects is not None:
            for children in self._children_objects.values():
                yield from children

    def get_all_children_recursive_gen(self):
        for child in self.get_all_children_gen():
            yield child
            yield from child.get_all_children_recursive_gen()

    def clear_default_children(self):
        if self._children_objects is not None:
            self._children_objects[ChildrenType.DEFAULT].clear()

    @assertions.hot_path
    def parenting_hierarchy_gen(self):
        self_parent = self.parent
        if self_parent is not None:
            master_parent = self_parent
            master_parent_parent = master_parent.parent
            while master_parent_parent is not None:
                master_parent = master_parent_parent
                master_parent_parent = master_parent.parent
            yield from master_parent._children_recursive_fast_gen()
        else:
            yield from self._children_recursive_fast_gen()

    def on_reset_send_op(self, reset_reason):
        super().on_reset_send_op(reset_reason)
        if self.valid_for_distribution:
            if reset_reason != ResetReason.BEING_DESTROYED or self.vehicle_component is not None:
                try:
                    reset_op = distributor.ops.ResetObject(self.id)
                    dist = Distributor.instance()
                    dist.add_op(self, reset_op)
                except:
                    logger.exception(
                        'Exception thrown sending reset op for {}', self)

    def on_reset_internal_state(self, reset_reason):
        if self.valid_for_distribution and reset_reason != ResetReason.BEING_DESTROYED:
            self.geometry_state = None
            self.material_state = None
            self.resend_location()
        self._reset_reference_arb()
        super().on_reset_internal_state(reset_reason)

    def on_reset_get_interdependent_reset_records(self, reset_reason,
                                                  reset_records):
        super().on_reset_get_interdependent_reset_records(
            reset_reason, reset_records)
        for child in set(self.get_all_children_gen()):
            reset_records.append(
                ResetRecord(child, ResetReason.RESET_EXPECTED, self, 'Child'))

    @property
    def slot_hash(self):
        return self._location.slot_hash

    @slot_hash.setter
    def slot_hash(self, value):
        if self._location.slot_hash != value:
            self.location = self._location.clone(slot_hash=value)

    @property
    def bone_name_hash(self):
        return self._location.joint_name_or_hash or self._location.slot_hash

    @property
    def part_suffix(self) -> str:
        pass

    @distributor.fields.Field(op=distributor.ops.SetModel)
    def model_with_material_variant(self):
        return (self._model, self._material_variant)

    resend_model_with_material_variant = model_with_material_variant.get_resend(
    )

    @model_with_material_variant.setter
    def model_with_material_variant(self, value):
        (self._model, self._material_variant) = value

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, value):
        model_res_key = None
        if isinstance(value, sims4.resources.Key):
            model_res_key = value
        elif isinstance(value, Definition):
            model_res_key = value.get_model(index=0)
            self.set_definition(value.id, ignore_rig_footprint=True)
        else:
            if value is not None:
                logger.error(
                    'Trying to set the model of object {} to the invalid value of {}.                                The object will revert to its default model instead.',
                    self,
                    value,
                    owner='tastle')
            model_res_key = self.definition.get_model(self._state_index)
        self.model_with_material_variant = (model_res_key,
                                            self._material_variant)

    @property
    def material_variant(self):
        return self._material_variant

    @material_variant.setter
    def material_variant(self, value):
        if value is None:
            self.model_with_material_variant = (self._model, None)
        else:
            if not isinstance(value, str):
                raise TypeError('Model variant value must be a string')
            if not value:
                self.model_with_material_variant = (self._model, None)
            else:
                try:
                    variant_value = int(value)
                except ValueError:
                    variant_value = sims4.hash_util.hash32(value)
                self.model_with_material_variant = (self._model, variant_value)

    @distributor.fields.Field(op=distributor.ops.SetStandInModel)
    def standin_model(self):
        return self._standin_model

    @standin_model.setter
    def standin_model(self, value):
        self._standin_model = value

    @distributor.fields.Field(op=distributor.ops.SetObjectDefStateIndex,
                              default=0)
    def state_index(self):
        return self._state_index

    resend_state_index = state_index.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetRig,
                              priority=distributor.fields.Field.Priority.HIGH)
    def rig(self):
        return self._rig

    @rig.setter
    def rig(self, value):
        if not isinstance(value, sims4.resources.Key):
            raise TypeError
        self._rig = value

    @distributor.fields.Field(op=distributor.ops.SetSlot)
    def slot(self):
        return self._slot

    resend_slot = slot.get_resend()

    @property
    def slots_resource(self):
        return self._slots_resource

    @distributor.fields.Field(op=distributor.ops.SetScale, default=1)
    def _client_scale(self):
        scale_value = self.scale
        for modifier in self.scale_modifiers_gen():
            scale_value *= modifier
        return scale_value

    _resend_client_scale = _client_scale.get_resend()

    @property
    def scale(self):
        return self._scale

    @forward_to_components_gen
    def scale_modifiers_gen(self):
        pass

    @scale.setter
    def scale(self, value):
        if self._scale != value:
            self._scale = value
            self.on_location_changed(self._location)
        self._resend_client_scale()

    @property
    def parent_type(self):
        return self._parent_type

    @parent_type.setter
    def parent_type(self, value):
        self._parent_type = value
        self._resend_parent_type_info()

    @distributor.fields.Field(op=distributor.ops.SetParentType, default=None)
    def parent_type_info(self):
        return (self._parent_type, self._parent_location)

    @parent_type_info.setter
    def parent_type_info(self, value):
        (self._parent_type, self._parent_location) = value

    _resend_parent_type_info = parent_type_info.get_resend()

    @property
    def build_buy_lockout(self):
        return self._build_buy_lockout

    @distributor.fields.Field(op=distributor.ops.SetTint, default=None)
    def tint(self):
        if self.build_buy_lockout and lockout_visualization:
            return sims4.color.ColorARGB32(23782)
        return self._tint

    @tint.setter
    def tint(self, tint_color):
        value = getattr(tint_color, 'value', tint_color)
        if value and not isinstance(value, sims4.color.ColorARGB32):
            raise TypeError('Tint value must be a Color')
        if value == sims4.color.Color.WHITE:
            self._tint = None
        else:
            self._tint = value

    resend_tint = tint.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetMulticolor, default=None)
    def multicolor(self):
        return self._multicolor

    @multicolor.setter
    def multicolor(self, value):
        self._multicolor = value

    resend_multicolor = multicolor.get_resend()

    @distributor.fields.Field(op=distributor.ops.SetDisplayNumber,
                              default=None)
    def display_number(self):
        return self._display_number

    @display_number.setter
    def display_number(self, value):
        self._display_number = value

    resend_display_number = display_number.get_resend()

    def update_display_number(self, display_number=None):
        if display_number is not None:
            self.display_number = display_number
            return
        if hasattr(self, 'get_display_number'):
            self.display_number = self.get_display_number()

    @distributor.fields.Field(op=distributor.ops.SetOpacity, default=None)
    def opacity(self):
        return self._opacity

    @opacity.setter
    def opacity(self, value):
        if self.allow_opacity_change:
            self._opacity = self._clamp_opacity(value)

    def _clamp_opacity(self, value):
        if value is None:
            return
        try:
            value = float(value)
        except:
            raise TypeError('Opacity value must be a float')
        return sims4.math.clamp(0.0, value, 1.0)

    @distributor.fields.Field(op=SetAwarenessSourceOp)
    def awareness_scores(self):
        return self._awareness_scores

    resend_awareness_scores = awareness_scores.get_resend()

    def add_awareness_scores(self, awareness_sources):
        if self._awareness_scores is None:
            self._awareness_scores = Counter()
        self._awareness_scores.update(awareness_sources)
        self.resend_awareness_scores()

    def remove_awareness_scores(self, awareness_sources):
        if self._awareness_scores is None:
            return
        self._awareness_scores.subtract(awareness_sources)
        for awareness_channel in tuple(self.awareness_scores):
            if not self._awareness_scores[awareness_channel]:
                del self._awareness_scores[awareness_channel]
        if not self._awareness_scores:
            self._awareness_scores = None
        self.resend_awareness_scores()

    def add_geometry_state_override(self, original_geometry_state,
                                    override_geometry_state):
        if self._geometry_state_overrides is None:
            self._geometry_state_overrides = {}
        original_state_hash = sims4.hash_util.hash32(original_geometry_state)
        override_state_hash = sims4.hash_util.hash32(override_geometry_state)
        logger.assert_raise(
            original_state_hash not in self._geometry_state_overrides,
            'add_geometry_state_override does not support multiple overrides per state'
        )
        self._geometry_state_overrides[
            original_state_hash] = override_state_hash
        self.geometry_state = self.geometry_state

    def remove_geometry_state_override(self, original_geometry_state):
        state_hash = sims4.hash_util.hash32(original_geometry_state)
        if state_hash in self._geometry_state_overrides:
            del self._geometry_state_overrides[state_hash]
        if not self._geometry_state_overrides:
            self._geometry_state_overrides = None

    @distributor.fields.Field(op=distributor.ops.SetGeometryState,
                              default=None)
    def geometry_state(self):
        return self._geometry_state

    @geometry_state.setter
    def geometry_state(self, value):
        self._geometry_state = self._get_geometry_state_for_value(value)

    def _get_geometry_state_for_value(self, value):
        if not value:
            return
        if isinstance(value, str):
            state_hash = sims4.hash_util.hash32(value)
        elif isinstance(value, int):
            state_hash = value
        if self._geometry_state_overrides is not None:
            if state_hash in self._geometry_state_overrides:
                state_hash = self._geometry_state_overrides[state_hash]
        return state_hash

    @distributor.fields.Field(op=distributor.ops.SetCensorState, default=None)
    def censor_state(self):
        return self._censor_state

    @censor_state.setter
    def censor_state(self, value):
        try:
            value = CensorState(value)
        except:
            raise TypeError('Censor State value must be an int')
        self._censor_state = value

    @distributor.fields.Field(op=distributor.ops.SetVisibility, default=None)
    def visibility(self):
        return self._visibility

    @visibility.setter
    def visibility(self, value):
        if not isinstance(value, VisibilityState):
            raise TypeError(
                'Visibility must be set to value of type VisibilityState')
        self._visibility = value
        if value is not None:
            if value.visibility is True:
                if value.inherits is False:
                    if value.enable_drop_shadow is False:
                        self._visibility = None

    @distributor.fields.Field(op=distributor.ops.SetVisibilityFlags)
    def visibility_flags(self):
        return self._visibility_flags

    @visibility_flags.setter
    def visibility_flags(self, value):
        self._visibility_flags = value

    @distributor.fields.Field(op=distributor.ops.SetMaterialState,
                              default=None)
    def material_state(self):
        return self._material_state

    @material_state.setter
    def material_state(self, value):
        if value is None:
            self._material_state = None
        else:
            if not isinstance(value, MaterialState):
                raise TypeError(
                    'Material State must be set to value of type MaterialState'
                )
            if value.state_name_hash == 0:
                self._material_state = None
            else:
                self._material_state = value

    @property
    def material_hash(self):
        if self.material_state is None:
            return 0
        else:
            return self.material_state.state_name_hash

    @distributor.fields.Field(op=distributor.ops.StartArb, default=None)
    def reference_arb(self):
        return self._reference_arb

    def update_reference_arb(self, arb):
        if self._reference_arb is None:
            self._reference_arb = animation.arb.Arb()
        native.animation.update_post_condition_arb(self._reference_arb, arb)

    def _reset_reference_arb(self):
        if self._reference_arb is not None:
            reset_arb_element = ArbElement(animation.arb.Arb())
            reset_arb_element.add_object_to_reset(self)
            reset_arb_element.distribute()
            reset_arb_element.cleanup()
        self._reference_arb = None

    _NO_SLOTS = EMPTY_SET

    @property
    def deco_slot_size(self):
        return get_object_decosize(self.definition.id)

    @property
    def deco_slot_types(self):
        return DecorativeSlotTuning.get_slot_types_for_object(
            self.deco_slot_size)

    @property
    def slot_type_set(self):
        key = get_object_slotset(self.definition.id)
        return get_slot_type_set_from_key(key)

    @property
    def slot_types(self):
        slot_type_set = self.slot_type_set
        if slot_type_set is not None:
            return slot_type_set.slot_types
        return self._NO_SLOTS

    @property
    def ideal_slot_types(self):
        carryable = self.get_component(CARRYABLE_COMPONENT)
        if carryable is not None:
            slot_type_set = carryable.ideal_slot_type_set
            if slot_type_set is not None:
                return slot_type_set.slot_types & (self.slot_types
                                                   | self.deco_slot_types)
        return self._NO_SLOTS

    @property
    def all_valid_slot_types(self):
        return self.deco_slot_types | self.slot_types

    def _add_child(self, child, location):
        if self._children_objects is None:
            self._children_objects = defaultdict(WeakSet)
        if not isinstance(self.children, (WeakSet, set)):
            raise TypeError(
                "self.children is not a WeakSet or a set, it's {}".format(
                    self.children))
        bone_name_hash = location.joint_name_or_hash or location.slot_hash
        found_runtime_slot = None
        for runtime_slot in location.parent.get_runtime_slots_gen(
                bone_name_hash=bone_name_hash):
            assert not found_runtime_slot is not None
            found_runtime_slot = runtime_slot
        if found_runtime_slot is not None:
            for slot_type in found_runtime_slot.slot_types:
                if not slot_type.bb_only:
                    self._children_objects[ChildrenType.DEFAULT].add(child)
                    break
            else:
                self._children_objects[ChildrenType.BB_ONLY].add(child)
        else:
            self._children_objects[ChildrenType.DEFAULT].add(child)
        if self.parts:
            for part in self.parts:
                part.on_children_changed()
        self.on_child_added(child, location)

    def _remove_child(self, child, new_parent=None):
        if not isinstance(self.children, (WeakSet, set)):
            raise TypeError(
                "self.children is not a WeakSet or a set, it's {}".format(
                    self.children))
        for (_, weak_obj_set) in self._children_objects.items():
            if child in weak_obj_set:
                weak_obj_set.discard(child)
                break
        if self.parts:
            for part in self.parts:
                part.on_children_changed()
        self.on_child_removed(child, new_parent=new_parent)

    @forward_to_components
    def on_remove_from_client(self):
        super().on_remove_from_client()
        for primitive in tuple(self.primitives):
            primitive.detach(self)

    def post_remove(self):
        super().post_remove()
        for primitive in tuple(self.primitives):
            primitive.detach(self)
        self.primitives = None

    @forward_to_components
    def on_child_added(self, child, location):
        if self._on_children_changed is None:
            return
        self._on_children_changed(child, location=location)

    @forward_to_components
    def on_child_removed(self, child, new_parent=None):
        if self._on_children_changed is None:
            return
        self._on_children_changed(child, new_parent=new_parent)

    @forward_to_components
    def pre_parent_change(self, parent):
        pass

    @forward_to_components
    def on_parent_change(self, parent):
        caches.clear_all_caches()
        if parent is None:
            self.parent_type = ObjectParentType.PARENT_NONE
        else:
            self.parent_type = ObjectParentType.PARENT_OBJECT

    def create_parent_location(self,
                               parent,
                               transform=sims4.math.Transform.IDENTITY(),
                               joint_name_or_hash=None,
                               slot_hash=0,
                               routing_surface=None):
        if parent is not None:
            if self in parent.ancestry_gen():
                raise ValueError(
                    'Invalid parent value (parent chain is circular)')
            if joint_name_or_hash:
                native.animation.get_joint_transform_from_rig(
                    parent.rig, joint_name_or_hash)
            if slot_hash and not parent.has_slot(slot_hash):
                raise KeyError(
                    'Could not slot {}/{} in slot {} on {}/{}'.format(
                        self, self.definition, hex(slot_hash), parent,
                        parent.definition))
        part_joint_name = joint_name_or_hash or slot_hash
        if parent is not None:
            if part_joint_name is not None:
                if not parent.is_part:
                    if parent.parts:
                        for part in parent.parts:
                            if part.has_slot(part_joint_name):
                                parent = part
                                break
        new_location = self._location.clone(
            transform=transform,
            joint_name_or_hash=joint_name_or_hash,
            slot_hash=slot_hash,
            parent=parent,
            routing_surface=routing_surface)
        return new_location

    def set_parent(self, *args, **kwargs):
        new_location = self.create_parent_location(*args, **kwargs)
        self.location = new_location

    def clear_parent(self, transform, routing_surface):
        return self.set_parent(None,
                               transform=transform,
                               routing_surface=routing_surface)

    def remove_reference_from_parent(self):
        parent = self.bb_parent
        if parent is not None:
            parent._remove_child(self, new_parent=UNSET)

    @distributor.fields.Field(op=distributor.ops.VideoSetPlaylistOp,
                              default=None)
    def video_playlist(self):
        return self._video_playlist

    @video_playlist.setter
    def video_playlist(self, playlist):
        self._video_playlist = playlist

    _resend_video_playlist = video_playlist.get_resend()

    def fade_opacity(self,
                     opacity: float,
                     duration: float,
                     immediate=False,
                     additional_channels=None):
        if self.allow_opacity_change:
            opacity = self._clamp_opacity(opacity)
            if opacity != self._opacity:
                self._opacity = opacity
                fade_op = distributor.ops.FadeOpacity(opacity,
                                                      duration,
                                                      immediate=immediate)
                if additional_channels:
                    for channel in additional_channels:
                        fade_op.add_additional_channel(*channel)
                distributor.ops.record(self, fade_op)

    def fade_in(self,
                fade_duration=None,
                immediate=False,
                additional_channels=None):
        if self.allow_opacity_change:
            if fade_duration is None:
                fade_duration = ClientObjectMixin.FADE_DURATION
            if self.visibility is not None:
                if not self.visibility.visibility:
                    self.visibility = VisibilityState()
                    self.opacity = 0
            self.fade_opacity(1,
                              fade_duration,
                              immediate=immediate,
                              additional_channels=additional_channels)

    def fade_out(self,
                 fade_duration=None,
                 immediate=False,
                 additional_channels=None):
        if self.allow_opacity_change:
            if fade_duration is None:
                fade_duration = ClientObjectMixin.FADE_DURATION
            self.fade_opacity(0,
                              fade_duration,
                              immediate=immediate,
                              additional_channels=additional_channels)

    @distributor.fields.Field(op=distributor.ops.SetValue, default=None)
    def current_value(self):
        new_value = self._base_value
        statistic_component = self.statistic_component
        if statistic_component is not None:
            new_value += statistic_component.get_added_monetary_value()
        state_component = self.state_component
        if state_component is not None:
            return max(
                round(new_value * state_component.state_based_value_mod), 0)
        return max(new_value, 0)

    @current_value.setter
    def current_value(self, value):
        state_component = self.state_component
        if state_component is not None:
            self.base_value = value / state_component.state_based_value_mod
        else:
            self.base_value = value

    _resend_current_value = current_value.get_resend()

    def update_current_value(self, update_tooltip=True):
        self._resend_current_value()
        if update_tooltip:
            self.update_tooltip_field(TooltipFieldsComplete.simoleon_value,
                                      self.current_value)

    @property
    def base_value(self):
        return self._base_value

    @base_value.setter
    def base_value(self, value):
        self._base_value = round(max(value, 0))
        update_tooltip = self.get_tooltip_field(
            TooltipFieldsComplete.simoleon_value) is not None
        self.update_current_value(update_tooltip=update_tooltip)

    @property
    def depreciated_value(self):
        if not self.definition.get_can_depreciate():
            return self.catalog_value
        return self.catalog_value * (1 - self.INITIAL_DEPRECIATION)

    @property
    def catalog_value(self):
        return self.get_object_property(GameObjectProperty.CATALOG_PRICE)

    @property
    def depreciated(self):
        return not self._needs_depreciation

    def set_post_bb_fixup_needed(self):
        self._needs_post_bb_fixup = True
        self._needs_depreciation = True

    def try_post_bb_fixup(self, force_fixup=False, active_household_id=0):
        if force_fixup or self._needs_depreciation:
            if force_fixup:
                self._needs_depreciation = True
            self._on_try_depreciation(active_household_id=active_household_id)
        if force_fixup or self._needs_post_bb_fixup:
            self._needs_post_bb_fixup = False
            self.on_post_bb_fixup()

    @forward_to_components
    def on_post_bb_fixup(self):
        services.get_event_manager().process_events_for_household(
            test_events.TestEvent.ObjectAdd,
            services.household_manager().get(self._household_owner_id),
            obj=self)

    def _on_try_depreciation(self, active_household_id=0):
        if self._household_owner_id != active_household_id:
            return
        self._needs_depreciation = False
        if not self.definition.get_can_depreciate():
            return
        self.base_value = floor(self._base_value *
                                (1 - self.INITIAL_DEPRECIATION))

    def register_for_on_children_changed_callback(self, callback):
        if self._on_children_changed is None:
            self._on_children_changed = CallableList()
        if callback not in self._on_children_changed:
            self._on_children_changed.append(callback)

    def unregister_for_on_children_changed_callback(self, callback):
        if self._on_children_changed is not None:
            if callback in self._on_children_changed:
                self._on_children_changed.remove(callback)
            if not self._on_children_changed:
                self._on_children_changed = None

    @distributor.fields.Field(op=distributor.ops.SetScratched, default=False)
    def scratched(self):
        return self._scratched

    @scratched.setter
    def scratched(self, scratched):
        self._scratched = scratched

    @distributor.fields.Field(op=distributor.ops.SetWindSpeedEffect,
                              default=None)
    def wind_speed_level(self):
        return self._wind_speed_effect

    @wind_speed_level.setter
    def wind_speed_level(self, value):
        self._wind_speed_effect = value.wind_speed
class RelationshipTracker:
    __qualname__ = 'RelationshipTracker'

    def __init__(self, sim_info):
        super().__init__()
        self._sim_info = sim_info
        self._relationships = {}
        self._suppress_client_updates = False
        self.spouse_sim_id = None
        self._relationship_multipliers = {}
        self._create_relationship_callbacks = CallableList()

    def __iter__(self):
        return self._relationships.values().__iter__()

    def __len__(self):
        return len(self._relationships)

    @property
    def suppress_client_updates(self):
        return self._suppress_client_updates

    @contextmanager
    def suppress_client_updates_context_manager(self):
        self._suppress_client_updates = True
        try:
            yield None
        finally:
            self._suppress_client_updates = False

    def create_relationship(self, target_sim_id):
        return self._find_relationship(target_sim_id, True)

    def destroy_relationship(self, target_sim_id, notify_client=True):
        if target_sim_id in self._relationships:
            relationship = self._relationships[target_sim_id]
            relationship.destroy(notify_client=notify_client)
            del self._relationships[target_sim_id]

    def _clear_relationships(self):
        for sim_id in tuple(self._relationships.keys()):
            self.destroy_relationship(sim_id)

    def destroy_all_relationships(self):
        sim_id = self._sim_info.id
        keys = tuple(self._relationships.keys())
        for target_sim_id in keys:
            relationship = self._relationships[target_sim_id]
            target_sim_info = relationship.find_target_sim_info()
            if target_sim_info is not None:
                target_sim_info.relationship_tracker.destroy_relationship(
                    sim_id)
            self.destroy_relationship(target_sim_id)

    def save(self):
        save_list = [
            relationship.get_persistance_protocol_buffer()
            for relationship in self._relationships.values()
        ]
        return save_list

    def load(self, relationship_save_data):
        with self.suppress_client_updates_context_manager():
            self._clear_relationships()
            while relationship_save_data:
                for rel_save in relationship_save_data:
                    relationship = self.create_relationship(rel_save.target_id)
                    relationship.load(self._sim_info, rel_save)

    def send_relationship_info(self, target_sim_id=None):
        if target_sim_id is None:
            for relationship in self._relationships.values():
                relationship.send_relationship_info()
        else:
            relationship = self._find_relationship(target_sim_id)
            if relationship is not None:
                relationship.send_relationship_info()

    def clean_and_send_remaining_relationship_info(self):
        sim_info_manager = services.sim_info_manager()
        for (target_sim_info_id,
             relationship) in tuple(self._relationships.items()):
            if target_sim_info_id in sim_info_manager:
                relationship.send_relationship_info()
            else:
                self.destroy_relationship(target_sim_info_id,
                                          notify_client=False)

    def add_create_relationship_listener(self, callback):
        if callback not in self._create_relationship_callbacks:
            self._create_relationship_callbacks.append(callback)

    def remove_create_relationship_listener(self, callback):
        self._create_relationship_callbacks.remove(callback)

    @caches.cached
    def get_relationship_score(self, target_sim_id, track=DEFAULT):
        if track is DEFAULT:
            track = RelationshipGlobalTuning.REL_INSPECTOR_TRACK
        relationship = self._find_relationship(target_sim_id)
        if relationship:
            return relationship.get_track_score(track)
        return Relationship.DEFAULT_RELATIONSHIP_VALUE

    def add_relationship_score(self,
                               target_sim_id,
                               increment,
                               track=DEFAULT,
                               threshold=None):
        if track is DEFAULT:
            track = RelationshipGlobalTuning.REL_INSPECTOR_TRACK
        if target_sim_id == self._sim_info.sim_id:
            return
        relationship = self._find_relationship(target_sim_id, True)
        if relationship:
            if threshold is None or threshold.compare(
                    relationship.get_track_score(track)):
                relationship.add_track_score(increment, track)
                logger.debug(
                    'Adding to score to track {} for {}: += {}; new score = {}',
                    track, relationship, increment,
                    relationship.get_track_score(track))
            else:
                logger.debug(
                    'Attempting to add to track {} for {} but {} not within threshold {}',
                    track, relationship, relationship.get_track_score(track),
                    threshold)
        else:
            logger.error(
                'Attempting to add to the relationship score with myself: Sim = {}',
                self._sim_info)

    def set_relationship_score(self,
                               target_sim_id,
                               value,
                               track=DEFAULT,
                               threshold=None):
        if track is DEFAULT:
            track = RelationshipGlobalTuning.REL_INSPECTOR_TRACK
        if target_sim_id == self._sim_info.sim_id:
            return
        relationship = self._find_relationship(target_sim_id, True)
        if relationship:
            if threshold is None or threshold.compare(
                    relationship.get_track_score(track)):
                relationship.set_track_score(value, track)
                logger.debug(
                    'Setting score on track {} for {}: = {}; new score = {}',
                    track, relationship, value,
                    relationship.get_track_score(track))
            else:
                logger.debug(
                    'Attempting to set score on track {} for {} but {} not within threshold {}',
                    track, relationship, relationship.get_track_score(track),
                    threshold)
        else:
            logger.error(
                'Attempting to set the relationship score with myself: Sim = {}',
                self._sim_info)

    def enable_selectable_sim_track_decay(self, to_enable=True):
        logger.debug('Enabling ({}) decay for selectable sim: {}'.format(
            to_enable, self._sim_info))
        for relationship in self._relationships.values():
            relationship.enable_selectable_sim_track_decay(to_enable)

    def get_relationship_track_utility_score(self,
                                             target_sim_id,
                                             track=DEFAULT):
        if track is DEFAULT:
            track = RelationshipGlobalTuning.REL_INSPECTOR_TRACK
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            return relationship.get_track_utility_score(track)
        return track.autonomous_desire

    def get_relationship_prevailing_short_term_context_track(
            self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            return relationship.get_prevailing_short_term_context_track()

    def get_default_short_term_context_bit(self):
        track = Relationship.DEFAULT_SHORT_TERM_CONTEXT_TRACK
        return track.get_bit_at_relationship_value(track.initial_value)

    def get_relationship_track_tracker(self, target_sim_id, add=False):
        relationship = self._find_relationship(target_sim_id, add)
        if relationship:
            return relationship.bit_track_tracker
        return

    def get_relationship_track(self, target_sim_id, track=DEFAULT, add=False):
        with self.suppress_client_updates_context_manager():
            if track is DEFAULT:
                track = RelationshipGlobalTuning.REL_INSPECTOR_TRACK
            relationship = self._find_relationship(target_sim_id, add)
            if relationship:
                return relationship.get_track(track, add)
            return

    def relationship_tracks_gen(self, target_sim_id):
        with self.suppress_client_updates_context_manager():
            relationship = self._find_relationship(target_sim_id)
            while relationship:
                for track in relationship.bit_track_tracker:
                    yield track

    def add_relationship_multipliers(self, handle, relationship_multipliers):
        if relationship_multipliers:
            for relationship in self:
                self._apply_relationship_multiplier_to_relationship(
                    relationship, relationship_multipliers)
            self._relationship_multipliers[handle] = relationship_multipliers

    def _apply_relationship_multiplier_to_relationship(
            self, relationship, relationship_multipliers):
        for (track_type, multiplier) in relationship_multipliers.items():
            relationship_track = relationship.get_track(
                track_type, add=track_type.add_if_not_in_tracker)
            while relationship_track is not None:
                relationship_track.add_statistic_multiplier(multiplier)

    def remove_relationship_multipliers(self, handle):
        relationship_multipliers = self._relationship_multipliers.pop(
            handle, None)
        if relationship_multipliers is None:
            return
        for relationship in self:
            for (track_type, multiplier) in relationship_multipliers.items():
                relationship_track = relationship.get_track(track_type,
                                                            add=False)
                while relationship_track is not None:
                    relationship_track.remove_statistic_multiplier(multiplier)

    def on_added_to_social_group(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            relationship.apply_social_group_decay()

    def on_removed_from_social_group(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            relationship.remove_social_group_decay()

    def set_default_tracks(self,
                           target_sim,
                           update_romance=True,
                           family_member=False):
        if target_sim is None or target_sim.sim_id == self._sim_info.sim_id:
            return
        with self.suppress_client_updates_context_manager():
            target_sim_id = target_sim.sim_id
            key = DefaultGenealogyLink.Roommate
            if family_member:
                key = DefaultGenealogyLink.FamilyMember
            default_relationship = DefaultRelationshipInHousehold.RelationshipSetupMap.get(
                key)
            default_relationship.apply(self, target_sim)
            if update_romance and self.spouse_sim_id == target_sim_id:
                key = DefaultGenealogyLink.Spouse
                default_relationship = DefaultRelationshipInHousehold.RelationshipSetupMap.get(
                    key)
                default_relationship.apply(self, target_sim)
                for (gender, gender_preference_statistic
                     ) in self._sim_info.get_gender_preferences_gen():
                    while gender == target_sim.gender:
                        gender_preference_statistic.set_value(
                            gender_preference_statistic.max_value)
            relationship_setup_logger.info(
                'Set default tracks {:25} -> {:25} as {}',
                self._sim_info.full_name, target_sim.full_name, key)

    def add_relationship_bit(self, target_sim_id, bit_to_add, force_add=False):
        if bit_to_add is None:
            logger.error('Attempting to add None bit to relationship for {}',
                         self._sim_info)
            return
        if bit_to_add.trait_replacement_bits is not None:
            trait_tracker = self._sim_info.trait_tracker
            for (trait,
                 replacement_bit) in bit_to_add.trait_replacement_bits.items():
                while trait_tracker.has_trait(trait):
                    self.add_relationship_bit(target_sim_id, replacement_bit,
                                              force_add)
                    return
        relationship = self._find_relationship(target_sim_id, True)
        if not relationship:
            logger.error('Failed to find relationship for {} and {}',
                         self._sim_info,
                         services.sim_info_manager().get(target_sim_id))
            return
        for requirement in bit_to_add.permission_requirements:
            while self._sim_info.sim_permissions.is_permission_enabled(
                    requirement.permission) != requirement.required_enabled:
                return
        if force_add:
            if bit_to_add.is_track_bit and bit_to_add.triggered_track is not None:
                track = bit_to_add.triggered_track
                mean_list = track.bit_data.get_track_mean_list_for_bit(
                    bit_to_add)
                for mean_tuple in mean_list:
                    self.set_relationship_score(target_sim_id, mean_tuple.mean,
                                                mean_tuple.track)
            for required_bit in bit_to_add.required_bits:
                self.add_relationship_bit(target_sim_id,
                                          required_bit,
                                          force_add=True)
        self._send_relationship_prechange_event(target_sim_id)
        if not bit_to_add.is_track_bit:
            relationship.add_bit(bit_to_add)
        self._send_relationship_changed_event(target_sim_id)
        services.social_service.post_relationship_message(self._sim_info,
                                                          bit_to_add,
                                                          target_sim_id,
                                                          added=True)

    def remove_relationship_bit(self, target_sim_id, bit):
        relationship = self._find_relationship(target_sim_id)
        if relationship:
            self._send_relationship_prechange_event(target_sim_id)
            relationship.remove_bit(bit)
            self._send_relationship_changed_event(target_sim_id)
            services.social_service.post_relationship_message(self._sim_info,
                                                              bit,
                                                              target_sim_id,
                                                              added=False)

    def _check_for_living_status(self, sim_id, allow_dead_targets,
                                 allow_living_targets):
        sim_info = services.sim_info_manager().get(sim_id)
        if sim_info is None:
            logger.error(
                '_check_for_living_status() could not find SimInfo for sim_id {} with requesting sim {}',
                sim_id,
                self._sim_info,
                owner='ayarger')
            return False
        is_sim_dead = sim_info.is_dead
        return allow_dead_targets and is_sim_dead or allow_living_targets and not is_sim_dead

    def get_all_bits(self,
                     target_sim_id: int = None,
                     allow_dead_targets=True,
                     allow_living_targets=True):
        bits = []
        if target_sim_id is None:
            for relationship in self._relationships.values():
                if not self._check_for_living_status(
                        relationship.target_sim_id, allow_dead_targets,
                        allow_living_targets):
                    pass
                bits.extend(relationship.get_bits())
        else:
            relationship = self._find_relationship(target_sim_id)
            if relationship:
                if not self._check_for_living_status(
                        relationship.target_sim_id, allow_dead_targets,
                        allow_living_targets):
                    return bits
                bits.extend(relationship.get_bits())
        return bits

    def get_relationship_depth(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship:
            return relationship.depth
        return 0

    def has_bit(self, target_sim_id, bit):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            return relationship.has_bit(bit)
        return False

    def get_highest_priority_track_bit(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            return relationship.get_highest_priority_track_bit()
        return

    def get_highest_priority_bit(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            return relationship.get_highest_priority_bit()
        return

    def update_bits_on_age_up(self, current_age):
        for relationship in self._relationships.values():
            relationship.add_historical_bits_on_age_up(current_age)

    def target_sim_gen(self):
        for target_sim_id in self._relationships.keys():
            yield target_sim_id

    def add_relationship_appropriateness_buffs(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            relationship.add_relationship_appropriateness_buffs()

    def add_neighbor_bit_if_necessary(self):
        for relationship in self._relationships.values():
            relationship.add_neighbor_bit_if_necessary(self._sim_info)

    def get_knowledge(self, target_sim_id, initialize=False):
        relationship = self._find_relationship(target_sim_id,
                                               create=initialize)
        if relationship is not None:
            return relationship.get_knowledge(initialize=initialize)

    def add_known_trait(self,
                        trait,
                        target_sim_id,
                        num_traits=None,
                        notify_client=True):
        relationship = self._find_relationship(target_sim_id, True)
        knowledge = relationship.get_knowledge(initialize=True)
        knowledge.add_known_trait(trait,
                                  num_traits=num_traits,
                                  notify_client=notify_client)

    def print_relationship_info(self, target_sim_id, _connection):
        relationship = self._find_relationship(target_sim_id)
        if relationship is not None:
            sims4.commands.output(
                '{}:\n\tTotal Depth: {}\n\tBits:\n{}\n\tTracks:\n{}'.format(
                    relationship, relationship.depth,
                    relationship.build_printable_string_of_bits(),
                    relationship.build_printable_string_of_tracks()),
                _connection)
        else:
            sims4.commands.output(
                'Relationship not found between {} and {}:\n\tTotal Depth: {}\n\tBits:\n{}\n\tTracks:\n{}'
                .format(self._sim_info, target_sim_id,
                        self.get_relationship_depth(target_sim_id),
                        self._build_printable_string_of_bits(target_sim_id),
                        self._build_printable_string_of_tracks(target_sim_id)),
                _connection)

    def _find_relationship(self, target_sim_id, create=False):
        if self._sim_info.sim_id == target_sim_id:
            return
        if target_sim_id in self._relationships:
            return self._relationships[target_sim_id]
        if create:
            logger.debug('Creating relationship for {0} and {1}',
                         self._sim_info, target_sim_id)
            relationship = Relationship(self, self._sim_info.sim_id,
                                        target_sim_id)
            self._relationships[target_sim_id] = relationship
            relationship.add_neighbor_bit_if_necessary(self._sim_info)
            for multiplier in self._relationship_multipliers.values():
                self._apply_relationship_multiplier_to_relationship(
                    relationship, multiplier)
            self._create_relationship_callbacks(relationship)
            return relationship

    def _build_printable_string_of_bits(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship:
            return relationship.build_printable_string_of_bits()
        return ''

    def _build_printable_string_of_tracks(self, target_sim_id):
        relationship = self._find_relationship(target_sim_id)
        if relationship:
            return relationship.build_printable_string_of_tracks()
        return ''

    def _send_relationship_prechange_event(self, target_sim_id):
        services.get_event_manager().process_event(
            event_testing.test_events.TestEvent.PrerelationshipChanged,
            self._sim_info,
            sim_id=self._sim_info.id,
            target_sim_id=target_sim_id)

    def _send_relationship_changed_event(self, target_sim_id):
        services.get_event_manager().process_event(
            event_testing.test_events.TestEvent.RelationshipChanged,
            sim_info=self._sim_info,
            sim_id=self._sim_info.id,
            target_sim_id=target_sim_id)
class ObjectManager(DistributableObjectManager):
    __qualname__ = 'ObjectManager'
    FIREMETER_DISPOSABLE_OBJECT_CAP = Tunable(int, 5, description='Number of disposable objects a lot can have at any given moment.')
    BED_TAGS = TunableTuple(description='\n        Tags to check on an object to determine what type of bed an object is.\n        ', beds=TunableSet(description='\n            Tags that consider an object as a bed other than double beds.\n            ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), double_beds=TunableSet(description='\n            Tags that consider an object as a double bed\n            ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), kid_beds=TunableSet(description='\n            Tags that consider an object as a kid bed\n            ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)), other_sleeping_spots=TunableSet(description='\n            Tags that considered sleeping spots.\n            ', tunable=TunableEnumWithFilter(tunable_type=tag.Tag, default=tag.Tag.INVALID, filter_prefixes=BED_PREFIX_FILTER)))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._crafting_cache = CraftingObjectCache()
        self._sim_spawn_conditions = collections.defaultdict(set)
        self._client_connect_callbacks = CallableList()
        self._portal_cache = WeakSet()
        self._portal_added_callbacks = CallableList()
        self._portal_removed_callbacks = CallableList()
        self._front_door_candidates_changed_callback = CallableList()
        self._all_bed_tags = self.BED_TAGS.beds | self.BED_TAGS.double_beds | self.BED_TAGS.kid_beds | self.BED_TAGS.other_sleeping_spots

    @property
    def crafting_cache(self):
        return self._crafting_cache

    def portal_cache_gen(self):
        yield self._portal_cache

    def on_client_connect(self, client):
        all_objects = list(self._objects.values())
        for game_object in all_objects:
            game_object.on_client_connect(client)

    def move_to_inventory(self, obj, inventory_manager):
        del self._objects[obj.id]
        obj.manager = inventory_manager
        inventory_manager._objects[obj.id] = obj
        if hasattr(obj, 'on_added_to_inventory'):
            obj.on_added_to_inventory()

    def add(self, obj, *args, **kwargs):
        super().add(obj, *args, **kwargs)
        if obj.objectage_component is None:
            current_zone = services.current_zone()
            current_zone.increment_object_count(obj)
            current_zone.household_manager.increment_household_object_count(obj.get_household_owner_id())

    def remove(self, obj, *args, **kwargs):
        super().remove(obj, *args, **kwargs)
        if obj.objectage_component is None:
            current_zone = services.current_zone()
            current_zone.decrement_object_count(obj)
            current_zone.household_manager.decrement_household_object_count(obj.get_household_owner_id())

    def _should_save_object_on_lot(self, obj):
        parent = obj.parent
        if parent is not None and parent.is_sim:
            if obj.can_go_in_inventory_type(InventoryType.SIM):
                return False
        return True

    def pre_save(self):
        all_objects = list(self._objects.values())
        lot = services.current_zone().lot
        for (_, inventory) in lot.get_all_object_inventories_gen():
            for game_object in inventory:
                all_objects.append(game_object)
        for game_object in all_objects:
            game_object.update_all_commodities()

    def save(self, object_list=None, zone_data=None, open_street_data=None, **kwargs):
        if object_list is None:
            return
        open_street_objects = file_serialization.ObjectList()
        total_beds = 0
        double_bed_exist = False
        kid_bed_exist = False
        alternative_sleeping_spots = 0
        for game_object in self._objects.values():
            while self._should_save_object_on_lot(game_object):
                if game_object.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT:
                    save_result = game_object.save_object(object_list.objects, ItemLocation.ON_LOT, 0)
                else:
                    if game_object.item_location == ItemLocation.ON_LOT or game_object.item_location == ItemLocation.INVALID_LOCATION:
                        item_location = ItemLocation.FROM_OPEN_STREET
                    else:
                        item_location = game_object.item_location
                    save_result = game_object.save_object(open_street_objects.objects, item_location, 0)
                if not save_result:
                    pass
                if zone_data is None:
                    pass
                def_build_buy_tags = game_object.definition.build_buy_tags
                if not def_build_buy_tags & self._all_bed_tags:
                    pass
                if def_build_buy_tags & self.BED_TAGS.double_beds:
                    double_bed_exist = True
                    total_beds += 1
                elif def_build_buy_tags & self.BED_TAGS.kid_beds:
                    total_beds += 1
                    kid_bed_exist = True
                elif def_build_buy_tags & self.BED_TAGS.other_sleeping_spots:
                    alternative_sleeping_spots += 1
                elif def_build_buy_tags & self.BED_TAGS.beds:
                    total_beds += 1
        if open_street_data is not None:
            open_street_data.objects = open_street_objects
        if zone_data is not None:
            bed_info_data = gameplay_serialization.ZoneBedInfoData()
            bed_info_data.num_beds = total_beds
            bed_info_data.double_bed_exist = double_bed_exist
            bed_info_data.kid_bed_exist = kid_bed_exist
            bed_info_data.alternative_sleeping_spots = alternative_sleeping_spots
            zone_data.gameplay_zone_data.bed_info_data = bed_info_data
        lot = services.current_zone().lot
        for (inventory_type, inventory) in lot.get_all_object_inventories_gen():
            for game_object in inventory:
                game_object.save_object(object_list.objects, ItemLocation.OBJECT_INVENTORY, inventory_type)

    def valid_objects(self):
        return [obj for obj in self._objects.values() if not obj._hidden_flags]

    def get_objects_of_type_gen(self, *definitions):
        for obj in self._objects.values():
            while any(obj.definition is d for d in definitions):
                yield obj

    def get_objects_with_tag_gen(self, tag):
        for obj in self._objects.values():
            while build_buy.get_object_has_tag(obj.definition.id, tag):
                yield obj

    def add_sim_spawn_condition(self, sim_id, callback):
        for sim in services.sim_info_manager().instanced_sims_gen():
            while sim.id == sim_id:
                logger.error('Sim {} is already in the world, cannot add the spawn condition', sim)
                return
        self._sim_spawn_conditions[sim_id].add(callback)

    def remove_sim_spawn_condition(self, sim_id, callback):
        if callback not in self._sim_spawn_conditions.get(sim_id, ()):
            logger.error('Trying to remove sim spawn condition with invalid id-callback pair ({}-{}).', sim_id, callback)
            return
        self._sim_spawn_conditions[sim_id].remove(callback)

    def trigger_sim_spawn_condition(self, sim_id):
        if sim_id in self._sim_spawn_conditions:
            for callback in self._sim_spawn_conditions[sim_id]:
                callback()
            del self._sim_spawn_conditions[sim_id]

    def register_portal_added_callback(self, callback):
        if callback not in self._portal_added_callbacks:
            self._portal_added_callbacks.append(callback)

    def unregister_portal_added_callback(self, callback):
        if callback in self._portal_added_callbacks:
            self._portal_added_callbacks.remove(callback)

    def register_portal_removed_callback(self, callback):
        if callback not in self._portal_removed_callbacks:
            self._portal_removed_callbacks.append(callback)

    def unregister_portal_removed_callback(self, callback):
        if callback in self._portal_removed_callbacks:
            self._portal_removed_callbacks.remove(callback)

    def add_portal_to_cache(self, portal):
        self._portal_cache.add(portal)
        self._portal_added_callbacks(portal)

    def remove_portal_from_cache(self, portal):
        self._portal_cache.remove(portal)
        self._portal_removed_callbacks(portal)

    def register_front_door_candidates_changed_callback(self, callback):
        if callback not in self._front_door_candidates_changed_callback:
            self._front_door_candidates_changed_callback.append(callback)

    def unregister_front_door_candidates_changed_callback(self, callback):
        if callback in self._front_door_candidates_changed_callback:
            self._front_door_candidates_changed_callback.remove(callback)

    def on_front_door_candidates_changed(self):
        self._front_door_candidates_changed_callback()

    def advertising_objects_gen(self, motives:set=DEFAULT):
        if not motives:
            return
        if motives is DEFAULT:
            for obj in self.valid_objects():
                while obj.commodity_flags:
                    yield obj
            return
        for obj in self.valid_objects():
            while obj.commodity_flags & motives:
                yield obj

    def get_all_objects_with_component_gen(self, component):
        if component is None:
            return
        for obj in self.valid_objects():
            if obj.has_component(component.instance_attr):
                yield obj
            else:
                while obj.has_component(component.class_attr):
                    yield obj

    def on_location_changed(self, obj):
        self._registered_callbacks[CallbackTypes.ON_OBJECT_LOCATION_CHANGED](obj)

    @classproperty
    def supports_parenting(self):
        return True
class BroadcasterService(Service):
    INTERVAL = TunableRealSecond(description='\n        The time between broadcaster pulses. A lower number will impact\n        performance.\n        ', default=5)
    DEFAULT_QUADTREE_RADIUS = 0.1

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._alarm_handle = None
        self._processing_task = None
        self._on_update_callbacks = CallableList()
        self._pending_broadcasters = []
        self._active_broadcasters = []
        self._cluster_requests = {}
        self._object_cache = None
        self._object_cache_tags = None
        self._pending_update = False
        self._quadtrees = defaultdict(sims4.geometry.QuadTree)

    def create_update_alarm(self):
        self._alarm_handle = add_alarm(self, interval_in_real_seconds(self.INTERVAL), self._on_update, repeating=True, use_sleep_time=False)

    def start(self):
        self.create_update_alarm()
        object_manager = services.object_manager()
        object_manager.register_callback(CallbackTypes.ON_OBJECT_LOCATION_CHANGED, self._update_object_cache)
        object_manager.register_callback(CallbackTypes.ON_OBJECT_ADD, self._update_object_cache)
        services.current_zone().wall_contour_update_callbacks.append(self._on_wall_contours_changed)

    def stop(self):
        if self._alarm_handle is not None:
            cancel_alarm(self._alarm_handle)
            self._alarm_handle = None
        if self._processing_task is not None:
            self._processing_task.stop()
            self._processing_task = None
        object_manager = services.object_manager()
        object_manager.unregister_callback(CallbackTypes.ON_OBJECT_LOCATION_CHANGED, self._update_object_cache)
        object_manager.unregister_callback(CallbackTypes.ON_OBJECT_ADD, self._update_object_cache)
        services.current_zone().wall_contour_update_callbacks.remove(self._on_wall_contours_changed)

    def add_broadcaster(self, broadcaster):
        if broadcaster not in self._pending_broadcasters:
            self._pending_broadcasters.append(broadcaster)
            if broadcaster.immediate:
                self._pending_update = True
            self._on_update_callbacks()

    def remove_broadcaster(self, broadcaster):
        if broadcaster in self._pending_broadcasters:
            self._pending_broadcasters.remove(broadcaster)
        if broadcaster in self._active_broadcasters:
            self._remove_from_cluster_request(broadcaster)
            self._remove_broadcaster_from_quadtree(broadcaster)
            self._active_broadcasters.remove(broadcaster)
        broadcaster.on_removed()
        self._on_update_callbacks()

    def _activate_pending_broadcasters(self):
        for broadcaster in self._pending_broadcasters:
            self._active_broadcasters.append(broadcaster)
            self.update_cluster_request(broadcaster)
            self._update_object_cache()
        self._pending_broadcasters.clear()

    def _add_broadcaster_to_quadtree(self, broadcaster):
        self._remove_broadcaster_from_quadtree(broadcaster)
        broadcaster_quadtree = self._quadtrees[broadcaster.routing_surface.secondary_id]
        broadcaster_bounds = sims4.geometry.QtCircle(sims4.math.Vector2(broadcaster.position.x, broadcaster.position.z), self.DEFAULT_QUADTREE_RADIUS)
        broadcaster_quadtree.insert(broadcaster, broadcaster_bounds)
        return broadcaster_quadtree

    def _remove_broadcaster_from_quadtree(self, broadcaster):
        broadcaster_quadtree = broadcaster.quadtree
        if broadcaster_quadtree is not None:
            broadcaster_quadtree.remove(broadcaster)

    def update_cluster_request(self, broadcaster):
        if broadcaster not in self._active_broadcasters:
            return
        clustering_request = broadcaster.get_clustering()
        if clustering_request is None:
            return
        self._remove_from_cluster_request(broadcaster)
        cluster_request_key = (type(broadcaster), broadcaster.routing_surface.secondary_id)
        if cluster_request_key in self._cluster_requests:
            cluster_request = self._cluster_requests[cluster_request_key]
            cluster_request.set_object_dirty(broadcaster)
        else:
            cluster_quadtree = self._quadtrees[broadcaster.routing_surface.secondary_id]
            cluster_request = clustering_request(lambda : self._get_broadcasters_for_cluster_request_gen(*cluster_request_key), quadtree=cluster_quadtree)
            self._cluster_requests[cluster_request_key] = cluster_request
        quadtree = self._add_broadcaster_to_quadtree(broadcaster)
        broadcaster.on_added_to_quadtree_and_cluster_request(quadtree, cluster_request)

    def _remove_from_cluster_request(self, broadcaster):
        cluster_request = broadcaster.cluster_request
        if cluster_request is not None:
            cluster_request.set_object_dirty(broadcaster)

    def _is_valid_cache_object(self, obj):
        if obj.is_sim:
            return False
        elif self._object_cache_tags:
            object_tags = obj.get_tags()
            if object_tags & self._object_cache_tags:
                return True
            else:
                return False
        return False
        return True

    def get_object_cache_info(self):
        return (self._object_cache, self._object_cache_tags)

    def _generate_object_cache(self):
        self._object_cache = WeakSet(obj for obj in services.object_manager().valid_objects() if self._is_valid_cache_object(obj))

    def _update_object_cache(self, obj=None):
        if obj is None:
            self._object_cache = None
            self._object_cache_tags = None
            return
        if self._object_cache is not None and self._is_valid_cache_object(obj):
            self._object_cache.add(obj)

    def _is_valid_broadcaster(self, broadcaster):
        broadcasting_object = broadcaster.broadcasting_object
        if broadcasting_object is None or not broadcasting_object.visible_to_client:
            return False
        if broadcasting_object.is_in_inventory():
            return False
        elif broadcasting_object.parent is not None and broadcasting_object.parent.is_sim:
            return False
        return True

    def _get_broadcasters_for_cluster_request_gen(self, broadcaster_type, broadcaster_level):
        for broadcaster in self._active_broadcasters:
            if broadcaster.guid == broadcaster_type.guid:
                if broadcaster.should_cluster():
                    if broadcaster.routing_surface.secondary_id == broadcaster_level:
                        yield broadcaster

    def get_broadcasters_debug_gen(self):
        for cluster_request in self._cluster_requests.values():
            for cluster in cluster_request.get_clusters_gen():
                broadcaster_iter = cluster.objects_gen()
                yield next(broadcaster_iter)
            yield from cluster_request.get_rejects()
        for broadcaster in self._active_broadcasters:
            if not broadcaster.should_cluster():
                if self._is_valid_broadcaster(broadcaster):
                    yield broadcaster

    def get_broadcasters_gen(self):
        for (cluster_request_key, cluster_request) in self._cluster_requests.items():
            is_cluster_dirty = cluster_request.is_dirty()
            for broadcaster in self._get_broadcasters_for_cluster_request_gen(*cluster_request_key):
                if self._is_valid_broadcaster(broadcaster):
                    broadcaster.regenerate_constraint()
            for cluster in cluster_request.get_clusters_gen():
                linkable_broadcasters_iter = (b for b in cluster.objects_gen() if self._is_valid_broadcaster(b))
                master_broadcaster = next(linkable_broadcasters_iter, None)
                if master_broadcaster is None:
                    continue
                master_broadcaster.set_linked_broadcasters(linkable_broadcasters_iter)
                yield master_broadcaster
            yield from (b for b in cluster_request.get_rejects() if self._is_valid_broadcaster(b))
        for broadcaster in self._active_broadcasters:
            if not broadcaster.should_cluster():
                if self._is_valid_broadcaster(broadcaster):
                    yield broadcaster

    PathSegmentData = namedtuple('PathSegmentData', ('prev_pos', 'cur_pos', 'segment_vec', 'segment_mag_sq', 'segment_normal'))

    def get_broadcasters_along_route_gen(self, sim, path, start_time=0, end_time=0):
        path_segment_datas = {}
        start_index = max(0, path.node_at_time(start_time).index - 1)
        end_index = min(len(path) - 1, path.node_at_time(end_time).index)
        for broadcaster in self.get_broadcasters_gen():
            if broadcaster.route_events:
                if not broadcaster.can_affect(sim):
                    continue
                constraint = broadcaster.get_constraint()
                geometry = constraint.geometry
                if geometry is None:
                    continue
                polygon = geometry.polygon
                if polygon is None:
                    continue
                if not constraint.valid:
                    continue
                constraint_pos = polygon.centroid()
                constraint_radius_sq = polygon.radius()
                constraint_radius_sq = constraint_radius_sq*constraint_radius_sq
                for index in range(end_index, start_index, -1):
                    prev_index = index - 1
                    prev_node = path.nodes[prev_index]
                    if not constraint.is_routing_surface_valid(prev_node.routing_surface_id):
                        continue
                    segment_key = (prev_index, index)
                    segment_data = path_segment_datas.get(segment_key, None)
                    if segment_data is None:
                        cur_node = path.nodes[index]
                        cur_pos = sims4.math.Vector3(*cur_node.position)
                        prev_pos = sims4.math.Vector3(*prev_node.position)
                        segment_vec = cur_pos - prev_pos
                        segment_vec.y = 0
                        segment_mag_sq = segment_vec.magnitude_2d_squared()
                        if sims4.math.almost_equal_sq(segment_mag_sq, 0):
                            segment_normal = None
                        else:
                            segment_normal = segment_vec/sims4.math.sqrt(segment_mag_sq)
                        segment_data = BroadcasterService.PathSegmentData(prev_pos, cur_pos, segment_vec, segment_mag_sq, segment_normal)
                        path_segment_datas[segment_key] = segment_data
                    else:
                        (prev_pos, cur_pos, segment_vec, segment_mag_sq, segment_normal) = segment_data
                    if segment_normal is None:
                        constraint_vec = constraint_pos - prev_pos
                        constraint_dist_sq = constraint_vec.magnitude_2d_squared()
                        if constraint_radius_sq < constraint_dist_sq:
                            continue
                    else:
                        constraint_vec = constraint_pos - prev_pos
                        constraint_vec.y = 0
                        contraint_proj = constraint_vec - segment_normal*sims4.math.vector_dot_2d(constraint_vec, segment_normal)
                        if constraint_radius_sq < contraint_proj.magnitude_2d_squared():
                            continue
                    for (transform, _, time) in path.get_location_data_along_segment_gen(prev_index, index):
                        if not geometry.test_transform(transform):
                            continue
                        yield (time, broadcaster)
                        break
                    break

    def get_pending_broadcasters_gen(self):
        yield from self._pending_broadcasters

    def _get_all_objects_gen(self):
        is_any_broadcaster_allowing_objects = True if self._object_cache else False
        if not is_any_broadcaster_allowing_objects:
            for broadcaster in self._active_broadcasters:
                (allow_objects, allow_objects_tags) = broadcaster.allow_objects.is_affecting_objects()
                if allow_objects:
                    is_any_broadcaster_allowing_objects = True
                    if allow_objects_tags is None:
                        self._object_cache_tags = None
                        break
                    else:
                        if self._object_cache_tags is None:
                            self._object_cache_tags = set()
                        self._object_cache_tags |= allow_objects_tags
        if is_any_broadcaster_allowing_objects:
            if self._object_cache is None:
                self._generate_object_cache()
            yield from list(self._object_cache)
        else:
            self._object_cache = None
            self._object_cache_tags = None
        yield from services.sim_info_manager().instanced_sims_gen()

    def register_callback(self, callback):
        if callback not in self._on_update_callbacks:
            self._on_update_callbacks.append(callback)

    def unregister_callback(self, callback):
        if callback in self._on_update_callbacks:
            self._on_update_callbacks.remove(callback)

    def _on_update(self, _):
        self._pending_update = True

    def _on_wall_contours_changed(self, *_, **__):
        self._update_object_cache()

    def provide_route_events(self, route_event_context, sim, path, failed_types=None, start_time=0, end_time=0, **kwargs):
        for (time, broadcaster) in self.get_broadcasters_along_route_gen(sim, path, start_time=start_time, end_time=end_time):
            resolver = broadcaster.get_resolver(sim)
            for route_event in broadcaster.route_events:
                if not failed_types is None:
                    pass
                if not route_event_context.route_event_already_scheduled(route_event, provider=broadcaster) and route_event.test(resolver):
                    route_event_context.add_route_event(RouteEventType.BROADCASTER, route_event(time=time, provider=broadcaster, provider_required=True))

    def update(self):
        if self._pending_update:
            self._pending_update = False
            self._update()

    def _is_location_affected(self, constraint, transform, routing_surface):
        if constraint.geometry is not None and not constraint.geometry.test_transform(transform):
            return False
        elif not constraint.is_routing_surface_valid(routing_surface):
            return False
        return True

    def update_broadcasters_one_shot(self, broadcasters):
        for obj in self._get_all_objects_gen():
            object_transform = None
            routing_surface = obj.routing_surface
            for broadcaster in broadcasters:
                if broadcaster.can_affect(obj):
                    constraint = broadcaster.get_constraint()
                    if not constraint.valid:
                        continue
                    if object_transform is None:
                        parent = obj.parent
                        if parent is None:
                            object_transform = obj.transform
                        else:
                            object_transform = parent.transform
                    if self._is_location_affected(constraint, object_transform, routing_surface):
                        broadcaster.apply_broadcaster_effect(obj)
                        broadcaster.remove_broadcaster_effect(obj)
                        if not obj.valid_for_distribution:
                            break

    def _update(self):
        try:
            self._activate_pending_broadcasters()
            current_broadcasters = set(self.get_broadcasters_gen())
            for obj in self._get_all_objects_gen():
                object_transform = None
                is_affected = False
                for broadcaster in current_broadcasters:
                    if broadcaster.can_affect(obj):
                        constraint = broadcaster.get_constraint()
                        if not constraint.valid:
                            continue
                        if object_transform is None:
                            parent = obj.parent
                            if parent is None:
                                object_transform = obj.transform
                            else:
                                object_transform = parent.transform
                        if self._is_location_affected(constraint, object_transform, obj.routing_surface):
                            broadcaster.apply_broadcaster_effect(obj)
                            if not obj.valid_for_distribution:
                                is_affected = False
                                break
                            is_affected = True
                if not is_affected:
                    if self._object_cache is not None:
                        self._object_cache.discard(obj)
            for broadcaster in current_broadcasters:
                broadcaster.on_processed()
        finally:
            self._on_update_callbacks()
class UiDialog(HasTunableFactory, AutoFactoryInit):
    __qualname__ = 'UiDialog'
    DIALOG_MSG_TYPE = Consts_pb2.MSG_UI_DIALOG_SHOW
    FACTORY_TUNABLES = {'title': OptionalTunable(description='\n            If enabled, this dialog will include title text.\n            ', tunable=TunableLocalizedStringFactory(description="\n                The dialog's title.\n                ")), 'text': TunableLocalizedStringFactoryVariant(description="\n            The dialog's text.\n            "), 'text_tokens': OptionalTunable(description='\n            If enabled, define text tokens to be used to localized text.\n            ', tunable=LocalizationTokens.TunableFactory(description='\n                Define the text tokens that are available to all text fields in\n                the dialog, such as title, text, responses, default and initial\n                text values, tooltips, etc.\n                '), disabled_value=DEFAULT), 'icon': OptionalTunable(description='\n            If enabled, specify an icon to be displayed.\n            ', tunable=TunableIconVariant(), needs_tuning=True), 'secondary_icon': OptionalTunable(description='\n            If enabled, specify a secondary icon to be displayed. Only certain\n            dialog types may support this field.\n            ', tunable=TunableIconVariant(), needs_tuning=True), 'phone_ring_type': TunableEnumEntry(description='\n             The phone ring type of this dialog.  If tuned to anything other\n             than None this dialog will only appear after clicking on the phone.\n             ', tunable_type=PhoneRingType, needs_tuning=True, default=PhoneRingType.NO_RING), 'audio_sting': OptionalTunable(description='\n            If enabled, play an audio sting when the dialog is shown.\n            ', tunable=TunablePlayAudio()), 'ui_responses': TunableList(description='\n            A list of buttons that are mapped to UI commands.\n            ', tunable=get_defualt_ui_dialog_response()), 'dialog_options': TunableEnumFlags(description='\n            Options to apply to the dialog.\n            ', enum_type=UiDialogOption, allow_no_flags=True, default=UiDialogOption.DISABLE_CLOSE_BUTTON)}

    def __init__(self, owner, resolver=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._owner = owner.ref()
        self._resolver = resolver
        self._additional_responses = {}
        self.response = None
        self._timestamp = None
        self._listeners = CallableList()

    @property
    def accepted(self) -> bool:
        return self.response is not None and self.response != ButtonType.DIALOG_RESPONSE_CLOSED

    @property
    def responses(self):
        return tuple()

    @property
    def owner(self):
        return self._owner()

    @property
    def dialog_type(self):
        return self._dialog_type

    def add_listener(self, listener_callback):
        self._listeners.append(listener_callback)

    def set_responses(self, responses):
        self._additional_responses = tuple(responses)

    def has_responses(self):
        return self.responses or self._additional_responses

    def _get_responses_gen(self):
        yield self.responses
        yield self._additional_responses
        yield self.ui_responses

    def respond(self, response) -> bool:
        try:
            self.response = response
            self._listeners(self)
            return True
        finally:
            self.on_response_received()
        return False

    def update(self) -> bool:
        return True

    def show_dialog(self, on_response=None, **kwargs):
        if self.audio_sting is not None:
            play_tunable_audio(self.audio_sting, None)
        if on_response is not None:
            self.add_listener(on_response)
        pythonutils.try_highwater_gc()
        services.ui_dialog_service().dialog_show(self, self.phone_ring_type, **kwargs)

    def distribute_dialog(self, dialog_type, dialog_msg):
        distributor = Distributor.instance()
        distributor.add_event(dialog_type, dialog_msg)

    def _build_localized_string_msg(self, string, *additional_tokens):
        if string is None:
            logger.callstack('_build_localized_string_msg received None for the string to build. This is probably not intended.', owner='tingyul')
            return
        tokens = ()
        if self._resolver is not None:
            if self.text_tokens is DEFAULT:
                tokens = self._resolver.get_localization_tokens()
            elif self.text_tokens is not None:
                tokens = self.text_tokens.get_tokens(self._resolver)
        return string(*tokens + additional_tokens)

    def _build_response_arg(self, response, response_msg, tutorial_id=None, additional_tokens=(), **kwargs):
        response_msg.choice_id = response.dialog_response_id
        response_msg.ui_request = response.ui_request
        if response.text is not None:
            response_msg.text = self._build_localized_string_msg(response.text, *additional_tokens)
        if tutorial_id is not None:
            response_msg.tutorial_args.tutorial_id = tutorial_id

    def build_msg(self, additional_tokens=(), icon_override=DEFAULT, secondary_icon_override=DEFAULT, **kwargs):
        msg = Dialog_pb2.UiDialogMessage()
        msg.dialog_id = self.dialog_id
        msg.owner_id = self.owner.id
        msg.dialog_type = Dialog_pb2.UiDialogMessage.DEFAULT
        if self.title is not None:
            msg.title = self._build_localized_string_msg(self.title, *additional_tokens)
        msg.text = self._build_localized_string_msg(self.text, *additional_tokens)
        if icon_override is DEFAULT:
            if self.icon is not None:
                icon_info = self.icon(self._resolver)
                key = icon_info[0]
                if key is not None:
                    msg.icon.type = key.type
                    msg.icon.group = key.group
                    msg.icon.instance = key.instance
                build_icon_info_msg(icon_info, None, msg.icon_info)
        elif icon_override is not None:
            build_icon_info_msg(icon_override, None, msg.icon_info)
        if secondary_icon_override is DEFAULT:
            if self.secondary_icon is not None:
                icon_info = self.secondary_icon(self._resolver)
                build_icon_info_msg(icon_info, None, msg.secondary_icon_info)
        elif secondary_icon_override is not None:
            build_icon_info_msg(secondary_icon_override, None, msg.secondary_icon_info)
        msg.dialog_options = self.dialog_options
        responses = []
        responses.extend(self._get_responses_gen())
        responses.sort(key=lambda response: response.sort_order)
        for response in responses:
            response_msg = msg.choices.add()
            self._build_response_arg(response, response_msg, additional_tokens=additional_tokens, **kwargs)
        return msg

    def on_response_received(self):
        pass

    def do_auto_respond(self):
        if ButtonType.DIALOG_RESPONSE_CANCEL in self.responses:
            response = ButtonType.DIALOG_RESPONSE_CANCEL
        elif ButtonType.DIALOG_RESPONSE_OK in self.responses:
            response = ButtonType.DIALOG_RESPONSE_OK
        else:
            response = ButtonType.DIALOG_RESPONSE_CLOSED
        services.ui_dialog_service().dialog_respond(self.dialog_id, response)
Exemple #27
0
class SituationGoal(SituationGoalDisplayMixin,
                    metaclass=HashedTunedInstanceMetaclass,
                    manager=services.get_instance_manager(
                        sims4.resources.Types.SITUATION_GOAL)):
    INSTANCE_SUBCLASSES_ONLY = True
    IS_TARGETED = False
    INSTANCE_TUNABLES = {
        '_pre_tests':
        TunableSituationGoalPreTestSet(
            description=
            '\n            A set of tests on the player sim and environment that all must\n            pass for the goal to be given to the player. e.g. Player Sim\n            has cooking skill level 7.\n            ',
            tuning_group=GroupNames.TESTS),
        '_post_tests':
        TunableSituationGoalPostTestSet(
            description=
            '\n            A set of tests that must all pass when the player satisfies the\n            goal_test for the goal to be consider completed. e.g. Player\n            has Drunk Buff when Kissing another sim at Night.\n            ',
            tuning_group=GroupNames.TESTS),
        '_cancel_on_travel':
        Tunable(
            description=
            '\n            If set, this situation goal will cancel (technically, complete\n            with score overridden to 0 so that situation score is not\n            progressed) if situation changes zone.\n            ',
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.TESTS),
        '_environment_pre_tests':
        TunableSituationGoalEnvironmentPreTestSet(
            description=
            '\n            A set of sim independent pre tests.\n            e.g. There are five desks.\n            ',
            tuning_group=GroupNames.TESTS),
        'role_tags':
        TunableSet(
            TunableEnumEntry(Tag, Tag.INVALID),
            description=
            '\n            This goal will only be given to Sims in SituationJobs or Role\n            States marked with one of these tags.\n            '
        ),
        '_cooldown':
        TunableSimMinute(
            description=
            '\n            The cooldown of this situation goal.  Goals that have been\n            completed will not be chosen again for the amount of time that\n            is tuned.\n            ',
            default=600,
            minimum=0),
        '_iterations':
        Tunable(
            description=
            '\n             Number of times the player must perform the action to complete the goal\n             ',
            tunable_type=int,
            default=1),
        '_score':
        Tunable(
            description=
            '\n            The number of points received for completing the goal.\n            ',
            tunable_type=int,
            default=10),
        'score_on_iteration_complete':
        OptionalTunable(
            description=
            '\n            If enabled then we will add an amount of score to the situation\n            with every iteration of the situation goal completing.\n            ',
            tunable=Tunable(
                description=
                '\n                An amount of score that should be applied when an iteration\n                completes.\n                ',
                tunable_type=int,
                default=10)),
        '_pre_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is started.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        '_goal_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to every\n            sim in the situation when this situation goal is completed.\n             \n            Do not use this loot list in an attempt to undo changes made by\n            the RoleStates to the sim. For example, do not attempt\n            to remove buffs or commodities added by the RoleState.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'noncancelable':
        Tunable(
            description=
            '\n            Checking this box will prevent the player from canceling this goal in the whim system.',
            tunable_type=bool,
            default=False),
        'time_limit':
        Tunable(
            description=
            '\n            Timeout (in Sim minutes) for Sim to complete this goal. The default state of 0 means\n            time is unlimited. If the goal is not completed in time, any tuned penalty loot is applied.',
            tunable_type=int,
            default=0),
        'penalty_loot_list':
        TunableList(
            description=
            '\n            A list of pre-defined loot actions that will applied to the Sim who fails\n            to complete this goal within the tuned time limit.\n            ',
            tunable=SituationGoalLootActions.TunableReference()),
        'goal_awarded_notification':
        OptionalTunable(
            description=
            '\n            If enabled, this goal will have a notification associated with it.\n            It is up to whatever system awards the goal (e.g. the Whim system)\n            to display the notification when necessary.\n            ',
            tunable=TunableUiDialogNotificationSnippet()),
        'goal_completion_notification':
        OptionalTunable(tunable=UiDialogNotification.TunableFactory(
            description=
            '\n                A TNS that will fire when this situation goal is completed.\n                '
        )),
        'goal_completion_notification_and_modal_target':
        OptionalTunable(
            description=
            '\n            If enabled then we will use the tuned situation job to pick a\n            random sim in the owning situation with that job to be the target\n            sim of the notification and modal dialog.\n            ',
            tunable=TunableReference(
                description=
                '\n                The situation job that will be used to find a sim in the owning\n                situation to be the target sim.\n                ',
                manager=services.get_instance_manager(
                    sims4.resources.Types.SITUATION_JOB))),
        'audio_sting_on_complete':
        TunableResourceKey(
            description=
            '\n            The sound to play when this goal is completed.\n            ',
            resource_types=(sims4.resources.Types.PROPX, ),
            default=None,
            allow_none=True,
            tuning_group=GroupNames.AUDIO),
        'goal_completion_modal_dialog':
        OptionalTunable(tunable=UiDialogOk.TunableFactory(
            description=
            '\n                A modal dialog that will fire when this situation goal is\n                completed.\n                '
        )),
        'visible_minor_goal':
        Tunable(
            description=
            '\n            Whether or not this goal should be displayed in the minor goals\n            list if this goal is for a player facing situation.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.UI),
        'display_type':
        TunableEnumEntry(
            description=
            '\n            How this goal is presented in user-facing situations.\n            ',
            tunable_type=SituationGoalDisplayType,
            default=SituationGoalDisplayType.NORMAL,
            tuning_group=GroupNames.UI)
    }

    @classmethod
    def can_be_given_as_goal(cls, actor, situation, **kwargs):
        if actor is not None:
            resolver = event_testing.resolver.DataResolver(
                actor.sim_info, None)
            result = cls._pre_tests.run_tests(resolver)
            if not result:
                return result
        else:
            resolver = GlobalResolver()
        environment_test_result = cls._environment_pre_tests.run_tests(
            resolver)
        if not environment_test_result:
            return environment_test_result
        return TestResult.TRUE

    def __init__(self,
                 sim_info=None,
                 situation=None,
                 goal_id=0,
                 count=0,
                 locked=False,
                 completed_time=None,
                 secondary_sim_info=None,
                 **kwargs):
        self._sim_info = sim_info
        self._secondary_sim_info = secondary_sim_info
        self._situation = situation
        self.id = goal_id
        self._on_goal_completed_callbacks = CallableList()
        self._completed_time = completed_time
        self._count = count
        self._locked = locked
        self._score_override = None
        self._goal_status_override = None
        self._setup = False

    def setup(self):
        self._setup = True

    def destroy(self):
        self.decommision()
        self._sim_info = None
        self._situation = None

    def decommision(self):
        if self._setup:
            self._decommision()

    def _decommision(self):
        self._on_goal_completed_callbacks.clear()

    def create_seedling(self):
        actor_id = 0 if self._sim_info is None else self._sim_info.sim_id
        target_sim_info = self.get_required_target_sim_info()
        target_id = 0 if target_sim_info is None else target_sim_info.sim_id
        secondary_target_id = 0 if self._secondary_sim_info is None else self._secondary_sim_info.sim_id
        seedling = situations.situation_serialization.GoalSeedling(
            type(self), actor_id, target_id, secondary_target_id, self._count,
            self._locked, self._completed_time)
        return seedling

    def register_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.append(listener)

    def unregister_for_on_goal_completed_callback(self, listener):
        self._on_goal_completed_callbacks.remove(listener)

    def get_gsi_name(self):
        if self._iterations <= 1:
            return self.__class__.__name__
        return '{} {}/{}'.format(self.__class__.__name__, self._count,
                                 self._iterations)

    def on_goal_offered(self):
        if self._situation is None:
            return
        for sim in self._situation.all_sims_in_situation_gen():
            resolver = sim.get_resolver()
            for loots in self._pre_goal_loot_list:
                for loot in loots.goal_loot_actions:
                    loot.apply_to_resolver(resolver)

    def _display_goal_completed_dialogs(self):
        actor_sim_info = services.active_sim_info()
        target_sim_info = None
        if self.goal_completion_notification_and_modal_target is not None:
            possible_sims = list(
                self._situation.all_sims_in_job_gen(
                    self.goal_completion_notification_and_modal_target))
            if possible_sims:
                target_sim_info = random.choice(possible_sims)
            if target_sim_info is None:
                return
        resolver = DoubleSimResolver(actor_sim_info, target_sim_info)
        if self.goal_completion_notification is not None:
            notification = self.goal_completion_notification(actor_sim_info,
                                                             resolver=resolver)
            notification.show_dialog()
        if self.goal_completion_modal_dialog is not None:
            dialog = self.goal_completion_modal_dialog(actor_sim_info,
                                                       resolver=resolver)
            dialog.show_dialog()

    def _on_goal_completed(self, start_cooldown=True):
        if start_cooldown:
            self._completed_time = services.time_service().sim_now
        loot_sims = (self._sim_info, ) if self._situation is None else tuple(
            self._situation.all_sims_in_situation_gen())
        for loots in self._goal_loot_list:
            for loot in loots.goal_loot_actions:
                for sim in loot_sims:
                    loot.apply_to_resolver(sim.get_resolver())
        self._display_goal_completed_dialogs()
        with situations.situation_manager.DelayedSituationDestruction():
            self._on_goal_completed_callbacks(self, True)

    def _on_iteration_completed(self):
        self._on_goal_completed_callbacks(self, False)

    def force_complete(self,
                       target_sim=None,
                       score_override=None,
                       start_cooldown=True):
        self._score_override = score_override
        self._count = self._iterations
        self._on_goal_completed(start_cooldown=start_cooldown)

    def _valid_event_sim_of_interest(self, sim_info):
        return self._sim_info is None or self._sim_info is sim_info

    def handle_event(self, sim_info, event, resolver):
        if not self._valid_event_sim_of_interest(sim_info):
            return
        if self._run_goal_completion_tests(sim_info, event, resolver):
            self._count += 1
            if self._count >= self._iterations:
                self._on_goal_completed()
            else:
                self._on_iteration_completed()

    def _run_goal_completion_tests(self, sim_info, event, resolver):
        return self._post_tests.run_tests(resolver)

    def should_autocomplete_on_load(self, previous_zone_id):
        if self._cancel_on_travel:
            zone_id = services.current_zone_id()
            if previous_zone_id != zone_id:
                return True
        return False

    def get_actual_target_sim_info(self):
        pass

    @property
    def sim_info(self):
        return self._sim_info

    def get_required_target_sim_info(self):
        pass

    def get_secondary_sim_info(self):
        return self._secondary_sim_info

    @property
    def created_time(self):
        pass

    @property
    def completed_time(self):
        return self._completed_time

    def is_on_cooldown(self):
        if self._completed_time is None:
            return False
        time_since_last_completion = services.time_service(
        ).sim_now - self._completed_time
        return time_since_last_completion < interval_in_sim_minutes(
            self._cooldown)

    def get_localization_tokens(self):
        target_sim_info = self.get_required_target_sim_info()
        return (self._numerical_token, self._sim_info, target_sim_info,
                self._secondary_sim_info)

    def get_display_name(self):
        display_name = self.display_name
        if display_name is not None:
            return display_name(*self.get_localization_tokens())

    def get_display_tooltip(self):
        display_tooltip = self.display_tooltip
        if display_tooltip is not None:
            return display_tooltip(*self.get_localization_tokens())

    @property
    def score(self):
        if self._score_override is not None:
            return self._score_override
        return self._score

    @property
    def goal_status_override(self):
        return self._goal_status_override

    @property
    def completed_iterations(self):
        return self._count

    @property
    def max_iterations(self):
        return self._iterations

    @property
    def _numerical_token(self):
        return self.max_iterations

    @property
    def locked(self):
        return self._locked

    def toggle_locked_status(self):
        self._locked = not self._locked

    def validate_completion(self):
        if self._completed_time is not None:
            return
        if self.completed_iterations < self.max_iterations:
            return
        self.force_complete()

    def show_goal_awarded_notification(self):
        if self.goal_awarded_notification is None:
            return
        icon_override = IconInfoData(icon_resource=self.display_icon)
        secondary_icon_override = IconInfoData(obj_instance=self._sim_info)
        notification = self.goal_awarded_notification(self._sim_info)
        notification.show_dialog(
            additional_tokens=self.get_localization_tokens(),
            icon_override=icon_override,
            secondary_icon_override=secondary_icon_override)
class UiDialog(HasTunableFactory, AutoFactoryInit):
    __qualname__ = 'UiDialog'
    DIALOG_MSG_TYPE = Consts_pb2.MSG_UI_DIALOG_SHOW
    FACTORY_TUNABLES = {
        'title':
        OptionalTunable(
            description=
            '\n            If enabled, this dialog will include title text.\n            ',
            tunable=TunableLocalizedStringFactory(
                description=
                "\n                The dialog's title.\n                ")),
        'text':
        TunableLocalizedStringFactoryVariant(
            description="\n            The dialog's text.\n            "),
        'text_tokens':
        OptionalTunable(
            description=
            '\n            If enabled, define text tokens to be used to localized text.\n            ',
            tunable=LocalizationTokens.TunableFactory(
                description=
                '\n                Define the text tokens that are available to all text fields in\n                the dialog, such as title, text, responses, default and initial\n                text values, tooltips, etc.\n                '
            ),
            disabled_value=DEFAULT),
        'icon':
        OptionalTunable(
            description=
            '\n            If enabled, specify an icon to be displayed.\n            ',
            tunable=TunableIconVariant(),
            needs_tuning=True),
        'secondary_icon':
        OptionalTunable(
            description=
            '\n            If enabled, specify a secondary icon to be displayed. Only certain\n            dialog types may support this field.\n            ',
            tunable=TunableIconVariant(),
            needs_tuning=True),
        'phone_ring_type':
        TunableEnumEntry(
            description=
            '\n             The phone ring type of this dialog.  If tuned to anything other\n             than None this dialog will only appear after clicking on the phone.\n             ',
            tunable_type=PhoneRingType,
            needs_tuning=True,
            default=PhoneRingType.NO_RING),
        'audio_sting':
        OptionalTunable(
            description=
            '\n            If enabled, play an audio sting when the dialog is shown.\n            ',
            tunable=TunablePlayAudio()),
        'ui_responses':
        TunableList(
            description=
            '\n            A list of buttons that are mapped to UI commands.\n            ',
            tunable=get_defualt_ui_dialog_response()),
        'dialog_options':
        TunableEnumFlags(
            description=
            '\n            Options to apply to the dialog.\n            ',
            enum_type=UiDialogOption,
            allow_no_flags=True,
            default=UiDialogOption.DISABLE_CLOSE_BUTTON)
    }

    def __init__(self, owner, resolver=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._owner = owner.ref()
        self._resolver = resolver
        self._additional_responses = {}
        self.response = None
        self._timestamp = None
        self._listeners = CallableList()

    @property
    def accepted(self) -> bool:
        return self.response is not None and self.response != ButtonType.DIALOG_RESPONSE_CLOSED

    @property
    def responses(self):
        return tuple()

    @property
    def owner(self):
        return self._owner()

    @property
    def dialog_type(self):
        return self._dialog_type

    def add_listener(self, listener_callback):
        self._listeners.append(listener_callback)

    def set_responses(self, responses):
        self._additional_responses = tuple(responses)

    def has_responses(self):
        return self.responses or self._additional_responses

    def _get_responses_gen(self):
        yield self.responses
        yield self._additional_responses
        yield self.ui_responses

    def respond(self, response) -> bool:
        try:
            self.response = response
            self._listeners(self)
            return True
        finally:
            self.on_response_received()
        return False

    def update(self) -> bool:
        return True

    def show_dialog(self, on_response=None, **kwargs):
        if self.audio_sting is not None:
            play_tunable_audio(self.audio_sting, None)
        if on_response is not None:
            self.add_listener(on_response)
        pythonutils.try_highwater_gc()
        services.ui_dialog_service().dialog_show(self, self.phone_ring_type,
                                                 **kwargs)

    def distribute_dialog(self, dialog_type, dialog_msg):
        distributor = Distributor.instance()
        distributor.add_event(dialog_type, dialog_msg)

    def _build_localized_string_msg(self, string, *additional_tokens):
        if string is None:
            logger.callstack(
                '_build_localized_string_msg received None for the string to build. This is probably not intended.',
                owner='tingyul')
            return
        tokens = ()
        if self._resolver is not None:
            if self.text_tokens is DEFAULT:
                tokens = self._resolver.get_localization_tokens()
            elif self.text_tokens is not None:
                tokens = self.text_tokens.get_tokens(self._resolver)
        return string(*tokens + additional_tokens)

    def _build_response_arg(self,
                            response,
                            response_msg,
                            tutorial_id=None,
                            additional_tokens=(),
                            **kwargs):
        response_msg.choice_id = response.dialog_response_id
        response_msg.ui_request = response.ui_request
        if response.text is not None:
            response_msg.text = self._build_localized_string_msg(
                response.text, *additional_tokens)
        if tutorial_id is not None:
            response_msg.tutorial_args.tutorial_id = tutorial_id

    def build_msg(self,
                  additional_tokens=(),
                  icon_override=DEFAULT,
                  secondary_icon_override=DEFAULT,
                  **kwargs):
        msg = Dialog_pb2.UiDialogMessage()
        msg.dialog_id = self.dialog_id
        msg.owner_id = self.owner.id
        msg.dialog_type = Dialog_pb2.UiDialogMessage.DEFAULT
        if self.title is not None:
            msg.title = self._build_localized_string_msg(
                self.title, *additional_tokens)
        msg.text = self._build_localized_string_msg(self.text,
                                                    *additional_tokens)
        if icon_override is DEFAULT:
            if self.icon is not None:
                icon_info = self.icon(self._resolver)
                key = icon_info[0]
                if key is not None:
                    msg.icon.type = key.type
                    msg.icon.group = key.group
                    msg.icon.instance = key.instance
                build_icon_info_msg(icon_info, None, msg.icon_info)
        elif icon_override is not None:
            build_icon_info_msg(icon_override, None, msg.icon_info)
        if secondary_icon_override is DEFAULT:
            if self.secondary_icon is not None:
                icon_info = self.secondary_icon(self._resolver)
                build_icon_info_msg(icon_info, None, msg.secondary_icon_info)
        elif secondary_icon_override is not None:
            build_icon_info_msg(secondary_icon_override, None,
                                msg.secondary_icon_info)
        msg.dialog_options = self.dialog_options
        responses = []
        responses.extend(self._get_responses_gen())
        responses.sort(key=lambda response: response.sort_order)
        for response in responses:
            response_msg = msg.choices.add()
            self._build_response_arg(response,
                                     response_msg,
                                     additional_tokens=additional_tokens,
                                     **kwargs)
        return msg

    def on_response_received(self):
        pass

    def do_auto_respond(self):
        if ButtonType.DIALOG_RESPONSE_CANCEL in self.responses:
            response = ButtonType.DIALOG_RESPONSE_CANCEL
        elif ButtonType.DIALOG_RESPONSE_OK in self.responses:
            response = ButtonType.DIALOG_RESPONSE_OK
        else:
            response = ButtonType.DIALOG_RESPONSE_CLOSED
        services.ui_dialog_service().dialog_respond(self.dialog_id, response)
Exemple #29
0
class UseListMixin:
    __qualname__ = 'UseListMixin'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._reservations = WeakKeyDictionary()
        self._reservations_multi = WeakKeyDictionary()
        self._use_list_changed_callbacks = CallableList()

    @property
    def using_sim(self):
        if not self._reservations:
            return
        return next(self._reservations.keys())

    @property
    def in_use(self):
        if self._reservations:
            return True
        return False

    def in_use_by(self, sim, owner=None):
        owners = self._reservations.get(sim)
        if not owners:
            return False
        return owner is None or owner in owners

    def _may_reserve_obj(self, sim, affordance, context):
        if self.in_use_by(sim):
            return True
        if self.children:
            for child in self.children:
                while not child.may_reserve(sim, multi=False, affordance=affordance, context=context):
                    return False
        return not self._reservations

    def _may_reserve_part(self, sim, affordance, context, check_overlapping_parts=True):
        part_list = self.part_owner.parts
        for part in part_list:
            if part is self:
                pass
            using_sim = part.using_sim
            while not using_sim is None:
                if using_sim is sim:
                    pass
                for si in using_sim.si_state:
                    reserve_object_tests = si.object_reservation_tests
                    while reserve_object_tests:
                        reserve_result = reserve_object_tests.run_tests(si.get_resolver(target=sim))
                        if not reserve_result:
                            return reserve_result
                if using_sim.queue.transition_controller is not None:
                    transitioning_interaction = using_sim.queue.transition_controller.interaction
                    if transitioning_interaction.is_super:
                        reserve_object_tests = transitioning_interaction.object_reservation_tests
                        if reserve_object_tests:
                            target_sim = sim if transitioning_interaction.sim is not sim else using_sim
                            reserve_result = reserve_object_tests.run_tests(transitioning_interaction.get_resolver(target=target_sim))
                            if not reserve_result:
                                return reserve_result
                while affordance is not None and affordance.is_super:
                    if context is None:
                        logger.error('Attempt to call may_reserve() with an affordance but no context!', owner='maxr')
                    reserve_object_tests = affordance.object_reservation_tests
                    if reserve_object_tests:
                        reserve_result = reserve_object_tests.run_tests(affordance.get_resolver(target=using_sim, context=context))
                        if not reserve_result:
                            return reserve_result
        if affordance is not None and not self.supports_affordance(affordance):
            return TestResult(False, '{} does not support {}'.format(self, affordance))
        reserve_result = self._may_reserve_obj(sim, affordance=affordance, context=context)
        if not reserve_result:
            return reserve_result
        if check_overlapping_parts:
            for overlapping_part in self.get_overlapping_parts():
                if overlapping_part is self:
                    pass
                reserve_result = overlapping_part._may_reserve_part(sim, None, None, check_overlapping_parts=False)
                while not reserve_result:
                    return reserve_result
        return TestResult(True, 'Passed all reservation tests.')

    def may_reserve(self, sim, multi=False, affordance=None, context=None):
        if self.parts:
            logger.callstack('Cannot reserve an object that has parts: {}, for {} running {} - multi: {}', self, sim, affordance, multi, level=sims4.log.LEVEL_ERROR)
            return False
        if multi:
            return True
        if self.is_part:
            result = self._may_reserve_part(sim, affordance, context)
        else:
            result = self._may_reserve_obj(sim, affordance, context)
        return result

    def reserve(self, sim, owner, multi=False):
        use_list = self._reservations_multi if multi else self._reservations
        sim_list = setdefault_callable(use_list, sim, WeakSet)
        sim_list.add(owner)
        self._use_list_changed_callbacks(user=sim, added=True)

    def release(self, sim, owner, multi=False):
        use_list = self._reservations_multi if multi else self._reservations
        sim_list = use_list.get(sim)
        sim_list.remove(owner)
        if not sim_list:
            del use_list[sim]
        self._use_list_changed_callbacks(user=sim, added=False)
        self._destroy_if_necessary()

    def make_transient(self):
        self.transient = True
        self._destroy_if_necessary()

    def _destroy_if_necessary(self):
        if not self._reservations_multi and not self._reservations and self.transient:
            if self.is_part:
                self.part_owner.schedule_destroy_asap(source=self, cause='Destroying unused transient part.')
            else:
                self.schedule_destroy_asap(source=self, cause='Destroying unused transient object.')

    def usable_by_transition_controller(self, transition_controller):
        if transition_controller is not None:
            required_sims = transition_controller.interaction.required_sims()
            if self.is_part:
                all_overlapping_parts = self.get_overlapping_parts()
                all_overlapping_parts.append(self)
                for part in all_overlapping_parts:
                    for user in part.get_users():
                        while user not in required_sims:
                            return False
                return True
            for required_sim in required_sims:
                while self.in_use_by(required_sim):
                    return True
        return False

    def get_users(self, sims_only=False, include_multi=True):
        targets = (self,) if not self.parts else self.parts
        if include_multi:
            return {sim for target in targets for sim in itertools.chain(target._reservations, target._reservations_multi) if not sims_only or sim.is_sim}
        return {sim for target in targets for sim in target._reservations if not sims_only or sim.is_sim}

    def on_reset_get_interdependent_reset_records(self, reset_reason, reset_records):
        super().on_reset_get_interdependent_reset_records(reset_reason, reset_records)
        relevant_sims = self.get_users(sims_only=True)
        for sim in relevant_sims:
            if self.reset_reason() == ResetReason.BEING_DESTROYED:
                reset_records.append(ResetRecord(sim, ResetReason.RESET_EXPECTED, self, 'In use list of object being destroyed.'))
            body_target_part_owner = sim.posture_state.body.target
            if body_target_part_owner is not None and body_target_part_owner.is_part:
                body_target_part_owner = body_target_part_owner.part_owner
            transition_controller = sim.queue.transition_controller
            while body_target_part_owner is self or transition_controller is None or not transition_controller.will_derail_if_given_object_is_reset(self):
                reset_records.append(ResetRecord(sim, ResetReason.RESET_EXPECTED, self, 'Transitioning To or In.'))

    def register_on_use_list_changed(self, callback):
        self._use_list_changed_callbacks.append(callback)

    def unregister_on_use_list_changed(self, callback):
        if callback in self._use_list_changed_callbacks:
            self._use_list_changed_callbacks.remove(callback)

    def _print_in_use(self):
        if self._reservations:
            for sim in self._reservations:
                logger.debug('    Reservation: {}', sim)
        if self._reservations_multi:
            for sim in self._reservations_multi:
                logger.debug('    Reservation_Multi: {}', sim)
Exemple #30
0
class Client:
    __qualname__ = 'Client'
    _interaction_source = interactions.context.InteractionContext.SOURCE_PIE_MENU
    _interaction_priority = interactions.priority.Priority.High

    def __init__(self, session_id, account, household_id):
        self.id = session_id
        self.manager = None
        self._account = account
        self._household_id = household_id
        self._choice_menu = None
        self._interaction_parameters = {}
        self.active = True
        self.zone_id = sims4.zone_utils.get_zone_id()
        self._selectable_sims = SelectableSims(self)
        self._active_sim_info = None
        self._active_sim_changed = CallableList()
        self.ui_objects = weakref.WeakSet()
        self.primitives = ()
        self._live_drag_objects = []
        self._live_drag_start_system = LiveDragLocation.INVALID
        self._live_drag_is_stack = False
        self._live_drag_sell_dialog_active = False

    def __repr__(self):
        return '<Client {0:#x}>'.format(self.id)

    @property
    def account(self):
        return self._account

    @distributor.fields.Field(op=distributor.ops.UpdateClientActiveSim)
    def active_sim_info(self):
        return self._active_sim_info

    resend_active_sim_info = active_sim_info.get_resend()

    @active_sim_info.setter
    def active_sim_info(self, sim_info):
        self._set_active_sim_without_field_distribution(sim_info)

    @property
    def active_sim(self):
        if self.active_sim_info is not None:
            return self.active_sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)

    @active_sim.setter
    def active_sim(self, sim):
        self.active_sim_info = sim.sim_info

    def _set_active_sim_without_field_distribution(self, sim_info):
        if self._active_sim_info is not None and self._active_sim_info is sim_info:
            return
        current_sim = self._active_sim_info.get_sim_instance() if self._active_sim_info is not None else None
        new_sim = sim_info.get_sim_instance() if sim_info is not None else None
        if sim_info is not None:
            self._active_sim_info = sim_info
            sim_info.household.on_active_sim_changed(sim_info)
        else:
            self._active_sim_info = None
        self.notify_active_sim_changed(current_sim, new_sim)

    @property
    def choice_menu(self):
        return self._choice_menu

    @property
    def interaction_source(self):
        return self._interaction_source

    @interaction_source.setter
    def interaction_source(self, value):
        if value is None:
            del self._interaction_source
        else:
            self._interaction_source = value

    @property
    def interaction_priority(self):
        return self._interaction_priority

    @interaction_priority.setter
    def interaction_priority(self, value):
        if value is None:
            del self._interaction_priority
        else:
            self._interaction_priority = value

    @property
    def household_id(self):
        return self._household_id

    @property
    def household(self):
        household_manager = services.household_manager()
        if household_manager is not None:
            return household_manager.get(self._household_id)

    @property
    def selectable_sims(self):
        return self._selectable_sims

    def create_interaction_context(self, sim, **kwargs):
        context = interactions.context.InteractionContext(sim, self.interaction_source, self.interaction_priority, client=self, **kwargs)
        return context

    @property
    def live_drag_objects(self):
        return self._live_drag_objects

    def get_interaction_parameters(self):
        return self._interaction_parameters

    def set_interaction_parameters(self, **kwargs):
        self._interaction_parameters = kwargs

    def set_choices(self, new_choices):
        self._choice_menu = new_choices

    def select_interaction(self, choice_id, revision):
        if self.choice_menu is not None and revision == self.choice_menu.revision:
            choice_menu = self.choice_menu
            self._choice_menu = None
            self.set_interaction_parameters()
            try:
                return choice_menu.select(choice_id)
            except:
                if choice_menu.context.sim is not None:
                    choice_menu.context.sim.reset(ResetReason.RESET_ON_ERROR, cause='Exception while selecting interaction from the pie menu.')
                raise

    def get_create_op(self, *args, **kwargs):
        return distributor.ops.ClientCreate(self, is_active=True, *args, **kwargs)

    def get_delete_op(self):
        return distributor.ops.ClientDelete()

    def get_create_after_objs(self):
        active = self.active_sim
        if active is not None:
            yield active
        household = self.household
        if household is not None:
            yield household

    @property
    def valid_for_distribution(self):
        return True

    def refresh_achievement_data(self):
        active_sim_info = None
        if self.active_sim is not None:
            active_sim_info = self.active_sim.sim_info
        self.account.achievement_tracker.refresh_progress(active_sim_info)

    def send_message(self, msg_id, msg):
        if self.active:
            omega.send(self.id, msg_id, msg.SerializeToString())
        else:
            logger.warn('Message sent to client {} after it has already disconnected.', self)

    def send_serialized_message(self, msg_id, msg):
        if self.active:
            omega.send(self.id, msg_id, msg)
        else:
            logger.warn('Serialized message sent to client {} after it has already disconnected.', self)

    def set_next_sim(self):
        sim_info = self._selectable_sims.get_next_selectable(self._active_sim_info)
        if sim_info is self.active_sim_info:
            return False
        return self.set_active_sim_info(sim_info)

    def set_next_sim_or_none(self, only_if_this_active_sim_info=None):
        if only_if_this_active_sim_info is not None and self._active_sim_info is not only_if_this_active_sim_info:
            return
        sim_info = self._selectable_sims.get_next_selectable(self._active_sim_info)
        if sim_info is None:
            return self.set_active_sim_info(None)
        if sim_info is self._active_sim_info:
            return self.set_active_sim_info(None)
        return self.set_active_sim_info(sim_info)

    def set_active_sim_by_id(self, sim_id):
        if self.active_sim_info is not None and self.active_sim_info.id == sim_id:
            return False
        for sim_info in self._selectable_sims:
            while sim_info.sim_id == sim_id:
                if not sim_info.is_enabled_in_skewer:
                    return False
                return self.set_active_sim_info(sim_info)
        return False

    def set_active_sim(self, sim):
        return self.set_active_sim_info(sim.sim_info)

    def set_active_sim_info(self, sim_info):
        with telemetry_helper.begin_hook(writer, TELEMETRY_HOOK_ACTIVE_SIM_CHANGED, sim=sim_info):
            pass
        self.active_sim_info = sim_info
        return self._active_sim_info is not None

    def add_selectable_sim_info(self, sim_info, send_relationship_update=True):
        self._selectable_sims.add_selectable_sim_info(sim_info, send_relationship_update=send_relationship_update)
        if self.active_sim_info is None:
            self.set_next_sim()
        self.household.refresh_aging_updates(sim_info)

    def add_selectable_sim_by_id(self, sim_id):
        sim_info = services.sim_info_manager().get(sim_id)
        if sim_info is not None:
            self.add_selectable_sim_info(sim_info)

    def remove_selectable_sim_info(self, sim_info):
        self._selectable_sims.remove_selectable_sim_info(sim_info)
        if self.active_sim_info is None:
            self.set_next_sim()
        self.household.refresh_aging_updates(sim_info)

    def remove_selectable_sim_by_id(self, sim_id):
        if len(self._selectable_sims) <= 1:
            return False
        sim_info = services.sim_info_manager().get(sim_id)
        if sim_info is not None:
            self.remove_selectable_sim_info(sim_info)
        return True

    def make_all_sims_selectable(self):
        self.clear_selectable_sims()
        for sim_info in services.sim_info_manager().objects:
            self._selectable_sims.add_selectable_sim_info(sim_info)
        self.set_next_sim()

    def clear_selectable_sims(self):
        self.active_sim_info = None
        self._selectable_sims.clear_selectable_sims()

    def register_active_sim_changed(self, callback):
        if callback not in self._active_sim_changed:
            self._active_sim_changed.append(callback)

    def unregister_active_sim_changed(self, callback):
        if callback in self._active_sim_changed:
            self._active_sim_changed.remove(callback)

    def on_sim_added_to_skewer(self, sim_info, send_relationship_update=True):
        if send_relationship_update:
            sim_info.relationship_tracker.send_relationship_info()
        sim_info.relationship_tracker.enable_selectable_sim_track_decay()
        sim_info.on_sim_added_to_skewer()
        sim_info.commodity_tracker.send_commodity_progress_update()
        sim_info.career_tracker.on_sim_added_to_skewer()
        sim_info.send_whim_bucks_update(SetWhimBucks.LOAD)
        sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is not None:
            sim.ui_manager.refresh_ui_data()
            services.autonomy_service().logging_sims.add(sim)
            sim.family_funds.empty_sim_personal_funds(sim)
            sim_info.aspiration_tracker.force_send_data_update()
            sim_info.aspiration_tracker.initialize_aspiration()
            sim_info.aspiration_tracker.set_update_alarm()
            sim_info.career_tracker.activate_career_aspirations()

    def on_sim_removed_from_skewer(self, sim_info, update_relationship_tracker=True):
        if update_relationship_tracker:
            if sim_info.is_child and sim_info.is_dead:
                sim_info.relationship_tracker.destroy_all_relationships()
            else:
                sim_info.relationship_tracker.enable_selectable_sim_track_decay(False)
        sim_info.aspiration_tracker.clear_update_alarm()
        sim = sim_info.get_sim_instance()
        if sim is not None:
            autonomy_service = services.autonomy_service()
            if autonomy_service is not None:
                autonomy_service.logging_sims.discard(sim)

    def clean_and_send_remaining_relationship_info(self):
        for sim_info in self.selectable_sims:
            sim_info.relationship_tracker.clean_and_send_remaining_relationship_info()

    def cancel_live_drag_on_objects(self):
        for obj in self._live_drag_objects:
            obj.live_drag_component.cancel_live_dragging()
        self._live_drag_objects = []

    def _get_stack_items_from_drag_object(self, drag_object, remove=False, is_stack=False):
        if drag_object.inventoryitem_component is None:
            return (False, None)
        previous_inventory = drag_object.inventoryitem_component.get_inventory()
        if previous_inventory is None:
            return (False, None)
        stack_id = drag_object.inventoryitem_component.get_stack_id()
        if remove:
            success = previous_inventory.try_remove_object_by_id(drag_object.id, force_remove_stack=is_stack)
        else:
            success = True
        stack_items = previous_inventory.get_stack_items(stack_id)
        return (success, stack_items)

    def remove_drag_object_and_get_next_item(self, drag_object):
        next_object_id = None
        (success, stack_items) = self._get_stack_items_from_drag_object(drag_object, remove=True)
        if success and stack_items:
            next_object_id = stack_items[0].id
        return (success, next_object_id)

    def get_live_drag_object_value(self, drag_object, is_stack=False):
        (_, stack_items) = self._get_stack_items_from_drag_object(drag_object, remove=False, is_stack=is_stack)
        value = 0
        if is_stack and stack_items:
            for item in stack_items:
                value += item.current_value*item.stack_count()
        else:
            value = drag_object.current_value
        return value

    def start_live_drag(self, live_drag_object, start_system, is_stack):
        self._live_drag_start_system = start_system
        success = True
        if is_stack:
            inventoryitem_component = live_drag_object.inventoryitem_component
            stack_id = inventoryitem_component.get_stack_id()
            current_inventory = inventoryitem_component.get_inventory()
            stack_items = current_inventory.get_stack_items(stack_id)
        else:
            stack_items = [live_drag_object]
        for item in stack_items:
            live_drag_component = live_drag_object.live_drag_component
            live_drag_component = item.live_drag_component
            if live_drag_component is None:
                logger_live_drag.error('Live Drag Start called on an object with no Live Drag Component. Object: {}'.format(item))
                self.send_live_drag_cancel(live_drag_object.id)
                return
            if item.in_use and not item.in_use_by(self) or not live_drag_component.can_live_drag:
                logger_live_drag.warn('Live Drag Start called on an object that is in use. Object: {}'.format(item))
                self.send_live_drag_cancel(item.id)
                return
            success = live_drag_component.start_live_dragging(self, start_system)
            if not success:
                break
            self._live_drag_objects.append(item)
        if not success:
            self.cancel_live_drag_on_objects()
            self.send_live_drag_cancel(live_drag_object.id, LiveDragLocation.INVALID)
        self._live_drag_is_stack = is_stack
        if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled:
            gsi_handlers.live_drag_handlers.archive_live_drag('Start', 'Operation', LiveDragLocation.GAMEPLAY_SCRIPT, start_system, live_drag_object_id=live_drag_object.id)
        if live_drag_object.live_drag_component.active_household_has_sell_permission:
            sell_value = self.get_live_drag_object_value(live_drag_object, self._live_drag_is_stack) if live_drag_object.definition.get_is_deletable() else -1
        else:
            sell_value = -1
        (valid_drop_object_ids, valid_stack_id) = live_drag_component.get_valid_drop_object_ids()
        op = distributor.ops.LiveDragStart(live_drag_object.id, start_system, valid_drop_object_ids, valid_stack_id, sell_value)
        distributor_system = Distributor.instance()
        distributor_system.add_op_with_no_owner(op)

    def end_live_drag(self, source_object, target_object=None, end_system=LiveDragLocation.INVALID, location=None):
        live_drag_component = source_object.live_drag_component
        if live_drag_component is None:
            logger_live_drag.error('Live Drag End called on an object with no Live Drag Component. Object: {}'.format(source_object))
            self.send_live_drag_cancel(source_object.id, end_system)
            return
        if source_object not in self._live_drag_objects:
            logger_live_drag.warn('Live Drag End called on an object not being Live Dragged. Object: {}'.format(source_object))
            self.send_live_drag_cancel(source_object.id, end_system)
            return
        source_object_id = source_object.id
        self.cancel_live_drag_on_objects()
        next_object_id = None
        success = False
        if target_object is not None:
            live_drag_target_component = target_object.live_drag_target_component
            if live_drag_target_component is not None:
                (success, next_object_id) = live_drag_target_component.drop_live_drag_object(source_object, self._live_drag_is_stack)
            else:
                logger_live_drag.error('Live Drag Target Component missing on object: {} is now required on all drop targets.'.format(target_object))
                success = False
        else:
            success = True
            if location is not None:
                source_object.set_location(location)
            inventory_item = source_object.inventoryitem_component
            if inventory_item is not None and inventory_item.is_in_inventory():
                (success, next_object_id) = self.remove_drag_object_and_get_next_item(source_object)
        if success:
            if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled:
                gsi_handlers.live_drag_handlers.archive_live_drag('End', 'Operation', LiveDragLocation.GAMEPLAY_SCRIPT, end_system, live_drag_object_id=source_object_id, live_drag_target=target_object)
            if not self._live_drag_is_stack:
                next_object_id = None
            op = distributor.ops.LiveDragEnd(source_object_id, self._live_drag_start_system, end_system, next_object_id)
            distributor_system = Distributor.instance()
            distributor_system.add_op_with_no_owner(op)
            self._live_drag_objects = []
            self._live_drag_start_system = LiveDragLocation.INVALID
            self._live_drag_is_stack = False
        else:
            self.send_live_drag_cancel(source_object_id, end_system)

    def cancel_live_drag(self, live_drag_object, end_system=LiveDragLocation.INVALID):
        live_drag_component = live_drag_object.live_drag_component
        if live_drag_component is None:
            logger_live_drag.warn('Live Drag Cancel called on an object with no Live Drag Component. Object: {}'.format(live_drag_object))
            self.send_live_drag_cancel(live_drag_object.id)
            return
        if live_drag_component.live_drag_state == LiveDragState.NOT_LIVE_DRAGGING:
            logger_live_drag.warn('Live Drag Cancel called on an object not being Live Dragged. Object: {}'.format(live_drag_object))
        else:
            self.cancel_live_drag_on_objects()
        self.send_live_drag_cancel(live_drag_object.id, end_system)

    def sell_live_drag_object(self, live_drag_object, end_system=LiveDragLocation.INVALID):
        live_drag_component = live_drag_object.live_drag_component
        if live_drag_component is None or not live_drag_object.definition.get_is_deletable():
            logger_live_drag.error("Live Drag Sell called on object with no Live Drag Component or can't be deleted. Object: {}".format(live_drag_object))
            self.send_live_drag_cancel(live_drag_object.id, end_system)
            return

        def sell_response(dialog):
            if not dialog.accepted:
                return
            value = int(self.get_live_drag_object_value(live_drag_object, self._live_drag_is_stack))
            live_drag_component.cancel_live_dragging(should_reset=False)
            object_tags = set()
            if self._live_drag_is_stack:
                (_, stack_items) = self._get_stack_items_from_drag_object(live_drag_object, remove=True, is_stack=True)
                for item in stack_items:
                    item.current_value = 0
                    item.set_stack_count(0)
                    object_tags.update(item.get_tags())
                    item.destroy(source=item, cause='Selling stack of live drag objects.')
            else:
                object_tags.update(live_drag_object.get_tags())
                if live_drag_object.is_in_inventory():
                    self.remove_drag_object_and_get_next_item(live_drag_object)
                else:
                    live_drag_object.remove_from_client()
            object_tags = frozenset(object_tags)
            live_drag_object.current_value = 0
            live_drag_object.destroy(source=live_drag_object, cause='Selling live drag object.')
            services.active_household().funds.add(value, Consts_pb2.TELEMETRY_OBJECT_SELL, self.active_sim, tags=object_tags)
            self._live_drag_objects = []
            self._live_drag_start_system = LiveDragLocation.INVALID
            self._live_drag_is_stack = False
            self._live_drag_sell_dialog_active = False

        if self._live_drag_is_stack:
            dialog = LiveDragTuning.LIVE_DRAG_SELL_STACK_DIALOG(owner=live_drag_object)
        else:
            dialog = LiveDragTuning.LIVE_DRAG_SELL_DIALOG(owner=live_drag_object)
        dialog.show_dialog(on_response=sell_response)
        self._live_drag_sell_dialog_active = True

    def send_live_drag_cancel(self, live_drag_object_id, live_drag_end_system=LiveDragLocation.INVALID):
        if gsi_handlers.live_drag_handlers.live_drag_archiver.enabled:
            gsi_handlers.live_drag_handlers.archive_live_drag('Cancel', 'Operation', LiveDragLocation.GAMEPLAY_SCRIPT, live_drag_end_system, live_drag_object_id=live_drag_object_id)
        op = distributor.ops.LiveDragCancel(live_drag_object_id, self._live_drag_start_system, live_drag_end_system)
        distributor_system = Distributor.instance()
        distributor_system.add_op_with_no_owner(op)
        if not self._live_drag_sell_dialog_active:
            self._live_drag_objects = []
            self._live_drag_start_system = LiveDragLocation.INVALID
            self._live_drag_is_stack = False

    def on_add(self):
        if self._account is not None:
            self._account.register_client(self)
        for sim_info in self._selectable_sims:
            self.on_sim_added_to_skewer(sim_info)
        distributor = Distributor.instance()
        distributor.add_object(self)
        distributor.add_client(self)
        self.send_selectable_sims_update()
        self.selectable_sims.add_watcher(self, self.send_selectable_sims_update)

    def on_remove(self):
        if self.active_sim is not None:
            self._set_active_sim_without_field_distribution(None)
        if self._account is not None:
            self._account.unregister_client(self)
        for sim_info in self._selectable_sims:
            self.on_sim_removed_from_skewer(sim_info, update_relationship_tracker=False)
        self.selectable_sims.remove_watcher(self)
        distributor = Distributor.instance()
        distributor.remove_client(self)
        self._selectable_sims = None
        self.active = False

    def get_objects_in_view_gen(self):
        for manager in services.client_object_managers():
            for obj in manager.get_all():
                yield obj

    def notify_active_sim_changed(self, old_sim, new_sim):
        self._active_sim_changed(old_sim, new_sim)

    def _get_selector_visual_type(self, sim_info):
        if sim_info.is_baby:
            return (Sims_pb2.SimPB.BABY, None)
        for career in sim_info.career_tracker.careers.values():
            if career.currently_at_work:
                return (Sims_pb2.SimPB.AT_WORK, career.career_category)
            while career.is_late:
                return (Sims_pb2.SimPB.LATE_FOR_WORK, career.career_category)
        sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is not None and sim.has_hidden_flags(HiddenReasonFlag.RABBIT_HOLE):
            return (Sims_pb2.SimPB.OTHER, None)
        return (Sims_pb2.SimPB.NORMAL, None)

    def send_selectable_sims_update(self):
        msg = Sims_pb2.UpdateSelectableSims()
        for sim_info in self._selectable_sims:
            with ProtocolBufferRollback(msg.sims) as new_sim:
                new_sim.id = sim_info.sim_id
                new_sim.at_work = sim_info.career_tracker.currently_at_work
                new_sim.is_selectable = sim_info.is_enabled_in_skewer
                (selector_visual_type, career_category) = self._get_selector_visual_type(sim_info)
                new_sim.selector_visual_type = selector_visual_type
                if career_category is not None:
                    new_sim.career_category = career_category
                while not sim_info.is_instanced(allow_hidden_flags=ALL_HIDDEN_REASONS):
                    new_sim.instance_info.zone_id = sim_info.zone_id
                    new_sim.instance_info.world_id = sim_info.world_id
                    new_sim.firstname = sim_info.first_name
                    new_sim.lastname = sim_info.last_name
                    zone_data_proto = services.get_persistence_service().get_zone_proto_buff(sim_info.zone_id)
                    while zone_data_proto is not None:
                        new_sim.instance_info.zone_name = zone_data_proto.name
        distributor = Distributor.instance()
        distributor.add_op_with_no_owner(GenericProtocolBufferOp(Operation.SELECTABLE_SIMS_UPDATE, msg))

    @property
    def is_sim(self):
        return False