class LocalizedStringHouseholdNameSelector(HasTunableSingletonFactory,
                                           AutoFactoryInit):
    __qualname__ = 'LocalizedStringHouseholdNameSelector'
    FACTORY_TUNABLES = {
        'empty_household':
        OptionalTunable(
            description=
            '\n            When enabled, this string will be used if the provided household\n            does not have any members.\n            ',
            tunable=TunableLocalizedStringFactoryVariant(
                description=
                '\n                The string to use if the provided household has no members. This\n                string is provided the same tokens as the original string.\n                '
            )),
        'single_sim':
        OptionalTunable(
            description=
            '\n            When enabled, this string will be used if the Sim is the only member\n            of the household. If disabled, this check will be ignored.\n            ',
            tunable=TunableLocalizedStringFactoryVariant(
                description=
                '\n                The string to use if the Sim is the only member of the\n                household. The first token is the only Sim of the household. It\n                might differ from the original Sim if the provided household is\n                different. The original Sim is the last token.\n                '
            )),
        'single_family':
        OptionalTunable(
            description=
            '\n            When enabled, this string will be used if the Sim is part of a\n            household where all Sims share the same last name. If disabled, this\n            check will be ignored.\n            ',
            tunable=TunableLocalizedStringFactoryVariant(
                description=
                '\n                The string to use if all Sims in the household share the same\n                last name. The first token is a string containing the household\n                name. The original Sim is the last token.\n                '
            )),
        'fallback':
        TunableLocalizedStringFactoryVariant(
            description=
            '\n            The string to use of no other rule applies. The first token is a\n            string containing the household name.\n            '
        )
    }

    def __call__(self, sim, *args, household=None, **kwargs):
        household = household if household is not None else sim.household
        if self.empty_household is not None and len(household) == 0:
            return self.empty_household(sim, *args, **kwargs)
        if self.single_sim is not None and len(household) == 1:
            return self.single_sim(next(iter(household)), *args + (sim, ),
                                   **kwargs)
        if self.single_family is not None and all(
                sim_info.last_name == sim.last_name
                for sim_info in household) and sim.last_name == household.name:
            return self.single_family(sim.last_name, *args + (sim, ), **kwargs)
        return self.fallback(household.name, *args + (sim, ), **kwargs)
Example #2
0
class TooltipText(HasTunableSingletonFactory, AutoFactoryInit):
    __qualname__ = 'TooltipText'
    FACTORY_TUNABLES = {
        'text':
        TunableLocalizedStringFactoryVariant(
            description=
            '\n            Text that will be displayed on the tuned tooltip_fields of the \n            tooltip.\n            '
        ),
        'text_tokens':
        OptionalTunable(
            description=
            "\n            If enabled, localization tokens to be passed into 'text' can be\n            explicitly defined. For example, you could use a participant that is\n            not normally used, such as a owned sim. Or you could also\n            pass in statistic and commodity values. If disabled, the standard\n            tokens from the interaction will be used (such as Actor and\n            TargetSim).\n            Participants tuned here should only be relevant to objects.  If \n            you try to tune a participant which only exist when you run an \n            interaction (e.g. carry_target) tooltip wont show anything.\n            ",
            tunable=LocalizationTokens.TunableFactory())
    }
Example #3
0
class TooltipText(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'text':
        TunableLocalizedStringFactoryVariant(
            description=
            '\n            Text that will be displayed on the tuned tooltip_fields of the \n            tooltip.\n            '
        ),
        'text_tokens':
        OptionalTunable(
            description=
            "\n            If enabled, localization tokens to be passed into 'text' can be\n            explicitly defined. For example, you could use a participant that is\n            not normally used, such as a owned sim. Or you could also\n            pass in statistic and commodity values. If disabled, the standard\n            tokens from the interaction will be used (such as Actor and\n            TargetSim).\n            Participants tuned here should only be relevant to objects.  If \n            you try to tune a participant which only exist when you run an \n            interaction (e.g. carry_target) tooltip wont show anything.\n            ",
            tunable=LocalizationTokens.TunableFactory()),
        'override_component_information':
        OptionalTunable(
            description=
            '\n            When override component fields is chosen, this tooltip field\n            tuning will have the highest priority over what data is displayed.\n            So if an object has some fields set by the crafting component or\n            name component, if this is set, this will trump that information.\n                        \n            When concatenate with component fields is set, you have the option\n            to combine the tooltip information given by the a component with\n            any string you decide to add.\n            \n            Example:\n            If we choose to override component fields on the recipe_name field\n            of a craftable we will override the recipe_name that gets set \n            by the crafting system.\n            \n            If we choose to concatenate we could have things like \n            recipe_name, my_new_text\n            ',
            disabled_name='override_component_fields',
            enabled_name='concatenate_with_component_fields',
            tunable=TooltipConcatenateData.TunableFactory())
    }
 def __init__(
         self,
         description='Properties of a textbox where a user can type in text',
         **kwargs):
     super().__init__(
         description=description,
         default_text=OptionalTunable(
             description=
             "\n                             Default text that will show up when the text box is\n                             not in focus if the user hasn't entered anything in\n                             the text box yet. If only default text is set, the\n                             text box will be blank when the user puts it in\n                             focus.\n                             ",
             tunable=TunableLocalizedStringFactory()),
         initial_value=OptionalTunable(
             description=
             '\n                             The initial value of the text in the textbox. This\n                             is different from default text because the initial\n                             value stays regardless of if the text box is in\n                             focus.\n                             ',
             tunable=TunableLocalizedStringFactoryVariant()),
         min_length=OptionalTunable(
             description=
             "\n                             If enabled, specify the minimum length of input\n                             text the player has to enter before he/she can hit\n                             the 'OK' button.\n                             ",
             tunable=TunableTuple(
                 length=TunableRange(
                     description=
                     '\n                                     Minimum amount of characters the user must\n                                     enter in to the text box before he/she can\n                                     click on the OK button.\n                                     ',
                     tunable_type=int,
                     minimum=1,
                     default=1),
                 tooltip=OptionalTunable(
                     description=
                     '\n                                     If enabled, allows specification of a\n                                     tooltip to display if the user has entered\n                                     text length less than min_length.\n                                     ',
                     tunable=TunableLocalizedStringFactory())),
             disabled_value=0),
         max_length=Tunable(
             description=
             '\n                             Max amount of characters the user can enter into\n                             the text box.\n                             ',
             tunable_type=int,
             default=20),
         title=OptionalTunable(
             description=
             '\n                             Text that will be shown with the text input to\n                             describe what that user is inputing.\n                             ',
             tunable=TunableLocalizedStringFactory()),
         **kwargs)
