337 lines
21 KiB
Markdown
337 lines
21 KiB
Markdown
|
|
# VQ2-SPEC — Spécification `ULPVideoQualitySubsystem`
|
|||
|
|
|
|||
|
|
**Fiche :** VQ2-T1
|
|||
|
|
**Rôle :** A-ARCH
|
|||
|
|
**Modèle attendu :** claude-sonnet-5-5-high (M2)
|
|||
|
|
**Date :** 07 octobre 2026
|
|||
|
|
**Workspace :** `d:\Game\LifePlay-master 5.5`
|
|||
|
|
**Statut :** Spécification — prête pour implémentation (VQ2-T2) et branchement onglet (VQ2-T3)
|
|||
|
|
**Dépend de :** `VQ0-CART.md` (§5–§7), `VQ1-SPEC.md` (§4–§6, `Config/DefaultScalability.ini`)
|
|||
|
|
**Patron :** `Source/LifePlay/LPColorGradeSubsystem.h/.cpp` (CHG-161 / CHG-163)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Objectif
|
|||
|
|
|
|||
|
|
Ajouter, dans l'onglet **Video** de `SettingsScrollBox`, un bloc de six lignes C++ (`LPVideo*`) qui pilote les **4 groupes Scalability** définis en VQ1 (ombres, textures, effets, anti-aliasing) par un curseur **Overall** (préréglage Low → Epic) et quatre curseurs individuels. Le bloc est inséré **avant** `LPColorBrightness` (VQ0-CART §7).
|
|||
|
|
|
|||
|
|
Hors périmètre VQ2 : calcul VRAM du bouton Recommended (VQ3), tout changement de `DefaultScalability.ini` / `DefaultEngine.ini`, tout changement Blueprint, GI / VSM / méthode d'AA / reflets (verrous L7c, VQ1 §2.2).
|
|||
|
|
|
|||
|
|
## 2. Fichiers
|
|||
|
|
|
|||
|
|
| Fichier | Action | Fiche |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `Source/LifePlay/LPVideoQualitySubsystem.h` | Nouveau | VQ2-T2 |
|
|||
|
|
| `Source/LifePlay/LPVideoQualitySubsystem.cpp` | Nouveau | VQ2-T2 |
|
|||
|
|
| `Source/LifePlay/LPSettingsSubsystem.cpp` (`CategoryOf`, `ApplyRows`) | Modifié (§9) | VQ2-T3 |
|
|||
|
|
| `Source/LifePlay/LifePlay.Build.cs` | **Inchangé** : `Engine` (Scalability), `Slate`, `SlateCore`, `UMG` déjà présents | — |
|
|||
|
|
| `Saved/Config/Windows/LPVideoQuality.ini` | Créé à l'exécution | — |
|
|||
|
|
|
|||
|
|
Includes attendus (.cpp) : `Scalability.h`, `Components/Button.h`, `Components/ScrollBox.h`, `Components/ScrollBoxSlot.h`, `Components/HorizontalBox.h`, `Components/HorizontalBoxSlot.h`, `Components/SizeBox.h`, `Components/Slider.h`, `Components/TextBlock.h`, `Blueprint/UserWidget.h`, `HAL/IConsoleManager.h`, `Misc/ConfigCacheIni.h`, `Misc/Paths.h`, `LifePlay.h` (`LogLifePlay`).
|
|||
|
|
|
|||
|
|
## 3. Types
|
|||
|
|
|
|||
|
|
### 3.1 `FLPVideoLevels`
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
USTRUCT()
|
|||
|
|
struct FLPVideoLevels
|
|||
|
|
{
|
|||
|
|
GENERATED_BODY()
|
|||
|
|
|
|||
|
|
UPROPERTY() int32 Overall = 2; // 0..3, ou -1 = Custom
|
|||
|
|
UPROPERTY() int32 Shadows = 2; // 0..3 -> sg.ShadowQuality
|
|||
|
|
UPROPERTY() int32 Textures = 2; // 0..3 -> sg.TextureQuality
|
|||
|
|
UPROPERTY() int32 Effects = 2; // 0..3 -> sg.EffectsQuality
|
|||
|
|
UPROPERTY() int32 AA = 2; // 0..3 -> sg.AntiAliasingQuality
|
|||
|
|
|
|||
|
|
static FLPVideoLevels FromOverall(int32 Level); // 4 groupes = Level (clampé 0..3), Overall = Level
|
|||
|
|
void Normalize(); // clamp 0..3 des 4 groupes ; Overall := (4 groupes égaux ? valeur : -1)
|
|||
|
|
bool operator==(const FLPVideoLevels& Other) const;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Les valeurs par défaut du struct ne servent que de repli ; la première exécution dérive les niveaux de l'état Scalability courant (§6.2).
|
|||
|
|
|
|||
|
|
### 3.2 Invariant
|
|||
|
|
|
|||
|
|
Après `Normalize()` : `Overall == -1` **si et seulement si** les 4 groupes ne sont pas tous égaux. Si `Overall >= 0`, les 4 groupes valent `Overall`. L'invariant est ré-imposé à chaque `Load()` : une valeur `Overall` incohérente lue dans l'INI est ignorée et recalculée.
|
|||
|
|
|
|||
|
|
### 3.3 Libellés de niveaux
|
|||
|
|
|
|||
|
|
| Niveau | Libellé (readout) |
|
|||
|
|
|---|---|
|
|||
|
|
| 0 | `Low` |
|
|||
|
|
| 1 | `Med` |
|
|||
|
|
| 2 | `High` |
|
|||
|
|
| 3 | `Epic` |
|
|||
|
|
| Overall = -1 | `Custom` |
|
|||
|
|
|
|||
|
|
## 4. Classe
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
UCLASS()
|
|||
|
|
class LIFEPLAY_API ULPVideoQualitySubsystem : public UGameInstanceSubsystem
|
|||
|
|
{
|
|||
|
|
GENERATED_BODY()
|
|||
|
|
public:
|
|||
|
|
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
|||
|
|
virtual void Deinitialize() override;
|
|||
|
|
|
|||
|
|
// Point d'entrée unique de modification (UI, Recommended, VQ3).
|
|||
|
|
void SetLevels(FLPVideoLevels NewLevels, bool bFromOverall);
|
|||
|
|
FLPVideoLevels GetLevels() const { return Levels; }
|
|||
|
|
void RefreshWidgets();
|
|||
|
|
|
|||
|
|
UFUNCTION() void OnOverallChanged(float Value);
|
|||
|
|
UFUNCTION() void OnShadowsChanged(float Value);
|
|||
|
|
UFUNCTION() void OnTexturesChanged(float Value);
|
|||
|
|
UFUNCTION() void OnEffectsChanged(float Value);
|
|||
|
|
UFUNCTION() void OnAAChanged(float Value);
|
|||
|
|
UFUNCTION() void OnRecommendedClicked();
|
|||
|
|
|
|||
|
|
private:
|
|||
|
|
enum ERow { Overall, Shadows, Textures, Effects, AA, NumRows };
|
|||
|
|
struct FRowRefs { TWeakObjectPtr<USlider> Slider; TWeakObjectPtr<UTextBlock> Readout; };
|
|||
|
|
|
|||
|
|
bool Tick(float DeltaTime); // 0.5 s, comme ColorGrade
|
|||
|
|
void EnsureControls();
|
|||
|
|
void Load();
|
|||
|
|
void Save() const;
|
|||
|
|
void ApplyToScalability(); // §6
|
|||
|
|
USlider* AddRow(...); // même signature que ColorGrade::AddRow (+ ERow)
|
|||
|
|
UButton* AddButtonRow(...); // ligne Recommended
|
|||
|
|
static FString LevelLabel(int32 Level);
|
|||
|
|
|
|||
|
|
FTSTicker::FDelegateHandle TickHandle;
|
|||
|
|
FRowRefs Rows[NumRows];
|
|||
|
|
TWeakObjectPtr<UButton> RecommendedButton;
|
|||
|
|
TWeakObjectPtr<UWorld> LastAppliedWorld;
|
|||
|
|
FLPVideoLevels Levels;
|
|||
|
|
bool bRefreshing = false; // garde anti-réentrance
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Initialize` : `Load()` → `ApplyToScalability()` → enregistre le ticker 0.5 s (même patron que `ULPColorGradeSubsystem`). `Deinitialize` : retire le ticker.
|
|||
|
|
|
|||
|
|
## 5. API
|
|||
|
|
|
|||
|
|
### 5.1 `SetLevels(FLPVideoLevels NewLevels, bool bFromOverall)`
|
|||
|
|
|
|||
|
|
Ordre strict :
|
|||
|
|
|
|||
|
|
1. **Normaliser.**
|
|||
|
|
- `bFromOverall == true` : `Overall` clampé 0..3 → `NewLevels = FromOverall(Overall)`. Si `Overall < 0`, la valeur est ignorée et on retombe sur le cas `false` (pas de fan-out).
|
|||
|
|
- `bFromOverall == false` : clamp des 4 groupes ; `Overall` recalculé par `Normalize()` (égaux → valeur, sinon `-1`). Le champ `Overall` fourni est **ignoré**.
|
|||
|
|
2. `Levels = NewLevels`.
|
|||
|
|
3. **`ApplyToScalability()`** (§6).
|
|||
|
|
4. **`RefreshWidgets()`** (§5.2).
|
|||
|
|
5. **`Save()`** (§7).
|
|||
|
|
|
|||
|
|
Aucune étape ne doit dépendre de la présence des widgets : `SetLevels` est appelable menu fermé (Initialize, VQ3, console de debug).
|
|||
|
|
|
|||
|
|
### 5.2 `RefreshWidgets()`
|
|||
|
|
|
|||
|
|
Garde `bRefreshing` (set/reset autour du corps). Pour chaque `FRowRefs` valide :
|
|||
|
|
|
|||
|
|
| Ligne | Position du curseur | Readout |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Overall | `Overall >= 0` ? `Overall` : `clamp(round(moyenne des 4 groupes), 0, 3)` | `LevelLabel(Overall)` ou **`Custom`** |
|
|||
|
|
| Shadows / Textures / Effects / AA | valeur du groupe | `LevelLabel(valeur)` |
|
|||
|
|
|
|||
|
|
`USlider::SetValue` ne diffuse pas `OnValueChanged` (seul l'événement Slate utilisateur le fait) ; la garde `bRefreshing` protège néanmoins contre un changement de comportement. Les handlers `On*Changed` retournent immédiatement si `bRefreshing`.
|
|||
|
|
|
|||
|
|
### 5.3 Handlers
|
|||
|
|
|
|||
|
|
| Handler | Action |
|
|||
|
|
|---|---|
|
|||
|
|
| `OnOverallChanged(V)` | `Overall = clamp(RoundToInt(V),0,3)` → `SetLevels(FromOverall(Overall), true)` |
|
|||
|
|
| `OnShadowsChanged(V)` | `L = Levels; L.Shadows = clamp(RoundToInt(V),0,3)` → `SetLevels(L, false)` |
|
|||
|
|
| `OnTexturesChanged` / `OnEffectsChanged` / `OnAAChanged` | idem sur leur champ |
|
|||
|
|
| `OnRecommendedClicked()` | Stub VQ2 : `SetLevels(FromOverall(2), true)` + `UE_LOG(... "Video : Recommended = stub (High) ; VRAM prevue en VQ3")`. VQ3 remplace uniquement le corps de cette fonction. |
|
|||
|
|
|
|||
|
|
### 5.4 `Load()` / `Save()` — voir §7.
|
|||
|
|
|
|||
|
|
### 5.5 `EnsureControls()` — voir §8.
|
|||
|
|
|
|||
|
|
## 6. Application Scalability
|
|||
|
|
|
|||
|
|
### 6.1 `ApplyToScalability()`
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
Scalability::FQualityLevels Q = Scalability::GetQualityLevels(); // préserve les champs non gérés
|
|||
|
|
Q.ShadowQuality = Levels.Shadows;
|
|||
|
|
Q.TextureQuality = Levels.Textures;
|
|||
|
|
Q.EffectsQuality = Levels.Effects;
|
|||
|
|
Q.AntiAliasingQuality = Levels.AA;
|
|||
|
|
Scalability::SetQualityLevels(Q, /*bForce=*/true);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Règles :
|
|||
|
|
|
|||
|
|
- **Seuls 4 champs sont écrits** (VQ1 §5). `ResolutionQuality`, `ViewDistanceQuality`, `PostProcessQuality`, `GlobalIlluminationQuality`, `ReflectionQuality`, `ShadingQuality`, `FoliageQuality`, etc. restent ceux de l'état courant. Aucun appel à `SetFromSingleQualityLevel`.
|
|||
|
|
- Niveau 4 (Cinematic) non exposé : pas de valeur > 3 écrite.
|
|||
|
|
- Pas d'appel à `Scalability::SaveState(GGameUserSettingsIni)` : la source de vérité est `LPVideoQuality.ini` (§7). `GameUserSettings.ini` peut contenir des `sg.*` périmés ; ils sont appliqués par le moteur **avant** `Initialize`, puis écrasés par `ApplyToScalability()`.
|
|||
|
|
- **Vérification de relecture** (log `Display`, une ligne) : après `SetQualityLevels`, relire `Scalability::GetQualityLevels()` et comparer aux 4 niveaux ; en cas d'écart, `UE_LOG(Warning)` + repli (R1, §11) : pour chaque groupe divergent, `IConsoleManager::Get().FindConsoleVariable(TEXT("sg.<Groupe>"))->Set(Level, ECVF_SetByConsole)` puis un second `SetQualityLevels`.
|
|||
|
|
|
|||
|
|
### 6.2 Premier lancement (pas d'INI)
|
|||
|
|
|
|||
|
|
Si `LPVideoQuality.ini` n'existe pas : `Levels` = niveaux 4 groupes lus dans `Scalability::GetQualityLevels()` (clampés 0..3 ; un niveau 4/Cine est ramené à 3), puis `Normalize()` ; **aucun `Save()` à l'initialisation** (le fichier n'est écrit qu'au premier changement de l'utilisateur). Ainsi un joueur qui n'ouvre jamais l'onglet conserve exactement son comportement actuel.
|
|||
|
|
|
|||
|
|
### 6.3 Déclencheurs de ré-application (idempotents)
|
|||
|
|
|
|||
|
|
| # | Déclencheur | Raison |
|
|||
|
|
|---|---|---|
|
|||
|
|
| a | `Initialize` | Appliquer l'INI sur l'état moteur chargé |
|
|||
|
|
| b | Premier `Tick` après changement de monde de jeu (`LastAppliedWorld != GetWorld()` et `IsGameWorld()`) | Le Blueprint `MainMenu` / `GameSettings` peut réappliquer ses valeurs `.sav` au chargement d'un niveau |
|
|||
|
|
| c | À chaque injection réussie de `EnsureControls()` (nouvelle instance `MainMenu`) | Idem, à l'ouverture du menu |
|
|||
|
|
|
|||
|
|
Pas de ré-application continue à 0.5 s : cela écraserait les commandes console `sg.*` de debug.
|
|||
|
|
|
|||
|
|
## 7. Persistance — `Saved/Config/Windows/LPVideoQuality.ini`
|
|||
|
|
|
|||
|
|
Chemin : `FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config/Windows/LPVideoQuality.ini"))` (patron `LPColorGrade::IniPath`). Lecture/écriture via `FConfigFile` (`Read` → `SetString` → `Write`).
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
[LPVideoQuality]
|
|||
|
|
Version=1
|
|||
|
|
Overall=2
|
|||
|
|
Shadows=2
|
|||
|
|
Textures=2
|
|||
|
|
Effects=2
|
|||
|
|
AA=2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- `Load()` : lit les 5 clés (`FCString::Atoi`), clamp 0..3 (`Overall` : -1..3), **`Normalize()` recalcule `Overall`**. Une clé manquante conserve la valeur dérivée du §6.2 ; si **aucune** clé n'est présente, comportement §6.2. Log `Display` en français, style ColorGrade : `Video : relu Overall %d, Shadows %d, Textures %d, Effects %d, AA %d depuis %s`.
|
|||
|
|
- `Save()` : écrit les 6 clés + log `... enregistres dans %s`. Appelé uniquement par `SetLevels`.
|
|||
|
|
- `Version` réservé aux migrations futures ; une version > 1 est lue en mode best-effort (clés connues seulement).
|
|||
|
|
|
|||
|
|
## 8. `EnsureControls()` — injection UI
|
|||
|
|
|
|||
|
|
### 8.1 Recherche du `SettingsScrollBox`
|
|||
|
|
|
|||
|
|
Identique à `ULPColorGradeSubsystem::EnsureControls` : `TObjectIterator<UScrollBox>`, nom `SettingsScrollBox`, hors CDO/archétype, `GetTypedOuter<UUserWidget>()` valide, non CDO, `GetGameInstance() == GetGameInstance()`. Sortie silencieuse si introuvable.
|
|||
|
|
|
|||
|
|
### 8.2 Garde
|
|||
|
|
|
|||
|
|
Sortie si un enfant direct du scroll s'appelle `LPVideoOverall` (déjà injecté pour cette instance de menu). Une nouvelle instance `MainMenu` n'a pas ce widget → réinjection ; les `TWeakObjectPtr` précédents sont remplacés.
|
|||
|
|
|
|||
|
|
### 8.3 Lignes injectées (ordre d'affichage)
|
|||
|
|
|
|||
|
|
| # | Nom widget | Libellé | Type | Min–Max | Pas | Handler | Info-bulle |
|
|||
|
|
|---|---|---|---|---|---|---|---|
|
|||
|
|
| 1 | `LPVideoOverall` | `Overall Quality` | Slider + readout | 0–3 | 1 | `OnOverallChanged` | `Preset applied to shadows, textures, effects and anti-aliasing. Custom when they differ.` |
|
|||
|
|
| 2 | `LPVideoShadows` | `Shadows` | Slider + readout | 0–3 | 1 | `OnShadowsChanged` | `Shadow resolution and distance.` |
|
|||
|
|
| 3 | `LPVideoTextures` | `Textures` | Slider + readout | 0–3 | 1 | `OnTexturesChanged` | `Texture filtering and streaming budget.` |
|
|||
|
|
| 4 | `LPVideoEffects` | `Effects` | Slider + readout | 0–3 | 1 | `OnEffectsChanged` | `Translucency, refraction, subsurface scattering and particles.` |
|
|||
|
|
| 5 | `LPVideoAA` | `Anti-Aliasing` | Slider + readout | 0–3 | 1 | `OnAAChanged` | `Anti-aliasing quality. The method itself is fixed.` |
|
|||
|
|
| 6 | `LPVideoRecommended` | `Recommended` | `UButton` + `UTextBlock` | — | — | `OnRecommendedClicked` | `Apply the recommended preset for this PC.` |
|
|||
|
|
|
|||
|
|
Les libellés contiennent les mots-clés `shadow` (ligne 2) / `quality` (ligne 1) ; sans importance, `CategoryOf` classe sur le préfixe `LPVideo` avant le texte (§9).
|
|||
|
|
|
|||
|
|
### 8.4 Structure d'une ligne
|
|||
|
|
|
|||
|
|
Copie du patron `ColorGrade::AddRow` : `UHorizontalBox` nommé (`NewObject<UHorizontalBox>(Scroll, Name)`) contenant `USizeBox` 220 px + `UTextBlock` (légende), `USlider` (Fill), `UTextBlock` (readout). Mêmes paddings (10/0/20/0, 0/0/20/0, 0/0/10/0 ; bas 10 px côté scroll). Style : police/couleur/`FSliderStyle` copiés d'une ligne existante via `FindLook` (repli `MenuFont()` : `Regular 20`, contour noir 2 px, opacité 0.95).
|
|||
|
|
|
|||
|
|
Particularités sliders : `SetMinValue(0)`, `SetMaxValue(3)`, `SetStepSize(1)`, `SetLocked(false)`. Branchement `OnValueChanged` via `FScriptDelegate::BindUFunction` (patron ColorGrade). La ligne Recommended : `UHorizontalBox` → `UButton` (`OnClicked` via `FScriptDelegate`) contenant un `UTextBlock` `Recommended`, même police/couleur, alignement gauche, padding 10 px.
|
|||
|
|
|
|||
|
|
Les helpers `FindLook` / `MenuFont` sont **dupliqués** dans un `namespace LPVideoQuality` local au `.cpp` (V1 : aucune modification de `LPColorGradeSubsystem`). Refactor commun éventuel hors périmètre.
|
|||
|
|
|
|||
|
|
### 8.5 Position d'insertion : avant le premier `LPColor*`
|
|||
|
|
|
|||
|
|
Algorithme :
|
|||
|
|
|
|||
|
|
1. `InsertAt = INDEX_NONE` ; parcourir les enfants directs du scroll, `InsertAt` = index du **premier** enfant dont `GetName().StartsWith(TEXT("LPColor"))`.
|
|||
|
|
2. Créer les 6 lignes (jamais ajoutées directement).
|
|||
|
|
3. Si `InsertAt != INDEX_NONE` : pour `i = 0..5`, `Scroll->InsertChildAt(InsertAt + i, Row[i])` (`UPanelWidget::InsertChildAt`) ; configurer ensuite le `UScrollBoxSlot` (padding bas 10) via `Cast<UScrollBoxSlot>(Row->Slot)`.
|
|||
|
|
4. Sinon (`LPColor*` pas encore injecté) : `Scroll->AddChild(Row[i])` dans l'ordre → les lignes seront suivies des `LPColor*` quand ColorGrade s'injectera (il ajoute en fin).
|
|||
|
|
5. **Repli** si `InsertChildAt` retourne `nullptr` : `AddChild` des 6 lignes, puis réordonner : collecter les enfants `LPColor*`, `RemoveChild` de chacun, et les `AddChild` de nouveau dans leur ordre d'origine (même objets ; `ColorGrade` garde ses `TWeakObjectPtr` valides).
|
|||
|
|
|
|||
|
|
Le garde de `ColorGrade::EnsureControls` (`LPColorBrightness` présent) n'est pas affecté par l'ordre. Les deux sous-systèmes sont indépendants (aucune référence croisée).
|
|||
|
|
|
|||
|
|
### 8.6 Masquage des widgets BP redondants
|
|||
|
|
|
|||
|
|
Widgets dirigés vers **Collapsed** : `ShadowQuality`, `PostProcessQuality`, `PostProcessAAQuality` (noms UMG, VQ0-CART §5). Conservés visibles : `ScreenPercentage`, `FullscreenButton`, `ShowFPS`, cases et SpinBoxes (VQ0-CART §8).
|
|||
|
|
|
|||
|
|
Deux niveaux de mise en œuvre :
|
|||
|
|
|
|||
|
|
1. **`EnsureControls()` (VQ2-T2)** : après injection, `SetVisibility(Collapsed)` sur ces trois enfants directs (recherche par `GetFName()`). Sert d'effet immédiat.
|
|||
|
|
2. **`LPSettingsSubsystem` (VQ2-T3)** : **obligatoire** pour la durabilité — `ApplyRows()` remet `Visible` toute ligne dont `Category == ActiveTab` à chaque changement d'onglet ; sans la modification du §9, le masquage du point 1 est annulé au premier clic d'onglet.
|
|||
|
|
|
|||
|
|
Conséquences V1 (pragmatique, documentées VQ0-CART §6.1/§6.2) :
|
|||
|
|
|
|||
|
|
- Le curseur `PP Quality (FPS intensive)` disparaît ; `sg.PostProcessQuality` n'est plus piloté par l'utilisateur et reste à sa dernière valeur / valeur moteur (VQ1 §2.2 : groupe hors périmètre). Voir question Q1.
|
|||
|
|
- Le curseur `AA Quality` BP (écriture directe de `r.PostProcessAAQuality`) est remplacé par `LPVideoAA` (`sg.AntiAliasingQuality`, VQ1 §4.4 : 0/2/4/4).
|
|||
|
|
- Aucune synchronisation BP ↔ LPVideo : les widgets masqués ne sont plus lus ni écrits par le joueur.
|
|||
|
|
|
|||
|
|
## 9. Intégration `LPSettingsSubsystem` (fiche VQ2-T3, rappel de contrat)
|
|||
|
|
|
|||
|
|
Dans `LPSettingsUi::CategoryOf`, **avant** `RowText` (même endroit que la règle `LPColor`) :
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
if (Widget && (Widget->GetName().StartsWith(TEXT("LPColor")) || Widget->GetName().StartsWith(TEXT("LPVideo"))))
|
|||
|
|
{
|
|||
|
|
return TabVideo;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Et, pour les trois widgets BP masqués, une catégorie « toujours masquée » :
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
constexpr int32 TabHidden = -2;
|
|||
|
|
// dans CategoryOf, après BackButton :
|
|||
|
|
if (Widget && (Widget->GetFName() == TEXT("ShadowQuality")
|
|||
|
|
|| Widget->GetFName() == TEXT("PostProcessQuality")
|
|||
|
|
|| Widget->GetFName() == TEXT("PostProcessAAQuality")))
|
|||
|
|
{
|
|||
|
|
return TabHidden;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`ApplyRows()` : `Category == -1` reste « toujours visible » (`BackButton`) ; `Category == TabHidden` → `Collapsed` quel que soit l'onglet. **Attention :** l'actuel `bShow = Category < 0 || ...` rendrait `-2` visible ; la condition doit devenir `Category == -1 || (...)`. Aucun autre appelant de `CategoryOf` ne doit traiter `< 0` comme « visible » (à vérifier par Grep en T3).
|
|||
|
|
|
|||
|
|
Ordre dans le `ScrollBox` : `ApplyRows()` ne change que la visibilité, pas l'ordre ; l'ordre « BP, LPVideo*, LPColor* » est donc stable. Après injection tardive de `LPVideo*`, la visibilité initiale des nouvelles lignes est celle de `SWidget` par défaut (visible) tant que `ApplyRows()` n'a pas été rappelé : **T3 doit déclencher `ApplyRows()` après injection** (ou `LPVideo` ne peut pas l'appeler : voir Q2).
|
|||
|
|
|
|||
|
|
## 10. Journalisation
|
|||
|
|
|
|||
|
|
Style ColorGrade, `LogLifePlay`, `Display`, français sans accents dans les littéraux (cohérence existante) :
|
|||
|
|
|
|||
|
|
- `Video : relu ... depuis <ini>` (Load)
|
|||
|
|
- `Video : Overall %d, Shadows %d, Textures %d, Effects %d, AA %d enregistres dans <ini>` (Save)
|
|||
|
|
- `Video : qualite appliquee (sg.ShadowQuality %d, sg.TextureQuality %d, sg.EffectsQuality %d, sg.AntiAliasingQuality %d)` (Apply)
|
|||
|
|
- `Video : controles ajoutes avant les reglages de couleur` (EnsureControls, une fois par injection)
|
|||
|
|
- `Warning` : écart de relecture Scalability (§6.1), `InsertChildAt` en repli (§8.5).
|
|||
|
|
|
|||
|
|
## 11. Risques
|
|||
|
|
|
|||
|
|
| ID | Risque | Mitigation |
|
|||
|
|
|---|---|---|
|
|||
|
|
| R1 | Le Blueprint (`SettingSlider`/`GameSettings`) a pu écrire `sg.*` ou `r.PostProcessAAQuality` avec une priorité **Console/Code**, supérieure à `SetByScalability` : `SetQualityLevels` serait alors ignoré silencieusement. | Relecture + repli `ECVF_SetByConsole` (§6.1) ; ré-application aux déclencheurs b/c (§6.3). À confirmer en VQ2-VAL (valeurs `r.Shadow.MaxResolution`, `r.Streaming.PoolSize`, `r.PostProcessAAQuality` conformes à VQ1 §4). |
|
|||
|
|
| R2 | `ApplyRows()` annule le masquage des widgets BP. | §9 obligatoire (T3). |
|
|||
|
|
| R3 | `InsertChildAt` indisponible/inopérant sur `UScrollBox`. | Repli §8.5.5. |
|
|||
|
|
| R4 | Perte de `PP Quality` : joueur ne peut plus baisser le coût post-process. | Décision V1 assumée ; voir Q1. |
|
|||
|
|
| R5 | `GameUserSettings.ini` contient des `sg.*` différents de `LPVideoQuality.ini`. | `LPVideoQuality.ini` est prioritaire dès qu'il existe (§6.1) ; sans INI, état moteur conservé (§6.2). |
|
|||
|
|
| R6 | Écart d'ordre d'initialisation ColorGrade/Video. | Insertion avant premier `LPColor*` si présent, sinon append ; les deux ordres donnent « Video avant Color » (§8.5). |
|
|||
|
|
| R7 | Interdits VQ1 (GI, VSM, méthode AA, reflets). | Seuls 4 champs de `FQualityLevels` écrits ; aucune cvar écrite directement hors repli R1 (`sg.*` uniquement). |
|
|||
|
|
|
|||
|
|
## 12. Questions ouvertes
|
|||
|
|
|
|||
|
|
| ID | Question | Proposition par défaut |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Q1 | Faut-il lier `sg.PostProcessQuality` à `Effects` (ou `Overall`) pour ne pas figer un groupe devenu sans contrôle ? | **Non** en V1 (VQ1 §5 : 4 groupes seulement). À trancher avec A-VAL après mesure de coût. |
|
|||
|
|
| Q2 | Comment T3 est-il notifié d'une injection pour relancer `ApplyRows()` ? | `ULPSettingsSubsystem` a son propre ticker : `ApplyRows()` est déjà réexécuté après détection de nouvelles lignes (à vérifier en T3) ; sinon un delegate `FSimpleMulticastDelegate OnControlsInjected` exposé par `ULPVideoQualitySubsystem`. |
|
|||
|
|
| Q3 | Niveau recommandé du stub. | `High` (2) ; VQ3 le remplace par un calcul VRAM. |
|
|||
|
|
|
|||
|
|
## 13. Critères d'acceptation (VQ2-T2 / VQ2-T3 / VQ2-VAL)
|
|||
|
|
|
|||
|
|
- [ ] `LPVideoQualitySubsystem.h/.cpp` compilent sans toucher `LifePlay.Build.cs` ni `LPColorGradeSubsystem.*`.
|
|||
|
|
- [ ] Ordre dans l'onglet Video : lignes BP visibles → `LPVideoOverall`, `Shadows`, `Textures`, `Effects`, `AA`, `Recommended` → `LPColorBrightness` … `LPColorTint` (quel que soit l'ordre d'initialisation ColorGrade/Video).
|
|||
|
|
- [ ] `ShadowQuality`, `PostProcessQuality`, `PostProcessAAQuality` restent masqués après changement d'onglet aller-retour.
|
|||
|
|
- [ ] Overall 0→3 : les 4 curseurs suivent, readouts `Low/Med/High/Epic` ; `sg.ShadowQuality`, `sg.TextureQuality`, `sg.EffectsQuality`, `sg.AntiAliasingQuality` valent le niveau choisi (console `sg.X` en lecture) ; `sg.GlobalIlluminationQuality`, `sg.ReflectionQuality`, `sg.PostProcessQuality`, `sg.ViewDistanceQuality` inchangés.
|
|||
|
|
- [ ] Modifier un curseur individuel → Overall affiche `Custom` ; ramener les 4 au même niveau → Overall réaffiche ce niveau.
|
|||
|
|
- [ ] Redémarrage du jeu : niveaux restaurés depuis `Saved/Config/Windows/LPVideoQuality.ini` ; sans INI, aucun changement par rapport à l'état actuel.
|
|||
|
|
- [ ] Fermer/rouvrir le menu : lignes réinjectées avec les bonnes valeurs.
|
|||
|
|
- [ ] `Recommended` : applique High, ne plante pas, log stub.
|
|||
|
|
- [ ] Niveau 3 : GI dynamique, VSM et méthode d'AA inchangés (Lumen/VSM off, TAA) — rappel VQ1 §8.
|
|||
|
|
- [ ] Aucune occurrence de `Virtual`, `Lumen`, `AntiAliasingMethod`, `ReflectionMethod` dans le code ajouté.
|