class UiDialogObjectPicker(UiDialogOkCancel): __qualname__ = 'UiDialogObjectPicker' FACTORY_TUNABLES = { 'max_selectable': TunableVariant( description= '\n Method of determining maximum selectable items.\n ', static_count=TunableTuple( description= '\n static maximum selectable\n ', number_selectable=TunableRange( description= '\n Maximum items selectable\n ', tunable_type=int, default=1, minimum=1), locked_args={'max_type': MaxSelectableType.STATIC}), unlimited=TunableTuple( description= '\n Unlimited Selectable\n ', locked_args={'max_type': MaxSelectableType.NONE}), slot_based_count=TunableTuple( description= '\n maximum selectable based on empty/full slots on target\n ', slot_type=SlotType.TunableReference( description= ' \n A particular slot type to be tested.\n ' ), require_empty=Tunable( description= '\n based on empty slots\n ', tunable_type=bool, default=True), delta=Tunable( description= '\n offset from number of empty slots\n ', tunable_type=int, default=0), locked_args={'max_type': MaxSelectableType.SLOT_COUNT}), default='static_count'), 'min_selectable': TunableRange( description= '\n The minimum number of items that must be selected to treat the\n dialog as accepted and push continuations. If 0, then multi-select\n sim pickers will push continuations even if no items are selected.\n ', tunable_type=int, default=1, minimum=0), 'is_sortable': Tunable( description= '\n Should list of items be presented sorted\n ', tunable_type=bool, default=False), 'hide_row_description': Tunable( description= '\n If set to True, we will not show the row description for this picker dialog.\n ', tunable_type=bool, default=False) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.picker_rows = [] self.picked_results = [] self.target_sim = None self.target = None self.ingredient_check = None def add_row(self, row): if row is None: return if not self._validate_row(row): return if row.option_id is None: row.option_id = len(self.picker_rows) self._customize_add_row(row) self.picker_rows.append(row) def _validate_row(self, row): raise NotImplementedError def _customize_add_row(self, row): pass def set_target_sim(self, target_sim): self.target_sim = target_sim def set_target(self, target): self.target = target def pick_results(self, picked_results=[], ingredient_check=None): option_ids = [picker_row.option_id for picker_row in self.picker_rows] for result in picked_results: while result not in option_ids: logger.error( 'Player choose {0} out of provided {1} for dialog {2}', picked_results, option_ids, self) return False self.picked_results = picked_results self.ingredient_check = ingredient_check return True def get_result_rows(self): return [ row for row in self.picker_rows if row.option_id in self.picked_results ] def get_result_tags(self): return [row.tag for row in self.get_result_rows()] def get_single_result_tag(self): tags = self.get_result_tags() if not tags: return if len(tags) != 1: raise ValueError('Multiple selections not supported') return tags[0] def build_msg(self, **kwargs): msg = super().build_msg(**kwargs) msg.dialog_type = Dialog_pb2.UiDialogMessage.OBJECT_PICKER msg.picker_data = self.build_object_picker() return msg def _build_customize_picker(self, picker_data): raise NotImplementedError def build_object_picker(self): picker_data = Dialog_pb2.UiDialogPicker() picker_data.title = self._build_localized_string_msg(self.title) if self.picker_type is not None: picker_data.type = self.picker_type if self.max_selectable: if self.max_selectable.max_type == MaxSelectableType.STATIC: picker_data.max_selectable = self.max_selectable.number_selectable picker_data.multi_select = True elif self.max_selectable.max_type == MaxSelectableType.SLOT_COUNT: if self.target is not None: get_slots = self.target.get_runtime_slots_gen( slot_types={self.max_selectable.slot_type}, bone_name_hash=None) if self.max_selectable.require_empty: picker_data.max_selectable = sum(1 for slot in get_slots if slot.empty) else: picker_data.max_selectable = sum(1 for slot in get_slots if not slot.empty) picker_data.multi_select = True else: logger.error( 'attempting to use slot based picker without a target object for dialog: {}', self, owner='nbaker') picker_data.multi_select = True else: picker_data.multi_select = True else: picker_data.multi_select = True picker_data.owner_sim_id = self.owner.sim_id if self.target_sim is not None: picker_data.target_sim_id = self.target_sim.sim_id picker_data.is_sortable = self.is_sortable picker_data.hide_row_description = self.hide_row_description self._build_customize_picker(picker_data) return picker_data
class DeliverBabyOnSurgeryTableInteraction(DeliverBabySuperInteraction): INSTANCE_TUNABLES = { 'bassinet_to_use': TunableReference( description= '\n Bassinet with Baby object definition id.\n ', manager=services.definition_manager()), 'bassinet_slot_type': SlotType.TunableReference( description= '\n SlotType used to place the bassinet when it is created.\n ' ), 'surgery_table_participant_type': TunableEnumEntry( description= '\n A reference to the ParticipantType that the surgery table will be\n in this interaction.\n ', tunable_type=ParticipantType, default=ParticipantType.Object), '_loot_per_baby': TunableList( description= '\n Loot that will be applied when a baby is born to a non NPC Sim. \n Actor will be the mom and TargetSim will be the sim_info of the baby.\n This will work for multiple babies as each loot will be applied to \n each baby sim info.\n \n None of these loots will be applied to a baby born to an NPC Sim.\n ', tunable=LootActions.TunableReference(pack_safe=True)), 'destroy_baby_in_bassinet_vfx': PlayEffect.TunableFactory( description= '\n The VFX to play when the baby is being destroyed during an NPC \n birth sequence.\n ' ), 'after_delivery_interaction': TunablePackSafeReference( description= '\n The interaction to push on the Sim after delivering a baby at the\n hospital.\n ', manager=services.affordance_manager(), allow_none=True) } FADE_BASSINET_EVENT_TAG = 201 DESTROY_BASSINET_EVENT_TAG = 202 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_exit_function(self._destroy_sim_info_if_neccesary) self._bassinet = None self._baby_object = None def _pre_perform(self, *args, **kwargs): bassinet_def = BabyTuning.get_corresponding_definition( self.bassinet_to_use) self._bassinet = create_object(bassinet_def) surgery_table = self.get_participant( self.surgery_table_participant_type) if surgery_table is None: logger.error('No surgery table found for {}', self, owner='tastle') return False surgery_table.part_owner.slot_object(self.bassinet_slot_type, self._bassinet) return True def _build_outcome_sequence(self): sequence = super()._build_outcome_sequence() return build_element( (self._handle_npc_bassinet_fade_and_effect, sequence)) def _handle_npc_bassinet_fade_and_effect(self, timeline): if self.sim.is_npc: def _fade_bassinet(_): if self._baby_object is not None: self._baby_object.fade_out() effect = self.destroy_baby_in_bassinet_vfx( self._baby_object) effect.start_one_shot() self.store_event_handler(_fade_bassinet, handler_id=self.FADE_BASSINET_EVENT_TAG) def _destroy_sim_info_if_neccesary(self): if self.sim.is_npc and self._baby_object is not None: baby_sim_info = self._baby_object.sim_info if self._parent_is_playable(baby_sim_info): baby_sim_info.inject_into_inactive_zone( baby_sim_info.household.home_zone_id) else: baby_sim_info.remove_permanently( household=baby_sim_info.household) def _parent_is_playable(self, sim_info): for parent_info in sim_info.genealogy.get_parent_sim_infos_gen(): if parent_info is not None: if parent_info.is_player_sim: return True return False def _complete_pregnancy_gen(self, timeline, pregnancy_tracker): offspring_data_list = list(pregnancy_tracker.get_offspring_data_gen()) new_baby = self._create_new_bassinet_with_baby(pregnancy_tracker, offspring_data_list[0], self._bassinet) self._bassinet.destroy() self._bassinet = None self._reset_actor_in_asms(new_baby, 'bassinet') self.interaction_parameters['created_target_id'] = new_baby.id self._baby_object = new_baby sim_infos = [] if not self.sim.is_npc or self._parent_is_playable(new_baby.sim_info): sim_infos = self._create_additional_babies( pregnancy_tracker, offspring_data_list[1:], position=new_baby.position, routing_surface=new_baby.routing_surface, create_bassinet=not self.sim.is_npc) sim_infos.append(new_baby.sim_info) if not self.sim.is_npc: self._apply_per_baby_loot(sim_infos) self._apply_inherited_loots(sim_infos, pregnancy_tracker) self._push_post_delivery_interaction(sim_infos) pregnancy_tracker.complete_pregnancy() return True yield def _push_post_delivery_interaction(self, sim_infos): if self._baby_object is not None and self.after_delivery_interaction is not None: def _destroy_all_bassinets(): for sim_info in sim_infos: bassinet_object = services.object_manager().get( sim_info.sim_id) if bassinet_object is not None: bassinet_object.make_transient() liabilities = (( CLEANUP_INTERACTION_CALLBACK_LIABILITY, CleanupInteractionCallbackLiability( cleanup_interaction_callback=_destroy_all_bassinets)), ) context = self.context.clone_for_continuation(self) self.sim.push_super_affordance(self.after_delivery_interaction, self._baby_object, context, liabilities=liabilities) def _apply_per_baby_loot(self, sim_infos): for sim_info in sim_infos: resolver = DoubleSimResolver(self.sim.sim_info, sim_info) for loot in self._loot_per_baby: loot.apply_to_resolver(resolver)
class RetailComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.RETAIL_COMPONENT, persistence_priority=ComponentPriority.PRIORITY_RETAIL, persistence_key=SimObjectAttributes_pb2. PersistenceMaster.PersistableData.RetailComponent): FACTORY_TUNABLES = { 'sellable': OptionalTunable( description= '\n If enabled, this object can be sold on a retail lot.\n ', disabled_name='Not_For_Sale', enabled_by_default=True, tunable=TunableTuple( description= '\n The data associated with selling this item.\n ', placard_override=OptionalTunable( description= '\n If enabled, we will only use this placard when this object\n is sold. If disabled, the game will attempt to smartly\n choose a placard.\n ', tunable=TunableTuple( description= '\n The placard and vfx to use for this object.\n ', model=TunablePackSafeReference( description= '\n The placard to use when this object is sold.\n ', manager=services.definition_manager()), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' ))), for_sale_extra_affordances=TunableList( description= "\n When this object is marked For Sale, these are the extra\n interactions that will still be available. For instance, a\n debug interaction to sell the object could go here or you\n may want Sit to still be on chairs so customers can try\n them out. You may also want Clean on anything that can get\n dirty so employees can clean them.\n \n Note: These interactions are specific to this object. Do not\n add interactions that don't make sense for this object.\n ", tunable=TunableReference( description= '\n The affordance to be left available on the object.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True)), buy_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to buy retail objects. This\n affordance should handle destroying the object and adding\n the money to the retail funds.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), restock_affordances=TunableList( description= '\n The affordances a Sim will run to restock retail objects.\n ', tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), pack_safe=True)), clear_placard_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to remove a placard from the lot.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=True), browse_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to browse retail objects.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), set_for_sale_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to set a retail object as For\n Sale.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=True), set_not_for_sale_affordance=TunablePackSafeReference( description= '\n The affordance a Sim will run to set a retail object as Not\n For Sale.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION), allow_none=True), allowed_occupied_slots_for_sellable=TunableSet( description= '\n By default, an object will become unsellable if anything is\n parented to it. This is to prevent awkward scenarios such\n as selling a counter with a sink is parented, leaving the\n sink floating in mid-air. However, some slots on objects\n are okay occupied, e.g. a table should still be sellable if\n chairs are parented. Adding those slots to this set (e.g.\n slot_SitChair on tables) will allow this object to remain\n sellable even if those slots are occupied.\n ', tunable=SlotType.TunableReference()))), 'advertises': OptionalTunable( description= '\n If enabled, this object will contribute to the curb appeal for the\n retail lot on which it is placed. Curb appeal affects foot traffic\n for the store as well as the type of clientele.\n ', disabled_name='Does_Not_Advertise', enabled_by_default=True, tunable=RetailCurbAppeal.TunableFactory()) } NOT_FOR_SALE_STATE = TunableStateValueReference( description= '\n The state value that represents and object, on a retail lot, that is\n not for sale at all. Objects in this state should function like normal.\n ', pack_safe=True) FOR_SALE_STATE = TunableStateValueReference( description= '\n The state value that represents an object that is valid for sale on a\n retail lot. This is the state that will be tested in order to show sale\n interactions.\n ', pack_safe=True) SOLD_STATE = TunableStateValueReference( description= "\n The state value that represents an object that is no longer valid for\n sale on a retail lot. This is the state that will be set on the object\n when it's sold and in its Placard form.\n ", pack_safe=True) DEFAULT_SALE_STATE = TunableStateValueReference( description= '\n This is the default for sale state for an object. When it is given a\n retail component for this first time, this is what the For Sale state\n will be set to.\n ', pack_safe=True) SET_FOR_SALE_VFX = PlayEffect.TunableFactory( description= '\n An effect that will play on an object when it gets set for sale.\n ' ) SET_NOT_FOR_SALE_VFX = PlayEffect.TunableFactory( description= '\n An effect that will play on an object when it gets set not for sale.\n ' ) PLACARD_FLOOR = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on the floor\n when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) PLACARD_SURFACE = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on a surface\n when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) PLACARD_WALL = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on the wall\n when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) PLACARD_CEILING = TunableTuple( description= '\n The placard to use, and vfx to show, for objects that were on the\n ceiling when sold.\n ', model=TunableReference( description= '\n The placard to use when the object is sold.\n ', manager=services.definition_manager(), pack_safe=True), vfx=PlayEffect.TunableFactory( description= '\n The effect to play when the object is sold.\n ' )) UNINTERACTABLE_AUTONOMY_MODIFIER = TunableAutonomyModifier( description= '\n Autonomy modifier that disables interactions on this object. Applied\n to this object and its children when marked as sellable or sold.\n ', locked_args={'relationship_multipliers': None}) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._cached_value = None self._uninteractable_autonomy_modifier_handle = None @classproperty def required_packs(cls): return (Pack.EP01, ) def on_add(self): if self.owner.state_component is None: self.owner.add_component( StateComponent(self.owner, states=(), state_triggers=(), unique_state_changes=None, delinquency_state_changes=None, timed_state_triggers=None)) self.owner.set_state_dynamically(state=self.DEFAULT_SALE_STATE.state, new_value=self.DEFAULT_SALE_STATE, seed_value=self.SOLD_STATE) self.owner.update_component_commodity_flags() def save(self, persistence_master_message): persistable_data = SimObjectAttributes_pb2.PersistenceMaster.PersistableData( ) persistable_data.type = SimObjectAttributes_pb2.PersistenceMaster.PersistableData.RetailComponent retail_data = persistable_data.Extensions[ SimObjectAttributes_pb2.PersistableRetailComponent. persistable_data] if self._cached_value is not None: retail_data.cached_value = self._cached_value persistence_master_message.data.extend([persistable_data]) def load(self, persistence_master_message): retail_data = persistence_master_message.Extensions[ SimObjectAttributes_pb2.PersistableRetailComponent. persistable_data] if retail_data.HasField('cached_value'): self._cached_value = retail_data.cached_value def get_current_curb_appeal(self): curb_appeal = 0 if self.advertises: curb_appeal = self.advertises.base_curb_appeal curb_appeal += sum([ curb_appeal for (state, curb_appeal ) in self.advertises.state_based_curb_appeal.items() if self.owner.state_value_active(state) ]) return curb_appeal def on_state_changed(self, state, old_value, new_value, from_init): reapply_state = False if old_value is new_value: if state is not self.FOR_SALE_STATE.state: return reapply_state = True retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: return if new_value is self.NOT_FOR_SALE_STATE or new_value is self.DEFAULT_SALE_STATE: show_vfx = not reapply_state and ( old_value is self.FOR_SALE_STATE or old_value is self.DEFAULT_SALE_STATE) if show_vfx: if self.owner.swapping_to_parent is not None: if self.FOR_SALE_STATE in self.owner.swapping_to_parent.slot_component.state_values: show_vfx = False self._set_not_for_sale_internal(show_vfx=show_vfx) elif new_value is self.FOR_SALE_STATE: show_vfx = not reapply_state and ( old_value is self.NOT_FOR_SALE_STATE or old_value is self.DEFAULT_SALE_STATE) if show_vfx: if self.owner.swapping_to_parent is not None: if self.NOT_FOR_SALE_STATE in self.owner.swapping_to_parent.slot_component.state_values: show_vfx = False self._set_for_sale_internal(show_vfx=show_vfx) if old_value is self.SOLD_STATE: self.owner.remove_client_state_suppressor(state) self.owner.reset_states_to_default() elif new_value is self.SOLD_STATE: self.owner.add_client_state_suppressor(self.SOLD_STATE.state) self._set_sold_internal() self.owner.update_component_commodity_flags() def on_added_to_inventory(self): inventory = self.owner.get_inventory() if inventory is None: return if inventory.inventory_type in RetailUtils.RETAIL_INVENTORY_TYPES: if not self.is_sold: self.set_for_sale() else: if self.owner.get_state( self.DEFAULT_SALE_STATE.state) == self.DEFAULT_SALE_STATE: return if not self.is_sold: self.set_not_for_sale() def on_child_added(self, child, location): if self.is_for_sale and not self.is_allowed_slot(location.slot_hash): self.set_not_for_sale() child_retail = child.retail_component if child_retail is not None and self.is_for_sale_or_sold: child_retail.set_uninteractable() def on_child_removed(self, child, new_parent=None): child_retail = child.retail_component if child_retail is not None: child_retail.set_interactable(from_unparent=True) def on_finalize_load(self): self.owner.update_component_commodity_flags() retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: if self.is_sold or self.is_for_sale: self.set_not_for_sale() elif self.is_sold: self.owner.add_client_state_suppressor(self.SOLD_STATE.state) @property def is_for_sale(self): return self.owner.state_value_active(self.FOR_SALE_STATE) @property def is_sold(self): return self.owner.state_value_active(self.SOLD_STATE) @property def is_not_for_sale(self): return self.owner.state_value_active( self.NOT_FOR_SALE_STATE) or self.owner.state_value_active( self.DEFAULT_SALE_STATE) @property def is_for_sale_or_sold(self): return not self.is_not_for_sale def get_retail_value(self): obj = self.owner crafting_component = obj.get_component(types.CRAFTING_COMPONENT) if crafting_component is None: return obj.catalog_value if self._cached_value is None: return obj.current_value state_component = obj.state_component if state_component is not None: return max( round(self._cached_value * state_component.state_based_value_mod), 0) else: return self._cached_value def get_sell_price(self): base_value = self.get_retail_value() retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( "Trying to get the sell price of a retail item but no retail_manager was found for this lot. Defaulting to the object's value without a markup applied." ) return base_value return retail_manager.get_value_with_markup(base_value) def get_buy_affordance(self): sellable_data = self.sellable if sellable_data is not None: return sellable_data.buy_affordance def is_allowed_slot(self, slot_hash): if not self.sellable: return False for runtime_slot in self.owner.get_runtime_slots_gen( bone_name_hash=slot_hash): if self.sellable.allowed_occupied_slots_for_sellable & runtime_slot.slot_types: return True return False def get_can_be_sold(self): if not self.sellable: return False elif any(not self.is_allowed_slot(child.slot_hash) for child in self.owner.children): return False return True def set_not_for_sale(self): self.owner.set_state(self.NOT_FOR_SALE_STATE.state, self.NOT_FOR_SALE_STATE) def set_for_sale(self): self.owner.set_state(self.FOR_SALE_STATE.state, self.FOR_SALE_STATE) def _set_sold_internal(self): retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( 'Trying to set an item as sold but the retail manager is None.' ) return item = self.owner self._handle_children_of_sold_object(item) self._change_to_placard() item.on_set_sold() def set_uninteractable(self, propagate_to_children=False): if self._uninteractable_autonomy_modifier_handle is not None: return self._uninteractable_autonomy_modifier_handle = self.owner.add_statistic_modifier( self.UNINTERACTABLE_AUTONOMY_MODIFIER) if propagate_to_children: for child in self.owner.children: retail_component = child.retail_component if retail_component is not None: child.retail_component.set_uninteractable( propagate_to_children=False) def set_interactable(self, propagate_to_children=False, from_unparent=False): if self._uninteractable_autonomy_modifier_handle is None: return if not from_unparent: parent = self.owner.parent if parent is not None: parent_retail = parent.retail_component if parent_retail is not None and parent_retail.is_for_sale_or_sold: return self.owner.remove_statistic_modifier( self._uninteractable_autonomy_modifier_handle) self._uninteractable_autonomy_modifier_handle = None if propagate_to_children: for child in self.owner.children: retail_component = child.retail_component if retail_component is not None: if not retail_component.is_for_sale_or_sold: child.retail_component.set_interactable( propagate_to_children=False) def _set_not_for_sale_internal(self, show_vfx=True): self.set_interactable(propagate_to_children=True) retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: return retail_manager.refresh_for_sale_vfx_for_object(self.owner) if show_vfx: self.SET_NOT_FOR_SALE_VFX(self.owner).start_one_shot() def _set_for_sale_internal(self, show_vfx=True): self.set_uninteractable(propagate_to_children=True) item = self.owner if item.standin_model is not None: item.standin_model = None item.on_restock() if self._cached_value is not None: item.base_value = self._cached_value self._cached_value = None retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( "Trying to set an item For Sale but it's not on a valid retail lot." ) return False retail_manager.refresh_for_sale_vfx_for_object(self.owner) if show_vfx: self.SET_FOR_SALE_VFX(self.owner).start_one_shot() return True def _choose_placard_info(self): placard_override = self.sellable.placard_override if placard_override is not None: if placard_override.model is not None: return placard_override logger.error( 'Object [{}] has a placard override enabled on the retail component [{}] but the placard override has no model set. We will attempt to pick the correct placard.', self.owner, self) item = self.owner if item.parent is not None: return self.PLACARD_SURFACE if item.wall_or_fence_placement: return self.PLACARD_WALL if item.ceiling_placement: return self.PLACARD_CEILING else: return self.PLACARD_FLOOR def _change_to_placard(self, play_vfx=True): item = self.owner if self._cached_value is None: self._cached_value = item.base_value item.base_value = 0 placard_info = self._choose_placard_info() item.standin_model = placard_info.model.definition.get_model(0) item.set_state(self.SOLD_STATE.state, self.SOLD_STATE) if play_vfx: effect = placard_info.vfx(item) effect.start_one_shot() retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: logger.error( "Trying to change a retail object to a placard but it's not on a valid retail lot." ) return False retail_manager.refresh_for_sale_vfx_for_object(self.owner) def _handle_children_of_sold_object(self, obj): retail_manager = services.business_service( ).get_retail_manager_for_zone() active_household_is_owner = retail_manager.is_owner_household_active should_show_notification = False for child in tuple(obj.children): if child.has_component(types.RETAIL_COMPONENT): child.retail_component.set_not_for_sale() if active_household_is_owner: should_show_notification = True build_buy.move_object_to_household_inventory(child) else: child.schedule_destroy_asap() if should_show_notification: notification = retail_manager.ITEMS_SENT_TO_HH_INVENTORY_NOTIFICATION( obj) notification.show_dialog() def component_super_affordances_gen(self, **kwargs): retail_manager = services.business_service( ).get_retail_manager_for_zone() if retail_manager is None: return sellable_data = self.sellable if sellable_data is not None: if self.is_for_sale: yield from sellable_data.for_sale_extra_affordances if sellable_data.set_not_for_sale_affordance is not None: yield sellable_data.set_not_for_sale_affordance if retail_manager.is_open: if sellable_data.buy_affordance is not None: yield sellable_data.buy_affordance if sellable_data.browse_affordance is not None: yield sellable_data.browse_affordance elif self.is_not_for_sale: if sellable_data.set_for_sale_affordance is not None and self.get_can_be_sold( ): yield sellable_data.set_for_sale_affordance elif self.is_sold: yield from sellable_data.restock_affordances if sellable_data.clear_placard_affordance is not None: yield sellable_data.clear_placard_affordance def modify_interactable_flags(self, interactable_flag_field): if self.is_for_sale: if not self.is_sold: interactable_flag_field.flags |= interaction_protocol.Interactable.FORSALE
class ScriptObject(BaseObject, HasStatisticComponent, HasFootprintComponent, metaclass=HashedTunedInstanceMetaclass, manager=services.definition_manager()): __qualname__ = 'ScriptObject' INSTANCE_TUNABLES = { '_super_affordances': TunableList( description= '\n Super affordances on this object.\n ', tunable=TunableReference( description= '\n A super affordance on this object.\n ', manager=services.affordance_manager(), class_restrictions=('SuperInteraction', ), pack_safe=True)), '_part_data': TunableList( description= '\n Use this to define parts for an object. Parts allow multiple Sims to\n use an object in different or same ways, at the same time. The model\n and the animations for this object will have to support parts.\n Ensure this is the case with animation and modeling.\n \n There will be one entry in this list for every part the object has.\n \n e.g. The bed has six parts (two sleep parts, and four sit parts).\n add two entries for the sleep parts add four entries for the sit\n parts\n ', tunable=TunableTuple( description= '\n Data that is specific to this part.\n ', part_definition=TunableReference( description= '\n The part definition data.\n ', manager=services.object_part_manager()), subroot_index=OptionalTunable( description= '\n If enabled, this part will have a subroot index associated\n with it. This will affect the way Sims animate, i.e. they\n will animate relative to the position of the part, not\n relative to the object.\n ', tunable=Tunable( description= '\n The subroot index/suffix associated to this part.\n ', tunable_type=int, default=0, needs_tuning=False), enabled_by_default=True), overlapping_parts=TunableList( description= "\n The indices of parts that are unusable when this part is in\n use. The index is the zero-based position of the part within\n the object's Part Data list.\n ", tunable=int), adjacent_parts=OptionalTunable( description= '\n Define adjacent parts. If disabled, adjacent parts will be\n generated automatically based on indexing. If enabled,\n adjacent parts must be specified here.\n ', tunable=TunableList( description= "\n The indices of parts that are adjacent to this part. The\n index is the zero-based position of the part within the\n object's Part Data list.\n \n An empty list indicates that no part is ajdacent to this\n part.\n ", tunable=int)), is_mirrored=OptionalTunable( description= '\n Specify whether or not solo animations played on this part\n should be mirrored or not.\n ', tunable=Tunable( description= '\n If checked, mirroring is enabled. If unchecked,\n mirroring is disabled.\n ', tunable_type=bool, default=False)), forward_direction_for_picking=TunableVector2( description= "\n When you click on the object this part belongs to, this\n offset will be applied to this part when determining which\n part is closest to where you clicked. By default, the\n object's forward vector will be used. It should only be\n necessary to tune this value if multiple parts overlap at\n the same location (e.g. the single bed).\n ", default=sims4.math.Vector2(0, 1), x_axis_name='x', y_axis_name='z'), disable_sim_aop_forwarding=Tunable( description= '\n If checked, Sims using this specific part will never forward\n AOPs.\n ', tunable_type=bool, default=False), disable_child_aop_forwarding=Tunable( description= '\n If checked, objects parented to this specific part will\n never forward AOPs.\n ', tunable_type=bool, default=False), anim_overrides=TunableAnimationOverrides( description='Animation overrides for this part.'))), 'custom_posture_target_name': Tunable( description= '\n An additional non-virtual actor to set for this object when used as\n a posture target.\n \n This tunable is used when the object has parts. In most cases, the\n state machines will only have one actor for the part that is\n involved in animation. In that case, this field should not be set.\n \n e.g. The Sit posture requires the sitTemplate actor to be set, but\n does not make a distinction between, for instance, Chairs and Sofas,\n because no animation ever involves the whole object.\n \n However, there may be cases when, although we are dealing with\n parts, the animation will need to also reference the entire object.\n In that case, the ASM will have an extra actor to account for the\n whole object, in addition to the part. Set this field to be that\n actor name.\n \n e.g. The Sleep posture on the bed animates the Sim on one part.\n However, the sheets and pillows need to animate on the entire bed.\n In that case, we need to set this field on Bed so that the state\n machine can have this actor set.\n ', tunable_type=str, default=None), 'posture_transition_target_tag': TunableEnumEntry( description= '\n A tag to apply to this script object so that it is taken into\n account for posture transition preference scoring. For example, you\n could tune this object (and others) to be a DINING_SURFACE. Any SI\n that is set up to have posture preference scoring can override the\n score for any objects that are tagged with DINING_SURFACE.\n \n For a more detailed description of how posture preference scoring\n works, see the posture_target_preference tunable field description\n in SuperInteraction.\n ', tunable_type=postures.PostureTransitionTargetPreferenceTag, default=postures.PostureTransitionTargetPreferenceTag.INVALID), '_anim_overrides': OptionalTunable( description= '\n If enabled, specify animation overrides for this object.\n ', tunable=TunableAnimationObjectOverrides()), '_focus_score': TunableEnumEntry( description= '\n Determines how likely a Sim is to look at this object when focusing\n ambiently. A higher value means this object is more likely to draw\n Sim focus.\n ', tunable_type=FocusInterestLevel, default=FocusInterestLevel.LOW, needs_tuning=True), 'social_clustering': OptionalTunable( description= '\n If enabled, specify how this objects affects clustering for\n preferred locations for socialization.\n ', tunable=TunableTuple(is_datapoint=Tunable( description= '\n Whether or not this object is a data point for social\n clusters.\n ', tunable_type=bool, default=True))), '_should_search_forwarded_sim_aop': Tunable( description= "\n If enabled, interactions on Sims using this object will appear in\n this object's pie menu as long as they are also tuned to allow\n forwarding.\n ", tunable_type=bool, default=False), '_should_search_forwarded_child_aop': Tunable( description= "\n If enabled, interactions on children of this object will appear in\n this object's pie menu as long as they are also tuned to allow\n forwarding.\n ", tunable_type=bool, default=False), '_disable_child_footprint_and_shadow': Tunable( description= "\n If checked, all objects parented to this object will have their\n footprints and dropshadows disabled.\n \n Example Use: object_sim has this checked so when a Sim picks up a\n plate of food, the plate's footprint and dropshadow turn off\n temporarily.\n ", tunable_type=bool, default=False), 'disable_los_reference_point': Tunable( description= '\n If checked, goal points for this interaction will not be discarded\n if a ray-test from the object fails to connect without intersecting\n walls or other objects. The reason for allowing this, is for\n objects like the door where we want to allow the sim to interact\n with the object, but since the object doesnt have a footprint we\n want to allow him to use the central point as a reference point and\n not fail the LOS test.\n ', tunable_type=bool, default=False), '_components': TunableTuple( description= '\n The components that instances of this object should have.\n ', tuning_group=GroupNames.COMPONENTS, affordance_tuning=OptionalTunable( AffordanceTuningComponent.TunableFactory()), autonomy=OptionalTunable(TunableAutonomyComponent()), canvas=OptionalTunable(CanvasComponent.TunableFactory()), carryable=OptionalTunable(TunableCarryableComponent()), censor_grid=OptionalTunable(TunableCensorGridComponent()), collectable=OptionalTunable(CollectableComponent.TunableFactory()), consumable=OptionalTunable(ConsumableComponent.TunableFactory()), crafting_station=OptionalTunable( CraftingStationComponent.TunableFactory()), fishing_location=OptionalTunable( FishingLocationComponent.TunableFactory()), flowing_puddle=OptionalTunable( FlowingPuddleComponent.TunableFactory()), game=OptionalTunable(TunableGameComponent()), gardening_component=TunableGardeningComponent(), idle_component=OptionalTunable(IdleComponent.TunableFactory()), inventory=OptionalTunable( ObjectInventoryComponent.TunableFactory()), inventory_item=OptionalTunable( InventoryItemComponent.TunableFactory()), lighting=OptionalTunable(LightingComponent.TunableFactory()), line_of_sight=OptionalTunable(TunableLineOfSightComponent()), live_drag_target=OptionalTunable( LiveDragTargetComponent.TunableFactory()), name=OptionalTunable(NameComponent.TunableFactory()), object_age=OptionalTunable(TunableObjectAgeComponent()), object_relationships=OptionalTunable( ObjectRelationshipComponent.TunableFactory()), object_teleportation=OptionalTunable( ObjectTeleportationComponent.TunableFactory()), ownable_component=OptionalTunable( OwnableComponent.TunableFactory()), proximity_component=OptionalTunable( ProximityComponent.TunableFactory()), spawner_component=OptionalTunable( SpawnerComponent.TunableFactory()), state=OptionalTunable(TunableStateComponent()), time_of_day_component=OptionalTunable( TimeOfDayComponent.TunableFactory()), tooltip_component=OptionalTunable( TooltipComponent.TunableFactory()), video=OptionalTunable(TunableVideoComponent()), welcome_component=OptionalTunable( WelcomeComponent.TunableFactory())), '_components_native': TunableTuple( description= '\n Tuning for native components, those that an object will have even\n if not tuned.\n ', tuning_group=GroupNames.COMPONENTS, Slot=OptionalTunable(SlotComponent.TunableFactory())), '_persists': Tunable( description= '\n Whether object should persist or not.\n ', tunable_type=bool, default=True, tuning_filter=FilterTag.EXPERT_MODE), '_world_file_object_persists': Tunable( description= "\n If object is from world file, check this if object state should\n persist. \n Example:\n If grill is dirty, but this is unchecked and it won't stay\n dirty when reloading the street. \n If Magic tree has this checked, all object relationship data\n will be saved.\n ", tunable_type=bool, default=False, tuning_filter=FilterTag.EXPERT_MODE), '_object_state_remaps': TunableList( description= '\n If this object is part of a Medator object suite, this list\n specifies which object tuning file to use for each catalog object\n state.\n ', tunable=TunableReference( description= '\n Current object state.\n ', manager=services.definition_manager(), tuning_filter=FilterTag.EXPERT_MODE)), 'environment_score_trait_modifiers': TunableMapping( description= '\n Each trait can put modifiers on any number of moods as well as the\n negative environment scoring.\n \n If tuning becomes a burden, consider making prototypes for many\n objects and tuning the prototype.\n \n Example: A Sim with the Geeky trait could have a modifier for the\n excited mood on objects like computers and tablets.\n \n Example: A Sim with the Loves Children trait would have a modifier\n for the happy mood on toy objects.\n \n Example: A Sim that has the Hates Art trait could get an Angry\n modifier, and should set modifiers like Happy to multiply by 0.\n ', key_type=TunableReference( description= '\n The Trait that the Sim must have to enable this modifier.\n ', manager=services.get_instance_manager( sims4.resources.Types.TRAIT)), value_type=TunableEnvironmentScoreModifiers.TunableFactory( description= '\n The Environmental Score modifiers for a particular trait.\n ' ), key_name='trait', value_name='modifiers'), 'slot_cost_modifiers': TunableMapping( description= "\n A mapping of slot types to modifier values. When determining slot\n scores in the transition sequence, if the owning object of a slot\n has a modifier for its type specified here, that slot will have the\n modifier value added to its cost. A positive modifier adds to the\n cost of a path using this slot and means that a slot will be less\n likely to be chosen. A negative modifier subtracts from the cost\n of a path using this slot and means that a slot will be more likely\n to be chosen.\n \n ex: Both bookcases and toilets have deco slots on them, but you'd\n rather a Sim prefer to put down an object in a bookcase than on the\n back of a toilet.\n ", key_type=SlotType.TunableReference( description= '\n A reference to the type of slot to be given a score modifier\n when considered for this object.\n ' ), value_type=Tunable( description= '\n A tunable float specifying the score modifier for the\n corresponding slot type on this object.\n ', tunable_type=float, default=0)), 'fire_retardant': Tunable( description= '\n If an object is fire retardant then not only will it not burn, but\n it also cannot overlap with fire, so fire will not spread into an\n area occupied by a fire retardant object.\n ', tunable_type=bool, default=False) } _commodity_flags = None additional_interaction_constraints = None def __init__(self, definition, **kwargs): super().__init__(definition, tuned_native_components=self._components_native, **kwargs) self._dynamic_commodity_flags_map = dict() for component_factory in self._components.values(): while component_factory is not None: self.add_component(component_factory(self)) self.item_location = ItemLocation.INVALID_LOCATION if self._persists: self._persistence_group = PersistenceGroups.OBJECT else: self._persistence_group = PersistenceGroups.NONE self._registered_transition_controllers = set() if self.definition.negative_environment_score != 0 or ( self.definition.positive_environment_score != 0 or self.definition.environment_score_mood_tags ) or self.environment_score_trait_modifiers: self.add_dynamic_component( objects.components.types.ENVIRONMENT_SCORE_COMPONENT. instance_attr) def on_reset_early_detachment(self, reset_reason): super().on_reset_early_detachment(reset_reason) for transition_controller in self._registered_transition_controllers: transition_controller.on_reset_early_detachment(self, 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 transition_controller in self._registered_transition_controllers: transition_controller.on_reset_add_interdependent_reset_records( self, reset_reason, reset_records) def on_reset_internal_state(self, reset_reason): if reset_reason == ResetReason.BEING_DESTROYED: self._registered_transition_controllers.clear() else: if not (self.parent is not None and self.parent.is_sim and self.parent.posture_state.is_carrying(self)): if not CarryingObject.snap_to_good_location_on_floor( self, self.parent.transform, self.parent.routing_surface): self.clear_parent(self.parent.transform, self.parent.routing_surface) self.location = self.location super().on_reset_internal_state(reset_reason) def register_transition_controller(self, controller): self._registered_transition_controllers.add(controller) def unregister_transition_controller(self, controller): self._registered_transition_controllers.discard(controller) @classmethod def _verify_tuning_callback(cls): for (i, part_data) in enumerate(cls._part_data): while part_data.forward_direction_for_picking.magnitude() != 1.0: logger.warn( 'On {}, forward_direction_for_picking is {} on part {}, which is not a normalized vector.', cls, part_data.forward_direction_for_picking, i, owner='bhill') for sa in cls._super_affordances: if sa.allow_user_directed and not sa.display_name: logger.error( 'Interaction {} on {} does not have a valid display name.', sa.__name__, cls.__name__) while sa.consumes_object() or sa.contains_stat( CraftingTuning.CONSUME_STATISTIC): logger.error( 'ScriptObject: Interaction {} on {} is consume affordance, should tune on ConsumableComponent of the object.', sa.__name__, cls.__name__, owner='tastle/cjiang') @flexmethod def update_commodity_flags(cls, inst): commodity_flags = set() inst_or_cls = inst if inst is not None else cls for sa in inst_or_cls.super_affordances(): commodity_flags |= sa.commodity_flags if commodity_flags: cls._commodity_flags = frozenset(commodity_flags) else: cls._commodity_flags = EMPTY_SET @flexproperty def commodity_flags(cls, inst): if cls._commodity_flags is None: if inst is not None: inst.update_commodity_flags() else: cls.update_commodity_flags() if inst is not None: dynamic_commodity_flags = set() for dynamic_commodity_flags_entry in inst._dynamic_commodity_flags_map.values( ): dynamic_commodity_flags.update(dynamic_commodity_flags_entry) return frozenset(cls._commodity_flags | dynamic_commodity_flags) return cls._commodity_flags def add_dynamic_commodity_flags(self, key, commodity_flags): self._dynamic_commodity_flags_map[key] = commodity_flags def remove_dynamic_commodity_flags(self, key): if key in self._dynamic_commodity_flags_map: del self._dynamic_commodity_flags_map[key] @classproperty def tuned_components(cls): return cls._components @flexproperty def allowed_hands(cls, inst): if inst is not None: carryable = inst.carryable_component if carryable is not None: return carryable.allowed_hands return () carryable_tuning = cls._components.carryable if carryable_tuning is not None: return carryable_tuning.allowed_hands return () @flexproperty def holster_while_routing(cls, inst): if inst is not None: carryable = inst.carryable_component if carryable is not None: return carryable.holster_while_routing return False carryable_tuning = cls._components.carryable if carryable_tuning is not None: return carryable_tuning.holster_while_routing return False def is_surface(self, *args, **kwargs): return False @classproperty def _anim_overrides_cls(cls): if cls._anim_overrides is not None: return cls._anim_overrides(None) @property def object_routing_surface(self): pass @property def _anim_overrides_internal(self): params = { 'isParented': self.parent is not None, 'heightAboveFloor': slots.get_surface_height_parameter_for_object(self) } if self.is_part: params['subroot'] = self.part_suffix params['isMirroredPart'] = True if self.is_mirrored() else False overrides = AnimationOverrides(params=params) for component_overrides in self.component_anim_overrides_gen(): overrides = overrides(component_overrides()) if self._anim_overrides is not None: return overrides(self._anim_overrides()) return overrides @forward_to_components_gen def component_anim_overrides_gen(self): pass @property def parent(self): pass def ancestry_gen(self): obj = self while obj is not None: yield obj if obj.is_part: obj = obj.part_owner else: obj = obj.parent @property def parent_slot(self): pass def get_closest_parts_to_position(self, position, posture=None, posture_spec=None): best_parts = set() best_distance = MAX_FLOAT if position is not None and self.parts is not None: while True: for part in self.parts: while (posture is None or part.supports_posture_type(posture)) and ( posture_spec is None or part.supports_posture_spec(posture_spec)): dist = (part.position_with_forward_offset - position).magnitude_2d_squared() if dist < best_distance: best_parts.clear() best_parts.add(part) best_distance = dist elif dist == best_distance: best_parts.add(part) return best_parts def num_valid_parts(self, posture): raise RuntimeError( '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.' ) if self.parts is not None: return sum( part.supports_posture_type(posture.posture_type) for part in self.parts) return 0 def is_same_object_or_part(self, obj): if not isinstance(obj, ScriptObject): return False if obj is self: return True if obj.is_part and obj.part_owner is self or self.is_part and self.part_owner is obj: return True return False def is_same_object_or_part_of_same_object(self, obj): if not isinstance(obj, ScriptObject): return False if self.is_same_object_or_part(obj): return True if self.is_part and obj.is_part and self.part_owner is obj.part_owner: return True return False def get_compatible_parts(self, posture, interaction=None): if posture is not None and posture.target is not None and posture.target.is_part: return (posture.target, ) return self.get_parts_for_posture(posture, interaction) def get_parts_for_posture(self, posture, interaction=None): if self.parts is not None: return (part for part in self.parts if part.supports_posture_type( posture.posture_type, interaction)) return () def get_parts_for_affordance(self, affordance): raise RuntimeError( '[bhill] This function is believed to be dead code and is scheduled for pruning. If this exception has been raised, the code is not dead and this exception should be removed.' ) if self.parts is not None and affordance is not None: return (part for part in self.parts if part.supports_affordance(affordance)) return () def may_reserve(self, *args, **kwargs): return True def reserve(self, *args, **kwargs): pass def release(self, *args, **kwargs): pass @property def build_buy_lockout(self): return False @property def route_target(self): return (RouteTargetType.NONE, None) @flexmethod def super_affordances(cls, inst, context=None): from objects.base_interactions import BaseInteractionTuning inst_or_cls = inst if inst is not None else cls component_affordances_gen = inst.component_super_affordances_gen( ) if inst is not None else EMPTY_SET super_affordances = itertools.chain( inst_or_cls._super_affordances, BaseInteractionTuning.GLOBAL_AFFORDANCES, component_affordances_gen) super_affordances = list(super_affordances) shift_held = False if context is not None: shift_held = context.shift_held for sa in super_affordances: if shift_held: if sa.cheat: yield sa elif sa.debug and __debug__: yield sa elif sa.automation and paths.AUTOMATION_MODE: yield sa while not sa.debug and not sa.cheat: yield sa else: while not sa.debug and not sa.cheat: yield sa @forward_to_components_gen def component_super_affordances_gen(self): pass @caches.cached_generator def posture_interaction_gen(self): for affordance in self._super_affordances: while not affordance.debug: if affordance._provided_posture_type is not None: while True: for aop in affordance.potential_interactions( self, None): yield aop def supports_affordance(self, affordance): return True def potential_interactions(self, context, get_interaction_parameters=None, allow_forwarding=True, **kwargs): try: for affordance in self.super_affordances(context): if not self.supports_affordance(affordance): pass if get_interaction_parameters is not None: interaction_parameters = get_interaction_parameters( affordance, kwargs) else: interaction_parameters = kwargs for aop in affordance.potential_interactions( self, context, **interaction_parameters): yield aop for aop in self.potential_component_interactions(context): yield aop while allow_forwarding and ( self._should_search_forwarded_sim_aop or self._should_search_forwarded_child_aop): for aop in self._search_forwarded_interactions( context, self._should_search_forwarded_sim_aop, self._should_search_forwarded_child_aop, get_interaction_parameters=get_interaction_parameters, **kwargs): yield aop except Exception: logger.exception( 'Exception while generating potential interactions for {}:', self) def supports_posture_type(self, posture_type): for super_affordance in self._super_affordances: while super_affordance.provided_posture_type == posture_type: return True return False def _search_forwarded_interactions(self, context, search_sim_aops, search_child_aops, **kwargs): if search_sim_aops: for part_or_object in (self, ) if not self.parts else self.parts: user_list = part_or_object.get_users(sims_only=True) for user in user_list: if part_or_object.is_part and part_or_object.disable_child_aop_forwarding: pass for aop in user.potential_interactions(context, **kwargs): while aop.affordance.allow_forward: yield aop if not search_child_aops: return for child in self.children: if child.parent.is_part and child.parent.disable_child_aop_forwarding: pass for aop in child.potential_interactions(context, **kwargs): while aop.affordance.allow_forward: yield aop def add_dynamic_component(self, *args, **kwargs): result = super().add_dynamic_component(*args, **kwargs) if result: self.resend_interactable() return result @distributor.fields.Field(op=distributor.ops.SetInteractable, default=False) def interactable(self): if self.build_buy_lockout: return False if self._super_affordances: return True for _ in self.component_interactable_gen(): pass return False resend_interactable = interactable.get_resend() @forward_to_components_gen def component_interactable_gen(self): pass @caches.cached(maxsize=20) def check_line_of_sight(self, transform, verbose=False): top_level_parent = self while top_level_parent.parent is not None: top_level_parent = top_level_parent.parent if top_level_parent.wall_or_fence_placement: if verbose: return (routing.RAYCAST_HIT_TYPE_NONE, None) return (True, None) if self.is_in_inventory(): if verbose: return (routing.RAYCAST_HIT_TYPE_NONE, None) return (True, None) slot_routing_location = self.get_routing_location_for_transform( transform) if verbose: ray_test = routing.ray_test_verbose else: ray_test = routing.ray_test return ray_test(slot_routing_location, self.routing_location, self.raycast_context(), return_object_id=True) clear_check_line_of_sight_cache = check_line_of_sight.cache.clear def _create_raycast_context(self, *args, **kwargs): super()._create_raycast_context(*args, **kwargs) if not self.is_sim: self.clear_check_line_of_sight_cache() @property def connectivity_handles(self): routing_context = self.get_or_create_routing_context() return routing_context.connectivity_handles def _clear_connectivity_handles(self): if self._routing_context is not None: self._routing_context.connectivity_handles.clear() @property def focus_bone(self): return 0 @forward_to_components def on_state_changed(self, state, old_value, new_value): pass @forward_to_components def on_post_load(self): pass @forward_to_components def on_finalize_load(self): pass @property def attributes(self): pass @property def flammable(self): return False @attributes.setter def attributes(self, value): logger.debug('PERSISTENCE: Attributes property on {0} were set', self) try: object_data = ObjectData() object_data.ParseFromString(value) self.load_object(object_data) except: logger.exception('Exception applying attributes to object {0}', self) def load_object(self, object_data): save_data = protocols.PersistenceMaster() save_data.ParseFromString(object_data.attributes) self.load(save_data) self.on_post_load() def is_persistable(self): if self.persistence_group == objects.persistence_groups.PersistenceGroups.OBJECT: return True if self.item_location == ItemLocation.FROM_WORLD_FILE: return self._world_file_object_persists if self.item_location == ItemLocation.ON_LOT or self.item_location == ItemLocation.FROM_OPEN_STREET: return self._persists if self.persistence_group == objects.persistence_groups.PersistenceGroups.IN_OPEN_STREET and self.item_location == ItemLocation.INVALID_LOCATION: return self._persist return False def save_object(self, object_list, item_location=ItemLocation.ON_LOT, container_id=0): if not self.is_persistable(): return with ProtocolBufferRollback(object_list) as save_data: attribute_data = self.get_attribute_save_data() save_data.object_id = self.id if attribute_data is not None: save_data.attributes = attribute_data.SerializeToString() save_data.guid = self.definition.id save_data.loc_type = item_location save_data.container_id = container_id return save_data def get_attribute_save_data(self): attribute_data = protocols.PersistenceMaster() self.save(attribute_data) return attribute_data @forward_to_components def save(self, persistence_master_message): pass def load(self, persistence_master_message): component_priority_list = [] for persistable_data in persistence_master_message.data: component_priority_list.append( (get_component_priority_and_name_using_persist_id( persistable_data.type), persistable_data)) component_priority_list.sort(key=lambda priority: priority[0][0], reverse=True) for ((_, (_, inst_comp)), persistable_data) in component_priority_list: while inst_comp: self.add_dynamic_component(inst_comp) if self.has_component(inst_comp): getattr(self, inst_comp).load(persistable_data) def finalize(self, **kwargs): self.on_finalize_load() def clone(self, **kwargs): clone = objects.system.create_object(self.definition, **kwargs) object_list = file_serialization.ObjectList() save_data = self.save_object(object_list.objects) clone.load_object(save_data) return clone
class GardeningFruitComponent( _GardeningComponent, component_name=objects.components.types.GARDENING_COMPONENT, persistence_key=protocols.PersistenceMaster.PersistableData. GardeningComponent): FACTORY_TUNABLES = { 'plant': TunableReference( description= '\n The plant that this fruit will grow into if planted or if it\n spontaneously germinates.\n ', manager=services.definition_manager()), 'splicing_recipies': TunableMapping( description= '\n The set of splicing recipes for this fruit. If a plant grown from\n this fruit is spliced with one of these other fruits, the given type\n of fruit will be also be spawned.\n ', key_type=TunableReference(manager=services.definition_manager()), value_type=TunableReference( manager=services.definition_manager())), 'spawn_slot': SlotType.TunableReference(), 'spawn_state_mapping': TunableMapping( description= '\n Mapping of states from the spawner object into the possible states\n that the spawned object may have.\n ', key_type=TunableStateValueReference(), value_type=TunableList( description= '\n List of possible child states for a parent state.\n ', tunable=TunableTuple( description= '\n Pair of weight and possible state that the spawned object\n may have.\n ', weight=TunableRange( description= '\n Weight that object will have on the probability\n calculation of which object to spawn.\n ', tunable_type=int, default=1, minimum=0), child_state=TunableStateValueReference()))), 'fruit_name': TunableLocalizedString( description= '\n Fruit name that will be used on the spliced plant description.\n ', allow_catalog_name=True), 'season_harvest_times': TunableLocalizedString( description= '\n The text to display for the harvestable time to grow when seasons\n are available.\n ' ), 'season_harvest_times_fallback': TunableLocalizedString( description= '\n The text to display for the harvestable time to grow when seasons\n are uninstalled.\n ' ) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._germination_handle = None def on_add(self, *args, **kwargs): self.start_germination_timer() return super().on_add(*args, **kwargs) def on_remove(self, *_, **__): self.stop_germination_timer() def scale_modifiers_gen(self): yield self.owner.get_stat_value(GardeningTuning.SCALE_COMMODITY) def on_state_changed(self, state, old_value, new_value, from_init): self.update_hovertip() def on_added_to_inventory(self): for (on_state_value, to_state_value) in GardeningTuning.PICKUP_STATE_MAPPING.items(): if self.owner.has_state(on_state_value): self.owner.set_state(to_state_value) @componentmethod_with_fallback(lambda: None) def get_notebook_information(self, reference_notebook_entry, notebook_sub_entries): notebook_entry = reference_notebook_entry(self.owner.definition.id) return (notebook_entry, ) def start_germination_timer(self): if self._germination_handle is not None: return germinate_stat = self.owner.commodity_tracker.add_statistic( GardeningTuning.SPONTANEOUS_GERMINATION_COMMODITY) value = random.uniform( germinate_stat.initial_value - GardeningTuning.SPONTANEOUS_GERMINATION_COMMODITY_VARIANCE, germinate_stat.initial_value) germinate_stat.set_value(value) threshold = sims4.math.Threshold(germinate_stat.convergence_value, operator.le) self._germination_handle = self.owner.commodity_tracker.create_and_add_listener( germinate_stat.stat_type, threshold, self.germinate) def stop_germination_timer(self): if self._germination_handle is None: return self.owner.commodity_tracker.remove_listener(self._germination_handle) self._germination_handle = None def _on_failure_to_germinate(self): self.owner.fade_in() dialog = GardeningTuning.GERMINATE_FAILURE_NOTIFICATION(self.owner) dialog.show_dialog(additional_tokens=(self.owner, )) def germinate(self, *_, **__): self.stop_germination_timer() result = None try: result = self._germinate() except: self.start_germination_timer() raise if result == False: self.start_germination_timer() self._on_failure_to_germinate() if result is None: self.stop_germination_timer() return result def _germinate(self): plant = None try: plant = create_object(self.plant) location = self._find_germinate_location(plant) if location is None: plant.destroy(source=self.owner, cause='Failed to germinate: No location found') plant = None return False if self.owner.parent_slot is not None: self.owner.parent_slot.add_child(plant) else: plant.location = location gardening_component = plant.get_component( types.GARDENING_COMPONENT) gardening_component.add_fruit(self.owner, sprouted_from=True) created_object_quality = self.owner.get_state( GardeningTuning.QUALITY_STATE_VALUE) current_household = services.owning_household_of_active_lot() if current_household is not None: plant.set_household_owner_id(current_household.id) services.get_event_manager().process_events_for_household( test_events.TestEvent.ItemCrafted, current_household, crafted_object=plant, skill=None, quality=created_object_quality, masterwork=None) if self.owner.in_use: self.owner.transient = True else: self.owner.destroy(source=self.owner, cause='Successfully germinated.') except: logger.exception('Failed to germinate.') if plant is not None: plant.destroy(source=self.owner, cause='Failed to germinate.') plant = None return False def _find_germinate_location(self, plant): if self.owner.parent_slot is not None: result = self.owner.parent_slot.is_valid_for_placement( definition=self.plant, objects_to_ignore=(self.owner, )) if not result: return location = self.owner.location else: search_flags = FGLSearchFlagsDefault | FGLSearchFlag.ALLOW_GOALS_IN_SIM_INTENDED_POSITIONS | FGLSearchFlag.ALLOW_GOALS_IN_SIM_POSITIONS | FGLSearchFlag.SHOULD_TEST_BUILDBUY starting_location = create_starting_location( location=self.owner.location) context = FindGoodLocationContext( starting_location, ignored_object_ids=(self.owner.id, ), object_id=plant.id, object_def_state_index=plant.state_index, object_footprints=(plant.get_footprint(), ), search_flags=search_flags) (translation, orientation) = find_good_location(context) if translation is None or orientation is None: return location = sims4.math.Location( sims4.math.Transform(translation, orientation), self.owner.routing_surface) return location @property def is_on_tree(self): if self.owner.parent_slot is not None and self.spawn_slot in self.owner.parent_slot.slot_types: return True return False def _ui_metadata_gen(self): yield from super()._ui_metadata_gen() season_service = services.season_service() if season_service is not None: season_text = GardeningTuning.get_seasonality_text_from_plant( self.plant) if season_text: yield (TooltipFields.season_text.name, season_text)
class SlotTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): TEST_EMPTY_SLOT = 1 TEST_USED_SLOT = 2 FACTORY_TUNABLES = {'description': 'Verify slot status. This test will only apply for single entity participants', 'participant': TunableEnumEntry(description='\n The subject of this situation data test.', tunable_type=ParticipantType, default=ParticipantType.Object), 'child_slot': TunableVariant(description=' \n The slot on the participant to be tested. \n ', by_name=Tunable(description=' \n The exact name of a slot on the participant to be tested.\n ', tunable_type=str, default='_ctnm_'), by_reference=SlotType.TunableReference(description=' \n A particular slot type to be tested.\n '), default='by_reference'), 'slot_test_type': TunableVariant(description='\n Type of slot test to run on target subject.\n ', has_empty_slot=TunableTuple(description="\n Verify the slot exists on the participant and it's unoccupied\n ", check_all_slots=Tunable(description='\n Check this if you want to check that all the slots of the \n subject are empty.\n ', tunable_type=bool, default=False), locked_args={'test_type': TEST_EMPTY_SLOT}), has_used_slot=TunableTuple(description='\n Verify if any slot of the child slot type is currently occupied\n ', check_all_slots=Tunable(description='\n Check this if you want to check that all the slots of the \n subject are used.\n ', tunable_type=bool, default=False), object_type=OptionalTunable(description='\n If enabled one of the children in the used slot must be of\n a certain kind of object. This test can be done by \n definition id or object tags.\n ', tunable=TunableVariant(description='\n If set to definition id then at least one of the child\n objects must pass the definition test specified.\n \n If set to object tags then at least one of the child\n objects must pass the object tag test specified. \n ', definition_id=ObjectTypeFactory.TunableFactory(), object_tags=ObjectTagFactory.TunableFactory())), locked_args={'test_type': TEST_USED_SLOT}), default='has_empty_slot'), 'slot_count_required': Tunable(description='\n Minimum number of slots that must pass test \n only valid for reference slots And not if all are required to pass\n ', tunable_type=int, default=1), 'check_part_owner': Tunable(description='\n If enabled and target of tests is a part, the test will be run\n on the part owner instead.\n ', tunable_type=bool, default=False)} def get_expected_args(self): return {'test_targets': self.participant} @cached_test def __call__(self, test_targets=()): for target in test_targets: if target.is_sim: continue if self.check_part_owner: if target.is_part: target = target.part_owner valid_count = 0 if self.slot_test_type.test_type == self.TEST_EMPTY_SLOT: if isinstance(self.child_slot, str): runtime_slot = RuntimeSlot(target, sims4.hash_util.hash32(self.child_slot), singletons.EMPTY_SET) if runtime_slot is not None: if runtime_slot.empty: return TestResult.TRUE elif self.slot_test_type.check_all_slots: if all(runtime_slot.empty for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None)): return TestResult.TRUE else: for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None): if runtime_slot.empty: valid_count += 1 if valid_count >= self.slot_count_required: return TestResult.TRUE elif self.slot_test_type.test_type == self.TEST_USED_SLOT: if isinstance(self.child_slot, str): runtime_slot = RuntimeSlot(target, sims4.hash_util.hash32(self.child_slot), singletons.EMPTY_SET) if runtime_slot is not None: if not runtime_slot.empty: if self.slot_test_type.object_type is not None: for child in runtime_slot.children: if self.slot_test_type.object_type(child): break else: return TestResult(False, 'None of the children objects were of the specified type. {} children={}', self.slot_test.object_type, runtime_slot.children) return TestResult.TRUE if self.slot_test_type.check_all_slots: if all(not runtime_slot.empty for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None)): return TestResult.TRUE for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None): if self.slot_test_type.object_type is not None: for child in runtime_slot.children: if self.slot_test_type.object_type(child): break else: return TestResult(False, 'None of the children objects were of the specified type. {} children={}', self.slot_test_type.object_type, runtime_slot.children) valid_count += 1 if not runtime_slot.empty and valid_count >= self.slot_count_required: return TestResult.TRUE else: for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None): if self.slot_test_type.object_type is not None: for child in runtime_slot.children: if self.slot_test_type.object_type(child): break else: return TestResult(False, 'None of the children objects were of the specified type. {} children={}', self.slot_test_type.object_type, runtime_slot.children) valid_count += 1 if not runtime_slot.empty and valid_count >= self.slot_count_required: return TestResult.TRUE elif self.slot_test_type.check_all_slots: if all(not runtime_slot.empty for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None)): return TestResult.TRUE for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None): if self.slot_test_type.object_type is not None: for child in runtime_slot.children: if self.slot_test_type.object_type(child): break else: return TestResult(False, 'None of the children objects were of the specified type. {} children={}', self.slot_test_type.object_type, runtime_slot.children) valid_count += 1 if not runtime_slot.empty and valid_count >= self.slot_count_required: return TestResult.TRUE else: for runtime_slot in target.get_runtime_slots_gen(slot_types={self.child_slot}, bone_name_hash=None): if self.slot_test_type.object_type is not None: for child in runtime_slot.children: if self.slot_test_type.object_type(child): break else: return TestResult(False, 'None of the children objects were of the specified type. {} children={}', self.slot_test_type.object_type, runtime_slot.children) valid_count += 1 if not runtime_slot.empty and valid_count >= self.slot_count_required: return TestResult.TRUE return TestResult(False, "SlotTest: participant doesn't meet slot availability requirements", tooltip=self.tooltip)
class RelatedSlotsTest(HasTunableSingletonFactory, AutoFactoryInit, event_testing.test_base.BaseTest): FACTORY_TUNABLES = {'participant': TunableEnumEntry(description='\n The subject of this slot test.', tunable_type=ParticipantType, default=ParticipantType.Object), 'slot_tests': TunableList(description='\n A list of slot tests that must all pass on a single part in order\n for that part to count.\n ', tunable=TunableTuple(description='\n A tuple containing all the information for the slot tests.\n ', slot=SlotType.TunableReference(description=' \n A particular slot type to be tested.\n '), requires_child=OptionalTunable(description='\n If set to has children then there must be a child in the \n slot to pass the test.\n \n If not checked then the slot must be a empty in order to\n pass the test.\n ', disabled_name='No_Children', enabled_name='Has_Children', tunable=TunableTuple(description='\n A tuple holding all of the different tuning for what\n matters about the child of a specified slot. For\n instance the test for what kind of object you are \n looking for in this specific slot type.\n ', object_type=OptionalTunable(description='\n A test for what type of object at least one of the\n children of this slot must be.\n ', tunable=TunableVariant(description='\n If set to definition id then at least one of the child\n objects must pass the definition test specified.\n \n If set to object tags then at least one of the child\n objects must pass the object tag test specified. \n ', definition_id=ObjectTypeFactory.TunableFactory(), object_tags=ObjectTagFactory.TunableFactory())))), count_required=Tunable(description='\n Minimum number of slots that must pass the test (to either be\n empty or have children) before the requirement is met.\n ', tunable_type=int, default=1))), 'parts_required': Tunable(description='\n The number of parts that must pass all of the slot tests in order\n for this test to return True.\n ', tunable_type=int, default=1)} def get_expected_args(self): return {'test_targets': self.participant} def test_part(self, part): for entry in self.slot_tests: valid_count = 0 for runtime_slot in part.get_runtime_slots_gen(slot_types={entry.slot}, bone_name_hash=None): if (entry.requires_child is None) == runtime_slot.empty: if entry.requires_child is not None and entry.requires_child.object_type is not None: for obj in runtime_slot.children: if entry.requires_child.object_type(obj): valid_count += 1 else: valid_count += 1 if valid_count >= entry.count_required: break else: return False return True @cached_test def __call__(self, test_targets=None): if test_targets is None: return TestResult(False, 'RelatedSlotsTest: There are no test targets') for target in test_targets: if target.is_part: if self.test_part(target): if self.parts_required == 1: return TestResult.TRUE return TestResult(False, 'Running a related slot test against an object part {} with a required parts count > 1 ({}). This will always fail.', target, self.parts_required) if target.parts: valid_parts = 0 for part in target.parts: if self.test_part(part): valid_parts += 1 if valid_parts >= self.parts_required: return TestResult.TRUE elif target.parts: valid_parts = 0 for part in target.parts: if self.test_part(part): valid_parts += 1 if valid_parts >= self.parts_required: return TestResult.TRUE return TestResult(False, '{} Failed RelatedSlotTest. Not enough parts passed all of the slot tests.', test_targets)
class GardeningFruitComponent( _GardeningComponent, component_name=objects.components.types.GARDENING_COMPONENT, persistence_key=protocols.PersistenceMaster.PersistableData. GardeningComponent): __qualname__ = 'GardeningFruitComponent' FACTORY_TUNABLES = { 'plant': TunableReference( description= '\n The plant that this fruit will grow into if planted or if it\n spontaneously germinates.\n ', manager=services.definition_manager()), 'splicing_families': TunableEnumFlags( description= '\n The set of splicing families compatible with this fruit. Any fruit\n matching one of these families may be spliced with a plant grown from\n this fruit.\n ', enum_type=SplicingFamily, needs_tuning=True, default=SplicingFamily(0)), 'splicing_recipies': TunableMapping( description= '\n The set of splicing recipes for this fruit. If a plant grown from this\n fruit is spliced with one of these other fruits, the given type of fruit\n will be also be spawned.\n ', key_type=TunableReference(services.definition_manager()), value_type=TunableReference(services.definition_manager())), 'spawn_slot': SlotType.TunableReference(), 'spawn_state_mapping': TunableMapping( description= '\n Mapping of states from the spawner object into the possible\n states that the spawned object may have\n ', key_type=TunableStateValueReference(), value_type=TunableList( description= '\n List of possible childs for a parent state\n ', tunable=TunableTuple( description= '\n Pair of weight and possible state that the spawned \n object may have\n ', weight=TunableRange( description= '\n Weight that object will have on the probability calculation \n of which object to spawn.\n ', tunable_type=int, default=1, minimum=0), child_state=TunableStateValueReference()))), 'spawn_times': OptionalTunable( description= '\n Schedule of when the fruit spawners should trigger.\n If this is set, spawn commodities will be removed and it will spawn\n based on the times set.\n ', tunable=TunableWeeklyScheduleFactory(), disabled_name='No_custom_spawn_times', enabled_name='Set_custom_spawn_times'), 'base_weight': Tunable( description= "\n The base weight of a fruit if its size gene's value is 1. The value of\n that gene is controlled by the range of the SCALE_COMMODITY.\n ", tunable_type=float, default=100), 'weight_units': TunableEnumEntry(MassUnit, MassUnit.GRAMS, needs_tuning=True), 'fruit_fall_behavior': OptionalTunable( description= '\n Controls automatic fruit-fall behavior.\n ', disabled_name='fruit_does_not_fall', enabled_name='fruit_falls_to_ground', tunable=TunableTuple( fall_at_state_value=ObjectStateValue.TunableReference(), search_radius=TunableInterval(tunable_type=float, default_lower=0.5, default_upper=3))), 'fruit_name': TunableLocalizedString( description= '\n Fruit name that will be used on the spliced plant description.\n ' ) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._germination_handle = None self._fall_to_ground_retry_alarm_handle = None def on_add(self, *_, **__): self.start_germination_timer() self.owner.hover_tip = ui_protocols.UiObjectMetadata.HOVER_TIP_GARDENING def on_remove(self, *_, **__): self.stop_germination_timer() self._cancel_fall_to_ground_retry_alarm() def scale_modifiers_gen(self): yield self.owner.get_stat_value(GardeningTuning.SCALE_COMMODITY) def on_state_changed(self, state, old_value, new_value): if not self.object_addition_to_world_complete: return if self.fruit_fall_behavior is not None and new_value == self.fruit_fall_behavior.fall_at_state_value: self._fall_to_ground() self.update_hovertip() def on_added_to_inventory(self): for (on_state_value, to_state_value) in GardeningTuning.PICKUP_STATE_MAPPING.items(): while self.owner.has_state(on_state_value): self.owner.set_state(to_state_value) def start_germination_timer(self): if self._germination_handle is not None: return germinate_stat = self.owner.commodity_tracker.add_statistic( GardeningTuning.SPONTANEOUS_GERMINATION_COMMODITY) value = random.uniform( germinate_stat.initial_value - GardeningTuning.SPONTANEOUS_GERMINATION_COMMODITY_VARIANCE, germinate_stat.initial_value) germinate_stat.set_value(value) threshold = sims4.math.Threshold(germinate_stat.convergence_value, operator.le) self._germination_handle = self.owner.commodity_tracker.create_and_activate_listener( germinate_stat.stat_type, threshold, self.germinate) def stop_germination_timer(self): if self._germination_handle is None: return self.owner.commodity_tracker.remove_listener(self._germination_handle) self._germination_handle = None def _cancel_fall_to_ground_retry_alarm(self): if self._fall_to_ground_retry_alarm_handle is not None: alarms.cancel_alarm(self._fall_to_ground_retry_alarm_handle) self._fall_to_ground_retry_alarm_handle = None def germinate(self, *_, **__): self.stop_germination_timer() result = None try: result = self._germinate() except: self.start_germination_timer() raise if result == False: self.start_germination_timer() if result is None: self.stop_germination_timer() return result def _germinate(self): plant = None try: plant = create_object(self.plant) location = self._find_germinate_location(plant) if location is None: logger.warn('Failed to germinate: No location found') plant.destroy(source=self.owner, cause='Failed to germinate: No location found') plant = None return False if self.owner.parent_slot is not None: self.owner.parent_slot.add_child(plant) else: plant.location = location plant.gardening_component.add_fruit(self.owner, sprouted_from=True) created_object_quality = self.owner.get_state( GardeningTuning.QUALITY_STATE_VALUE) current_household = services.owning_household_of_active_lot() if current_household is not None: plant.set_household_owner_id(current_household.id) services.get_event_manager().process_events_for_household( test_events.TestEvent.ItemCrafted, current_household, crafted_object=plant, skill=None, quality=created_object_quality, masterwork=None) if self.owner.in_use: self.owner.transient = True else: self.owner.destroy(source=self.owner, cause='Successfully germinated.') return except: logger.exception('Failed to germinate.') if plant is not None: plant.destroy(source=self.owner, cause='Failed to germinate.') plant = None return False return plant def _find_germinate_location(self, plant): if self.owner.parent_slot is not None: result = self.owner.parent_slot.is_valid_for_placement( definition=self.plant, objects_to_ignore=(self.owner, )) if not result: return location = self.owner.location else: search_flags = FGLSearchFlagsDefault | FGLSearchFlag.ALLOW_GOALS_IN_SIM_INTENDED_POSITIONS | FGLSearchFlag.ALLOW_GOALS_IN_SIM_POSITIONS | FGLSearchFlag.SHOULD_TEST_BUILDBUY context = FindGoodLocationContext( starting_location=self.owner.location, ignored_object_ids=(self.owner.id, ), object_id=plant.id, object_footprints=(plant.get_footprint(), ), search_flags=search_flags) (translation, orientation) = find_good_location(context) if translation is None or orientation is None: return location = sims4.math.Location( sims4.math.Transform(translation, orientation), self.owner.routing_surface) return location @property def is_on_tree(self): if self.owner.parent_slot is not None and self.spawn_slot in self.owner.parent_slot.slot_types: return True return False def _fall_to_ground(self): self._cancel_fall_to_ground_retry_alarm() if not self.is_on_tree: return if self.owner.in_use: self._fall_to_ground_retry_alarm_handle = alarms.add_alarm( self.owner, TimeSpan.in_real_world_seconds(10), self._fall_to_ground) return parent_obj = self.owner.parent if parent_obj is None: logger.warn('{}: Fruit failed to fall, it is no longer parented.', self.owner) return target_location = routing.Location( self.owner.routing_location.position, parent_obj.routing_location.orientation, parent_obj.routing_location.routing_surface) context = FindGoodLocationContext( starting_routing_location=target_location, object_footprints=(self.plant.get_footprint(0), ), max_distance=self.fruit_fall_behavior.search_radius.upper_bound) (translation, orientation) = find_good_location(context) if translation is None or orientation is None: logger.warn('{}: Failed to fall because FGL failed.', self.owner) self.owner.destroy(source=parent_obj, cause='Failed to fall because FGL failed') return if self.owner.parent is not None: self.owner.clear_parent( sims4.math.Transform(translation, orientation), self.owner.routing_surface) else: self.owner.set_location( sims4.math.Location( sims4.math.Transform(translation, orientation), self.owner.routing_surface)) def on_parent_wilted(self): if self.is_on_tree: self.owner.destroy(source=self.owner.parent, cause='Parent plant wilted') @property def show_splicing_families_in_tooltip(self): return GardeningTuning.is_shoot(self.owner)