// Lot STE, fiches ORG5-I1 et ORG5-I2a (affichage) - voir LPBonusRewardSubsystem.h. // Origine : comportement reconstruit d'apres LifePlay 2023.04 de Vinfamy. Licence MIT // (SPDX-License-Identifier: MIT), texte de reference : Licenses/Vinfamy_License.txt. #include "LPBonusRewardSubsystem.h" #include "Blueprint/UserWidget.h" #include "Blueprint/WidgetTree.h" #include "Components/CanvasPanelSlot.h" #include "Components/PanelWidget.h" #include "Components/TextBlock.h" #include "Components/VerticalBox.h" #include "Components/VerticalBoxSlot.h" #include "Engine/GameInstance.h" #include "HAL/PlatformTime.h" #include "Kismet/KismetMathLibrary.h" #include "LifePlay.h" #include "Styling/CoreStyle.h" #include "UObject/TextProperty.h" #include "UObject/UnrealType.h" namespace LPBonusReward { constexpr float PollSeconds = 0.1f; // sondage H1 (STE-ORG5-SPEC.md 2.2) constexpr double DebounceSeconds = 0.2; // RetriggerableDelay(0.2) de l'auteur (SPEC 1.1 E3) constexpr int32 CandidateCount = 10; // Array_Resize(10) apres Array_Shuffle (SPEC 1.1 E5) constexpr int32 MaxRewards = 3; // NotEqual_IntInt(countBR, 3) (SPEC 1.1 E6e) const TCHAR* const AttractionKey = TEXT("attractiontoplayer"); // Affichage (fiche ORG5-I2a, STE-ORG5-I2-SPEC.md 2.2 et decisions de l'utilisateur). constexpr double RewardLineSeconds = 20.0; // Q-I2-2 : chaque ligne retiree 20 s apres son affichage constexpr int32 MaxRewardLines = 1; // V-min : une ligne, remplacee a la recompense suivante (SPEC 2.2, V6-3) constexpr double EquipMenuMinHeight = 500.0; // Q-I2-3 : hauteur d'EquipMenu chez l'auteur (SPEC 1.1 A3) constexpr float RewardFontSize = 16.f; // RewardText de l'auteur (SPEC 1.1 A5) constexpr int32 RewardOutlineSize = 2; // idem, contour noir alpha 0,8 const TCHAR* const CursorProp = TEXT("Cursor"); const TCHAR* const EquipMenuName = TEXT("EquipMenu"); const TCHAR* const ActionTextName = TEXT("ActionText"); const TCHAR* const RewardBoxName = TEXT("LPBonusRewardBox"); // Retire une ligne de l'ecran si elle existe encore (pas d'acces apres destruction). void RemoveLine(const FLPBonusRewardLine& Entry) { UTextBlock* Line = Entry.Line.Get(); if (IsValid(Line)) { Line->RemoveFromParent(); } } // Police de la ligne : copiee d'ActionText (texte voisin dans EquipMenu, SPEC 1.4 N1), comme // LPColorGradeSubsystem.cpp:295, sinon police par defaut de Slate ; puis taille 16 et contour 2 noir 0,8 de // l'auteur (SPEC 1.1 A5). FSlateFontInfo::Size et OutlineSettings : SlateFontInfo.h:175, :160 ; // FFontOutlineSettings(int32, FLinearColor) : SlateFontInfo.h:78. FSlateFontInfo RewardFont(const UUserWidget* Cursor) { const UTextBlock* Sample = Cast(Cursor->GetWidgetFromName(FName(ActionTextName))); FSlateFontInfo Font = Sample ? Sample->GetFont() : FCoreStyle::GetDefaultFontStyle("Regular", RewardFontSize); Font.Size = RewardFontSize; Font.OutlineSettings = FFontOutlineSettings(RewardOutlineSize, FLinearColor(0.f, 0.f, 0.f, 0.8f)); return Font; } FProperty* FindProp(const UObject* Owner, const TCHAR* Name) { return Owner ? Owner->GetClass()->FindPropertyByName(FName(Name)) : nullptr; } // Parametre d'une fonction Blueprint, par nom (comme LPDirectControlSubsystem.cpp:375-385). FProperty* FindParm(UFunction* Function, const TCHAR* Name) { for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { if (It->GetAuthoredName() == Name || It->GetName() == Name) { return *It; } } return nullptr; } // Liste lisible des parametres, pour l'avertissement unique quand la signature n'est pas celle attendue. FString DescribeParms(UFunction* Function) { FString Out; for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { if (!Out.IsEmpty()) { Out += TEXT(", "); } Out += FString::Printf(TEXT("%s (%s%s)"), *It->GetAuthoredName(), *It->GetClass()->GetName(), It->HasAnyPropertyFlags(CPF_OutParm) ? TEXT(", sortie") : TEXT("")); } return Out; } // Reel Blueprint : double en 5.x (FDoubleProperty), float accepte aussi (comme LPUserSettings.cpp:32-55). bool IsReal(const FProperty* Prop) { return CastField(Prop) || CastField(Prop); } double ReadReal(const FProperty* Prop, const void* Container) { if (const FDoubleProperty* AsDouble = CastField(Prop)) { return AsDouble->GetPropertyValue_InContainer(Container); } if (const FFloatProperty* AsFloat = CastField(Prop)) { return static_cast(AsFloat->GetPropertyValue_InContainer(Container)); } return 0.0; } void WriteReal(const FProperty* Prop, void* Container, double Value) { if (const FDoubleProperty* AsDouble = CastField(Prop)) { AsDouble->SetPropertyValue_InContainer(Container, Value); } else if (const FFloatProperty* AsFloat = CastField(Prop)) { AsFloat->SetPropertyValue_InContainer(Container, static_cast(Value)); } } // Booleen d'entree d'une fonction : present et booleen, sinon faux (l'appelant avertit et abandonne). bool SetBoolParm(UFunction* Function, void* Parms, const TCHAR* Name, bool bValue) { const FBoolProperty* Prop = CastField(FindParm(Function, Name)); if (!Prop) { return false; } Prop->SetPropertyValue_InContainer(Parms, bValue); return true; } // Parametres d'un appel par ProcessEvent : memoire fournie par l'appelant (FMemory_Alloca_Aligned, qui doit rester // dans le cadre de l'appelant), mise a zero et initialisee ici, detruite a la sortie de portee // (meme sequence que LPDirectControlSubsystem.cpp:426-455). struct FScopedParms { UFunction* Function; uint8* Memory; FScopedParms(UFunction* InFunction, uint8* InMemory) : Function(InFunction) , Memory(InMemory) { FMemory::Memzero(Memory, Function->ParmsSize); for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { It->InitializeValue_InContainer(Memory); } } ~FScopedParms() { for (TFieldIterator It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) { It->DestroyValue_InContainer(Memory); } } FScopedParms(const FScopedParms&) = delete; FScopedParms& operator=(const FScopedParms&) = delete; }; // Proprietes lues par reflexion. Faux : propriete absente ou d'un autre type (l'appelant avertit). bool ReadObject(const UObject* Owner, const TCHAR* Name, UObject*& Out) { Out = nullptr; const FObjectPropertyBase* Prop = CastField(FindProp(Owner, Name)); if (!Prop) { return false; } Out = Prop->GetObjectPropertyValue_InContainer(Owner); return true; } // Tableau d'objets (comme LPShowCoupleSubsystem.cpp:80-96). bool ReadObjects(const UObject* Owner, const TCHAR* Name, TArray& Out) { Out.Reset(); const FArrayProperty* ArrayProp = CastField(FindProp(Owner, Name)); const FObjectPropertyBase* Inner = ArrayProp ? CastField(ArrayProp->Inner) : nullptr; if (!Inner) { return false; } FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Owner)); for (int32 Index = 0; Index < Helper.Num(); ++Index) { Out.Add(Inner->GetObjectPropertyValue(Helper.GetRawPtr(Index))); } return true; } // Tableau de chaines (GI.CurrentTags). D2 (STE-ORG5-V1-VAL.md) : type reel non lu, elements chaine, nom ou texte // acceptes et convertis en chaines. bool ReadStrings(const UObject* Owner, const TCHAR* Name, TArray& Out) { Out.Reset(); const FArrayProperty* ArrayProp = CastField(FindProp(Owner, Name)); if (!ArrayProp) { return false; } const FStrProperty* AsStr = CastField(ArrayProp->Inner); const FNameProperty* AsName = CastField(ArrayProp->Inner); const FTextProperty* AsText = CastField(ArrayProp->Inner); if (!AsStr && !AsName && !AsText) { return false; } FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(Owner)); Out.Reserve(Helper.Num()); for (int32 Index = 0; Index < Helper.Num(); ++Index) { const void* Item = Helper.GetRawPtr(Index); if (AsStr) { Out.Add(AsStr->GetPropertyValue(Item)); } else if (AsName) { Out.Add(AsName->GetPropertyValue(Item).ToString()); } else { Out.Add(AsText->GetPropertyValue(Item).ToString()); } } return true; } } void ULPBonusRewardSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); Rng.GenerateNewSeed(); FString Failure; if (CheckFormula(Failure)) { UE_LOG(LogLifePlay, Log, TEXT("Bonus : contrôle formule OK")); } else { UE_LOG(LogLifePlay, Error, TEXT("Bonus : contrôle formule en erreur, %s"), *Failure); } UE_LOG(LogLifePlay, Log, TEXT("Bonus : réglage bDisabled = %s ([/Script/LifePlay.LPBonusRewardSettings], config Game)"), GetDefault()->bDisabled ? TEXT("vrai") : TEXT("faux")); TickHandle = FTSTicker::GetCoreTicker().AddTicker( FTickerDelegate::CreateUObject(this, &ULPBonusRewardSubsystem::Tick), LPBonusReward::PollSeconds); } void ULPBonusRewardSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); Pending.Reset(); LastCounts.Reset(); // Garde 3 : minuteries des lignes videes ; les widgets ne sont pas touches ici (ils partent avec le monde et le // GameInstance en cours de fermeture). ClearRewardDisplay(false); RewardBox.Reset(); Super::Deinitialize(); } const TArray& ULPBonusRewardSubsystem::Table() { // STE-ORG5-SPEC.md section 1.2 (valeur par defaut de Cursor.statTags chez l'auteur), meme ordre. static const TArray Stats = { { TEXT("perversion"), {} }, { TEXT("masochist"), { TEXT("Aggressive"), TEXT("AggressiveFM") } }, { TEXT("prone_to_orgasm"), {} }, { TEXT("attractiontoplayer"), {} }, { TEXT("fitness"), { TEXT("Penetration"), TEXT("Foreplay"), TEXT("Cum") } }, { TEXT("likes_anal"), { TEXT("Anal") } }, { TEXT("likes_boobjob"), { TEXT("Titfuck"), TEXT("Boobjob") } }, { TEXT("likes_cowgirl"), { TEXT("Cow"), TEXT("Cowgirl") } }, { TEXT("likes_doggy"), { TEXT("Doggy"), TEXT("Doggystyle") } }, { TEXT("likes_handjob"), { TEXT("Handjob"), TEXT("RubPussy"), TEXT("FingerPussy") } }, { TEXT("likes_missionary"), { TEXT("Missionary") } }, { TEXT("likes_oral"), { TEXT("Oral"), TEXT("Blowjob"), TEXT("Cunnilingus"), TEXT("LickPussy"), TEXT("LickAss"), TEXT("Rimjob") } }, { TEXT("likes_rcowgirl"), { TEXT("ReverseCow"), TEXT("ReverseCowgirl") } }, { TEXT("likes_rough"), { TEXT("Aggressive"), TEXT("AggressiveFM") } }, { TEXT("likes_sidefuck"), { TEXT("Spooning"), TEXT("Sidefuck"), TEXT("Spoon") } }, { TEXT("likes_toys"), { TEXT("Toys"), TEXT("Dildo"), TEXT("Vibrator") } }, { TEXT("likes_tribadism"), { TEXT("Tribadism"), TEXT("Scissor"), TEXT("Scissoring") } }, { TEXT("likes_vaginal"), { TEXT("Vaginal") } }, { TEXT("masochist-"), { TEXT("Aggressive"), TEXT("AggressiveFM") } }, { TEXT("prone_to_orgasm-"), { TEXT("Penetration"), TEXT("Foreplay"), TEXT("Cum") } }, }; return Stats; } double ULPBonusRewardSubsystem::RewardChange(double X, bool bNegative) { // Ordre de l'auteur (SPEC 1.3) : Abs, MapRangeClamped(0, 100, 0, 1.9), 2 - m, * 10, FCeil, entier -> reel, / 10, // * signe (SelectFloat(-1, 1, EndsWith("-"))). Memes fonctions que les noeuds Blueprint (KismetMathLibrary.h:796, // :836), en double comme les Blueprints 5.x. const double Mapped = UKismetMathLibrary::MapRangeClamped(FMath::Abs(X), 0.0, 100.0, 0.0, 1.9); const int32 Tenths = UKismetMathLibrary::FCeil((2.0 - Mapped) * 10.0); const double Value = static_cast(Tenths) / 10.0; return Value * (bNegative ? -1.0 : 1.0); } bool ULPBonusRewardSubsystem::CheckFormula(FString& OutFailure) { struct FCase { double X; bool bNegative; double Expected; }; // SPEC 2.4 : 0, 10, 25, 50, 75, 90, 100 -> 2.0, 1.9, 1.6, 1.1, 0.6, 0.3, 0.2 ; plus le signe d'une cle en '-'. static const FCase Cases[] = { { 0.0, false, 2.0 }, { 10.0, false, 1.9 }, { 25.0, false, 1.6 }, { 50.0, false, 1.1 }, { 75.0, false, 0.6 }, { 90.0, false, 0.3 }, { 100.0, false, 0.2 }, { 50.0, true, -1.1 }, }; for (const FCase& Case : Cases) { const double Got = RewardChange(Case.X, Case.bNegative); if (!FMath::IsNearlyEqual(Got, Case.Expected, 1e-9)) { OutFailure = FString::Printf(TEXT("x=%.1f%s : %.17g au lieu de %.1f"), Case.X, Case.bNegative ? TEXT(" (baisse)") : TEXT(""), Got, Case.Expected); return false; } } return true; } bool ULPBonusRewardSubsystem::Overlaps(const TArray& A, const TArray& B, bool bIfEmptyThenTrue) { // GI.Overlaps (L6f_dump_GI.txt:19622-19673) : IfEmptyThenTrue ET (A vide OU B vide) -> vrai ; sinon un element de // A contenu dans B (Array_Contains) -> vrai ; sinon faux. if (bIfEmptyThenTrue && (A.Num() == 0 || B.Num() == 0)) { return true; } for (const FString& Item : A) { if (B.Contains(Item)) { return true; } } return false; } void ULPBonusRewardSubsystem::Pick(const TArray& PositionTags, FRandomStream& InRng, bool bAttractionAllowed, TArray& OutStats, TArray& OutNegative) { OutStats.Reset(); OutNegative.Reset(); const TArray& Stats = Table(); // Map_Keys(statTags) puis Array_Shuffle : meme algorithme que KismetArrayLibrary.cpp:338-352 (ici par le flux // InRng, graine injectable), puis Array_Resize(10). TArray Order; Order.Reserve(Stats.Num()); for (int32 Index = 0; Index < Stats.Num(); ++Index) { Order.Add(Index); } const int32 LastIndex = Order.Num() - 1; for (int32 i = 0; i <= LastIndex; ++i) { const int32 Index = InRng.RandRange(i, LastIndex); if (i != Index) { Order.Swap(i, Index); } } Order.SetNum(FMath::Min(Order.Num(), LPBonusReward::CandidateCount)); for (const int32 Index : Order) { const FLPBonusStat& Entry = Stats[Index]; if (!Overlaps(PositionTags, Entry.Tags, true)) { continue; } if (Entry.Key == LPBonusReward::AttractionKey && !bAttractionAllowed) { continue; } const bool bNegative = Entry.Key.EndsWith(TEXT("-")); OutStats.Add(bNegative ? Entry.Key.LeftChop(1) : Entry.Key); OutNegative.Add(bNegative); if (OutStats.Num() == LPBonusReward::MaxRewards) { break; } } } bool ULPBonusRewardSubsystem::Tick(float DeltaTime) { (void)DeltaTime; if (UGameInstance* GI = GetGameInstance()) { Poll(GI); } FirePending(); ExpireRewardLines(); return true; } void ULPBonusRewardSubsystem::Poll(UObject* GI) { const FBoolProperty* SexOn = CastField(LPBonusReward::FindProp(GI, TEXT("SexOn"))); if (!SexOn) { // D1 (STE-ORG5-V1-VAL.md) : message construit seulement au premier passage (Poll tourne toutes les 0,1 s). if (FirstWarning(TEXT("GI.SexOn"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de propriete booleenne SexOn dans %s, aucun orgasme detecte"), *GI->GetClass()->GetName()); } bInScene = false; ClearRewardDisplay(true); LastCounts.Reset(); return; } bInScene = SexOn->GetPropertyValue_InContainer(GI); if (!bInScene) { // Hors scene : memoire des compteurs videe. Un orgasme deja detecte (Pending) garde son tempo, comme le // RetriggerableDelay de l'auteur qui ne depend pas de la scene. Garde 3 (ORG5-I2a) : lignes de recompense // retirees de l'ecran et minuteries videes a la sortie de scene. LastCounts.Reset(); ClearRewardDisplay(true); return; } TArray Persons; if (!LPBonusReward::ReadObjects(GI, TEXT("SexPersons"), Persons)) { if (FirstWarning(TEXT("GI.SexPersons"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de tableau d'objets SexPersons dans %s, aucun orgasme detecte"), *GI->GetClass()->GetName()); } LastCounts.Reset(); return; } TSet> Seen; for (UObject* Person : Persons) { if (!IsValid(Person)) { continue; } const FIntProperty* CountProp = CastField(LPBonusReward::FindProp(Person, TEXT("Sex_OrgasmCounts"))); if (!CountProp) { if (FirstWarning(TEXT("Person.Sex_OrgasmCounts"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de propriete entiere Sex_OrgasmCounts dans %s, personne ignoree"), *Person->GetClass()->GetName()); } continue; } const int32 Count = CountProp->GetPropertyValue_InContainer(Person); const TWeakObjectPtr Key(Person); Seen.Add(Key); int32* Last = LastCounts.Find(Key); if (!Last) { // Premier passage (debut de scene, personne qui entre) : memorisation sans recompense. LastCounts.Add(Key, Count); continue; } if (Count > *Last) { TArray Tags; if (!LPBonusReward::ReadStrings(GI, TEXT("CurrentTags"), Tags)) { WarnOnce(TEXT("GI.CurrentTags"), FString::Printf(TEXT("pas de tableau de chaines CurrentTags dans %s, orgasme sans recompense"), *GI->GetClass()->GetName())); } else { if (Pending.IsSet()) { UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : orgasme en attente remplace par celui de %s (tempo %.1f s)"), *PersonName(Person), LPBonusReward::DebounceSeconds); } FLPBonusPending NewPending; NewPending.Person = Person; NewPending.Tags = MoveTemp(Tags); NewPending.Count = Count; NewPending.FireAt = FPlatformTime::Seconds() + LPBonusReward::DebounceSeconds; Pending = MoveTemp(NewPending); } } // Hausse : nouveau compteur retenu ; baisse (remise a zero du jeu) : resynchronisation sans recompense. *Last = Count; } // Personne sortie de la scene : oubliee (si elle revient, premier passage sans recompense). for (auto It = LastCounts.CreateIterator(); It; ++It) { if (!Seen.Contains(It.Key())) { It.RemoveCurrent(); } } } void ULPBonusRewardSubsystem::FirePending() { if (!Pending.IsSet() || FPlatformTime::Seconds() < Pending->FireAt) { return; } const FLPBonusPending Fire = MoveTemp(Pending.GetValue()); Pending.Reset(); UObject* Person = Fire.Person.Get(); if (!IsValid(Person)) { UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : personne disparue avant la fin du tempo, pas de recompense")); return; } OnOrgasm(Person, Fire.Tags, Fire.Count); } void ULPBonusRewardSubsystem::OnOrgasm(UObject* Person, const TArray& TagsCapturedAtDetection, int32 Count) { const FString Name = PersonName(Person); UE_LOG(LogLifePlay, Log, TEXT("Bonus : orgasme de %s (compteur %d)"), *Name, Count); if (GetDefault()->bDisabled) { UE_LOG(LogLifePlay, Log, TEXT("Bonus : désactivé (bDisabled)")); return; } UGameInstance* GI = GetGameInstance(); const bool bAttraction = GI && AttractionAllowed(GI, Person); TArray Stats; TArray Negative; Pick(TagsCapturedAtDetection, Rng, bAttraction, Stats, Negative); if (Stats.Num() == 0) { UE_LOG(LogLifePlay, Log, TEXT("Bonus : aucune récompense éligible pour %s"), *Name); return; } // V-min : la premiere recompense retenue est appliquee (l'auteur en propose jusqu'a 3 et le joueur choisit). double X = 0.0; if (!QueryStat(Person, Stats[0], X)) { return; } const double Change = RewardChange(X, Negative[0]); if (!ModifyStat(Person, Stats[0], Change)) { return; } UE_LOG(LogLifePlay, Log, TEXT("Bonus : %s %s %+.1f (x=%.2f, tags=%d)"), *Name, *Stats[0], Change, X, TagsCapturedAtDetection.Num()); // ORG5-I2a : ligne affichee dans le menu d'actions (EquipMenu de Cursor) ; un echec d'affichage laisse la // recompense appliquee (garde 1). bDisabled est sorti plus haut : rien n'est affiche (Q-I2-4). ShowAppliedReward(Name, Stats[0], Change); } void ULPBonusRewardSubsystem::ShowAppliedReward(const FString& Name, const FString& Stat, double Change) { // STE-ORG5-I2-SPEC.md option (a), section 2.2 : la ligne va dans une case ajoutee comme dernier enfant d'EquipMenu // (emplacement de BonusRewardBox chez l'auteur, SPEC 1.1 A1 a A4) ; elle suit donc l'ouverture du menu et la // visibilite d'EquipMenu (liee a GI.SexOn, SPEC 1.1 A7) sans code de plus. Appel depuis FirePending (ticker). if (!bInScene) { // Tempo de 0,2 s fini apres la fin de la scene : pas de ligne (elle serait retiree au sondage suivant). UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : scene terminee avant l'affichage, ligne non affichee")); return; } UGameInstance* GI = GetGameInstance(); if (!GI) { return; } UUserWidget* Cursor = FindCursor(GI); if (!Cursor) { return; } UPanelWidget* Equip = FindEquipMenu(Cursor); if (!Equip) { return; } EnsureEquipMenuHeight(Equip); UVerticalBox* Box = EnsureRewardBox(Cursor, Equip); if (!Box) { return; } // Remplacement (SPEC 2.2 point 4, V6-3) : au plus MaxRewardLines lignes, la plus ancienne retiree d'abord. while (RewardLines.Num() != 0 && RewardLines.Num() >= LPBonusReward::MaxRewardLines) { LPBonusReward::RemoveLine(RewardLines[0]); RewardLines.RemoveAt(0); } // Ligne creee dans l'arbre de widgets de Cursor (UWidgetTree::ConstructWidget, WidgetTree.h:106-117 : // NewObject avec l'arbre pour outer), nom automatique. UTextBlock* Line = Cursor->WidgetTree->ConstructWidget(UTextBlock::StaticClass()); if (!Line) { return; } // Meme format que la ligne de journal ci-dessus (%+.1f, SPEC Q-I2-6). Line->SetText(FText::FromString(FString::Printf(TEXT("Bonus : %s %s %+.1f"), *Name, *Stat, Change))); Line->SetFont(LPBonusReward::RewardFont(Cursor)); Line->SetColorAndOpacity(FSlateColor(FLinearColor::White)); if (UVerticalBoxSlot* LineSlot = Box->AddChildToVerticalBox(Line)) { // Marges de ScrollBonuses chez l'auteur (SPEC 1.1 A4) : gauche 10, haut 5, droite 10. LineSlot->SetPadding(FMargin(10.f, 5.f, 10.f, 0.f)); LineSlot->SetHorizontalAlignment(HAlign_Left); } FLPBonusRewardLine Entry; Entry.Line = Line; Entry.ExpireAt = FPlatformTime::Seconds() + LPBonusReward::RewardLineSeconds; RewardLines.Add(Entry); UE_LOG(LogLifePlay, Verbose, TEXT("Bonus : ligne affichee pour %.0f s"), LPBonusReward::RewardLineSeconds); } UUserWidget* ULPBonusRewardSubsystem::FindCursor(UObject* GI) { // Garde 1 : GI.Cursor est une propriete Blueprint, lue par reflexion comme GI.PlayerCharacter ; type reel non lu, // verifie par Cast en UUserWidget. Introuvable : un avertissement par cause, la recompense reste appliquee. UObject* Value = nullptr; if (!LPBonusReward::ReadObject(GI, LPBonusReward::CursorProp, Value)) { if (FirstWarning(TEXT("GI.Cursor"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de propriete objet Cursor dans %s, recompense appliquee sans affichage"), *GI->GetClass()->GetName()); } return nullptr; } UUserWidget* Cursor = Cast(Value); if (!IsValid(Cursor) || !Cursor->WidgetTree) { if (FirstWarning(TEXT("GI.Cursor.Value"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : GI.Cursor vide, sans arbre de widgets ou d'une classe autre que UserWidget (%s), recompense appliquee sans affichage"), *GetNameSafe(Value ? Value->GetClass() : nullptr)); } return nullptr; } return Cursor; } UPanelWidget* ULPBonusRewardSubsystem::FindEquipMenu(UUserWidget* Cursor) { // UUserWidget::GetWidgetFromName = WidgetTree->FindWidget(Name) (UserWidget.cpp:1259-1262), parcours de l'arbre // (WidgetTree.cpp:40-54) : trouve aussi un widget qui n'est pas une variable. Type verifie par Cast (garde 1). UWidget* Found = Cursor->GetWidgetFromName(FName(LPBonusReward::EquipMenuName)); UPanelWidget* Equip = Cast(Found); if (!IsValid(Equip)) { if (FirstWarning(TEXT("Cursor.EquipMenu"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : pas de panneau EquipMenu dans %s (trouve : %s), recompense appliquee sans affichage"), *Cursor->GetClass()->GetName(), *GetNameSafe(Found ? Found->GetClass() : nullptr)); } return nullptr; } return Equip; } void ULPBonusRewardSubsystem::EnsureEquipMenuHeight(UPanelWidget* Equip) { // Q-I2-3 : hauteur de l'auteur (500, SPEC 1.1 A3) si elle est inferieure (350 chez nous, SPEC 1.4 N1). EquipMenu // est enfant du CanvasPanel BelowCursor : sa hauteur est celle de son slot (UCanvasPanelSlot::GetSize / SetSize, // CanvasPanelSlot.h:116, :112), qui n'a ce sens que sans taille automatique (GetAutoSize, :148) et sans ancres // etirees verticalement (GetAnchors, :132 ; FAnchors::IsStretchedVertical, Anchors.h:78). Sinon : rien change. UPanelSlot* EquipSlot = Equip->Slot.Get(); UCanvasPanelSlot* CanvasSlot = Cast(EquipSlot); if (!CanvasSlot) { if (FirstWarning(TEXT("EquipMenu.Slot"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : EquipMenu n'est pas dans un CanvasPanel (slot %s), hauteur inchangee"), *GetNameSafe(EquipSlot ? EquipSlot->GetClass() : nullptr)); } return; } if (CanvasSlot->GetAutoSize() || CanvasSlot->GetAnchors().IsStretchedVertical()) { if (FirstWarning(TEXT("EquipMenu.Slot.Layout"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : slot d'EquipMenu en taille automatique ou etire verticalement, hauteur inchangee")); } return; } const FVector2D Size = CanvasSlot->GetSize(); if (Size.Y < LPBonusReward::EquipMenuMinHeight) { CanvasSlot->SetSize(FVector2D(Size.X, LPBonusReward::EquipMenuMinHeight)); UE_LOG(LogLifePlay, Log, TEXT("Bonus : hauteur d'EquipMenu portee de %.0f a %.0f"), Size.Y, LPBonusReward::EquipMenuMinHeight); } } UVerticalBox* ULPBonusRewardSubsystem::EnsureRewardBox(UUserWidget* Cursor, UPanelWidget* Equip) { // Garde 2 (aucun ajout en double) : la case est retrouvee avant toute creation, dans l'ordre : pointeur faible // encore enfant de cet EquipMenu ; enfant d'EquipMenu de ce nom ; objet de ce nom dans l'arbre de Cursor (case // detachee, reprise au lieu d'un NewObject du meme nom). Creee seulement si aucune n'existe. UVerticalBox* Box = RewardBox.Get(); if (IsValid(Box) && Box->GetParent() == Equip) { return Box; } Box = nullptr; const FName BoxName(LPBonusReward::RewardBoxName); bool bAttached = false; for (int32 Index = 0; Index < Equip->GetChildrenCount(); ++Index) { UWidget* Child = Equip->GetChildAt(Index); if (Child && Child->GetFName() == BoxName) { Box = Cast(Child); bAttached = true; break; } } UWidgetTree* Tree = Cursor->WidgetTree.Get(); if (!bAttached) { // FindObjectFast(Outer, Name) : UObjectGlobals.h:2059 ; UObject pour voir un objet de ce nom de toute classe. if (UObject* Existing = FindObjectFast(Tree, BoxName)) { Box = Cast(Existing); if (IsValid(Box)) { Box->RemoveFromParent(); } } else { Box = Tree->ConstructWidget(UVerticalBox::StaticClass(), BoxName); } } if (!IsValid(Box)) { if (FirstWarning(TEXT("LPBonusRewardBox.Type"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : un objet %s existe deja sans etre une VerticalBox utilisable, recompense appliquee sans affichage"), LPBonusReward::RewardBoxName); } return nullptr; } if (!bAttached) { // UPanelWidget::AddChild (PanelWidget.h:59) ajoute en dernier : meme indice que BonusRewardBox chez l'auteur. UPanelSlot* BoxSlot = Equip->AddChild(Box); if (!BoxSlot) { if (FirstWarning(TEXT("EquipMenu.AddChild"))) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : EquipMenu (%s) refuse un enfant de plus, recompense appliquee sans affichage"), *Equip->GetClass()->GetName()); } return nullptr; } // Slot Fill comme BonusRewardBox chez l'auteur (SPEC 1.1 A4) ; autre panneau que VerticalBox : slot laisse tel quel. if (UVerticalBoxSlot* VerticalSlot = Cast(BoxSlot)) { VerticalSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); } } // Case retrouvee ou reprise : ses anciennes lignes ne sont plus suivies, elles sont retirees ici. Box->ClearChildren(); RewardLines.Reset(); RewardBox = Box; if (!bDisplayTargetLogged) { bDisplayTargetLogged = true; UE_LOG(LogLifePlay, Log, TEXT("Bonus : affichage dans EquipMenu (classe %s, %d enfants)"), *Equip->GetClass()->GetName(), Equip->GetChildrenCount()); } return Box; } void ULPBonusRewardSubsystem::ExpireRewardLines() { // Q-I2-2 : chaque ligne est retiree 20 s apres son affichage. Echeance en temps reel (FPlatformTime::Seconds, meme // horloge que le tempo de 0,2 s), relue a chaque passage du ticker existant (0,1 s) : retrait entre 20,0 et 20,1 s. if (RewardLines.Num() == 0) { return; } const double Now = FPlatformTime::Seconds(); for (int32 Index = RewardLines.Num() - 1; Index >= 0; --Index) { if (Now >= RewardLines[Index].ExpireAt || !RewardLines[Index].Line.IsValid()) { LPBonusReward::RemoveLine(RewardLines[Index]); RewardLines.RemoveAt(Index); } } } void ULPBonusRewardSubsystem::ClearRewardDisplay(bool bRemoveWidgets) { // Garde 3 : fin de scene (Poll, SexOn faux) : lignes retirees de l'ecran et minuteries videes ; Deinitialize : // minuteries videes seulement. if (RewardLines.Num() == 0) { return; } if (bRemoveWidgets) { for (const FLPBonusRewardLine& Entry : RewardLines) { LPBonusReward::RemoveLine(Entry); } } RewardLines.Reset(); } bool ULPBonusRewardSubsystem::AttractionAllowed(UObject* GI, UObject* Person) { // Condition de l'auteur (SPEC 1.1 E6c) : NON Person.isPlayer ET GI.IsInterestedIn(Person, GI.PlayerCharacter) ET // GI.SexPersons contient GI.PlayerCharacter. Propriete ou fonction absente : avertissement, cle refusee. const FBoolProperty* IsPlayerProp = CastField(LPBonusReward::FindProp(Person, TEXT("isPlayer"))); if (!IsPlayerProp) { WarnOnce(TEXT("Person.isPlayer"), FString::Printf(TEXT("pas de propriete booleenne isPlayer dans %s, cle attractiontoplayer refusee"), *Person->GetClass()->GetName())); return false; } if (IsPlayerProp->GetPropertyValue_InContainer(Person)) { return false; } UObject* Player = nullptr; if (!LPBonusReward::ReadObject(GI, TEXT("PlayerCharacter"), Player)) { WarnOnce(TEXT("GI.PlayerCharacter"), FString::Printf(TEXT("pas de propriete objet PlayerCharacter dans %s, cle attractiontoplayer refusee"), *GI->GetClass()->GetName())); return false; } if (!IsValid(Player)) { return false; } TArray Persons; if (!LPBonusReward::ReadObjects(GI, TEXT("SexPersons"), Persons) || !Persons.Contains(Player)) { return false; } bool bInterested = false; if (!IsInterestedIn(GI, Person, Player, bInterested)) { return false; } return bInterested; } bool ULPBonusRewardSubsystem::IsInterestedIn(UObject* GI, UObject* WhoIsInterested, UObject* WhoInterests, bool& bOutInterested) { // GI.IsInterestedIn(WhoIsInterested, WhoInterests) -> Interested (L6f_dump_GI.txt:23421-23424, :23442-23443). bOutInterested = false; UFunction* Function = GI->FindFunction(FName(TEXT("IsInterestedIn"))); if (!Function) { WarnOnce(TEXT("GI.IsInterestedIn"), FString::Printf(TEXT("fonction IsInterestedIn introuvable dans %s, cle attractiontoplayer refusee"), *GI->GetClass()->GetName())); return false; } const FObjectPropertyBase* WhoIsParm = CastField(LPBonusReward::FindParm(Function, TEXT("WhoIsInterested"))); const FObjectPropertyBase* WhoParm = CastField(LPBonusReward::FindParm(Function, TEXT("WhoInterests"))); const FBoolProperty* ResultParm = CastField(LPBonusReward::FindParm(Function, TEXT("Interested"))); if (!WhoIsParm || !WhoParm || !ResultParm || !ResultParm->HasAnyPropertyFlags(CPF_OutParm) || !WhoIsParm->PropertyClass || !WhoParm->PropertyClass) { WarnOnce(TEXT("GI.IsInterestedIn.Parms"), FString::Printf(TEXT("signature inattendue de IsInterestedIn : %s ; cle attractiontoplayer refusee"), *LPBonusReward::DescribeParms(Function))); return false; } if (!WhoIsInterested->IsA(WhoIsParm->PropertyClass) || !WhoInterests->IsA(WhoParm->PropertyClass)) { WarnOnce(TEXT("GI.IsInterestedIn.Types"), FString::Printf(TEXT("IsInterestedIn attend %s et %s, recu %s et %s ; cle attractiontoplayer refusee"), *WhoIsParm->PropertyClass->GetName(), *WhoParm->PropertyClass->GetName(), *WhoIsInterested->GetClass()->GetName(), *WhoInterests->GetClass()->GetName())); return false; } const int32 ParmsSize = Function->ParmsSize; const int32 Alignment = Function->GetMinAlignment(); uint8* Memory = static_cast(FMemory_Alloca_Aligned(ParmsSize, Alignment)); LPBonusReward::FScopedParms Parms(Function, Memory); WhoIsParm->SetObjectPropertyValue_InContainer(Memory, WhoIsInterested); WhoParm->SetObjectPropertyValue_InContainer(Memory, WhoInterests); GI->ProcessEvent(Function, Memory); bOutInterested = ResultParm->GetPropertyValue_InContainer(Memory); return true; } bool ULPBonusRewardSubsystem::QueryStat(UObject* Person, const FString& Stat, double& OutValue) { // Person.QueryStat(Stat, WithoutBuff, RNG) -> Value, Bonus (L6f_dump_Person.txt:20-29) ; appel de l'auteur : // QueryStat(Stat, false, false).Value (SPEC 1.1 E6d). OutValue = 0.0; UFunction* Function = Person->FindFunction(FName(TEXT("QueryStat"))); if (!Function) { WarnOnce(TEXT("Person.QueryStat"), FString::Printf(TEXT("fonction QueryStat introuvable dans %s, recompense abandonnee"), *Person->GetClass()->GetName())); return false; } const FStrProperty* StatParm = CastField(LPBonusReward::FindParm(Function, TEXT("Stat"))); const FProperty* ValueParm = LPBonusReward::FindParm(Function, TEXT("Value")); const bool bSignature = StatParm && LPBonusReward::IsReal(ValueParm) && ValueParm->HasAnyPropertyFlags(CPF_OutParm) && CastField(LPBonusReward::FindParm(Function, TEXT("WithoutBuff"))) && CastField(LPBonusReward::FindParm(Function, TEXT("RNG"))); if (!bSignature) { WarnOnce(TEXT("Person.QueryStat.Parms"), FString::Printf(TEXT("signature inattendue de QueryStat : %s ; recompense abandonnee"), *LPBonusReward::DescribeParms(Function))); return false; } const int32 ParmsSize = Function->ParmsSize; const int32 Alignment = Function->GetMinAlignment(); uint8* Memory = static_cast(FMemory_Alloca_Aligned(ParmsSize, Alignment)); LPBonusReward::FScopedParms Parms(Function, Memory); StatParm->SetPropertyValue_InContainer(Memory, Stat); LPBonusReward::SetBoolParm(Function, Memory, TEXT("WithoutBuff"), false); LPBonusReward::SetBoolParm(Function, Memory, TEXT("RNG"), false); Person->ProcessEvent(Function, Memory); OutValue = LPBonusReward::ReadReal(ValueParm, Memory); return true; } bool ULPBonusRewardSubsystem::ModifyStat(UObject* Person, const FString& Stat, double Change) { // Person.ModifyStat(Stat, Change, DifficultyAdjusted, NoDisplay) -> Value (L6f_dump_Person.txt:561-570) ; appel de // l'auteur au clic : ModifyStat(Stat, Change, false, false) (SPEC 1.1 E7). UFunction* Function = Person->FindFunction(FName(TEXT("ModifyStat"))); if (!Function) { WarnOnce(TEXT("Person.ModifyStat"), FString::Printf(TEXT("fonction ModifyStat introuvable dans %s, recompense abandonnee"), *Person->GetClass()->GetName())); return false; } const FStrProperty* StatParm = CastField(LPBonusReward::FindParm(Function, TEXT("Stat"))); const FProperty* ChangeParm = LPBonusReward::FindParm(Function, TEXT("Change")); const bool bSignature = StatParm && LPBonusReward::IsReal(ChangeParm) && !ChangeParm->HasAnyPropertyFlags(CPF_OutParm) && CastField(LPBonusReward::FindParm(Function, TEXT("DifficultyAdjusted"))) && CastField(LPBonusReward::FindParm(Function, TEXT("NoDisplay"))); if (!bSignature) { WarnOnce(TEXT("Person.ModifyStat.Parms"), FString::Printf(TEXT("signature inattendue de ModifyStat : %s ; recompense abandonnee"), *LPBonusReward::DescribeParms(Function))); return false; } const int32 ParmsSize = Function->ParmsSize; const int32 Alignment = Function->GetMinAlignment(); uint8* Memory = static_cast(FMemory_Alloca_Aligned(ParmsSize, Alignment)); LPBonusReward::FScopedParms Parms(Function, Memory); StatParm->SetPropertyValue_InContainer(Memory, Stat); LPBonusReward::WriteReal(ChangeParm, Memory, Change); LPBonusReward::SetBoolParm(Function, Memory, TEXT("DifficultyAdjusted"), false); LPBonusReward::SetBoolParm(Function, Memory, TEXT("NoDisplay"), false); Person->ProcessEvent(Function, Memory); return true; } FString ULPBonusRewardSubsystem::PersonName(const UObject* Person) { // Name_First : texte chez l'auteur (SPEC 2.4) ; type non lu chez nous, texte et chaine acceptes. const FProperty* Prop = LPBonusReward::FindProp(Person, TEXT("Name_First")); FString Name; if (const FTextProperty* AsText = CastField(Prop)) { Name = AsText->GetPropertyValue_InContainer(Person).ToString(); } else if (const FStrProperty* AsString = CastField(Prop)) { Name = AsString->GetPropertyValue_InContainer(Person); } else if (Person) { WarnOnce(TEXT("Person.Name_First"), FString::Printf(TEXT("pas de propriete texte ou chaine Name_First dans %s, nom d'objet journalise a la place"), *Person->GetClass()->GetName())); } return Name.IsEmpty() ? GetNameSafe(Person) : Name; } bool ULPBonusRewardSubsystem::FirstWarning(const FString& Key) { // D1 (STE-ORG5-V1-VAL.md) : la cause est testee avant que l'appelant construise son message. if (Warned.Contains(Key)) { return false; } Warned.Add(Key); return true; } void ULPBonusRewardSubsystem::WarnOnce(const FString& Key, const FString& Message) { if (FirstWarning(Key)) { UE_LOG(LogLifePlay, Warning, TEXT("Bonus : %s"), *Message); } }