Example #5
0
class Business(HasTunableReference,
               metaclass=HashedTunedInstanceMetaclass,
               manager=services.get_instance_manager(
                   sims4.resources.Types.BUSINESS)):
    INSTANCE_TUNABLES = {
        'employee_data_map':
        TunableMapping(
            description=
            '\n            The mapping between Business Employee Type and the Employee Data for\n            that type.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                The Business Employee Type that should get the specified Career.\n                ',
                tunable_type=BusinessEmployeeType,
                default=BusinessEmployeeType.INVALID,
                invalid_enums=(BusinessEmployeeType.INVALID, )),
            value_type=TunableBusinessEmployeeDataSnippet(
                description=
                '\n                The Employee Data for the given Business Employee Type.\n                '
            ),
            tuning_group=GroupNames.EMPLOYEES),
        'npc_starting_funds':
        TunableRange(
            description=
            '\n            The amount of money an npc-owned store will start with in their\n            funds. Typically should be set to the same cost as the interaction\n            to buy the business.\n            ',
            tunable_type=int,
            default=0,
            minimum=0,
            tuning_group=GroupNames.CURRENCY),
        'funds_category_data':
        TunableMapping(
            description=
            '\n            Data associated with specific business funds categories.\n            ',
            key_type=TunableEnumEntry(
                description=
                '\n                The funds category.\n                ',
                tunable_type=BusinessFundsCategory,
                default=BusinessFundsCategory.NONE,
                invalid_enums=(BusinessFundsCategory.NONE, )),
            value_type=TunableTuple(
                description=
                '\n                The data associated with this retail funds category.\n                ',
                summary_dialog_entry=OptionalTunable(
                    description=
                    "\n                    If enabled, an entry for this category is displayed in the\n                    business' summary dialog.\n                    ",
                    tunable=TunableLocalizedString(
                        description=
                        '\n                        The dialog entry for this retail funds category. This\n                        string takes no tokens.\n                        '
                    ))),
            tuning_group=GroupNames.CURRENCY),
        'default_markup_multiplier':
        TunableRange(
            description=
            '\n            The default markup multiplier for a new business. This must match a\n            multiplier that\'s in the Markup Multiplier Data tunable. It\'s also\n            possible for this to be less than 1, meaning the default "markup"\n            will actually cause prices to be lower than normal.\n            ',
            tunable_type=float,
            default=1.25,
            minimum=math.EPSILON,
            tuning_group=GroupNames.CURRENCY),
        'advertising_name_map':
        TunableMapping(
            description=
            '\n            The mapping between advertising enum and the name used in the UI for\n            that type.\n            ',
            key_name='advertising_enum',
            key_type=TunableEnumEntry(
                description=
                '\n                The Advertising Type.\n                ',
                tunable_type=BusinessAdvertisingType,
                default=BusinessAdvertisingType.INVALID,
                invalid_enums=(BusinessAdvertisingType.INVALID, ),
                binary_type=EnumBinaryExportType.EnumUint32),
            value_name='advertising_name',
            value_type=TunableLocalizedString(
                description=
                '\n                The name of the advertising type used in the UI.\n                '
            ),
            tuple_name='AdvertisingEnumDataMappingTuple',
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.All),
        'advertising_type_sort_order':
        TunableList(
            description=
            '\n            Sort order for the advertising types in the UI\n            ',
            tunable=TunableEnumEntry(
                description=
                '\n                The Advertising Type.\n                ',
                tunable_type=BusinessAdvertisingType,
                default=BusinessAdvertisingType.INVALID,
                invalid_enums=(BusinessAdvertisingType.INVALID, ),
                binary_type=EnumBinaryExportType.EnumUint32),
            unique_entries=True,
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.All),
        'quality_settings':
        TunableList(
            description=
            '\n            Tunable Business quality settings.  \n            \n            Quality type can be interpreted in different ways \n            by specific businesses, and can be used for tests.\n            \n            These are quality settings that are exported to the client.\n            \n            The order in this list should be the order we want them displayed\n            in the UI.\n            ',
            tunable=TunableTuple(
                quality_type=TunableEnumEntry(
                    description=
                    '\n                    The quality Type.\n                    ',
                    tunable_type=BusinessQualityType,
                    default=BusinessQualityType.INVALID,
                    invalid_enums=(BusinessQualityType.INVALID, ),
                    binary_type=EnumBinaryExportType.EnumUint32),
                quality_name=TunableLocalizedString(
                    description=
                    '\n                    The name of the quality type used in the UI.\n                    '
                ),
                export_class_name='QualitySettingsData'),
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.All),
        'show_settings_button':
        OptionalTunable(
            description=
            "\n            If enabled, this business type will show the settings button with \n            the tuned tooltip text. If disabled, this business type won't show\n            the settings button.\n            ",
            tunable=TunableLocalizedString(
                description=
                '\n                The tooltip to show on the settings button when it is shown\n                for this business type.\n                '
            ),
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'business_summary_tooltip':
        OptionalTunable(
            description=
            '\n            If enabled, allows tuning a business summary tooltip. If disabled, no\n            tooltip will be used or displayed by the UI.\n            ',
            tunable=TunableLocalizedString(
                description=
                '\n                The tooltip to show on the business panel.\n                '
            ),
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'show_sell_button':
        Tunable(
            description=
            "\n            If checked, the sell button will be shown in the business panel if\n            the business is on the active lot. If left unchecked, the sell button\n            won't be shown on the business panel at all.\n            ",
            tunable_type=bool,
            default=False,
            tuning_group=GroupNames.UI,
            export_modes=ExportModes.ClientBinary),
        'show_employee_button':
        Tunable(description='\n            ',
                tunable_type=bool,
                default=False,
                tuning_group=GroupNames.UI,
                export_modes=ExportModes.ClientBinary),
        'default_quality':
        OptionalTunable(
            description=
            '\n            The default quality type for the business.',
            tunable=TunableEnumEntry(
                tunable_type=BusinessQualityType,
                default=BusinessQualityType.INVALID,
                invalid_enums=(BusinessQualityType.INVALID, ),
                binary_type=EnumBinaryExportType.EnumUint32),
            disabled_value=BusinessQualityType.INVALID,
            tuning_group=GroupNames.BUSINESS),
        'quality_unlock_perk':
        OptionalTunable(
            description=
            '\n            Reference to a perk that, if unlocked, allow the player to adjust\n            the quality type specific to this business.\n            ',
            tunable=TunablePackSafeReference(
                manager=services.get_instance_manager(
                    sims4.resources.Types.BUCKS_PERK)),
            tuning_group=GroupNames.BUSINESS),
        'advertising_configuration':
        AdvertisingConfiguration.TunableFactory(
            description=
            '\n            Tunable Business advertising configuration.\n            ',
            tuning_group=GroupNames.BUSINESS),
        'markup_multiplier_data':
        TunableList(
            description=
            '\n            A list of markup multiplier display names and the actual multiplier\n            associated with that name. This is used for sending the markup\n            information to the UI.\n            ',
            tunable=TunableTuple(
                description=
                '\n               A tuple of the markup multiplier display name and the actual\n               multiplier associated with that display name.\n               ',
                name=TunableLocalizedString(
                    description=
                    '\n                   The display name for this markup multiplier. e.g. a\n                   multiplier of 1.2 will have "20 %" tuned here.\n                   '
                ),
                markup_multiplier=TunableRange(
                    description=
                    '\n                    The multiplier associated with this display name.\n                    ',
                    tunable_type=float,
                    default=1,
                    minimum=math.EPSILON),
                export_class_name='MarkupMultiplierData'),
            tuning_group=GroupNames.CURRENCY,
            export_modes=ExportModes.All),
        'star_rating_to_screen_slam_map':
        TunableMapping(
            description=
            '\n            A mapping of star ratings to screen slams.\n            Screen slams will be triggered when the rating increases to a new\n            whole value.\n            ',
            key_type=int,
            value_type=ui.screen_slam.TunableScreenSlamSnippet(),
            key_name='star_rating',
            value_name='screen_slam',
            tuning_group=GroupNames.BUSINESS),
        'show_empolyee_skill_level_up_notification':
        Tunable(
            description=
            '\n            If true, skill level up notifications will be shown for employees.\n            ',
            tunable_type=bool,
            default=True,
            tuning_group=GroupNames.EMPLOYEES),
        'bucks':
        TunableEnumEntry(
            description=
            '\n            The Bucks Type this business will use for Perk unlocks.\n            ',
            tunable_type=BucksType,
            default=BucksType.INVALID,
            invalid_enums=(BucksType.INVALID, ),
            tuning_group=GroupNames.CURRENCY,
            export_modes=ExportModes.All),
        'off_lot_star_rating_decay_multiplier_perk':
        OptionalTunable(
            description=
            '\n            If enabled, allows the tuning of a perk which can adjust the off-lot star rating decay.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The off lot star rating decay multiplier tuning.\n                ',
                perk=TunableReference(
                    description=
                    '\n                    The perk that will cause the multiplier to be applied to the\n                    star rating decay during off-lot simulations.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.BUCKS_PERK)),
                decay_multiplier=TunableRange(
                    description=
                    '\n                    If the household has the specified perk, the off-lot star\n                    rating decay rate will be multiplied by this value.\n                    ',
                    tunable_type=float,
                    default=1.1,
                    minimum=0)),
            tuning_group=GroupNames.OFF_LOT),
        'manage_outfit_affordances':
        TunableSet(
            description=
            '\n            A list of affordances that are shown when the player clicks on the\n            Manage Outfits button.\n            ',
            tunable=TunableReference(
                description=
                '\n                An affordance shown when the player clicks on the Manage Outfits\n                button.\n                ',
                manager=services.affordance_manager(),
                pack_safe=True),
            tuning_group=GroupNames.EMPLOYEES),
        'employee_training_buff_tag':
        TunableEnumWithFilter(
            description=
            '\n            A tag to indicate a buff is used for employee training.\n            ',
            tunable_type=tag.Tag,
            default=tag.Tag.INVALID,
            invalid_enums=(tag.Tag.INVALID, ),
            filter_prefixes=('buff', ),
            pack_safe=True,
            tuning_group=GroupNames.EMPLOYEES),
        'customer_buffs_to_save_tag':
        TunableEnumWithFilter(
            description=
            '\n            All buffs with this tag will be saved and reapplied to customer sims\n            on load.\n            ',
            tunable_type=tag.Tag,
            default=tag.Tag.INVALID,
            invalid_enums=(tag.Tag.INVALID, ),
            filter_prefixes=('buff', ),
            pack_safe=True,
            tuning_group=GroupNames.CUSTOMER),
        'customer_buffs_to_remove_tags':
        TunableSet(
            description=
            '\n            Tags that indicate which buffs should be removed from customers when\n            they leave the business.\n            ',
            tunable=TunableEnumWithFilter(
                description=
                '\n                A tag that indicates a buff should be removed from the customer\n                when they leave the business.\n                ',
                tunable_type=tag.Tag,
                default=tag.Tag.INVALID,
                invalid_enums=(tag.Tag.INVALID, ),
                filter_prefixes=('buff', ),
                pack_safe=True),
            tuning_group=GroupNames.CUSTOMER),
        'current_business_lot_transfer_dialog_entry':
        TunableLocalizedString(
            description=
            '\n            This is the text that will show in the funds transfer dialog drop\n            down for the current lot if it\'s a business lot. Typically, the lot\n            name would show but if the active lot is a business lot it makes\n            more sense to say something along the lines of\n            "Current Retail Lot" or "Current Restaurant" instead of the name of the lot.\n            ',
            tuning_group=GroupNames.UI),
        'open_business_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that shows up when the player opens the business.\n            We need to trigger this from code because we need the notification\n            to show up when we open the store through the UI or through an\n            Interaction.\n            ',
            tuning_group=GroupNames.UI),
        'no_way_to_make_money_notification':
        TunableUiDialogNotificationSnippet(
            description=
            '\n            The notification that shows up when the player opens a store that has no\n            way of currently making money (e.g. retail store having no items set for\n            sale or restaurants having nothing on the menu). It will replace the\n            Open Business Notification.\n            ',
            tuning_group=GroupNames.UI),
        'audio_sting_open':
        TunablePlayAudioAllPacks(
            description=
            '\n            The audio sting to play when the store opens.\n            ',
            tuning_group=GroupNames.UI),
        'audio_sting_close':
        TunablePlayAudioAllPacks(
            description=
            '\n            The audio sting to play when the store closes.\n            ',
            tuning_group=GroupNames.UI),
        'sell_store_dialog':
        UiDialogOkCancel.TunableFactory(
            description=
            '\n            This dialog is to confirm the sale of the business.\n            ',
            tuning_group=GroupNames.UI),
        'lighting_helper_open':
        LightingHelper.TunableFactory(
            description=
            '\n            The lighting helper to execute when the store opens.\n            e.g. Turn on all neon signs.\n            ',
            tuning_group=GroupNames.TRIGGERS),
        'lighting_helper_close':
        LightingHelper.TunableFactory(
            description=
            '\n            The lighting helper to execute when the store closes.\n            e.g. Turn off all neon signs.\n            ',
            tuning_group=GroupNames.TRIGGERS),
        'min_and_max_star_rating':
        TunableInterval(
            description=
            '\n            The lower and upper bounds for a star rating. This affects both the\n            customer star rating and the overall business star rating.\n            ',
            tunable_type=float,
            default_lower=1,
            default_upper=5,
            tuning_group=GroupNames.BUSINESS),
        'min_and_max_star_rating_value':
        TunableInterval(
            description=
            '\n            The minimum and maximum star rating value for this business.\n            ',
            tunable_type=float,
            default_lower=0,
            default_upper=100,
            tuning_group=GroupNames.BUSINESS),
        'star_rating_value_to_user_facing_rating_curve':
        TunableCurve(
            description=
            '\n           Curve that maps star rating values to the user-facing star rating.\n           ',
            x_axis_name='Star Rating Value',
            y_axis_name='User-Facing Star Rating',
            tuning_group=GroupNames.BUSINESS),
        'default_business_star_rating_value':
        TunableRange(
            description=
            '\n            The star rating value a newly opened business will begin with. Keep in mind, this is not the actual star rating. This is the value which maps to a rating using \n            ',
            tunable_type=float,
            default=1,
            minimum=0,
            tuning_group=GroupNames.BUSINESS),
        'customer_rating_delta_to_business_star_rating_value_change_curve':
        TunableCurve(
            description=
            '\n            When a customer is done with their meal, we will take the delta\n            between their rating and the business rating and map that to an\n            amount it should change the star rating value for the restaurant.\n            \n            For instance, the business has a current rating of 3 stars and the\n            customer is giving a rating of 4.5 stars. 4.5 - 3 = a delta of 1.5.\n            That 1.5 will map, on this curve, to the amount we should adjust the\n            star rating value for the business.\n            ',
            x_axis_name=
            'Customer Rating to Business Rating Delta (restaurant rating - customer rating)',
            y_axis_name='Business Star Rating Value Change',
            tuning_group=GroupNames.BUSINESS),
        'default_customer_star_rating':
        TunableRange(
            description=
            '\n            The star rating a new customer starts with.\n            ',
            tunable_type=float,
            default=3,
            minimum=0,
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_buff_bucket_data':
        TunableMapping(
            description=
            "\n            A mapping from Business Customer Star Rating Buff Bucket to the data\n            associated with the buff bucker for this business.\n            \n            Each buff bucket has a minimum, median, and maximum value. For every\n            buff a customer has that falls within a buff bucket, that buff's\n            Buff Bucket Delta is added to that bucket's totals. The totals are\n            clamped between -1 and 1 and interpolated against the\n            minimum/medium/maximum value for their associated buckets. All of\n            the final values of the buckets are added together and that value is\n            used in the Customer Star Buff Bucket To Rating Curve to determine\n            the customer's final star rating of this business.\n            \n            For instance, assume a buff bucket has a minimum value of -200, median of 0,\n            and maximum of 100, and the buff bucket's clamped total is 0.5, the actual\n            value of that bucket will be 50 (half way, or 0.5, between 0 and\n            100). If, however, the bucket's total is -0.5, we'd interpolate\n            between the bucket's minimum value, -200, and median value, 0, to arrive at a\n            bucket value of -100.\n            ",
            key_name='Star_Rating_Buff_Bucket',
            key_type=TunableEnumEntry(
                description=
                '\n                The Business Customer Star Rating Buff Bucket enum.\n                ',
                tunable_type=BusinessCustomerStarRatingBuffBuckets,
                default=BusinessCustomerStarRatingBuffBuckets.INVALID,
                invalid_enums=(
                    BusinessCustomerStarRatingBuffBuckets.INVALID, )),
            value_name='Star_Rating_Buff_Bucket_Data',
            value_type=TunableTuple(
                description=
                '\n                All of the data associated with a specific customer star rating\n                buff bucket.\n                ',
                bucket_value_minimum=Tunable(
                    description=
                    "\n                    The minimum value for this bucket's values.\n                    ",
                    tunable_type=float,
                    default=-100),
                positive_bucket_vfx=PlayEffect.TunableFactory(
                    description=
                    '\n                    The vfx to play when positive change star value occurs. \n                    '
                ),
                negative_bucket_vfx=PlayEffect.TunableFactory(
                    description=
                    '\n                    The vfx to play when negative change star value occurs.\n                    '
                ),
                bucket_value_median=Tunable(
                    description=
                    "\n                    The median/middle value for this bucket's values.\n                    ",
                    tunable_type=float,
                    default=0),
                bucket_value_maximum=Tunable(
                    description=
                    "\n                    The maximum value for this bucket's values.\n                    ",
                    tunable_type=float,
                    default=100),
                bucket_icon=TunableIconAllPacks(
                    description=
                    '\n                    The icon that represents this buff bucket.\n                    '
                ),
                bucket_positive_text=TunableLocalizedStringFactoryVariant(
                    description=
                    '\n                    The possible text strings to show up when this bucket\n                    results in a positive star rating.\n                    '
                ),
                bucket_negative_text=TunableLocalizedStringFactoryVariant(
                    description=
                    '\n                    The possible text strings to show up when this bucket\n                    results in a bad star rating.\n                    '
                ),
                bucket_excellence_text=TunableLocalizedStringFactoryVariant(
                    description=
                    "\n                    The description text to use in the business summary panel if\n                    this buff bucket is in the 'Excellence' section.\n                    "
                ),
                bucket_growth_opportunity_text=
                TunableLocalizedStringFactoryVariant(
                    description=
                    "\n                    The description text to use in the business summary panel if\n                    this buff bucket is in the 'Growth Opportunity' section.\n                    "
                ),
                bucket_growth_opportunity_threshold=TunableRange(
                    description=
                    '\n                    The amount of score this bucket must be from the maximum to be\n                    considered a growth opportunity. \n                    ',
                    tunable_type=float,
                    minimum=0,
                    default=10),
                bucket_excellence_threshold=TunableRange(
                    description=
                    '\n                    The amount of score this bucket must be before it is \n                    considered an excellent bucket\n                    ',
                    tunable_type=float,
                    minimum=0,
                    default=1),
                bucket_title=TunableLocalizedString(
                    description=
                    '\n                    The name for this bucket.\n                    '
                )),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_buff_data':
        TunableMapping(
            description=
            '\n            A mapping of Buff to the buff data associated with that buff.\n            \n            Refer to the description on Customer Star Rating Buff Bucket Data\n            for a detailed explanation of how this tuning works.\n            ',
            key_name='Buff',
            key_type=Buff.TunableReference(
                description=
                "\n                A buff meant to drive a customer's star rating for a business.\n                ",
                pack_safe=True),
            value_name='Buff Data',
            value_type=TunableTuple(
                description=
                '\n                The customer star rating for this buff.\n                ',
                buff_bucket=TunableEnumEntry(
                    description=
                    '\n                    The customer star rating buff bucket associated with this buff.\n                    ',
                    tunable_type=BusinessCustomerStarRatingBuffBuckets,
                    default=BusinessCustomerStarRatingBuffBuckets.INVALID,
                    invalid_enums=(
                        BusinessCustomerStarRatingBuffBuckets.INVALID, )),
                buff_bucket_delta=Tunable(
                    description=
                    '\n                    The amount of change this buff should contribute to its bucket.\n                    ',
                    tunable_type=float,
                    default=0),
                update_star_rating_on_add=Tunable(
                    description=
                    "\n                    If enabled, the customer's star rating will be re-\n                    calculated when this buff is added.\n                    ",
                    tunable_type=bool,
                    default=True),
                update_star_rating_on_remove=Tunable(
                    description=
                    "\n                    If enabled, the customer's star rating will be re-\n                    calculated when this buff is removed.\n                    ",
                    tunable_type=bool,
                    default=False)),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_buff_bucket_to_rating_curve':
        TunableCurve(
            description=
            '\n            A mapping of the sum of all buff buckets for a single customer to\n            the star rating for that customer.\n            \n            Refer to the description on Customer Star Rating Buff Bucket Data\n            for a detailed explanation of how this tuning works.\n            ',
            x_axis_name='Buff Bucket Total',
            y_axis_name='Star Rating',
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_vfx_increase_arrow':
        OptionalTunable(
            description=
            '\n            The "up arrow" VFX to play when a customer\'s star rating goes up.\n            These will play even if the customer\'s rating doesn\'t go up enough\n            to trigger a star change.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_vfx_decrease_arrow':
        OptionalTunable(
            description=
            '\n            The "down arrow" VFX to play when a customer\'s star rating goes\n            down. These will play even if the customer\'s rating doesn\'t go down\n            enough to trigger a star change.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_vfx_mapping':
        TunableStarRatingVfxMapping(
            description=
            '\n            Maps the star rating for the customer to the persistent star effect\n            that shows over their head.\n            ',
            tuning_group=GroupNames.CUSTOMER),
        'customer_final_star_rating_vfx':
        OptionalTunable(
            description=
            '\n            The VFX to play when the customer is done and is submitting their\n            final star rating to the business.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_max_star_rating_vfx':
        OptionalTunable(
            description=
            '\n            The VFX to play when the customer hits the maximum star rating.\n            ',
            tunable=PlayEffect.TunableFactory(),
            tuning_group=GroupNames.CUSTOMER),
        'customer_star_rating_statistic':
        TunablePackSafeReference(
            description=
            '\n            The statistic on a customer Sim that represents their current star\n            rating.\n            ',
            manager=services.get_instance_manager(Types.STATISTIC),
            allow_none=True,
            tuning_group=GroupNames.CUSTOMER),
        'buy_business_lot_affordance':
        TunableReference(
            description=
            '\n            The affordance to buy a lot for this type of business.\n            ',
            manager=services.get_instance_manager(Types.INTERACTION),
            tuning_group=GroupNames.UI),
        'initial_funds_transfer_amount':
        TunableRange(
            description=
            '\n            The amount to default the funds transfer dialog when a player\n            initially buys this business.\n            ',
            tunable_type=int,
            minimum=0,
            default=2500,
            tuning_group=GroupNames.CURRENCY),
        'summary_dialog_icon':
        TunableIcon(
            description=
            '\n            The Icon to show in the header of the dialog.\n            ',
            tuning_group=GroupNames.UI),
        'summary_dialog_subtitle':
        TunableLocalizedString(
            description=
            "\n            The subtitle for the dialog. The main title will be the store's name.\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_transactions_header':
        TunableLocalizedString(
            description=
            "\n            The header for the 'Items Sold' line item. By design, this should say\n            something along the lines of 'Items Sold:' or 'Transactions:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_transactions_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Items Sold' line item. By design, this should say\n            the number of items sold.\n            {0.Number} = number of items sold since the store was open\n            i.e. {0.Number}\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_cost_of_ingredients_header':
        TunableLocalizedString(
            description=
            "\n            The header for the 'Cost of Ingredients' line item. By design, this\n            should say something along the lines of 'Cost of Ingredients:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_cost_of_ingredients_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Cost of Ingredients' line item. {0.Number} = the\n            amount of money spent on ingredients.\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_food_profit_header':
        TunableLocalizedString(
            description=
            "\n            The header for the 'Food Profits' line item. This line item is the\n            total revenue minus the cost of ingredients. By design, this should\n            say something along the lines of 'Food Profits:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_food_profit_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Food Profits' line item. {0.Number} = the amount of\n            money made on food.\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_owed_header':
        TunableLocalizedString(
            description=
            "\n            The header text for the 'Wages Owned' line item. By design, this\n            should say 'Wages Owed:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_owed_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Wages Owed' line item. By design, this should say the\n            number of hours worked and the price per hour.\n            {0.Number} = number of hours worked by all employees\n            {1.Money} = amount employees get paid per hour\n            i.e. {0.Number} hours worked x {1.Money}/hr\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_payroll_header':
        TunableLocalizedStringFactory(
            description=
            '\n            The header text for each unique Sim on payroll. This is provided one\n            token, the Sim.\n            ',
            tuning_group=GroupNames.UI),
        'summary_dialog_payroll_text':
        TunableLocalizedStringFactory(
            description=
            '\n            The text for each job that the Sim on payroll has held today. This is\n            provided three tokens: the career level name, the career level salary,\n            and the total hours worked.\n            \n            e.g.\n             {0.String} ({1.Money}/hr) * {2.Number} {S2.hour}{P2.hours}\n            ',
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_advertising_header':
        TunableLocalizedString(
            description=
            "\n            The header text for the 'Advertising' line item. By design, this\n            should say 'Advertising Spent:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_advertising_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Advertising' line item. By design, this should say the\n            amount spent on advertising\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_net_profit_header':
        TunableLocalizedString(
            description=
            "\n            The header text for the 'Net Profit' line item. By design, this\n            should say 'Net Profit:'\n            ",
            tuning_group=GroupNames.UI),
        'summary_dialog_wages_net_profit_text':
        TunableLocalizedStringFactory(
            description=
            "\n            The text in the 'Net Profit' line item. By design, this should say the\n            total amount earnt so far in this shift\n            ",
            tuning_group=GroupNames.UI),
        'grand_opening_notification':
        OptionalTunable(
            description=
            '\n            If enabled, allows a notification to be tuned that will show only\n            the first time you arrive on your business lot.\n            ',
            tunable=TunableUiDialogNotificationSnippet(),
            tuning_group=GroupNames.UI),
        'business_icon':
        TunableIcon(
            description=
            '\n            The Icon to show in the header of the dialog.\n            ',
            tuning_group=GroupNames.UI),
        'star_rating_to_customer_count_curve':
        TunableCurve(
            description=
            '\n            A curve mapping of current star rating of the restaurant to the base\n            number of customers that should come per interval.\n            ',
            x_axis_name='Star Rating',
            y_axis_name='Base Customer Count',
            tuning_group=GroupNames.CUSTOMER),
        'time_of_day_to_customer_count_multiplier_curve':
        TunableCurve(
            description=
            '\n            A curve that lets you tune a specific customer multiplier based on the \n            time of day. \n            \n            Time of day should range between 0 and 23, 0 being midnight.\n            ',
            tuning_group=GroupNames.CUSTOMER,
            x_axis_name='time_of_day',
            y_axis_name='customer_multiplier'),
        'off_lot_customer_count_multiplier':
        TunableRange(
            description=
            '\n            This value will be multiplied by the Base Customer Count (derived\n            from the Star Rating To Customer Count Curve) to determine the base\n            number of customers per hour during off-lot simulation.\n            ',
            tunable_type=float,
            minimum=0,
            default=0.5,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_customer_count_penalty_multiplier':
        TunableRange(
            description=
            '\n            A penalty multiplier applied to the off-lot customer count. This is\n            applied after the Off Lot Customer Count Multiplier is applied.\n            ',
            tunable_type=float,
            default=0.2,
            minimum=0,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_chance_of_star_rating_increase':
        TunableRange(
            description=
            "\n            Every time we run offlot simulations, we'll use this as the chance\n            to increase in star rating instead of decrease.\n            ",
            tunable_type=float,
            default=0.1,
            minimum=0,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_star_rating_decay_per_hour_curve':
        TunableCurve(
            description=
            '\n            Maps the current star rating of the business to the decay per hour\n            of star rating value. This value will be added to the current star\n            rating value so use negative numbers to make the rating decay.\n            ',
            x_axis_name='Business Star Rating',
            y_axis_name='Off-Lot Star Rating Value Decay Per Hour',
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_star_rating_increase_per_hour_curve':
        TunableCurve(
            description=
            '\n            Maps the current star rating of the business to the increase per\n            hour of the star rating value, assuming the Off Lot Chance Of Star\n            Rating Increase passes.\n            ',
            x_axis_name='Business Star Rating',
            y_axis_name='Off-Lot Star Rating Value Increase Per Hour',
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_profit_per_item_multiplier':
        TunableRange(
            description=
            '\n            This is multiplied by the average cost of the business specific\n            service that is the main source of profit, to determine how much \n            money the business makes per customer during off-lot simulation.\n            ',
            tunable_type=float,
            default=0.3,
            minimum=0,
            tuning_group=GroupNames.OFF_LOT),
        'off_lot_net_loss_notification':
        OptionalTunable(
            description=
            '\n            If enabled, the notification that will show if a business turns a \n            negative net profit during off-lot simulation.\n            ',
            tunable=TunableUiDialogNotificationSnippet(),
            tuning_group=GroupNames.OFF_LOT),
        'critic':
        OptionalTunable(
            description=
            '\n            If enabled, allows tuning a critic for this business type.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Critic tuning for this business.\n                ',
                critic_trait=TunableReference(
                    description=
                    '\n                    The trait used to identify a critic of this business.\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.TRAIT)),
                critic_star_rating_application_count=TunableRange(
                    description=
                    '\n                    The number of times a critics star rating should count towards the\n                    business star rating.\n                    ',
                    tunable_type=int,
                    default=10,
                    minimum=1),
                critic_star_rating_vfx_mapping=TunableStarRatingVfxMapping(
                    description=
                    '\n                    Maps the star rating for the critic to the persistent star effect\n                    that shows over their head.\n                    '
                ),
                critic_banner_vfx=PlayEffect.TunableFactory(
                    description=
                    '\n                    A persistent banner VFX that is started when the critic\n                    arrives and stopped when they leave.\n                    '
                )),
            tuning_group=GroupNames.CUSTOMER)
    }

    @classmethod
    def _verify_tuning_callback(cls):
        advertising_data_types = frozenset(
            cls.advertising_configuration.advertising_data_map.keys())
        advertising_types_with_mapped_names = frozenset(
            cls.advertising_name_map.keys())
        advertising_sort_ordered_types = frozenset(
            cls.advertising_name_map.keys())
        if advertising_data_types:
            if advertising_data_types != advertising_types_with_mapped_names:
                logger.error(
                    'Advertising type list {} does not match list of mapped names: {}',
                    advertising_data_types,
                    advertising_types_with_mapped_names)
            if advertising_data_types != advertising_sort_ordered_types:
                logger.error(
                    'Advertising type list {} does not sorted UI list types: {}',
                    advertising_data_types, advertising_sort_ordered_types)
        if cls.advertising_configuration.default_advertising_type is not None and cls.advertising_configuration.default_advertising_type not in advertising_types_with_mapped_names:
            logger.error(
                'Default advertising type {} is not in advertising name map',
                cls.default_advertising_type)
Example #6
0
class UiTextInput(HasTunableSingletonFactory, AutoFactoryInit):
    FACTORY_TUNABLES = {
        'default_text':
        OptionalTunable(
            description=
            "\n            Default text that will show up when the text box is not in focus if\n            the user hasn't entered anything in the text box yet.\n            \n            If only default text is set, the text box will be blank when the\n            user puts it in focus.\n            ",
            tunable=TunableLocalizedStringFactory()),
        'initial_value':
        OptionalTunable(
            description=
            '\n            The initial value of the text in the textbox. This is different from\n            default text because the initial value stays regardless of if the\n            text box is in focus.\n            ',
            tunable=TunableLocalizedStringFactoryVariant()),
        'title':
        OptionalTunable(
            description=
            '\n             Text that will be shown with the text input to describe what that\n             user is inputing.\n             ',
            tunable=TunableLocalizedStringFactory()),
        'length_restriction':
        _TunableTextInputLengthVariant(),
        'restricted_characters':
        OptionalTunable(
            description=
            '\n             A string containing the character set regex to determine restricted\n             characters in the text input.\n             ',
            tunable=TunableLocalizedStringFactory())
    }

    def __init__(self, *args, sort_order, **kwargs):
        super().__init__(*args, **kwargs)
        self.sort_order = sort_order

    @property
    def min_length(self):
        return self.length_restriction.min_length

    def build_msg(self,
                  dialog,
                  msg,
                  name,
                  text_input_overrides=None,
                  additional_tokens=(),
                  max_value=None,
                  invalid_max_tooltip=None,
                  min_value=None,
                  invalid_min_tooltip=None):
        initial_value = self.initial_value
        if text_input_overrides is not None:
            if name not in text_input_overrides:
                return
            initial_value = text_input_overrides[name] or self.initial_value
        text_input_msg = msg.text_input.add()
        text_input_msg.text_input_name = name
        if initial_value is not None:
            text_input_msg.initial_value = dialog._build_localized_string_msg(
                initial_value, *additional_tokens)
        if self.default_text is not None:
            text_input_msg.default_text = dialog._build_localized_string_msg(
                self.default_text, *additional_tokens)
        if self.title:
            text_input_msg.title = dialog._build_localized_string_msg(
                self.title, *additional_tokens)
        if self.restricted_characters is not None:
            text_input_msg.restricted_characters = dialog._build_localized_string_msg(
                self.restricted_characters, *additional_tokens)
        if max_value is not None:
            text_input_msg.max_value = int(max_value)
        if invalid_max_tooltip is not None:
            text_input_msg.input_invalid_max_tooltip = dialog._build_localized_string_msg(
                invalid_max_tooltip, *additional_tokens)
        if min_value is not None:
            text_input_msg.min_value = int(min_value)
        if invalid_min_tooltip is not None:
            text_input_msg.input_invalid_min_tooltip = dialog._build_localized_string_msg(
                invalid_min_tooltip, *additional_tokens)
        self.length_restriction.build_msg(dialog, text_input_msg,
                                          *additional_tokens)
Example #7
0
class AdventureMoment(HasTunableFactory, AutoFactoryInit):
    LOOT_NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to recursively build loot notification text. It will be\n        given two tokens: a loot display text string, if any, and the previously\n        built LOOT_NOTIFICATION_TEXT string.\n        ')
    NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to format notifications. It will be given two arguments:\n        the notification text and the built version of LOOT_NOTIFICATION_TEXT,\n        if not empty.\n        ')
    CHEAT_TEXT = TunableTuple(description='\n        Strings to be used for display text on cheat buttons to trigger all\n        adventure moments. \n        ', previous_display_text=TunableLocalizedStringFactory(description='\n            Text that will be displayed on previous cheat button.\n            '), next_display_text=TunableLocalizedStringFactory(description='\n            Text that will be displayed on next cheat button.\n            '), text_pattern=TunableLocalizedStringFactory(description='\n            Format for displaying next and previous buttons text including the\n            progress.\n            '), tooltip=TunableLocalizedStringFactory(description='\n            Tooltip to show when disabling previous or next button.\n            '))
    COST_TYPE_SIMOLEONS = 0
    COST_TYPE_ITEMS = 1
    CHEAT_PREVIOUS_INDEX = 1
    CHEAT_NEXT_INDEX = 2
    FACTORY_TUNABLES = {'description': '\n            A phase of an adventure. Adventure moments may present\n            some information in a dialog form and for a choice to be\n            made regarding how the overall adventure will branch.\n            ', '_visibility': OptionalTunable(description='\n            Control whether or not this moment provides visual feedback to\n            the player (i.e., a modal dialog).\n            ', tunable=UiDialog.TunableFactory(), disabled_name='not_visible', enabled_name='show_dialog'), '_finish_actions': TunableList(description='\n            A list of choices that can be made by the player to determine\n            branching for the adventure. They will be displayed as buttons\n            in the UI. If no dialog is displayed, then the first available\n            finish action will be selected. If this list is empty, the\n            adventure ends.\n            ', tunable=TunableTuple(availability_tests=TunableTestSet(description='\n                    A set of tests that must pass in order for this Finish\n                    Action to be available on the dialog. A Finish Action failing\n                    all tests is handled as if it were never tuned.\n                    '), display_text=TunableLocalizedStringFactoryVariant(description="\n                   This finish action's title. This will be the button text in\n                   the UI.\n                   ", allow_none=True), display_subtext=TunableLocalizedStringFactoryVariant(description='\n                    If tuned, this text will display below the button for this Finish Action.\n                    \n                    Span tags can be used to change the color of the text to green/positive and red/negative.\n                    <span class="positive">TEXT</span> will make the word TEXT green\n                    <span class="negative">TEXT</span> will make the word TEXT red\n                    ', allow_none=True), disabled_text=OptionalTunable(description="\n                    If enabled, this is the string that will be displayed if \n                    this finishing action is not available because the tests \n                    don't pass.\n                    ", tunable=TunableLocalizedStringFactory()), cost=TunableVariant(description='\n                    The cost associated with this finish action. Only one type\n                    of cost may be tuned. The player is informed of the cost\n                    before making the selection by modifying the display_text\n                    string to include this information.\n                    ', simoleon_cost=TunableTuple(description="The specified\n                        amount will be deducted from the Sim's funds.\n                        ", locked_args={'cost_type': COST_TYPE_SIMOLEONS}, amount=TunableRange(description='How many Simoleons to\n                            deduct.\n                            ', tunable_type=int, default=0, minimum=0)), item_cost=TunableTuple(description="The specified items will \n                        be removed from the Sim's inventory.\n                        ", locked_args={'cost_type': COST_TYPE_ITEMS}, item_cost=ItemCost.TunableFactory()), default=None), action_results=TunableList(description='\n                    A list of possible results that can occur if this finish\n                    action is selected. Action results can award loot, display\n                    notifications, and control the branching of the adventure by\n                    selecting the next adventure moment to run.\n                    ', tunable=TunableTuple(weight_modifiers=TunableList(description='\n                            A list of modifiers that affect the probability that\n                            this action result will be chosen. These are exposed\n                            in the form (test, multiplier). If the test passes,\n                            then the multiplier is applied to the running total.\n                            The default multiplier is 1. To increase the\n                            likelihood of this action result being chosen, tune\n                            multiplier greater than 1. To decrease the\n                            likelihood of this action result being chose, tune\n                            multipliers lower than 1. If you want to exclude\n                            this action result from consideration, tune a\n                            multiplier of 0.\n                            ', tunable=TunableTuple(description='\n                                A pair of test and weight multiplier. If the\n                                test passes, the associated weight multiplier is\n                                applied. If no test is specified, the multiplier\n                                is always applied.\n                                ', test=TunableTestVariant(description='\n                                    The test that has to pass for this weight\n                                    multiplier to be applied. The information\n                                    available to this test is the same\n                                    information available to the interaction\n                                    owning this adventure.\n                                    ', test_locked_args={'tooltip': None}), weight_multiplier=Tunable(description='\n                                    The weight multiplier to apply if the\n                                    associated test passes.\n                                    ', tunable_type=float, default=1))), notification=OptionalTunable(description='\n                            If set, this notification will be displayed.\n                            ', tunable=TunableUiDialogNotificationSnippet()), next_moments=TunableList(description='\n                            A list of adventure moment keys. One of these keys will\n                            be selected to determine which adventure moment is\n                            selected next. If the list is empty, the adventure ends\n                            here. Any of the keys tuned here will have to be tuned\n                            in the _adventure_moments tunable for the owning adventure.\n                            ', tunable=AdventureMomentKey), loot_actions=TunableList(description='\n                            List of Loot actions that are awarded if this action result is selected.\n                            ', tunable=LootActions.TunableReference()), continuation=TunableContinuation(description='\n                            A continuation to push when running finish actions.\n                            '), results_dialog=OptionalTunable(description='\n                            A results dialog to show. This dialog allows a list\n                            of icons with labels.\n                            ', tunable=UiDialogLabeledIcons.TunableFactory()), events_to_send=TunableList(description='\n                            A list of events to send.\n                            ', tunable=TunableEnumEntry(description='\n                                events types to send\n                                ', tunable_type=TestEvent, default=TestEvent.Invalid))))))}

    def __init__(self, parent_adventure, **kwargs):
        super().__init__(**kwargs)
        self._parent_adventure = parent_adventure
        self.resolver = self._interaction.get_resolver()

    @property
    def _interaction(self):
        return self._parent_adventure.interaction

    @property
    def _sim(self):
        return self._interaction.sim

    def run_adventure(self):
        if self._visibility is None:
            if self._finish_actions:
                self._run_first_valid_finish_action()
        else:
            dialog = self._get_dialog()
            if dialog is not None:
                self._parent_adventure.force_action_result = True
                dialog.show_dialog(auto_response=0)

    def _run_first_valid_finish_action(self):
        resolver = self.resolver
        for (action_id, finish_action) in enumerate(self._finish_actions):
            if finish_action.availability_tests.run_tests(resolver):
                return self._run_action_from_index(action_id)

    def _is_action_result_available(self, action_result):
        if not action_result.next_moments:
            return True
        for moment_key in action_result.next_moments:
            if self._parent_adventure.is_adventure_moment_available(moment_key):
                return True
        return False

    def _run_action_from_cheat(self, action_index):
        cheat_index = action_index - len(self._finish_actions) + 1
        if cheat_index == self.CHEAT_PREVIOUS_INDEX:
            self._parent_adventure.run_cheat_previous_moment()
        elif cheat_index == self.CHEAT_NEXT_INDEX:
            self._parent_adventure.run_cheat_next_moment()

    def _get_action_result_weight(self, action_result):
        interaction_resolver = self.resolver
        weight = 1
        for modifier in action_result.weight_modifiers:
            if not modifier.test is None:
                if interaction_resolver(modifier.test):
                    weight *= modifier.weight_multiplier
            weight *= modifier.weight_multiplier
        return weight

    def _apply_action_cost(self, action):
        if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
            if not self._sim.family_funds.try_remove(action.cost.amount, Consts_pb2.TELEMETRY_INTERACTION_COST, sim=self._sim):
                return False
        elif action.cost.cost_type == self.COST_TYPE_ITEMS:
            item_cost = action.cost.item_cost
            return item_cost.consume_interaction_cost(self._interaction)()
        return True

    def _run_action_from_index(self, action_index):
        try:
            finish_action = self._finish_actions[action_index]
        except IndexError as err:
            logger.exception('Exception {} while attempting to get finish action.\nFinishActions length: {}, ActionIndex: {},\nCurrent Moment: {},\nResolver: {}.\n', err, len(self._finish_actions), action_index, self._parent_adventure._current_moment_key, self.resolver)
            return
        forced_action_result = False
        weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results if self._is_action_result_available(action_result)]
        if not weight_pairs:
            if self._parent_adventure.force_action_result:
                forced_action_result = True
                weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results]
        action_result = weighted_random_item(weight_pairs)
        if not (action_result is not None or not finish_action.action_results) and not self._apply_action_cost(finish_action):
            return
        if action_result is not None:
            loot_display_text = None
            resolver = self.resolver
            for actions in action_result.loot_actions:
                for (loot_op, test_ran) in actions.get_loot_ops_gen(resolver):
                    (success, _) = loot_op.apply_to_resolver(resolver, skip_test=test_ran)
                    if success and action_result.notification is not None:
                        current_loot_display_text = loot_op.get_display_text()
                        if current_loot_display_text is not None:
                            if loot_display_text is None:
                                loot_display_text = current_loot_display_text
                            else:
                                loot_display_text = self.LOOT_NOTIFICATION_TEXT(loot_display_text, current_loot_display_text)
            if action_result.notification is not None:
                if loot_display_text is not None:
                    notification_text = lambda *tokens: self.NOTIFICATION_TEXT(action_result.notification.text(*tokens), loot_display_text)
                else:
                    notification_text = action_result.notification.text
                dialog = action_result.notification(self._sim, self.resolver)
                dialog.text = notification_text
                dialog.show_dialog()
            if action_result.next_moments:
                if forced_action_result:
                    next_moment_key = random.choice(action_result.next_moments)
                else:
                    next_moment_key = random.choice(tuple(moment_key for moment_key in action_result.next_moments if self._parent_adventure.is_adventure_moment_available(moment_key)))
                self._parent_adventure.queue_adventure_moment(next_moment_key)
            if action_result.results_dialog:
                dialog = action_result.results_dialog(self._sim, resolver=self.resolver)
                dialog.show_dialog()
            event_manager = services.get_event_manager()
            for event_type in action_result.events_to_send:
                event_manager.process_event(event_type, sim_info=self._sim.sim_info)
            if action_result.continuation:
                self._interaction.push_tunable_continuation(action_result.continuation)

    def _is_cheat_response(self, response):
        cheat_response = response - len(self._finish_actions)
        if cheat_response < 0:
            return False
        return True

    def _on_dialog_response(self, dialog):
        response_index = dialog.response
        if response_index is None:
            return
        if False and self._is_cheat_response(response_index):
            self._run_action_from_cheat(response_index)
            return
        if response_index >= len(self._finish_actions):
            return
        self._run_action_from_index(response_index)

    def _get_action_display_text(self, action):
        display_name = self._interaction.create_localized_string(action.display_text)
        if action.cost is not None:
            if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
                amount = action.cost.amount
                display_name = self._interaction.SIMOLEON_COST_NAME_FACTORY(display_name, amount)
            elif action.cost.cost_type == self.COST_TYPE_ITEMS:
                item_cost = action.cost.item_cost
                display_name = item_cost.get_interaction_name(self._interaction, display_name)
        return lambda *_, **__: display_name

    def _get_dialog(self):
        resolver = self.resolver
        dialog = self._visibility(self._sim, resolver)
        responses = []
        for (action_id, finish_action) in enumerate(self._finish_actions):
            result = finish_action.availability_tests.run_tests(resolver)
            if not result:
                if finish_action.disabled_text is not None:
                    disabled_text = finish_action.disabled_text if not result else None
                    responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None))
            disabled_text = finish_action.disabled_text if not result else None
            responses.append(UiDialogResponse(dialog_response_id=action_id, text=self._get_action_display_text(finish_action), subtext=self._interaction.create_localized_string(finish_action.display_subtext), disabled_text=disabled_text() if disabled_text is not None else None))
        if not responses:
            return
        if False and _show_all_adventure_moments:
            responses.extend(self._parent_adventure.get_cheat_responses(action_id))
        dialog.set_responses(responses)
        dialog.add_listener(self._on_dialog_response)
        return dialog
class LocalizedStringHouseholdNameSelector(HasTunableSingletonFactory,
                                           AutoFactoryInit):
    FACTORY_TUNABLES = {
        'empty_household':
        OptionalTunable(
            description=
            '\n            When enabled, this string will be used if the provided household\n            does not have any members.\n            ',
            tunable=TunableLocalizedStringFactoryVariant(
                description=
                '\n                The string to use if the provided household has no members. This\n                string is provided the same tokens as the original string.\n                '
            )),
        'single_sim':
        OptionalTunable(
            description=
            '\n            When enabled, this string will be used if the Sim is the only member\n            of the household. If disabled, this check will be ignored.\n            ',
            tunable=TunableLocalizedStringFactoryVariant(
                description=
                '\n                The string to use if the Sim is the only member of the\n                household. The first token is the only Sim of the household. It\n                might differ from the original Sim if the provided household is\n                different. The original Sim is the last token.\n                '
            )),
        'single_family':
        OptionalTunable(
            description=
            '\n            When enabled, this string will be used if the Sim is part of a\n            household where all Sims share the same last name. If disabled, this\n            check will be ignored.\n            ',
            tunable=TunableLocalizedStringFactoryVariant(
                description=
                '\n                The string to use if all Sims in the household share the same\n                last name. The first token is a string containing the household\n                name. The original Sim is the last token.\n                '
            )),
        'fallback':
        TunableLocalizedStringFactoryVariant(
            description=
            '\n            The string to use of no other rule applies. The first token is a\n            string containing the household name.\n            '
        ),
        'pets':
        OptionalTunable(
            description=
            '\n            If enabled, pet specific text is appended to the string.\n            ',
            tunable=TunableTuple(
                description=
                '\n                The various strings that apply specifically to pets.\n                ',
                single_pet=TunableLocalizedStringFactoryVariant(
                    description=
                    '\n                    The string to use if there is only one pet in the household.\n                    The first token is the pet.\n                    '
                ),
                multiple_pets=TunableLocalizedStringFactoryVariant(
                    description=
                    '\n                    The string to use if there is more than one pet in the\n                    household. The first token is the list of pets.\n                    '
                )))
    }

    def _get_string_for_humans(self, sim, household, *args, **kwargs):
        humans = tuple(
            household.get_humans_gen()) if household is not None else ()
        if not (self.empty_household is not None and
                (household is None or not humans)):
            return self.empty_household(sim, *args, **kwargs)
        if household is None:
            logger.error(
                "LocalizedStringHouseholdNameSelector is being provided a None household, but 'empty_household' text is unset."
            )
            return LocalizationHelperTuning.get_raw_text('')
        if self.single_sim is not None and len(humans) == 1:
            return self.single_sim(humans[0], *args + (sim, ), **kwargs)
        if self.single_family is not None and all(
                sim_info.last_name == sim.last_name
                for sim_info in humans) and sim.last_name == household.name:
            return self.single_family(sim.last_name, *args + (sim, ), **kwargs)
        return self.fallback(household.name, *args + (sim, ), **kwargs)

    def _get_string_for_pets(self, sim, household, *args, **kwargs):
        if self.pets is None:
            return
        pets = tuple(household.get_pets_gen()) if household is not None else ()
        if len(pets) == 1:
            return self.pets.single_pet(pets[0], *args, **kwargs)
        elif pets:
            return self.pets.multiple_pets(pets, *args, **kwargs)

    def __call__(self, sim, *args, household=DEFAULT, **kwargs):
        household = sim.household if household is DEFAULT else household
        string = self._get_string_for_humans(sim, household, *args, **kwargs)
        pets_string = self._get_string_for_pets(sim, household, *args,
                                                **kwargs)
        if pets_string is not None:
            string = LocalizationHelperTuning.NEW_LINE_LIST_STRUCTURE(
                string, pets_string)
        return string
Example #9
0
class BaseCivicPolicyProvider(ComponentContainer, HasStatisticComponent, HasTunableFactory, AutoFactoryInit):
    CIVIC_POLICY_SCHEDULE = TunableTuple(description='\n        Global schedule to control when voting on civic policies is active.\n        ', voting_open=TunableTimeOfWeek(description='\n            The time of the week that voting for civic policies starts.\n            ', default_day=Days.MONDAY, default_hour=8, default_minute=0), voting_close=TunableTimeOfWeek(description='\n            The time of the week that voting for civic policies ends.  Votes are\n            tallied and policies are modified at this time.\n            ', default_day=Days.SUNDAY, default_hour=16, default_minute=0), voting_close_warning_duration=TunableTimeSpan(description='\n            Duration before the Voting Close to warn players that voting is about to close.\n            ', default_hours=8), schedule_text=TunableLocalizedStringFactory(description='\n            Text for the schedule string.\n            '))
    INFLUENCE_BUCK_TYPE = TunableEnumEntry(description='\n        The type of Bucks used to hold Influence.\n        ', tunable_type=BucksType, default=BucksType.INVALID, pack_safe=True)
    INFLUENCE_TO_VOTE_COST = Tunable(description='\n        The amount of influence used with 1 vote.\n        ', tunable_type=int, default=10, export_modes=ExportModes.All)
    REPEAL_PETITION_THRESHOLD = Tunable(description='\n        The number of petition signatures required to have a policy repealed.\n        ', tunable_type=int, default=10, export_modes=ExportModes.All)
    COMMUNITY_BOARD_TAG = TunableTag(description='\n        The tag of the community boards so we can find them in the world.\n        ')
    VOTING_OPEN_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period opens.\n        ')
    VOTING_OPEN_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period opens with maximum enabled policies.\n        ')
    VOTING_CLOSE_WARNING_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting close approaches.\n        ')
    VOTING_CLOSE_WARNING_MAX_ENABLED_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting close approaches with maximum enabled policies and\n        a policy being repealed.\n        ')
    VOTING_CLOSE_NOTIFICATION = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes.\n        ')
    VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_SUCCESS = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes with maximum enabled\n        policies and a policy being successfully repealed.\n        ')
    VOTING_CLOSE_MAX_ENABLED_NOTIFICATION_FAIL = UiDialogNotification.TunableFactory(description='\n        A TNS that will fire when the voting period closes with maximum enabled\n        policies and a policy being unsuccessfully repealed.\n        ')
    VOTING_CONTINUATION_AUTONOMY_COMMODITIES = TunableList(description='\n        A list of static commodities that will be solved for by autonomy to\n        find and push the vote interaction after viewing the community board.\n        ', tunable=StaticCommodity.TunableReference(description='\n            A static commodity that is solved for by autonomy to find the vote\n            interaction to push. \n            ', pack_safe=True))
    COMMUNITY_BOARD_TEXT = TunableTuple(voting_closed_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible\n            because voting is closed.\n            "), voting_open_add_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            Text for the tooltip on the add policy button when it's disabled because\n            voting is open. \n            "), ineligible_voter_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible because\n            the sim (first token) lives on a different street.\n            "), ineligible_voter_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n            Text for the tooltip on the confirm button when the button is disabled because\n            the sim (first token) lives on a different street.\n            '), no_room_confirm_tooltip_text=TunableLocalizedStringFactory(description='\n            Text for the tooltip on the confirm button when the button is disabled because\n            already full up on enacted policies.\n            '), no_room_policy_tooltip_text=TunableLocalizedStringFactory(description="\n            String to insert into the policy tooltips when voting isn't possible  because\n            already full up on enacted policies.\n            "), add_policy_picker=TunablePickerDialogVariant(description='\n            The item picker dialog.\n            ', available_picker_flags=ObjectPickerTuningFlags.ITEM))
    CALL_TO_ACTIONS = TunableList(description='\n        List of Call to Action that should be started to introduce the Civic Policy features.\n        ', tunable=TunableReference(manager=services.get_instance_manager(sims4.resources.Types.CALL_TO_ACTION), pack_safe=True))
    FACTORY_TUNABLES = {'civic_policies': TunableSet(description='\n            The civic policies that may be enacted.\n            ', tunable=TunableReference(description='\n                A civic policy.\n                ', manager=services.get_instance_manager(sims4.resources.Types.SNIPPET), class_restrictions=('BaseCivicPolicy',), pack_safe=True, export_modes=ExportModes.All)), 'voting_open_loot': TunableList(description='\n            Loot applied to Resident Sims when voting opens.\n            ', tunable=LootActions.TunableReference(description='\n                Loot to apply on voting open.\n                ', pack_safe=True), export_modes=ExportModes.ServerXML), 'voting_close_loot': TunableList(description='\n            Loot applied to Resident Sims when voting opens.\n            ', tunable=LootActions.TunableReference(description='\n                Loot to apply on voting open.\n                ', pack_safe=True), export_modes=ExportModes.ServerXML), 'community_board_dialog_title': TunableLocalizedStringFactoryVariant(description="\n            The Community Board Dialog's title text.\n            ", export_modes=ExportModes.ServerXML), 'initial_vote_test': TunableTestSet(description='\n            If at least one test passes, and the user option is enabled, initial voting will\n            be performed when voting opens.\n            ', export_modes=ExportModes.ServerXML), 'daily_random_vote_test': TunableTestSet(description='\n            If at least one test passes, and the user option is enabled, daily random voting\n            will be performed at midnight.\n            ', export_modes=ExportModes.ServerXML)}
    CIVIC_POLICY_TEST_EVENTS = (TestEvent.CivicPolicyOpenVoting, TestEvent.CivicPolicyDailyRandomVoting, TestEvent.CivicPolicyCloseVoting)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.add_statistic_component()
        self._enacted_policies = set()
        self._balloted_policies = set()
        self._up_for_repeal_policies = set()
        self._civic_policies = set()
        for policy in self.civic_policies:
            self._civic_policies.add(policy(self))

    @constproperty
    def is_sim():
        return False

    @property
    def is_downloaded(self):
        return False

    def get_enacted_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._enacted_policies])
        return self._enacted_policies

    def get_balloted_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._balloted_policies])
        return self._balloted_policies

    def get_up_for_repeal_policies(self, tuning=False):
        if tuning:
            return set([type(p) for p in self._up_for_repeal_policies])
        return self._up_for_repeal_policies

    def get_dormant_policies(self, tuning=False):
        policies = self._civic_policies - self._enacted_policies - self._balloted_policies - self._up_for_repeal_policies
        if tuning:
            return set([type(p) for p in policies])
        return policies

    def get_civic_policies(self, tuning=False):
        if tuning:
            return self.civic_policies
        return self._civic_policies

    def new_enact_max_count(self):
        return min(len(self.civic_policies), self.max_enacted_policy_count) - len(self._enacted_policies)

    def _reset_voting_statistics(self):
        for policy in self._civic_policies:
            if policy.vote_count_statistic is None:
                logger.error('{} tuned without voting statistic', policy)
            else:
                self.set_stat_value(policy.vote_count_statistic, 0)
        self.commodity_tracker.check_for_unneeded_initial_statistics()
        self.statistic_tracker.check_for_unneeded_initial_statistics()

    def run_sim_voting_loot(self, loot_actions):
        for resolver in self.open_close_voting_loot_resolver_list:
            for loot in loot_actions:
                loot.apply_to_resolver(resolver)

    def run_auto_voting_tests(self, test_set):
        if not services.street_service().enable_automatic_voting:
            return False
        if not test_set:
            return True
        return test_set.run_tests(GlobalResolver())

    def open_voting(self):
        self._reset_voting_statistics()
        self.finalize_startup()
        self.run_sim_voting_loot(self.voting_open_loot)
        if self.run_auto_voting_tests(self.initial_vote_test):
            for policy in self._balloted_policies:
                self.vote(policy, policy.get_initial_vote_count())

    def close_voting(self):

        def get_most_voted_for_policy(policies):
            if not policies:
                return set()
            policy = max(policies, key=lambda policy: self.get_stat_value(policy.vote_count_statistic))
            if self.get_stat_value(policy.vote_count_statistic) <= 0:
                return set()
            return set((policy,))

        balloted_policies = self.get_balloted_policies()
        to_enact = get_most_voted_for_policy(balloted_policies)
        repealable_policies = self.get_up_for_repeal_policies()
        to_repeal = set()
        for policy in repealable_policies:
            if BaseCivicPolicyProvider.REPEAL_PETITION_THRESHOLD <= self.get_stat_value(policy.vote_count_statistic):
                to_repeal.add(policy)
        to_repeal -= to_enact
        to_enact -= self._enacted_policies
        self._enacted_policies -= to_repeal
        for policy in to_repeal:
            policy.repeal()
        to_enact_max_count = self.new_enact_max_count()
        while len(to_enact) > to_enact_max_count:
            to_enact.pop()
        self._enacted_policies.update(to_enact)
        for policy in to_enact:
            policy.enact()
        self._balloted_policies = set()
        self._up_for_repeal_policies = set()
        self._reset_voting_statistics()
        self.finalize_startup()
        self.run_sim_voting_loot(self.voting_close_loot)

    def get_schedule_text(self):
        return self.CIVIC_POLICY_SCHEDULE.schedule_text(self.CIVIC_POLICY_SCHEDULE.voting_open(), self.CIVIC_POLICY_SCHEDULE.voting_close())

    def do_daily_vote(self):
        if self.run_auto_voting_tests(self.daily_random_vote_test):
            for policy in self._balloted_policies:
                self.vote(policy, policy.get_daily_vote_count())

    @property
    def max_enacted_policy_count(self):
        raise NotImplementedError

    @property
    def max_balloted_policy_count(self):
        raise NotImplementedError

    @property
    def initial_balloted_policy_count(self):
        raise NotImplementedError

    @property
    def max_repealable_policy_count(self):
        raise NotImplementedError

    @property
    def open_close_voting_loot_resolver_list(self):
        raise NotImplementedError

    @classproperty
    def provider_type_id(cls):
        raise NotImplementedError

    def get_world_description_id(self):
        return 0

    def is_eligible_voter(self, sim_info):
        raise NotImplementedError

    def is_new_policy_allowed(self, sim_info):
        return False

    def _select_balloted_policies(self):
        self._balloted_policies.clear()
        count_needed = self.initial_balloted_policy_count
        r = random.Random()
        dormant_policies = list(self.get_dormant_policies())
        while dormant_policies:
            while len(self._balloted_policies) < count_needed:
                policy = r.choice(dormant_policies)
                dormant_policies.remove(policy)
                self._balloted_policies.add(policy)

    def finalize_startup(self):
        statistic_component = self.get_component(objects.components.types.STATISTIC_COMPONENT)
        statistic_component.on_finalize_load()
        if not self._civic_policies:
            return
        services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)
        services.get_event_manager().register(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)
        if not self._balloted_policies:
            self._select_balloted_policies()
        for policy in self._civic_policies:
            policy.finalize_startup()

    def stop_civic_policy_provider(self):
        services.get_event_manager().unregister(self, BaseCivicPolicyProvider.CIVIC_POLICY_TEST_EVENTS)

    def get_policy_instance_for_tuning(self, policy_guid64):
        for inst in self._civic_policies:
            if policy_guid64 == inst.guid64:
                return inst

    def enact(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None or policy in self._enacted_policies:
            return False
        if self.new_enact_max_count() == 0:
            return False
        self._enacted_policies.add(policy)
        self._balloted_policies.discard(policy)
        self._up_for_repeal_policies.discard(policy)
        policy.enact()
        return True

    def repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None or policy not in self._enacted_policies:
            return False
        self._enacted_policies.discard(policy)
        self._up_for_repeal_policies.discard(policy)
        policy.repeal()
        return True

    def vote(self, policy, count=1, user_directed=False, lobby_interaction=False):
        policy_instance = self.get_policy_instance_for_tuning(policy.guid64)
        if policy_instance is None:
            return False
        return self.vote_by_instance(policy_instance, count, user_directed, lobby_interaction)

    def vote_by_instance(self, policy_instance, count=1, user_directed=False, lobby_interaction=False):
        if policy_instance.vote_count_statistic is not None:
            policy_list = None

            def get_current_rank():
                policy_list.sort(key=lambda policy: (self.get_stat_value(policy.vote_count_statistic), policy.guid64), reverse=True)
                return policy_list.index(policy_instance)

            if user_directed:
                factor = 0
                if policy_instance in self._balloted_policies:
                    policy_list = list(self._balloted_policies)
                    factor = 1
                elif policy_instance in self._up_for_repeal_policies:
                    policy_list = list(self._up_for_repeal_policies)
                    factor = -1
                orig_rank = get_current_rank()
            elif lobby_interaction:
                factor = 0
                if policy_instance in self._balloted_policies:
                    factor = 1
                elif policy_instance in self._up_for_repeal_policies:
                    factor = -1
            value = self.get_stat_value(policy_instance.vote_count_statistic) + count
            self.set_stat_value(policy_instance.vote_count_statistic, value)
            services.street_service().update_community_board_tooltip(self)
            if user_directed:
                if policy_list is not None:
                    with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_VOTE) as hook:
                        hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
                        hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
                        hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value)
                        hook.write_guid(TELEMETRY_FIELD_PLAYER_VOTES, factor*count)
                        hook.write_guid(TELEMETRY_FIELD_OLD_RANK, orig_rank)
                        hook.write_guid(TELEMETRY_FIELD_NEW_RANK, get_current_rank())
            if lobby_interaction:
                with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_LOBBY) as hook:
                    hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
                    hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
                    hook.write_guid(TELEMETRY_FIELD_VOTES, factor*value)
            return True
        return False

    def _log_propose_telemetry(self, policy_instance, action):
        with telemetry_helper.begin_hook(civic_policy_telemetry_writer, TELEMETRY_HOOK_CIVIC_POLICY_PROPOSE) as hook:
            hook.write_guid(TELEMETRY_FIELD_NEIGHBORHOOD, self.get_world_description_id())
            hook.write_guid(TELEMETRY_FIELD_POLICY, policy_instance.guid64)
            hook.write_guid(TELEMETRY_FIELD_PROPOSE_ACTION, action)

    def add_to_ballot(self, policy_instance):
        if policy_instance.vote_count_statistic is not None and policy_instance not in self._balloted_policies:
            self._balloted_policies.add(policy_instance)
            self._log_propose_telemetry(policy_instance, TELEMETRY_FIELD_ACTION_VALUE_BALLOT)
            return True
        return False

    def add_for_repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None:
            return False
        if policy not in self._enacted_policies:
            return False
        if policy in self._up_for_repeal_policies:
            return False
        self._up_for_repeal_policies.add(policy)
        self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_REPEAL)
        return True

    def remove_from_repeal(self, policy):
        policy = self.get_policy_instance_for_tuning(policy.guid64)
        if policy is None:
            return False
        if policy not in self._up_for_repeal_policies:
            return False
        self._up_for_repeal_policies.discard(policy)
        self._log_propose_telemetry(policy, TELEMETRY_FIELD_ACTION_VALUE_CANCEL_REPEAL)
        return True

    def save(self, parent_data_msg):
        parent_data_msg.ClearField('policy_data')
        for policy in self._civic_policies:
            policy.save(parent_data_msg)
        parent_data_msg.ClearField('balloted_policy_ids')
        for policy in self._balloted_policies:
            parent_data_msg.balloted_policy_ids.append(policy.guid64)
        parent_data_msg.ClearField('up_for_repeal_policy_ids')
        for policy in self._up_for_repeal_policies:
            parent_data_msg.up_for_repeal_policy_ids.append(policy.guid64)
        parent_data_msg.ClearField('commodity_tracker')
        parent_data_msg.ClearField('statistics_tracker')
        parent_data_msg.ClearField('ranked_statistic_tracker')
        self.update_all_commodities()
        (commodites, _, ranked_statistics) = self.commodity_tracker.save()
        parent_data_msg.commodity_tracker.commodities.extend(commodites)
        regular_statistics = self.statistic_tracker.save()
        parent_data_msg.statistics_tracker.statistics.extend(regular_statistics)
        parent_data_msg.ranked_statistic_tracker.ranked_statistics.extend(ranked_statistics)

    def load(self, parent_data_msg):
        self.commodity_tracker.load(parent_data_msg.commodity_tracker.commodities)
        self.statistic_tracker.load(parent_data_msg.statistics_tracker.statistics)
        self.commodity_tracker.load(parent_data_msg.ranked_statistic_tracker.ranked_statistics)
        self._enacted_policies.clear()
        for policy_data in parent_data_msg.policy_data:
            policy = self.get_policy_instance_for_tuning(policy_data.policy_id)
            if policy:
                policy.load(policy_data)
                if policy.enacted:
                    self._enacted_policies.add(policy)
        for policy_id in parent_data_msg.balloted_policy_ids:
            policy = self.get_policy_instance_for_tuning(policy_id)
            if policy:
                self._balloted_policies.add(policy)
        for policy_id in parent_data_msg.up_for_repeal_policy_ids:
            policy = self.get_policy_instance_for_tuning(policy_id)
            if policy:
                self._up_for_repeal_policies.add(policy)

    def handle_event(self, sim_info, event, resolver):
        if event == TestEvent.CivicPolicyDailyRandomVoting:
            self.do_daily_vote()
        elif event == TestEvent.CivicPolicyOpenVoting:
            self.open_voting()
        elif event == TestEvent.CivicPolicyCloseVoting:
            self.close_voting()

    def get_influence(self, sim_info):
        tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=False)
        if tracker is None:
            return 0
        return tracker.get_bucks_amount_for_type(self.INFLUENCE_BUCK_TYPE)

    def modify_influence(self, sim_info, delta):
        if delta == 0:
            return
        tracker = BucksUtils.get_tracker_for_bucks_type(self.INFLUENCE_BUCK_TYPE, owner_id=sim_info.id, add_if_none=True)
        if tracker is None:
            return
        tracker.try_modify_bucks(self.INFLUENCE_BUCK_TYPE, delta)

    def populate_community_board_op(self, sim_info, op, target_id):
        op.sim_id = sim_info.id
        op.target_id = target_id
        op.influence_points = self.get_influence(sim_info)
        op.title = self.community_board_dialog_title()
        if hasattr(op, 'schedule_text'):
            op.schedule_text = self.get_schedule_text()
        for policy in self._enacted_policies:
            with ProtocolBufferRollback(op.enacted_policies) as enacted_policy:
                enacted_policy.policy_id = policy.guid64
                if policy in self._up_for_repeal_policies:
                    if policy.vote_count_statistic is None:
                        enacted_policy.count = 0
                    else:
                        enacted_policy.count = int(self.get_stat_value(policy.vote_count_statistic))
        for policy in self._balloted_policies:
            with ProtocolBufferRollback(op.balloted_policies) as balloted_policy:
                balloted_policy.policy_id = policy.guid64
                stat = policy.vote_count_statistic
                if stat is None:
                    balloted_policy.count = 0
                else:
                    balloted_policy.count = int(self.get_stat_value(stat))
                    balloted_policy.max_count = stat.max_value
        op.provider_type = self.provider_type_id
        op.new_policy_allowed = self.is_new_policy_allowed(sim_info)
        if not services.street_service().voting_open:
            op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.voting_closed_policy_tooltip_text()
        if not self.is_eligible_voter(sim_info):
            op.disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_confirm_tooltip_text(sim_info)
            op.policy_disabled_tooltip = self.COMMUNITY_BOARD_TEXT.ineligible_voter_policy_tooltip_text(sim_info)

    def _on_add_picker_selected(self, dialog):
        tag_objs = dialog.get_result_tags()
        if not tag_objs:
            return
        num_tags = len(tag_objs)
        can_add_more = dialog.max_selectable.number_selectable - num_tags > 0
        if can_add_more:
            can_add_more = len(dialog.picker_rows) > num_tags
        op = CommunityBoardAddPolicy(tag_objs, dialog.target_sim.sim_id, can_add_more)
        Distributor.instance().add_op_with_no_owner(op)

    def create_add_policy_picker(self, sim_info, used_policy_ids):
        resolver = SingleSimResolver(sim_info)
        dialog = self.COMMUNITY_BOARD_TEXT.add_policy_picker(sim_info, resolver=resolver)
        for policy in self.get_dormant_policies():
            if policy.guid64 not in used_policy_ids:
                tooltip = lambda *_, tooltip=policy.display_description: tooltip(sim_info)
                dialog.add_row(BasePickerRow(name=policy.display_name(sim_info), icon=policy.display_icon, tag=policy.guid64, row_tooltip=tooltip))
        dialog.max_selectable.number_selectable = min(len(dialog.picker_rows), self.max_balloted_policy_count - len(self._balloted_policies) - len(used_policy_ids))
        dialog.set_target_sim(sim_info)
        dialog.add_listener(self._on_add_picker_selected)
        dialog.show_dialog()

    def handle_vote_interaction(self, sim_info, target_id, push_continuation):
        sim = sim_info.get_sim_instance(allow_hidden_flags=ALL_HIDDEN_REASONS)
        if sim is None:
            return
        target_object = services.object_manager().get(target_id)
        if not target_object:
            return
        for current_interaction in sim.si_state:
            interaction_target = current_interaction.target
            if interaction_target is None:
                continue
            if interaction_target.is_part:
                if interaction_target.part_owner is target_object:
                    target_object = interaction_target
                    break
            if interaction_target is target_object:
                break
        else:
            return
        if push_continuation:
            context = current_interaction.context.clone_for_continuation(current_interaction)
            autonomy_request = autonomy.autonomy_request.AutonomyRequest(sim, FullAutonomy, static_commodity_list=self.VOTING_CONTINUATION_AUTONOMY_COMMODITIES, object_list=(target_object,), insert_strategy=QueueInsertStrategy.NEXT, apply_opportunity_cost=False, is_script_request=True, ignore_user_directed_and_autonomous=True, context=context, si_state_view=sim.si_state, limited_autonomy_allowed=True, autonomy_mode_label_override='ParameterizedAutonomy', off_lot_autonomy_rule_override=UNLIMITED_AUTONOMY_RULE)
            autonomy_service = services.autonomy_service()
            results = autonomy_service.score_all_interactions(autonomy_request)
            chosen_interaction = autonomy_service.choose_best_interaction(results, autonomy_request)
            autonomy_request.invalidate_created_interactions(excluded_si=chosen_interaction)
            if chosen_interaction:
                target_affordance = current_interaction.generate_continuation_affordance(chosen_interaction.affordance)
                sim.push_super_affordance(target_affordance, target_object, context)
        current_interaction.cancel(FinishingType.NATURAL, 'Finished viewing board')
Example #10
0
class TooltipComponent(Component,
                       TooltipProvidingComponentMixin,
                       HasTunableFactory,
                       AutoFactoryInit,
                       component_name=types.TOOLTIP_COMPONENT):
    __qualname__ = 'TooltipComponent'
    FACTORY_TUNABLES = {
        'custom_tooltips':
        TunableList(
            description=
            '\n            List of possible tooltips that will be displayed on an object when\n            moused over.\n            Each tooltip has its set of tests which will be evaluated whenever\n            the object its created or when its state changes.  The test that \n            passes its the tooltip that the object will display.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Variation of tooltips that may show when an object is hover \n                over.\n                Which tooltip is shows will depend on the object_tests that are \n                tuned.    \n                ',
                object_tests=TunableObjectModifyTestSet(
                    description=
                    '\n                    All least one subtest group (AKA one list item) must pass\n                    within this list for the tooltip values to be valid on the \n                    object.\n                    '
                ),
                tooltip_style=TunableEnumEntry(
                    description=
                    "\n                    Types of possible tooltips that can be displayed for an\n                    object.  It's recomended to use default or \n                    HOVER_TIP_CUSTOM_OBJECT on most objects. \n                    ",
                    tunable_type=HovertipStyle,
                    default=HovertipStyle.HOVER_TIP_DEFAULT),
                tooltip_fields=TunableMapping(
                    description=
                    '\n                    Mapping of tooltip fields to its localized values.  Since \n                    this fields are created from a system originally created \n                    for recipes, all of them may be tuned, but these are the \n                    most common fields to show on a tooltip:\n                    - recipe_name = This is the actual title of the tooltip.  \n                    This is the main text\n                    - recipe_description = This description refers to the main \n                    text that will show below the title\n                    - header = Smaller text that will show just above the title\n                    - subtext = Smaller text that will show just bellow the \n                    title\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        Fields to be populated in the tooltip.  These fields\n                        will be populated with the text and tokens tuned.\n                        ',
                        tunable_type=TooltipFields,
                        default=TooltipFields.recipe_name),
                    value_type=TooltipText.TunableFactory()))),
        'state_value_numbers':
        TunableList(
            description=
            '\n            Ordered list mapping a state value to a number that will be passed\n            as token to the State Value String.  Will use the number associated\n            with the first state matched.\n            \n            e.g.\n            if the object has all the states and the list looks like:\n            state value masterwork\n            state value poor quality\n            \n            then the number passed to the State Value Strings will be the number\n            associated with the masterwork state.\n            \n            Does *not* have to be the same size or states as the state value\n            strings\n            ',
            tunable=TunableTuple(
                description=
                '\n                Map of state value to an number that will be passed as token to\n                the state value strings   \n                ',
                state_value=TunableReference(
                    description=
                    '\n                    The state value for the associated number\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.OBJECT_STATE),
                    class_restrictions='ObjectStateValue'),
                number=Tunable(
                    description=
                    '\n                    Number passed to localization as the token for the state value\n                    strings below\n                    ',
                    tunable_type=float,
                    default=0))),
        'state_value_strings':
        TunableList(
            description=
            '\n            List of lists of mapping a state value to a localized string.\n            The first string mapped to a valid state in each sub list will be\n            added.\n            \n            e.g.\n            if the object has all the states and the lists look like:\n            List 1:\n                state_value masterwork\n                state_value poor quality\n            list 2:\n                state_value flirty\n                \n            then it will show the strings for masterwork and flirty, but\n            not the string for poor quality.\n            \n            Does *not* have to be the same size or states as the state value \n            numbers.  Additionally, it does *not* have to utilize the number\n            passed in as token from State Value Numbers.  i.e. if something is \n            *always* Comfort: 5, regardless of quality, the string can simply \n            be "Comfort: 5".\n            ',
            tunable=TunableList(
                description=
                '\n                Ordered list mapping a state value to a localized string.\n                The first string mapped to a valid state will be added.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    Map of state value to a string\n                    ',
                    state_value=TunableReference(
                        description=
                        '\n                        The state value for the associated string\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.OBJECT_STATE),
                        class_restrictions='ObjectStateValue'),
                    text=TunableLocalizedStringFactoryVariant(
                        description=
                        '\n                        Text that will be displayed if the object has the\n                        associated state value, with any number matched to a state\n                        in state value numbers passed in as {0.Number}, defaulting to\n                        0 if no state in the state value numbers matches\n                        '
                    ))))
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._ui_metadata_handles = []

    @componentmethod_with_fallback(lambda: None)
    def update_object_tooltip(self):
        if services.client_manager() is None:
            pass
        else:
            old_handles = list(self._ui_metadata_handles)
            try:
                self._ui_metadata_handles = []
                found_subtext = False
                for (name, value) in self._ui_metadata_gen():
                    handle = self.owner.add_ui_metadata(name, value)
                    self._ui_metadata_handles.append(handle)
                    while name == 'subtext':
                        found_subtext = True
                while self._ui_metadata_handles and not found_subtext:
                    subtext = self.get_state_strings()
                    while subtext is not None:
                        handle = self.owner.add_ui_metadata(
                            'subtext', self.get_state_strings())
                        self._ui_metadata_handles.append(handle)
            finally:
                for handle in old_handles:
                    self.owner.remove_ui_metadata(handle)

    @componentmethod_with_fallback(lambda: None)
    def get_state_strings(self):
        obj = self.owner
        int_token = 0
        for state_int_data in self.state_value_numbers:
            state_value = state_int_data.state_value
            while obj.has_state(state_value.state) and obj.get_state(
                    state_value.state) is state_value:
                int_token = state_int_data.number
                break
        bullet_points = []
        for state_string_datas in self.state_value_strings:
            for state_string_data in state_string_datas:
                state_value = state_string_data.state_value
                while obj.has_state(state_value.state) and obj.get_state(
                        state_value.state) is state_value:
                    bullet_point = state_string_data.text(int_token)
                    bullet_points.append(bullet_point)
                    break
        if bullet_points:
            if len(bullet_points) == 1:
                return LocalizationHelperTuning.get_raw_text(bullet_points[0])
            return LocalizationHelperTuning.get_bulleted_list(
                None, *bullet_points)

    def on_add(self):
        self.update_object_tooltip()

    def on_state_changed(self, state, old_value, new_value):
        self.update_object_tooltip()

    def _ui_metadata_gen(self):
        resolver = SingleObjectResolver(self.owner)
        for tooltip_data in self.custom_tooltips:
            object_tests = tooltip_data.object_tests
            if not (object_tests is not None
                    and object_tests.run_tests(resolver)):
                pass
            self.owner.hover_tip = tooltip_data.tooltip_style
            for (tooltip_key,
                 tooltip_text) in tooltip_data.tooltip_fields.items():
                if tooltip_text.text_tokens is not None:
                    tokens = tooltip_text.text_tokens.get_tokens(resolver)
                else:
                    tokens = ()
                yield (TooltipFields(tooltip_key).name,
                       tooltip_text.text(*tokens))
Example #11
0
class UiDialog(HasTunableFactory, AutoFactoryInit):
    __qualname__ = 'UiDialog'
    DIALOG_MSG_TYPE = Consts_pb2.MSG_UI_DIALOG_SHOW
    FACTORY_TUNABLES = {
        'title':
        OptionalTunable(
            description=
            '\n            If enabled, this dialog will include title text.\n            ',
            tunable=TunableLocalizedStringFactory(
                description=
                "\n                The dialog's title.\n                ")),
        'text':
        TunableLocalizedStringFactoryVariant(
            description="\n            The dialog's text.\n            "),
        'text_tokens':
        OptionalTunable(
            description=
            '\n            If enabled, define text tokens to be used to localized text.\n            ',
            tunable=LocalizationTokens.TunableFactory(
                description=
                '\n                Define the text tokens that are available to all text fields in\n                the dialog, such as title, text, responses, default and initial\n                text values, tooltips, etc.\n                '
            ),
            disabled_value=DEFAULT),
        'icon':
        OptionalTunable(
            description=
            '\n            If enabled, specify an icon to be displayed.\n            ',
            tunable=TunableIconVariant(),
            needs_tuning=True),
        'secondary_icon':
        OptionalTunable(
            description=
            '\n            If enabled, specify a secondary icon to be displayed. Only certain\n            dialog types may support this field.\n            ',
            tunable=TunableIconVariant(),
            needs_tuning=True),
        'phone_ring_type':
        TunableEnumEntry(
            description=
            '\n             The phone ring type of this dialog.  If tuned to anything other\n             than None this dialog will only appear after clicking on the phone.\n             ',
            tunable_type=PhoneRingType,
            needs_tuning=True,
            default=PhoneRingType.NO_RING),
        'audio_sting':
        OptionalTunable(
            description=
            '\n            If enabled, play an audio sting when the dialog is shown.\n            ',
            tunable=TunablePlayAudio()),
        'ui_responses':
        TunableList(
            description=
            '\n            A list of buttons that are mapped to UI commands.\n            ',
            tunable=get_defualt_ui_dialog_response()),
        'dialog_options':
        TunableEnumFlags(
            description=
            '\n            Options to apply to the dialog.\n            ',
            enum_type=UiDialogOption,
            allow_no_flags=True,
            default=UiDialogOption.DISABLE_CLOSE_BUTTON)
    }

    def __init__(self, owner, resolver=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._owner = owner.ref()
        self._resolver = resolver
        self._additional_responses = {}
        self.response = None
        self._timestamp = None
        self._listeners = CallableList()

    @property
    def accepted(self) -> bool:
        return self.response is not None and self.response != ButtonType.DIALOG_RESPONSE_CLOSED

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

    @property
    def owner(self):
        return self._owner()

    @property
    def dialog_type(self):
        return self._dialog_type

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

    def set_responses(self, responses):
        self._additional_responses = tuple(responses)

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

    def _get_responses_gen(self):
        yield self.responses
        yield self._additional_responses
        yield self.ui_responses

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

    def update(self) -> bool:
        return True

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

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

    def _build_localized_string_msg(self, string, *additional_tokens):
        if string is None:
            logger.callstack(
                '_build_localized_string_msg received None for the string to build. This is probably not intended.',
                owner='tingyul')
            return
        tokens = ()
        if self._resolver is not None:
            if self.text_tokens is DEFAULT:
                tokens = self._resolver.get_localization_tokens()
            elif self.text_tokens is not None:
                tokens = self.text_tokens.get_tokens(self._resolver)
        return string(*tokens + additional_tokens)

    def _build_response_arg(self,
                            response,
                            response_msg,
                            tutorial_id=None,
                            additional_tokens=(),
                            **kwargs):
        response_msg.choice_id = response.dialog_response_id
        response_msg.ui_request = response.ui_request
        if response.text is not None:
            response_msg.text = self._build_localized_string_msg(
                response.text, *additional_tokens)
        if tutorial_id is not None:
            response_msg.tutorial_args.tutorial_id = tutorial_id

    def build_msg(self,
                  additional_tokens=(),
                  icon_override=DEFAULT,
                  secondary_icon_override=DEFAULT,
                  **kwargs):
        msg = Dialog_pb2.UiDialogMessage()
        msg.dialog_id = self.dialog_id
        msg.owner_id = self.owner.id
        msg.dialog_type = Dialog_pb2.UiDialogMessage.DEFAULT
        if self.title is not None:
            msg.title = self._build_localized_string_msg(
                self.title, *additional_tokens)
        msg.text = self._build_localized_string_msg(self.text,
                                                    *additional_tokens)
        if icon_override is DEFAULT:
            if self.icon is not None:
                icon_info = self.icon(self._resolver)
                key = icon_info[0]
                if key is not None:
                    msg.icon.type = key.type
                    msg.icon.group = key.group
                    msg.icon.instance = key.instance
                build_icon_info_msg(icon_info, None, msg.icon_info)
        elif icon_override is not None:
            build_icon_info_msg(icon_override, None, msg.icon_info)
        if secondary_icon_override is DEFAULT:
            if self.secondary_icon is not None:
                icon_info = self.secondary_icon(self._resolver)
                build_icon_info_msg(icon_info, None, msg.secondary_icon_info)
        elif secondary_icon_override is not None:
            build_icon_info_msg(secondary_icon_override, None,
                                msg.secondary_icon_info)
        msg.dialog_options = self.dialog_options
        responses = []
        responses.extend(self._get_responses_gen())
        responses.sort(key=lambda response: response.sort_order)
        for response in responses:
            response_msg = msg.choices.add()
            self._build_response_arg(response,
                                     response_msg,
                                     additional_tokens=additional_tokens,
                                     **kwargs)
        return msg

    def on_response_received(self):
        pass

    def do_auto_respond(self):
        if ButtonType.DIALOG_RESPONSE_CANCEL in self.responses:
            response = ButtonType.DIALOG_RESPONSE_CANCEL
        elif ButtonType.DIALOG_RESPONSE_OK in self.responses:
            response = ButtonType.DIALOG_RESPONSE_OK
        else:
            response = ButtonType.DIALOG_RESPONSE_CLOSED
        services.ui_dialog_service().dialog_respond(self.dialog_id, response)
Example #12
0
class UiDialog(UiDialogBase, HasTunableFactory, AutoFactoryInit):
	DIALOG_MSG_TYPE = Consts_pb2.MSG_UI_DIALOG_SHOW
	FACTORY_TUNABLES = {
		'title': OptionalTunable(description = '\n            If enabled, this dialog will include title text.\n            ', tunable = TunableLocalizedStringFactory(description = "\n                The dialog's title.\n                ")),
		'text': TunableLocalizedStringFactoryVariant(description = "\n            The dialog's text.\n            "),
		'text_tokens': OptionalTunable(description = '\n            If enabled, define text tokens to be used to localized text.\n            ', tunable = LocalizationTokens.TunableFactory(description = '\n                Define the text tokens that are available to all text fields in\n                the dialog, such as title, text, responses, default and initial\n                text values, tooltips, etc.\n                '), disabled_value = DEFAULT),
		'icon': OptionalTunable(description = '\n            If enabled, specify an icon to be displayed.\n            ', tunable = TunableIconVariant()),
		'secondary_icon': OptionalTunable(description = '\n            If enabled, specify a secondary icon to be displayed. Only certain\n            dialog types may support this field.\n            ', tunable = TunableIconVariant()),
		'phone_ring_type': TunableEnumEntry(description = '\n             The phone ring type of this dialog.  If tuned to anything other\n             than None this dialog will only appear after clicking on the phone.\n             ', tunable_type = PhoneRingType, default = PhoneRingType.NO_RING),
		'anonymous_target_sim': Tunable(description = '\n            If this dialog is using a target sim id to give a conversation type view and this is checked, then the\n            target sim icon will instead be replaced by an anonymous caller.\n            ', tunable_type = bool, default = False),
		'audio_sting': OptionalTunable(description = '\n            If enabled, play an audio sting when the dialog is shown.\n            ', tunable = TunablePlayAudio()),
		'ui_responses': TunableList(description = '\n            A list of buttons that are mapped to UI commands.\n            ', tunable = get_defualt_ui_dialog_response(show_text = True)),
		'dialog_style': TunableEnumEntry(description = '\n            The style layout to apply to this dialog.\n            ', tunable_type = UiDialogStyle, default = UiDialogStyle.DEFAULT),
		'dialog_bg_style': TunableEnumEntry(description = '\n            The style background to apply to this dialog.\n            ', tunable_type = UiDialogBGStyle, default = UiDialogBGStyle.BG_DEFAULT),
		'dialog_options': TunableEnumFlags(description = '\n            Options to apply to the dialog.\n            ', enum_type = UiDialogOption, allow_no_flags = True, default = UiDialogOption.DISABLE_CLOSE_BUTTON),
		'timeout_duration': OptionalTunable(description = '\n            If enabled, override the timeout duration for this dialog in game\n            time.\n            ', tunable = TunableSimMinute(description = '\n                The time, in sim minutes, that this dialog should time out.\n                ', default = 5, minimum = 5)),
		'icon_override_participant': OptionalTunable(description = '\n            If enabled, allows a different participant to be considered the\n            owner of this dialog. Typically, this will only affect the Sim\n            portrait used at the top of the dialog, but there could be other\n            adverse affects so be sure to talk to your UI partner before tuning\n            this.\n            ', tunable = TunableEnumEntry(description = "\n                The participant to be used as the owner of this dialog. If this\n                participant doesn't exist, the default owner will be used\n                instead.\n                ", tunable_type = ParticipantTypeSingleSim, default = ParticipantTypeSingleSim.Invalid, invalid_enums = ParticipantTypeSingleSim.Invalid)),
		'additional_texts': OptionalTunable(description = '\n            If enabled, add additional text to the dialog\n            ', tunable = TunableList(tunable = TunableLocalizedStringFactory()))
	}

	def __init__ (self, owner, resolver = None, target_sim_id = None, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self._owner = owner.ref() if owner is not None else None
		self._resolver = resolver
		self._additional_responses = { }
		self._timestamp = None
		self._target_sim_id = target_sim_id

	@property
	def accepted (self) -> bool:
		return self.response is not None and self.response != ButtonType.DIALOG_RESPONSE_CLOSED

	@property
	def closed (self) -> bool:
		return self.response == ButtonType.DIALOG_RESPONSE_CLOSED

	@property
	def owner (self):
		if self._owner is not None:
			return self._owner()

	@property
	def dialog_type (self):
		return self._dialog_type

	def set_responses (self, responses):
		self._additional_responses = tuple(responses)

	def _get_responses_gen (self):
		yield from self.responses
		yield from self._additional_responses
		yield from self.ui_responses

	def get_phone_ring_type (self):
		return self.phone_ring_type

	def update (self) -> bool:
		return True

	def show_dialog (self, **kwargs):
		if self.audio_sting is not None:
			play_tunable_audio(self.audio_sting, None)
		if self.phone_ring_type == PhoneRingType.ALARM:
			return super().show_dialog(caller_id = self._owner().id, **kwargs)
		return super().show_dialog(caller_id = self._target_sim_id, **kwargs)

	def _build_localized_string_msg (self, string, *additional_tokens):
		if string is None:
			logger.callstack('_build_localized_string_msg received None for the string to build. This is probably not intended.', owner = 'tingyul')
			return
		tokens = ()
		if self._resolver is not None:
			if self.text_tokens is DEFAULT:
				tokens = self._resolver.get_localization_tokens()
			elif self.text_tokens is not None:
				tokens = self.text_tokens.get_tokens(self._resolver)
		return string(*tokens + additional_tokens)

	def _build_response_arg (self, response, response_msg, tutorial_id = None, additional_tokens = (), response_command_tuple = None, **kwargs):
		response_msg.choice_id = response.dialog_response_id
		response_msg.ui_request = response.ui_request
		if response.text is not None:
			response_msg.text = self._build_localized_string_msg(response.text, *additional_tokens)
		if response.subtext is not None:
			response_msg.subtext = response.subtext
		if response.disabled_text is not None:
			response_msg.disabled_text = response.disabled_text
		if tutorial_id is not None:
			response_msg.tutorial_args.tutorial_id = tutorial_id
		if response.response_command:
			response_msg.command_with_args.command_name = response.response_command.command
			for argument in response.response_command.arguments:
				with ProtocolBufferRollback(response_msg.command_with_args.command_remote_args.args) as entry:
					if argument.arg_type == CommandArgType.ARG_TYPE_SPECIAL:
						arg_type = response_command_tuple[0]
						arg_value = response_command_tuple[1]
					elif argument.arg_type == CommandArgType.ARG_TYPE_RESOLVED:
						(arg_type, arg_value) = argument.resolve_response_arg(self._resolver)
					else:
						arg_type = argument.arg_type
						arg_value = argument.arg_value
					if arg_type == CommandArgType.ARG_TYPE_BOOL:
						entry.bool = arg_value
					elif arg_type == CommandArgType.ARG_TYPE_STRING:
						entry.string = arg_value
					elif arg_type == CommandArgType.ARG_TYPE_FLOAT:
						entry.float = arg_value
					elif arg_type == CommandArgType.ARG_TYPE_INT:
						entry.int64 = arg_value

	def build_msg (self, additional_tokens = (), icon_override = DEFAULT, secondary_icon_override = DEFAULT, text_override = DEFAULT, **kwargs):
		msg = Dialog_pb2.UiDialogMessage()
		msg.dialog_id = self.dialog_id
		msg.owner_id = self.owner.id if self.owner is not None else 0
		msg.dialog_type = Dialog_pb2.UiDialogMessage.DEFAULT
		msg.dialog_style = self.dialog_style
		msg.dialog_bg_style = self.dialog_bg_style
		if self._target_sim_id is not None:
			msg.target_id = self._target_sim_id
		if self.title is not None:
			msg.title = self._build_localized_string_msg(self.title, *additional_tokens)
		if text_override is DEFAULT:
			msg.text = self._build_localized_string_msg(self.text, *additional_tokens)
		else:
			msg.text = self._build_localized_string_msg(text_override, *additional_tokens)
		if self.timeout_duration is not None:
			msg.timeout_duration = self.timeout_duration
		if icon_override is DEFAULT:
			if self.icon is not None:
				icon_info = self.icon(self._resolver)
				key = icon_info[0]
				if key is not None:
					msg.icon.type = key.type
					msg.icon.group = key.group
					msg.icon.instance = key.instance
				build_icon_info_msg(icon_info, None, msg.icon_info)
		elif icon_override is not None:
			build_icon_info_msg(icon_override, None, msg.icon_info)
		if secondary_icon_override is DEFAULT:
			if self.secondary_icon is not None:
				icon_info = self.secondary_icon(self._resolver)
				build_icon_info_msg(icon_info, None, msg.secondary_icon_info)
		elif secondary_icon_override is not None:
			build_icon_info_msg(secondary_icon_override, None, msg.secondary_icon_info)
		if self.icon_override_participant is not None:
			msg.override_sim_icon_id = self._resolver.get_participants(self.icon_override_participant)[0].id
		msg.dialog_options = self.dialog_options
		msg.anonymous_target_sim = self.anonymous_target_sim
		responses = []
		responses.extend(self._get_responses_gen())
		responses.sort(key = lambda response: response.sort_order)
		for response in responses:
			response_msg = msg.choices.add()
			self._build_response_arg(response, response_msg, additional_tokens = additional_tokens, **kwargs)
		if self.additional_texts:
			for additional_text in self.additional_texts:
				msg.additional_texts.append(self._build_localized_string_msg(additional_text, *additional_tokens))
		return msg
class AdventureMoment(HasTunableFactory, AutoFactoryInit):
    __qualname__ = 'AdventureMoment'
    LOOT_NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to recursively build loot notification text. It will be\n        given two tokens: a loot display text string, if any, and the previously\n        built LOOT_NOTIFICATION_TEXT string.\n        ')
    NOTIFICATION_TEXT = TunableLocalizedStringFactory(description='\n        A string used to format notifications. It will be given two arguments:\n        the notification text and the built version of LOOT_NOTIFICATION_TEXT,\n        if not empty.\n        ')
    COST_TYPE_SIMOLEONS = 0
    COST_TYPE_ITEMS = 1
    FACTORY_TUNABLES = {'description': '\n            A phase of an adventure. Adventure moments may present\n            some information in a dialog form and for a choice to be\n            made regarding how the overall adventure will branch.\n            ', '_visibility': OptionalTunable(description='\n            Control whether or not this moment provides visual feedback to\n            the player (i.e., a modal dialog).\n            ', tunable=UiDialog.TunableFactory(), disabled_name='not_visible', enabled_name='show_dialog'), '_finish_actions': TunableList(description='\n            A list of choices that can be made by the player to determine\n            branching for the adventure. At most two finish actions can\n            be tuned. They will be displayed as buttons in the UI. If no\n            dialog is displayed, then the first finish action will be selected.\n            If this list is empty, the adventure ends.\n            ', tunable=TunableTuple(display_text=TunableLocalizedStringFactoryVariant(description="\n                   This finish action's title. This will be the button text in\n                   the UI.\n                   "), cost=TunableVariant(description='\n                    The cost associated with this finish action. Only one type\n                    of cost may be tuned. The player is informed of the cost\n                    before making the selection by modifying the display_text\n                    string to include this information.\n                    ', simoleon_cost=TunableTuple(description="The specified\n                        amount will be deducted from the Sim's funds.\n                        ", locked_args={'cost_type': COST_TYPE_SIMOLEONS}, amount=TunableRange(description='How many Simoleons to\n                            deduct.\n                            ', tunable_type=int, default=0, minimum=0)), item_cost=TunableTuple(description="The specified items will \n                        be removed from the Sim's inventory.\n                        ", locked_args={'cost_type': COST_TYPE_ITEMS}, item_cost=ItemCost.TunableFactory()), default=None), action_results=TunableList(description='\n                    A list of possible results that can occur if this finish\n                    action is selected. Action results can award loot, display\n                    notifications, and control the branching of the adventure by\n                    selecting the next adventure moment to run.\n                    ', tunable=TunableTuple(weight_modifiers=TunableList(description='\n                            A list of modifiers that affect the probability that\n                            this action result will be chosen. These are exposed\n                            in the form (test, multiplier). If the test passes,\n                            then the multiplier is applied to the running total.\n                            The default multiplier is 1. To increase the\n                            likelihood of this action result being chosen, tune\n                            multiplier greater than 1. To decrease the\n                            likelihood of this action result being chose, tune\n                            multipliers lower than 1. If you want to exclude\n                            this action result from consideration, tune a\n                            multiplier of 0.\n                            ', tunable=TunableTuple(description='\n                                A pair of test and weight multiplier. If the\n                                test passes, the associated weight multiplier is\n                                applied. If no test is specified, the multiplier\n                                is always applied.\n                                ', test=TunableTestVariant(description='\n                                    The test that has to pass for this weight\n                                    multiplier to be applied. The information\n                                    available to this test is the same\n                                    information available to the interaction\n                                    owning this adventure.\n                                    ', test_locked_args={'tooltip': None}), weight_multiplier=Tunable(description='\n                                    The weight multiplier to apply if the\n                                    associated test passes.\n                                    ', tunable_type=float, default=1))), notification=OptionalTunable(description='\n                            If set, this notification will be displayed.\n                            ', tunable=TunableUiDialogNotificationSnippet()), next_moments=TunableList(description='\n                            A list of adventure moment keys. One of these keys will\n                            be selected to determine which adventure moment is\n                            selected next. If the list is empty, the adventure ends\n                            here. Any of the keys tuned here will have to be tuned\n                            in the _adventure_moments tunable for the owning adventure.\n                            ', tunable=AdventureMomentKey), loot_actions=TunableList(description='\n                            List of Loot actions that are awarded if this action result is selected.\n                            ', tunable=LootActions.TunableReference()), continuation=TunableContinuation(description='\n                            A continuation to push when running finish actions.\n                            ')))), maxlength=2)}

    def __init__(self, parent_adventure, **kwargs):
        super().__init__(**kwargs)
        self._parent_adventure = parent_adventure

    @property
    def _interaction(self):
        return self._parent_adventure.interaction

    @property
    def _sim(self):
        return self._interaction.sim

    def run_adventure(self):
        if self._visibility is None:
            self._run_action_from_index(0)
        else:
            dialog = self._get_dialog()
            dialog.show_dialog()

    def _run_action_from_index(self, action_index):
        action = self._finish_actions[action_index]
        if self._apply_action_cost(action):
            self._run_finish_actions(action)

    def _get_action_result_weight(self, action_result):
        interaction_resolver = self._interaction.get_resolver()
        weight = 1
        for modifier in action_result.weight_modifiers:
            while modifier.test is None or interaction_resolver(modifier.test):
                weight *= modifier.weight_multiplier
        return weight

    def _apply_action_cost(self, action):
        if action.cost is not None:
            if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
                amount = action.cost.amount
                if amount > self._sim.family_funds.money:
                    return False
                self._sim.family_funds.remove(amount, Consts_pb2.TELEMETRY_INTERACTION_COST, sim=self._sim)
            elif action.cost.cost_type == self.COST_TYPE_ITEMS:
                item_cost = action.cost.item_cost
                return item_cost.consume_interaction_cost(self._interaction)()
        return True

    def _run_finish_actions(self, finish_action):
        weight_pairs = [(self._get_action_result_weight(action_result), action_result) for action_result in finish_action.action_results]
        action_result = weighted_random_item(weight_pairs)
        if action_result is not None:
            loot_display_text = None
            resolver = self._interaction.get_resolver()
            for actions in action_result.loot_actions:
                for (loot_op, test_ran) in actions.get_loot_ops_gen(resolver):
                    while loot_op.apply_to_resolver(resolver, skip_test=test_ran):
                        if action_result.notification is not None:
                            current_loot_display_text = loot_op.get_display_text()
                            if current_loot_display_text is not None:
                                if loot_display_text is None:
                                    loot_display_text = current_loot_display_text
                                else:
                                    loot_display_text = self.LOOT_NOTIFICATION_TEXT(loot_display_text, current_loot_display_text)
            if action_result.notification is not None:
                if loot_display_text is not None:
                    notification_text = lambda *tokens: self.NOTIFICATION_TEXT(action_result.notification.text(*tokens), loot_display_text)
                else:
                    notification_text = action_result.notification.text
                dialog = action_result.notification(self._sim, self._interaction.get_resolver())
                dialog.text = notification_text
                dialog.show_dialog()
            if action_result.next_moments:
                next_moment_key = random.choice(action_result.next_moments)
                self._parent_adventure.queue_adventure_moment(next_moment_key)
            if action_result.continuation:
                self._interaction.push_tunable_continuation(action_result.continuation)

    def _on_dialog_response(self, dialog):
        if dialog.response is not None:
            self._run_action_from_index(dialog.response)

    def _get_action_display_text(self, action):
        display_name = self._interaction.create_localized_string(action.display_text)
        if action.cost is not None:
            if action.cost.cost_type == self.COST_TYPE_SIMOLEONS:
                amount = action.cost.amount
                display_name = self._interaction.SIMOLEON_COST_NAME_FACTORY(display_name, amount)
            elif action.cost.cost_type == self.COST_TYPE_ITEMS:
                item_cost = action.cost.item_cost
                display_name = item_cost.get_interaction_name(self._interaction, display_name)
        return lambda *_, **__: display_name

    def _get_dialog(self):
        dialog = self._visibility(self._sim, self._interaction.get_resolver())
        dialog.set_responses(tuple(UiDialogResponse(dialog_response_id=i, text=self._get_action_display_text(action)) for (i, action) in enumerate(self._finish_actions)))
        dialog.add_listener(self._on_dialog_response)
        return dialog
Example #14
0
class TooltipComponent(Component,
                       TooltipProvidingComponentMixin,
                       HasTunableFactory,
                       AutoFactoryInit,
                       component_name=types.TOOLTIP_COMPONENT):
    NON_SELLABLE_BY_PLAYER_TEXT = TooltipText.TunableFactory(
        description=
        '\n        Text shown on tooltip for objects which cannot be sold by\n        the player from inside the inventory.\n        '
    )
    FACTORY_TUNABLES = {
        'custom_tooltips':
        TunableList(
            description=
            '\n            List of possible tooltips that will be displayed on an object when\n            moused over.\n            Each tooltip has its set of tests which will be evaluated whenever\n            the object its created or when its state changes.  The test that \n            passes its the tooltip that the object will display.\n            ',
            tunable=TunableTuple(
                description=
                '\n                Variation of tooltips that may show when an object is hover \n                over.\n                Which tooltip is shows will depend on the object_tests that are \n                tuned.    \n                ',
                object_tests=TunableObjectModifyTestSet(
                    description=
                    '\n                    All least one subtest group (AKA one list item) must pass\n                    within this list for the tooltip values to be valid on the \n                    object.\n                    ',
                    additional_tests={
                        'in_inventory':
                        objects.object_tests.InInventoryTest.TunableFactory(
                            locked_args={'tooltip': None})
                    }),
                tooltip_style=TunableEnumEntry(
                    description=
                    "\n                    Types of possible tooltips that can be displayed for an\n                    object.  It's recomended to use default or \n                    HOVER_TIP_CUSTOM_OBJECT on most objects. \n                    ",
                    tunable_type=HovertipStyle,
                    default=HovertipStyle.HOVER_TIP_DEFAULT),
                tooltip_fields=TunableMapping(
                    description=
                    '\n                    Mapping of tooltip fields to its localized values.  Since \n                    this fields are created from a system originally created \n                    for recipes, all of them may be tuned, but these are the \n                    most common fields to show on a tooltip:\n                    - recipe_name = This is the actual title of the tooltip.  \n                    This is the main text\n                    - recipe_description = This description refers to the main \n                    text that will show below the title\n                    - header = Smaller text that will show just above the title\n                    - subtext = Smaller text that will show just bellow the \n                    title\n                    ',
                    key_type=TunableEnumEntry(
                        description=
                        '\n                        Fields to be populated in the tooltip.  These fields\n                        will be populated with the text and tokens tuned.\n                        ',
                        tunable_type=TooltipFields,
                        default=TooltipFields.recipe_name),
                    value_type=TooltipText.TunableFactory()),
                tooltip_main_icon=OptionalTunable(
                    description=
                    '\n                    Main icon for the tooltip. Not all tooltip styles support\n                    tunable main icons. Consult your GPE and UI partners if \n                    you are unsure if this will work for your use case.\n                    ',
                    tunable=TunableResourceKey(
                        resource_types=CompoundTypes.IMAGE)),
                display_object_preference=OptionalTunable(
                    description=
                    '\n                    If enabled, will display autonomous preference for the\n                    specified tag in the tooltip.\n                    ',
                    tunable=TunableEnumEntry(
                        description=
                        '\n                        The preference tag associated with the information to \n                        display.\n                        ',
                        tunable_type=ObjectPreferenceTag,
                        default=ObjectPreferenceTag.INVALID,
                        invalid_enums=(ObjectPreferenceTag.INVALID, ))))),
        'state_value_numbers':
        TunableList(
            description=
            '\n            Ordered list mapping a state value to a number that will be passed\n            as token to the State Value String.  Will use the number associated\n            with the first state matched.\n            \n            e.g.\n            if the object has all the states and the list looks like:\n            state value masterwork\n            state value poor quality\n            \n            then the number passed to the State Value Strings will be the number\n            associated with the masterwork state.\n            \n            Does *not* have to be the same size or states as the state value\n            strings\n            ',
            tunable=TunableTuple(
                description=
                '\n                Map of state value to an number that will be passed as token to\n                the state value strings   \n                ',
                state_value=TunableReference(
                    description=
                    '\n                    The state value for the associated number\n                    ',
                    manager=services.get_instance_manager(
                        sims4.resources.Types.OBJECT_STATE),
                    class_restrictions='ObjectStateValue',
                    pack_safe=True),
                number=Tunable(
                    description=
                    '\n                    Number passed to localization as the token for the state value\n                    strings below\n                    ',
                    tunable_type=float,
                    default=0))),
        'state_value_strings':
        TunableList(
            description=
            '\n            List of lists of mapping a state value to a localized string.\n            The first string mapped to a valid state in each sub list will be\n            added.\n            \n            e.g.\n            if the object has all the states and the lists look like:\n            List 1:\n                state_value masterwork\n                state_value poor quality\n            list 2:\n                state_value flirty\n                \n            then it will show the strings for masterwork and flirty, but\n            not the string for poor quality.\n            \n            Does *not* have to be the same size or states as the state value \n            numbers.  Additionally, it does *not* have to utilize the number\n            passed in as token from State Value Numbers.  i.e. if something is \n            *always* Comfort: 5, regardless of quality, the string can simply \n            be "Comfort: 5".\n            ',
            tunable=TunableList(
                description=
                '\n                Ordered list mapping a state value to a localized string.\n                The first string mapped to a valid state will be added.\n                ',
                tunable=TunableTuple(
                    description=
                    '\n                    Map of state value to a string\n                    ',
                    state_value=TunableReference(
                        description=
                        '\n                        The state value for the associated string\n                        ',
                        manager=services.get_instance_manager(
                            sims4.resources.Types.OBJECT_STATE),
                        class_restrictions='ObjectStateValue',
                        pack_safe=True),
                    text=TunableLocalizedStringFactoryVariant(
                        description=
                        '\n                        Text that will be displayed if the object has the\n                        associated state value, with any number matched to a state\n                        in state value numbers passed in as {0.Number}, defaulting to\n                        0 if no state in the state value numbers matches\n                        '
                    )))),
        'tooltip_tests':
        TunableObjectModifyTestSet(
            description=
            '\n            At least one subtest group (AKA one list item) must pass within \n            this list for the tooltip values to be shown on the object.\n            ',
            additional_tests={
                'in_inventory':
                objects.object_tests.InInventoryTest.TunableFactory(
                    locked_args={'tooltip': None})
            }),
        'update_if_stat_or_buck_changes':
        Tunable(
            description=
            '\n            If enabled and the tooltip has a statistic based string token, any\n            change to the relevant statistic will cause the tooltip to update.\n            ',
            tunable_type=bool,
            default=False),
        'update_on_game_option_changed':
        Tunable(
            description=
            '\n            If checked, the tooltip will update when a gameplay option is\n            changed.\n            ',
            tunable_type=bool,
            default=False)
    }
    SUBTEXT_HANDLE = 'subtext'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._ui_metadata_handles = {}
        self._external_field_to_data = {}
        self.hovertip_requested = False
        self._callbacks_registered = False
        self._stat_update_listeners = defaultdict(list)
        self._buck_callback_datas = list()

    def handle_event(self, sim_info, event_type, resolver):
        self.update_object_tooltip()

    def register_calbacks(self):
        if self.update_on_game_option_changed:
            services.get_event_manager().register_single_event(
                self, TestEvent.TestedGameOptionChanged)
        if self.update_if_stat_or_buck_changes:
            resolver = SingleObjectResolver(self.owner)
            always_true_threshold = Threshold(-1, operator.ne)
            for custom_tooltip in self.custom_tooltips:
                for tooltip_value in custom_tooltip.tooltip_fields.values():
                    if tooltip_value.text_tokens is not None:
                        for text_token in tooltip_value.text_tokens.tokens:
                            if text_token.token_type == LocalizationTokens.TOKEN_STATISTIC:
                                participant = text_token.participant
                                statistic = text_token.statistic
                                for obj in resolver.get_participants(
                                        participant):
                                    if obj.has_component(
                                            objects.components.types.
                                            STATISTIC_COMPONENT):
                                        statistic_tracker = obj.get_component(
                                            objects.components.types.
                                            STATISTIC_COMPONENT
                                        ).get_statistic_tracker()
                                        statistic_listener = statistic_tracker.create_and_add_listener(
                                            statistic, always_true_threshold,
                                            lambda _: self.
                                            update_object_tooltip())
                                        self._stat_update_listeners[
                                            statistic_tracker].append(
                                                statistic_listener)
                            if text_token.token_type == LocalizationTokens.TOKEN_BUCK:
                                participant = text_token.participant
                                bucks_type = text_token.bucks_type
                                for obj in resolver.get_participants(
                                        participant):
                                    tracker = BucksUtils.get_tracker_for_bucks_type(
                                        bucks_type, owner_id=obj.id)
                                    callback = lambda: self.update_object_tooltip(
                                    )
                                    tracker.add_bucks_modified_callback(
                                        bucks_type, callback)
                                    self._buck_callback_datas.append(
                                        (tracker, bucks_type, callback))

    def on_remove(self):
        if self.update_on_game_option_changed:
            services.get_event_manager().unregister_single_event(
                self, TestEvent.TestedGameOptionChanged)
        if self.update_if_stat_or_buck_changes:
            for (tracker, listeners) in self._stat_update_listeners.items():
                for listener in listeners:
                    tracker.remove_listener(listener)
            self._stat_update_listeners.clear()
            for (tracker, bucks_type, callback) in self._buck_callback_datas:
                tracker.remove_bucks_modified_callback(bucks_type, callback)

    def on_hovertip_requested(self):
        if not self._callbacks_registered:
            self._callbacks_registered = True
            self.register_calbacks()
        if not self.hovertip_requested:
            self.hovertip_requested = True
            self.update_object_tooltip()
            return True
        return False

    @componentmethod_with_fallback(lambda: None)
    def update_object_tooltip(self):
        if not self.hovertip_requested:
            return
        if services.client_manager() is None:
            return
        caches.clear_all_caches()
        tooltip_component = None
        tooltip_override = self.owner.get_tooltip_override()
        if tooltip_override is not None:
            tooltip_component = tooltip_override.get_component(
                types.TOOLTIP_COMPONENT)
        if tooltip_component is None:
            tooltip_component = self
        old_handles = dict(self._ui_metadata_handles)
        try:
            self._ui_metadata_handles = {}
            subtext_field = None
            resolver = SingleObjectResolver(self.owner)
            if self.tooltip_tests.run_tests(resolver):
                for (name, value, tooltip_override_data
                     ) in tooltip_component._ui_metadata_gen():
                    external_field_data_tuple = tooltip_component._external_field_to_data.get(
                        name)
                    if external_field_data_tuple:
                        if tooltip_override_data is not None:
                            if tooltip_override_data.concatenation_type == TooltipFieldConcatenationType.CONCATENATE_BEFORE:
                                value = LocalizationHelperTuning.get_separated_string_by_style(
                                    tooltip_override_data.concatenation_style,
                                    value,
                                    external_field_data_tuple.field_data)
                            else:
                                value = LocalizationHelperTuning.get_separated_string_by_style(
                                    tooltip_override_data.concatenation_style,
                                    external_field_data_tuple.field_data,
                                    value)
                    handle = self.owner.add_ui_metadata(name, value)
                    self._ui_metadata_handles[name] = handle
                    if name == self.SUBTEXT_HANDLE:
                        subtext_field = value
                if tooltip_component._ui_metadata_handles:
                    subtext = tooltip_component.get_state_strings(
                        subtext_field)
                    if subtext is not None:
                        if self.SUBTEXT_HANDLE in self._ui_metadata_handles:
                            self.owner.remove_ui_metadata(
                                self._ui_metadata_handles[self.SUBTEXT_HANDLE])
                        handle = self.owner.add_ui_metadata(
                            self.SUBTEXT_HANDLE, subtext)
                        self._ui_metadata_handles[self.SUBTEXT_HANDLE] = handle
                for index_unused in tooltip_component._external_field_to_data.keys(
                ) - self._ui_metadata_handles.keys():
                    external_field_data = tooltip_component._external_field_to_data.get(
                        index_unused)
                    handle = self.owner.add_ui_metadata(
                        index_unused, external_field_data.field_data)
                    self._ui_metadata_handles[index_unused] = handle
        finally:
            for handle in old_handles.values():
                self.owner.remove_ui_metadata(handle)
            self.owner.update_ui_metadata()
        if not self._ui_metadata_handles:
            self.owner.hover_tip = ui_protocols.UiObjectMetadata.HOVER_TIP_DISABLED
            self.owner.update_ui_metadata()

    @componentmethod_with_fallback(lambda: None)
    def get_state_strings(self, first_string=None):
        obj = self.owner
        int_token = 0
        for state_int_data in self.state_value_numbers:
            state_value = state_int_data.state_value
            if state_value is None:
                continue
            if obj.has_state(state_value.state):
                if obj.get_state(state_value.state) is state_value:
                    int_token = state_int_data.number
                    break
        bullet_points = [] if first_string is None else [first_string]
        for state_string_datas in self.state_value_strings:
            for state_string_data in state_string_datas:
                state_value = state_string_data.state_value
                if state_value is None:
                    continue
                if obj.has_state(state_value.state) and obj.get_state(
                        state_value.state) is state_value:
                    bullet_point = state_string_data.text(int_token)
                    bullet_points.append(bullet_point)
                    break
        if bullet_points:
            if len(bullet_points) == 1:
                return LocalizationHelperTuning.get_raw_text(bullet_points[0])
            else:
                return LocalizationHelperTuning.get_bulleted_list(
                    None, *bullet_points)

    def on_state_changed(self, state, old_value, new_value, from_init):
        self.update_object_tooltip()

    def _get_restriction_icon_info_msg(self,
                                       tracker,
                                       object_id,
                                       icon_infos,
                                       preference_tag,
                                       subroot_index=None,
                                       description=None):
        restricted_sim = tracker.get_restricted_sim(object_id, subroot_index,
                                                    preference_tag)
        if restricted_sim is None:
            return
        sim_info = services.sim_info_manager().get(restricted_sim)
        if sim_info is None:
            return
        icon_info_data = sim_info.get_icon_info_data()
        icon_infos.append(
            create_icon_info_msg(
                icon_info_data,
                name=LocalizationHelperTuning.get_sim_full_name(sim_info),
                desc=description))

    def _ui_metadata_gen(self):
        owner = self.owner
        resolver = SingleObjectResolver(owner)
        for tooltip_data in self.custom_tooltips:
            object_tests = tooltip_data.object_tests
            if not not object_tests and not not object_tests.run_tests(
                    resolver):
                continue
            self.owner.hover_tip = tooltip_data.tooltip_style
            for (tooltip_key,
                 tooltip_text) in tooltip_data.tooltip_fields.items():
                if tooltip_text.text_tokens is not None:
                    tokens = tooltip_text.text_tokens.get_tokens(resolver)
                else:
                    tokens = ()
                if tooltip_key == TooltipFields.rel_override_id:
                    logger.error(
                        'Attempting to set rel_override_id without a required token of type Game Object Property, Object Type Rel Id. Tooltip Field not created on object'
                    )
                    break
                    yield (TooltipFieldsComplete(tooltip_key).name, tokens[0],
                           tooltip_text.override_component_information)
                else:
                    yield (TooltipFieldsComplete(tooltip_key).name,
                           tooltip_text.text(*tokens),
                           tooltip_text.override_component_information)
            if tooltip_data.tooltip_main_icon is not None:
                icon_data = sims4.resources.get_protobuff_for_key(
                    tooltip_data.tooltip_main_icon)
                yield (TooltipFieldsComplete.main_icon.name, icon_data, None)
            if tooltip_data.display_object_preference is not None and not owner.is_sim:
                icon_infos = []
                object_preference_tracker = services.object_preference_tracker(
                )
                if object_preference_tracker is not None:
                    object_id = owner.id
                    for part in owner.parts:
                        if not part.restrict_autonomy_preference:
                            continue
                        self._get_restriction_icon_info_msg(
                            object_preference_tracker,
                            object_id,
                            icon_infos,
                            tooltip_data.display_object_preference,
                            subroot_index=part.subroot_index,
                            description=part.part_name)
                    if not icon_infos:
                        self._get_restriction_icon_info_msg(
                            object_preference_tracker, object_id, icon_infos,
                            tooltip_data.display_object_preference)
                    if icon_infos:
                        yield (TooltipFieldsComplete.icon_infos.name,
                               icon_infos, None)
        if owner.non_deletable_by_user and owner.is_in_inventory():
            non_sellable_text_data = TooltipComponent.NON_SELLABLE_BY_PLAYER_TEXT
            yield (TooltipFieldsComplete.stolen_from_text.name,
                   non_sellable_text_data.text(),
                   non_sellable_text_data.override_component_information)

    @componentmethod_with_fallback(lambda *args, **kwargs: False)
    def update_tooltip_field(self,
                             field_id,
                             field_data,
                             priority=0,
                             should_update=False):
        field_string = TooltipFieldsComplete(field_id).name
        data_priority_tuple = self._external_field_to_data.get(field_string)
        self._external_field_to_data[field_string] = TooltipPriorityData(
            field_data, priority)
        if (data_priority_tuple is None or priority >=
                data_priority_tuple.field_priority) and should_update:
            self.update_object_tooltip()

    @componentmethod_with_fallback(lambda *args, **kwargs: None)
    def get_tooltip_field(self, field, context=None, target=None):
        if field == TooltipFields.relic_description:
            sim = context.sim
            if sim is None:
                return
            return sim.sim_info.relic_tracker.get_description_for_objects(
                self.owner, target)
        name = TooltipFieldsComplete(field).name
        existing_handle = self._ui_metadata_handles.get(name, None)
        if existing_handle is not None:
            (_, _, value) = self.owner.get_ui_metadata(existing_handle)
            return value
        tooltip_component = None
        tooltip_override = self.owner.get_tooltip_override()
        if tooltip_override is not None:
            tooltip_component = tooltip_override.get_component(
                types.TOOLTIP_COMPONENT)
        if tooltip_component is None:
            tooltip_component = self
        tooltip_text = None
        resolver = SingleObjectResolver(self.owner)
        for tooltip_data in tooltip_component.custom_tooltips:
            object_tests = tooltip_data.object_tests
            if object_tests and not object_tests.run_tests(resolver):
                continue
            tooltip_text = tooltip_data.tooltip_fields.get(field, tooltip_text)
        if tooltip_text is not None:
            if tooltip_text.text_tokens is not None:
                tokens = tooltip_text.text_tokens.get_tokens(resolver)
                if None in tokens:
                    return
            else:
                tokens = ()
            text = tooltip_text.text(*tokens)
            external_field_data_tuple = tooltip_component._external_field_to_data.get(
                name)
            if external_field_data_tuple:
                tooltip_override_data = tooltip_text.override_component_information
                if tooltip_override_data is not None:
                    if tooltip_override_data.concatenation_type == TooltipFieldConcatenationType.CONCATENATE_BEFORE:
                        text = LocalizationHelperTuning.get_separated_string_by_style(
                            tooltip_override_data.concatenation_style, text,
                            external_field_data_tuple.field_data)
                    else:
                        text = LocalizationHelperTuning.get_separated_string_by_style(
                            tooltip_override_data.concatenation_style,
                            external_field_data_tuple.field_data, text)
            if name == self.SUBTEXT_HANDLE:
                if tooltip_component._ui_metadata_handles:
                    subtext = tooltip_component.get_state_strings(text)
                    if subtext is not None:
                        text = subtext
            handle = self.owner.add_ui_metadata(name, text)
            self._ui_metadata_handles[name] = handle
            return text

    @componentmethod
    def get_tooltip_override(self, *args, **kwargs):
        pass