def refresh_start_times(self):
     for buff in self._buffs.values():
         time_elapsed = DateAndTime(0)
         for (sim_id, last_time_started) in buff._stored_buff_data[data_const.BuffData.LastTimeBuffStarted].items():
             if last_time_started == DateAndTime(0):
                 pass
             else:
                 time_elapsed += services.time_service().sim_now - last_time_started
                 buff._stored_buff_data[data_const.BuffData.LastTimeBuffStarted][sim_id] = last_time_started = services.time_service().sim_now
         buff._stored_buff_data[data_const.BuffData.TotalBuffTimeElapsed] += TimeSpan(time_elapsed.absolute_ticks())
예제 #2
0
class BusinessManager:
    EVENTS = (TestEvent.SimDeathTypeSet,)

    def __init__(self, business_type):
        self._business_type = business_type
        self._business_type_data = services.business_service().get_business_tuning_data_for_business_type(business_type)
        self._owner_household_id = None
        self._zone_id = None
        self._employee_manager = BusinessEmployeeManager(self)
        self._customer_manager = BusinessCustomerManager(self)
        self._is_open = False
        self._open_time = None
        self._last_off_lot_update = None
        self.on_store_closed = CallableList()
        self._funds_category_tracker = Counter()
        self._funds = None
        self._markup_multiplier = self.tuning_data.default_markup_multiplier
        self._daily_revenue = 0
        self._daily_items_sold = 0
        self._grand_opening = False
        self._star_rating_value = self._business_type_data.default_business_star_rating_value
        self._buff_bucket_totals = defaultdict(float)
        self._buff_bucket_size = 0
        self._summary_dialog_class = None
        self._quality_unlocked = False
        self._off_lot_negative_profit_notification_shown = False

    @property
    def business_type(self):
        return self._business_type

    @property
    def owner_household_id(self):
        return self._owner_household_id

    @property
    def is_owned_by_npc(self):
        return self._owner_household_id is None

    @property
    def business_zone_id(self):
        return self._zone_id

    @property
    def tuning_data(self):
        return self._business_type_data

    @property
    def is_open(self):
        return self._is_open

    @property
    def minutes_open(self):
        timespan = self.get_timespan_since_open()
        if timespan is None:
            return 0
        return timespan.in_minutes()

    @property
    def daily_revenue(self):
        return self._daily_revenue

    @property
    def funds(self):
        return self._funds

    @property
    def funds_transfer_gain_reason(self):
        return Consts_pb2.FUNDS_RETAIL_TRANSFER_GAIN

    @property
    def funds_transfer_loss_reason(self):
        return Consts_pb2.FUNDS_RETAIL_TRANSFER_LOSS

    @property
    def daily_items_sold(self):
        return self._daily_items_sold

    @daily_items_sold.setter
    def daily_items_sold(self, value):
        self._daily_items_sold = value
        self._send_daily_items_sold_update()

    @property
    def is_owner_household_active(self):
        if self._owner_household_id is None:
            return False
        return self._owner_household_id == services.active_household_id()

    @property
    def markup_multiplier(self):
        return self._markup_multiplier

    @property
    def employee_count(self):
        return self._employee_manager.employee_count

    @property
    def quality_setting(self):
        return BusinessQualityType.INVALID

    def set_owner_household_id(self, owner_household_id):
        self._owner_household_id = owner_household_id
        starting_funds = self.tuning_data.npc_starting_funds if self._owner_household_id is None else 0
        self._funds = BusinessFunds(self._owner_household_id, starting_funds, self)
        self._grand_opening = self._owner_household_id is not None

    def set_zone_id(self, business_zone_id):
        self._zone_id = business_zone_id

    def get_star_rating(self):
        star_rating = self._get_star_rating_from_curve()
        star_rating = round(star_rating*2)/2
        return sims4.math.clamp(self.tuning_data.min_and_max_star_rating.lower_bound, star_rating, self.tuning_data.min_and_max_star_rating.upper_bound)

    def is_household_owner(self, household_id):
        if self._owner_household_id is None:
            return False
        return self._owner_household_id == household_id

    def should_automatically_close(self):
        return False

    def _update_off_lot_time_and_get_delta(self):
        now = services.time_service().sim_now
        hours_since_last_sim = (now - self._last_off_lot_update).in_hours()
        self._last_off_lot_update = now
        return hours_since_last_sim

    def run_off_lot_simulation(self):
        if self.should_show_no_way_to_make_money_notification():
            return
        if self._last_off_lot_update is None:
            return
        if not self.tuning_data.off_lot_star_rating_increase_per_hour_curve:
            return
        hours_since_last_sim = self._update_off_lot_time_and_get_delta()
        star_rating = self.get_star_rating()
        value_change = 0
        if sims4.random.random_chance(self.tuning_data.off_lot_chance_of_star_rating_increase):
            value_change = self.tuning_data.off_lot_star_rating_increase_per_hour_curve.get(star_rating)
            if business_handlers.business_archiver.enabled:
                business_handlers.archive_business_event('OffLot', None, 'business with star rating: {} using increase mapping with value change of: {}'.format(star_rating, value_change))
        else:
            value_change = self.tuning_data.off_lot_star_rating_decay_per_hour_curve.get(star_rating)
            perk_tuning = self.tuning_data.off_lot_star_rating_decay_multiplier_perk
            perk_unlocked = False
            if perk_tuning is not None:
                bucks_tracker = services.active_household().bucks_tracker
                if bucks_tracker.is_perk_unlocked(perk_tuning.perk):
                    value_change *= perk_tuning.decay_multiplier
                    perk_unlocked = True
            if business_handlers.business_archiver.enabled:
                if perk_unlocked:
                    event_description = 'business with star rating: {} using decay mapping with value change of: {} after multiplier applied :{}'.format(star_rating, value_change, perk_tuning.decay_multiplier)
                else:
                    event_description = 'business with star rating: {} using decay mapping with value change of: {}'.format(star_rating, value_change)
                business_handlers.archive_business_event('OffLot', None, event_description)
        value_change *= hours_since_last_sim
        self._adjust_star_rating_value(value_change, send_update_message=False)
        self._adjust_profit(hours_since_last_sim)
        self._send_review_update_message()
        self.send_daily_profit_and_cost_update()

    def prepare_for_off_lot_simulation(self):
        self._off_lot_negative_profit_notification_shown = False

    def _adjust_profit(self, hours_since_last_sim):
        final_customer_count = self._get_off_lot_customer_count(hours_since_last_sim)
        self._customer_manager._session_customers_served += final_customer_count
        self._customer_manager._lifetime_customers_served += final_customer_count
        self._customer_manager._send_daily_customers_served_update()
        profit_per_service = self._get_average_profit_per_service()
        profit_per_service *= self.tuning_data.off_lot_profit_per_item_multiplier
        total_profit = profit_per_service*final_customer_count
        self.modify_funds(total_profit, from_item_sold=True)
        if self.tuning_data.off_lot_net_loss_notification is None:
            return
        employee_wages = self._employee_manager.get_total_employee_wages_per_hour()*hours_since_last_sim
        advertising_cost = self._advertising_manager.get_advertising_cost_per_hour()*hours_since_last_sim
        net_profit = total_profit - employee_wages - advertising_cost
        if net_profit < 0:
            if not self._off_lot_negative_profit_notification_shown:
                zone_data = services.get_persistence_service().get_zone_proto_buff(self._zone_id)
                active_sim_info = services.active_sim_info()
                if zone_data is not None:
                    if active_sim_info is not None:
                        notification = self.tuning_data.off_lot_net_loss_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
                        notification.show_dialog(additional_tokens=(zone_data.name,))
                        self._off_lot_negative_profit_notification_shown = True

    def set_open(self, is_open):
        if self._is_open == is_open:
            return
        if is_open:
            self._open_business()
        else:
            self._close_business()

    def start_already_opened_business(self):
        self._employee_manager.open_business()
        self.tuning_data.lighting_helper_open.execute_lighting_helper(self)

    def should_close_after_load(self):
        current_zone = services.current_zone()
        if current_zone.active_household_changed_between_save_and_load() and services.active_household_id() == self._owner_household_id:
            return True
        if current_zone.id == self._zone_id:
            return self.should_automatically_close() and current_zone.time_has_passed_in_world_since_zone_save()
        return self.should_automatically_close()

    def is_employee(self, sim_info):
        return self._employee_manager.is_employee(sim_info)

    def is_valid_employee_type(self, employee_type):
        return employee_type in self.tuning_data.employee_data_map

    def is_valid_employee_skill(self, employee_skill, employee_type):
        employee_type_data = self.tuning_data.employee_data_map.get(employee_type, None)
        if employee_type_data is None:
            return False
        return employee_skill in employee_type_data.employee_skills

    def get_employee_count(self, employee_type):
        return self._employee_manager.get_number_of_employees_by_type(employee_type)

    def get_employees_by_type(self, employee_type):
        return self._employee_manager.get_employees_by_type(employee_type)

    def get_employee_data(self, sim_info):
        return self._employee_manager.get_employee_data(sim_info)

    def remove_employee(self, sim_info):
        self._employee_manager.remove_employee(sim_info)
        self.send_min_employee_req_met_update_message()
        if self.should_show_no_way_to_make_money_notification():
            active_sim_info = services.active_sim_info()
            notification = self.tuning_data.no_way_to_make_money_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
            notification.show_dialog()

    def get_total_employee_wages_per_hour(self):
        return self._employee_manager.get_total_employee_wages_per_hour()

    def get_desired_career_level(self, sim_info, employee_type):
        return self._employee_manager.get_desired_career_level(sim_info, employee_type)

    def get_customer_star_rating(self, sim_id):
        return self._customer_manager.get_customer_star_rating(sim_id)

    def _should_make_customer(self, sim_info):
        return self.is_owner_household_active and not self.is_household_owner(sim_info.household_id)

    def add_customer(self, sim_info):
        if self._should_make_customer(sim_info):
            self._customer_manager.add_customer(sim_info.sim_id)

    def remove_customer(self, sim_info, review_business=True):
        if not self._should_make_customer(sim_info):
            return
        self._customer_manager.remove_customer(sim_info, review_business)

    def set_daily_revenue(self, value):
        self._daily_revenue = value
        if self._owner_household_id is not None:
            self.send_daily_profit_and_cost_update()

    def set_markup_multiplier(self, multiplier):
        valid_multipliers = [entry.markup_multiplier for entry in self._business_type_data.markup_multiplier_data]
        if multiplier in valid_multipliers:
            self._markup_multiplier = multiplier
            self.send_markup_multiplier_message()
        else:
            logger.error('Tried setting the markup multiplier to an invalid multiplier. Invalid multiplier is: {}. Valid multipliers are: {}.', multiplier, valid_multipliers)
        self._distribute_markup_multiplier_update()

    def _distribute_markup_multiplier_update(self):
        markup_msg = Business_pb2.BusinessMarkupUpdate()
        markup_msg.zone_id = self.business_zone_id
        markup_msg.markup_chosen = self.markup_multiplier
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_MARKUP_DATA_UPDATE, markup_msg)
        Distributor.instance().add_op_with_no_owner(op)

    def get_value_with_markup(self, value) -> int:
        markup_multiplier = self.markup_multiplier
        if self.owner_household_id is not None:
            tracker = services.business_service().get_business_tracker_for_household(self.owner_household_id, self.business_type)
            markup_multiplier += tracker.additional_markup_multiplier
        else:
            active_household = services.active_household()
            if active_household is not None:
                markup_multiplier *= active_household.holiday_tracker.get_active_holiday_business_price_multiplier(self.business_type)
        return int(value*markup_multiplier)

    def add_to_funds_category(self, funds_category, amount):
        if not self.is_open:
            return
        self._funds_category_tracker[funds_category] += amount
        self.send_daily_profit_and_cost_update()

    def get_funds_category_entries_gen(self):
        for (funds_category, amount) in self._funds_category_tracker.items():
            funds_category_data = self.tuning_data.funds_category_data.get(funds_category)
            if funds_category_data is not None:
                if funds_category_data.summary_dialog_entry is not None:
                    yield (funds_category_data.summary_dialog_entry, amount)

    def potential_manage_outfit_interactions_gen(self, context, **kwargs):
        for affordance in self.tuning_data.manage_outfit_affordances:
            for aop in affordance.potential_interactions(context.sim, context, **kwargs):
                yield aop

    def modify_funds(self, amount, from_item_sold=True, from_comped_item=False, funds_category=None):
        if amount == 0:
            return
        if amount < 0:
            if from_item_sold:
                logger.warn("Trying to deduct funds from a business but claiming it's from an item sold. Makes no sense so we're bailing.")
                return
            self._funds.try_remove(-amount, Consts_pb2.FUNDS_RETAIL_PROFITS, funds_category=funds_category)
        else:
            if from_comped_item:
                logger.warn("Trying to add funds to a business but claiming it's from a comped meal. Comped meals should deduct money, not add.")
            self._funds.add(amount, Consts_pb2.FUNDS_RETAIL_PROFITS)
        if from_item_sold or from_comped_item:
            self.set_daily_revenue(self.daily_revenue + amount)
        if from_item_sold:
            self.daily_items_sold += 1

    def transfer_balance_to_household(self):
        owner_household = services.household_manager().get(self._owner_household_id)
        if owner_household is None:
            return
        sim = next(owner_household.instanced_sims_gen(allow_hidden_flags=ALL_HIDDEN_REASONS))
        funds = self._funds.money
        if funds > 0:
            owner_household._funds.add(funds, Consts_pb2.FUNDS_RETAIL_PROFITS, sim)
        self._funds.try_remove(funds, Consts_pb2.FUNDS_RETAIL_PROFITS, sim)

    def get_daily_outgoing_costs(self, include_employee_wages=True):
        employee_wages = self._employee_manager._daily_employee_wages if include_employee_wages else 0
        return int(employee_wages + sum(self._funds_category_tracker.values()))

    def get_cost_from_tracker(self, funds_category):
        return self._funds_category_tracker.get(funds_category, 0)

    def get_daily_net_profit(self, **kwargs):
        return int(self.daily_revenue - self.get_daily_outgoing_costs(**kwargs))

    def get_skills_for_employee_type(self, employee_type):
        employee_type_tuning_data = self._get_tuning_data_for_employee_type(employee_type)
        if employee_type_tuning_data is None:
            return
        return employee_type_tuning_data.skills

    def get_uniform_pose_for_employee_type(self, employee_type):
        employee_type_tuning_data = self._get_tuning_data_for_employee_type(employee_type)
        if employee_type_tuning_data is None:
            return
        return employee_type_tuning_data.uniform_pose

    def is_employee_clocked_in(self, sim_info):
        return self._employee_manager.is_employee_clocked_in(sim_info)

    def on_employee_clock_in(self, employee_sim_info):
        self._employee_manager.on_employee_clock_in(employee_sim_info)

    def on_employee_clock_out(self, employee_sim_info, career_level=DEFAULT):
        self._employee_manager.on_employee_clock_out(employee_sim_info, career_level=career_level)

    def update_employees(self):
        self._employee_manager.update_employees()

    def get_employee_uniform_data(self, employee_type, gender, sim_id=0):
        return self._employee_manager.get_employee_uniform_data(employee_type, gender, sim_id)

    def set_quality_setting(self, quality):
        raise NotImplementedError('set_quality is not implemented this means this business is using the base business manager, no functionality is implemented')

    def set_advertising_type(self, advertising_type):
        raise NotImplementedError('set_advertising_type is not implemented this means this business is using the base business manager, no functionality is implemented')

    def get_current_advertising_cost(self):
        raise NotImplementedError('get_current_advertising_cost is not implemented this means this business is using the base business manager, no functionality is implemented')

    def get_advertising_type_for_gsi(self):
        return ''

    def get_total_employee_wages(self):
        if not self.is_open:
            return self._employee_manager.final_daily_wages()
        return self._employee_manager.get_total_employee_wages()

    def get_employee_career_level(self, employee_sim_info):
        return self._employee_manager.get_employee_career_level(employee_sim_info)

    def get_employee_career(self, employee_sim_info):
        return self._employee_manager.get_employee_career(employee_sim_info)

    def add_employee(self, sim_info, employee_type, is_npc_employee=False):
        self._employee_manager.add_employee(sim_info, employee_type, is_npc_employee=is_npc_employee)
        self.send_min_employee_req_met_update_message()

    def get_employees_on_payroll(self):
        return self._employee_manager.get_employees_on_payroll()

    def get_employee_wages(self, employee_sim_info):
        return self._employee_manager.get_employee_wages(employee_sim_info)

    def get_employee_wages_breakdown_gen(self, employee_sim_info):
        return self._employee_manager.get_employee_wages_breakdown_gen(employee_sim_info)

    def run_hire_interaction(self, target_sim, employee_type):
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_hire, target_sim)

    def run_fire_employee_interaction(self, target_sim):
        employee_data = self._employee_manager.get_employee_data(target_sim.sim_info)
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_data.employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_fire, target_sim)

    def run_promote_employee_interaction(self, target_sim):
        employee_data = self._employee_manager.get_employee_data(target_sim.sim_info)
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_data.employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_promote, target_sim)

    def run_demote_employee_interaction(self, target_sim):
        employee_data = self._employee_manager.get_employee_data(target_sim.sim_info)
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_data.employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_demote, target_sim)

    def try_open_npc_store(self):
        if self.owner_household_id is None:
            self._open_pure_npc_store(services.active_lot().get_premade_status() == PremadeLotStatus.IS_PREMADE)
        elif not self.is_owner_household_active:
            self._open_household_owned_npc_store()
        with telemetry_helper.begin_hook(business_telemetry_writer, TELEMETRY_HOOK_NPC_BUSINESS_VISITED, household=services.household_manager().get(self.owner_household_id)) as hook:
            hook.write_enum(TELEMETRY_HOOK_BUSINESS_TYPE, self.business_type)

    def send_data_to_client(self):
        zone = services.get_zone(self._zone_id, allow_uninstantiated_zones=True)
        if zone is None:
            logger.error('Trying to send the business data to client but the business manager {} has an invalid zone id.', self)
            return
        business_data_msg = Business_pb2.SetBusinessData()
        self.construct_business_message(business_data_msg)
        business_data_op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.SET_BUSINESS_DATA, business_data_msg)
        Distributor.instance().add_op_with_no_owner(business_data_op)

    def construct_business_message(self, msg):
        msg.zone_id = self.business_zone_id
        persistence = services.get_persistence_service()
        zone_data = persistence.get_zone_proto_buff(self.business_zone_id)
        if zone_data is not None:
            msg.name = zone_data.name
        msg.is_open = self.is_open
        if self._open_time is not None:
            msg.time_opened = self._open_time.absolute_ticks()
        msg.daily_items_sold = self._daily_items_sold
        msg.daily_outgoing_costs = self.get_daily_outgoing_costs(include_employee_wages=False)
        msg.funds = self.funds.money
        msg.daily_customers_served = self._customer_manager.session_customers_served
        msg.net_profit = self.get_daily_net_profit(include_employee_wages=False)
        msg.markup_chosen = self.markup_multiplier
        msg.daily_revenue = int(self._daily_revenue)
        icon_tuning = self.tuning_data.business_icon
        msg.icon = ResourceKey_pb2.ResourceKey()
        msg.icon.instance = icon_tuning.instance
        msg.icon.group = icon_tuning.group
        msg.icon.type = icon_tuning.type
        msg.review_data = Business_pb2.ReviewDataUpdate()
        self._populate_review_update_message(msg.review_data)
        msg.minimum_employee_requirements_met = self.meets_minimum_employee_requirment()

    def get_interpolated_buff_bucket_value(self, buff_bucket_type, value):
        bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data.get(buff_bucket_type)
        bucket_total = sims4.math.clamp(-1, value, 1)
        if bucket_total >= 0:
            return sims4.math.interpolate(bucket_data.bucket_value_median, bucket_data.bucket_value_maximum, bucket_total)
        return sims4.math.interpolate(bucket_data.bucket_value_median, bucket_data.bucket_value_minimum, abs(bucket_total))

    def process_customer_rating(self, customer_sim_info, customer_star_rating, customer_buff_bucket_totals):
        star_rating_delta = customer_star_rating - self.get_star_rating()
        star_rating_value_change = self.tuning_data.customer_rating_delta_to_business_star_rating_value_change_curve.get(star_rating_delta)
        critic_tuning = self.tuning_data.critic
        if critic_tuning is not None:
            if customer_sim_info.has_trait(critic_tuning.critic_trait):
                star_rating_value_change *= critic_tuning.critic_star_rating_application_count
        for (bucket, value) in customer_buff_bucket_totals.items():
            self._buff_bucket_totals[bucket] += self.get_interpolated_buff_bucket_value(bucket, value)
        self._buff_bucket_size += 1
        self._adjust_star_rating_value(star_rating_value_change)

    def show_summary_dialog(self, is_from_close=False):
        if self._summary_dialog_class is None:
            raise NotImplementedError('No Summary Dialog specified for business {}'.format(self.business_type()))
        if services.active_household() is None:
            return
        self._summary_dialog_class(self).show_business_summary_dialog()

    def send_min_employee_req_met_update_message(self):
        update_message = Business_pb2.MinEmployeeReqMetUpdate()
        update_message.zone_id = self.business_zone_id
        update_message.minimum_employee_requirements_met = self.meets_minimum_employee_requirment()
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_EMPLOYEE_MIN_REQUIREMENT_UPDATE, update_message)
        Distributor.instance().add_op_with_no_owner(op)

    def _open_household_owned_npc_store(self):
        self.set_open(True)

    def _get_star_rating_from_curve(self):
        curve_data = self.tuning_data.star_rating_value_to_user_facing_rating_curve
        if curve_data is None:
            return 0
        return curve_data.get(self._star_rating_value)

    def _get_sorted_delta_buff_buckets_tuple(self):
        if self._buff_bucket_size == 0:
            return
        difference = defaultdict(float)
        for (bucket, value) in self._buff_bucket_totals.items():
            buff_bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[bucket]
            difference[bucket] = buff_bucket_data.bucket_value_maximum - value/self._buff_bucket_size
        return sorted(difference.items(), key=operator.itemgetter(1), reverse=True)

    def _get_top_performing_bucket_tuple(self):
        if self._buff_bucket_size == 0:
            return
        top_value = None
        top_bucket = None
        for (bucket, value) in self._buff_bucket_totals.items():
            value /= self._buff_bucket_size
            if not top_value is None:
                if value > top_value:
                    bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[bucket]
                    if value > bucket_data.bucket_excellence_threshold:
                        top_value = value
                        top_bucket = bucket
            bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[bucket]
            if value > bucket_data.bucket_excellence_threshold:
                top_value = value
                top_bucket = bucket
        return (top_bucket, top_value)

    def set_star_rating_value(self, value):
        self._star_rating_value = 0
        self._adjust_star_rating_value(value)

    def _adjust_star_rating_value(self, delta, send_update_message=True):
        previous_whole_star_rating = int(self.get_star_rating())
        self._star_rating_value = sims4.math.clamp(self.tuning_data.min_and_max_star_rating_value.lower_bound, self._star_rating_value + delta, self.tuning_data.min_and_max_star_rating_value.upper_bound)
        current_whole_star_rating = int(self.get_star_rating())
        if current_whole_star_rating > previous_whole_star_rating and current_whole_star_rating in self.tuning_data.star_rating_to_screen_slam_map:
            screen_slam = self.tuning_data.star_rating_to_screen_slam_map.get(current_whole_star_rating)
            screen_slam.send_screen_slam_message(services.active_sim_info())
        if send_update_message:
            self._send_review_update_message()

    def start_off_lot_simulation_time(self):
        self._last_off_lot_update = services.time_service().sim_now

    def should_show_no_way_to_make_money_notification(self):
        return False

    def meets_minimum_employee_requirment(self):
        return True

    def meets_zone_requirement(self):
        zone_director = services.venue_service().get_zone_director()
        return zone_director is not None and zone_director.supports_business_type(self._business_type)

    def meets_requirements_to_be_open(self):
        return self.meets_zone_requirement() and (self.meets_minimum_employee_requirment() and not self.should_show_no_way_to_make_money_notification())

    def _show_appropriate_open_business_notification(self):
        if self.is_owned_by_npc or not self.is_owner_household_active:
            return
        active_sim_info = services.active_sim_info()
        if self.should_show_no_way_to_make_money_notification():
            notification = self.tuning_data.no_way_to_make_money_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
        else:
            notification = self.tuning_data.open_business_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
        notification.show_dialog()

    def _open_business(self):
        self._show_appropriate_open_business_notification()
        self._clear_state()
        self._is_open = True
        self._open_time = services.time_service().sim_now
        self._employee_manager.open_business()
        if self._owner_household_id is not None:
            owner_household = services.household_manager().get(self.owner_household_id)
            owner_household.bucks_tracker.activate_stored_temporary_perk_timers_of_type(self.tuning_data.bucks)
        self._distribute_business_open_status(is_open=True, open_time=self._open_time.absolute_ticks())
        if self.business_zone_id == services.current_zone_id():
            self.tuning_data.lighting_helper_open.execute_lighting_helper(self)
            zone_director = services.venue_service().get_zone_director()
            zone_director.set_customers_allowed(True)
            zone_director.refresh_open_street_director_status()
        else:
            self.start_off_lot_simulation_time()
        sound = PlaySound(services.get_active_sim(), self.tuning_data.audio_sting_open.instance)
        sound.start()
        self.send_daily_profit_and_cost_update()
        self._send_daily_items_sold_update()
        self._send_review_update_message()

    def _close_business(self, play_sound=True):
        if not self._is_open:
            return
        if play_sound:
            sound = PlaySound(services.get_active_sim(), self.tuning_data.audio_sting_close.instance)
            sound.start()
        self._employee_manager.close_business()
        self.send_daily_profit_and_cost_update()
        self._send_business_closed_telemetry()
        if self._owner_household_id is not None:
            owner_household = services.household_manager().get(self._owner_household_id)
            owner_household.bucks_tracker.deactivate_all_temporary_perk_timers_of_type(self.tuning_data.bucks)
            self.modify_funds(-self._employee_manager.final_daily_wages(), from_item_sold=False)
        self.on_store_closed()
        services.get_event_manager().process_event(TestEvent.BusinessClosed)
        self._distribute_business_open_status(False)
        if self.business_zone_id == services.current_zone_id():
            self.tuning_data.lighting_helper_close.execute_lighting_helper(self)
            zone_director = services.venue_service().get_zone_director()
            if zone_director is not None:
                zone_director.refresh_open_street_director_status()
        else:
            self.run_off_lot_simulation()
            self._last_off_lot_update = None
        self._is_open = False
        self.show_summary_dialog(is_from_close=True)
        self._open_time = None

    def _clear_state(self):
        self._open_time = None
        self._daily_revenue = 0
        self._daily_items_sold = 0
        self._funds_category_tracker.clear()
        self._buff_bucket_totals.clear()
        self._buff_bucket_size = 0
        self._employee_manager._clear_state()
        self._customer_manager._clear_state()
        self._off_lot_negative_profit_notification_shown = False

    def get_timespan_since_open(self, is_from_close=False):
        if not (self.is_open or is_from_close) or self._open_time is None:
            return
        return services.game_clock_service().now() - self._open_time

    def _send_daily_items_sold_update(self):
        if self.is_active_household_and_zone():
            items_sold_msg = Business_pb2.BusinessDailyItemsSoldUpdate()
            items_sold_msg.zone_id = self.business_zone_id
            items_sold_msg.daily_items_sold = self.daily_items_sold
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_DAILY_ITEMS_SOLD_UPDATE, items_sold_msg)
            Distributor.instance().add_op_with_no_owner(op)

    def is_active_household_and_zone(self):
        return self.is_owner_household_active and (self._zone_id is not None and self._zone_id == services.current_zone_id())

    def _get_tuning_data_for_employee_type(self, employee_type):
        return self.tuning_data.employee_data_map.get(employee_type, None)

    def _send_business_closed_telemetry(self):
        with telemetry_helper.begin_hook(business_telemetry_writer, TELEMETRY_HOOK_BUSINESS_CLOSED, household=services.household_manager().get(self.owner_household_id)) as hook:
            hook.write_int(TELEMETRY_HOOK_LENGTH_BUSINESS_OPENED, self.minutes_open)
            hook.write_int(TELEMETRY_HOOK_NUM_WORKERS, self.employee_count)
            hook.write_int(TELEMETRY_HOOK_AMOUNT_PROFIT, self.get_daily_net_profit())
            hook.write_float(TELEMETRY_HOOK_STAR_RATING, self.get_star_rating())
            hook.write_enum(TELEMETRY_HOOK_BUSINESS_TYPE, self.business_type)

    def send_business_funds_update(self):
        if self.is_owner_household_active:
            funds_msg = Business_pb2.BusinessFundsUpdate()
            funds_msg.available_funds = self.funds.money
            funds_msg.zone_id = self.business_zone_id
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_FUNDS_UPDATE, funds_msg)
            Distributor.instance().add_op_with_no_owner(op)

    def send_daily_profit_and_cost_update(self):
        if self.is_owner_household_active:
            profit_msg = Business_pb2.BusinessProfitUpdate()
            profit_msg.zone_id = self.business_zone_id
            profit_msg.net_profit = self.get_daily_net_profit(include_employee_wages=False)
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_PROFIT_UPDATE, profit_msg)
            Distributor.instance().add_op_with_no_owner(op)
            cost_msg = Business_pb2.BusinessDailyCostsUpdate()
            cost_msg.zone_id = self.business_zone_id
            cost_msg.daily_outgoing_costs = int(self.get_daily_outgoing_costs())
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_DAILY_OUTGOING_COSTS_UPDATE, cost_msg)
            Distributor.instance().add_op_with_no_owner(op)

    def send_markup_multiplier_message(self):
        if not self.is_active_household_and_zone():
            return
        markup_msg = UI_pb2.RetailMarkupMultiplierMessage()
        for markup_multiplier in self.tuning_data.markup_multiplier_data:
            with ProtocolBufferRollback(markup_msg.markup_multipliers) as multiplier_entry:
                multiplier_entry.name = markup_multiplier.name
                multiplier_entry.multiplier = markup_multiplier.markup_multiplier
                multiplier_entry.is_selected = markup_multiplier.markup_multiplier == self._markup_multiplier
        op = distributor.shared_messages.create_message_op(markup_msg, Consts_pb2.MSG_RETAIL_MARKUP_MULTIPLIER)
        Distributor.instance().add_op_with_no_owner(op)

    def _distribute_business_open_status(self, is_open=True, open_time=0):
        open_msg = Business_pb2.BusinessIsOpenUpdate()
        open_msg.is_open = is_open
        open_msg.time_opened = open_time
        open_msg.zone_id = self.business_zone_id
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_OPEN_UPDATE, open_msg)
        Distributor.instance().add_op_with_no_owner(op)

    def _send_review_update_message(self):
        review_msg = Business_pb2.ReviewDataUpdate()
        self._populate_review_update_message(review_msg)
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.REVIEW_DATA_UPDATE, review_msg)
        Distributor.instance().add_op_with_no_owner(op)

    def _populate_review_update_message(self, review_msg):
        review_msg.zone_id = self.business_zone_id
        review_msg.score = self.get_star_rating()
        review_msg.review_count = self._customer_manager.lifetime_customers_served
        sorted_buckets = self._get_sorted_delta_buff_buckets_tuple()
        if sorted_buckets is None or len(sorted_buckets) == 0:
            return
        bottom_growth_msg = review_msg.icons.add()
        top_growth_msg = review_msg.icons.add()
        excellence_msg = review_msg.icons.add()
        (top_performer_bucket, top_performer_value) = self._get_top_performing_bucket_tuple()
        if top_performer_bucket is not None:
            top_bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[top_performer_bucket]
            self._fill_review_data(True, top_bucket_data, excellence_msg, top_bucket_data.bucket_value_maximum - top_performer_value)
        top_growth_filled = False
        for (buff_bucket, delta) in sorted_buckets:
            buff_bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[buff_bucket]
            if delta < buff_bucket_data.bucket_growth_opportunity_threshold:
                continue
            if not top_growth_filled:
                self._fill_review_data(False, buff_bucket_data, top_growth_msg, delta)
                top_growth_filled = True
            else:
                self._fill_review_data(False, buff_bucket_data, bottom_growth_msg, delta)
                break

    def _fill_review_data(self, is_positive, buff_bucket_data, icon_msg, delta):
        icon_msg.desc = buff_bucket_data.bucket_excellence_text() if is_positive else buff_bucket_data.bucket_growth_opportunity_text()
        icon_msg.icon = ResourceKey_pb2.ResourceKey()
        icon_msg.icon.instance = buff_bucket_data.bucket_icon.instance
        icon_msg.icon.group = buff_bucket_data.bucket_icon.group
        icon_msg.icon.type = buff_bucket_data.bucket_icon.type
        icon_msg.title = buff_bucket_data.bucket_title

    def populate_employee_msg(self, sim_info, employee_msg, business_employee_type, business_employee_data):
        employee_msg.sim_id = sim_info.sim_id
        employee_data = self._employee_manager.get_employee_data(sim_info)
        employee_is_training = sim_info.has_buff_with_tag(self.tuning_data.employee_training_buff_tag)
        for (skill_type, skill_type_data) in business_employee_data.employee_skills.items():
            with ProtocolBufferRollback(employee_msg.skill_data) as employee_skill_msg:
                employee_skill_msg.skill_id = skill_type.guid64
                employee_skill_msg.curr_points = int(sim_info.get_stat_value(skill_type))
                employee_skill_msg.is_training = employee_is_training
                employee_skill_msg.has_skilled_up = employee_data.has_leveled_up_skill(skill_type) if employee_data is not None else False
                employee_skill_msg.skill_tooltip = skill_type_data.business_summary_description
        if self.is_employee(sim_info):
            satisfaction_stat = sim_info.get_statistic(business_employee_data.satisfaction_commodity)
            statisfaction_state_index = satisfaction_stat.get_state_index()
            if statisfaction_state_index is not None:
                employee_msg.satisfaction_string = satisfaction_stat.states[statisfaction_state_index].buff.buff_type.buff_name(sim_info)
            career_level = self.get_employee_career_level(sim_info)
            employee_msg.pay = career_level.simoleons_per_hour
            career = self.get_employee_career(sim_info)
            employee_msg.current_career_level = career.level
            employee_msg.max_career_level = len(career.current_track_tuning.career_levels) - 1
        else:
            desired_level = self.get_desired_career_level(sim_info, business_employee_type)
            career_level = business_employee_data.career.start_track.career_levels[desired_level]
            employee_msg.pay = career_level.simoleons_per_hour
            employee_msg.current_career_level = desired_level
            employee_msg.max_career_level = len(business_employee_data.career.start_track.career_levels) - 1

    def on_protocols_loaded(self):
        pass

    def on_build_buy_enter(self):
        self.send_business_funds_update()

    def on_build_buy_exit(self):
        if self._owner_household_id is not None:
            owner_household = services.household_manager().get(self._owner_household_id)
            owner_household.funds.send_money_update(vfx_amount=0)
        if not self.meets_requirements_to_be_open():
            self.set_open(False)

    def on_zone_load(self):
        services.get_event_manager().register(self, self.EVENTS)
        self._employee_manager.on_zone_load()
        self._customer_manager.on_zone_load()
        if self.should_close_after_load():
            self._close_business(play_sound=False)
        self._funds._send_money_update_internal(self._owner_household_id, 0)
        self.send_daily_profit_and_cost_update()
        self._send_daily_items_sold_update()

    def on_client_disconnect(self):
        self._employee_manager.on_client_disconnect()
        event_manager = services.get_event_manager()
        if event_manager is not None:
            event_manager.unregister(self, self.EVENTS)

    def on_registered_perk_callback(self):
        if self.tuning_data.quality_unlock_perk is None:
            return
        if self.is_owner_household_active:
            active_household = services.active_household()
            bucks_tracker = active_household.bucks_tracker
            quality_unlocked = bucks_tracker.is_perk_unlocked(self.tuning_data.quality_unlock_perk)
            if self._quality_unlocked != quality_unlocked:
                self._quality_unlocked = quality_unlocked
                self.send_data_to_client()
            if not self._quality_unlocked:
                active_household.bucks_tracker.add_perk_unlocked_callback(self.tuning_data.bucks, self._business_perk_unlocked_callback)
                current_zone = services.current_zone()
                if current_zone is not None:
                    current_zone.register_callback(ZoneState.SHUTDOWN_STARTED, self._unregister_perk_unlock_callback)

    def _unregister_perk_unlock_callback(self):
        if not self._quality_unlocked:
            owning_household = services.household_manager().get(self.owner_household_id)
            if owning_household is None:
                return
            owning_household.bucks_tracker.remove_perk_unlocked_callback(self.tuning_data.bucks, self._business_perk_unlocked_callback)

    def _business_perk_unlocked_callback(self, perk):
        if perk == self.tuning_data.quality_unlock_perk:
            self._unregister_perk_unlock_callback()
            self._quality_unlocked = True
            self._distribute_business_manager_data_message()
            current_zone = services.current_zone()
            if current_zone is not None:
                current_zone.unregister_callback(ZoneState.SHUTDOWN_STARTED, self._unregister_perk_unlock_callback)

    def _distribute_business_manager_data_message(self):
        pass

    def handle_event(self, sim_info, event, resolver):
        if event == TestEvent.SimDeathTypeSet and self.is_employee(sim_info):
            self.remove_employee(sim_info)

    def save_data(self, business_save_data):
        business_save_data.is_open = self._is_open
        business_save_data.funds = self._funds.money
        business_save_data.markup = self._markup_multiplier
        business_save_data.grand_opening = self._grand_opening
        business_save_data.daily_revenue = int(self._daily_revenue)
        business_save_data.daily_items_sold = self._daily_items_sold
        business_save_data.star_rating_value = self._star_rating_value
        self.start_off_lot_simulation_time()
        business_save_data.last_off_lot_update = self._last_off_lot_update
        if self._open_time is not None:
            business_save_data.open_time = self._open_time.absolute_ticks()
        for (funds_category, amount) in self._funds_category_tracker.items():
            with ProtocolBufferRollback(business_save_data.funds_category_tracker_data) as funds_category_msg:
                funds_category_msg.funds_category = funds_category
                funds_category_msg.amount = int(amount)
        for (buff_bucket, buff_bucket_total) in self._buff_bucket_totals.items():
            with ProtocolBufferRollback(business_save_data.buff_bucket_totals) as buff_bucket_total_msg:
                buff_bucket_total_msg.buff_bucket = buff_bucket
                buff_bucket_total_msg.buff_bucket_total = buff_bucket_total
        business_save_data.buff_bucket_size = self._buff_bucket_size
        self._employee_manager.save_data(business_save_data.employee_payroll)
        self._customer_manager.save_data(business_save_data)

    def load_data(self, business_save_data, is_legacy=False):
        self._is_open = business_save_data.is_open
        self._markup_multiplier = business_save_data.markup
        self._grand_opening = business_save_data.grand_opening
        self._daily_revenue = business_save_data.daily_revenue
        self._daily_items_sold = business_save_data.daily_items_sold
        if self._is_open:
            self._open_time = DateAndTime(business_save_data.open_time)
            self._distribute_business_open_status(is_open=self._is_open, open_time=self._open_time.absolute_ticks())
        else:
            self._distribute_business_open_status(self._is_open)
        for funds_category_msg in business_save_data.funds_category_tracker_data:
            self._funds_category_tracker[funds_category_msg.funds_category] = funds_category_msg.amount
        self._funds = BusinessFunds(self._owner_household_id, business_save_data.funds, self)
        if is_legacy:
            self._employee_manager.load_legacy_data(business_save_data)
        else:
            self._buff_bucket_totals.clear()
            for buff_bucket_total_msg in business_save_data.buff_bucket_totals:
                self._buff_bucket_totals[buff_bucket_total_msg.buff_bucket] = buff_bucket_total_msg.buff_bucket_total
            self._last_off_lot_update = DateAndTime(business_save_data.last_off_lot_update)
            self._star_rating_value = business_save_data.star_rating_value
            self._buff_bucket_size = business_save_data.buff_bucket_size
            self._customer_manager.load_data(business_save_data)
            self._employee_manager.load_data(business_save_data.employee_payroll)

    def on_loading_screen_animation_finished(self):
        self._customer_manager.on_loading_screen_animation_finished()
        if self._grand_opening:
            self._grand_opening = False
            FundsTransferDialog.show_dialog(first_time_buyer=True)
            if self.tuning_data.grand_opening_notification is not None:
                active_sim_info = services.active_sim_info()
                notification = self.tuning_data.grand_opening_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
                notification.show_dialog()
        elif self.should_close_after_load():
            self._close_business(play_sound=False)
