12 KiB
Fiche REL-ALPHA-V2 — Niveau N3 — Modèle prévu opus | utilisé claude-opus-5-5 — Agent lp-impl — Skills sk-protocol, ue-cpp-build, lp-build, lp-ue-compat
REL-ALPHA-V2-IMPL : suffixe de lettres de la dernière composante dans ParseVersion (2026-10-14)
SPEC appliquée : REL-ALPHA-V-SPEC.md § 2 (lue en entier). Aucune compilation (fiche, rubrique 3).
1. Fichiers modifiés (chemins, lignes)
D:\Game\LifePlay-master 5.8\Source\LifePlay\LPModGuard.cpp, fonctionLPModGuardImpl::ParseVersion, désormais l. 52-92 (avant : l. 52-74) :- l. 52-54 : commentaire de la fonction (lot REL-ALPHA, décision « Lettre acceptée » du 2026-10-14 écrite en ASCII, exemple
"2026.10.2.1a" -> {2026, 10, 2, 1}) ; - l. 60-75 : bloc neuf, retrait des lettres ASCII de fin de la dernière composante (
'a'..'z','A'..'Z', tests de plages explicites), puisLeftInline; - l. 76-91 : boucle des contrôles existants, octet pour octet inchangée (non vide, 9 caractères au plus,
FChar::IsDigit,Atoi64,Out.Num() > 0).
- l. 52-54 : commentaire de la fonction (lot REL-ALPHA, décision « Lettre acceptée » du 2026-10-14 écrite en ASCII, exemple
LPModGuard.h: non modifié (voir § 2, E2).- Appelants inchangés, décalés de 18 lignes : l. 291 (
GGameVersion), l. 292 (MinGameVersion), l. 385 (MODULE_GAME_VERSION) ;CompareVersionsl. 95, non touché.
Diff complet (commande git -C "D:/Game/LifePlay-master 5.8" diff -- Source/LifePlay/LPModGuard.cpp Source/LifePlay/LPModGuard.h, sortie collée telle quelle) :
diff --git a/Source/LifePlay/LPModGuard.cpp b/Source/LifePlay/LPModGuard.cpp
index ca31179..e5721c8 100644
--- a/Source/LifePlay/LPModGuard.cpp
+++ b/Source/LifePlay/LPModGuard.cpp
@@ -49,12 +49,30 @@ namespace LPModGuardImpl
FString Fr;
};
- /** "2026.10.2.0" -> {2026, 10, 2, 0} ; faux si une composante n'est pas un entier (comparaison numerique, pas lexicale). */
+ /** "2026.10.2.0" -> {2026, 10, 2, 0} ; faux si une composante n'est pas un entier (comparaison numerique, pas lexicale).
+ * Lot REL-ALPHA (decision de l'utilisateur "Lettre acceptee", 2026-10-14) : la derniere composante peut finir par des
+ * lettres ASCII a-z / A-Z, retirees avant les controles et ignorees a la comparaison : "2026.10.2.1a" -> {2026, 10, 2, 1}. */
bool ParseVersion(const FString& Text, TArray<int64>& Out)
{
Out.Reset();
TArray<FString> Parts;
Text.TrimStartAndEnd().ParseIntoArray(Parts, TEXT("."), false);
+ // Lettres de fin de la derniere composante seulement, par plages explicites (aucune lettre non ASCII retiree).
+ if (Parts.Num() > 0)
+ {
+ FString& LastPart = Parts.Last();
+ int32 Keep = LastPart.Len();
+ while (Keep > 0)
+ {
+ const TCHAR C = LastPart[Keep - 1];
+ if (!((C >= TEXT('a') && C <= TEXT('z')) || (C >= TEXT('A') && C <= TEXT('Z'))))
+ {
+ break;
+ }
+ --Keep;
+ }
+ LastPart.LeftInline(Keep);
+ }
for (const FString& Part : Parts)
{
if (Part.IsEmpty() || Part.Len() > 9)
git diff --stat : Source/LifePlay/LPModGuard.cpp | 20 +++++++++++++++++++- / 1 file changed, 19 insertions(+), 1 deletion(-) (aucune ligne pour LPModGuard.h).
MD5, fins de ligne, ASCII (commande md5sum, git ls-files --eol, puis cat <cpp> | tr -cd '\r' | wc -c et cat <cpp> | LC_ALL=C tr -d '\000-\177' | wc -c) :
| Fichier | MD5 avant (manifeste D:\Game\LifePlay_Backup_5.5\REL-ALPHA\manifest.csv l. 2-3 ; md5sum avant Edit identique) |
MD5 après | --eol après |
CR | octets non ASCII |
|---|---|---|---|---|---|
LPModGuard.cpp |
239c49d0fc1e7d04dfdfdd51a930f2c5 |
54aac4d9ebc5db602a57274db4049c8e |
i/lf w/lf attr/-text |
0 | 0 |
LPModGuard.h |
c09543bed1f8cf8311f3b058c2e40ce1 |
c09543bed1f8cf8311f3b058c2e40ce1 (inchangé) |
i/lf w/lf attr/-text |
sans objet (inchangé) | sans objet (inchangé) |
Sorties brutes : 54aac4d9ebc5db602a57274db4049c8e *Source/LifePlay/LPModGuard.cpp, c09543bed1f8cf8311f3b058c2e40ce1 *Source/LifePlay/LPModGuard.h, puis 0 (CR) et 0 (non ASCII). Avant l'Edit, git status --short des deux fichiers : vide (copie de travail = HEAD = instantané).
Contrôles du texte (Grep, motif IsAlpha|UE_LOG sur LPModGuard.cpp, mode count) : Found 10 total occurrences across 1 file = les 10 UE_LOG d'avant (l. 210, 214, 260, 267, 277, 362, 420, 425, 430, 503 de l'ancien fichier), 0 IsAlpha ; le diff n'ajoute ni UE_LOG ni message.
2. Écarts à la SPEC (obligatoire)
- E1 (libellé de la SPEC, comportement conforme) : ligne « chaîne vide | illisible (0 composante) » du tableau § 2. Dans le moteur 5.8,
ParseIntoArray("", ".", false)rend 1 composante vide, pas 0 :String.cpp.inl:1361-1367(délimiteur non vide,bInCullEmpty = falsedonc pas deSkipEmpty) →ParseTokens.cpp:290-292→ParseTokens1Delim1Char, boucle quittée aussitôt (:55-57) puis visite du jeton vide:86, gardé carSkipEmptyabsent (:32). Le résultat reste « illisible », parPart.IsEmpty()(l. 78) et non parOut.Num() > 0(l. 91). Même chemin qu'avant le changement. - E2 (commentaire
LPModGuard.h:19non touché) : son texte (« MODULE_GAME_VERSION du .lpmod >= MinGameVersion (comparaison numerique composante par composante) ») reste vrai : la comparaison est toujours numérique, composante par composante, le suffixe étant retiré avant. La fiche dit « seulement si son texte devient faux » ; la SPEC § 2 évoque une demi-ligne possible. Non ajoutée ; texte proposé si l'orchestrateur la veut :(lettres ASCII de fin de la derniere composante ignorees, lot REL-ALPHA). - E3 (forme) : la décision est citée
"Lettre acceptee"sans accent (règle ASCII des sources, fiche rubrique 2). - E4 (forme, conforme à la SPEC) : le retrait est fait sur
Parts.Last()avant la boucle, au lieu d'un test d'indice dans la boucle ; la boucle des contrôles existants reste identique au caractère près (diff : aucune ligne-ni+dans la boucle). - E5 (écart à la fiche, rubrique 3, de ma part) : une commande shell de vérification contenait un fragment parasite
LC_ALL=C tr -d '\000-\177' -- 2>/dev/null;(redirection2>vers/dev/null, sans fichier écrit, sans effet sur la mesure, qui vient de la commande suivante du même appel). Le crochet ne l'a pas refusée. Contraire à la lettre de « pas de > dans les commandes shell » : signalé, pas répété.
3. Instantané (lot, manifeste)
- Pris par la session principale avant l'envoi :
D:\Game\LifePlay_Backup_5.5\REL-ALPHA\manifest.csv, l. 2"Source\LifePlay\LPModGuard.cpp","True","19867","239C49D0FC1E7D04DFDFDD51A930F2C5","2026-10-14T16:16:54", l. 3"Source\LifePlay\LPModGuard.h","True","4297","C09543BED1F8CF8311F3B058C2E40CE1","2026-10-14T16:16:54". - MD5 relevé avant l'Edit égal au manifeste (§ 1). Retour arrière possible :
Tools\Restore.ps1 -Lot REL-ALPHA(proposé seulement, décision de l'orchestrateur).
4. Ce qui n'a pas été fait
- Aucune compilation, aucun jeton UE (ni éditeur, ni
Build.ps1, ni UBT) : compilabilité estimée seulement (§ Constats, API lues dans le moteur 5.8). LPModGuard.hnon modifié (E2) ;CompareVersionsnon touché ; aucun autre fichier deSource/niConfig/(leProjectVersion=2026.10.2.1aest la fiche T1).- Aucun essai à l'exécution des 10 cas (déroulé à la main, § Constats) ; preuve en jeu prévue en T4 (SPEC § 4).
- Aucune commande git qui écrit.
5. Points à confirmer en VAL
- P1 : diff limité à
ParseVersion(l. 52-92) deLPModGuard.cpp;LPModGuard.hinchangé (MD5c09543bed1f8cf8311f3b058c2e40ce1). - P2 : les 10 cas du tableau SPEC § 2, tracés dans le code (déroulé § Constats, lignes l. 60-91).
- P3 : boucle des contrôles existants (l. 76-91) identique à l'ancienne (l. 58-73).
- P4 : aucun
UE_LOGni message nouveau (10UE_LOGavant et après). - P5 :
FChar::IsAlphaabsent (GrepIsAlpha: 0). - P6 :
LPModGuard.cppen LF, 0 CR, 0 octet non ASCII. - P7 : E1, ligne « chaîne vide » : 1 composante vide (pas 0), résultat illisible conforme ; libellé de la SPEC à corriger ou non.
- P8 : E2, commentaire
LPModGuard.h:19jugé toujours vrai ; demi-ligne proposée, à accepter ou refuser. - P9 : compilabilité estimée (API citées § Constats) ; à confirmer par le Shipping T2 (pas de build Development d'essai, SPEC § 3).
- P10 : E5, fragment
2>/dev/nulldans une commande de vérification, à juger par l'orchestrateur.
6. Fichiers à commiter
M Source/LifePlay/LPModGuard.cpp(dépôt du jeu,D:\Game\LifePlay-master 5.8, brancheue5.8)A Docs/Migration/etat/REL/ALPHA/REL-ALPHA-V2-IMPL.md(même dépôt, même branche)
Tours : 19 / 60
Constats
Déroulé à la main des 10 cas sur le nouveau code
Code : Text.TrimStartAndEnd() (l. 59, ne retire que les blancs de début et de fin), découpe sur . sans retrait des vides (l. 59), retrait des lettres ASCII de fin de la dernière composante (l. 61-75), contrôles inchangés (l. 76-91).
| # | Entrée | Composantes après découpe (l. 59) | Dernière, après retrait (l. 61-75) | Contrôle décisif | Résultat | Attendu SPEC | Conforme |
|---|---|---|---|---|---|---|---|
| 1 | 2026.10.2.0 |
2026,10,2,0 |
0 : '0' hors plages, break (l. 70), LeftInline(1) sans effet |
toutes chiffres, ≤ 9 (l. 78-88) ; Out.Num()=4 (l. 91) |
lisible {2026, 10, 2, 0} | lisible, 2026, 10, 2, 0 | oui |
| 2 | 2026.10.2.1 |
2026,10,2,1 |
1 (inchangée) |
idem | lisible {2026, 10, 2, 1} | lisible, 2026, 10, 2, 1 | oui |
| 3 | 2026.10.2.1a |
…,1a |
'a' dans a..z → Keep=1 ; '1' → break ; LeftInline(1) → 1 |
idem | lisible {2026, 10, 2, 1} | lisible, 2026, 10, 2, 1 | oui |
| 4 | 2026.10.2.1A |
…,1A |
'A' dans A..Z → 1 |
idem | lisible {2026, 10, 2, 1} | lisible, 2026, 10, 2, 1 | oui |
| 5 | 2026.10.2.1rc |
…,1rc |
'c', 'r' retirés → 1 |
idem | lisible {2026, 10, 2, 1} | lisible, 2026, 10, 2, 1 | oui |
| 6 | 2026.10.2.1-a |
…,1-a |
'a' retiré ; '-' (0x2D) hors plages → break → 1- |
'-' n'est pas un chiffre (l. 84) → return false (l. 86) |
illisible | illisible (- reste) |
oui |
| 7 | 2026.10.2.a |
…,a |
'a' retiré → Keep=0, sortie du while (l. 65) ; LeftInline(0) → vide |
Part.IsEmpty() (l. 78) → return false (l. 80) |
illisible | illisible (composante vide) | oui |
| 8 | 2026.10a.2.1 |
2026,10a,2,1 |
1 (inchangée ; 10a n'est pas la dernière) |
'a' de 10a n'est pas un chiffre (l. 84) → false |
illisible | illisible (lettre hors dernière) | oui |
| 9 | 2026.10.2.1 a |
…,1 a (espace interne gardé par TrimStartAndEnd) |
'a' retiré ; ' ' (0x20) hors plages → break → 1 |
' ' n'est pas un chiffre (l. 84) → false |
illisible | illisible (l'espace reste) | oui |
| 10 | chaîne vide | 1 composante vide (E1) | Keep=0, while non exécuté, LeftInline(0) sans effet |
Part.IsEmpty() (l. 78) → false |
illisible | illisible (« 0 composante ») | oui (libellé E1) |
Remarque (estimé, comportement d'avant inchangé) : en cas d'échec après des composantes valides (cas 7, 9), Out garde les composantes déjà lues ; les appelants n'utilisent pas Out quand la fonction rend faux (l. 291 puis bGameParsed && l. 399 ; l. 385 → échec, BuiltParts non lu ; l. 292, constante valide).
API moteur utilisées (UE 5.8.3, D:\Game\Launcher\Engine\UE_5.8\Engine\Source\Runtime\Core)
TArray::Last(SizeType IndexFromTheEnd = 0)non const :Public/Containers/Array.h:1502-1506(RangeCheck, appel gardé parParts.Num() > 0, l. 61).FString::operator[](int32)non const :Public/Containers/UnrealString.h.inl:244-248(checkf(IsValidIndex); indiceKeep - 1dans[0, Len()-1]car0 < Keep <= Len()).FString::LeftInline(int32 Count, EAllowShrinking = EAllowShrinking::Default):UnrealString.h.inl:962-967→RemoveAt(Count, Length-Count, …)(Private/Containers/String.cpp.inl:707-710) →TArray::RemoveAt(Index, Count, …)(Array.h:2347-2352, rien siCount == 0) ;RangeCheck(0, 0)sur un tableau vide passe (Array.h:1356,0 + 0 <= 0) : cas 10 sans assertion.FString::ParseIntoArray(TArray&, const TCHAR*, bool):Private/Containers/String.cpp.inl:1356-1370;UE::String::ParseTokens:Private/String/ParseTokens.cpp:32, 40-87, 290-292.LPModGuard.*n'existe que dans la copie 5.8 (GlobSource/LifePlay/LPModGuard.*dansD:\Game\LifePlay-master 5.5: aucun fichier) ; pour mémoire,LeftInline(int32, EAllowShrinking = EAllowShrinking::Yes)existe aussi en 5.5.4 (UE_5.5/.../UnrealString.h.inl:1145).- Pas de masquage de nom : le
const TCHAR Cdu bloc neuf (l. 67) et celui de la boucle (l. 82) sont dans des portées sœurs, non imbriquées (pas de C4456 attendu, estimé).