class DisplaySnippetPickerSuperInteraction(PickerSuperInteraction): INSTANCE_TUNABLES = { 'picker_dialog': TunablePickerDialogVariant( description='\n The item picker dialog.\n ', available_picker_flags=ObjectPickerTuningFlags.ITEM, tuning_group=GroupNames.PICKERTUNING), 'subject': TunableEnumFlags( description= "\n To whom 'loot on selected' should be applied.\n ", enum_type=ParticipantTypeSim, default=ParticipantTypeSim.Actor, tuning_group=GroupNames.PICKERTUNING), 'display_snippets': TunableList( description= '\n The list of display snippets available to select and paired loot actions\n that will run if selected.\n ', tunable=_PickerDisplaySnippet.TunableFactory( description= '\n Display snippet available to select.\n ' ), tuning_group=GroupNames.PICKERTUNING), 'display_snippet_text_tokens': LocalizationTokens.TunableFactory( description= '\n Localization tokens passed into the display snippet text fields.\n \n When acting on the individual items within the snippet list, the \n following text tokens will be appended to this list of tokens (in \n order):\n 0: snippet instance display name\n 1: snippet instance display description\n 2: snippet instance display tooltip\n 3: tokens tuned alongside individual snippets within the snippet list\n ', tuning_group=GroupNames.PICKERTUNING), 'display_snippet_text_overrides': OptionalTunable( description= '\n If enabled, display snippet text overrides for all snippets \n to be displayed in the picker. \n \n Can be used together with the display snippet text tokens to \n act as text wrappers around the existing snippet display data.\n ', tunable=_DisplaySnippetTextOverrides.TunableFactory( description= '\n Display snippet text overrides for all snippets to be displayed\n in the picker. \n \n Can be used together with the display snippet text tokens to \n act as text wrappers around the existing snippet display data.\n ' ), tuning_group=GroupNames.PICKERTUNING), 'continuations': TunableList( description= '\n List of continuations to push when a snippet is selected.\n \n ID of the snippet will be the PickedItemID participant in the \n continuation.\n ', tunable=TunableContinuation(), tuning_group=GroupNames.PICKERTUNING), 'run_continuations_on_no_selection': Tunable( description= '\n Checked, runs continuations regardless if anything is selected.\n Unchecked, continuations are only run if something is selected.\n ', tunable_type=bool, default=True, tuning_group=GroupNames.PICKERTUNING) } @classmethod def has_valid_choice(cls, target, context, **kwargs): snippet_count = 0 for _ in cls.picker_rows_gen(target, context, **kwargs): snippet_count += 1 if snippet_count >= cls.picker_dialog.min_selectable: return True return False def _run_interaction_gen(self, timeline): self._show_picker_dialog(self.sim, target_sim=self.sim) return True yield @flexmethod def picker_rows_gen(cls, inst, target, context, **kwargs): inst_or_cls = inst if inst is not None else cls target = target if target is not DEFAULT else inst.target context = context if context is not DEFAULT else inst.context resolver = InteractionResolver(cls, inst, target=target, context=context) general_tokens = inst_or_cls.display_snippet_text_tokens.get_tokens( resolver) overrides = inst_or_cls.display_snippet_text_overrides index = 0 for display_snippet_data in inst_or_cls.display_snippets: display_snippet = display_snippet_data.display_snippet resolver = InteractionResolver( cls, inst, target=target, context=context, picked_item_ids={display_snippet.guid64}) test_result = display_snippet_data.test(resolver) is_enable = test_result.result if is_enable or test_result.tooltip is not None: snippet_default_tokens = ( display_snippet.display_name(*general_tokens) if display_snippet.display_name is not None else None, display_snippet.display_description(*general_tokens) if display_snippet.display_description is not None else None, display_snippet.display_tooltip(*general_tokens) if display_snippet.display_tooltip is not None else None) snippet_additional_tokens = display_snippet_data.display_snippet_text_tokens.get_tokens( resolver) tokens = general_tokens + snippet_default_tokens + snippet_additional_tokens display_snippet = overrides( display_snippet_data.display_snippet) tooltip = None if not overrides is not None or test_result.tooltip is None else lambda *_, tooltip=test_result.tooltip: tooltip( *tokens) tooltip = None if display_snippet.display_tooltip is None else lambda *_, tooltip=display_snippet.display_tooltip: tooltip( *tokens) row = BasePickerRow( is_enable=is_enable, name=display_snippet.display_name(*tokens), icon=display_snippet.display_icon, tag=index, row_description=display_snippet.display_description( *tokens), row_tooltip=tooltip) yield row index += 1 def _on_display_snippet_selected(self, picked_choice, **kwargs): resolver = self.get_resolver(**kwargs) for loot_on_selected in self.display_snippets[ picked_choice].loot_on_selected: loot_on_selected.apply_to_resolver(resolver) def on_choice_selected(self, picked_choice, **kwargs): if picked_choice is None: if self.run_continuations_on_no_selection: for continuation in self.continuations: self.push_tunable_continuation(continuation) return display_snippet = self.display_snippets[picked_choice].display_snippet picked_item_set = {display_snippet.guid64} self._on_display_snippet_selected(picked_choice, picked_item_ids=picked_item_set) for continuation in self.continuations: self.push_tunable_continuation(continuation, picked_item_ids=picked_item_set)
class UiTuning: LOADING_SCREEN_STRINGS = TunableMapping( description= '\n Mapping from the Pack to its associated loading strings.\n ', key_type=TunableEnumEntry( description= '\n The pack containing the strings.\n ', tunable_type=Pack, default=Pack.BASE_GAME), value_type=TunableList( description= '\n The list of loading screen strings which belongs to the pack.\n We always display the strings from base game AND from the latest\n pack which the player is entitled to and has installed. \n ', tunable=TunableLocalizedString()), export_modes=(ExportModes.ClientBinary, ), tuple_name='LoadingScreenStringsTuple') GO_HOME_INTERACTION = TunableReference( description= '\n The interaction to push a Sim to go home.\n ', manager=services.affordance_manager(), export_modes=(ExportModes.ClientBinary, )) COME_NEAR_ACTIVE_SIM = TunableReference( description= '\n An affordance to push on a Sim so they come near the active Sim.\n ', manager=services.affordance_manager()) BRING_HERE_INTERACTION = TunableReference( description= '\n An affordance to push on household members to summon them to the\n current lot if they are not instanced.\n ', manager=services.affordance_manager()) NEW_CONTENT_ALERT_TUNING = TunableMapping( description= '\n Mapping from Pack to its associated new content alert tuning\n ', key_type=TunableEnumEntry( description= '\n The pack containing the new content tuning. NOTE: this should never\n be tuned to BASE_GAME. That would trigger for all users.\n ', tunable_type=Pack, default=Pack.BASE_GAME), value_type=TunableTuple( description= '\n Each pack will have a set of tuning of images and text to display\n to inform the user what new features have been introduced in the \n pack.\n ', export_class_name='TunablePackContentTuple', title=TunableLocalizedString( description= '\n The title to be displayed at the top of the New Content Alert\n UI for this pack.\n ' ), cycle_images=TunableList( description= '\n A list of images (screenshots) that the UI cycles through to\n show off some of the new features.\n ', tunable=TunableResourceKey( resource_types=sims4.resources.CompoundTypes.IMAGE)), feature_list=TunableList( description= '\n A list of tuples that describe each new feature in the New\n Content Alert UI. NOTE: This should NEVER have more than 4\n elements in it.\n ', maxlength=4, tunable=TunableTuple( description= '\n A tuple that contains title text, description, an icon,\n and a reference to the matching lesson for this new \n feature.\n ', export_class_name='TunableFeatureTuple', title_text=TunableLocalizedString( description= '\n A title to be displayed in bold for the feature.\n ' ), description_text=TunableLocalizedString( description= '\n A short description of the new feature.\n ' ), icon=TunableResourceKey( description= '\n An icon that represents the feature.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), lesson=TunableReference( description= '\n A reference to the lesson that the user can go look at\n for this new feature.\n ', manager=services.get_instance_manager( sims4.resources.Types.TUTORIAL), allow_none=True, pack_safe=True)))), export_modes=(ExportModes.ClientBinary, ), tuple_name='NewContentAlertTuple') PACK_SPECIFIC_DATA = TunableMapping( description= '\n Mapping from a Pack to its associated data. This includes pack icons,\n filter strings, and the credits file.\n ', key_name='packId', key_type=TunableEnumEntry( description= '\n The pack id for the associated data.\n ', tunable_type=Pack, default=Pack.BASE_GAME), value_name='packData', value_type=TunableTuple( description= '\n Each pack will have a set icons and can have an optional filter \n string for use in Build/CAS and an optional Credits Title\n ', export_class_name='TunablePackDataTuple', credits_title=TunableLocalizedString( description= '\n The title used in the credits dropdown to select this packs credits.\n If set, there must be a creditsxml file for this pack\n in Assets/InGame/UI/Flash/data/\n ', allow_none=True), filter_name=TunableLocalizedString( description= '\n The name to used to describe the pack in CAS and BuildBuy filters.\n If set, this pack will appear in the filter list.\n ', allow_none=True), pack_type=TunableEnumEntry( description= '\n Which type of pack is this.\n ', tunable_type=PackTypes, default=PackTypes.BASE), icon_32=TunableResourceKey( description= '\n Pack icon. 32x32.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), icon_64=TunableResourceKey( description= '\n Pack icon. 64x64.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), icon_128=TunableResourceKey( description= '\n Pack icon. 128x128.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), icon_owned=TunableIcon( description= '\n Pack icon that is displayed in the main menu\n pack display when the player owns that pack.\n ', allow_none=True), icon_unowned=TunableIcon( description= '\n Pack icon that is displayed in the main menu\n pack display when the player does not own that pack.\n ', allow_none=True), webstore_id=Tunable( description= '\n web store pack specific url identifier\n\t\t\t\t', tunable_type=str, default=None), region_list=TunableList( description= '\n A list of tuples that describe each new region in the pack.\n ', tunable=TunableTuple( description= '\n A tuple that contains metadata for a world select region.\n ', export_class_name='TunablePackRegionTuple', region_resource=TunableRegionDescription( description= '\n Reference to the region description catalog resource associated with this region\n ', pack_safe=True), is_player_facing=Tunable( description= '\n Whether to display this region in world select when the user does not own the associated pack\n ', tunable_type=bool, default=False), region_name=TunableLocalizedString( description= '\n Localized name of region.\n ', allow_none=True), region_description=TunableLocalizedString( description= '\n Localized description of region.\n ', allow_none=True), overlay_layer=TunableResourceKey( description= '\n Hero image displayed on mouse over of region in\n\t\t\t\t\t\tworld selection UI.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, allow_none=True), parallax_layers=TunableList( description= '\n Images used for scrolling parallax layers for region\n in world selection UI. Max number of images = 5.\n ', maxlength=5, tunable=TunableResourceKey( resource_types=sims4.resources.CompoundTypes.IMAGE )), is_destination_region=Tunable( description= '\n Whether this region is a destination world.\n ', tunable_type=bool, default=False))), promo_cycle_images=TunableList( description= '\n A list of promo screenshots and titles to display in the \n Pack Detail panel.\n ', tunable=PromoCycleImagesTuning( description= '\n Screenshots and label displayed in the Pack Detail Panel\n and Pack Preview Panel.\n ' )), short_description=TunableLocalizedString( description= '\n Short description of the pack meant to be displayed in \n a tooltip.\n ', allow_none=True)), export_modes=(ExportModes.ClientBinary, ), tuple_name='PackSpecificDataTuple') BUNDLE_SPECIFIC_DATA = TunableMapping( description= '\n Mapping from an MTX Bundle to its associated data. This is for bundles that\n should appear in the ui, but are not packs. This includes main menu icons,\n description, and the action associated with that bundle.\n ', key_type=TunableMTXBundle( description= '\n The MTX bundle id for the associated data.\n ', pack_safe=True), value_type=TunableTuple( description= '\n Each bundle has icons and a description, as well as an\n data for the action performed when the bundle is interacted \n with either the PromotionDialog or the PackDisplayPanel.\n ', bundle_name=TunableLocalizedString( description= '\n Name used in pack detail panel and main menu. If empty,\n we fall back to using the MTX product name.\n ', allow_none=True), icon_owned=TunableIcon( description= '\n Bundle icon that is displayed in the main menu\n pack display when the player is entitled to that bundle.\n ' ), icon_unowned=TunableIcon( description= '\n Bundle icon that is displayed in the main menu\n pack display when the player is not entitled to that bundle.\n ' ), short_description=TunableLocalizedString( description= '\n Short description of the bundle meant to be displayed in \n a tooltip.\n ' ), action=TunableVariant( description= '\n The action that should be performed when this bundle is interacted with\n in either the PromotionDialog or the PackDisplayPanel.\n ', url=Tunable( description= '\n External url to open from PackDisplayPanel.\n ', tunable_type=str, default=None), promo_data=TunableTuple( description= '\n Data that populates PromotionDialog.\n ', title=TunableLocalizedString( description= '\n Title of the promotion.\n ' ), text=TunableLocalizedString( description= '\n Text describing the promotion.\n ' ), image=TunableIcon( description= '\n Image displayed in the promotion dialog.\n ' ), legal_text=TunableLocalizedString( description= '\n Legal text required for this promotion.\n ', allow_none=True), export_class_name='TunablePromoDataTuple'), default='url'), export_class_name='TunableBundleDataTuple'), export_modes=(ExportModes.ClientBinary, ), tuple_name='BundleSpecificDataTuple') PACK_RELEASE_ORDER = TunableList( description='\n List of Pack Ids in release order.\n ', tunable=TunableEnumEntry( description='\n A pack Id.\n ', tunable_type=Pack, default=Pack.BASE_GAME), export_modes=(ExportModes.ClientBinary, )) CHALLENGE_DATA = TunableList( description= '\n List of challenge event data for engagement challenge notification UI.\n ', tunable=TunableTuple( description= '\n Data for each engagement challenge event.\n ', export_class_name='TunableChallengeNotificationTuple', challenge_list=TunableList( description= '\n A list of tuples that describe each challenge.\n ', tunable=TunableTuple( description= '\n A tuple that contains data for a challenge.\n ', export_class_name='TunableChallengeDataTuple', challenge_description=TunableLocalizedString( description= '\n The description of the challenge.\n ', allow_none=True), challenge_name=TunableLocalizedString( description= '\n The name of the challenge.\n ', allow_none=True), image=TunableResourceKey( description= '\n The main image displayed for challenge info.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), info_link=Tunable( description= '\n The url link to page for more info on a challenge.\n ', tunable_type=str, default='', allow_empty=True), event_display=TunableTuple( description= '\n Display data for a challenge event.\n ', export_class_name='TunableChallengeEventDisplayTuple', event_icon=TunableIcon( description= '\n An icon to use for the challenge event.\n ', allow_none=True), event_title=TunableLocalizedString( description= '\n Title to display. If not provided, challenge name will be used.\n ', allow_none=True), end_time=TunableTuple( description= '\n Date and time (UTC) for when the challenge event is expected to end.\n This is currently used to compute the time remaining in the UI.\n ', display_name='End Time (UTC)', export_class_name='TunableChallengeDateTuple', year=TunableRange( description= '\n Year\n ', tunable_type=int, default=2016, minimum=2014), month=TunableRange( description= '\n Month\n ', tunable_type=int, default=1, minimum=1, maximum=12), day=TunableRange( description= '\n Day\n ', tunable_type=int, default=1, minimum=1, maximum=31), hour=TunableRange( description= '\n Hour (24-hour)\n ', tunable_type=int, default=0, minimum=0, maximum=23), minute=TunableRange( description= '\n Minute\n ', tunable_type=int, default=0, minimum=0, maximum=59)), activity_icon=TunableIcon( description= '\n Icon to display beside the activity progress bar.\n ', allow_none=True), activity_progress_text=TunableLocalizedString( description= "\n Status text for when the player is still making progress towards\n the challenge goal. This is currently displayed on a tooltip.\n A CSS class of 'timeremaining' will have its color changed\n when the event is close to ending.\n The following tokens are available:\n 0 - Number: Current collection progress, if available.\n 1 - Number: Collection goal, if available.\n 2 - Number: Hours remaining.\n 3 - Number: Days remaining.\n ", allow_none=True), activity_progress_icon=TunableIcon( description= '\n Icon to be paired with the progress text.\n ', allow_none=True), activity_complete_text=TunableLocalizedString( description= "\n Status text for when the player has met the challenge goal.\n This is currently displayed on a tooltip.\n If not specified, the in-progress text will be used.\n A CSS class of 'timeremaining' will have its color changed\n when the event is close to ending.\n The following tokens are available (same as the in-progress text):\n 0 - Number: Current collection progress, if available.\n 1 - Number: Collection goal, if available.\n 2 - Number: Hours remaining.\n 3 - Number: Days remaining.\n ", allow_none=True), activity_complete_icon=TunableIcon( description= '\n Icon to be paired with the challenge complete text.\n If not specified, the in-progress icon will be used.\n ', allow_none=True), community_progress_text=TunableLocalizedString( description= "\n Status text describing the community's progress.\n This is currently displayed on a tooltip.\n This text is displayed even when challenges do not have\n community goals.\n Two Number tokens are available:\n 0 - Current community collection progress.\n 1 - Community collection goal, if any.\n ", allow_none=True), community_progress_icon=TunableIcon( description= '\n Icon to be paired with the community status text.\n ', allow_none=True), community_complete_text=TunableLocalizedString( description= '\n Status text for when the community has met the challenge goal.\n This text is only used when a goal is defined.\n If not specified, the in-progress status text will be used.\n Two Number tokens are available:\n 0 - Current community collection progress.\n 1 - Community collection goal, if any.\n ', allow_none=True), community_complete_icon=TunableIcon( description= '\n Icon to be paired with the community challenge complete text.\n ', allow_none=True), community_goal_amount=Tunable( description= '\n Optional collection goal for the community to reach.\n ', tunable_type=int, default=0)), collection_id=TunableEnumEntry( description= "\n A CollectionIdentifier that is associated with this\n challenge. This is used by the UI to tie a collectible \n with this challenge.\n \n Use the default of Unindentified for challenges that\n aren't associated with a particular collection.\n ", tunable_type=CollectionIdentifier, default=CollectionIdentifier.Unindentified, export_modes=ExportModes.All), reward_items=TunableList( description= '\n A list of tuples that describe rewards for challenge.\n ', tunable=TunableTuple( description= '\n A tuple that contains data for a challenge reward item.\n ', export_class_name='TunableChallengeRewardTuple', reward_icon=TunableResourceKey( description= '\n The icon of reward item.\n ', resource_types=sims4.resources.CompoundTypes. IMAGE), reward_name=TunableLocalizedString( description= '\n The name of reward item.\n ', allow_none=True))))), challenge_subtitle=TunableLocalizedString( description= '\n The subtitle text to be displayed in notification UI.\n ', allow_none=True), challenge_title=TunableLocalizedString( description= '\n The title text to be displayed in notification UI.\n ', allow_none=True), switch_name=Tunable( description= '\n Server switch name to check whether challenge is active.\n ', tunable_type=str, default='', allow_empty=True)), export_modes=(ExportModes.ClientBinary, )) PLATFORM_STRING_REPLACEMENTS = TunableList( description= '\n A list of strings that will be swapped out when in use on different \n platforms. Each entry contains the original and replacement LocKey, the platforms\n to perform the swap on, and the input method that is in use when the\n LocKey is used.\n ', tunable=TunableTuple( original_string=TunableLocalizedString( description= '\n The string that will be replaced or ignored.\n ' ), replacement_string=OptionalTunable( description= '\n The string that will be used in place of original_string. If\n omitted, original_string will simply be ignored entirely.\n ', tunable=TunableLocalizedString()), platform=TunableEnumEntry( description= '\n The platforms on which the string will be replaced.\n ', tunable_type=Platform, default=Platform.CONSOLE), input_method=TunableEnumEntry( description= '\n The input method that should be in use when attempting to replace\n the original_string.\n ', tunable_type=InputMethod, default=InputMethod.ANY), export_modes=ExportModes.ClientBinary, export_class_name='PlatformStringReplacementTuple')) SCALING = TunableList( description= '\n Defines a min/max ui scaling value for a screen resolution.\n ', tunable=TunableTuple( screen_width=Tunable( description= '\n Provide an integer value.\n ', tunable_type=int, default=0), screen_height=Tunable( description= '\n Provide an integer value.\n ', tunable_type=int, default=0), scale_max=Tunable( description= '\n Provide a float value.\n ', tunable_type=float, default=1), scale_min=Tunable( description= '\n Provide a float value.\n ', tunable_type=float, default=1), export_modes=ExportModes.ClientBinary, export_class_name='UIScaleTuple')) CG_CHALLENGE_DATAS = TunableList( description="\n A list of a challenge's data.\n ", tunable=TunableTuple( cg_challenge_hashtag=TunableLocalizedString( description= '\n Hashtag of this challenge\n ' ), cg_challenge_name=TunableLocalizedString( description= '\n Name of this challenge\n '), export_modes=ExportModes.ClientBinary, export_class_name='CGChallengeTuning')) DEFAULT_OVERLAY_MAP = TunableMapping( description= '\n This is a mapping of MapOverlayEnum -> List of MapOverlayEnums. The key\n is used as the layer to be shown when no other overlays are present.\n The value is a list of overlay types that would result in the default\n layer being turned off if both are active.\n ', key_type=TunableEnumEntry( description= '\n This is the OverlayType that acts as the default for the grouping\n of OverlayTypes.\n ', tunable_type=MapOverlayEnum, default=MapOverlayEnum.NONE), value_type=TunableList( description= '\n A list of OverlayTypes, that if turned on would result in the\n default OverlayType being shut off.\n ', tunable=TunableEnumEntry( description= '\n The OverlayType that causes the default value to turn off.\n ', tunable_type=MapOverlayEnum, default=MapOverlayEnum.NONE)), export_modes=ExportModes.All, tuple_name='OverlayDefaultData')
class Narrative(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(Types.NARRATIVE)): INSTANCE_TUNABLES = { 'narrative_groups': TunableEnumSet( description= '\n A set of narrative groups this narrative is a member of.\n ', enum_type=NarrativeGroup, enum_default=NarrativeGroup.INVALID, invalid_enums=(NarrativeGroup.INVALID, )), 'narrative_links': TunableMapping( description= '\n A mapping of narrative event to the narrative that will trigger \n when that narrative event triggers.\n ', key_type=TunableEnumEntry( description= '\n Event of interest.\n ', tunable_type=NarrativeEvent, default=NarrativeEvent.INVALID, invalid_enums=(NarrativeEvent.INVALID, )), value_type=TunableReference( description= '\n The narrative the respective event transitions to while\n this specific narrative is active. \n ', manager=services.get_instance_manager(Types.NARRATIVE))), 'additional_situation_shifts': TunableMapping( description= '\n A mapping of situation shift type to the shift curve it provides.\n ', key_type=TunableEnumEntry( description='\n Shift type.\n ', tunable_type=NarrativeSituationShiftType, default=NarrativeSituationShiftType.INVALID, invalid_enums=(NarrativeSituationShiftType.INVALID, )), value_type=SituationCurve.TunableFactory( description= '\n The situation schedule this adds to the situation scheduler\n if this shift type is opted into as an additional source.\n ', get_create_params={'user_facing': False})), 'situation_replacements': TunableMapping( description= '\n A mapping of situation to a tuple of situation and tests to apply.\n ', key_type=TunableReference( description= '\n A situation that is available for situation replacement.\n ', manager=services.get_instance_manager(Types.SITUATION)), value_type=TunableTuple(replacement=TunableReference( description= '\n A situation that is available for situation replacement.\n ', manager=services.get_instance_manager(Types.SITUATION)), replacement_tests= SituationReplacementTestList())), 'environment_override': OptionalTunable( description= '\n If tuned, this narrative can have some effect on world controls\n such as skyboxes, ambient sounds, and vfx.\n ', tunable=NarrativeEnvironmentOverride.TunableFactory()), 'introduction': OptionalTunable( description= '\n If enabled, an introduction dialog will be shown on the next zone\n load (which could be a save/load, travel, switch to another\n household, etc.) if the test passes.\n ', tunable=TunableTuple( dialog=UiDialogOk.TunableFactory( description= '\n The dialog to show that introduces the narrative.\n ' ), tests=TunableTestSet( description= '\n The test set that must pass for the introduction to be\n given. Only the global resolver is available.\n Sample use: Must be in a specific region.\n ' ))), 'dialog_on_activation': OptionalTunable( description= '\n If enabled, an introduction dialog will be shown when the narrative\n is activated, if the test passes.\n ', tunable=TunableTuple( dialog=TunableUiDialogVariant( description= '\n The dialog to show when the narrative starts.\n ' ), tests=TunableTestSet( description= '\n The test set that must pass for the dialog to be\n given. Only the global resolver is available.\n Sample use: Must be in a specific region.\n ' ))), 'audio_sting': OptionalTunable( description= '\n If enabled, play the specified audio sting when this narrative starts.\n ', tunable=TunablePlayAudio()), 'sim_info_loots': OptionalTunable( description= '\n Loots that will be given to all sim_infos when this narrative starts.\n ', tunable=TunableTuple( loots=TunableList(tunable=TunableReference( manager=services.get_instance_manager( sims4.resources.Types.ACTION), class_restrictions=('LootActions', ), pack_safe=True)), save_lock_tooltip=TunableLocalizedString( description= '\n The tooltip/message to show on the save lock tooltip while\n the loots are processing.\n ' ))), 'narrative_threshold_links': TunableMapping( description= "\n A mapping between the event listener to a narrative link\n that will be activated if progress of that event type hits \n the tuned threshold. \n \n For example, if this narrative has the following narrative threshold\n link:\n \n {\n key type: GoldilocksListener\n value_type:\n Interval: -10, 10\n below_link: TooCold_Goldilocks\n above_link: TooHot_Goldilocks\n }\n \n ... any Narrative Progression Loot tagged with the GoldilocksListener\n event will increment this instance's narrative_progression_value. If\n it ever goes above 10 or below -10, the corresponding narrative is\n activated and this narrative will complete.\n \n NOTE: All active narratives' progression values begin at 0. \n ", key_type=TunableEnumEntry( description= '\n The progression event that triggers the narrative transition\n if a threshold is met.\n ', tunable_type=NarrativeProgressionEvent, default=NarrativeProgressionEvent.INVALID, invalid_enums=(NarrativeProgressionEvent.INVALID, )), value_type=TunableTuple( interval=TunableInterval( description= '\n The interval defines the upper and lower bound of the\n narrative thresholds. If any of the thresholds are crossed,\n the corresponding narrative is activated.\n ', tunable_type=int, default_lower=-50, default_upper=50), below_link=OptionalTunable( description= '\n The narrative that is activated if the lower threshold is\n passed.\n ', tunable=TunableReference( manager=services.get_instance_manager( Types.NARRATIVE))), above_link=OptionalTunable( description= '\n The narrative that is activated if the upper threshold is\n passed.\n ', tunable=TunableReference( manager=services.get_instance_manager( Types.NARRATIVE))))) } def __init__(self): self._introduction_shown = False self._should_suppress_travel_sting = False self._narrative_progression = {} for event in self.narrative_threshold_links.keys(): self._narrative_progression[event] = 0 def save(self, msg): msg.narrative_id = self.guid64 msg.introduction_shown = self._introduction_shown for (event, progression) in self._narrative_progression.items(): with ProtocolBufferRollback( msg.narrative_progression_entries) as progression_msg: progression_msg.event = event progression_msg.progression = progression def load(self, msg): self._introduction_shown = msg.introduction_shown for narrative_progression_data in msg.narrative_progression_entries: self._narrative_progression[ narrative_progression_data. event] = narrative_progression_data.progression def on_zone_load(self): self._should_suppress_travel_sting = False if self.introduction is not None: if not self._introduction_shown: resolver = GlobalResolver() if self.introduction.tests.run_tests(resolver): dialog = self.introduction.dialog(None, resolver=resolver) dialog.show_dialog() self._introduction_shown = True self._should_suppress_travel_sting = self.introduction.dialog.audio_sting is not None @property def should_suppress_travel_sting(self): return self._should_suppress_travel_sting def start(self): if self.dialog_on_activation is not None: resolver = GlobalResolver() if self.dialog_on_activation.tests.run_tests(resolver): dialog = self.dialog_on_activation.dialog(None, resolver=resolver) dialog.show_dialog() if self.audio_sting is not None: play_tunable_audio(self.audio_sting) if self.sim_info_loots is not None: services.narrative_service().add_sliced_sim_info_loots( self.sim_info_loots.loots, self.sim_info_loots.save_lock_tooltip) def apply_progression_for_event(self, event, amount): if event not in self.narrative_threshold_links: return () self._narrative_progression[event] += amount new_amount = self._narrative_progression[event] link_data = self.narrative_threshold_links[event] if new_amount in link_data.interval: return () if new_amount < link_data.interval.lower_bound and link_data.below_link is not None: return (link_data.below_link, ) elif link_data.above_link is not None: return (link_data.above_link, ) return () def get_progression_stat(self, event): return self._narrative_progression.get(event)
class SpellbookCategoryData(HasTunableSingletonFactory, AutoFactoryInit): FACTORY_TUNABLES = {'content_list': TunableTuple(description='\n Tuning used for the content list.\n ', icon=TunableIconAllPacks(description='\n Icon used to display this category in the content list.\n '), tooltip=OptionalTunable(description='\n Tooltip used in the spellbook for this category.\n If unset, no tooltip is shown.\n ', tunable=TunableLocalizedString())), 'front_page': TunableTuple(description='\n Tuning used for the first page of the category.\n ', category_description=OptionalTunable(description='\n Description used in the spellbook.\n If unset, description is not shown.\n ', tunable=TunableLocalizedString()), icon=TunableIconAllPacks(description='\n Icon used to display this category in first page.\n ')), 'page': TunableTuple(description='\n Tuning used for pages other than the front page of the category.\n ', icon=OptionalTunable(description='\n Icon shown on each page of this category.\n ', tunable=TunableIconAllPacks())), 'tab': TunableTuple(description='\n Tuning used to display the category on the tabs at the\n top of the book.\n ', icon=TunableIconAllPacks(description='\n Icon used to display the category on a tab.\n '), tooltip=OptionalTunable(description='\n Tooltip used in the spellbook on the the tab for this category.\n If unset, Category Name is used.\n ', tunable=TunableLocalizedString())), 'category_name': TunableLocalizedString(description='Name of this category'), 'content': TunableVariant(spells=TunableTuple(entries=TunableList(description='\n List of spells in this category.\n ', tunable=TunableReference(description='The spell.', manager=services.get_instance_manager(Types.SPELL), pack_safe=True)), category_type=TunableEnumEntry(description='\n The category this corresponds to.\n ', tunable_type=BookCategoryDisplayType, default=BookCategoryDisplayType.WITCH_PRACTICAL_SPELL, invalid_enums=(BookCategoryDisplayType.WITCH_POTION,))), potions=TunableTuple(entries=TunableList(description='\n List of potions in this category.\n ', tunable=TunableReference(description="The potion's recipe.", manager=services.get_instance_manager(Types.RECIPE), class_restrictions=('Recipe',), pack_safe=True)), locked_args={'category_type': BookCategoryDisplayType.WITCH_POTION}), default='spells')}
class BuffTransferOp(BaseTargetedLootOperation): __qualname__ = 'BuffTransferOp' FACTORY_TUNABLES = { 'moods_only': Tunable( description= '\n Checking this box will limit buff transfer between Actor to Target Sim to only mood\n associated buffs.', tunable_type=bool, default=True), 'buff_reason': OptionalTunable( description= '\n If set, specify a reason why the buff was added.\n ', tunable=TunableLocalizedString( description= '\n The reason the buff was added. This will be displayed in the\n buff tooltip.\n ' )), 'mood_types': OptionalTunable( TunableList( TunableReference( description= '\n If enabled, only transfer buffs with associated moods in this list.\n ', manager=services.mood_manager()))), 'polarity': OptionalTunable( TunableEnumEntry( description= '\n If enabled, only transfer buffs that match the selected polarity.\n ', tunable_type=BuffPolarity, default=BuffPolarity.NEUTRAL, needs_tuning=True, tuning_group=GroupNames.UI)) } def __init__(self, moods_only, buff_reason, mood_types=None, polarity=None, **kwargs): super().__init__(**kwargs) self._moods_only = moods_only self._buff_reason = buff_reason self._mood_types = mood_types self._polarity = polarity def _apply_to_subject_and_target(self, subject, target, resolver): old_buff_types = list(subject.get_active_buff_types()) if self._moods_only: for buff_entry in old_buff_types: while buff_entry.mood_type is not None: subject.remove_buff_by_type(buff_entry) else: for buff_entry in old_buff_types: subject.remove_buff_by_type(buff_entry) for target_buff in target.get_active_buff_types(): if self._moods_only and target_buff.mood_type is None: pass if self._mood_types is not None and target_buff.mood_type not in self._mood_types: pass if self._polarity is not None and self._polarity is not target_buff.polarity: pass buff_commodity = target_buff.commodity subject.add_buff(target_buff) while buff_commodity is not None: tracker = subject.get_tracker(buff_commodity) tracker.set_max(buff_commodity) subject.set_buff_reason(target_buff, self._buff_reason)
class RepoSituation(SituationComplexCommon): INSTANCE_TUNABLES = { 'repo_person_job_and_role_state': TunableSituationJobAndRoleState( description= '\n The job and role state for the repo-person.\n ', tuning_group=GroupNames.ROLES), 'debtor_sim_job_and_role_state': TunableSituationJobAndRoleState( description= '\n The job and role state for the Sim from the active household whose\n unpaid debt is being collected by the repo-person.\n ', tuning_group=GroupNames.ROLES), 'repo_amount': TunableTuple( description= '\n Tuning that determines the simoleon amount the repo-person is\n trying to collect.\n ', target_amount=TunablePercent( description= '\n The percentage of current debt which determines the base\n amount the repo-person will try to collect.\n ', default=10), min_and_max_collection_range=TunableInterval( description= '\n Multipliers that define the range around the target amount\n that determine which objects should be taken.\n ', tunable_type=float, default_lower=1, default_upper=1), tuning_group=GroupNames.SITUATION), 'save_lock_tooltip': TunableLocalizedString( description= '\n The tooltip to show when the player tries to save the game while\n this situation is running. The save is locked when the situation\n starts.\n ', tuning_group=GroupNames.SITUATION), 'find_object_state': _FindObjectState.TunableFactory( description= '\n The state that picks an object for the repo-person to take.\n ', display_name='1. Find Object State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'nothing_to_take_state': _NothingToTakeState.TunableFactory( description= '\n The state at which there is nothing for the repo-person to take.\n ', display_name='2. Nothing To Take State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'idle_at_object_state': _IdleAtObjectState.TunableFactory( description= '\n The state at which the repo-person waits near the picked object\n and can be asked not to take the object.\n ', display_name='3. Idle At Object State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'repossess_object_state': _RepossessObjectState.TunableFactory( description= '\n The state at which the repo-person will repossess the picked object.\n ', display_name='4. Repossess Object State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'leave_state': _LeaveState.TunableFactory( description= '\n The state at which the repo-person leaves the lot.\n ', display_name='5. Leave State', tuning_group=SituationComplexCommon.SITUATION_STATE_GROUP), 'valid_object_tests': TunableTestSet( description= '\n Test set that determines if an object on the lot is valid for\n repossession.\n ', tuning_group=GroupNames.SITUATION), 'ask_not_to_take_success_chances': TunableList( description= '\n List of values that determine the chance of success of the ask\n not to take interaction, with each chance being used once and then\n moving to the next. After using all the tuned chances the next\n ask not to take interaction will always fail.\n ', tunable=SuccessChance.TunableFactory( description= '\n Chance of success of the "Ask Not To Take" interaction.\n ' ), tuning_group=GroupNames.SITUATION), 'bribe_interaction': TunableInteractionOfInterest( description= '\n If this interaction completes successfully, the repo-person will\n leave the lot without repossessing anything.\n ' ), 'ask_not_to_take_interaction': TunableInteractionOfInterest( description= '\n When this interaction completes, the situation will determine if\n the repo-person should find another object to repossess or not\n based on the tuned success chances.\n ' ), 'ask_not_to_take_failure_notification': OptionalTunable( description= '\n A TNS that displays when an ask-not-to-take interaction fails, if enabled.\n ', tunable=UiDialogNotification.TunableFactory()), 'ask_not_to_take_success_notification': OptionalTunable( description= '\n A TNS that displays when an ask-not-to-take interaction succeeds, if enabled.\n ', tunable=UiDialogNotification.TunableFactory()), 'debt_source': TunableEnumEntry( description= "\n The source of where the debt is coming from and where it'll be removed.\n ", tunable_type=DebtSource, default=DebtSource.SCHOOL_LOAN), 'maximum_object_to_repossess': OptionalTunable( description= '\n The total maximum objects that the situation will take.\n ', tunable=TunableRange( description= '\n The total maximum objects that the situation will take.\n If Use Debt Amount is specified then the situation will keep taking objects\n until there are no more valid objects to take or we have removed all of the\n debt.\n ', tunable_type=int, default=1, minimum=1), enabled_by_default=True, enabled_name='has_maximum_value', disabled_name='use_debt_amount'), 'auto_clear_debt_event': OptionalTunable( description= '\n If enabled then we will have an even we listen to to cancel the debt.\n ', tunable=TunableEnumEntry( description= '\n The event that when triggered will cause all the debt to be cancelled and the\n repo man to leave.\n ', tunable_type=TestEvent, default=TestEvent.Invalid, invalid_enums=(TestEvent.Invalid, ))) } REMOVE_INSTANCE_TUNABLES = Situation.NON_USER_FACING_REMOVE_INSTANCE_TUNABLES def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.objects_to_take = [] self.current_object = None self.ask_not_to_take_success_chances_list = list( self.ask_not_to_take_success_chances) self._reservation_handler = None self._objects_repossessed = 0 @classmethod def _states(cls): return (SituationStateData(1, _WaitForRepoPersonState), SituationStateData(2, _FindObjectState, factory=cls.find_object_state), SituationStateData(3, _NothingToTakeState, factory=cls.nothing_to_take_state), SituationStateData(4, _IdleAtObjectState, factory=cls.idle_at_object_state), SituationStateData(5, _RepossessObjectState, factory=cls.repossess_object_state), SituationStateData(6, _LeaveState, factory=cls.leave_state)) @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return [(cls.repo_person_job_and_role_state.job, cls.repo_person_job_and_role_state.role_state), (cls.debtor_sim_job_and_role_state.job, cls.debtor_sim_job_and_role_state.role_state)] @classmethod def default_job(cls): pass def repo_person(self): sim = next( self.all_sims_in_job_gen(self.repo_person_job_and_role_state.job), None) return sim def debtor_sim(self): sim = next( self.all_sims_in_job_gen(self.debtor_sim_job_and_role_state.job), None) return sim def _cache_valid_objects(self): debt_value = self.get_debt_value() if debt_value is None: self._self_destruct() return target_amount = debt_value * self.repo_amount.target_amount unsorted = [] plex_service = services.get_plex_service() check_common_area = plex_service.is_active_zone_a_plex() debtor_household_id = self.debtor_sim().household_id for obj in services.object_manager().valid_objects(): if not obj.get_household_owner_id() == debtor_household_id: continue if not obj.is_on_active_lot(): continue if check_common_area and plex_service.get_plex_zone_at_position( obj.position, obj.level) is None: continue if not obj.is_connected(self.repo_person()): continue if obj.children: continue resolver = SingleObjectResolver(obj) if self.valid_object_tests.run_tests(resolver): delta = abs(obj.depreciated_value - target_amount) unsorted.append((obj.id, delta)) self.objects_to_take = sorted(unsorted, key=operator.itemgetter(1)) def _on_add_sim_to_situation(self, sim, job_type, role_state_type_override=None): super()._on_add_sim_to_situation( sim, job_type, role_state_type_override=role_state_type_override) if self.debtor_sim() is not None and self.repo_person() is not None: self._cache_valid_objects() self._change_state(self.find_object_state()) def _destroy(self): super()._destroy() self.clear_current_object() services.get_persistence_service().unlock_save(self) if self.auto_clear_debt_event is not None: services.get_event_manager().unregister_single_event( self, self.auto_clear_debt_event) def start_situation(self): services.get_persistence_service().lock_save(self) super().start_situation() self._change_state(_WaitForRepoPersonState()) if self.auto_clear_debt_event is not None: services.get_event_manager().register_single_event( self, self.auto_clear_debt_event) def handle_event(self, sim_info, event, resolver): super().handle_event(sim_info, event, resolver) if self.auto_clear_debt_event is None: return if event != self.auto_clear_debt_event: return self.clear_debt() self._change_state(self.leave_state()) def reduce_debt(self, amount): if self.debt_source == DebtSource.SCHOOL_LOAN: host_sim_info = services.sim_info_manager().get( self._guest_list.host_sim_id) statistic = host_sim_info.get_statistic( LoanTunables.DEBT_STATISTIC, add=False) if statistic is None: return else: statistic.add_value(-amount) elif self.debt_source == DebtSource.BILLS: services.active_household().bills_manager.reduce_amount_owed( amount) else: logger.error('Attempting to use a debt source that is not handled', owner='jjacobson') return def clear_debt(self): if self.debt_source == DebtSource.SCHOOL_LOAN: host_sim_info = services.sim_info_manager().get( self._guest_list.host_sim_id) statistic = host_sim_info.get_statistic( LoanTunables.DEBT_STATISTIC, add=False) if statistic is None: return else: statistic.set_value(0) elif self.debt_source == DebtSource.BILLS: services.active_household().bills_manager.pay_bill(clear_bill=True) else: logger.error( 'Attempting to use a debt source {} that is not handled', self.debt_source, owner='jjacobson') return def get_debt_value(self): if self.debt_source == DebtSource.SCHOOL_LOAN: host_sim_info = services.sim_info_manager().get( self._guest_list.host_sim_id) statistic = host_sim_info.get_statistic( LoanTunables.DEBT_STATISTIC, add=False) if statistic is None: return return statistic.get_value() if self.debt_source == DebtSource.BILLS: return services.active_household( ).bills_manager.current_payment_owed else: logger.error('Attempting to use a debt source that is not handled', owner='jjacobson') return def on_object_repossessed(self): self._objects_repossessed += 1 if self.maximum_object_to_repossess is None or self._objects_repossessed < self.maximum_object_to_repossess: debt_value = self.get_debt_value() if debt_value is not None and debt_value > 0: self._change_state(self.find_object_state()) return self._change_state(self.leave_state()) def get_target_object(self): return self.current_object def get_lock_save_reason(self): return self.save_lock_tooltip def set_current_object(self, obj): self.current_object = obj if self._reservation_handler is not None: logger.error( 'Trying to reserve an object when an existing reservation already exists: {}', self._reservation_handler) self._reservation_handler.end_reservation() self._reservation_handler = self.current_object.get_reservation_handler( self.repo_person()) self._reservation_handler.begin_reservation() def clear_current_object(self): self.current_object = None if self._reservation_handler is not None: self._reservation_handler.end_reservation() self._reservation_handler = None
class SecretLabZoneDirector(RegisterTestEventMixin, SchedulingZoneDirectorMixin, ZoneDirectorBase): INSTANCE_TUNABLES = { 'section_doors': TunableList( description= '\n An ordered set of doors, each of which unlocks a section of the lab\n to explore.\n ', tunable=TunableReference( manager=services.get_instance_manager(Types.OBJECT)), unique_entries=True), 'door_lock_operations': TunableTuple( description= '\n These operations are applied to doors that should be locked\n based on the progress into the zone.\n ', object_state=ObjectStateValue.TunableReference( description= '\n An object state that should be set on the door when locked.\n ' ), lock_data=LockDoor.TunableFactory( description= '\n The LockDoor loot to run on the doors in the lab to lock them.\n ' )), 'door_unlock_operations': TunableTuple( description= '\n These operations are applied to doors that should be unlocked\n based on the progress into the zone.\n ', object_state=ObjectStateValue.TunableReference( description= '\n An object state that should be set on the door when unlocked.\n ' ), lock_data=UnlockDoor.TunableFactory( description= '\n The UnlockDoor loot to run on the doors when they should be unlocked.\n ' )), 'reveal_interactions': TunableInteractionOfInterest( description= '\n Interactions that, when run on a door, reveal the plex associated \n to the interacted door.\n ' ), 'object_commodities_to_fixup_on_load': TunableList( description= '\n Normally object commodities retain their previously saved value on \n load and do not simulate the decay up to the current time.\n This list allows specific objects to update commodities based off\n passage of time if time had elapsed between load and the last time\n the zone was saved.\n ', tunable=TunableTuple( commodity=TunableReference( description= '\n The commodity to fix up if time elapsed since zone was last saved.\n ', manager=services.get_instance_manager( sims4.resources.Types. STATISTIC), class_restrictions='Commodity'), object_test=TunableObjectMatchesDefinitionOrTagTest( description= '\n Test whether or not an object applies for this fixup.\n ' ))) } def __init__(self): super().__init__() self._revealed_plex = 0 self._plex_door_map = self._generate_plex_door_map() self._reset_lab_data() self._command_handlers = { SecretLabCommand.RevealNextSection: self._reveal_next_section, SecretLabCommand.RevealAllSections: self._reveal_all_sections, SecretLabCommand.ResetLab: self._reset_all_sections } def on_startup(self): super().on_startup() self._register_test_event_for_keys( TestEvent.InteractionStart, self.reveal_interactions.custom_keys_gen()) self._register_test_event_for_keys( TestEvent.InteractionComplete, self.reveal_interactions.custom_keys_gen()) def on_shutdown(self): self._unregister_for_all_test_events() super().on_shutdown() def on_loading_screen_animation_finished(self): super().on_loading_screen_animation_finished() active_sim = services.get_active_sim() if active_sim is None: return if active_sim.is_on_active_lot(): return camera.focus_on_sim(services.get_active_sim()) def on_cleanup_zone_objects(self): super().on_cleanup_zone_objects() current_zone = services.current_zone() if current_zone.time_has_passed_in_world_since_zone_save(): if self.object_commodities_to_fixup_on_load: time_of_last_zone_save = current_zone.time_of_last_save() for obj in list(services.object_manager().values()): if not obj.is_on_active_lot(): continue for commodity_fixup in self.object_commodities_to_fixup_on_load: if not commodity_fixup.object_test(objects=(obj, )): continue fixup_commodity = obj.get_stat_instance( commodity_fixup.commodity) if fixup_commodity is not None: fixup_commodity.update_commodity_to_time( time_of_last_zone_save, update_callbacks=True) if self._should_reset_progress_on_load(): self._revealed_plex = 0 self._update_locks_and_visibility() def _determine_zone_saved_sim_op(self): if self._should_reset_progress_on_load(): return _ZoneSavedSimOp.CLEAR return _ZoneSavedSimOp.MAINTAIN def _on_clear_zone_saved_sim(self, sim_info): if sim_info.is_selectable: self._request_spawning_of_sim_at_spawn_point( sim_info, SimSpawnReason.ACTIVE_HOUSEHOLD) return self._send_sim_home(sim_info) def _save_custom_zone_director(self, zone_director_proto, writer): writer.write_uint32(SAVE_LAST_REVEALED_PLEX, self._revealed_plex) super()._save_custom_zone_director(zone_director_proto, writer) def _load_custom_zone_director(self, zone_director_proto, reader): if reader is not None: self._revealed_plex = reader.read_uint32(SAVE_LAST_REVEALED_PLEX, None) super()._load_custom_zone_director(zone_director_proto, reader) def handle_event(self, sim_info, event, resolver): interaction_start = event == TestEvent.InteractionStart interaction_complete = event == TestEvent.InteractionComplete if interaction_start or interaction_complete: if resolver(self.reveal_interactions): door = resolver.get_participant(ParticipantType.Object) try: plex_to_unlock = self.section_doors.index( door.definition) + 1 except ValueError: logger.error('Ran interaction {} on unexpected door {}', resolver.interaction, door) plex_to_unlock = 0 if interaction_complete: self._handle_door_state(sim_info, door, True) else: build_buy.set_plex_visibility(plex_to_unlock, True) self._revealed_plex = max(plex_to_unlock, self._revealed_plex) def handle_command(self, command: SecretLabCommand, **kwargs): if command in self._command_handlers: self._command_handlers[command](**kwargs) def _should_reset_progress_on_load(self): current_zone = services.current_zone() return current_zone.active_household_changed_between_save_and_load( ) or current_zone.time_has_passed_in_world_since_zone_save() def _generate_plex_door_map(self): plex_door_map = {} obj_mgr = services.object_manager() for (i, door_def) in enumerate(self.section_doors, 1): door = next(iter(obj_mgr.get_objects_of_type_gen(door_def)), None) if door is None: logger.error( 'Unable to find the door {} on lot to unlock plex {}', door_def, i) else: plex_door_map[i] = door return plex_door_map def _handle_door_state(self, sim_info, door, set_open): operations = self.door_unlock_operations if set_open else self.door_lock_operations resolver = SingleActorAndObjectResolver(sim_info, door, self) operations.lock_data.apply_to_resolver(resolver) state_value = operations.object_state door.set_state(state_value.state, state_value, force_update=True) def _update_locks_and_visibility(self): active_sim_info = services.active_sim_info() for i in range(1, len(self.section_doors) + 1): reveal = i <= self._revealed_plex build_buy.set_plex_visibility(i, reveal) door = self._plex_door_map.get(i, None) if door is None: continue self._handle_door_state(active_sim_info, door, reveal) def _reset_lab_data(self): self._revealed_plex = 0 def _reveal_next_section(self): self._revealed_plex = min(len(self.section_doors), self._revealed_plex + 1) self._update_locks_and_visibility() def _reveal_all_sections(self): self._revealed_plex = len(self.section_doors) self._update_locks_and_visibility() def _reset_all_sections(self): self._reset_lab_data() self._update_locks_and_visibility()
class GlobalPolicy(DisplaySnippet): GLOBAL_POLICY_TOKEN_NON_ACTIVE = TunableLocalizedStringFactory( description= '\n Display string that appears when trying to use a Global Policy Token\n referencing a non-active Global Policy.\n ' ) INSTANCE_TUNABLES = { 'decay_days': TunableRange( description= '\n The number of days it will take for the global policy to revert to\n not-complete. Decay begins when the policy is completed.\n ', tunable_type=int, default=5, minimum=0), 'progress_initial_value': TunableRange( description= '\n The initial value of global policy progress. Progress begins when\n the policy is first set to in-progress.\n ', tunable_type=int, default=0, minimum=0), 'progress_max_value': TunableRange( description= '\n The max value of global policy progress. Once the policy progress\n reaches the max threshold, global policy state becomes complete.\n ', tunable_type=int, default=100, minimum=1), 'loot_on_decay': TunableList( description= '\n A list of loot actions that will be run when the policy decays.\n ', tunable=LootActions.TunableReference( description= '\n The loot action will target the active Sim.\n ' )), 'loot_on_complete': TunableList( description= '\n A list of loot actions that will be run when the policy is complete.\n ', tunable=LootActions.TunableReference( description= '\n The loot action will target the active Sim.\n ' )), 'global_policy_effects': TunableList( description= '\n Actions to apply when the global policy is enacted.\n ', tunable=GlobalPolicyEffectVariants( description= '\n The action to apply.\n ')) } @classmethod def _verify_tuning_callback(cls): if cls.progress_max_value < cls.progress_initial_value: logger.error( 'Global Policy {} has a max value less than the initial value. This is not allowed.', cls) def __init__(self, progress_initial_value=None, **kwargs): super().__init__(**kwargs) self._progress_state = GlobalPolicyProgressEnum.NOT_STARTED self._progress_value = 0 self.decay_handler = None self.end_time_from_load = 0 @property def progress_state(self): return self._progress_state @property def progress_value(self): return self._progress_value def pre_load(self, global_policy_data): self.set_progress_state(GlobalPolicyProgressEnum( global_policy_data.progress_state), from_load=True) self.set_progress_value(global_policy_data.progress_value, from_load=True) if global_policy_data.decay_days != 0: self.end_time_from_load = global_policy_data.decay_days def set_progress_state(self, progress_enum, from_load=False): old_state = self._progress_state self._progress_state = progress_enum if old_state != self._progress_state and not from_load: services.get_event_manager().process_event( TestEvent.GlobalPolicyProgress, custom_keys=(type(self), self)) def set_progress_value(self, new_value, from_load=False): self._progress_value = new_value if not from_load: self._process_new_value(new_value) return self.progress_state def _process_new_value(self, new_value): if new_value <= self.progress_initial_value and self.progress_state != GlobalPolicyProgressEnum.NOT_STARTED: self.set_progress_state(GlobalPolicyProgressEnum.NOT_STARTED) self.decay_handler = None for effect in self.global_policy_effects: effect.turn_off(self.guid64) elif new_value >= self.progress_max_value and self.progress_state != GlobalPolicyProgressEnum.COMPLETE: self.set_progress_state(GlobalPolicyProgressEnum.COMPLETE) for effect in self.global_policy_effects: effect.turn_on(self.guid64) elif self.progress_state != GlobalPolicyProgressEnum.IN_PROGRESS: self.set_progress_state(GlobalPolicyProgressEnum.IN_PROGRESS) def apply_policy_loot_to_active_sim(self, loot_list, resolver=None): if resolver is None: resolver = SingleSimResolver(services.active_sim_info()) for loot_action in loot_list: loot_action.apply_to_resolver(resolver) def decay_policy(self, timeline): yield timeline.run_child(SleepElement(TimeSpan.ZERO)) services.global_policy_service().set_global_policy_progress( self, self.progress_initial_value) self.decay_handler = None self.apply_policy_loot_to_active_sim(self.loot_on_decay) @classmethod def get_non_active_display(cls, token_data): if token_data.token_property == GlobalPolicyTokenType.NAME: return LocalizationHelperTuning.get_raw_text( token_data.global_policy.display_name()) if token_data.token_property == GlobalPolicyTokenType.PROGRESS: return LocalizationHelperTuning.get_raw_text( cls.GLOBAL_POLICY_TOKEN_NON_ACTIVE()) logger.error( 'Invalid Global Policy Property {} tuned on the Global Policy token.' .format(token_data.property)) def get_active_policy_display(self, token_data): if token_data.token_property == GlobalPolicyTokenType.NAME: return LocalizationHelperTuning.get_raw_text(self.display_name()) if token_data.token_property == GlobalPolicyTokenType.PROGRESS: progress_percentage_str = str( int( round( float(self.progress_value) / float(self.progress_max_value), 2) * 100)) return LocalizationHelperTuning.get_raw_text( progress_percentage_str) logger.error( 'Invalid Global Policy Property {} tuned on the Global Policy token.' .format(token_data.property))
class InventoryStorage: UI_SORT_TYPES = TunableList( description= "\n A list of gameplay-based sort types used in the sim's inventory in the UI.\n ", tunable=TunableTuple( description= '\n Data that defines this sort for the inventory UI.\n ', sort_name=TunableLocalizedString( description= '\n The name displayed in the UI for this sort type.\n ' ), object_data=TunableVariant( description= '\n The object data that determines the sort order of\n this sort type.\n ', states=TunableList( description= '\n States whose values are used to sort on for this sort type. \n ', tunable=TunableReference( description= '\n A State to sort on.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE), class_restrictions='ObjectState')), default='states'), is_ascending=Tunable( description= '\n Whether a higher value from object_data will sort first.\n If a high value means that the object should sort lower \n (E.G. brokenness), this should be false.\n ', tunable_type=bool, default=True), debug_name=Tunable( description= '\n A unique name used to select this inventory sort type through \n the console command ui.inventory.set_sort_filter when the inventory\n ui is open.\n ', tunable_type=str, default='NONE'), export_class_name='InventoryUISortTypeTuple', export_modes=ExportModes.ClientBinary)) UI_FILTER_TYPES = TunableList( description= "\n A list of filter categories containing filter types used to filter the sim's\n inventory in the UI. The inventory can also be sorted by filter type; \n filters lower on this list will sort lower when sorted by filter type.\n ", tunable=TunableTuple( description= '\n A category of filters in the UI. Contains a name and a list of filters.\n ', filters=TunableList( description= '\n The filters used in this category. \n ', tunable=TunableTuple( description= '\n Data that defines a filter type in the inventory UI.\n ', tags=TunableTags( description= '\n Tags that should be considered part of this filter.\n ', binary_type=EnumBinaryExportType.EnumUint32), filter_name=TunableLocalizedString( description= '\n The name displayed in the UI for this filter type. \n ' ), debug_name=Tunable( description= '\n A unique name used to select this inventory filter type through \n the console command ui.inventory.set_sort_filter when the inventory\n ui is open.\n ', tunable_type=str, default='NONE'), export_class_name='InventoryUIFilterTypeTuple')), category_name=TunableLocalizedString( description= '\n The name displayed in the UI for this filter category.\n ' ), export_class_name='InventoryUIFilterCategoryTuple', export_modes=ExportModes.ClientBinary)) def __init__(self, inventory_type, item_location, max_size=None, allow_compaction=True, allow_ui=True, hidden_storage=False): self._objects = {} self._owners = WeakSet() self._inventory_type = inventory_type self._item_location = item_location self._max_size = max_size self._allow_compaction = allow_compaction self._allow_ui = allow_ui self._hidden_storage = hidden_storage self._stacks_with_options_counter = None def __len__(self): return len(self._objects) def __iter__(self): yield from iter(self._objects.values()) def __contains__(self, obj_id): return obj_id in self._objects def __getitem__(self, obj_id): if obj_id in self._objects: return self._objects[obj_id] def __repr__(self): return 'InventoryStorage<{},{}>'.format(self._inventory_type, self._get_inventory_id()) def register(self, owner): self._owners.add(owner) def unregister(self, owner): self._owners.discard(owner) def has_owners(self): if self._owners: return True return False def get_owners(self): return tuple(self._owners) @property def allow_ui(self): return self._allow_ui @allow_ui.setter def allow_ui(self, value): self._allow_ui = value def discard_object_id(self, obj_id): if obj_id in self._objects: del self._objects[obj_id] def discard_all_objects(self): for obj in self._objects.values(): self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) obj.inventoryitem_component.set_inventory_type(None, None) self._objects.clear() def can_insert(self, obj): if not obj.can_go_in_inventory_type(self._inventory_type): return False elif self._max_size is not None and sum( inventory_obj.stack_count() for inventory_obj in self) >= self._max_size: return False return True def insert(self, obj, inventory_object=None, compact=True): if not self.can_insert(obj): return False try: obj.on_before_added_to_inventory() except: logger.exception( 'Exception invoking on_before_added_to_inventory. obj: {}', obj) self._insert(obj, inventory_object) try: obj.on_added_to_inventory() except: logger.exception( 'Exception invoking on_added_to_inventory. obj: {}', obj) compacted_obj_id = None compacted_count = None if compact: (compacted_obj_id, compacted_count) = self._try_compact(obj) if compacted_obj_id is None: for owner in self._owners: try: owner.on_object_inserted(obj) except: logger.exception( 'Exception invoking on_object_inserted. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj) sent_stack_update = False if obj.inventoryitem_component.has_stack_option: if self._stacks_with_options_counter is None: self._stacks_with_options_counter = defaultdict(int) stack_id = obj.inventoryitem_component.get_stack_id() stack_objects = self._stacks_with_options_counter[stack_id] if stack_objects == 0: self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) sent_stack_update = True self._stacks_with_options_counter[stack_id] += 1 if not sent_stack_update: obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim and obj_owner.sim_info.favorites_tracker is not None and obj_owner.sim_info.favorites_tracker.is_favorite_stack( obj): self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) else: for owner in self._owners: try: owner.on_object_id_changed(obj, compacted_obj_id, compacted_count) except: logger.exception( 'Exception invoking on_object_id_changed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj, obj_id=compacted_obj_id) return True def update_object_stack_by_id(self, obj_id, new_stack_id): if obj_id not in self._objects: return obj = self._objects[obj_id] self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) obj.set_stack_id(new_stack_id) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj) def remove(self, obj, count=1, move_to_object_manager=True): if obj.id not in self._objects: return False old_stack_count = obj.stack_count() split_obj = self._try_split(obj, count) try: obj.on_before_removed_from_inventory() except: logger.exception( 'Exception invoking on_before_removed_from_inventory. obj: {}', obj) self._remove(obj, move_to_object_manager=move_to_object_manager) try: obj.on_removed_from_inventory() except: logger.exception( 'Exception invoking on_removed_from_inventory. obj: {}', obj) if split_obj is None: for owner in self._owners: try: owner.on_object_removed(obj) except: logger.exception( 'Exception invoking on_object_removed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_REMOVE, obj) if obj.inventoryitem_component.has_stack_option and self._stacks_with_options_counter is not None: stack_id = obj.inventoryitem_component.get_stack_id() self._stacks_with_options_counter[stack_id] -= 1 if stack_id in self._stacks_with_options_counter <= 0: if self._stacks_with_options_counter[stack_id] < 0: logger.error( 'Counter went negative for stack_id {} with scheme {}', stack_id, obj.inventoryitem_component.stack_scheme, owner='jdimailig') del self._stacks_with_options_counter[stack_id] else: for owner in self._owners: try: owner.on_object_id_changed(split_obj, obj.id, old_stack_count) except: logger.exception( 'Exception invoking on_object_id_changed. obj: {}, owner: {}', obj, owner) self._distribute_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, split_obj, obj_id=obj.id) return True def _insert(self, obj, inventory_object): self._objects[obj.id] = obj obj.inventoryitem_component.set_inventory_type(self._inventory_type, inventory_object) obj.item_location = self._item_location if self._inventory_type == InventoryType.SIM: obj.inventoryitem_component.is_hidden = self._hidden_storage object_manager = services.object_manager() if obj.id in object_manager: object_manager.move_to_inventory( obj, services.current_zone().inventory_manager) obj.set_parent(None) posture_graph_service = services.current_zone( ).posture_graph_service if posture_graph_service.is_object_pending_deletion(obj): posture_graph_service.finalize_object_deletion(obj) def _remove(self, obj, move_to_object_manager=False): if move_to_object_manager: services.current_zone().inventory_manager.move_to_world( obj, services.object_manager()) obj.item_location = ItemLocation.ON_LOT obj.inventoryitem_component.set_inventory_type( None, None, from_removal=not move_to_object_manager) del self._objects[obj.id] def _get_compact_data(self, obj): try: obj.inventoryitem_component.save_for_stack_compaction = True return obj.get_attribute_save_data() finally: obj.inventoryitem_component.save_for_stack_compaction = False obj.post_tooltip_save_data_stored() def _try_compact(self, obj): if not self._allow_compaction: return (None, None) if len(self._objects) < 2: return (None, None) if obj.has_component( components.types.OBJECT_CLAIM_COMPONENT ) and obj.object_claim_component.requires_claiming: return (None, None) similar = None def_id = obj.definition.id data = self._get_compact_data(obj) stack_id = obj.inventoryitem_component.get_stack_id() for other in self._objects.values(): if def_id != other.definition.id: continue if other is obj: continue if stack_id != other.inventoryitem_component.get_stack_id(): continue if not any(interaction.should_reset_based_on_pipeline_progress for interaction in other.interaction_refs): other_data = self._get_compact_data(other) if data == other_data: similar = other break if similar is None: return (None, None) similar_id = similar.id similar_count = similar.stack_count() self._remove(similar) similar.destroy(source=self, cause='InventoryStorage compaction') obj.update_stack_count(similar_count) return (similar_id, similar_count) def _try_split(self, obj, count): if count >= obj.stack_count(): return clone = obj.inventoryitem_component.get_clone_for_stack_split() self._insert(clone, obj.inventoryitem_component.last_inventory_owner) clone.update_stack_count(-count) obj.set_stack_count(count) clone.on_added_to_inventory() return clone def _get_inventory_id(self): if InventoryTypeTuning.is_shared_between_objects(self._inventory_type): return int(self._inventory_type) if self._owners: return next(iter(self._owners)).owner.id logger.error( "Non-shared storage that's missing an owner: InventoryStorage<{},{}>", self._inventory_type, 0) return 0 def _get_inventory_ui_type(self): if InventoryTypeTuning.is_shared_between_objects(self._inventory_type): return UI_pb2.InventoryItemUpdate.TYPE_SHARED return UI_pb2.InventoryItemUpdate.TYPE_OBJECT def _get_inventory_update_message(self, update_type, obj, obj_id=None, allow_while_zone_not_running=False): if not self._allow_ui: return if not services.current_zone( ).is_zone_running and not allow_while_zone_not_running: return if services.current_zone().is_zone_shutting_down: return msg = UI_pb2.InventoryItemUpdate() msg.type = update_type msg.inventory_id = self._get_inventory_id() msg.inventory_type = self._get_inventory_ui_type() msg.stack_id = obj.inventoryitem_component.get_stack_id() if obj_id is None: msg.object_id = obj.id else: msg.object_id = obj_id if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD: add_data = UI_pb2.InventoryItemData() add_data.definition_id = obj.definition.id msg.add_data = add_data if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD or update_type == UI_pb2.InventoryItemUpdate.TYPE_UPDATE: dynamic_data = UI_pb2.DynamicInventoryItemData() dynamic_data.value = obj.current_value dynamic_data.count = obj.stack_count() dynamic_data.new_object_id = obj.id dynamic_data.is_new = obj.new_in_inventory dynamic_data.sort_order = obj.get_stack_sort_order() icon_info = obj.get_icon_info_data() build_icon_info_msg(icon_info, None, dynamic_data.icon_info) recipe_name = obj.get_tooltip_field( TooltipFieldsComplete.recipe_name ) or obj.get_craftable_property(GameObjectProperty.RECIPE_NAME) if recipe_name is not None: dynamic_data.recipe_name = recipe_name if obj.custom_name is not None: dynamic_data.custom_name = obj.custom_name if InventoryStorage.UI_SORT_TYPES: sort_type = 0 for sort_type_data in InventoryStorage.UI_SORT_TYPES: value = None try: abs_value = None state_component = obj.state_component if state_component is None: continue for state in sort_type_data.object_data: if state_component.has_state(state): test_value = float( state_component.get_state(state).value) abs_test_value = abs(test_value) if value is None: value = test_value elif abs_value < abs_test_value: value = test_value abs_value = abs_test_value except TypeError: pass if value is not None: sort_data_item = UI_pb2.InventoryItemSortData() sort_data_item.type = sort_type sort_data_item.value = value dynamic_data.sort_data.append(sort_data_item) sort_type += 1 if update_type == UI_pb2.InventoryItemUpdate.TYPE_ADD: msg.add_data.dynamic_data = dynamic_data else: msg.update_data = dynamic_data if update_type == UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION: dynamic_data = UI_pb2.DynamicInventoryItemData() if obj.inventoryitem_component.has_stack_option: obj.inventoryitem_component.populate_stack_icon_info_data( dynamic_data.icon_info) obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim: favorites_tracker = obj_owner.sim_info.favorites_tracker if favorites_tracker is not None: if favorites_tracker.is_favorite_stack(obj): dynamic_data.is_favorite = True msg.update_data = dynamic_data return msg def _distribute_inventory_update_message(self, update_type, obj, obj_id=None): msg = self._get_inventory_update_message(update_type, obj, obj_id=obj_id) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_inventory_update_message(self, obj): if obj.id not in self._objects: return False msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_inventory_stack_update_message(self, obj): if obj.id not in self._objects: return msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op_with_no_owner(op) def distribute_owned_inventory_update_message(self, obj, owner): if obj.id not in self._objects: return False msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_UPDATE, obj) if msg is not None: op = GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, msg) Distributor.instance().add_op(owner, op) def get_item_update_ops_gen(self): stack_options_set = set() for obj in self._objects.values(): message = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_ADD, obj, allow_while_zone_not_running=True) if message is None: continue yield (obj, GenericProtocolBufferOp(Operation.INVENTORY_ITEM_UPDATE, message)) if not obj.inventoryitem_component.has_stack_option: obj_owner = obj.inventoryitem_component.get_inventory().owner if obj_owner.is_sim: if obj_owner.sim_info.favorites_tracker is None: continue stack_id = obj.inventoryitem_component.get_stack_id() if stack_id in stack_options_set: continue option_msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj, allow_while_zone_not_running=True) if option_msg is not None: stack_options_set.add(stack_id) yield (obj, GenericProtocolBufferOp( Operation.INVENTORY_ITEM_UPDATE, option_msg)) else: stack_id = obj.inventoryitem_component.get_stack_id() if stack_id in stack_options_set: continue option_msg = self._get_inventory_update_message( UI_pb2.InventoryItemUpdate.TYPE_SET_STACK_OPTION, obj, allow_while_zone_not_running=True) if option_msg is not None: stack_options_set.add(stack_id) yield (obj, GenericProtocolBufferOp( Operation.INVENTORY_ITEM_UPDATE, option_msg)) def open_ui_panel(self, obj): if not self._allow_ui: return False msg = UI_pb2.OpenInventory() msg.object_id = obj.id msg.inventory_id = self._get_inventory_id() msg.inventory_type = self._get_inventory_ui_type() op = GenericProtocolBufferOp(Operation.OPEN_INVENTORY, msg) Distributor.instance().add_op_with_no_owner(op) return True
def __init__( self, description='Holds information about carrying and putting down an object.', **kwargs): super().__init__( put_down_tuning=TunableVariant(reference=TunableReference( description= '\n Tuning for how to score where a Sim might want to set an\n object down.\n ', manager=services.get_instance_manager( sims4.resources.Types.STRATEGY)), literal=TunablePutDownStrategy(). TunableFactory(), default='literal'), state_based_put_down_tuning=TunableMapping( description= '\n A mapping from a state value to a putdownstrategy. If the\n owning object is in any of the states tuned here, it will use\n that state\'s associated putdownstrategy in place of the one\n putdownstrategy tuned in the "put_down_tuning" field. If the\n object is in multiple states listed in this mapping, the\n behavior is undefined.\n ', key_type=TunableReference( description= '\n The state value this object must be in in order to use the\n associated putdownstrategy.\n ', manager=services.get_instance_manager( sims4.resources.Types.OBJECT_STATE)), value_type=TunableVariant(reference=TunableReference( description= '\n Tuning for how to score where a Sim might want to set\n an object down.\n ', manager=services.get_instance_manager( sims4.resources.Types.STRATEGY)), literal=TunablePutDownStrategy(). TunableFactory()), key_name='State', value_name='PutDownStrategy'), carry_affordances=OptionalTunable(TunableList( TunableReference( description= '\n The versions of the HoldObject affordance that this object\n supports.\n ', manager=services.affordance_manager())), disabled_name= 'use_default_affordances', enabled_name= 'use_custom_affordances'), provided_affordances=TunableList( description= '\n A list of affordances that are generated when a Sim holding\n this object selects another Sim to interact with. The generated\n interactions will target the selected Sim but will have this\n object set as their carry target.\n ', tunable=TunableReference( manager=services.affordance_manager())), constraint_pick_up=OptionalTunable( description= '\n A list of constraints that must be fulfilled in order to\n interact with this object.\n ', tunable=TunableList( tunable=interactions.constraints.TunableConstraintVariant( description= '\n A constraint that must be fulfilled in order to\n interact with this object.\n ' ))), allowed_hands=TunableVariant(locked_args={ 'both': (Hand.LEFT, Hand.RIGHT), 'left_only': (Hand.LEFT, ), 'right_only': (Hand.RIGHT, ) }, default='both'), holster_while_routing=Tunable( description= '\n If True, the Sim will holster the object before routing and\n unholster when the route is complete.\n ', tunable_type=bool, default=False), holster_compatibility=TunableAffordanceFilterSnippet( description= '\n Define interactions for which holstering this object is\n explicitly disallowed.\n \n e.g. The Scythe is tuned to be holster-incompatible with\n sitting, meaning that Sims will holster the Sctyhe when sitting.\n ' ), unholster_on_long_route_only=Tunable( description= '\n If True, then the Sim will not unholster this object (assuming\n it was previously holstered) unless a transition involving a\n long route is about to happen.\n \n If False, then the standard holstering rules apply.\n ', tunable_type=bool, default=False), prefer_owning_sim_inventory_when_not_on_home_lot=Tunable( description= "\n If checked, this object will highly prefer to be put into the\n owning Sim's inventory when being put down by the owning Sim on\n a lot other than their home lot.\n \n Certain objects, like consumables, should be exempt from this.\n ", tunable_type=bool, default=True), description=description, **kwargs)
class Venue(metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager(sims4.resources.Types.VENUE) ): __qualname__ = 'Venue' INSTANCE_TUNABLES = { 'display_name': TunableLocalizedString( description= '\n Name that will be displayed for the venue\n ', export_modes=ExportModes.All), 'display_name_incomplete': TunableLocalizedString( description= '\n Name that will be displayed for the incomplete venue\n ', export_modes=ExportModes.All), 'venue_description': TunableLocalizedString( description='Description of Venue that will be displayed', export_modes=ExportModes.All), 'venue_icon': TunableResourceKey(None, resource_types=sims4.resources.CompoundTypes.IMAGE, description='Venue Icon for UI', export_modes=ExportModes.All), 'venue_thumbnail': TunableResourceKey(None, resource_types=sims4.resources.CompoundTypes.IMAGE, description='Image of Venue that will be displayed', export_modes=ExportModes.All), 'allow_game_triggered_events': Tunable( description= '\n Whether this venue can have game triggered events. ex for careers\n ', tunable_type=bool, default=False), 'background_event_schedule': TunableSituationWeeklyScheduleFactory( description= '\n The Background Events that run on this venue. They run underneath\n any user facing Situations and there can only be one at a time. The\n schedule times and durations are windows in which background events\n can start.\n ' ), 'special_event_schedule': TunableSituationWeeklyScheduleFactory( description= '\n The Special Events that run on this venue. These run on top of\n Background Events. We run only one user facing event at a time, so\n if the player started something then this may run in the\n background, otherwise the player will be invited to join in on this\n Venue Special Event.\n ' ), 'required_objects': TunableList( description= '\n A list of objects that are required to be on a lot before\n that lot can be labeled as this venue.\n ', tunable=TunableVenueObject( description= "\n Specify object tag(s) that must be on this venue.\n Allows you to group objects, i.e. weight bench,\n treadmill, and basketball goals are tagged as\n 'exercise objects.'\n \n This is not the same as automatic objects tuning. \n Please read comments for both the fields.\n " ), export_modes=ExportModes.All), 'npc_summoning_behavior': sims4.tuning.tunable.TunableMapping( description= '\n Whenever an NPC is summoned to a lot by the player, determine\n which action to take based on the summoning purpose. The purpose\n is a dynamic enum: venues.venue_constants.NPCSummoningPurpose.\n \n The action will generally involve either adding a sim to an existing\n situation or creating a situation then adding them to it.\n \n \\depot\\Sims4Projects\\Docs\\Design\\Open Streets\\Open Street Invite Matrix.xlsx\n \n residential: This is behavior pushed on the NPC if this venue was a residential lot.\n create_situation: Place the NPC in the specified situation/job pair.\n add_to_background_situation: Add the NPC the currently running background \n situation in the venue.\n ', key_type=sims4.tuning.tunable.TunableEnumEntry( venues.venue_constants.NPCSummoningPurpose, venues.venue_constants.NPCSummoningPurpose.DEFAULT), value_type=TunableVariant( locked_args={'disabled': None}, residential=ResidentialLotArrivalBehavior.TunableFactory(), create_situation=CreateAndAddToSituation.TunableFactory(), add_to_background_situation=AddToBackgroundSituation. TunableFactory(), default='disabled'), tuning_group=GroupNames.TRIGGERS), 'player_requires_visitation_rights': OptionalTunable( description= 'If enabled, then lots of this venue type \n will require player Sims that are not on their home lot to go through \n the process of being greeted before they are\n given full rights to using the lot.\n ', tunable=TunableTuple( ungreeted=Situation.TunableReference( description= '\n The situation to create for ungreeted player sims on this lot.', display_name='Player Ungreeted Situation'), greeted=Situation .TunableReference( description= '\n The situation to create for greeted player sims on this lot.', display_name='Player Greeted Situation'))), 'zone_fixup': TunableVariant( description= '\n Specify what to do with a non resident NPC\n when the zone has to be fixed up on load. \n This fix up will occur if sim time or the\n active household has changed since the zone was last saved.\n ', residential=ResidentialZoneFixupForNPC.TunableFactory(), create_situation=CreateAndAddToSituation.TunableFactory(), add_to_background_situation=AddToBackgroundSituation. TunableFactory(), default='residential', tuning_group=GroupNames.SPECIAL_CASES), 'travel_interaction_name': TunableVariant( description= '\n Specify what name a travel interaction gets when this Venue is an\n adjacent lot.\n ', visit_residential=ResidentialTravelDisplayName.TunableFactory( description= '\n The interaction name for when the destination lot is a\n residence.\n ' ), visit_venue=TunableLocalizedStringFactory( description= '\n The interaction name for when the destination lot is a\n commercial venue.\n Tokens: 0:ActorSim\n Example: "Visit The Bar"\n ' ), tuning_group=GroupNames.SPECIAL_CASES), 'travel_with_interaction_name': TunableVariant( description= '\n Specify what name a travel interaction gets when this Venue is an\n adjacent lot.\n ', visit_residential=ResidentialTravelDisplayName.TunableFactory( description= '\n The interaction name for when the destination lot is a\n residence and the actor Sim is traveling with someone.\n ' ), visit_venue=TunableLocalizedStringFactory( description= '\n The interaction name for when the destination lot is a\n commercial venue and the actor is traveling with someone.\n Tokens: 0:ActorSim\n Example: "Visit The Bar With..."\n ' ), tuning_group=GroupNames.SPECIAL_CASES), 'venue_requires_front_door': Tunable( description= '\n True if this venue should run the front door generation code. \n If it runs, venue will have the ring doorbell interaction and \n its additional behavior.\n ', tunable_type=bool, default=False), 'automatic_objects': TunableList( description= '\n A list of objects that is required to exist on this venue (e.g. the\n mailbox). If any of these objects are missing from this venue, they\n will be auto-placed on zone load.', tunable=TunableTuple( description= "\n An item that is required to be present on this venue. The object's tag \n will be used to determine if any similar objects are present. If no \n similar objects are present, then the object's actual definition is used to \n create an object of this type.\n \n This is not the same as required objects tuning. Please read comments \n for both the fields.\n \n E.g. To require a mailbox to be present on a lot, tune a hypothetical basicMailbox \n here. The code will not trigger as long as a basicMailbox, fancyMailbox, or \n cheapMailbox are present on the lot. If none of them are, then a basicMailbox \n will be automatically created.\n ", default_value=TunableReference( manager=services.definition_manager(), description= 'The default object to use if no suitably tagged object is present on the lot.' ), tag=TunableEnumEntry(description='The tag to search for', tunable_type=tag.Tag, default=tag.Tag.INVALID))), 'hide_from_buildbuy_ui': Tunable( description= '\n If True, this venue type will not be available in the venue picker\n in build/buy.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All), 'allows_fire': Tunable( description= '\n If True a fire can happen on this venue, \n otherwise fires will not spawn on this venue.\n ', tunable_type=bool, default=False), 'allow_rolestate_routing_on_navmesh': Tunable( description= '\n Allow all RoleStates routing permission on lot navmeshes of this\n venue type. This is particularly useful for outdoor venue types\n (lots with no walls), where it is awkward to have to "invite a sim\n in" before they may route on the lot, be called over, etc.\n \n This tunable overrides the "Allow Npc Routing On Active Lot"\n tunable of individual RoleStates.\n ', tunable_type=bool, default=False) } @classmethod def _verify_tuning_callback(cls): if cls.special_event_schedule is not None: for entry in cls.special_event_schedule.schedule_entries: while entry.situation.venue_situation_player_job is None: logger.error( 'Venue Situation Player Job {} tuned in Situation: {}', entry.situation.venue_situation_player_job, entry.situation) def __init__(self, **kwargs): self._active_background_event_id = None self._active_special_event_id = None self._background_event_schedule = None self._special_event_schedule = None def set_active_event_ids(self, background_event_id=None, special_event_id=None): self._active_background_event_id = background_event_id self._active_special_event_id = special_event_id @property def active_background_event_id(self): return self._active_background_event_id @property def active_special_event_id(self): return self._active_special_event_id def schedule_background_events(self, schedule_immediate=True): self._background_event_schedule = self.background_event_schedule( start_callback=self._start_background_event, schedule_immediate=False) if schedule_immediate: ( best_time_span, best_data_list ) = self._background_event_schedule.time_until_next_scheduled_event( services.time_service().sim_now, schedule_immediate=True) if best_time_span is not None and best_time_span == date_and_time.TimeSpan.ZERO: while True: for best_data in best_data_list: self._start_background_event( self._background_event_schedule, best_data) def schedule_special_events(self, schedule_immediate=True): self._special_event_schedule = self.special_event_schedule( start_callback=self._try_start_special_event, schedule_immediate=schedule_immediate) def _start_background_event(self, scheduler, alarm_data, extra_data=None): entry = alarm_data.entry situation = entry.situation situation_manager = services.get_zone_situation_manager() if self._active_background_event_id is not None and self._active_background_event_id in situation_manager: situation_manager.destroy_situation_by_id( self._active_background_event_id) situation_id = services.get_zone_situation_manager().create_situation( situation, user_facing=False, spawn_sims_during_zone_spin_up=True) self._active_background_event_id = situation_id def _try_start_special_event(self, scheduler, alarm_data, extra_data): entry = alarm_data.entry situation = entry.situation situation_manager = services.get_zone_situation_manager() if self._active_special_event_id is None: client_manager = services.client_manager() client = next(iter(client_manager.values())) invited_sim = client.active_sim active_sim_available = situation.is_situation_available( invited_sim) def _start_special_event(dialog): guest_list = None if dialog.accepted: start_user_facing = True guest_list = SituationGuestList() guest_info = SituationGuestInfo.construct_from_purpose( invited_sim.id, situation.venue_situation_player_job, SituationInvitationPurpose.INVITED) guest_list.add_guest_info(guest_info) else: start_user_facing = False situation_id = situation_manager.create_situation( situation, guest_list=guest_list, user_facing=start_user_facing) self._active_special_event_id = situation_id if not situation_manager.is_user_facing_situation_running( ) and active_sim_available: dialog = situation.venue_invitation_message( invited_sim, SingleSimResolver(invited_sim)) dialog.show_dialog( on_response=_start_special_event, additional_tokens=( situation.display_name, situation.venue_situation_player_job.display_name)) else: situation_id = situation_manager.create_situation( situation, user_facing=False) self._active_special_event_id = situation_id def shut_down(self): if self._background_event_schedule is not None: self._background_event_schedule.destroy() if self._special_event_schedule is not None: self._special_event_schedule.destroy() situation_manager = services.get_zone_situation_manager() if self._active_background_event_id is not None: situation_manager.destroy_situation_by_id( self._active_background_event_id) self._active_background_event_id = None if self._active_special_event_id is not None: situation_manager.destroy_situation_by_id( self._active_special_event_id) self._active_special_event_id = None @classmethod def lot_has_required_venue_objects(cls, lot): failure_reasons = [] for required_object_tuning in cls.required_objects: object_test = required_object_tuning.object object_list = object_test() num_objects = len(object_list) while num_objects < required_object_tuning.number: pass failure_message = None failure = len(failure_reasons) > 0 if failure: failure_message = '' for message in failure_reasons: failure_message += message + '\n' return (not failure, failure_message) def summon_npcs(self, npc_infos, purpose, host_sim_info=None): if self.npc_summoning_behavior is None: return summon_behavior = self.npc_summoning_behavior.get(purpose) if summon_behavior is None: summon_behavior = self.npc_summoning_behavior.get( venues.venue_constants.NPCSummoningPurpose.DEFAULT) if summon_behavior is None: return summon_behavior(npc_infos, host_sim_info) @classproperty def requires_visitation_rights(cls): return cls.player_requires_visitation_rights is not None @classproperty def player_ungreeted_situation_type(cls): if cls.player_requires_visitation_rights is None: return return cls.player_requires_visitation_rights.ungreeted @classproperty def player_greeted_situation_type(cls): if cls.player_requires_visitation_rights is None: return return cls.player_requires_visitation_rights.greeted
class CarryableComponent( objects.components.Component, component_name=objects.components.types.CARRYABLE_COMPONENT): __qualname__ = 'CarryableComponent' DEFAULT_CARRY_AFFORDANCES = TunableList( TunableReference(manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), description='A list of default carry affordances.') PUT_IN_INVENTORY_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put objects in inventory.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) PUT_DOWN_HERE_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put down here via the\n PutDownContinuationToken liability.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) PUT_DOWN_ANYWHERE_AFFORDANCE = TunableReference( description= '\n The affordance used by carryable component to put down objects anywhere\n via the PutDownContinuationToken liability.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)) def __init__(self, owner, put_down_tuning, state_based_put_down_tuning, carry_affordances, provided_affordances, allowed_hands, holster_while_routing, holster_compatibility, unholster_on_long_route_only, prefer_owning_sim_inventory_when_not_on_home_lot, constraint_pick_up, visibility_override=None, display_name_override=None): super().__init__(owner) self.put_down_tuning = put_down_tuning self.state_based_put_down_tuning = state_based_put_down_tuning self.provided_affordances = provided_affordances self._attempted_putdown = False self._attempted_alternative_putdown = False self._carry_affordances = carry_affordances self.allowed_hands = allowed_hands self.holster_while_routing = holster_while_routing self.holster_compatibility = holster_compatibility self.unholster_on_long_route_only = unholster_on_long_route_only self.constraint_pick_up = constraint_pick_up self.prefer_owning_sim_inventory_when_not_on_home_lot = prefer_owning_sim_inventory_when_not_on_home_lot self._current_put_down_strategy = self.put_down_tuning @property def attempted_putdown(self): return self._attempted_putdown @property def attempted_alternative_putdown(self): return self._attempted_alternative_putdown @property def current_put_down_strategy(self): return self._current_put_down_strategy @property def ideal_slot_type_set(self): return self.current_put_down_strategy.ideal_slot_type_set @componentmethod def get_provided_affordances_gen(self): for affordance in self.provided_affordances: yield CarryTargetInteraction.generate(affordance, self.owner) def component_super_affordances_gen(self, **kwargs): if self._carry_affordances is None: affordances = self.DEFAULT_CARRY_AFFORDANCES else: affordances = self._carry_affordances for affordance in affordances: yield affordance def component_interactable_gen(self): yield self def on_state_changed(self, state, old_value, new_value): if new_value in self.state_based_put_down_tuning or old_value in self.state_based_put_down_tuning: self._generate_put_down_tuning() def _generate_put_down_tuning(self): for (state_value, put_down_strategy) in self.state_based_put_down_tuning.items(): while self.owner.state_value_active(state_value): self._current_put_down_strategy = put_down_strategy break self._current_put_down_strategy = self.put_down_tuning @objects.components.componentmethod def get_put_down_aop(self, interaction, context, alternative_multiplier=1, own_inventory_multiplier=1, object_inventory_multiplier=1, in_slot_multiplier=1, on_floor_multiplier=1, visibility_override=None, display_name_override=None, additional_post_run_autonomy_commodities=None, add_putdown_liability=False, **kwargs): sim = interaction.sim owner = self.owner if not owner.transient: if not self.current_put_down_strategy.affordances: self._attempted_alternative_putdown = True if not self._attempted_alternative_putdown: self._attempted_alternative_putdown = True scored_aops = [] for scored_aop in self._gen_affordance_score_and_aops( interaction, multiplier=alternative_multiplier): while scored_aop.aop.test(context): scored_aops.append(scored_aop) if scored_aops: scored_aops.sort(key=operator.itemgetter(0)) return scored_aops[-1].aop affordance = CarryableComponent.PUT_DOWN_ANYWHERE_AFFORDANCE slot_types_and_costs = self._get_slot_types_and_costs( multiplier=in_slot_multiplier) terrain_transform = self._get_terrain_transform(interaction) objects = self._get_objects_with_inventory(interaction) objects = [ obj for obj in objects if obj.inventory_component.allow_putdown_in_inventory ] if self.current_put_down_strategy.floor_cost is not None and on_floor_multiplier is not None: world_cost = self.current_put_down_strategy.floor_cost * on_floor_multiplier else: world_cost = None if self.current_put_down_strategy.inventory_cost is not None and own_inventory_multiplier is not None: sim_inventory_cost = self.current_put_down_strategy.inventory_cost * own_inventory_multiplier else: sim_inventory_cost = None if self.current_put_down_strategy.object_inventory_cost is not None and object_inventory_multiplier is not None: object_inventory_cost = self.current_put_down_strategy.object_inventory_cost * object_inventory_multiplier else: object_inventory_cost = None aop = AffordanceObjectPair( affordance, self.owner, affordance, None, slot_types_and_costs=slot_types_and_costs, world_cost=world_cost, sim_inventory_cost=sim_inventory_cost, object_inventory_cost=object_inventory_cost, terrain_transform=terrain_transform, objects_with_inventory=objects, visibility_override=visibility_override, display_name_override=display_name_override, additional_post_run_autonomy_commodities= additional_post_run_autonomy_commodities, **kwargs) if add_putdown_liability: _add_putdown_liability_to_aop(aop, interaction) self._attempted_putdown = True return aop return self._get_destroy_aop(sim, **kwargs) def _gen_affordance_score_and_aops(self, interaction, multiplier=1, add_putdown_liability=False): for affordance in self.current_put_down_strategy.affordances: aop = AffordanceObjectPair(affordance, self.owner, affordance, None) if add_putdown_liability: _add_putdown_liability_to_aop(aop, interaction) yield ScoredAOP(multiplier, aop) def _get_cost_for_slot_type(self, slot_type): if slot_type in self.owner.ideal_slot_types: return self.current_put_down_strategy.preferred_slot_cost return self.current_put_down_strategy.normal_slot_cost def _get_slot_types_and_costs(self, multiplier=1): slot_types_and_costs = [] for slot_type in self.owner.all_valid_slot_types: cost = self._get_cost_for_slot_type(slot_type) if cost is not None and multiplier is not None: cost *= multiplier else: cost = None slot_types_and_costs.append((slot_type, cost)) return slot_types_and_costs def _get_terrain_transform(self, interaction): if self.owner.footprint_component is not None: sim = interaction.sim additional_put_down_distance = sim.posture_state.body.additional_put_down_distance starting_position = sim.position + sim.forward * ( sim.object_radius + additional_put_down_distance) sim_los_constraint = sim.lineofsight_component.constraint if not sims4.geometry.test_point_in_compound_polygon( starting_position, sim_los_constraint.geometry.polygon): starting_position = sim.position search_flags = FGLSearchFlag.STAY_IN_CURRENT_BLOCK | FGLSearchFlag.SHOULD_TEST_ROUTING | FGLSearchFlag.CALCULATE_RESULT_TERRAIN_HEIGHTS | FGLSearchFlag.DONE_ON_MAX_RESULTS | FGLSearchFlag.SHOULD_TEST_BUILDBUY MAX_PUTDOWN_STEPS = 8 MAX_PUTDOWN_DISTANCE = 10 (position, orientation) = placement.find_good_location( placement.FindGoodLocationContext( starting_position=starting_position, starting_orientation=sim.orientation, starting_routing_surface=sim.routing_surface, object_footprints=(self.owner.get_footprint(), ), object_id=self.owner.id, max_steps=MAX_PUTDOWN_STEPS, max_distance=MAX_PUTDOWN_DISTANCE, search_flags=search_flags)) if position is not None: put_down_transform = sims4.math.Transform( position, orientation) return put_down_transform def _get_objects_with_inventory(self, interaction): objects = [] inventory_item = self.owner.inventoryitem_component if inventory_item is not None and CarryableComponent.PUT_IN_INVENTORY_AFFORDANCE is not None: while True: for obj in inventory_item.valid_object_inventory_gen(): objects.append(obj) return objects def _get_destroy_aop(self, sim, **kwargs): affordance = CarryableComponent.PUT_DOWN_HERE_AFFORDANCE return AffordanceObjectPair(affordance, self.owner, affordance, None, put_down_transform=None, **kwargs) def reset_put_down_count(self): self._attempted_alternative_putdown = False self._attempted_putdown = False
class AggregateMixerInteraction(MixerInteraction): INSTANCE_TUNABLES = { 'aggregated_affordances': TunableList( description= '\n A list of affordances composing this aggregate. A random one\n will be chosen from sub-action weights if multiple interactions\n pass at the same priority.\n ', tunable=TunableTuple( description= '\n An affordance and priority entry.\n ', priority=Tunable( description= '\n The relative priority of this affordance compared to\n other affordances in this aggregate.\n ', tunable_type=int, default=0), affordance=MixerInteraction.TunableReference( description= '\n The aggregated affordance.\n ', pack_safe=True)), tuning_group=GroupNames.GENERAL) } _allow_user_directed = True @classmethod def _aops_sorted_gen(cls, target, context, super_interaction=DEFAULT, **interaction_parameters): affordances = [] source_interaction = context.sim.posture.source_interaction if super_interaction == DEFAULT else super_interaction for aggregated_affordance in cls.aggregated_affordances: aop = AffordanceObjectPair(aggregated_affordance.affordance, target, source_interaction.affordance, source_interaction, **interaction_parameters) affordances.append((aggregated_affordance.priority, aop)) return sorted(affordances, key=operator.itemgetter(0), reverse=True) @classmethod def _test(cls, target, context, **interaction_parameters): result = super()._test(target, context, **interaction_parameters) if not result: return result cls._allow_user_directed = False context = context.clone_for_sim(sim=context.sim) for (_, aop) in cls._aops_sorted_gen(target, context, **interaction_parameters): result = aop.test(context) if result: if aop.affordance.allow_user_directed: cls._allow_user_directed = True return result return TestResult(False, 'No sub-affordances passed their tests.') @classmethod def consumes_object(cls): for aggregated_affordance in cls.aggregated_affordances: if aggregated_affordance.affordance.consumes_object(): return True return False @classproperty def allow_user_directed(cls): return cls._allow_user_directed def _do_perform_gen(self, timeline): context = self.context.clone_for_continuation(self) max_priority = None aops_valid = [] invalid_aops_with_result = [] for (priority, aop) in self._aops_sorted_gen( self.target, context, super_interaction=self.super_interaction, **self.interaction_parameters): if max_priority is not None: if priority < max_priority: break test_result = aop.test(context) if test_result: aops_valid.append(aop) max_priority = priority else: invalid_aops_with_result.append((aop, test_result)) if not aops_valid: logger.error( 'Failed to find valid mixer affordance in AggregateMixerInteraction: {}, did we not run its test immediately before executing it?\n{}', self, invalid_aops_with_result, owner='rmccord') return ExecuteResult.NONE yield interactions_by_weight = [] for aop in aops_valid: interaction_result = aop.interaction_factory(context) if not interaction_result: raise RuntimeError( 'Failed to generate interaction from aop {}. {} [rmccord]'. format(aop, interaction_result)) interaction = interaction_result.interaction if len(aops_valid) == 1: weight = 0 else: weight = interaction.affordance.calculate_autonomy_weight( context.sim) interactions_by_weight.append((weight, interaction)) if not interactions_by_weight: return ExecuteResult.NONE yield (_, interaction) = max(interactions_by_weight, key=operator.itemgetter(0)) return AffordanceObjectPair.execute_interaction(interaction) yield
class AggregateSuperInteraction(SuperInteraction): INSTANCE_TUNABLES = { 'aggregated_affordances': TunableList( description= '\n A list of affordances composing this aggregate. Distance\n estimation will be used to break ties if there are multiple\n valid interactions at the same priority level.\n ', tunable=TunableTuple( description= '\n An affordance and priority entry.\n ', priority=Tunable( description= '\n The relative priority of this affordance compared to\n other affordances in this aggregate.\n ', tunable_type=int, default=0), affordance=SuperInteraction.TunableReference( description= '\n The aggregated affordance.\n ', pack_safe=True)), tuning_group=GroupNames.GENERAL), 'sim_to_push_affordance_on': TunableEnumEntry( description= '\n The Sim to push the affordance on. If this is Actor, the\n affordance will be pushed as a continuation of this.\n ', tunable_type=ParticipantType, default=ParticipantType.Actor, tuning_group=GroupNames.TRIGGERS), 'use_aggregated_affordance_constraints': Tunable( description= "\n If enabled, this interaction will pull it's constraints from the\n interaction constraints of the aggregated affordances. The benefit\n is that we are compatible with interactions we intend to run, even\n if they have constraints different from one another. This prevents\n us from having to add a bunch of tests to those affordances and a\n generic constraint here.\n ", tunable_type=bool, default=False, tuning_group=GroupNames.CONSTRAINTS) } _allow_user_directed = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._valid_aops = None @classproperty def affordances(cls): return (a.affordance.get_interaction_type() for a in cls.aggregated_affordances) @classmethod def _aops_sorted_gen(cls, target, **interaction_parameters): affordances = [] for aggregated_affordance in cls.aggregated_affordances: aop = AffordanceObjectPair(aggregated_affordance.affordance, target, aggregated_affordance.affordance, None, **interaction_parameters) affordances.append((aggregated_affordance.priority, aop)) return sorted(affordances, key=operator.itemgetter(0), reverse=True) @flexmethod def _get_tested_aops(cls, inst, target, context, **interaction_parameters): inst_or_cls = inst if inst is not None else cls if inst is not None and inst._valid_aops is not None: return inst._valid_aops aops_valid = [] cls._allow_user_directed = False for (priority, aop) in inst_or_cls._aops_sorted_gen(target, **interaction_parameters): test_result = aop.test(context) if test_result: if aop.affordance.allow_user_directed: cls._allow_user_directed = True aops_valid.append((aop, priority)) if inst is not None: inst._valid_aops = aops_valid return aops_valid @flexmethod def test(cls, inst, target=DEFAULT, context=DEFAULT, super_interaction=None, skip_safe_tests=False, **interaction_parameters): inst_or_cls = inst if inst is not None else cls result = super(__class__, inst_or_cls).test(target=target, context=context, super_interaction=super_interaction, skip_safe_tests=skip_safe_tests, **interaction_parameters) if result: target = target if target is not DEFAULT else inst.target context = context if context is not DEFAULT else inst.context context = context.clone_for_sim( cls.get_participant( participant_type=cls.sim_to_push_affordance_on, sim=context.sim, target=target)) valid_aops = inst_or_cls._get_tested_aops(target, context, **interaction_parameters) result = TestResult.TRUE if valid_aops else TestResult( False, 'No sub-affordances passed their tests.') return result @classmethod def consumes_object(cls): for affordance_tuple in cls.aggregated_affordances: if affordance_tuple.affordance.consumes_object(): return True return False @classproperty def allow_user_directed(cls): return cls._allow_user_directed @flexmethod def _constraint_gen(cls, inst, sim, target, participant_type=ParticipantType.Actor, **kwargs): inst_or_cls = cls if inst is None else inst yield from super(SuperInteraction, inst_or_cls)._constraint_gen( sim, target, participant_type=participant_type, **kwargs) if inst_or_cls.use_aggregated_affordance_constraints: aggregated_constraints = [] affordances = [] affordances = [ aop.super_affordance for (aop, _) in inst._valid_aops ] affordances = affordances if not inst is not None or not inst._valid_aops is not None or affordances else [ affordance_tuple.affordance for affordance_tuple in inst_or_cls.aggregated_affordances ] if not affordances: yield Nowhere for aggregated_affordance in affordances: intersection = ANYWHERE constraint_gen = aggregated_affordance.constraint_gen constraint_gen = super(SuperInteraction, aggregated_affordance)._constraint_gen for constraint in constraint_gen( sim, inst_or_cls.get_constraint_target(target), participant_type=participant_type, **kwargs): intersection = constraint.intersect(intersection) if not intersection.valid: continue aggregated_constraints.append(intersection) if aggregated_constraints: yield create_constraint_set( aggregated_constraints, debug_name='AggregatedConstraintSet') def _do_perform_gen(self, timeline): sim = self.get_participant(self.sim_to_push_affordance_on) if sim == self.context.sim: context = self.context.clone_for_continuation(self) else: context = context.clone_for_sim(sim) max_priority = None aops_valid = [] self._valid_aops = None valid_aops = self._get_tested_aops(self.target, context, **self.interaction_parameters) for (aop, priority) in valid_aops: if max_priority is not None: if priority < max_priority: break aops_valid.append(aop) max_priority = priority if not aops_valid: logger.warn( 'Failed to find valid super affordance in AggregateSuperInteraction: {}, did we not run its test immediately before executing it?', self) return ExecuteResult.NONE yield compatible_interactions = [] for aop in aops_valid: interaction_result = aop.interaction_factory(context) if not interaction_result: raise RuntimeError( 'Failed to generate interaction from aop {}. {} [rmccord]'. format(aop, interaction_result)) interaction = interaction_result.interaction if self.use_aggregated_affordance_constraints: if interactions.si_state.SIState.test_compatibility( interaction, force_concrete=True): compatible_interactions.append(interaction) compatible_interactions.append(interaction) if not compatible_interactions: return ExecuteResult.NONE yield interactions_by_distance = [] for interaction in compatible_interactions: if len(compatible_interactions) == 1: distance = 0 else: (distance, _, _) = interaction.estimate_distance() if distance is not None: interactions_by_distance.append((distance, interaction)) else: interactions_by_distance.append( (sims4.math.MAX_INT32, interaction)) (_, interaction) = min(interactions_by_distance, key=operator.itemgetter(0)) return AffordanceObjectPair.execute_interaction(interaction) yield
class ZoneModifier(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.ZONE_MODIFIER)): INSTANCE_TUNABLES = { 'zone_modifier_locked': Tunable( description= '\n Whether this is a locked trait that cannot be assigned/removed\n through build/buy.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All, tuning_group=GroupNames.UI), 'enter_lot_loot': TunableSet( description= '\n Loot applied to Sims when they enter or spawn in on the lot while\n this zone modifier is active.\n \n NOTE: The corresponding exit loot is not guaranteed to be given.\n For example, if the Sim walks onto the lot, player switches to a\n different zone, then summons that Sim, that Sim will bypass\n getting the exit loot.\n A common use case for exit lot loot is to remove buffs granted\n by this zone_mod. This case is already covered as buffs are \n automatically removed if they are non-persistable (have no associated commodity)\n ', tunable=LootActions.TunableReference(pack_safe=True), tuning_group=GroupNames.LOOT), 'exit_lot_loot': TunableSet( description= '\n Loot applied to Sims when they exit or spawn off of the lot while\n this zone modifier is active.\n \n NOTE: This loot is not guaranteed to be given after the enter loot.\n For example, if the Sim walks onto the lot, player switches to a\n different zone, then summons that Sim, that Sim will bypass\n getting the exit loot.\n A common use case for exit lot loot is to remove buffs granted\n by this zone_mod. This case is already covered as buffs are \n automatically removed if they are non-persistable (have no associated commodity)\n ', tunable=LootActions.TunableReference(pack_safe=True), tuning_group=GroupNames.LOOT), 'interaction_triggers': TunableList( description= '\n A mapping of interactions to possible loots that can be applied\n when an on-lot Sim executes them if this zone modifier is set.\n ', tunable=ZoneInteractionTriggers.TunableFactory()), 'schedule': ZoneModifierWeeklySchedule.TunableFactory( description= '\n Schedule to be activated for this particular zone modifier.\n ' ), 'household_actions': TunableList( description= '\n Actions to apply to the household that owns this lot when this zone\n modifier is set.\n ', tunable=ZoneModifierHouseholdActionVariants( description= '\n The action to apply to the household.\n ' )), 'object_tag_to_actions': TunableMapping( description= '\n Mapping of object tag to zone modifier from object actions. Objects \n in this tuning can be buy objects, build objects (column, window, pool),\n and materials (floor tiles, roof tiles, wallpaper).\n \n This is primarily intended for architectural elements such as wallpaper, \n roof materials, windows will give effect to utilities and eco footprint.\n \n NOTE: The actions will only be applied if user enables the \n "Architecture Affects Eco Living" option under Game Options.\n ', key_type=TunableTag( description= '\n The object tag that will be used to do actions.\n ' ), value_type=TunableList( description= '\n The list of action to apply.\n ', tunable=ZoneModifierFromObjectsActionVariants())), 'prohibited_situations': OptionalTunable( description= '\n Optionally define if this zone should prevent certain situations\n from running or getting scheduled.\n ', tunable=SituationIdentityTest.TunableFactory( description= '\n Prevent a situation from running if it is one of the specified \n situations or if it contains one of the specified tags.\n ' )), 'venue_requirements': TunableVariant( description= '\n Whether or not we use a blacklist or white list for the venue\n requirements on this zone modifier.\n ', allowed_venue_types=TunableSet( description= '\n A list of venue types that this Zone Modifier can be placed on.\n All other venue types are not allowed.\n ', tunable=TunableReference( description= '\n A venue type that this Zone Modifier can be placed on.\n ', manager=services.get_instance_manager( sims4.resources.Types.VENUE), pack_safe=True)), prohibited_venue_types=TunableSet( description= '\n A list of venue types that this Zone Modifier cannot be placed on.\n ', tunable=TunableReference( description= '\n A venue type that this Zone Modifier cannot be placed on.\n ', manager=services.get_instance_manager( sims4.resources.Types.VENUE), pack_safe=True)), export_modes=ExportModes.All), 'conflicting_zone_modifiers': TunableSet( description= '\n Conflicting zone modifiers for this zone modifier. If the lot has any of the\n specified zone modifiers, then it is not allowed to be equipped with this\n one.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ZONE_MODIFIER), pack_safe=True), export_modes=ExportModes.All), 'additional_situations': SituationCurve.TunableFactory( description= "\n An additional schedule of situations that can be added in addition\n a situation scheduler's source tuning.\n ", get_create_params={'user_facing': False}), 'zone_wide_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when spawning into a zone with \n this zone modifier. This loot is also applied to all sims, \n objects, etc. in the zone when this zone modifier is added to a lot.\n ', tuning_group=GroupNames.LOOT), 'cleanup_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when this zone modifier is removed.\n ', tuning_group=GroupNames.LOOT), 'on_add_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when this zone modifier is added.\n ', tuning_group=GroupNames.LOOT), 'spin_up_lot_loot': ZoneModifierUpdateAction.TunableFactory( description= '\n Loots applied when the zone spins up.\n ', tuning_group=GroupNames.LOOT), 'utility_supply_surplus_loot': TunableMapping( description= '\n Loots applied when utility supply statistic change\n from deficit to surplus.\n ', key_type=TunableEnumEntry( description= '\n The utility that we want to listen for supply change.\n ', tunable_type=Utilities, default=Utilities.POWER), value_type=ZoneModifierUpdateAction.TunableFactory( description= '\n Loots to apply.\n '), tuning_group=GroupNames.LOOT), 'utility_supply_deficit_loot': TunableMapping( description= '\n Loots applied when utility supply statistic change\n from surplus to deficit.\n ', key_type=TunableEnumEntry( description= '\n The utility that we want to listen for supply change.\n ', tunable_type=Utilities, default=Utilities.POWER), value_type=ZoneModifierUpdateAction.TunableFactory( description= '\n Loots to apply.\n '), tuning_group=GroupNames.LOOT), 'ignore_route_events_during_zone_spin_up': Tunable( description= "\n Don't handle sim route events during zone spin up. Useful for preventing\n unwanted loot from being applied when enter_lot_loot runs situation blacklist tests.\n If we require sims to retrieve loot on zone spin up, we can tune spin_up_lot_loot. \n ", tunable_type=bool, default=False), 'hide_screen_slam': Tunable( description= '\n If checked, this zone modifier will not show the usual screen slam\n when first applied.\n ', tunable_type=bool, default=False, tuning_group=GroupNames.UI) } _obj_tag_id_to_count = None @classproperty def obj_tag_id_to_count(cls): return cls._obj_tag_id_to_count @classmethod def on_start_actions(cls): cls.register_interaction_triggers() @classmethod def on_spin_up_actions(cls, is_build_eco_effects_enabled): sim_spawner_service = services.sim_spawner_service() if not sim_spawner_service.is_registered_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim): sim_spawner_service.register_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.spin_up_lot_loot.apply_all_actions() cls.zone_wide_loot.apply_all_actions() cls.apply_object_actions(is_build_eco_effects_enabled) @classmethod def on_add_actions(cls, is_build_eco_effects_enabled): sim_spawner_service = services.sim_spawner_service() if not sim_spawner_service.is_registered_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim): sim_spawner_service.register_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.register_interaction_triggers() cls.start_household_actions() cls.on_add_loot.apply_all_actions() cls.zone_wide_loot.apply_all_actions() cls.apply_object_actions(is_build_eco_effects_enabled) @classmethod def on_stop_actions(cls): services.sim_spawner_service().unregister_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.unregister_interaction_triggers() cls.stop_household_actions() cls.revert_object_actions() @classmethod def on_remove_actions(cls): services.sim_spawner_service().unregister_sim_spawned_callback( cls.zone_wide_loot.apply_to_sim) cls.unregister_interaction_triggers() cls.stop_household_actions() cls.cleanup_loot.apply_all_actions() cls.revert_object_actions() @classmethod def on_utility_supply_surplus(cls, utility): if utility in cls.utility_supply_surplus_loot: cls.utility_supply_surplus_loot[utility].apply_all_actions() @classmethod def on_utility_supply_deficit(cls, utility): if utility in cls.utility_supply_deficit_loot: cls.utility_supply_deficit_loot[utility].apply_all_actions() @classmethod def handle_event(cls, sim_info, event, resolver): if event not in InteractionTestEvents: return sim = sim_info.get_sim_instance() if sim is None or not sim.is_on_active_lot(): return for trigger in cls.interaction_triggers: trigger.handle_interaction_event(sim_info, event, resolver) @classmethod def start_household_actions(cls): if not cls.household_actions: return household_id = services.owning_household_id_of_active_lot() if household_id is not None: for household_action in cls.household_actions: household_action.start_action(household_id) @classmethod def stop_household_actions(cls): if not cls.household_actions: return household_id = services.owning_household_id_of_active_lot() if household_id is not None: for household_action in cls.household_actions: household_action.stop_action(household_id) @classmethod def _on_build_objects_environment_score_update(cls): household = services.active_household() if household is None: return for sim in household.instanced_sims_gen( allow_hidden_flags=ALL_HIDDEN_REASONS): sim.on_build_objects_environment_score_update() @classmethod def apply_object_actions(cls, is_build_eco_effects_enabled): if not is_build_eco_effects_enabled: return if not cls.object_tag_to_actions: return object_tags = list(cls.object_tag_to_actions.keys()) curr_obj_tag_id_to_count = services.active_lot( ).get_object_count_by_tags(object_tags) if cls._obj_tag_id_to_count is None: delta_obj_tag_id_to_count = curr_obj_tag_id_to_count else: delta_obj_tag_id_to_count = { key: curr_obj_tag_id_to_count[key] - cls._obj_tag_id_to_count[key] for key in curr_obj_tag_id_to_count } zone = services.current_zone() for (obj_tag_id, obj_count) in delta_obj_tag_id_to_count.items(): if obj_count != 0: for action in cls.object_tag_to_actions[Tag(obj_tag_id)]: success = action.apply(obj_count) if not success: continue if action.action_type == ZoneModifierFromObjectsActionType.STATISTIC_CHANGE: zone.zone_architectural_stat_effects[ action.stat.guid64] += action.get_value(obj_count) cls._on_build_objects_environment_score_update() cls._obj_tag_id_to_count = curr_obj_tag_id_to_count @classmethod def revert_object_actions(cls): if not cls._obj_tag_id_to_count: return zone = services.current_zone() for (obj_tag_id, obj_count) in cls._obj_tag_id_to_count.items(): if obj_count != 0: for action in cls.object_tag_to_actions[Tag(obj_tag_id)]: success = action.revert(obj_count) if not success: continue if action.action_type == ZoneModifierFromObjectsActionType.STATISTIC_CHANGE: zone.zone_architectural_stat_effects[ action.stat.guid64] -= action.get_value(obj_count) cls._on_build_objects_environment_score_update() cls._obj_tag_id_to_count = None @classmethod def register_interaction_triggers(cls): services.get_event_manager().register_tests(cls, cls._get_trigger_tests()) @classmethod def unregister_interaction_triggers(cls): services.get_event_manager().unregister_tests(cls, cls._get_trigger_tests()) @classmethod def _get_trigger_tests(cls): tests = list() for trigger in cls.interaction_triggers: tests.extend(trigger.get_trigger_tests()) return tests @classmethod def is_situation_prohibited(cls, situation_type): if cls.prohibited_situations is None: return False return cls.prohibited_situations(situation=situation_type)
class StreetBaseLootEffect(StreetEffect): INSTANCE_SUBCLASSES_ONLY = True INSTANCE_TUNABLES = { 'enact_loot': TunableList( description= '\n If enabled, Loot applied when the effect is enacted\n ', tunable=LootActions.TunableReference( description= '\n Loot applied when the effect is enacted.\n ', pack_safe=True)), 'repeal_loot': TunableList( description= '\n If enabled, Loot applied when the effect is repealed\n ', tunable=LootActions.TunableReference( description= '\n Loot applied when the effect is repealed.\n ', pack_safe=True)), 'scheduled_loot': OptionalTunable( description= '\n While enacted, loot to award on a schedule.\n ', tunable=ScheduledLoot.TunableFactory()) } @classmethod def _verify_tuning_callback(cls): pass @classmethod def _tuning_loaded_callback(cls): if cls.scheduled_loot is not None: cls.scheduled_loot = cls.scheduled_loot() def _collect_resolvers(self): raise NotImplementedError def _enact_for_resolver(self, resolver): for loot in self.enact_loot: loot.apply_to_resolver(resolver) def _repeal_for_resolver(self, resolver): for loot in self.repeal_loot: loot.apply_to_resolver(resolver) def _start_schedule(self): if self.scheduled_loot is not None: self.scheduled_loot.set_resolver_gen(self._collect_resolvers) self.scheduled_loot.start_loot_schedule() def finalize_startup(self, policy): super().finalize_startup(policy) if self._street is not None and self.policy.enacted: self._start_schedule() def enact(self): if self.enact_loot is None: return for resolver in self._collect_resolvers(): self._enact_for_resolver(resolver) self._start_schedule() def repeal(self): if self.repeal_loot is None: return for resolver in self._collect_resolvers(): self._repeal_for_resolver(resolver) if self.scheduled_loot is not None: self.scheduled_loot.set_resolver_gen(None) self.scheduled_loot.stop_loot_schedule()
class FestivalDramaNode(BaseDramaNode): GO_TO_FESTIVAL_INTERACTION = TunablePackSafeReference(description='\n Reference to the interaction used to travel the Sims to the festival.\n ', manager=services.get_instance_manager(sims4.resources.Types.INTERACTION)) INSTANCE_TUNABLES = {'festival_open_street_director': TunableReference(description='\n Reference to the open street director in question.\n ', manager=services.get_instance_manager(sims4.resources.Types.OPEN_STREET_DIRECTOR)), 'street': TunableReference(description='\n The street that this festival is allowed to run on.\n ', manager=services.get_instance_manager(sims4.resources.Types.STREET)), 'scoring': OptionalTunable(description='\n If enabled this DramaNode will be scored and chosen by the drama\n service.\n ', tunable=TunableTuple(description='\n Data related to scoring this DramaNode.\n ', base_score=TunableRange(description='\n The base score of this drama node. This score will be\n multiplied by the score of the different filter results\n used to find the Sims for this DramaNode to find the final\n result.\n ', tunable_type=int, default=1, minimum=1), bucket=TunableEnumEntry(description="\n Which scoring bucket should these drama nodes be scored as\n part of. Only Nodes in the same bucket are scored against\n each other.\n \n Change different bucket settings within the Drama Node's\n module tuning.\n ", tunable_type=DramaNodeScoringBucket, default=DramaNodeScoringBucket.DEFAULT), locked_args={'receiving_sim_scoring_filter': None})), 'pre_festival_duration': TunableSimMinute(description='\n The amount of time in Sim minutes that this festival will be in a\n pre-running state. Testing against this Drama Node will consider\n the node to be running, but the festival will not actually be.\n ', default=120, minimum=1), 'fake_duration': TunableSimMinute(description="\n The amount of time in Sim minutes that we will have this drama node\n run when the festival isn't actually up and running. When the\n festival actually runs we will trust in the open street director to\n tell us when we should actually end.\n ", default=60, minimum=1), 'festival_dynamic_sign_info': OptionalTunable(description='\n If enabled then this festival drama node can be used to populate\n a dynamic sign.\n ', tunable=TunableTuple(description='\n Data for populating the dynamic sign view for the festival.\n ', festival_name=TunableLocalizedString(description='\n The name of this festival.\n '), festival_time=TunableLocalizedString(description='\n The time that this festival should run.\n '), travel_to_festival_text=TunableLocalizedString(description='\n The text that will display to get you to travel to the festival.\n '), festival_not_started_tooltip=TunableLocalizedString(description='\n The tooltip that will display on the travel to festival\n button when the festival has not started.\n '), on_street_tooltip=TunableLocalizedString(description='\n The tooltip that will display on the travel to festival\n button when the player is already at the festival.\n '), on_vacation_tooltip=TunableLocalizedString(description='\n The tooltip that will display on the travel to festival\n button when the player is on vacation.\n '), display_image=TunableResourceKey(description='\n The image for this festival display.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE), background_image=TunableResourceKey(description='\n The background image for this festival display.\n ', default=None, resource_types=sims4.resources.CompoundTypes.IMAGE), activity_info=TunableList(description='\n The different activities that are advertised to be running at this\n festival.\n ', tunable=TunableTuple(description='\n A single activity that will be taking place at this festival.\n ', activity_name=TunableLocalizedString(description='\n The name of this activity.\n '), activity_description=TunableLocalizedString(description='\n The description of this activity.\n '), icon=TunableIcon(description='\n The Icon that represents this festival activity.\n ')))), tuning_group=GroupNames.UI), 'starting_notification': OptionalTunable(description='\n If enabled then when this festival runs we will surface a\n notification to the players.\n ', tunable=TunableTestedUiDialogNotificationSnippet(description='\n The notification that will appear when this drama node runs.\n '), tuning_group=GroupNames.UI), 'additional_drama_nodes': TunableList(description='\n A list of additional drama nodes that we will score and schedule\n when this drama node is run. Only 1 drama node is run.\n ', tunable=TunableReference(description='\n A drama node that we will score and schedule when this drama\n node is run.\n ', manager=services.get_instance_manager(sims4.resources.Types.DRAMA_NODE))), 'delay_timeout': TunableSimMinute(description='\n The amount of time in Sim minutes that the open street director has\n been delayed that we will no longer start the festival.\n ', default=120, minimum=0), 'travel_lot_override': OptionalTunable(description='\n If enabled, sims will spawn at this lot instead of the Travel Lot \n tuned on the street.\n ', tunable=TunableLotDescription(description='\n The specific lot that we will travel to when asked to travel to\n this street.\n ')), 'reject_same_street_travel': Tunable(description='\n If True, we will disallow the drama node travel interaction to run\n if the Sim is on the same street as the destination zone. If False,\n same street travel will be allowed.\n ', tunable_type=bool, default=True)} REMOVE_INSTANCE_TUNABLES = ('receiver_sim', 'sender_sim_info', 'picked_sim_info') @classproperty def drama_node_type(cls): return DramaNodeType.FESTIVAL @classproperty def persist_when_active(cls): return True @classproperty def simless(cls): return True @classmethod def get_travel_lot_id(cls, reject_same_street=False): if reject_same_street and cls.street is services.current_street(): return if cls.travel_lot_override is not None: return get_lot_id_from_instance_id(cls.travel_lot_override) return get_lot_id_from_instance_id(cls.street.travel_lot) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._duration_alarm = None self._additional_nodes_processor = None def cleanup(self, from_service_stop=False): super().cleanup(from_service_stop=from_service_stop) if self._duration_alarm is not None: alarms.cancel_alarm(self._duration_alarm) self._duration_alarm = None if self._additional_nodes_processor is not None: self._additional_nodes_processor.trigger_hard_stop() self._additional_nodes_processor = None def _alarm_finished_callback(self, _): services.drama_scheduler_service().complete_node(self.uid) def _request_timed_out_callback(self): services.drama_scheduler_service().complete_node(self.uid) def _open_street_director_destroyed_early_callback(self): services.drama_scheduler_service().complete_node(self.uid) def _get_time_till_end(self): now = services.time_service().sim_now time_since_started = now - self._selected_time duration = create_time_span(minutes=self.fake_duration + self.pre_festival_duration) time_left_to_go = duration - time_since_started return time_left_to_go def _setup_end_alarm(self): time_left_to_go = self._get_time_till_end() self._duration_alarm = alarms.add_alarm(self, time_left_to_go, self._alarm_finished_callback) def _create_open_street_director_request(self): festival_open_street_director = self.festival_open_street_director(drama_node_uid=self._uid) preroll_time = self._selected_time + create_time_span(minutes=self.pre_festival_duration) request = OpenStreetDirectorRequest(festival_open_street_director, priority=festival_open_street_director.priority, preroll_start_time=preroll_time, timeout=create_time_span(minutes=self.delay_timeout), timeout_callback=self._request_timed_out_callback, premature_destruction_callback=self._open_street_director_destroyed_early_callback) services.venue_service().request_open_street_director(request) def _try_and_start_festival(self): street = services.current_street() if street is not self.street: self._setup_end_alarm() return self._create_open_street_director_request() def _process_scoring_gen(self, timeline): try: yield from services.drama_scheduler_service().score_and_schedule_nodes_gen(self.additional_drama_nodes, 1, street_override=self.street, timeline=timeline) except GeneratorExit: raise except Exception as exception: logger.exception('Exception while scoring DramaNodes: ', exc=exception, level=sims4.log.LEVEL_ERROR) finally: self._additional_nodes_processor = None def _pre_festival_alarm_callback(self, _): self._try_and_start_festival() services.get_event_manager().process_events_for_household(TestEvent.FestivalStarted, services.active_household()) if self.starting_notification is not None: resolver = GlobalResolver() starting_notification = self.starting_notification(services.active_sim_info(), resolver=resolver) starting_notification.show_dialog(response_command_tuple=tuple([CommandArgType.ARG_TYPE_INT, self.guid64])) if self.additional_drama_nodes: sim_timeline = services.time_service().sim_timeline self._additional_nodes_processor = sim_timeline.schedule(elements.GeneratorElement(self._process_scoring_gen)) def _setup_pre_festival_alarm(self): now = services.time_service().sim_now time_since_started = now - self._selected_time duration = create_time_span(minutes=self.pre_festival_duration) time_left_to_go = duration - time_since_started self._duration_alarm = alarms.add_alarm(self, time_left_to_go, self._pre_festival_alarm_callback) def _run(self): self._setup_pre_festival_alarm() services.get_event_manager().process_events_for_household(TestEvent.FestivalStarted, services.active_household()) return DramaNodeRunOutcome.SUCCESS_NODE_INCOMPLETE def resume(self): now = services.time_service().sim_now time_since_started = now - self._selected_time if time_since_started < create_time_span(minutes=self.pre_festival_duration): self._setup_pre_festival_alarm() else: self._try_and_start_festival() def is_on_festival_street(self): street = services.current_street() return street is self.street def is_during_pre_festival(self): now = services.time_service().sim_now time_since_started = now - self._selected_time if time_since_started < create_time_span(minutes=self.pre_festival_duration): return True return False @classmethod def show_festival_info(cls): if cls.festival_dynamic_sign_info is None: return ui_info = cls.festival_dynamic_sign_info festival_info = UI_pb2.DynamicSignView() festival_info.drama_node_guid = cls.guid64 festival_info.name = ui_info.festival_name lot_id = cls.get_travel_lot_id() persistence_service = services.get_persistence_service() zone_id = persistence_service.resolve_lot_id_into_zone_id(lot_id, ignore_neighborhood_id=True) zone_protobuff = persistence_service.get_zone_proto_buff(zone_id) if zone_protobuff is not None: festival_info.venue = LocalizationHelperTuning.get_raw_text(zone_protobuff.name) festival_info.time = ui_info.festival_time festival_info.image = sims4.resources.get_protobuff_for_key(ui_info.display_image) festival_info.background_image = sims4.resources.get_protobuff_for_key(ui_info.background_image) festival_info.action_label = ui_info.travel_to_festival_text running_nodes = services.drama_scheduler_service().get_running_nodes_by_class(cls) active_sim_info = services.active_sim_info() if all(active_node.is_during_pre_festival() for active_node in running_nodes): festival_info.disabled_tooltip = ui_info.festival_not_started_tooltip elif any(active_node.is_on_festival_street() for active_node in running_nodes): festival_info.disabled_tooltip = ui_info.on_street_tooltip elif active_sim_info.is_in_travel_group(): festival_info.disabled_tooltip = ui_info.on_vacation_tooltip for activity in ui_info.activity_info: with ProtocolBufferRollback(festival_info.activities) as activity_msg: activity_msg.name = activity.activity_name activity_msg.description = activity.activity_description activity_msg.icon = create_icon_info_msg(IconInfoData(activity.icon)) distributor = Distributor.instance() distributor.add_op(active_sim_info, GenericProtocolBufferOp(Operation.DYNAMIC_SIGN_VIEW, festival_info)) @classmethod def travel_to_festival(cls): active_sim_info = services.active_sim_info() active_sim = active_sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS_EXCEPT_UNINITIALIZED) if active_sim is None: return lot_id = cls.get_travel_lot_id(reject_same_street=cls.reject_same_street_travel) if lot_id is None: return pick = PickInfo(pick_type=PickType.PICK_TERRAIN, lot_id=lot_id, ignore_neighborhood_id=True) context = interactions.context.InteractionContext(active_sim, interactions.context.InteractionContext.SOURCE_SCRIPT_WITH_USER_INTENT, interactions.priority.Priority.High, insert_strategy=interactions.context.QueueInsertStrategy.NEXT, pick=pick) active_sim.push_super_affordance(FestivalDramaNode.GO_TO_FESTIVAL_INTERACTION, None, context)
class StreetResidentSimLootEffect(StreetEffect): INSTANCE_TUNABLES = { 'enact_loot': TunableList( description= "\n If enabled, Loot applied on a Street's resident Sims when the effect is enacted\n ", tunable=LootActions.TunableReference( description= "\n Loot applied on a Street's resident Sims when the effect is enacted.\n ", pack_safe=True)), 'repeal_loot': TunableList( description= "\n If enabled, Loot applied on a Street's resident Sims when the effect is repealed\n ", tunable=LootActions.TunableReference( description= "\n Loot applied on a Street's resident Sims when the effect is repealed.\n ", pack_safe=True)), 'scheduled_loot': OptionalTunable( description= '\n While enacted, loot to award on a schedule.\n ', tunable=ScheduledLoot.TunableFactory()) } @classmethod def _verify_tuning_callback(cls): pass @classmethod def _tuning_loaded_callback(cls): if cls.scheduled_loot is not None: cls.scheduled_loot = cls.scheduled_loot() def _start_schedule(self): if self.scheduled_loot is not None: self.scheduled_loot.set_resolver_gen(lambda: [ SingleSimResolver(sim_info) for sim_info in self.policy.provider.get_resident_sim_infos() ]) self.scheduled_loot.start_loot_schedule() def _enact_for_sim_info(self, sim_info): resolver = SingleSimResolver(sim_info) for loot in self.enact_loot: loot.apply_to_resolver(resolver) def _repeal_for_sim_info(self, sim_info): resolver = SingleSimResolver(sim_info) for loot in self.repeal_loot: loot.apply_to_resolver(resolver) def finalize_startup(self, policy): super().finalize_startup(policy) if self._street is None: return def handle_moved_sim_info(sim_info, old_street, new_street): if not self.policy.enacted: return if old_street is self._street and self.repeal_loot is not None: self._repeal_for_sim_info(sim_info) if new_street is self._street and self.enact_loot is not None: self._enact_for_sim_info(sim_info) services.street_service( ).register_sim_info_home_street_change_callback( self._street, handle_moved_sim_info) if self.policy.enacted: self._start_schedule() def enact(self): if self.enact_loot is not None: for sim_info in self.policy.provider.get_resident_sim_infos(): self._enact_for_sim_info(sim_info) self._start_schedule() def repeal(self): if self.repeal_loot is not None: for sim_info in self.policy.provider.get_resident_sim_infos(): self._repeal_for_sim_info(sim_info) if self.scheduled_loot is not None: self.scheduled_loot.set_resolver_gen(None) self.scheduled_loot.stop_loot_schedule()
class Reward(HasTunableReference, metaclass=HashedTunedInstanceMetaclass, manager=services.get_instance_manager( sims4.resources.Types.REWARD)): INSTANCE_SUBCLASSES_ONLY = True INSTANCE_TUNABLES = { 'name': TunableLocalizedString( description= '\n The display name for this reward.\n ', allow_catalog_name=True, export_modes=ExportModes.All), 'reward_description': OptionalTunable( description= '\n If enabled, this text is used to describe this reward.\n ', tunable=TunableLocalizedString( description= '\n Description for this reward.\n ', export_modes=ExportModes.All), export_modes=ExportModes.All), 'icon': TunableResourceKey( description= '\n The icon image for this reward.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, export_modes=ExportModes.All), 'tests': TunableTestSet( description= '\n A series of tests that must pass in order for reward to be available.\n ' ), 'rewards': TunableList( TunableVariant( description= '\n The gifts that will be given for this reward. They can be either\n a specific reward or a random reward, in the form of a list of\n specific rewards.\n ', specific_reward=TunableSpecificReward(), random_reward=TunableList(TunableRandomReward()))), 'notification': OptionalTunable( description= '\n If enabled, this notification will show when the sim/household receives this reward.\n ', tunable=TunableUiDialogNotificationSnippet()), 'reward_unavailable_tooltip': OptionalTunable( description= '\n If enabled, this text will appear if a reward is unavailable. \n Otherwise the default unavailable reward text is used.\n ', tunable=TunableLocalizedStringFactory()) } @classmethod def give_reward(cls, sim_info, disallowed_reward_types=()): raise NotImplementedError @classmethod def try_show_notification(cls, sim_info): if cls.notification is not None: dialog = cls.notification(sim_info, SingleSimResolver(sim_info)) dialog.show_dialog() @classmethod def is_valid(cls, sim_info): if not cls.tests.run_tests(SingleSimResolver(sim_info)): return False for reward in cls.rewards: if not isinstance(reward, tuple): reward_instance = reward() return reward_instance.valid_reward(sim_info) for each_reward in reward: reward_instance = each_reward.reward() if not reward_instance.valid_reward(sim_info): return False return True @classmethod def get_unavailable_tooltip(cls, sim_info): if cls.reward_unavailable_tooltip is not None: return cls.reward_unavailable_tooltip(sim_info)
class StreetInstancedSimLootEffect(StreetEffect): INSTANCE_TUNABLES = { 'enact_loot': TunableList( description= '\n Loots applied when a sim is instanced on a street where this effect\n is enacted.\n ', tunable=LootActions.TunableReference(pack_safe=True)), 'repeal_loot': TunableList( description= '\n Loots applied when a sim is de-instanced on a street where this\n effect is enacted. \n ', tunable=LootActions.TunableReference(pack_safe=True)), 'scheduled_loot': OptionalTunable( description= '\n While enacted, loot to award on a schedule.\n ', tunable=ScheduledLoot.TunableFactory()) } @classmethod def _verify_tuning_callback(cls): pass @classmethod def _tuning_loaded_callback(cls): if cls.scheduled_loot is not None: cls.scheduled_loot = cls.scheduled_loot() def _register_callbacks(self): street_service = services.street_service() street_service.register_sim_added_callback(self._street, self._enact_for_sim_info) street_service.register_sim_removed_callback(self._street, self._repeal_for_sim_info) def _unregister_callbacks(self): street_service = services.street_service() street_service.unregister_sim_added_callback(self._street, self._enact_for_sim_info) street_service.unregister_sim_removed_callback( self._street, self._repeal_for_sim_info) def _enact_for_sim_info(self, sim_info): resolver = SingleSimResolver(sim_info) for loot in self.enact_loot: loot.apply_to_resolver(resolver) def _repeal_for_sim_info(self, sim_info): resolver = SingleSimResolver(sim_info) for loot in self.repeal_loot: loot.apply_to_resolver(resolver) def _start_schedule(self): if self.scheduled_loot is not None: self.scheduled_loot.set_resolver_gen(lambda: [ SingleSimResolver(sim.sim_info) for sim in services.sim_info_manager().instanced_sims_gen() ]) self.scheduled_loot.start_loot_schedule() def finalize_startup(self, policy): super().finalize_startup(policy) if self._street is None: return if self.policy.enacted and self._street is services.current_street(): for sim in services.sim_info_manager().instanced_sims_gen(): self._enact_for_sim_info(sim.sim_info) self._register_callbacks() if self.policy.enacted: self._start_schedule() def enact(self): if self.enact_loot is None: return for sim in services.sim_info_manager().instanced_sims_gen(): self._enact_for_sim_info(sim.sim_info) self._register_callbacks() self._start_schedule() def repeal(self): if self.repeal_loot is None: return for sim in services.sim_info_manager().instanced_sims_gen(): self._repeal_for_sim_info(sim.sim_info) self._unregister_callbacks() if self.scheduled_loot is not None: self.scheduled_loot.set_resolver_gen(None) self.scheduled_loot.stop_loot_schedule()
class ObjectCreationMixin: INVENTORY = 'inventory' CARRY = 'carry' INSTANCE_TUNABLES = FACTORY_TUNABLES = { 'creation_data': TunableObjectCreationDataVariant( description= '\n Define the object to create.\n '), 'initial_states': TunableList( description= '\n A list of states to apply to the object as soon as it is created.\n ', tunable=TunableTuple( description= '\n The state to apply and optional tests to decide if the state\n should apply.\n ', state=TunableStateValueReference(), tests=OptionalTunable( description= '\n If enabled, the state will only get set on the created\n object if the tests pass. Note: These tests can not be\n performed on the newly created object.\n ', tunable=TunableTestSet()))), 'destroy_on_placement_failure': Tunable( description= "\n If checked, the created object will be destroyed on placement failure.\n If unchecked, the created object will be placed into an appropriate\n inventory on placement failure if possible. If THAT fails, object\n will be destroyed.\n By default it goes into location target's inventory, you can use \n fallback_location_target_override to make the created object go to\n another participant's inventory.\n ", tunable_type=bool, default=False), 'owner_sim': TunableEnumEntry( description= '\n The participant Sim whose household should own the object. Leave this\n as Invalid to not assign ownership.\n ', tunable_type=ParticipantTypeSingleSim, default=ParticipantType.Invalid), 'location': TunableVariant( description= '\n Where the object should be created.\n ', default='position', position=_PlacementStrategyLocation.TunableFactory(), slot=_PlacementStrategySlot.TunableFactory(), inventory=TunableTuple( description= '\n An inventory based off of the chosen Participant Type.\n ', locked_args={'location': INVENTORY}, location_target=TunableEnumEntry( description= '\n "The owner of the inventory the object will be created in."\n ', tunable_type=ParticipantType, default=ParticipantType.Actor), mark_object_as_stolen_from_career=Tunable( description= '\n Marks the object as stolen from a career by the tuned location_target participant.\n This should only be checked if this basic extra is on a CareerSuperInteraction.\n ', tunable_type=bool, default=False), place_in_hidden_inventory=Tunable( description= '\n If True, the object is placed in the hidden inventory rather than the user-facing inventory.\n ', tunable_type=bool, default=False)), carry=TunableTuple( description= '\n Carry the object. Note: This expects an animation in the\n interaction to trigger the carry.\n ', locked_args={'location': CARRY}, carry_track_override=OptionalTunable( description= '\n If enabled, specify which carry track the Sim must use to carry the\n created object.\n ', tunable=TunableEnumEntry( description= '\n Which hand to carry the object in.\n ', tunable_type=PostureTrackGroup, default=PostureTrack.RIGHT)))), 'reserve_object': OptionalTunable( description= '\n If this is enabled, the created object will be reserved for use by\n the set Sim.\n ', tunable=TunableEnumEntry( tunable_type=ParticipantTypeActorTargetSim, default=ParticipantTypeActorTargetSim.Actor)), 'fallback_location_target_override': OptionalTunable( description= "\n This will be ignored if destroy_on_placement_failure is checked. If this is enabled, we override fallback\n location target.\n Currently this is used when location target is different with the target whose inventory we want this\n created object to go into. For example we want to create an object near another object but we want this\n object to go to actor's inventory when placement fails.\n ", tunable=TunableEnumEntry(tunable_type=ParticipantType, default=ParticipantType.Actor)), 'notification_inventory': OptionalTunable( description= '\n The notification to show when created object is placed in an inventory.\n ', tunable=TunableTuple( participant_inventory=UiDialogNotification.TunableFactory( description= "\n The notification to show when created object is placed in a participant's (such as sim's) inventory.\n " ), household_inventory=UiDialogNotification.TunableFactory( description= '\n The notification to show when created object is placed in a household inventory.\n ' ))), 'temporary_tags': OptionalTunable( description= '\n If enabled, these Tags are added to the created object and DO NOT\n persist.\n ', tunable=TunableSet( description= '\n A set of temporary tags that are added to the created object.\n These tags DO NOT persist.\n ', tunable=TunableEnumEntry( description= '\n A tag that is added to the created object. This tag DOES\n NOT persist.\n ', tunable_type=Tag, default=Tag.INVALID), minlength=1)), 'require_claim': Tunable( description= "\n If checked, the created object will be claimed, and will need to\n be reclaimed on load. If it isn't reclaimed on load, the object\n will be destroyed.\n ", tunable_type=bool, default=False), 'set_sim_as_owner': Tunable( description= '\n If checked and owner_sim is set, the sim will also be set on the\n object ownership component and not just the household.\n ', tunable_type=bool, default=False), 'set_value_to_crafted_tooltip': Tunable( description= '\n If checked, the value will be set to the tooltip if this item has\n a crafting component.\n ', tunable_type=bool, default=True) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.resolver = None self._object_helper = None self._assigned_ownership = set() self._definition = None self._setup_params = None def initialize_helper(self, resolver, post_add=None): self._assigned_ownership.clear() self.resolver = resolver reserved_sim = None if self.reserve_object is not None: reserved_sim_info = self.resolver.get_participant( self.reserve_object) reserved_sim = reserved_sim_info.get_sim_instance() interaction = None if isinstance(self.resolver, InteractionResolver): interaction = self.resolver.interaction (self._definition, self._setup_params) = self.creation_data.get_creation_params(resolver) self._object_helper = CreateObjectHelper( reserved_sim, self._definition, interaction, object_to_clone=self.creation_data.get_source_object( self.resolver), init=self._setup_created_object, post_add=post_add) @property def definition(self): return self.creation_data.get_definition(self.resolver) def create_object(self, resolver): self.initialize_helper(resolver, post_add=self._place_object) created_object = self._object_helper.create_object() self._object_helper = None return created_object def _setup_created_object(self, created_object): self.creation_data.setup_created_object(self.resolver, created_object, **self._setup_params) if self.owner_sim != ParticipantType.Invalid: owner_sim = self.resolver.get_participant(self.owner_sim) if owner_sim is not None and owner_sim.is_sim: created_object.update_ownership( owner_sim, make_sim_owner=self.set_sim_as_owner) self._assigned_ownership.add(created_object.id) for initial_state in self.initial_states: if created_object.state_component is None: created_object.add_component(StateComponent(created_object)) if not initial_state.tests is None: if initial_state.tests.run_tests(self.resolver): if created_object.has_state(initial_state.state.state): created_object.set_state(initial_state.state.state, initial_state.state, from_creation=True) if created_object.has_state(initial_state.state.state): created_object.set_state(initial_state.state.state, initial_state.state, from_creation=True) if self.temporary_tags is not None: created_object.append_tags(self.temporary_tags) if created_object.has_component( objects.components.types.CRAFTING_COMPONENT): created_object.crafting_component.update_simoleon_tooltip() created_object.crafting_component.update_quality_tooltip() if self.set_value_to_crafted_tooltip: created_object.update_tooltip_field( TooltipFieldsComplete.simoleon_value, created_object.current_value) created_object.update_object_tooltip() if self.require_claim: created_object.claim() def _get_ignored_object_ids(self): pass def _place_object_no_fallback(self, created_object): if hasattr(self.location, 'try_place_object'): ignored_object_ids = self._get_ignored_object_ids() return self.location.try_place_object( created_object, self.resolver, ignored_object_ids=ignored_object_ids) elif self.location.location == self.CARRY: return True return False def _get_fallback_location_target(self, created_object): if self.fallback_location_target_override is not None: target_override = self.resolver.get_participant( self.fallback_location_target_override) if target_override is not None: return target_override logger.error( 'Fallback location target override for participant {} and created object {} is none.\n Invalid participant?', self.fallback_location_target_override, created_object) if hasattr(self.location, '_get_reference_objects_gen'): for obj in self.location._get_reference_objects_gen( created_object, self.resolver): return obj return self.resolver.get_participant(self.location.location_target) def _place_object(self, created_object): self._setup_created_object(created_object) if self._place_object_no_fallback(created_object): return True if not self.destroy_on_placement_failure: participant = self._get_fallback_location_target(created_object) if participant.is_sim: if isinstance(participant, sims.sim_info.SimInfo): participant = participant.get_sim_instance( allow_hidden_flags=ALL_HIDDEN_REASONS) location_type = getattr(self.location, 'location', None) if location_type == self.INVENTORY and self.location.mark_object_as_stolen_from_career: interaction = self.resolver.interaction if interaction is None: logger.error( 'Mark Object As Stolen From Career is checked on CreateObject loot {}. \n This should only be check on basic extra in a CareerSuperInteraction.', self) return False career_uid = interaction.interaction_parameters.get( 'career_uid') if career_uid is not None: career = interaction.sim.career_tracker.get_career_by_uid( career_uid) if career is not None: name_data = career.get_career_location( ).get_persistable_company_name_data() text = None guid = None if isinstance(name_data, str): text = name_data else: guid = name_data MarkObjectAsStolen.mark_object_as_stolen( created_object, stolen_from_text=text, stolen_from_career_guid=guid) else: logger.error( 'Interaction {} is tuned with a CreateObject basic extra that has mark_object_as_stolen_from_career as True,\n but is not a CareerSuperInteraction. This is not supported.', interaction) if created_object.inventoryitem_component is not None: if created_object.id not in self._assigned_ownership: if participant.is_sim: participant_household_id = participant.household.id else: participant_household_id = participant.get_household_owner_id( ) created_object.set_household_owner_id( participant_household_id) self._assigned_ownership.add(created_object.id) if participant.inventory_component.player_try_add_object( created_object, hidden=location_type == self.INVENTORY and self.location.place_in_hidden_inventory): if self.notification_inventory: notification = self.notification_inventory.participant_inventory( participant, self.resolver) notification.show_dialog() return True sim = self.resolver.get_participant(ParticipantType.Actor) if not (participant is not None and participant.inventory_component is not None and sim is None or not sim.is_sim): owning_household = services.owning_household_of_active_lot() if owning_household is not None: for sim_info in owning_household.sim_info_gen(): if sim_info.is_instanced(): sim = sim_info.get_sim_instance() break if sim is not None: if not sim.is_npc: try: created_object.set_household_owner_id(sim.household.id) if build_buy.move_object_to_household_inventory( created_object): if self.notification_inventory: notification = self.notification_inventory.household_inventory( sim, self.resolver) notification.show_dialog() return True logger.error( 'Creation: Failed to place object {} in household inventory.', created_object, owner='rmccord') except KeyError: pass return False
class InventoryTypeTuning: INVENTORY_TYPE_DATA = TunableMapping( description= '\n A mapping of Inventory Type to any static information required by the\n client to display inventory data as well information about allowances\n for each InventoryType.\n ', key_type=InventoryType, value_type=TunableTuple( description= '\n Any information required by the client to display inventory data.\n ', skip_carry_pose_allowed=Tunable( description= '\n If checked, an object tuned to be put away in this inventory\n type will be allowed to skip the carry pose. If unchecked, it\n will not be allowed to skip the carry pose.\n ', tunable_type=bool, default=False), put_away_allowed=Tunable( description= '\n If checked, objects can be manually "put away" in this\n inventory type. If unchecked, objects cannot be manually "put\n away" in this inventory type.\n ', tunable_type=bool, default=True), shared_between_objects=TunableEnumEntry( description= '\n If shareable, this inventory will be shared between all objects\n that have it. For example, if you put an item in one fridge,\n you would be able to remove it from a different fridge on the\n lot.', tunable_type=ObjectShareability, default=ObjectShareability.SHARED), max_inventory_size=OptionalTunable(tunable=TunableRange( description= '\n Max number of items inventory type can have\n ', tunable_type=int, default=sims4.math.MAX_INT32, minimum=1, maximum=sims4.math.MAX_INT32), disabled_name='unbounded', enabled_name='fixed_size'))) GAMEPLAY_MODIFIERS = TunableMapping( description= "\n A mapping of Inventory Type to the gameplay effects they provide. If an\n inventory does not affect contained objects, it is fine to leave that\n inventory's type out of this mapping.\n ", key_type=InventoryType, value_type=TunableTuple( description='\n Gameplay modifiers.\n ', decay_modifiers=CommodityDecayModifierMapping( description= '\n Multiply the decay rate of specific commodities by a tunable\n integer in order to speed up or slow down decay while the\n object is contained within this inventory. This modifier will\n be multiplied with other modifiers on the object, if it has\n any.\n ' ), decay_modifiers_tests=TunableTestSet( description= '\n Set of tests that must be passed to apply decay modifiers.\n ' ), autonomy_modifiers=TunableList( description= '\n Objects in the inventory of this object will have these\n autonomy modifiers applied to them.\n ', tunable=TunableAutonomyModifier( description= '\n Autonomy modifiers for objects that are placed in this\n inventory type.\n ', locked_args={'relationship_multipliers': None})))) @classmethod def _verify_tuning_callback(cls): for inventory_type in set(InventoryType) - set( cls.INVENTORY_TYPE_DATA.keys()): logger.error( 'Inventory type {} has no tuned inventory type data. This can be fixed in the tuning for objects.components.inventory_enum.tuning -> InventoryTypeTuning -> Inventory Type Data.', inventory_type.name, owner='bhill') @staticmethod def get_inventory_type_data_tuning(inventory_type): return InventoryTypeTuning.INVENTORY_TYPE_DATA.get(inventory_type) @staticmethod def get_gameplay_effects_tuning(inventory_type): return InventoryTypeTuning.GAMEPLAY_MODIFIERS.get(inventory_type) @staticmethod def is_shared_between_objects(inventory_type): tuning = InventoryTypeTuning.get_inventory_type_data_tuning( inventory_type) if tuning is None or tuning.shared_between_objects == ObjectShareability.SHARED: return True if tuning.shared_between_objects == ObjectShareability.NOT_SHARED: return False elif tuning.shared_between_objects == ObjectShareability.SHARED_IF_NOT_IN_APARTMENT: return not services.get_plex_service().is_zone_an_apartment( services.current_zone_id(), consider_penthouse_an_apartment=False) return True @staticmethod def is_put_away_allowed_on_inventory_type(inventory_type): tuning = InventoryTypeTuning.get_inventory_type_data_tuning( inventory_type) return tuning is None or tuning.put_away_allowed @staticmethod def get_max_inventory_size_for_inventory_type(inventory_type): tuning = InventoryTypeTuning.get_inventory_type_data_tuning( inventory_type) if tuning is None: return sims4.math.MAX_UINT32 return tuning.max_inventory_size
class ObjectRouteFromTargetObject(_ObjectRoutingBehaviorBase): FACTORY_TUNABLES = { 'radius': TunableDistanceSquared( description= '\n Only objects within this distance are considered.\n ', default=1), 'target_type': TunableVariant( description= '\n Type of target object to choose (object, sim).\n ', object=_RouteTargetTypeObject.TunableFactory(), sim=_RouteTargetTypeSim.TunableFactory(), default='object'), 'target_selection_test': TunableTestSet( description= '\n A test used for selecting a target.\n ', tuning_group=GroupNames.TESTS), 'no_target_loot': TunableList( description= "\n Loot to apply if no target is selected (eg, change state back to 'wander').\n ", tunable=LootActions.TunableReference()), 'constraints': TunableList( description= '\n Constraints relative to the relative participant.\n ', tunable=TunableGeometricConstraintVariant( description= '\n Use the point on the found object defined by these geometric constraints.\n ', disabled_constraints=('spawn_points', 'spawn_points_with_backup'))), 'target_action_rules': TunableList( description= '\n A set of conditions and a list of one or more TargetObjectActions to run\n on the target object after routing to it. These are applied in sequence.\n ', tunable=_TargetActionRules.TunableFactory()) } @classmethod def _verify_tuning_callback(cls): if not cls.target_selection_test and not cls.tags: logger.error( 'No selection test tuned for ObjectRouteFromTargetObject {}.', cls, owner='miking') def _find_target(self): all_objects = self.target_type.get_objects() objects = [] for o in all_objects: dist_sq = (o.position - self._obj.position).magnitude_squared() if dist_sq > self.radius: continue if o == self: continue if not o.is_sim and not o.may_reserve(self._obj): continue if self.target_selection_test: resolver = DoubleObjectResolver(self._obj, o) if not self.target_selection_test.run_tests(resolver): continue else: objects.append([o, dist_sq]) if not objects: return source_handles = [ routing.connectivity.Handle(self._obj.position, self._obj.routing_surface) ] dest_handles = [] for o in objects: obj = o[0] parent = obj.parent route_to_obj = parent if parent is not None else obj constraint = Anywhere() for tuned_constraint in self.constraints: constraint = constraint.intersect( tuned_constraint.create_constraint(self._obj, route_to_obj)) dests = constraint.get_connectivity_handles(self._obj, target=obj) if dests: dest_handles.extend(dests) if not dest_handles: return routing_context = self._obj.get_routing_context() connections = routing.estimate_path_batch( source_handles, dest_handles, routing_context=routing_context) if not connections: return connections.sort(key=lambda connection: connection[2]) best_connection = connections[0] best_dest_handle = best_connection[1] best_obj = best_dest_handle.target return best_obj def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._target = self._find_target() def get_routes_gen(self): if self._target is None: self.on_no_target() return False yield routing_slot_constraint = Anywhere() for tuned_constraint in self.constraints: routing_slot_constraint = routing_slot_constraint.intersect( tuned_constraint.create_constraint(self._obj, self._target)) goals = list( itertools.chain.from_iterable( h.get_goals() for h in routing_slot_constraint.get_connectivity_handles( self._obj))) routing_context = self._obj.get_routing_context() route = routing.Route(self._obj.routing_location, goals, routing_context=routing_context) yield route def do_target_action_rules_gen(self, timeline): if not self.target_action_rules or self._target is None: return resolver = DoubleObjectResolver(self._obj, self._target) for target_action_rule in self.target_action_rules: if random.random.random() >= target_action_rule.chance: continue if not target_action_rule.test.run_tests(resolver): continue if target_action_rule.actions is not None: for action in target_action_rule.actions: result = yield from action.run_action_gen( timeline, self._obj, self._target) if not result: return if target_action_rule.abort_if_applied: return def on_no_target(self): resolver = SingleObjectResolver(self._obj) for loot_action in self.no_target_loot: loot_action.apply_to_resolver(resolver)
class DeathTracker(SimInfoTracker): DEATH_ZONE_ID = 0 DEATH_TYPE_GHOST_TRAIT_MAP = TunableMapping( description= '\n The ghost trait to be applied to a Sim when they die with a given death\n type.\n ', key_type=TunableEnumEntry( description= '\n The death type to map to a ghost trait.\n ', tunable_type=DeathType, default=DeathType.NONE), key_name='Death Type', value_type=TunableReference( description= '\n The ghost trait to apply to a Sim when they die from the specified\n death type.\n ', manager=services.trait_manager()), value_name='Ghost Trait') DEATH_BUFFS = TunableList( description= '\n A list of buffs to apply to Sims when another Sim dies. For example, use\n this tuning to tune a "Death of a Good Friend" buff.\n ', tunable=TunableTuple( test_set=TunableReference( description= "\n The test that must pass between the dying Sim (TargetSim) and\n the Sim we're considering (Actor). If this test passes, no\n further test is executed.\n ", manager=services.get_instance_manager(sims4.resources.Types. SNIPPET), class_restrictions=('TestSetInstance', ), pack_safe=True), buff=TunableBuffReference( description= '\n The buff to apply to the Sim.\n ', pack_safe=True), notification=OptionalTunable( description= '\n If enabled, an off-lot death generates a notification for the\n target Sim. This is limited to one per death instance.\n ', tunable=TunableUiDialogNotificationReference( description= '\n The notification to show.\n ', pack_safe=True)))) IS_DYING_BUFF = TunableReference( description= '\n A reference to the buff a Sim is given when they are dying.\n ', manager=services.buff_manager()) DEATH_RELATIONSHIP_BIT_FIXUP_LOOT = TunableReference( description= '\n A reference to the loot to apply to a Sim upon death.\n \n This is where the relationship bit fixup loots will be tuned. This\n used to be on the interactions themselves but if the interaction was\n reset then the bits would stay as they were. If we add more relationship\n bits we want to clean up on death, the references Loot is the place to \n do it.\n ', manager=services.get_instance_manager(sims4.resources.Types.ACTION)) def __init__(self, sim_info): self._sim_info = sim_info self._death_type = None self._death_time = None @property def death_type(self): return self._death_type @property def death_time(self): return self._death_time @property def is_ghost(self): return self._sim_info.trait_tracker.has_any_trait( self.DEATH_TYPE_GHOST_TRAIT_MAP.values()) def get_ghost_trait(self): return self.DEATH_TYPE_GHOST_TRAIT_MAP.get(self._death_type) def set_death_type(self, death_type, is_off_lot_death=False): is_npc = self._sim_info.is_npc household = self._sim_info.household self._sim_info.inject_into_inactive_zone(self.DEATH_ZONE_ID, start_away_actions=False, skip_instanced_check=True, skip_daycare=True) household.remove_sim_info(self._sim_info, destroy_if_empty_household=True) if is_off_lot_death: household.pending_urnstone_ids.append(self._sim_info.sim_id) self._sim_info.transfer_to_hidden_household() clubs.on_sim_killed_or_culled(self._sim_info) if death_type is None: return relationship_service = services.relationship_service() for target_sim_info in relationship_service.get_target_sim_infos( self._sim_info.sim_id): resolver = DoubleSimResolver(target_sim_info, self._sim_info) for death_data in self.DEATH_BUFFS: if not death_data.test_set(resolver): continue target_sim_info.add_buff_from_op( death_data.buff.buff_type, buff_reason=death_data.buff.buff_reason) if is_npc and not target_sim_info.is_npc: notification = death_data.notification(target_sim_info, resolver=resolver) notification.show_dialog() break ghost_trait = DeathTracker.DEATH_TYPE_GHOST_TRAIT_MAP.get(death_type) if ghost_trait is not None: self._sim_info.add_trait(ghost_trait) traits = list(self._sim_info.trait_tracker.equipped_traits) for trait in traits: if trait.remove_on_death: self._sim_info.remove_trait(trait) self._death_type = death_type self._death_time = services.time_service().sim_now.absolute_ticks() self._sim_info.reset_age_progress() self._sim_info.resend_death_type() self._handle_remove_rel_bits_on_death() services.get_event_manager().process_event( test_events.TestEvent.SimDeathTypeSet, sim_info=self._sim_info) def _handle_remove_rel_bits_on_death(self): resolver = SingleSimResolver(self._sim_info) if self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT is not None: for (loot, _) in self.DEATH_RELATIONSHIP_BIT_FIXUP_LOOT.get_loot_ops_gen( ): result = loot.test_resolver(resolver) if result: loot.apply_to_resolver(resolver) def clear_death_type(self): self._death_type = None self._death_time = None self._sim_info.resend_death_type() def save(self): if self._death_type is not None: data = protocols.PersistableDeathTracker() data.death_type = self._death_type data.death_time = self._death_time return data def load(self, data): try: self._death_type = DeathType(data.death_type) except: self._death_type = DeathType.NONE self._death_time = data.death_time @classproperty def _tracker_lod_threshold(cls): return SimInfoLODLevel.MINIMUM
class GlobalLotTuningAndCleanup: __qualname__ = 'GlobalLotTuningAndCleanup' OBJECT_COUNT_TUNING = TunableMapping( description= '\n Mapping between statistic and a set of tests that are run over the\n objects on the lot on save. The value of the statistic is set to the\n number of objects that pass the tests.\n ', key_type=TunableReference( description= '\n The statistic on the lot that will be set the value of the number\n of objects that pass the test set that it is mapped to.\n ', manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), value_type=TunableTestSet( description= '\n Test set that will be run on all objects on the lot to determine\n what the value of the key statistic should be set to.\n ' )) SET_STATISTIC_TUNING = TunableList( description= '\n A list of statistics and values that they will be set to on the lot\n while saving it when the lot was running.\n \n These values are set before counting by tests on objects.\n ', tunable=TunableTuple( statistic=TunableReference( description= '\n The statistic that will have its value set.\n ', manager=services.get_instance_manager(sims4.resources.Types. STATISTIC)), amount=Tunable( description= '\n The value that the statistic will be set to.\n ', tunable_type=float, default=0.0))) OBJECT_CLEANUP_TUNING = TunableList( description= '\n A list of actions to take when spinning up a zone in order to fix it\n up based on statistic values that the lot has.\n ', tunable=TunableTuple( count=TunableVariant( all_items=AllItems(), statistic_value=StatisticValue(), statistic_difference=StatisticDifference(), default='all_items', description= '\n The maximum number of items that will have the action run\n on them. \n ' ), possible_actions= TunableList( description= '\n The different possible actions that can be taken on objects on\n the lot if tests pass.\n ', tunable=TunableTuple(actions=TunableList( description= '\n A group of actions to be taken on the object.\n ', tunable=TunableVariant( set_state=SetState(), destroy_object=DestroyObject(), statistic_change=StatisticChange(), default='set_state', description= '\n The actual action that will be performed on the\n object if test passes.\n ' )), tests=TunableTestSet( description= '\n Tests that if they pass the object will be under\n consideration for this action being done on them.\n ' ))))) objects_to_destroy = None @classmethod def calculate_object_quantity_statistic_values(cls, lot): for set_statatistic in cls.SET_STATISTIC_TUNING: lot.set_stat_value(set_statatistic.statistic, set_statatistic.amount) new_statistic_values = collections.defaultdict(int) for obj in services.object_manager().values(): if obj.is_sim: pass if not obj.is_on_active_lot(): pass resolver = SingleObjectResolver(obj) for (statistic, tests) in cls.OBJECT_COUNT_TUNING.items(): while tests.run_tests(resolver): new_statistic_values[statistic] += 1 for (statistic, value) in new_statistic_values.items(): lot.set_stat_value(statistic, value) @classmethod def cleanup_objects(cls, lot=None): if lot is None: logger.error('Lot is None when trying to run lot cleanup.', owner='jjacobson') return cls.objects_to_destroy = set() for cleanup in GlobalLotTuningAndCleanup.OBJECT_CLEANUP_TUNING: items_to_cleanup = cleanup.count(lot) if items_to_cleanup == 0: pass items_cleaned_up = 0 for obj in services.object_manager().values(): if items_cleaned_up >= items_to_cleanup: break if obj.is_sim: pass resolver = SingleObjectResolver(obj) run_action = False for possible_action in cleanup.possible_actions: while possible_action.tests.run_tests(resolver): while True: for action in possible_action.actions: action(obj, lot) run_action = True while run_action: items_cleaned_up += 1 for obj in cls.objects_to_destroy: obj.destroy(source=lot, cause='Cleaning up the lot') cls.objects_to_destroy = None
class TimedAspirationPickerInteraction(PickerSuperInteraction): INSTANCE_TUNABLES = { 'picker_dialog': UiItemPicker.TunableFactory( description= '\n The timed aspiration picker dialog.\n ', tuning_group=GroupNames.PICKERTUNING), 'timed_aspirations': TunableList( description= '\n The list of timed aspirations available to select.\n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.ASPIRATION), class_restrictions='TimedAspiration', pack_safe=True), unique_entries=True, tuning_group=GroupNames.PICKERTUNING), 'actor_continuation': TunableContinuation( description= '\n If specified, a continuation to push on the actor when a picker \n selection has been made.\n ', locked_args={'actor': ParticipantType.Actor}, tuning_group=GroupNames.PICKERTUNING), 'loot_on_picker_selection': TunableList( description= "\n Loot that will be applied to the Sim if an aspiration is selected.\n It will not be applied if the user doesn't select an aspiration.\n ", tunable=LootActions.TunableReference(), tuning_group=GroupNames.PICKERTUNING) } def _run_interaction_gen(self, timeline): self._show_picker_dialog(self.sim, target_sim=self.sim) return True yield @flexmethod def picker_rows_gen(cls, inst, target, context, **kwargs): inst_or_cls = inst if inst is not None else cls resolver = SingleSimResolver(target.sim_info) for timed_aspiration in inst_or_cls.timed_aspirations: test_result = timed_aspiration.tests.run_tests( resolver, search_for_tooltip=True) is_enable = test_result.result if is_enable or test_result.tooltip is not None: if test_result.tooltip is not None: row_tooltip = lambda *_, tooltip=test_result.tooltip, **__: inst_or_cls.create_localized_string( tooltip) else: row_tooltip = None row = BasePickerRow( is_enable=is_enable, name=inst_or_cls.create_localized_string( timed_aspiration.display_name), icon=timed_aspiration.display_icon, row_description=inst_or_cls.create_localized_string( timed_aspiration.display_description), row_tooltip=row_tooltip, tag=timed_aspiration) yield row def on_choice_selected(self, choice_tag, **kwargs): if choice_tag is None: return self.target.aspiration_tracker.activate_timed_aspiration(choice_tag) resolver = self.get_resolver() for loot_action in self.loot_on_picker_selection: loot_action.apply_to_resolver(resolver) if self.actor_continuation: self.push_tunable_continuation(self.actor_continuation)
def __init__(self, **kwargs): super().__init__( object=TunableVenueObjectTags( description= "\n Specify object tag(s) that must be on this venue. Allows you to\n group objects, i.e. weight bench, treadmill, and basketball\n goals are tagged as\n 'exercise objects.'\n ", export_modes=ExportModes.All), object_parent_pair_tests=TunableList( description= "\n Specify object tag(s) and/or parent attachment tags that\n requires to be on this venue. Allows you to group objects, i.e.\n weight bench, treadmill, and basketball goals are tagged as\n 'exercise objects.'\n ", tunable=TunableTuple( object_tags=TunableVenueObjectTags( description= '\n The objects (tag) that would count for the required items.\n ', export_modes=ExportModes.All), parent_tags=TunableVenueObjectTags( description= '\n If set, the object tuned in object_tags would required\n to be slotted to the parent object tuned in\n parent_tags. \n \n E.g. in restaurant, a chair (with restaurant_chair tag)\n would need to slot to a table (with\n restaurant_table_tag) to count as a dining slot. But\n since bar will not has the restaurant_table_tag, so a\n high chair that slots to the bar will not count as\n dining spot.\n ', export_modes=ExportModes.All), count=TunableRange( description= '\n How many required objects will be satisfied with this\n object(and/or with parent pair).\n \n E.g. a chair that slots to table will count as one\n dining spot, but booth slot to table will count as 2.\n ', tunable_type=int, default=1, minimum=1), required_object_test_tag=TunableEnumEntry( tunable_type=VenueObjectTestTag, default=VenueObjectTestTag.INVALID), export_class_name='VenueObjectParentPairTuple', export_modes=ExportModes.All)), min_number=TunableRange( description= '\n The lower bound above which the number of objects of this type on\n the lot must be.\n ', tunable_type=int, default=0, minimum=0, export_modes=ExportModes.All), max_number=TunableRange( description= '\n The upper bound below which the number of objects of this type on\n the lot must be.\n ', tunable_type=int, default=MAX_INT32, minimum=0, export_modes=ExportModes.All), object_display_name=TunableLocalizedString( description= '\n Name that will be displayed for the object(s)\n ', allow_catalog_name=True, export_modes=ExportModes.All), tooltip_override=TunableLocalizedString( description= '\n If tuned, the tooltip that will be shown when this requirement\n is moused over in the venue configuration requirements UI.\n ', export_modes=ExportModes.All, allow_none=True), is_optional=Tunable( description= '\n If True, this object requirement will be optional to this venue.\n \n E.g. Waiter station and host station for restaurant should set\n this entry to True.\n ', tunable_type=bool, default=False, export_modes=ExportModes.All), object_test_type=TunableEnumEntry( description= '\n This option determines what test will be applied. To test the\n number of objects of a certain type, select OBJECT. To test for\n a pool, select pool. To test the number of tiles used by the\n home, select tile (tiny home venues do this).\n ', tunable_type=VenueObjectTestType, default=VenueObjectTestType.OBJECT), **kwargs)
class MotherPlantBattleSituation(SituationComplexCommon): MOTHER_PLANT_METER_ID = 1 PLAYER_HEALTH_METER_ID = 2 INSTANCE_TUNABLES = { 'player_job': TunableReference( description= '\n Job for the main player sim that fights the plant.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'player_sim_role_state': TunableReference( description= '\n Role state for the main player sim Role.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'other_player_jobs': TunableReference( description= '\n Job for the other player Sims that are not the main Sim and are not\n participating as helpers.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'other_player_sims_role_state': TunableReference( description= '\n Role state for the other player Sims.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'helper_1_job': TunableReference( description= '\n Job for one of the helper Sims for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'helper_2_job': TunableReference( description= '\n Job for one of the helper Sims for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'helper_3_job': TunableReference( description= '\n Job for one of the helper Sims for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'helper_sim_prepare_role_state_1': TunableReference( description= '\n Role state for helper Sim 1 when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'helper_sim_prepare_role_state_2': TunableReference( description= '\n Role state for helper Sim 2 when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'helper_sim_prepare_role_state_3': TunableReference( description= '\n Role state for helper Sim 3 when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'zombie_job': TunableReference( description= '\n Job for the Zombies for the fight.\n ', manager=services.get_instance_manager( sims4.resources.Types.SITUATION_JOB)), 'zombie_prepare_role_state': TunableReference( description= '\n Role state for the zombie Sims when preparing for battle.\n ', manager=services.get_instance_manager( sims4.resources.Types.ROLE_STATE)), 'zombie_fight_interaction': TunableReference( description= '\n Interaction pushed on zombies to get them to fight a Sim.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'zombie_fight_interaction_timer': TunableSimMinute( description= '\n Timer for the amount of time between zombie attacks.\n ', minimum=1, default=30), 'player_health_statistic': TunableReference( description= "\n The statistic that we will use in order to determine the Sim's\n health for the motherplant.\n ", manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), 'motherplant_health_statisic': TunableReference( description= "\n The statistic that we will use in order to determine the Sim's\n health for the motherplant.\n ", manager=services.get_instance_manager( sims4.resources.Types.STATISTIC)), 'victory_interaction_of_interest': TunableInteractionOfInterest( description= '\n The interaction of interest that we are looking for to determine\n victory.\n ' ), 'retreat_interaction_of_interest': TunableInteractionOfInterest( description= '\n The interaction of interest that we are looking for to determine\n retreat.\n ' ), 'loss_interaction_mixer': TunableReference( description= '\n The affordance that will be pushed on the primary Sims if they\n lose.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'fight_affordance': TunableReference( description= '\n The primary fight interaction that we will use to run the defeat\n mixer the player Sim.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'helper_victory_affordance': TunableReference( description= '\n The affordance that will be pushed on the helper Sims if they\n achieve victory.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'helper_lose_affordance': TunableReference( description= '\n The affordance that will be pushed on the helper Sims if they\n lose.\n ', manager=services.get_instance_manager( sims4.resources.Types.INTERACTION)), 'mother_plant_definition': TunableReference( description= '\n The actual mother plant itself.\n ', manager=services.definition_manager()), 'base_battle_situation_state': BattleThePlantSituationState.TunableFactory( locked_args={ 'allow_join_situation': True, 'time_out': None }, tuning_group=GroupNames.STATE), 'attack_battle_situation_state': AttackBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'inspire_battle_situation_state': InspireBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'rally_battle_sitaution_state': RallyBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'warbling_warcry_battle_situation_state': WarblingWarcryBattleThePlantSituationState.TunableFactory( locked_args={'allow_join_situation': True}, tuning_group=GroupNames.STATE), 'save_lock_tooltip': TunableLocalizedString( description= '\n The tooltip/message to show when the player tries to save the game\n while this situation is running. Save is locked when situation starts.\n ', tuning_group=GroupNames.UI), 'mother_plant_meter_settings': StatBasedSituationMeterData.TunableFactory( description= '\n The meter used to track the health of the mother plant.\n ', tuning_group=GroupNames.SITUATION, locked_args={'_meter_id': MOTHER_PLANT_METER_ID}), 'player_health_meter_settings': StatBasedSituationMeterData.TunableFactory( description= '\n The meter used to track the health of the player team.\n ', tuning_group=GroupNames.SITUATION, locked_args={'_meter_id': PLAYER_HEALTH_METER_ID}), 'mother_plant_icon': TunableResourceKey( description= '\n Icon to be displayed in the situation UI beside the mother plant\n health bar.\n ', resource_types=sims4.resources.CompoundTypes.IMAGE, default=None, allow_none=True, tuning_group=GroupNames.SITUATION), 'states_to_set_on_start': TunableList( description= '\n A list of states to set on the motherplant on start.\n ', tunable=TunableStateValueReference( description= '\n The state to set.\n ')), 'states_to_set_on_end': TunableList( description= '\n A list of states to set on the motherplant on end.\n ', tunable=TunableStateValueReference( description= '\n The state to set.\n ')), 'victory_reward': TunableReference( description= '\n The Reward received when the Sim wins the situation.\n ', manager=services.get_instance_manager( sims4.resources.Types.REWARD)), 'victory_audio_sting': TunableResourceKey( description= '\n The sound to play when the Sim wins the battle.\n ', resource_types=(sims4.resources.Types.PROPX, ), default=None, tuning_group=GroupNames.AUDIO), 'defeat_audio_sting': TunableResourceKey( description= '\n The sound to play when the Sim loses the battle.\n ', resource_types=(sims4.resources.Types.PROPX, ), default=None, tuning_group=GroupNames.AUDIO), 'possessed_buff': TunableBuffReference( description= '\n Possessed Buff for zombie Sims. \n ') } @property def user_facing_type(self): return SituationUserFacingType.MOTHER_PLANT_EVENT @property def situation_display_type(self): return SituationDisplayType.VET @property def situation_display_priority(self): return SituationDisplayPriority.VET @classmethod def _states(cls): return (SituationStateData(1, PrepareForBattleSituationState), SituationStateData.from_auto_factory( 2, cls.base_battle_situation_state), SituationStateData.from_auto_factory( 3, cls.attack_battle_situation_state), SituationStateData.from_auto_factory( 4, cls.inspire_battle_situation_state), SituationStateData.from_auto_factory( 5, cls.rally_battle_sitaution_state), SituationStateData.from_auto_factory( 6, cls.warbling_warcry_battle_situation_state)) @classmethod def default_job(cls): pass @classmethod def _get_tuned_job_and_default_role_state_tuples(cls): return ((cls.player_job, cls.player_sim_role_state), (cls.other_player_jobs, cls.other_player_sims_role_state), (cls.helper_1_job, cls.helper_sim_prepare_role_state_1), (cls.helper_2_job, cls.helper_sim_prepare_role_state_2), (cls.helper_3_job, cls.helper_sim_prepare_role_state_3), (cls.zombie_job, cls.zombie_prepare_role_state)) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._zombie_attack_alarm_handle = None self._registered_test_events = set() self._player_health_tracking_situation_goal = None self._statistic_watcher_handle = None self._victory = False @property def end_audio_sting(self): if self._victory: return self.victory_audio_sting return self.defeat_audio_sting def _get_reward(self): if self._victory: return self.victory_reward def _get_motherplant(self): return next( iter(services.object_manager().get_objects_of_type_gen( self.mother_plant_definition))) def _push_loss_on_player(self): motherplant = self._get_motherplant() for (sim, situation_sim) in self._situation_sims.items(): if situation_sim.current_job_type is self.player_job: parent_si = sim.si_state.get_si_by_affordance( self.fight_affordance) if parent_si is not None: interaction_context = InteractionContext( sim, InteractionSource.PIE_MENU, Priority.Critical) aop = AffordanceObjectPair(self.loss_interaction_mixer, motherplant, self.fight_affordance, parent_si) if not aop.test_and_execute(interaction_context): logger.error( 'Attempting to push Motherplant Battle Ending Interaction, but failed.' ) self._push_interaction_on_all_helpers(self.helper_lose_affordance) def on_goal_completed(self, goal): super().on_goal_completed(goal) self._push_loss_on_player() self._self_destruct() def _on_set_sim_job(self, sim, job_type): super()._on_set_sim_job(sim, job_type) if job_type is self.zombie_job: sim.add_buff_from_op(self.possessed_buff.buff_type, buff_reason=self.possessed_buff.buff_reason) def _on_statistic_updated(self, stat_type, old_value, new_value): if stat_type is self.player_health_statistic: self._player_health_tracking_situation_goal.set_count(new_value) self._player_health_meter.send_update_if_dirty() elif stat_type is self.motherplant_health_statisic: self._mother_plant_meter.send_update_if_dirty() def _zombie_attack(self, _): if not self._cur_state.zombie_attack_valid: return zombies = [] for (sim, situation_sim) in self._situation_sims.items(): if situation_sim.current_job_type is self.zombie_job: zombies.append(sim) zombie_to_attack = random.choice(zombies) context = InteractionContext( sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT) zombie_to_attack.push_super_affordance(self.zombie_fight_interaction, None, context) def _push_interaction_on_all_helpers(self, interaction_to_push): for (sim, situation_sim) in self._situation_sims.items(): if not situation_sim.current_job_type is self.helper_1_job: if not situation_sim.current_job_type is self.helper_2_job: if situation_sim.current_job_type is self.helper_3_job: context = InteractionContext( sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType. DEFAULT) sim.push_super_affordance(interaction_to_push, None, context) context = InteractionContext( sim, InteractionContext.SOURCE_SCRIPT, interactions.priority.Priority.High, insert_strategy=QueueInsertStrategy.NEXT, bucket=interactions.context.InteractionBucketType.DEFAULT) sim.push_super_affordance(interaction_to_push, None, context) def handle_event(self, sim_info, event, resolver): super().handle_event(sim_info, event, resolver) if event != TestEvent.InteractionComplete: return if resolver(self.victory_interaction_of_interest): self._push_interaction_on_all_helpers( self.helper_victory_affordance) self._victory = True self._self_destruct() elif resolver(self.retreat_interaction_of_interest): self._push_loss_on_player() self._self_destruct() def start_situation(self): services.get_persistence_service().lock_save(self) super().start_situation() self._change_state(PrepareForBattleSituationState()) motherplant = self._get_motherplant() motherplant.set_stat_value(self.player_health_statistic, 0, add=True) motherplant.set_stat_value(self.motherplant_health_statisic, self.motherplant_health_statisic.max_value, add=True) for state_value in self.states_to_set_on_start: motherplant.set_state(state_value.state, state_value) statistic_tracker = motherplant.statistic_tracker self._statistic_watcher_handle = statistic_tracker.add_watcher( self._on_statistic_updated) self._setup_situation_meters() self._zombie_attack_alarm_handle = alarms.add_alarm( self, create_time_span(minutes=self.zombie_fight_interaction_timer), self._zombie_attack, repeating=True) for custom_key in itertools.chain( self.victory_interaction_of_interest.custom_keys_gen(), self.retreat_interaction_of_interest.custom_keys_gen()): custom_key_tuple = (TestEvent.InteractionComplete, custom_key) self._registered_test_events.add(custom_key_tuple) services.get_event_manager().register_with_custom_key( self, TestEvent.InteractionComplete, custom_key) def _setup_situation_meters(self): motherplant = self._get_motherplant() self._mother_plant_meter = self.mother_plant_meter_settings.create_meter_with_sim_info( self, motherplant) self._player_health_meter = self.player_health_meter_settings.create_meter_with_sim_info( self, motherplant) def build_situation_start_message(self): msg = super().build_situation_start_message() with ProtocolBufferRollback(msg.meter_data) as meter_data_msg: self.mother_plant_meter_settings.build_data_message(meter_data_msg) with ProtocolBufferRollback(msg.meter_data) as meter_data_msg: self.player_health_meter_settings.build_data_message( meter_data_msg) build_icon_info_msg(IconInfoData(icon_resource=self.mother_plant_icon), None, msg.icon_info) return msg def _destroy(self): super()._destroy() services.get_persistence_service().unlock_save(self) for (event_type, custom_key) in self._registered_test_events: services.get_event_manager().unregister_with_custom_key( self, event_type, custom_key) motherplant = self._get_motherplant() statistic_tracker = motherplant.statistic_tracker statistic_tracker.remove_watcher(self._statistic_watcher_handle) for state_value in self.states_to_set_on_end: motherplant.set_state(state_value.state, state_value) self._registered_test_events.clear() if self._mother_plant_meter is not None: self._mother_plant_meter.destroy() if self._player_health_meter is not None: self._player_health_meter.destroy() def get_lock_save_reason(self): return self.save_lock_tooltip def set_motherplant_situation_state(self, motherplant_battle_state): if motherplant_battle_state == MotherplantBattleStates.ATTACK: self._change_state(self.attack_battle_situation_state()) elif motherplant_battle_state == MotherplantBattleStates.INSPIRE: self._change_state(self.inspire_battle_situation_state()) elif motherplant_battle_state == MotherplantBattleStates.RALLY: self._change_state(self.rally_battle_sitaution_state()) elif motherplant_battle_state == MotherplantBattleStates.WARBLING_WARCRY: self._change_state(self.warbling_warcry_battle_situation_state()) def _on_proxy_situation_goal_added(self, goal): self._player_health_tracking_situation_goal = goal def _issue_requests(self): super()._issue_requests() request = SelectableSimRequestFactory( self, _RequestUserData(), self.other_player_jobs, self.exclusivity, request_priority=BouncerRequestPriority.EVENT_DEFAULT_JOB) self.manager.bouncer.submit_request(request)
class GardeningTuning: INHERITED_STATE = ObjectState.TunableReference( description= '\n Controls the state value that will be inherited by offspring.\n ' ) SPONTANEOUS_GERMINATION_COMMODITY = Commodity.TunableReference() SPONTANEOUS_GERMINATION_COMMODITY_VARIANCE = TunableRange( description= '\n Max variance to apply when the spawn commodity is reset. This helps\n plants all not to sprout from seeds at the same time.\n ', tunable_type=int, default=10, minimum=0) SCALE_COMMODITY = Commodity.TunableReference() SCALE_VARIANCE = TunableInterval( description= "\n Control how much the size of child fruit can vary from its father's\n size.\n ", tunable_type=float, default_lower=0.8, default_upper=1.2) EVOLUTION_STATE = ObjectState.TunableReference( description= '\n Object state which will represent the icon behind the main icon of \n the gardening tooltip. This should be tied to the evolution state\n of gardening objects.\n ' ) SHOOT_DESCRIPTION_STRING = TunableLocalizedString( description= "\n Text that will be given to a shoot description following ':' to its \n fruit name.\n e.g. 'Shoot taken from: Apple'\n " ) DISABLE_DETAILS_STATE_VALUES = TunableList( description= '\n List of object state values where the gardening details should not \n be shown. This is for cases like Wild plants where we dont want\n details that will not be used.\n ', tunable=ObjectStateValue.TunableReference( description= '\n The state that will disable the plant additional information.\n ' )) DISABLE_TOOLTIP_STATE_VALUES = TunableList( description= '\n List of object state values where the gardening object will disable \n its tooltip.\n ', tunable=ObjectStateValue.TunableReference( description= '\n The state that will disable the object tooltip.\n ' )) SPLICED_PLANT_NAME = TunableLocalizedStringFactory( description= '\n Localized name to be set when a plant is spliced. \n ' ) SPLICED_STATE_VALUE = ObjectStateValue.TunableReference( description= '\n The state that will mean this plant has been already spliced. \n ' ) PICKUP_STATE_MAPPING = TunableMapping( description= '\n Mapping that will set a state that should be set on the fruit when \n its picked up, depending on a state fruit is currently in.\n ', key_type=ObjectStateValue.TunableReference(), value_type=ObjectStateValue.TunableReference()) GARDENING_SLOT = TunableReference( description= '\n Slot type used by the gardening system to create its fruit.\n ', manager=services.get_instance_manager(sims4.resources.Types.SLOT_TYPE)) GERMINATE_FAILURE_NOTIFICATION = UiDialogNotification.TunableFactory( description= '\n Notification that will tell the player that the plant has failed to\n germinate.\n ' ) UNIDENTIFIED_STATE_VALUE = ObjectStateValue.TunableReference( description= '\n The state value all unidentified plants will have. Remember to add this\n as the default value for a state in the identifiable plants state\n component tuning.\n ' ) SEASONALITY_STATE = ObjectState.TunablePackSafeReference( description= "\n A reference to the state that determines whether a plant is\n Dormant/Indoors/In Season/Out of Season.\n \n The state value's display data is used in the UI tooltip for the plant.\n " ) SEASONALITY_IN_SEASON_STATE_VALUE = ObjectStateValue.TunablePackSafeReference( description= '\n A reference to the state value that marks a plant as being In Season.\n \n This state value is determined to detect seasonality.\n ' ) SEASONALITY_ALL_SEASONS_TEXT = TunableLocalizedString( description= '\n The seasons text to display if the plant has no seasonality.\n ' ) PLANT_SEASONALITY_TEXT = TunableLocalizedStringFactory( description= "\n The text to display for the plant's seasonality.\n e.g.:\n Seasonality:\n{0.String}\n " ) FRUIT_STATES = TunableMapping( description= '\n A mapping that defines which states on plants support fruits, and the\n behavior when plants transition out of these states.\n ', key_type=ObjectState.TunableReference(pack_safe=True), value_type=TunableTuple( states=TunableList( description= '\n The list of states that supports fruit. If the object changes\n state (for the specified state track) and the new value is not\n in this list, the fruit is destroyed according to the specified\n rule.\n ', tunable=ObjectStateValue.TunableReference(pack_safe=True), unique_entries=True), behavior= TunableVariant( description= "\n Define the fruit's behavior when plants exit a state that\n supports fruit.\n ", rot=TunablePercent( description= '\n Define the chance that the fruit falls and rots, as opposed\n to just being destroyed.\n ', default=5), locked_args={'destroy': None}, default='destroy'))) FRUIT_DECAY_COMMODITY = TunableReference( description= '\n The commodity that defines fruit decay (e.g. rotten/ripe).\n ', manager=services.get_instance_manager(sims4.resources.Types.STATISTIC)) FRUIT_DECAY_COMMODITY_DROPPED_VALUE = Tunable( description= '\n Value to set the Fruit Decay Commodity on a harvestable that has\n been dropped from a plant during a seasonal transition.\n ', tunable_type=int, default=10) SPAWN_WEIGHTS = TunableMapping( description= "\n A fruit's chance to be spawned in a multi-fruit plant (e.g. via\n splicing/grafting) is determined by its rarity.\n \n The weight is meant to curb the chance of spawning rarer fruits growing\n on more common plants. It would never reduce the chance of the root\n stock from spawning on its original plant.\n \n e.g.\n A common Apple on a rare Pomegranate tree spawns at a 1:1 ratio.\n A rare Pomegranate on a common Apple tree spawns at a 1:5 ratio.\n ", key_type=TunableEnumEntry(tunable_type=ObjectCollectionRarity, default=ObjectCollectionRarity.COMMON), value_type=TunableRange(tunable_type=int, default=1, minimum=0)) EXCLUSIVE_FRUITS = TunableSet( description= '\n A set of fruits, which, when added onto a plant, can restrict\n what other fruits the plant produces to this set of fruits. \n This is done by adjusting spawn weight of non-exclusive fruits \n on the plant to zero. \n ', tunable=TunableReference(manager=services.get_instance_manager( sims4.resources.Types.OBJECT), pack_safe=True)) VERTICAL_GARDEN_OBJECTS = TunableSet( description='\n A set of Vertical garden objects.\n ', tunable=TunableReference(manager=services.definition_manager(), pack_safe=True)) @classmethod def is_spliced(cls, obj): if obj.has_state(cls.SPLICED_STATE_VALUE.state) and obj.get_state( cls.SPLICED_STATE_VALUE.state) == cls.SPLICED_STATE_VALUE: return True return False @classmethod def is_unidentified(cls, obj): if cls.UNIDENTIFIED_STATE_VALUE is not None and obj.has_state( cls.UNIDENTIFIED_STATE_VALUE.state) and obj.get_state( cls.UNIDENTIFIED_STATE_VALUE.state ) == cls.UNIDENTIFIED_STATE_VALUE: return True return False @classmethod def get_seasonality_text_from_plant(cls, plant_definition): season_component = plant_definition.cls._components.season_aware_component if season_component is not None: seasons = [] season_tuned_values = season_component._tuned_values for (season_type, season_states ) in season_tuned_values.seasonal_state_mapping.items(): if any(s is GardeningTuning.SEASONALITY_IN_SEASON_STATE_VALUE for s in season_states): season = SeasonsTuning.SEASON_TYPE_MAPPING[season_type] seasons.append((season_type, season)) if seasons: return GardeningTuning.PLANT_SEASONALITY_TEXT( LocalizationHelperTuning.get_comma_separated_list(*tuple( season.season_name for (_, season) in sorted(seasons)))) ALWAYS_GERMINATE_IF_NOT_SPAWNED_STATE = ObjectStateValue.TunableReference( description= '\n If the specified state value is active on the gardening object, it will\n have a 100% germination chance for when it is placed in the world in\n any way other than through a spawner.\n ' ) QUALITY_STATE_VALUE = ObjectState.TunableReference( description= '\n The quality state all gardening plants will have. \n ' )
class DisplayComponent(Component, HasTunableFactory, AutoFactoryInit, component_name=types.DISPLAY_COMPONENT): DISPLAY_STATE = TunableStateValueReference( description= '\n The state a display object will be set to when it is parented to a\n Display Parent.\n ' ) DEFAULT_STATE = TunableStateValueReference( description= '\n The default state a display object will be set to when it is unparented\n from a Display Parent.\n ' ) FACTORY_TUNABLES = { 'display_parent': CraftTaggedItemFactory( description= '\n If an object matches the tag(s), it will be considered a Display\n Parent for this display object. All display objects with a Display\n Component MUST have a Display Parent tuned, otherwise there is no\n need in the Display Component.\n ' ), 'use_display_state': Tunable( description= "\n If enabled, this object will change to the Display State when it is\n parented to a Display Parent. The Display State is tuned in the\n objects.components.display_component module tuning. NOTICE: If you\n are only tuning this and not tuning any Inventory State Triggers,\n it's recommended that you use the Slot Component in the Native\n Components section of the parent object.\n ", tunable_type=bool, default=True), 'inventory_state_triggers': TunableList( description= '\n Change states on the owning object based on tests applied to the\n inventory of the Display Parent. Tests will be done in order and\n will stop at the first success.\n ', tunable=TunableTuple(inventory_test=InventoryTest.TunableFactory(), set_state=TunableStateValueReference())) } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.inventory_state_triggers: services.get_event_manager().register_tests( self, (self.inventory_state_triggers[0].inventory_test, )) @property def _is_on_display_parent(self): parent = self.owner.parent if parent is None: return False return self.display_parent(crafted_object=parent, skill=None) is not None def handle_event(self, sim_info, event, resolver): if sim_info is not None: return if not self._is_on_display_parent: return self._handle_inventory_changed() def _handle_inventory_changed(self): obj_resolver = SingleObjectResolver(self.owner) for trigger in self.inventory_state_triggers: if obj_resolver(trigger.inventory_test): if self.owner.has_state(trigger.set_state.state): self.owner.set_state(trigger.set_state.state, trigger.set_state) break def slotted_to_object(self, parent): if self._should_change_display_state(parent) and self.owner.has_state( self.DISPLAY_STATE.state): self.owner.set_state(self.DISPLAY_STATE.state, self.DISPLAY_STATE) self._handle_inventory_changed() def unslotted_from_object(self, parent): if self._should_change_display_state(parent) and self.owner.has_state( self.DEFAULT_STATE.state): self.owner.set_state(self.DEFAULT_STATE.state, self.DEFAULT_STATE) def _should_change_display_state(self, parent): if not self.use_display_state: return False return self.display_parent(crafted_object=parent, skill=None)