예제 #3
0
class TimedAspirationData:
    def __init__(self, tracker, aspiration):
        self._tracker = tracker
        self._aspiration = aspiration
        self._end_time = None
        self._end_alarm_handle = None
        self._warning_alarm_handle = None

    @property
    def end_time(self):
        return self._end_time

    def set_tracker(self, tracker):
        self._tracker = tracker

    def clear(self, **kwargs):
        self.send_timed_aspiration_to_client(
            Sims_pb2.TimedAspirationUpdate.REMOVE)
        self._tracker.deactivate_aspiration(self._aspiration)
        if self._end_alarm_handle is not None:
            self._end_alarm_handle.cancel()
            self._end_alarm_handle = None
        if self._warning_alarm_handle is not None:
            self._warning_alarm_handle.cancel()
            self._warning_alarm_handle = None
        self._end_time = None

    def save(self, msg):
        msg.aspiration = self._aspiration.guid64
        msg.end_time = self._end_time.absolute_ticks()

    def load(self, msg):
        now = services.time_service().sim_now
        self._end_time = DateAndTime(msg.end_time)
        time_till_end = self._end_time - now
        if time_till_end <= TimeSpan.ZERO:
            return False
        self._tracker.activate_aspiration(self._aspiration, from_load=True)
        self.send_timed_aspiration_to_client(
            Sims_pb2.TimedAspirationUpdate.ADD)
        self._end_alarm_handle = alarms.add_alarm(self,
                                                  time_till_end,
                                                  self._aspiration_timed_out,
                                                  cross_zone=True)
        time_till_warning = time_till_end - create_time_span(days=1)
        if time_till_warning > TimeSpan.ZERO:
            self._warning_alarm_handle = alarms.add_alarm(
                self,
                time_till_warning,
                self._give_aspiration_warning,
                cross_zone=True)
        return True

    def schedule(self):
        now = services.time_service().sim_now
        duration = self._aspiration.duration()
        if isinstance(duration, WeeklySchedule):
            (duration, _) = duration.time_until_next_scheduled_event(
                now, schedule_immediate=False)
        warning_time = create_time_span(days=1)
        if duration > warning_time:
            warning_duration = duration - warning_time
            self._warning_alarm_handle = alarms.add_alarm(
                self,
                warning_duration,
                self._give_aspiration_warning,
                cross_zone=True)
        self._end_time = now + duration
        self._end_alarm_handle = alarms.add_alarm(self,
                                                  duration,
                                                  self._aspiration_timed_out,
                                                  cross_zone=True)
        self._tracker.activate_aspiration(self._aspiration)
        self.send_timed_aspiration_to_client(
            Sims_pb2.TimedAspirationUpdate.ADD)

    def complete(self, **kwargs):
        self._aspiration.apply_on_complete_loot_actions(
            self._tracker.owner_sim_info)
        self._tracker.deactivate_timed_aspiration(self._aspiration, **kwargs)

    def _aspiration_timed_out(self, _):
        self._aspiration.apply_on_failure_loot_actions(
            self._tracker.owner_sim_info)
        self._tracker.deactivate_timed_aspiration(self._aspiration)

    def _give_aspiration_warning(self, _):
        if self._aspiration.warning_buff:
            self._tracker.owner_sim_info.add_buff(
                self._aspiration.warning_buff.buff_type,
                buff_reason=self._aspiration.warning_buff.buff_reason)
        self._warning_alarm_handle.cancel()
        self._warning_alarm_handle = None

    def send_timed_aspiration_to_client(self, update_type):
        if services.current_zone().is_zone_shutting_down:
            return
        owner = self._tracker.owner_sim_info
        msg = Sims_pb2.TimedAspirationUpdate()
        msg.update_type = update_type
        msg.sim_id = owner.id
        msg.timed_aspiration_id = self._aspiration.guid64
        if update_type == Sims_pb2.TimedAspirationUpdate.ADD:
            msg.timed_aspiration_end_time = self._end_time.absolute_ticks()
        distributor = Distributor.instance()
        distributor.add_op(
            owner,
            GenericProtocolBufferOp(Operation.TIMED_ASPIRATIONS_UPDATE, msg))