diff --git a/.gitignore b/.gitignore
index 68b295e..11d2587 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@ EA/*
!EA/README.md
# Libraries
-Libraries/*/
+Libraries/S4CL/*
# Additional non mod related items
Scripts/rel
@@ -55,3 +55,4 @@ settings.py
Pipfile
.DS_Store
# Project exclude paths
+/Libraries/core-library/lot51_core/*
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..4d5baae
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "Libraries/core-library"]
+ path = Libraries/core-library/lot51_core
+ url = https://github.com/lot51/core-library.git
+[submodule "Package/src"]
+ path = Package/src
+ url = https://github.com/Simsipelago/Sims4ArchipelagoPackage.git
+[submodule "Libraries/lot51_core"]
+ path = Libraries/lot51_core
+ url = https://github.com/lot51/core-library.git
diff --git a/Libraries/lot51_core b/Libraries/lot51_core
new file mode 160000
index 0000000..b367e6e
--- /dev/null
+++ b/Libraries/lot51_core
@@ -0,0 +1 @@
+Subproject commit b367e6ec577b5c942f1686fa55f81b5a1123ecbf
diff --git a/Package/Cactus_S4AP.package b/Package/Cactus_S4AP.package
index 83d9da1..c0f0bcd 100644
Binary files a/Package/Cactus_S4AP.package and b/Package/Cactus_S4AP.package differ
diff --git a/Package/src b/Package/src
new file mode 160000
index 0000000..db64602
--- /dev/null
+++ b/Package/src
@@ -0,0 +1 @@
+Subproject commit db646022b858da91857043b837691951799ff7e5
diff --git a/Scripts/s4ap/enums/S4APLocalization.py b/Scripts/s4ap/enums/S4APLocalization.py
index ba2aef6..5a45870 100644
--- a/Scripts/s4ap/enums/S4APLocalization.py
+++ b/Scripts/s4ap/enums/S4APLocalization.py
@@ -1,9 +1,6 @@
-from sims4communitylib.enums.icons_enum import CommonIconId
-from sims4communitylib.enums.strings_enum import CommonStringId
-from sims4communitylib.enums.traits_enum import CommonTraitId
+from enum import Int
-
-class S4APTraitId(CommonTraitId):
+class S4APTraitId(Int):
LOCK_MIXOLOGY_SKILL: 'S4APTraitId' = 3494693146
LOCK_MISCHIEF_SKILL: 'S4APTraitId' = 3494693147
LOCK_HANDINESS_SKILL: 'S4APTraitId' = 3494693148
@@ -31,7 +28,7 @@ class S4APTraitId(CommonTraitId):
SHOW_RECEIVED_SKILLS: 'S4APTraitId' = 3511470836
RESYNC_LOCATIONS: 'S4APTraitId' = 2224316686
SHOW_YAML_OPTIONS: 'S4APTraitId' = 2241086516
-class S4APStringId(CommonStringId):
+class S4APStringId(Int):
# ap_client
CONNECTION_REFUSED: 'S4APStringId' = 0x964EABE6
CONNECTION_ERROR: 'S4APStringId' = 0xC50BC4AF
@@ -55,7 +52,7 @@ class S4APStringId(CommonStringId):
CONFLICTING_CONNECTION_DATA_DESC: 'S4APStringId' = 0x3E4E79D3
-class S4APIconId(CommonIconId):
+class S4APIconId(Int):
AP_LOGO_BLUE: 'S4APIconId' = 0xBD85B76B1017163F
class S4APBaseGameSkills:
@@ -242,7 +239,7 @@ def __init__(self):
2252361604: 'Competent Wordsmith (Bestselling Author 2)',
1903266201: 'Novelest Novelist (Bestselling Author 3)',
2893015914: 'Bestselling Author (Bestselling Author 4)',
- 669436583: 'Tone Deaf (Musical Genius 1)',
+ 669436583: 'Tone-Deaf (Musical Genius 1)',
527786058: 'Fine Tuned (Musical Genius 2)',
3847573573: 'Harmonious (Musical Genius 3)',
3320148467: 'Musical Genius (Musical Genius 4)',
@@ -276,11 +273,11 @@ def __init__(self):
3410479215: 'The Great Landscaper (Mansion Baron 2)',
1430851502: 'Home Renovator (Mansion Baron 3)',
3008497549: 'Mansion Baron (Mansion Baron 4)',
- 1603920936: 'Prudent Student (Renaissance Sim / Nerd Brain 1',
+ 1603920936: 'Prudent Student (Renaissance Sim / Nerd Brain 1)',
3576085519: 'Jack of Some Trades (Renaissance Sim 2)',
3843569882: 'Pantologist (Renaissance Sim 3)',
517310505: 'Renaissance Sim (Renaissance Sim 4)',
- 2769839652: 'With The Program (Computer Whiz 1)',
+ 2769839652: 'With the Program (Computer Whiz 1)',
1394699556: 'Technically Adept (Computer Whiz 2)',
1896269241: 'Computer Geek (Computer Whiz 3)',
2969693962: 'Computer Whiz (Computer Whiz 4)',
@@ -311,7 +308,7 @@ def __init__(self):
2167317685: 'Funny (Joke Star 3)',
2867786966: 'Joke Star (Joke Star 4)',
2179132818: 'New in Town (Party Animal / Friend of the World 1)',
- 6359719: 'Well liked (Friend of the World 2)',
+ 6359719: 'Well Liked (Friend of the World 2)',
3307311430: 'Super Friend (Friend of the World 3)',
3184496709: 'Friend of the World (Friend of the World 4)',
1596456398: 'Welcoming Host (Party Animal 2)',
diff --git a/Scripts/s4ap/events/aspiration_event_dispatcher.py b/Scripts/s4ap/events/aspiration_event_dispatcher.py
index 026f54f..8cb0f07 100644
--- a/Scripts/s4ap/events/aspiration_event_dispatcher.py
+++ b/Scripts/s4ap/events/aspiration_event_dispatcher.py
@@ -3,11 +3,11 @@
from s4ap.enums.S4APLocalization import HashLookup
from s4ap.events.checks.send_check_event import SendLocationEvent
from s4ap.modinfo import ModInfo
+from s4ap.utils.s4ap_sim_utils import S4APSimUtils
from sims4communitylib.events.event_handling.common_event import CommonEvent
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
from sims4communitylib.services.common_service import CommonService
-from sims4communitylib.utils.common_injection_utils import CommonInjectionUtils
-from sims4communitylib.utils.sims.common_sim_utils import CommonSimUtils
+from lot51_core.utils.injection import inject_to
class MilestoneCompletion(CommonEvent):
@@ -29,9 +29,7 @@ def milestone_display_name(self):
def milestone_count(self):
return self._milestone_count
-
-@CommonInjectionUtils.inject_safely_into(ModInfo.get_identity(), AspirationTracker,
- AspirationTracker.complete_milestone.__name__, handle_exceptions=True)
+@inject_to(AspirationTracker, AspirationTracker.complete_milestone.__name__)
def _on_milestone_complete(original, self, *args, **kwargs):
result = original(self, *args, **kwargs)
OnMilestoneCompletionEvent.get()._on_milestone_completion(*args, **kwargs)
@@ -42,9 +40,9 @@ class OnMilestoneCompletionEvent(CommonService):
def _on_milestone_completion(self, aspiration, *_, **__):
if aspiration.aspiration_type == AspriationType.FULL_ASPIRATION:
- if aspiration.is_valid_for_sim(CommonSimUtils.get_active_sim_info()):
+ if aspiration.is_valid_for_sim(S4APSimUtils.get_active_sim_info()):
aspiration_display_name = aspiration.display_name
- track = CommonSimUtils.get_active_sim_info().primary_aspiration
+ track = S4APSimUtils.get_active_sim_info().primary_aspiration
track_display_text = track.display_text # this takes the primary aspiration name as a LocalizedString object
return CommonEventRegistry.get().dispatch(
MilestoneCompletion(track_display_text, aspiration_display_name, aspiration))
diff --git a/Scripts/s4ap/events/career_event_dispatcher.py b/Scripts/s4ap/events/career_event_dispatcher.py
index 63da99f..640dbee 100644
--- a/Scripts/s4ap/events/career_event_dispatcher.py
+++ b/Scripts/s4ap/events/career_event_dispatcher.py
@@ -3,17 +3,18 @@
from s4ap.events.checks.send_check_event import SendLocationEvent
from s4ap.logging.s4ap_logger import S4APLogger
from s4ap.modinfo import ModInfo
+from s4ap.utils.s4ap_household_utils import S4APHouseholdUtils
+from s4ap.utils.s4ap_sim_utils import S4APSimUtils
from sims4communitylib.events.event_handling.common_event import CommonEvent
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
from sims4communitylib.services.common_service import CommonService
-from sims4communitylib.utils.common_injection_utils import CommonInjectionUtils
-from sims4communitylib.utils.sims.common_household_utils import CommonHouseholdUtils
+from lot51_core.utils.injection import inject_to
log = S4APLogger.get_log()
log.enable()
-class CarrerPromotionEvent(CommonEvent):
+class CareerPromotionEvent(CommonEvent):
def __init__(self, career, current_level):
self._career = career
self._current_level = current_level
@@ -29,25 +30,26 @@ def current_level(self):
class OnCareerPromotionEvent(CommonService):
- def _on_promotion(self, career, user_level: int, *_, **__):
- log.debug(f'here is the career: {career}')
- CommonEventRegistry.get().dispatch(CarrerPromotionEvent(career, user_level))
+ def _on_promotion(self, career, user_level: int, name: str, *_, **__):
+ career_name = HashLookup().get_career_name(career, user_level)
+ log.debug(f'here is the career: {career} {user_level} ({career_name})')
+ log.debug(f'{name} was promoted to {career_name}')
+ CommonEventRegistry.get().dispatch(CareerPromotionEvent(career, user_level))
-
-@CommonInjectionUtils.inject_safely_into(ModInfo.get_identity(), CareerBase,
- CareerBase._handle_promotion.__name__, handle_exceptions=True)
+@inject_to(CareerBase, CareerBase._handle_promotion.__name__)
def _on_milestone_complete(original, self, *args, **kwargs):
result = original(self, *args, **kwargs)
career = self._current_track.get_career_name(self._sim_info).hash
level = self._user_level
sim_info = self._sim_info
- if sim_info in CommonHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
- OnCareerPromotionEvent.get()._on_promotion(career, level)
+ if sim_info in S4APHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
+ sim_name = S4APSimUtils.get_sim_first_name(sim_info)
+ OnCareerPromotionEvent.get()._on_promotion(career, level, sim_name)
return result
@CommonEventRegistry.handle_events(ModInfo.get_identity())
-def _send_notif_on_event_handle(event_data: CarrerPromotionEvent):
+def _send_notif_on_event_handle(event_data: CareerPromotionEvent):
lookup = HashLookup()
CommonEventRegistry.get().dispatch(
SendLocationEvent(f'{lookup.get_career_name(event_data.career, event_data.current_level)}'))
diff --git a/Scripts/s4ap/events/checks/send_check_event.py b/Scripts/s4ap/events/checks/send_check_event.py
index 4cc5ac9..6f6076b 100644
--- a/Scripts/s4ap/events/checks/send_check_event.py
+++ b/Scripts/s4ap/events/checks/send_check_event.py
@@ -1,10 +1,10 @@
+from lot51_core.utils.dialog import DialogHelper
from s4ap.events.Utils.send_location_event import SendLocationEvent
from s4ap.jsonio.s4ap_json import print_json, read_json
from s4ap.logging.s4ap_logger import S4APLogger
from s4ap.modinfo import ModInfo
from s4ap.utils.s4ap_generic_utils import S4APUtils
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
-from sims4communitylib.notifications.common_basic_notification import CommonBasicNotification
logger = S4APLogger.get_log()
logger.enable()
@@ -36,9 +36,8 @@ def _handle_send_check_event(event_data: SendLocationEvent):
if event_data.location_name not in json_list["Locations"]:
json_list["Locations"].append(event_data.location_name)
print_json(json_list, 'locations_cached.json')
- notif = CommonBasicNotification(
- title_identifier='Saving on check',
- description_identifier=event_data.location_name
- )
- notif.show()
+ DialogHelper.create_notification(
+ 'Saving on check',
+ event_data.location_name
+ ).show_dialog()
S4APUtils.trigger_autosave()
diff --git a/Scripts/s4ap/events/items/receive_item_event.py b/Scripts/s4ap/events/items/receive_item_event.py
index f4fe904..ba01995 100644
--- a/Scripts/s4ap/events/items/receive_item_event.py
+++ b/Scripts/s4ap/events/items/receive_item_event.py
@@ -1,20 +1,22 @@
import time
+from lot51_core.utils.dialog import DialogHelper
+from protocolbuffers import Consts_pb2
+
from s4ap.enums.S4APLocalization import S4APTraitId
from s4ap.logging.s4ap_logger import S4APLogger
from s4ap.modinfo import ModInfo
from s4ap.persistance.ap_session_data_store import S4APSessionStoreUtils
+from s4ap.utils.s4ap_career_utils import S4APCareerUtils
+from s4ap.utils.s4ap_generic_utils import S4APUtils
+from s4ap.utils.s4ap_household_utils import S4APHouseholdUtils
+from s4ap.utils.s4ap_sim_currency_utils import S4APSimCurrencyUtils
+from s4ap.utils.s4ap_sim_utils import S4APSimUtils
from s4ap.utils.s4ap_skill_utils import lock_skills
-from sims4communitylib.enums.common_currency_modify_reasons import CommonCurrencyModifyReason
+from s4ap.utils.s4ap_trait_utils import S4APTraitUtils
+from sims4.resources import Types
from sims4communitylib.events.event_handling.common_event import CommonEvent
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
-from sims4communitylib.notifications.common_basic_notification import CommonBasicNotification
-from sims4communitylib.utils.sims.common_career_utils import CommonCareerUtils
-from sims4communitylib.utils.sims.common_household_utils import CommonHouseholdUtils
-from sims4communitylib.utils.sims.common_sim_career_utils import CommonSimCareerUtils
-from sims4communitylib.utils.sims.common_sim_currency_utils import CommonSimCurrencyUtils
-from sims4communitylib.utils.sims.common_sim_utils import CommonSimUtils
-from sims4communitylib.utils.sims.common_trait_utils import CommonTraitUtils
from collections import Counter
log = S4APLogger.get_log()
log.enable()
@@ -129,16 +131,16 @@ def handle_items(self, items):
log.debug(f'Processing {item}')
if 'Simoleons' in item: # Simoleon items are either '5000 Simoleons' or '2000 Simoleons'
number = item.split()[0]
- CommonSimCurrencyUtils.add_simoleons_to_household(CommonSimUtils.get_active_sim_info(), int(number),
- CommonCurrencyModifyReason.CHEAT)
+ S4APSimCurrencyUtils.add_simoleons_to_household(S4APSimUtils.get_active_sim_info(), int(number),
+ Consts_pb2.TELEMETRY_MONEY_CHEAT)
time.sleep(0.2)
elif 'boost' in item.lower():
if 'career' in item.lower():
- for sim_info in CommonHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
- for career in CommonSimCareerUtils.get_all_careers_for_sim_gen(sim_info):
+ for sim_info in S4APHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
+ for career in S4APCareerUtils.get_all_careers_for_sim_gen(sim_info):
if career is None:
break
- old_work_performance = CommonCareerUtils.get_work_performance(career)
+ old_work_performance = S4APCareerUtils.get_work_performance(career)
work_performance_left_to_add = 100 - old_work_performance
career.add_work_performance(work_performance_left_to_add)
career.resend_career_data()
@@ -157,10 +159,12 @@ def handle_items(self, items):
rem_traits = (S4APTraitId.SKILL_GAIN_BOOST_2_5X, S4APTraitId.SKILL_GAIN_BOOST_3X,
S4APTraitId.SKILL_GAIN_BOOST_3_5X)
add_trait = S4APTraitId.SKILL_GAIN_BOOST_4X
- for sim_info in CommonHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
+ for sim_info in S4APHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
if rem_traits is not None:
- CommonTraitUtils.remove_traits(sim_info, rem_traits)
- CommonTraitUtils.add_trait(sim_info, add_trait)
+ S4APTraitUtils.remove_traits(sim_info, rem_traits)
+ trait_obj = S4APUtils.load_instance(Types.TRAIT, add_trait)
+ if trait_obj and not sim_info.has_trait(trait_obj):
+ sim_info.add_trait(trait_obj)
elif 'skill' in item.lower():
count = data_store.get_items().count(item)
count += 2
@@ -175,9 +179,9 @@ def handle_items(self, items):
skill = skill.lower().replace('mixology', 'bartending')
lock_skills(count, skill, False)
- def show_received_notification(self, items, players, locations):
- notif = CommonBasicNotification(
- title_identifier='Received Items',
- description_identifier='\n'.join(
- [f'{item} from {player} ({location})' for item, player, location in zip(items, players, locations)]))
- notif.show()
+ @staticmethod
+ def show_received_notification(items, players, locations):
+ DialogHelper.create_notification(
+ 'Received Items',
+ '\n'.join(
+ [f'{item} from {player} ({location})' for item, player, location in zip(items, players, locations)])).show_dialog()
diff --git a/Scripts/s4ap/events/skill_event_dispatcher.py b/Scripts/s4ap/events/skill_event_dispatcher.py
index 06d7783..3a534cf 100644
--- a/Scripts/s4ap/events/skill_event_dispatcher.py
+++ b/Scripts/s4ap/events/skill_event_dispatcher.py
@@ -1,58 +1,29 @@
import re
from s4ap.events.checks.send_check_event import SendLocationEvent
+from s4ap.events.skill_events import SimSkillLeveledUpEvent
from s4ap.logging.s4ap_logger import S4APLogger
from s4ap.modinfo import ModInfo
+from s4ap.utils.s4ap_sim_utils import S4APSimUtils
+from s4ap.utils.s4ap_skill_utils_class import S4APSkillUtils
from sims.sim_info import SimInfo
-from sims4communitylib.events.event_handling.common_event import CommonEvent
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
from sims4communitylib.services.common_service import CommonService
-from sims4communitylib.utils.common_injection_utils import CommonInjectionUtils
-from sims4communitylib.utils.resources.common_skill_utils import CommonSkillUtils
-from sims4communitylib.utils.sims.common_sim_utils import CommonSimUtils
+from lot51_core.utils.injection import inject_to
from statistics.skill import Skill
logger = S4APLogger.get_log()
-class SimSkillLeveledUpEvent(CommonEvent):
-
- def __init__(self, sim_info: SimInfo, skill: Skill, new_skill_level: int):
- self._sim_info = sim_info
- self._skill = skill
- self._new_skill_level = new_skill_level
-
- @property
- def new_skill_level(self) -> int:
- """The level the Sim will be after leveling up."""
- return self._new_skill_level
-
- @property
- def sim_info(self) -> SimInfo:
- """The Sim that leveled up in a Skill."""
- return self._sim_info
-
- @property
- def skill(self) -> Skill:
- """The Skill that was leveled up."""
- return self._skill
-
- @property
- def skill_id(self) -> int:
- """The decimal identifier of the Skill."""
- return CommonSkillUtils.get_skill_id(self.skill)
-
-
class HandleSkillLevelUp(CommonService):
def _on_skill_updated(self, skill: Skill, new_skill_level: int) -> None:
if skill.tracker is None or skill.tracker._owner is None:
return
if not skill.tracker._owner.is_npc:
- sim_info = CommonSimUtils.get_sim_info(skill.tracker._owner)
+ sim_info = S4APSimUtils.get_sim_info(skill.tracker._owner)
CommonEventRegistry.get().dispatch(SimSkillLeveledUpEvent(sim_info, skill, new_skill_level))
-
-@CommonInjectionUtils.inject_safely_into(ModInfo.get_identity(), Skill, Skill._handle_skill_up.__name__)
+@inject_to(Skill, Skill._handle_skill_up.__name__)
def _common_on_sim_skill_level_up(original, self, *args, **kwargs):
result = original(self, *args, **kwargs)
HandleSkillLevelUp.get()._on_skill_updated(self, *args, **kwargs)
diff --git a/Scripts/s4ap/events/skill_events.py b/Scripts/s4ap/events/skill_events.py
new file mode 100644
index 0000000..43fb35d
--- /dev/null
+++ b/Scripts/s4ap/events/skill_events.py
@@ -0,0 +1,34 @@
+from typing import Optional
+
+from sims.sim_info import SimInfo
+from sims4communitylib.events.event_handling.common_event import CommonEvent
+from statistics.skill import Skill
+
+
+class SimSkillLeveledUpEvent(CommonEvent):
+
+ def __init__(self, sim_info: SimInfo, skill: Skill, new_skill_level: int):
+ self._sim_info = sim_info
+ self._skill = skill
+ self._new_skill_level = new_skill_level
+
+ @property
+ def new_skill_level(self) -> int:
+ """The level the Sim will be after leveling up."""
+ return self._new_skill_level
+
+ @property
+ def sim_info(self) -> SimInfo:
+ """The Sim that leveled up in a Skill."""
+ return self._sim_info
+
+ @property
+ def skill(self) -> Skill:
+ """The Skill that was leveled up."""
+ return self._skill
+
+ @property
+ def skill_id(self) -> Optional[int]:
+ """The decimal identifier of the Skill, or None if the Skill has no guid64 attribute."""
+ from s4ap.utils.s4ap_skill_utils_class import S4APSkillUtils
+ return S4APSkillUtils.get_skill_id(self.skill)
diff --git a/Scripts/s4ap/logging/s4ap_logger.py b/Scripts/s4ap/logging/s4ap_logger.py
index 6bb441b..7e9fc27 100644
--- a/Scripts/s4ap/logging/s4ap_logger.py
+++ b/Scripts/s4ap/logging/s4ap_logger.py
@@ -1,11 +1,13 @@
from s4ap.enums.S4APLocalization import S4APStringId
from s4ap.modinfo import ModInfo
+from s4ap.utils.s4ap_generic_utils import S4APUtils
+from s4ap.utils.s4ap_localization_utils import S4APLocalizationUtils
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
from sims4communitylib.events.zone_spin.events.zone_late_load import S4CLZoneLateLoadEvent
from sims4communitylib.logging.has_class_log import HasClassLog
from sims4communitylib.mod_support.mod_identity import CommonModIdentity
-from sims4communitylib.notifications.common_basic_notification import CommonBasicNotification
from sims4communitylib.utils.localization.common_localization_utils import CommonLocalizationUtils
+from lot51_core.utils.dialog import DialogHelper
class S4APLogger(HasClassLog):
@@ -20,11 +22,14 @@ def get_log_identifier(cls) -> str:
@staticmethod
def show_loaded_notification() -> None:
""" Show that the mod has loaded. """
- notification = CommonBasicNotification(
- CommonLocalizationUtils.create_localized_string(S4APStringId.S4AP_LOADED),
+ DialogHelper.create_notification(
+ S4APLocalizationUtils.localize(S4APStringId.S4AP_LOADED),
'Loaded Sims 4 Archipelago Mod (' + ModInfo.get_identity().version + ')'
- )
- notification.show()
+ ).show_dialog()
+ # S4APUtils.show_basic_notification(
+ # S4APLocalizationUtils.localize(S4APStringId.S4AP_LOADED),
+ # 'Loaded Sims 4 Archipelago Mod (' + ModInfo.get_identity().version + ')'
+ # )
@staticmethod
@CommonEventRegistry.handle_events('s4ap_loaded')
diff --git a/Scripts/s4ap/modinfo.py b/Scripts/s4ap/modinfo.py
index bc928eb..ec8463b 100644
--- a/Scripts/s4ap/modinfo.py
+++ b/Scripts/s4ap/modinfo.py
@@ -14,7 +14,7 @@ def _name(self) -> str:
@property
def _version(self) -> str:
# Mod version
- return '0.2.6'
+ return '0.3.0.beta3'
@property
def _author(self) -> str:
diff --git a/Scripts/s4ap/persistance/ap_data_store.py b/Scripts/s4ap/persistance/ap_data_store.py
index c417b68..2283306 100644
--- a/Scripts/s4ap/persistance/ap_data_store.py
+++ b/Scripts/s4ap/persistance/ap_data_store.py
@@ -48,6 +48,6 @@ def _default_data(self) -> Dict[str, Any]:
S4APSettings.LOCATIONS: None, # this should be a List[str] (a list of strings)
S4APSettings.SENDERS: None, # this should be a List[str] (a list of strings)
S4APSettings.GOAL: None, # this should be a string
- S4APSettings.CAREER: None, # currently this is a string, but in future versions, it will be a set coming from AP, which will probably deserialize as a list? i don't know exactly how JSON deserialization works with S4CL.
+ S4APSettings.CAREER: None, # currently this is a string, but in future versions, it will be a set coming from AP, which will probably deserialize as a list? i don't know exactly how JSON deserialization works with S4CL. from poking around, it'll be a list of strings, so it's been adjusted accordingly.
S4APSettings.SLOT: 1, # this should be an integer, if it isn't, something has gone terribly wrong here
}.copy()
diff --git a/Scripts/s4ap/persistance/ap_session_data_store.py b/Scripts/s4ap/persistance/ap_session_data_store.py
index 9d932c7..81d4612 100644
--- a/Scripts/s4ap/persistance/ap_session_data_store.py
+++ b/Scripts/s4ap/persistance/ap_session_data_store.py
@@ -1,4 +1,4 @@
-from typing import Any
+from typing import Any, List, Callable, Optional
import services
from s4ap.jsonio.s4ap_json import print_json
@@ -7,11 +7,10 @@
from s4ap.persistance.ap_data_store import S4APGenericDataStore, S4APSettings
from s4ap.persistance.ap_data_utils import S4APDataManagerUtils
from s4ap.utils.s4ap_generic_utils import S4APUtils
+from s4ap.utils.s4ap_localization_utils import S4APLocalizationUtils
from s4ap.utils.s4ap_reset_utils import ResetSimData
from sims4communitylib.dialogs.ok_cancel_dialog import CommonOkCancelDialog
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
-from sims4communitylib.utils.localization.common_localization_utils import CommonLocalizationUtils
-from sims4communitylib.utils.localization.common_localized_string_colors import CommonLocalizedStringColor
from ui.ui_dialog import UiDialogOkCancel
logger = S4APLogger.get_log()
@@ -24,7 +23,7 @@ class S4APSessionStoreUtils:
def __init__(self) -> None:
self._data_manager = S4APDataManagerUtils()
- def check_session_values(self, host_name: str, port: int, seed_name: str, player: str, slot: int) -> bool:
+ def check_session_values(self, host_name: str, port: int, seed_name: str, player: str, slot: int, on_complete: Optional[Callable[[bool], None]] = None) -> bool:
""" Check session store to make sure it's the same settings as before and send a warning otherwise
:returns True, if stored settings exist and all values match the incoming parameters; False otherwise. """
@@ -45,6 +44,8 @@ def check_session_values(self, host_name: str, port: int, seed_name: str, player
if isinstance(slot, list):
logger.warn("The slot coming in from the connection_status.json is a list. This means your APWorld is out of date. Please update.")
+ if on_complete:
+ on_complete(False)
return False
if isinstance(stored_slot, list):
@@ -55,6 +56,8 @@ def check_session_values(self, host_name: str, port: int, seed_name: str, player
logger.info(f"Correct json file for this slot: s4ap_main_guid_{slot_id}.json")
except Exception as ex:
logger.error("Failed to retrieve save slot ID for this slot.", exception=ex)
+ if on_complete:
+ on_complete(False)
return False
if (str(stored_seed), str(stored_host_name), int(stored_port), str(stored_player), int(stored_slot)) != \
@@ -64,6 +67,9 @@ def check_session_values(self, host_name: str, port: int, seed_name: str, player
def _cancel_chosen(_: UiDialogOkCancel):
# If cancel is chosen, stop parsing data until connection_status.json changes
+ logger.info("User cancelled session dialog")
+ if on_complete:
+ on_complete(False)
return False
def _ok_chosen(_: UiDialogOkCancel):
@@ -83,17 +89,43 @@ def _ok_chosen(_: UiDialogOkCancel):
print_json(True, 'sync.json')
print_json({}, 'locations_cached.json')
CommonEventRegistry.get().dispatch(AllowReceiveItems(True))
+ if on_complete:
+ on_complete(True)
return True # if okay is chosen then save seed values and resync items
# Prompt the user to either overwrite the previous session_data, or stop parsing the data packet and wait for the connection_status.json to update
- dialog = CommonOkCancelDialog(
- CommonLocalizationUtils.create_localized_string('Warning!',
- text_color=CommonLocalizedStringColor.RED),
- description_identifier="There's a mismatch with your AP session data. If you press 'Overwrite,' all previous items will be resynced, and your Sims' skill levels will reset. If you'd rather keep your current progress, select 'Cancel' and switch to a different save file so you can come back to this session later.",
- ok_text_identifier='Overwrite'
+ dialog = S4APUtils.show_ok_cancel_dialog(
+ title=S4APLocalizationUtils.create_from_string("Warning!"),
+ text=S4APLocalizationUtils.create_from_string(
+ "There's a mismatch with your AP session data. If you press 'Overwrite,' all previous "
+ "items will be resynced, and your Sims' skill levels will reset. If you'd rather keep your "
+ "current progress, select 'Cancel' and switch to a different save file so you can come back later."
+ ),
+ ok_text=S4APLocalizationUtils.create_from_string("Overwrite"),
+ cancel_text=S4APLocalizationUtils.create_from_string("Cancel"),
+ on_ok=_ok_chosen,
+ on_cancel=_cancel_chosen
)
- dialog.show(on_ok_selected=_ok_chosen, on_cancel_selected=_cancel_chosen)
- return False
+
+ if dialog is not None:
+ dialog.show_dialog()
+ if on_complete:
+ logger.debug("Async dialog shown; awaiting user choice")
+ return False
+ else:
+ logger.error(
+ "check_session_values called synchronously with dialog! "
+ "This path is deprecated and unsafe."
+ )
+ return True
+ else:
+ logger.warn("No active Sim to show dialog. Treating as cancel.")
+ if on_complete:
+ on_complete(False)
+ return False
+ else:
+ return False
+
else: # Settings exist and match
logger.debug("AP session data matched")
return True
@@ -111,20 +143,45 @@ def _ok_chosen(_: UiDialogOkCancel):
print_json({}, 'locations_cached.json')
CommonEventRegistry.get().dispatch(AllowReceiveItems(True))
self.save_seed_values(host_name, port, seed_name, player, slot)
- return True
+ # existing reset/save logic
+ if on_complete:
+ on_complete(True)
def _cancel_chosen(_: UiDialogOkCancel):
+ logger.info("User cancelled session dialog")
+ if on_complete:
+ on_complete(False)
return False
# Prompt the user to either overwrite the previous session_data, or stop parsing the data packet and wait for the connection_status.json to update
- dialog = CommonOkCancelDialog(
- CommonLocalizationUtils.create_localized_string('Warning!',
- text_color=CommonLocalizedStringColor.RED),
- description_identifier="Pressing 'Connect' will reset your Sims' skill levels and will sync the game to the client. If you don't want to use this save, click 'Cancel' and switch to a different one.",
- ok_text_identifier='Connect'
+ dialog = S4APUtils.show_ok_cancel_dialog(
+ title=S4APLocalizationUtils.create_from_string("Warning!"),
+ text=S4APLocalizationUtils.create_from_string(
+ "Pressing 'Connect' will reset your Sims' skill levels and will sync the game to the client. "
+ "If you don't want to use this save, click 'Cancel' and switch to a different one."
+ ),
+ ok_text=S4APLocalizationUtils.create_from_string("Connect"),
+ cancel_text=S4APLocalizationUtils.create_from_string("Cancel"),
+ on_ok=_ok_chosen,
+ on_cancel=_cancel_chosen
)
- dialog.show(on_ok_selected=_ok_chosen, on_cancel_selected=_cancel_chosen)
- return False
+
+ if dialog is not None:
+ dialog.show_dialog()
+ if on_complete:
+ logger.debug("Async dialog shown; awaiting user choice")
+ return False # prevent auto-continue
+ else:
+ logger.error(
+ "check_session_values called synchronously with dialog! "
+ "This path is deprecated and unsafe."
+ )
+ return True # legacy behavior
+ else:
+ logger.warn("No active Sim to show dialog. Treating as cancel.")
+ if on_complete:
+ on_complete(False)
+ return False
def check_index_value(self, index: str) -> bool:
"""Checks The Index from ReceivedItems to make sure it matches
@@ -168,7 +225,7 @@ def save_item_info(self, items: str, item_ids: str, locations: str, senders: str
self._set_value(S4APSettings.SENDERS, senders)
S4APUtils.trigger_autosave()
- def save_goal_and_career(self, goal: str, career: str):
+ def save_goal_and_career(self, goal: str, career: List[str]):
self._set_value(S4APSettings.GOAL, goal)
self._set_value(S4APSettings.CAREER, career)
S4APUtils.trigger_autosave()
@@ -191,7 +248,7 @@ def get_seed_name(self) -> str:
def get_goal(self) -> str:
return self._get_value(S4APSettings.GOAL)
- def get_career(self) -> str:
+ def get_career(self) -> List[str]:
return self._get_value(S4APSettings.CAREER)
def get_slot(self) -> int:
diff --git a/Scripts/s4ap/s4ap_commands.py b/Scripts/s4ap/s4ap_commands.py
index 0ccd28c..4944ab4 100644
--- a/Scripts/s4ap/s4ap_commands.py
+++ b/Scripts/s4ap/s4ap_commands.py
@@ -1,12 +1,18 @@
from s4ap.logging.s4ap_logger import S4APLogger
from s4ap.modinfo import ModInfo
-from sims4communitylib.services.commands.common_console_command import CommonConsoleCommand
-from sims4communitylib.services.commands.common_console_command_output import CommonConsoleCommandOutput
+from sims4.commands import Command, CommandType, Output
+# from sims4communitylib.services.commands.common_console_command import CommonConsoleCommand
+# from sims4communitylib.services.commands.common_console_command_output import CommonConsoleCommandOutput
log = S4APLogger.get_log()
log.enable()
-@CommonConsoleCommand(ModInfo.get_identity(), 's4ap.version', 'Prints the mod version to the console')
-def _show_mod_version(output: CommonConsoleCommandOutput):
+@Command('s4ap.version', command_type=CommandType.Live)
+def show_mod_version(_connection=None):
+ output = Output(_connection)
output(f"The Sims 4 Archipelago, Current Mod Version: {ModInfo.get_identity().version}")
+
+# @CommonConsoleCommand(ModInfo.get_identity(), 's4ap.version', 'Prints the mod version to the console')
+# def _show_mod_version(output: CommonConsoleCommandOutput):
+# output(f"The Sims 4 Archipelago, Current Mod Version: {ModInfo.get_identity().version}")
diff --git a/Scripts/s4ap/utils/s4ap_career_utils.py b/Scripts/s4ap/utils/s4ap_career_utils.py
new file mode 100644
index 0000000..9325db7
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_career_utils.py
@@ -0,0 +1,164 @@
+from careers.career_tuning import Career, CareerLevel, TunableCareerTrack
+from random import random
+from typing import Callable, Iterator, Tuple, Union, List
+from services import get_instance_manager
+from sims.sim_info import SimInfo
+from sims4.resources import Types
+
+class S4APCareerUtils:
+
+ @classmethod
+ def get_career_guid(cls, career: Career) -> Union[int, None]:
+ if career is None:
+ return None
+ return getattr(career, 'guid64', None)
+
+ @staticmethod
+ def load_career_by_guid(career: Union[int, Career]) -> Union[Career, None]:
+ if career is None:
+ return None
+ if isinstance(career, Career):
+ return career
+ try:
+ career_guid = int(career)
+ except Exception:
+ return None # invalid ID
+
+ # Use the game's instance manager for careers
+ career_manager = get_instance_manager(Types.CAREER)
+ if career_manager is None:
+ return None
+ return career_manager.get(career_guid)
+
+ @classmethod
+ def get_all_careers_for_sim_gen(cls, sim_info: SimInfo, include_career_callback: Callable[[Career], bool]=None) -> Iterator[Career]:
+ if sim_info is None:
+ return tuple()
+ career_tracker = sim_info.career_tracker
+ if career_tracker is None:
+ return tuple()
+
+ for career in career_tracker.careers.values():
+ if include_career_callback is not None and not include_career_callback(career):
+ continue
+ yield career
+
+ @staticmethod
+ def determine_entry_level_into_career_from_user_level(career: Career, desired_user_level: int) -> Tuple[
+ Union[int, None], Union[int, None], Union[TunableCareerTrack, None]]:
+ if career is None:
+ return None, None, None
+ track = S4APCareerUtils.get_starting_career_track(career)
+
+ @classmethod
+ def get_starting_career_track(cls, career: Career) -> Union[TunableCareerTrack, None]:
+ """get_starting_career_track(career)
+
+ Retrieve the starting Career Track of a Career.
+
+ :param career: A career.
+ :type career: Career
+ :return: The starting Career Track of the Career or None if not found.
+ :rtype: Union[TunableCareerTrack, None]
+ """
+ if career is None:
+ return None
+ return career.start_track
+
+ @classmethod
+ def determine_entry_level_into_career_track_by_user_level(cls, career_track: TunableCareerTrack,
+ desired_user_level: int) -> Tuple[
+ Union[int, None], Union[int, None], Union[TunableCareerTrack, None]]:
+ if career_track is None:
+ return None, None, None
+ track = career_track
+ track_start_level = 1
+
+ while True:
+ track_length = len(cls.get_career_levels(track))
+ level = desired_user_level - track_start_level
+ if level < track_length:
+ user_level = track_start_level + level
+ return level, user_level, track
+
+ branches = cls.get_branches(track)
+ if not branches:
+ # The exit path. When we run out of branches to check we'll just return the last info found.
+ level = track_length - 1
+ user_level = track_start_level + level
+ return level, user_level, track
+
+ track_start_level += track_length
+ track = random.choice(branches)
+
+ @classmethod
+ def get_career_levels(cls, career_track: TunableCareerTrack, include_branches: bool = False) -> Tuple[CareerLevel]:
+ if career_track is None:
+ return tuple()
+ if include_branches:
+ if career_track is None:
+ return tuple()
+ if include_branches:
+ # noinspection PyUnresolvedReferences
+ if hasattr(career_track, 'career_levels') and career_track.career_levels is not None:
+ career_levels: List[CareerLevel] = list(career_track.career_levels)
+ branches = cls.get_branches(career_track)
+ for branch_career_track in branches:
+ sub_career_levels = cls.get_career_levels(branch_career_track,
+ include_branches=include_branches)
+ if not sub_career_levels:
+ continue
+ career_levels.extend(sub_career_levels)
+ return tuple(career_levels)
+ else:
+ # noinspection PyUnresolvedReferences
+ if hasattr(career_track, 'career_levels') and career_track.career_levels is not None:
+ return tuple(career_track.career_levels)
+ return tuple()
+
+ @classmethod
+ def get_branches(cls, career_track: TunableCareerTrack, include_sub_branches: bool = False) -> Tuple[
+ TunableCareerTrack]:
+ """get_branches(career_track, include_sub_branches=True)
+
+ Retrieve a collection of all Career Tracks that branch off of a Career Track and if specified, the branches those branches branch off to.
+
+ :param career_track: A Career Track.
+ :type career_track: TunableCareerTrack
+ :param include_sub_branches: If True, all branches will be checked for their own branches and those branches will be included recursively. If False, only the top level branches will be included. Default is False.
+ :type include_sub_branches: bool, optional
+ :return: A collection of all Career Tracks that branch off from the specified Career Track.
+ :rtype: Tuple[TunableCareerTrack]
+ """
+ if career_track is None:
+ return tuple()
+ if include_sub_branches:
+ # noinspection PyUnresolvedReferences
+ if hasattr(career_track, 'branches') and career_track.branches is not None:
+ career_track_branches: List[TunableCareerTrack] = list(career_track.branches)
+ for sub_career_track in career_track_branches:
+ sub_branches = cls.get_branches(sub_career_track, include_sub_branches=include_sub_branches)
+ if not sub_branches:
+ continue
+ career_track_branches.extend(sub_branches)
+ return tuple(career_track_branches)
+ else:
+ # noinspection PyUnresolvedReferences
+ if hasattr(career_track, 'branches') and career_track.branches is not None:
+ return tuple(career_track.branches)
+ return tuple()
+
+ @staticmethod
+ def get_work_performance(career: Career) -> float:
+ """get_work_performance(career)
+
+ Add an amount to the work performance of a career.
+
+ :param career: The career to modify.
+ :type career: Career
+ :return: The amount of work performance acquired in the specified Career.
+ :rtype: float
+ """
+ if career is None:
+ return 0.0
+ return career.work_performance
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_dialog_utils.py b/Scripts/s4ap/utils/s4ap_dialog_utils.py
new file mode 100644
index 0000000..48501bd
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_dialog_utils.py
@@ -0,0 +1,58 @@
+import services
+from s4ap.utils.s4ap_localization_utils import S4APLocalizationUtils
+from sims4.resources import Types, get_resource_key
+from ui.ui_dialog_picker import ObjectPickerRow, ObjectPickerType, UiObjectPicker
+
+class S4APDialog:
+
+ class ObjectPickerDialog:
+
+ def __init__(self, sim=None, title=str(), text=str(), picker_rows=None, min_selectable=1, max_selectable=1,
+ is_sortable=False, picker_type=ObjectPickerType.OBJECT, callback=None):
+ self.sim = sim
+ self.title = S4APLocalizationUtils.localize(title)
+ self.text = S4APLocalizationUtils.localize(text)
+ self.picker_rows = picker_rows if picker_rows else []
+ self.min_selectable = min_selectable
+ self.max_selectable = max_selectable
+ self.is_sortable = is_sortable
+ self.picker_type = picker_type
+ self.callback = callback
+
+ def show_dialog(self):
+ object_picker = UiObjectPicker.TunableFactory().default(self.sim, text=lambda *args, **kwargs: self.text,
+ title=lambda *args, **kwargs: self.title,
+ min_selectable=self.min_selectable,
+ max_selectable=self.max_selectable,
+ is_sortable=self.is_sortable,
+ picker_type=self.picker_type)
+ for picker_row in self.picker_rows:
+ if picker_row is not None:
+ if isinstance(picker_row, ObjectPickerRow):
+ object_picker.add_row(picker_row)
+ else:
+ object_picker.add_row(picker_row.get_object_picker_row())
+ if self.callback:
+ object_picker.add_listener(self._internal_callback)
+ object_picker.show_dialog()
+
+ def _internal_callback(self, dialog):
+ result_tags = dialog.get_result_tags()
+ for tag in result_tags:
+ self.callback(result_tag=tag)
+
+ @staticmethod
+ def create_picker_row(option_id, title=str(), text=str(), rarity_text=str(), object_id=None, def_id=None,
+ icon_id=None, tag=None, is_enable=True):
+ name = S4APLocalizationUtils.localize(title)
+ row_description = S4APLocalizationUtils.localize(text)
+ rarity_text = S4APLocalizationUtils.localize(rarity_text)
+ if icon_id is not None:
+ icon = get_resource_key(icon_id, Types.PNG)
+ else:
+ icon = None
+ if tag is None:
+ tag = option_id
+ return ObjectPickerRow(option_id=option_id, name=name, row_description=row_description,
+ rarity_text=rarity_text, object_id=object_id, def_id=def_id, icon=icon, tag=tag,
+ count=0, is_enable=is_enable)
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_game_client_utils.py b/Scripts/s4ap/utils/s4ap_game_client_utils.py
new file mode 100644
index 0000000..548ed95
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_game_client_utils.py
@@ -0,0 +1,32 @@
+from typing import Union
+
+import services
+from server.client import Client
+from server.clientmanager import ClientManager
+
+class S4APGameClientUtils:
+
+ @staticmethod
+ def get_first_game_client() -> Union[Client, None]:
+ """get_first_game_client()
+
+ Retrieve an instance of the first available Game Client.
+
+ :return: An instance of the first available Game Client or None if not found.
+ :rtype: Union[Client, None]
+ """
+ client_manager = S4APGameClientUtils.get_game_client_manager()
+ if client_manager is None:
+ return None
+ return client_manager.get_first_client()
+
+ @staticmethod
+ def get_game_client_manager() -> ClientManager:
+ """get_game_client_manager()
+
+ Retrieve the manager that manages the Game Clients for the game.
+
+ :return: The manager that manages the Game Clients for the game.
+ :rtype: ClientManager
+ """
+ return services.client_manager()
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_generic_utils.py b/Scripts/s4ap/utils/s4ap_generic_utils.py
index f7f007d..ba927cf 100644
--- a/Scripts/s4ap/utils/s4ap_generic_utils.py
+++ b/Scripts/s4ap/utils/s4ap_generic_utils.py
@@ -1,11 +1,18 @@
import services
+from typing import Any, Callable, Optional, Union
+
+from lot51_core.utils.dialog import DialogHelper
from s4ap.modinfo import ModInfo
+from s4ap.utils.s4ap_sim_utils import S4APSimUtils
from services.persistence_service import SaveGameData
+from sims.sim_info import SimInfo
+from sims4.resources import Types
from sims4communitylib.events.zone_spin.common_zone_spin_event_dispatcher import CommonZoneSpinEventDispatcher
from sims4communitylib.exceptions.common_exceptions_handler import CommonExceptionHandler
-from sims4communitylib.notifications.common_basic_notification import CommonBasicNotification
-from sims4communitylib.utils.save_load.common_save_utils import CommonSaveUtils
-
+from s4ap.utils.s4ap_save_utils import S4APSaveUtils
+from sims4.localization import LocalizationHelperTuning
+from ui.ui_dialog import UiDialogOkCancel
+from ui.ui_dialog_notification import UiDialogNotification
class S4APUtils:
@@ -15,17 +22,87 @@ def trigger_autosave(*_) -> bool:
if CommonZoneSpinEventDispatcher().game_loading or not CommonZoneSpinEventDispatcher().game_loaded:
return False
import sims4.commands
- save_game_data = SaveGameData(CommonSaveUtils.get_save_slot_id(), 'S4APAutosave', True,
+ save_game_data = SaveGameData(S4APSaveUtils.get_save_slot_id(), 'S4APAutosave', True,
5000002)
persistence_service = services.get_persistence_service()
persistence_service.save_using(persistence_service.save_game_gen, save_game_data, send_save_message=True,
check_cooldown=False)
return True
except Exception as ex:
- CommonBasicNotification(
+ S4APUtils.show_basic_notification(
'A problem occured while saving S4AP Data',
0
- ).show()
+ )
CommonExceptionHandler.log_exception(ModInfo.get_identity(), 'An exception occurred while autosaving.',
exception=ex)
return False
+
+ @staticmethod
+ def load_instance(instance_type: Types, instance_id: int):
+ """Load a resource instance (Trait, Buff, Mood, etc.) directly from the game."""
+ instance_manager = services.get_instance_manager(instance_type)
+ if instance_manager is None:
+ return None
+ return instance_manager.get(instance_id)
+
+ @staticmethod
+ def load_icon_by_id(icon_id: int):
+ manager = services.get_instance_manager(Types.PNG)
+ return manager.get(icon_id) # Returns vanilla ResourceKey / instance
+
+ @staticmethod
+ def show_basic_notification(title_text: Union[int, str], description_text: Union[int, str]):
+ """Show a simple vanilla notification to the player."""
+ title = LocalizationHelperTuning.get_raw_text(title_text)
+ description = LocalizationHelperTuning.get_raw_text(description_text)
+
+ dialog = UiDialogNotification.TunableFactory().default(
+ owner=None,
+ title=title,
+ text=description
+ )
+ dialog.show_dialog()
+
+ @classmethod
+ def show_ok_cancel_dialog(
+ cls,
+ title,
+ text,
+ ok_text,
+ cancel_text,
+ on_ok: Optional[Callable[[UiDialogOkCancel], Any]] = None,
+ on_cancel: Optional[Callable[[UiDialogOkCancel], Any]] = None,
+ owner: Optional[SimInfo] = None
+ ):
+ """Show an Ok/Cancel dialog with optional handlers for each response.
+
+ :param title: LocalizedString for the title.
+ :param text: LocalizedString for the body text.
+ :param ok_text: LocalizedString for the OK button.
+ :param cancel_text: LocalizedString for the Cancel button.
+ :param on_ok: Callback if the user presses OK.
+ :param on_cancel: Callback if the user presses Cancel.
+ :param owner: SimInfo or None (defaults to None).
+ """
+
+ active_sim = S4APSimUtils.get_active_sim_info()
+ if active_sim is None:
+ # Skip showing the dialog if no active sim yet
+ return
+
+ def _on_response(dialog_instance: UiDialogOkCancel):
+ if dialog_instance.accepted:
+ if on_ok is not None:
+ on_ok(dialog_instance)
+ else:
+ if on_cancel is not None:
+ on_cancel(dialog_instance)
+
+ dialog = DialogHelper.create_dialog(
+ title,
+ text,
+ ok_text,
+ callback=_on_response
+ )
+
+ return dialog
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_household_utils.py b/Scripts/s4ap/utils/s4ap_household_utils.py
new file mode 100644
index 0000000..1f0171d
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_household_utils.py
@@ -0,0 +1,19 @@
+import services
+from services import active_household
+from typing import Iterator, Union
+from sims.household import Household
+from sims.sim_info import SimInfo
+
+class S4APHouseholdUtils:
+
+ @classmethod
+ def get_active_household(cls) -> Union[Household, None]:
+ return services.active_household()
+
+ @classmethod
+ def get_sim_info_of_all_sims_in_active_household_generator(cls) -> Iterator[SimInfo]:
+ household = active_household()
+ if household is not None:
+ for sim_info in household.sim_info_gen():
+ if sim_info is not None:
+ yield sim_info
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_localization_utils.py b/Scripts/s4ap/utils/s4ap_localization_utils.py
new file mode 100644
index 0000000..6691bab
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_localization_utils.py
@@ -0,0 +1,61 @@
+from protocolbuffers.Localization_pb2 import LocalizedString
+
+from typing import Any
+from sims4.localization import LocalizationHelperTuning, _create_localized_string, create_tokens
+
+class S4APLocalizationUtils:
+
+ @staticmethod
+ def localize(string_object, tokens=()):
+ tokens = S4APLocalizationUtils._localize_tokens(tokens)
+ if isinstance(string_object, str):
+ string_object = LocalizationHelperTuning.get_raw_text(string_object)
+ else:
+ if isinstance(string_object, int):
+ return _create_localized_string(string_object, tokens)
+ if hasattr(string_object, 'populate_localization_token'):
+ return string_object
+ if isinstance(string_object, LocalizedString):
+ create_tokens(string_object.tokens, tokens)
+ return string_object
+ elif isinstance(string_object, LocalizedString):
+ create_tokens(string_object.tokens, tokens)
+ return string_object
+ if isinstance(string_object, LocalizedString):
+ create_tokens(string_object.tokens, tokens)
+ return string_object
+
+ @staticmethod
+ def _localize_tokens(tokens_unlocalized):
+ tokens = list()
+ for token in tokens_unlocalized:
+ tokens.append(S4APLocalizationUtils.localize(token))
+ return tokens
+
+ @staticmethod
+ def create_from_string(string_text: str) -> LocalizedString:
+ """create_from_string(string_text)
+
+ Create a LocalizedString from a string.
+
+ :param string_text: The string to localize. The resulting LocalizedString will be '{0.String}'
+ :type string_text: str
+ :return: A LocalizedString created from the specified string.
+ :rtype: LocalizedString
+ """
+ return LocalizationHelperTuning.get_raw_text(string_text)
+
+ @staticmethod
+ def create_from_int(identifier: int, *tokens: Any) -> LocalizedString:
+ """create_from_int(identifier, *tokens)
+
+ Locate a LocalizedString by an identifier and format tokens into it.
+
+ :param identifier: A decimal number that identifies an existing LocalizedString.
+ :type identifier: int
+ :param tokens: A collection of objects to format into the LocalizedString. (Example types: LocalizedString, str, int, etc.)
+ :type tokens: Iterator[Any]
+ :return: A LocalizedString with the specified tokens formatted into it.
+ :rtype: LocalizedString
+ """
+ return _create_localized_string(identifier, *tokens)
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_phone_utils.py b/Scripts/s4ap/utils/s4ap_phone_utils.py
index 32f7bcd..7614852 100644
--- a/Scripts/s4ap/utils/s4ap_phone_utils.py
+++ b/Scripts/s4ap/utils/s4ap_phone_utils.py
@@ -1,26 +1,22 @@
import re
from aspirations.aspiration_types import AspriationType
+from lot51_core.utils.dialog import DialogHelper
from s4ap.enums.S4APLocalization import S4APTraitId, HashLookup, S4APBaseGameSkills
from s4ap.jsonio.s4ap_json import print_json
from s4ap.logging.s4ap_logger import S4APLogger
from s4ap.modinfo import ModInfo
from s4ap.persistance.ap_session_data_store import S4APSessionStoreUtils
+from s4ap.utils.s4ap_career_utils import S4APCareerUtils
+from s4ap.utils.s4ap_dialog_utils import S4APDialog
+from s4ap.utils.s4ap_generic_utils import S4APUtils
+from s4ap.utils.s4ap_household_utils import S4APHouseholdUtils
+from s4ap.utils.s4ap_skill_utils_class import S4APSkillUtils
from server_commands.argument_helpers import TunableInstanceParam
+from sims4.localization import LocalizationHelperTuning
from sims4.resources import Types
-from sims4communitylib.dialogs.choose_object_dialog import CommonChooseObjectDialog
-from sims4communitylib.dialogs.common_choice_outcome import CommonChoiceOutcome
from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
from sims4communitylib.events.sim.events.sim_trait_added import S4CLSimTraitAddedEvent
-from sims4communitylib.notifications.common_basic_notification import CommonBasicNotification
-from sims4communitylib.utils.common_icon_utils import CommonIconUtils
-from sims4communitylib.utils.localization.common_localization_utils import CommonLocalizationUtils
-from sims4communitylib.utils.resources.common_skill_utils import CommonSkillUtils
-from sims4communitylib.utils.sims.common_career_utils import CommonCareerUtils
-from sims4communitylib.utils.sims.common_household_utils import CommonHouseholdUtils
-from sims4communitylib.utils.sims.common_sim_career_utils import CommonSimCareerUtils
-from sims4communitylib.utils.sims.common_sim_skill_utils import CommonSimSkillUtils
-from sims4communitylib.utils.sims.common_trait_utils import CommonTraitUtils
from ui.ui_dialog_picker import ObjectPickerRow
logger = S4APLogger.get_log()
@@ -30,7 +26,9 @@
@CommonEventRegistry.handle_events(ModInfo.get_identity())
def _handle_show_max_skills_phone(event_data: S4CLSimTraitAddedEvent):
if event_data.trait_id == S4APTraitId.SHOW_RECEIVED_SKILLS:
- CommonTraitUtils.remove_trait(event_data.sim_info, S4APTraitId.SHOW_RECEIVED_SKILLS)
+ received_skills_trait_instance = TunableInstanceParam(Types.TRAIT)(S4APTraitId.SHOW_RECEIVED_SKILLS)
+ if event_data.sim_info.has_trait(received_skills_trait_instance):
+ event_data.sim_info.remove_trait(received_skills_trait_instance)
data_store = S4APSessionStoreUtils()
options = []
skills_and_levels = {}
@@ -67,34 +65,35 @@ def _handle_show_max_skills_phone(event_data: S4CLSimTraitAddedEvent):
for item, item_info in sorted(skills.items()):
options.append(ObjectPickerRow(
option_id=option,
- name=CommonLocalizationUtils.create_localized_string(
- f'{item} Max is {item_info[0] or 2}'),
+ name = LocalizationHelperTuning.get_raw_text(f'{item} Max is {item_info[0] or 2}'),
icon=item_info[1]
))
option += 1
- def _on_chosen(_, outcome: CommonChoiceOutcome):
- if outcome == CommonChoiceOutcome.CHOICE_MADE:
- dialog.show(on_chosen=_on_chosen)
+ sim = event_data.sim_info.get_sim_instance()
- dialog = CommonChooseObjectDialog(
- 'Max Possible Skills',
- 'The highest you can level your skills to.',
- choices=options
+ picker = S4APDialog.ObjectPickerDialog(
+ sim=sim,
+ title='Max Possible Skills',
+ text='The highest you can level your skills to.',
+ picker_rows=options
)
- dialog.show(on_chosen=_on_chosen)
+
+ picker.show_dialog()
@CommonEventRegistry.handle_events(ModInfo.get_identity())
def _resync_locations(event_data: S4CLSimTraitAddedEvent):
if event_data.trait_id == S4APTraitId.RESYNC_LOCATIONS:
- CommonTraitUtils.remove_trait(event_data.sim_info, S4APTraitId.RESYNC_LOCATIONS)
+ resync_trait_instance = TunableInstanceParam(Types.TRAIT)(S4APTraitId.RESYNC_LOCATIONS)
+ if event_data.sim_info.has_trait(resync_trait_instance):
+ event_data.sim_info.remove_trait(resync_trait_instance)
lookup = HashLookup()
locations = []
skill_dict = {}
careers_dict = {}
- for sim_info in CommonHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
- for skill in CommonSkillUtils.get_all_skills_gen():
- skill_level = CommonSimSkillUtils.get_current_skill_level(sim_info, skill, False)
+ for sim_info in S4APHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
+ for skill in S4APSkillUtils.get_all_skills_gen():
+ skill_level = S4APSkillUtils.get_current_skill_level(sim_info, skill)
skill_name = skill.skill_type.__name__
if skill_name.startswith("statistic_Skill_AdultMajor_") or 'fitness' in skill_name.lower():
skill_name = skill_name.replace("statistic_Skill_AdultMajor_", "")
@@ -122,8 +121,8 @@ def _resync_locations(event_data: S4CLSimTraitAddedEvent):
for level in range(2, int(skill_level) + 1):
location_name = f'{skill_new_name.title()} Skill {level}'
locations.append(location_name)
- for career in CommonSimCareerUtils.get_all_careers_for_sim_gen(sim_info):
- career_id = CommonCareerUtils.get_career_guid(career)
+ for career in S4APCareerUtils.get_all_careers_for_sim_gen(sim_info):
+ career_id = S4APCareerUtils.get_career_guid(career)
career_level = career.user_level
if careers_dict.get(career_id) is not None:
if career_level > careers_dict.get(career_id):
@@ -131,9 +130,9 @@ def _resync_locations(event_data: S4CLSimTraitAddedEvent):
else:
careers_dict[career_id] = career_level
for career_guid, level in careers_dict.items():
- career = CommonCareerUtils.load_career_by_guid(career_guid)
+ career = S4APCareerUtils.load_career_by_guid(career_guid)
for i in range(1, level + 1):
- (_, _, career_track) = CommonCareerUtils.determine_entry_level_into_career_from_user_level(career, i)
+ (_, _, career_track) = S4APCareerUtils.determine_entry_level_into_career_from_user_level(career, i)
career_hash = career_track.get_career_name(sim_info).hash
career_name = lookup.get_career_name(career_hash, i)
if career_name is not None:
@@ -148,25 +147,76 @@ def _resync_locations(event_data: S4CLSimTraitAddedEvent):
locations.append(milestone_display_name)
print_json(locations, 'locations_cached.json')
print_json(True, 'sync.json')
- notif = CommonBasicNotification(
+ DialogHelper.create_notification(
'Locations Resynced',
''
- )
- notif.show()
+ ).show_dialog()
@CommonEventRegistry.handle_events(ModInfo.get_identity())
def _show_aspiration_and_career(event_data: S4CLSimTraitAddedEvent):
if event_data.trait_id == S4APTraitId.SHOW_YAML_OPTIONS:
- CommonTraitUtils.remove_trait(event_data.sim_info, S4APTraitId.SHOW_YAML_OPTIONS)
+ yaml_options_trait_instance = TunableInstanceParam(Types.TRAIT)(S4APTraitId.SHOW_YAML_OPTIONS)
+ if event_data.sim_info.has_trait(yaml_options_trait_instance):
+ event_data.sim_info.remove_trait(yaml_options_trait_instance)
data_store = S4APSessionStoreUtils()
if data_store.get_goal() is not None:
goal = data_store.get_goal()
else:
- goal = 'Cant find the aspiration'
- if data_store.get_career() is not None:
- career = data_store.get_career()
- else:
- career = 'Cant find the career'
+ goal = "Can't find the aspiration"
+
+ options = [
+ S4APDialog.ObjectPickerDialog.create_picker_row(1, LocalizationHelperTuning.get_raw_text(goal.replace("_", " ").title()), 1903793975082081275),
+ ]
+
+ row_id = 2
+
+ # ---------- Careers Header ----------
+ options.append(S4APDialog.ObjectPickerDialog.create_picker_row(
+ row_id,
+ LocalizationHelperTuning.get_raw_text("──────── Careers ────────"),
+ 0
+ ))
+ row_id += 1
+
+ # ---------- Careers ----------
+ career_data = data_store.get_career()
+
+ if career_data:
+ if isinstance(career_data, list):
+ for career in career_data: # list with items in it
+ options.append(
+ ObjectPickerRow(
+ option_id=row_id,
+ name=LocalizationHelperTuning.get_raw_text(career.replace("_", " ").title()),
+ icon=12028399282094277793)
+ )
+ row_id += 1
+ elif isinstance(career_data, str):
+ # Legacy support for string career data
+ options.append(
+ ObjectPickerRow(
+ option_id=row_id,
+ name=LocalizationHelperTuning.get_raw_text(career_data.replace("_", " ").title()),
+ icon=12028399282094277793)
+ )
+ row_id += 1
+ else: # none or empty list
+ options.append(
+ ObjectPickerRow(
+ option_id=row_id,
+ name=LocalizationHelperTuning.get_raw_text("Can't find the career"),
+ icon=12028399282094277793)
+ )
+ row_id += 1
+
+ # ---------- Skill Multiplier ----------
+ options.append(ObjectPickerRow(
+ option_id=row_id,
+ name=LocalizationHelperTuning.get_raw_text("──────── Skill Bonus ────────"),
+ icon=0
+ ))
+ row_id += 1
+
if data_store.get_items() is not None:
item = 'Skill Gain Multiplier'
if data_store.get_items().count(item) is not None:
@@ -185,30 +235,20 @@ def _show_aspiration_and_career(event_data: S4CLSimTraitAddedEvent):
display = 'No Skill Multiplier'
else:
display = 'No Skill Multiplier'
- def _on_chosen(_, outcome: CommonChoiceOutcome):
- if outcome == CommonChoiceOutcome.CHOICE_MADE:
- dialog.show(on_chosen=_on_chosen)
- options = [
- ObjectPickerRow(
- option_id=1,
- name= CommonLocalizationUtils.create_localized_string(goal.replace("_", " ").title()),
- icon= CommonIconUtils.load_icon_by_id(1903793975082081275)
- ),
- ObjectPickerRow(
- option_id=2,
- name= CommonLocalizationUtils.create_localized_string(career.replace("_", " ").title()),
- icon= CommonIconUtils.load_icon_by_id(12028399282094277793)
- ),
- ObjectPickerRow(
- option_id=3,
- name= CommonLocalizationUtils.create_localized_string(display),
- icon= CommonIconUtils.load_icon_by_id(5906963266871873908)
- )
- ]
- dialog = CommonChooseObjectDialog(
- 'Your Yaml Options Plus Skill Multiplier',
- 'Options + Skill Multiplier',
- choices=options
+ options.append(
+ ObjectPickerRow(option_id=row_id, name=LocalizationHelperTuning.get_raw_text(display), icon=5906963266871873908)
)
- dialog.show(on_chosen=_on_chosen)
+
+ # ---------- Show Picker ----------
+
+ sim = event_data.sim_info.get_sim_instance()
+
+ picker = S4APDialog.ObjectPickerDialog(
+ sim=sim,
+ title='Your Yaml Options Plus Skill Multiplier',
+ text='Options + Skill Multiplier',
+ picker_rows=options
+ )
+
+ picker.show_dialog()
diff --git a/Scripts/s4ap/utils/s4ap_reset_utils.py b/Scripts/s4ap/utils/s4ap_reset_utils.py
index e7eb089..fc58955 100644
--- a/Scripts/s4ap/utils/s4ap_reset_utils.py
+++ b/Scripts/s4ap/utils/s4ap_reset_utils.py
@@ -1,26 +1,29 @@
+from lot51_core.utils.dialog import DialogHelper
from s4ap.enums.S4APLocalization import S4APTraitId
from s4ap.logging.s4ap_logger import S4APLogger
+from s4ap.utils.s4ap_generic_utils import S4APUtils
+from s4ap.utils.s4ap_household_utils import S4APHouseholdUtils
+from s4ap.utils.s4ap_skill_utils_class import S4APSkillUtils
+from server_commands.argument_helpers import TunableInstanceParam
+from sims4.localization import LocalizationHelperTuning
+from sims4.resources import Types
from sims4communitylib.enums.traits_enum import CommonTraitId
-from sims4communitylib.notifications.common_basic_notification import CommonBasicNotification
-from sims4communitylib.utils.sims.common_household_utils import CommonHouseholdUtils
-from sims4communitylib.utils.sims.common_sim_skill_utils import CommonSimSkillUtils
-from sims4communitylib.utils.sims.common_trait_utils import CommonTraitUtils
+from ui.ui_dialog_notification import UiDialogNotification
logger = S4APLogger.get_log()
logger.enable()
class ResetSimData:
def reset_all_skills(self):
- for sim_info in CommonHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
- for skill in CommonSimSkillUtils.get_all_skills_available_for_sim_gen(sim_info):
- CommonSimSkillUtils.remove_skill(sim_info, skill)
+ for sim_info in S4APHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
+ for skill in S4APSkillUtils.get_all_skills_available_for_sim_gen(sim_info):
+ sim_info.remove_statistic(skill)
def show_reset_notif(self):
- notif = CommonBasicNotification(
+ DialogHelper.create_notification(
'Progress Reset Completed',
"Your Sim's skills have been successfully reset. Please switch to a different sim or leave the lot and revisit to ensure the changes are visible in the UI."
- )
- notif.show()
+ ).show_dialog()
def remove_all_s4ap_traits(self):
# Get all traits from the base class CommonTraitId
@@ -29,5 +32,7 @@ def remove_all_s4ap_traits(self):
# Check if the trait is not a built-in attribute and is unique to S4APTraitId
if not trait.startswith("_") and trait not in common_trait_ids:
logger.debug(f"Removing trait {trait}: {trait_value}")
- for sim_info in CommonHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
- CommonTraitUtils.remove_trait(sim_info, trait_value)
+ trait_instance = TunableInstanceParam(Types.TRAIT)(trait_value)
+ for sim_info in S4APHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
+ if sim_info.has_trait(trait_instance):
+ sim_info.remove_trait(trait_instance)
diff --git a/Scripts/s4ap/utils/s4ap_save_utils.py b/Scripts/s4ap/utils/s4ap_save_utils.py
new file mode 100644
index 0000000..2aaade5
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_save_utils.py
@@ -0,0 +1,33 @@
+from typing import Any, Union
+
+import services
+
+class S4APSaveUtils:
+
+ @staticmethod
+ def get_save_slot() -> Any:
+ """get_save_slot()
+
+ Retrieve the current save slot.
+
+ :return: The current save slot.
+ :return: Union[int, None]
+ """
+ persistence_service = services.get_persistence_service()
+ if persistence_service is None:
+ return None
+ return persistence_service.get_save_slot_proto_buff()
+
+ @staticmethod
+ def get_save_slot_id() -> Union[int, None]:
+ """get_save_slot_id()
+
+ Retrieve the identifier for the current save slot.
+
+ :return: The identifier for the current save slot.
+ :return: int
+ """
+ save_slot = S4APSaveUtils.get_save_slot()
+ if save_slot is None:
+ return None
+ return save_slot.slot_id
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_sim_currency_utils.py b/Scripts/s4ap/utils/s4ap_sim_currency_utils.py
new file mode 100644
index 0000000..cf6e19e
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_sim_currency_utils.py
@@ -0,0 +1,23 @@
+from sims.sim_info import SimInfo
+
+class S4APSimCurrencyUtils:
+
+ @classmethod
+ def add_simoleons_to_household(cls, sim_info: SimInfo, amount: int, reason: int, **kwargs) -> bool:
+ """
+ Add an amount of simoleons to the Household of a Sim.
+
+ :param sim_info: The Sim whose household to modify.
+ :param amount: The number of simoleons to add (negative values will subtract).
+ :param reason: A string reason for the funds change (from Consts_pb2).
+ :return: True if successful, False otherwise.
+ """
+ if sim_info is None or sim_info.household is None:
+ return False
+
+ funds = sim_info.household.funds
+ if funds is None:
+ return False
+
+ funds.add(amount, reason, **kwargs)
+ return True
diff --git a/Scripts/s4ap/utils/s4ap_sim_utils.py b/Scripts/s4ap/utils/s4ap_sim_utils.py
new file mode 100644
index 0000000..9dead04
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_sim_utils.py
@@ -0,0 +1,70 @@
+import services
+from typing import Union
+from s4ap.utils.s4ap_game_client_utils import S4APGameClientUtils
+from sims.sim import Sim
+from sims.sim_info import SimInfo
+from sims.sim_info_base_wrapper import SimInfoBaseWrapper
+from sims.sim_info_manager import SimInfoManager
+
+class S4APSimUtils:
+
+ @staticmethod
+ def get_sim_first_name(sim_info: SimInfo):
+ if sim_info is None or not hasattr(sim_info, 'first_name'):
+ return ''
+ return getattr(sim_info, 'first_name')
+
+ @classmethod
+ def get_sim_instance(cls, sim_identifier: SimInfo):
+ if isinstance(sim_identifier, SimInfo):
+ return sim_identifier.get_sim_instance()
+
+ @classmethod
+ def get_sim_info(
+ cls,
+ sim_identifier: Union[int, Sim, SimInfo, SimInfoBaseWrapper]
+ ) -> Union[SimInfo, SimInfoBaseWrapper, None]:
+ """get_sim_info(sim_identifier)
+
+ Retrieve a SimInfo instance from a Sim identifier.
+
+ :param sim_identifier: The identifier or instance of a Sim to use.
+ :type sim_identifier: Union[int, Sim, SimInfo, SimInfoBaseWrapper]
+ :return: The SimInfo of the specified Sim instance or None if SimInfo is not found.
+ :rtype: Union[SimInfo, SimInfoBaseWrapper, None]
+ """
+ if sim_identifier is None or isinstance(sim_identifier, SimInfo):
+ return sim_identifier
+ if isinstance(sim_identifier, SimInfoBaseWrapper):
+ return sim_identifier.get_sim_info()
+ if isinstance(sim_identifier, Sim):
+ return sim_identifier.sim_info
+ if isinstance(sim_identifier, int):
+ return cls.get_sim_info_manager().get(sim_identifier)
+ return sim_identifier
+
+ @classmethod
+ def get_sim_info_manager(cls) -> SimInfoManager:
+ """get_sim_info_manager()
+
+ Retrieve the manager that manages the Sim Info of all Sims in a game world.
+
+ :return: The manager that manages the Sim Info of all Sims in a game world.
+ :rtype: SimInfoManager
+ """
+ return services.sim_info_manager()
+
+ @classmethod
+ def get_active_sim_info(cls) -> Union[SimInfo, None]:
+ """get_active_sim_info()
+
+ Retrieve a SimInfo object of the Currently Active Sim.
+
+ :return: The SimInfo of the Active Sim or None if not found.
+ :rtype: Union[SimInfo, None]
+ """
+ client = S4APGameClientUtils.get_first_game_client()
+ if client is None:
+ return None
+ # noinspection PyPropertyAccess
+ return client.active_sim_info
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_skill_utils.py b/Scripts/s4ap/utils/s4ap_skill_utils.py
index ba09d56..3a61544 100644
--- a/Scripts/s4ap/utils/s4ap_skill_utils.py
+++ b/Scripts/s4ap/utils/s4ap_skill_utils.py
@@ -1,95 +1,97 @@
-import re
-
-from s4ap.enums.S4APLocalization import S4APTraitId
-from s4ap.events.skill_event_dispatcher import SimSkillLeveledUpEvent
-from s4ap.logging.s4ap_logger import S4APLogger
-from s4ap.modinfo import ModInfo
-from s4ap.persistance.ap_session_data_store import S4APSessionStoreUtils
-from server_commands.argument_helpers import TunableInstanceParam
-from sims4.resources import Types
-from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
-from sims4communitylib.utils.sims.common_household_utils import CommonHouseholdUtils
-from sims4communitylib.utils.sims.common_sim_name_utils import CommonSimNameUtils
-from sims4communitylib.utils.sims.common_sim_skill_utils import CommonSimSkillUtils
-from sims4communitylib.utils.sims.common_trait_utils import CommonTraitUtils
-
-logger = S4APLogger.get_log()
-logger.enable()
-
-
-def lock_skills(skillcap: int, skill_name, from_level_up: bool):
- try:
- logger.debug(f"Skill cap is {skillcap}")
- data_store = S4APSessionStoreUtils()
- if skillcap < 2:
- skillcap = 2
- if not skill_name.startswith("statistic_Skill_AdultMajor_") and not 'fitness' in skill_name.lower():
- skill_name = f"statistic_Skill_AdultMajor_{skill_name}"
- logger.debug(f'{skill_name}')
- skill_id = skill_name.replace("statistic_Skill_AdultMajor_", '')
- skill_id = re.sub(r'(?<=[a-z])(?=[A-Z])', '_', skill_id)
- if 'bartending' in skill_id.lower():
- skill_id = skill_id.lower().replace('bartending', 'mixology')
- if from_level_up is True and data_store.get_items() is not None:
- skill_id_lower = skill_id.replace("_", " ").strip().lower()
- item_name = skill_id_lower
- if 'fitness' in skill_id_lower:
- item_name = skill_id_lower.replace('skill', '').strip()
- elif 'homestyle' in skill_id_lower:
- item_name = skill_id_lower.replace('homestyle', '').strip()
- elif 'gourmet' in skill_id_lower:
- item_name = skill_id_lower.replace("cooking", "").strip()
- elif 'bartending' in skill_id_lower:
- item_name = skill_id_lower.replace('bartending', 'mixology').strip()
- logger.debug(f"item_name: {item_name}")
- for item in data_store.get_items():
- if item_name in item.lower():
- skillcap = data_store.get_items().count(item) + 2
- logger.debug(f"new skillcap: {skillcap}")
- break
- else:
- continue
- trait = f"lock_{skill_id.lower().replace('skill_', '')}_skill"
- if 'gourmet' in trait:
- trait = "lock_gourmet_cooking_skill"
- logger.debug(f"Skill Id: {skill_id}")
- logger.debug(f"Trait: {trait}")
- skill = TunableInstanceParam(Types.STATISTIC)(skill_name)
- for sim_info in CommonHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
- current_level = CommonSimSkillUtils.get_current_skill_level(sim_info, skill, False)
- logger.debug(f"{CommonSimNameUtils.get_first_name(sim_info)}'s Current level is {current_level}.")
- if skillcap > current_level:
- logger.debug('Skill cap is > than current level')
- remove_lock_trait(sim_info, trait)
- elif skillcap == current_level:
- logger.debug('Skill cap is == as current level')
- add_lock_trait(sim_info, trait)
- elif skillcap < current_level:
- logger.debug('Skill cap is < than current level')
- CommonSimSkillUtils.set_current_skill_level(sim_info, skill, skillcap)
- add_lock_trait(sim_info, trait)
- except Exception as ex:
- logger.debug(f"Exception occurred: {ex}")
-
-def add_lock_trait(sim_info, trait):
- trait_upper = trait.upper()
- if hasattr(S4APTraitId, trait_upper):
- trait_id = getattr(S4APTraitId, trait_upper)
- CommonTraitUtils.add_trait(sim_info, trait_id)
- logger.debug(trait_id)
- logger.debug(trait_upper)
-
-
-def remove_lock_trait(sim_info, trait):
- trait_upper = trait.upper()
- if hasattr(S4APTraitId, trait_upper):
- trait_id = getattr(S4APTraitId, trait_upper)
- CommonTraitUtils.remove_trait(sim_info, trait_id)
- logger.debug(trait_id)
- logger.debug(trait_upper)
-
-
-@CommonEventRegistry.handle_events(ModInfo.get_identity())
-def _lock_on_level_up(event_data: SimSkillLeveledUpEvent):
- skill_name = event_data.skill.skill_type.__name__
- lock_skills(event_data.new_skill_level, skill_name, True)
+import re
+
+from s4ap.enums.S4APLocalization import S4APTraitId
+from s4ap.events.skill_events import SimSkillLeveledUpEvent
+from s4ap.logging.s4ap_logger import S4APLogger
+from s4ap.modinfo import ModInfo
+from s4ap.persistance.ap_session_data_store import S4APSessionStoreUtils
+from s4ap.utils.s4ap_household_utils import S4APHouseholdUtils
+from s4ap.utils.s4ap_sim_utils import S4APSimUtils
+from server_commands.argument_helpers import TunableInstanceParam
+from sims4.resources import Types
+from sims4communitylib.events.event_handling.common_event_registry import CommonEventRegistry
+from s4ap.utils.s4ap_skill_utils_class import S4APSkillUtils
+
+logger = S4APLogger.get_log()
+logger.enable()
+
+def lock_skills(skillcap: int, skill_name, from_level_up: bool):
+ try:
+ logger.debug(f"Processing level up for {skill_name}")
+ logger.debug(f"Skill cap is {skillcap}")
+ data_store = S4APSessionStoreUtils()
+ if skillcap < 2:
+ skillcap = 2
+ if not skill_name.startswith("statistic_Skill_AdultMajor_") and not 'fitness' in skill_name.lower():
+ skill_name = f"statistic_Skill_AdultMajor_{skill_name}"
+ skill_id = skill_name.replace("statistic_Skill_AdultMajor_", '')
+ skill_id = re.sub(r'(?<=[a-z])(?=[A-Z])', '_', skill_id)
+ if 'bartending' in skill_id.lower():
+ skill_id = skill_id.lower().replace('bartending', 'mixology')
+ if from_level_up is True and data_store.get_items() is not None:
+ skill_id_lower = skill_id.replace("_", " ").strip().lower()
+ item_name = skill_id_lower
+ if 'fitness' in skill_id_lower:
+ item_name = skill_id_lower.replace('skill', '').strip()
+ elif 'homestyle' in skill_id_lower:
+ item_name = skill_id_lower.replace('homestyle', '').strip()
+ elif 'gourmet' in skill_id_lower:
+ item_name = skill_id_lower.replace("cooking", "").strip()
+ elif 'bartending' in skill_id_lower:
+ item_name = skill_id_lower.replace('bartending', 'mixology').strip()
+ logger.debug(f"item_name: {item_name}")
+ for item in data_store.get_items():
+ if item_name in item.lower():
+ skillcap = data_store.get_items().count(item) + 2
+ logger.debug(f"new skillcap: {skillcap}")
+ break
+ else:
+ continue
+ trait = f"lock_{skill_id.lower().replace('skill_', '')}_skill"
+ if 'gourmet' in trait:
+ trait = "lock_gourmet_cooking_skill"
+ logger.debug(f"Skill Id: {skill_id}")
+ logger.debug(f"Trait: {trait}")
+ skill = TunableInstanceParam(Types.STATISTIC)(skill_name)
+ for sim_info in S4APHouseholdUtils.get_sim_info_of_all_sims_in_active_household_generator():
+ current_level = S4APSkillUtils.get_current_skill_level(sim_info, skill)
+ logger.debug(f"{S4APSimUtils.get_sim_first_name(sim_info)}'s Current {skill_id} level is {current_level}.")
+ if skillcap > current_level:
+ logger.debug(f"{skill_id} skill cap is greater than current level, unlocking skill.")
+ remove_lock_trait(sim_info, trait)
+ elif skillcap == current_level:
+ logger.debug(f"{skill_id} skill cap is the same as the current level, locking skill.")
+ add_lock_trait(sim_info, trait)
+ elif skillcap < current_level:
+ logger.debug(f"{skill_id} skill cap is less than current level, locking skill and setting skill level to {skillcap}")
+ S4APSkillUtils.set_current_skill_level(sim_info, skill, skillcap)
+ add_lock_trait(sim_info, trait)
+ except Exception as ex:
+ logger.debug(f"Exception occurred: {ex}")
+
+def add_lock_trait(sim_info, trait):
+ trait_upper = trait.upper()
+ if hasattr(S4APTraitId, trait_upper):
+ trait_id = getattr(S4APTraitId, trait_upper)
+ trait_instance = TunableInstanceParam(Types.TRAIT)(trait_id)
+ if not sim_info.has_trait(trait_instance):
+ sim_info.add_trait(trait_instance)
+ logger.debug(trait_id)
+ logger.debug(trait_upper)
+
+
+def remove_lock_trait(sim_info, trait):
+ trait_upper = trait.upper()
+ if hasattr(S4APTraitId, trait_upper):
+ trait_id = getattr(S4APTraitId, trait_upper)
+ trait_instance = TunableInstanceParam(Types.TRAIT)(trait_id)
+ if sim_info.has_trait(trait_instance):
+ sim_info.remove_trait(trait_instance)
+ logger.debug(trait_id)
+ logger.debug(trait_upper)
+
+
+@CommonEventRegistry.handle_events(ModInfo.get_identity())
+def _lock_on_level_up(event_data: SimSkillLeveledUpEvent):
+ skill_name = event_data.skill.skill_type.__name__
+ lock_skills(event_data.new_skill_level, skill_name, True)
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_skill_utils_class.py b/Scripts/s4ap/utils/s4ap_skill_utils_class.py
new file mode 100644
index 0000000..101df7e
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_skill_utils_class.py
@@ -0,0 +1,64 @@
+import services
+from typing import Callable, Iterator, Union
+from s4ap.utils.s4ap_sim_utils import S4APSimUtils
+from server_commands.argument_helpers import TunableInstanceParam
+from sims.sim_info import SimInfo
+from sims4.resources import Types
+from statistics.skill import Skill
+
+class S4APSkillUtils:
+
+ @staticmethod
+ def get_current_skill_level(sim_info: SimInfo, skill: TunableInstanceParam(Types.STATISTIC)) -> float:
+ skill_stat = sim_info.get_statistic(skill, add=False)
+ if skill_stat is None:
+ return 0.0
+ skill_level: float = skill_stat.get_user_value()
+ return skill_level
+
+ @staticmethod
+ def set_current_skill_level(sim_info: SimInfo, skill: TunableInstanceParam(Types.STATISTIC), level: float) -> bool:
+ skill_stat = sim_info.get_statistic(skill, add=False)
+ if skill_stat is None:
+ return False
+ exp = skill_stat.convert_from_user_value(level)
+ skill_stat.set_value(exp)
+ return True
+
+ @staticmethod
+ def get_all_skills_available_for_sim_gen(sim_info: SimInfo) -> Iterator[Skill]:
+ sim = S4APSimUtils.get_sim_instance(sim_info)
+ if sim is None:
+ return tuple()
+
+ def _is_skill_available_for_sim(skill: Skill) -> bool:
+ return skill.can_add(sim)
+
+ yield from S4APSkillUtils.get_all_skills_gen(include_skill_callback=_is_skill_available_for_sim)
+
+ @staticmethod
+ def get_all_skills_gen(include_skill_callback: Callable[[Skill], bool] = None) -> Iterator[Skill]:
+ statistic_manager = services.get_instance_manager(Types.STATISTIC)
+ for skill in statistic_manager.get_ordered_types(only_subclasses_of=Skill):
+ skill: Skill = skill
+ skill_id = S4APSkillUtils.get_skill_id(skill)
+ if skill_id is None:
+ continue
+ if include_skill_callback is not None and not include_skill_callback(skill):
+ continue
+ yield skill
+
+ @staticmethod
+ def get_skill_id(skill_identifier: Union[int, Skill]) -> Union[int, None]:
+ """get_skill_id(skill_identifier)
+
+ Retrieve the decimal identifier of a Skill.
+
+ :param skill_identifier: The identifier or instance of a Skill.
+ :type skill_identifier: Union[int, Skill]
+ :return: The decimal identifier of the Skill or None if the Skill does not have an id.
+ :rtype: Union[int, None]
+ """
+ if isinstance(skill_identifier, int):
+ return skill_identifier
+ return getattr(skill_identifier, 'guid64', None)
\ No newline at end of file
diff --git a/Scripts/s4ap/utils/s4ap_trait_utils.py b/Scripts/s4ap/utils/s4ap_trait_utils.py
new file mode 100644
index 0000000..8ff23a3
--- /dev/null
+++ b/Scripts/s4ap/utils/s4ap_trait_utils.py
@@ -0,0 +1,41 @@
+from typing import Iterator, Optional, Union
+from s4ap.utils.s4ap_generic_utils import S4APUtils
+from sims.sim_info import SimInfo
+from sims4.resources import Types
+from traits.traits import Trait
+
+class S4APTraitUtils:
+
+ @classmethod
+ def remove_traits(cls, sim_info: SimInfo, traits: Iterator[Union[int, Trait]]) -> bool:
+ """Remove traits from a Sim using only vanilla TS4 API.
+
+ :param sim_info: The Sim to remove the traits from.
+ :param traits: An iterator of Trait instances or trait IDs.
+ :return: True if all traits were successfully removed, False otherwise.
+ """
+ all_removed = True
+
+ for trait_item in traits:
+ trait = cls.load_trait_by_id(trait_item)
+ if trait is None:
+ all_removed = False
+ continue
+
+ if sim_info.has_trait(trait):
+ if not sim_info.remove_trait(trait):
+ all_removed = False
+
+ return all_removed
+
+ @classmethod
+ def load_trait_by_id(cls, trait) -> Optional['Trait']:
+ if isinstance(trait, Trait):
+ return trait
+
+ try:
+ trait_id = int(trait)
+ except (TypeError, ValueError):
+ return None
+
+ return S4APUtils.load_instance(Types.TRAIT, trait_id)
\ No newline at end of file