UI14 : menu principal (version 2026.10.4, credits Vinfamy, Donate Patreon edujiro5972, Discord, Commission masque)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-10-02 20:37:41 +02:00
parent 53a5209fdb
commit 3c25b1450b
11 changed files with 377 additions and 2 deletions

View file

@ -1,7 +1,7 @@
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=543F4F9E4DE525696C82F5A52303C15C
ProjectName=LifePlay
ProjectVersion=2026.9 beta
ProjectVersion=2026.10.4
Description=Portage UE 5.5 - assets integres depuis LifePlay 5.9 Full Source
[/Script/MoviePlayer.MoviePlayerSettings]

Binary file not shown.

View file

@ -0,0 +1,43 @@
# 14. Interface du menu principal (lot UI14, avant la V1.4)
Demande utilisateur (2026-10-02) : numéro de version au format année.mois.publication, liste des crédits vidée avec
Vinfamy comme auteur original, lien Donate vers le Patreon de LifePlay Revival, nouveau lien Discord, bouton
« Commission Game Content » masqué (il menait à une page de Vinfamy).
## 1. Où se trouvent ces éléments
| Élément | Emplacement | Avant |
|---|---|---|
| Texte de version | `UI/MainMenu`, fonction `Get_Version_Text_0` : `FormatText("LifePlay v{main}.{sub}{text}")` avec `GI.version_main`, `version_sub` (flottants, 5 et 9), `version_text` | « LifePlay v5.9 » |
| Titre des crédits | `UI/MainMenu`, `TextBlock_19` | « This project wouldn't be possible without the suppport from my generous patrons: » |
| Liste des crédits | `Content/Modules/donors.txt`, lu par `MainMenu` (`LoadTxt`, 4 colonnes) | 319 noms de donateurs de Vinfamy |
| Donate | `MainMenu` EventGraph, `K2Node_CallFunction_45` (`LaunchURL`, 3 boutons) | `https://www.patreon.com/vinfamy` |
| Discord | `MainMenu` EventGraph, `K2Node_CallFunction_32` (`LaunchURL`) | `https://discord.gg/eUEUhc6` |
| Commission Game Content | `MainMenu`, `CommissionButton` (visibilité liée à une fonction de l'écran titre) | visible |
`GI.version_main` et `version_sub` servent aussi aux sauvegardes et à `GI.ReturnStatIfNeeded` : ils ne sont pas modifiés.
## 2. Réalisation
- **Version** : format du menu ramené à `LifePlay {text}` ; nouveau sous-système `Source/LifePlay/LPVersionSubsystem`
qui remplit `GI.version_text` au démarrage avec `ProjectVersion` de `Config/DefaultGame.ini`
(`[/Script/EngineSettings.GeneralProjectSettings]`). Format : **année.mois.publication**, le dernier nombre étant un
compteur de publications (V1.4 = 4) : `ProjectVersion=2026.10.4`. **À chaque publication**, mettre à jour cette ligne
(année et mois de la publication, compteur + 1).
- **Menu** : nouvelle commandlet `LPEditWidget` (`Source/LifePlayEditorTools`), pilotée par une spécification versionnée
`Tools/ui/MainMenu_V1.4.json` ; chaque modification indique la valeur attendue avant (sinon rien n'est écrit), le
widget est compilé sans erreur puis sauvegardé. Modifications : format de version, URL Donate
(`https://www.patreon.com/cw/edujiro5972`) et Discord (`https://discord.gg/q4689pNSy`), titre des crédits
(« Original author: »), infobulle Donate (« Support LifePlay Revival on Patreon »), `CommissionButton` masqué
(liaison de visibilité retirée).
- **Crédits** : `Content/Modules/donors.txt` réduit à `Vinfamy`.
- Liens laissés inchangés (non demandés) : f95zone et bilan 2021 de Vinfamy.
## 3. Contrôles
- Essai à blanc : 6 modifications trouvées, toutes conformes aux valeurs attendues.
- Comparaison avant/après (`LPDumpFunction`, `LPDumpWidgetTree`) : seuls changent les broches `main`/`sub` du format
(retirées), les deux URL, le titre, l'infobulle et la visibilité du bouton.
- Build `LifePlay_Build_5.5_UI14` : « Version affichee : LifePlay 2026.10.4 » ; avertissements de script identiques au
build précédent (16 « Accessed None », déjà présents).
- Sauvegarde : `D:\Game\LifePlay_Backup_5.5\UI14` (`MainMenu.uasset`, `donors.txt`) ; retour : `Tools\Restore.ps1 -Lot UI14`.

View file

@ -938,3 +938,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol
| CHG-091 | 2026-10-02 | Doc | nouveaux `Docs/Migration/13_Protocole_Coiffure.md`, `Tools/HairImportFbx.py`, `Tools/HairMaterials.py` ; `README.md` (migration), `08_Guide_Developpeur.md`, `09_Commandes_Utiles.md` | protocole de conversion d'une coiffure (demande utilisateur) ; scripts ponctuels d'O7e rendus paramétrables ; protocole rejoué de bout en bout sur Pocolov 17 sous le nom `Pocolov17P` : bornes identiques au millimètre, dépendances OK ; essai retiré du projet (`D:\Game\LifePlay_Backup_5.5\O7e\proto_essai`) | `git revert` du commit |
| CHG-092 | 2026-10-02 | HAIRS1 | nouveaux `Tools/HairPipeline.ps1`, `Tools/FbxMaterialMap.py`, `Tools/hairs/HAIRS1.json` ; `Tools/HairImportFbx.py` (rapport JSON), `Tools/HairMaterials.py` (calotte seulement si utilisée, contrôle des dépendances) ; assets `Content/Heavy/New5/Hairs/Pocolov03/06/11/15/16*` (hors dépôt) ; `Content/Modules/lp_Revival/Hairstyles/pocolov03/06/11/15/16.lphair` ; `Licenses/Free_3D_Models_Credits.txt` ; `13_Protocole_Coiffure.md` §7 | industrialisation de la conversion (demande utilisateur) et test sur 5 coiffures Pocolov (CC BY 4.0, téléchargées avec l'accord de l'utilisateur) ; 5 cas d'archives différents intégrés aux règles ; build `LifePlay_Build_5.5_HAIRS1` : 6 coiffures ajoutées (index 126 à 131) ; contrôle visuel en jeu en attente | retirer les `.lphair` ; assets et sources dans `D:\Game\LifePlay_Backup_5.5\HAIRS1` ; `git checkout HAIRS1-avant -- Tools Licenses Content/Modules` |
| CHG-093 | 2026-10-02 | SKIN | `LPHairFromStaticMeshCommandlet` : liaison par transfert de poids depuis le corps (`-Skin=transfer` par défaut, 6 voisins, bras et jambes exclus) et sortie de la peau (`-Gap` 0,3 cm) ; `LifePlayEditorTools.Build.cs` (`SkeletalMeshDescription`, `AnimationCore`) ; `HairPipeline.ps1` (`-Rebuild`, options `skin`, `gap`) ; 6 coiffures Pocolov reconstruites (versions précédentes dans `HAIRS1\<nom>\precedente_*`), Pocolov 17 ajoutée au lot ; `.lphair` et crédits : description des modifications corrigée ; `13_Protocole_Coiffure.md` | vidéo utilisateur : mèches longues liées à la seule tête, qui traversent épaules et poitrine quand la tête tourne ; poids obtenus : tête 47 à 62 %, cou, clavicules, poitrine ; 254 à 950 sommets sortis de la peau par coiffure ; build `LifePlay_Build_5.5_SKIN` ; validé en jeu le 2026-10-02 (les mèches ne traversent plus la peau quand la tête tourne) | `-Skin=rigid` ; `git checkout SKIN-avant -- Source Tools` et remettre les versions précédentes |
| CHG-094 | 2026-10-02 | UI14 | nouveaux `Source/LifePlay/LPVersionSubsystem.h/.cpp`, `Source/LifePlayEditorTools/LPEditWidgetCommandlet.h/.cpp` (+ `Json` dans le `.Build.cs`), `Tools/ui/MainMenu_V1.4.json`, `Docs/Migration/14_Interface_V1.4.md` ; `Content/UI/MainMenu.uasset` (6 modifications, diff conforme) ; `Content/Modules/donors.txt` (→ `Vinfamy`) ; `Config/DefaultGame.ini` (`ProjectVersion=2026.10.4`) | demande utilisateur avant la V1.4 : version année.mois.publication, crédits vidés avec Vinfamy auteur original, Donate → Patreon edujiro5972, Discord → discord.gg/q4689pNSy, Commission masqué ; build `LifePlay_Build_5.5_UI14` : « LifePlay 2026.10.4 » ; contrôle en jeu en attente | `Tools\Restore.ps1 -Lot UI14` ; `git checkout UI14-avant -- Config Source Content/Modules/donors.txt` |

View file

@ -24,6 +24,7 @@ La migration a été reprise le 2026-09-29, après l'échec d'un premier portage
| [11_Audit_Optimisation.md](11_Audit_Optimisation.md) | pilotage | chantier d'optimisation UE5 : audit (lot O0), goulots, leviers, lots O1 à O8, Fab |
| [12_Reconciliation_Vinfamy.md](12_Reconciliation_Vinfamy.md) | historique | comparaison avec `vinfamy/LifePlay`, fusion d'historique, rien à récupérer |
| [13_Protocole_Coiffure.md](13_Protocole_Coiffure.md) | protocole | ajouter une coiffure (FBX statique, Fab) : import, ajustement sur la tête de Bridget, matériaux, fichier `.lphair`, contrôles |
| [14_Interface_V1.4.md](14_Interface_V1.4.md) | lot | menu principal : version année.mois.publication, crédits (Vinfamy, auteur original), liens Donate et Discord, bouton Commission masqué |
| [Journal_Modifications.md](Journal_Modifications.md) | traçabilité | `CHG-001` à `CHG-057`, chacune avec son retour arrière |
| [Registre_Erreurs.md](Registre_Erreurs.md) | traçabilité | `ERR-001` à `ERR-037`, statut et lot |
| `correctifs/` | relecture | écarts de code 4.27 → 5.5 en patchs (`P01` à `P06`) |

View file

@ -0,0 +1,23 @@
// Portage UE 5.5 - voir LPVersionSubsystem.h
#include "LPVersionSubsystem.h"
#include "Engine/GameInstance.h"
#include "GeneralProjectSettings.h"
#include "LifePlay.h"
#include "UObject/UnrealType.h"
void ULPVersionSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UGameInstance* GI = GetGameInstance();
const FStrProperty* Prop = GI ? CastField<FStrProperty>(GI->GetClass()->FindPropertyByName(TEXT("version_text"))) : nullptr;
const FString Version = GetDefault<UGeneralProjectSettings>()->ProjectVersion;
if (!Prop)
{
UE_LOG(LogLifePlay, Warning, TEXT("Version : GI.version_text introuvable sur %s, version %s non affichee"), *GetNameSafe(GI), *Version);
return;
}
Prop->SetPropertyValue_InContainer(GI, Version);
UE_LOG(LogLifePlay, Display, TEXT("Version affichee : LifePlay %s"), *Version);
}

View file

@ -0,0 +1,21 @@
// Portage UE 5.5 - lot UI14 (Docs/Migration/14_Interface_V1.4.md).
//
// Le menu principal affiche "LifePlay {text}" (UI/MainMenu, Get_Version_Text_0) avec text = GI.version_text. Ce
// sous-systeme remplit GI.version_text au demarrage avec la version du projet (Config/DefaultGame.ini,
// [/Script/EngineSettings.GeneralProjectSettings] ProjectVersion, format annee.mois.publication : 2026.10.4).
// GI.version_main et GI.version_sub (5 et 9, utilises par les sauvegardes et ReturnStatIfNeeded) ne sont pas modifies.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "LPVersionSubsystem.generated.h"
UCLASS()
class LIFEPLAY_API ULPVersionSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
};

View file

@ -0,0 +1,240 @@
// Portage UE 5.5 - voir LPEditWidgetCommandlet.h
#include "LPEditWidgetCommandlet.h"
#include "Blueprint/WidgetTree.h"
#include "Components/TextBlock.h"
#include "Components/Widget.h"
#include "Dom/JsonObject.h"
#include "EdGraph/EdGraph.h"
#include "EdGraphSchema_K2.h"
#include "K2Node.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
#include "WidgetBlueprint.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPEditWidget, Log, All);
namespace
{
UEdGraphNode* FindNode(UBlueprint* BP, const FString& GraphName, const FString& NodeName)
{
TArray<UEdGraph*> Graphs;
BP->GetAllGraphs(Graphs);
for (UEdGraph* Graph : Graphs)
{
if (Graph && Graph->GetName() == GraphName)
{
for (UEdGraphNode* Node : Graph->Nodes)
{
if (Node && Node->GetName() == NodeName)
{
return Node;
}
}
}
}
return nullptr;
}
/** Valeur par defaut d'une broche, texte ou non, telle qu'affichee. */
FString PinValue(const UEdGraphPin* Pin)
{
return Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Text ? Pin->DefaultTextValue.ToString() : Pin->DefaultValue;
}
bool ParseVisibility(const FString& Text, ESlateVisibility& Out)
{
const UEnum* Enum = StaticEnum<ESlateVisibility>();
const int64 Value = Enum->GetValueByNameString(Text);
if (Value == INDEX_NONE)
{
return false;
}
Out = static_cast<ESlateVisibility>(Value);
return true;
}
}
ULPEditWidgetCommandlet::ULPEditWidgetCommandlet()
{
IsClient = false;
IsEditor = true;
IsServer = false;
LogToConsole = true;
}
int32 ULPEditWidgetCommandlet::Main(const FString& Params)
{
TArray<FString> Tokens, Switches;
TMap<FString, FString> P;
ParseCommandLine(*Params, Tokens, Switches, P);
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
FString Json;
TSharedPtr<FJsonObject> Spec;
if (!FFileHelper::LoadFileToString(Json, *P.FindRef(TEXT("Spec"))) || !FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Json), Spec) || !Spec.IsValid())
{
UE_LOG(LogLPEditWidget, Error, TEXT("Usage : -Spec=<fichier.json> [-DryRun] (fichier illisible : %s)"), *P.FindRef(TEXT("Spec")));
return 1;
}
const FString BPPath = Spec->GetStringField(TEXT("bp"));
UWidgetBlueprint* BP = LoadObject<UWidgetBlueprint>(nullptr, *(BPPath + TEXT(".") + FPackageName::GetShortName(BPPath)));
if (!BP || !BP->WidgetTree)
{
UE_LOG(LogLPEditWidget, Error, TEXT("Blueprint de widget introuvable : %s"), *BPPath);
return 1;
}
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
int32 Errors = 0, Changes = 0;
// 1. Broches.
const TArray<TSharedPtr<FJsonValue>>* Pins = nullptr;
if (Spec->TryGetArrayField(TEXT("pins"), Pins))
{
for (const TSharedPtr<FJsonValue>& V : *Pins)
{
const TSharedPtr<FJsonObject>& O = V->AsObject();
const FString Where = O->GetStringField(TEXT("graph")) + TEXT(" / ") + O->GetStringField(TEXT("node")) + TEXT(" . ") + O->GetStringField(TEXT("pin"));
UEdGraphNode* Node = FindNode(BP, O->GetStringField(TEXT("graph")), O->GetStringField(TEXT("node")));
UEdGraphPin* Pin = Node ? Node->FindPin(*O->GetStringField(TEXT("pin"))) : nullptr;
if (!Pin)
{
UE_LOG(LogLPEditWidget, Error, TEXT("Broche introuvable : %s"), *Where);
++Errors;
continue;
}
const FString Before = PinValue(Pin);
const FString Expect = O->GetStringField(TEXT("expect"));
const FString Value = O->GetStringField(TEXT("value"));
if (Before == Value)
{
UE_LOG(LogLPEditWidget, Warning, TEXT(" %s : deja a la valeur cible"), *Where);
continue;
}
if (Before != Expect)
{
UE_LOG(LogLPEditWidget, Error, TEXT(" %s : valeur actuelle '%s', attendue '%s'"), *Where, *Before, *Expect);
++Errors;
continue;
}
UE_LOG(LogLPEditWidget, Warning, TEXT(" %s : '%s' -> '%s'"), *Where, *Before, *Value);
if (!bDryRun)
{
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Text)
{
Schema->TrySetDefaultText(*Pin, FText::FromString(Value));
}
else
{
Schema->TrySetDefaultValue(*Pin, Value);
}
if (PinValue(Pin) != Value)
{
UE_LOG(LogLPEditWidget, Error, TEXT(" %s : valeur relue '%s' apres modification"), *Where, *PinValue(Pin));
++Errors;
}
}
++Changes;
}
}
// 2. Elements de l'arbre.
const TArray<TSharedPtr<FJsonValue>>* Widgets = nullptr;
if (Spec->TryGetArrayField(TEXT("widgets"), Widgets))
{
for (const TSharedPtr<FJsonValue>& V : *Widgets)
{
const TSharedPtr<FJsonObject>& O = V->AsObject();
const FString Name = O->GetStringField(TEXT("name"));
UWidget* Widget = BP->WidgetTree->FindWidget(FName(*Name));
if (!Widget)
{
UE_LOG(LogLPEditWidget, Error, TEXT("Element introuvable : %s"), *Name);
++Errors;
continue;
}
FString Text;
if (O->TryGetStringField(TEXT("text"), Text))
{
UTextBlock* TextBlock = Cast<UTextBlock>(Widget);
const FString Before = TextBlock ? TextBlock->GetText().ToString() : FString();
if (!TextBlock || Before != O->GetStringField(TEXT("expect_text")))
{
UE_LOG(LogLPEditWidget, Error, TEXT(" %s : texte actuel '%s', attendu '%s'"), *Name, *Before, *O->GetStringField(TEXT("expect_text")));
++Errors;
}
else
{
UE_LOG(LogLPEditWidget, Warning, TEXT(" %s.Text : '%s' -> '%s'"), *Name, *Before, *Text);
if (!bDryRun) { TextBlock->SetText(FText::FromString(Text)); }
++Changes;
}
}
FString Tooltip;
if (O->TryGetStringField(TEXT("tooltip"), Tooltip))
{
UE_LOG(LogLPEditWidget, Warning, TEXT(" %s.ToolTipText : '%s' -> '%s'"), *Name, *Widget->GetToolTipText().ToString(), *Tooltip);
if (!bDryRun) { Widget->SetToolTipText(FText::FromString(Tooltip)); }
++Changes;
}
FString VisibilityText;
if (O->TryGetStringField(TEXT("visibility"), VisibilityText))
{
ESlateVisibility Visibility;
if (!ParseVisibility(VisibilityText, Visibility))
{
UE_LOG(LogLPEditWidget, Error, TEXT(" %s : visibilite inconnue '%s'"), *Name, *VisibilityText);
++Errors;
continue;
}
const int32 Bound = BP->Bindings.IndexOfByPredicate([&](const FDelegateEditorBinding& B) { return B.ObjectName == Name && B.PropertyName == TEXT("Visibility"); });
UE_LOG(LogLPEditWidget, Warning, TEXT(" %s.Visibility : %s -> %s%s"), *Name, *StaticEnum<ESlateVisibility>()->GetNameStringByValue(int64(Widget->GetVisibility())),
*VisibilityText, Bound != INDEX_NONE ? TEXT(" (liaison de visibilite retiree)") : TEXT(""));
if (!bDryRun)
{
Widget->SetVisibility(Visibility);
if (Bound != INDEX_NONE) { BP->Bindings.RemoveAt(Bound); }
}
++Changes;
}
}
}
if (Errors > 0)
{
UE_LOG(LogLPEditWidget, Error, TEXT("%d erreur(s) : rien n'est sauvegarde"), Errors);
return 1;
}
if (bDryRun || Changes == 0)
{
UE_LOG(LogLPEditWidget, Warning, TEXT("%d modification(s) %s"), Changes, bDryRun ? TEXT("(essai a blanc, rien n'est sauvegarde)") : TEXT(": rien a faire"));
return 0;
}
// 3. Compilation et sauvegarde.
BP->Modify();
FKismetEditorUtilities::CompileBlueprint(BP);
if (BP->Status == BS_Error)
{
UE_LOG(LogLPEditWidget, Error, TEXT("Compilation en erreur : rien n'est sauvegarde"));
return 1;
}
UPackage* Package = BP->GetOutermost();
Package->MarkPackageDirty();
const FString Filename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
SaveArgs.Error = GWarn;
if (!UPackage::SavePackage(Package, nullptr, *Filename, SaveArgs))
{
UE_LOG(LogLPEditWidget, Error, TEXT("Echec de sauvegarde de %s"), *Filename);
return 1;
}
UE_LOG(LogLPEditWidget, Warning, TEXT("%d modification(s), compile sans erreur, sauvegarde : %s"), Changes, *Filename);
return 0;
}

View file

@ -0,0 +1,28 @@
// Portage UE 5.5 - lot UI14 (interface du menu principal avant la V1.4, Docs/Migration/14_Interface_V1.4.md).
//
// Applique a un Blueprint de widget les modifications decrites par un fichier JSON (versionne dans Tools/ui/) :
// "pins" : valeur par defaut d'une broche d'un noeud ({ "graph", "node", "pin", "expect", "value" }) ; pour une
// broche texte (Format d'un FormatText) la valeur est un texte, le noeud reconstruit ses broches ;
// "widgets" : proprietes d'un element de l'arbre ({ "name", "expect_text", "text", "tooltip", "visibility" }) ;
// "visibility" retire aussi une liaison (binding) de visibilite qui l'ecraserait.
// Chaque modification indique la valeur attendue AVANT ("expect", "expect_text") : au moindre ecart, ou element
// introuvable, rien n'est sauvegarde. Le Blueprint est ensuite compile (aucune erreur toleree) puis sauvegarde.
//
// Usage :
// UnrealEditor-Cmd.exe <projet> -run=LPEditWidget -Spec=<projet>/Tools/ui/<fichier>.json [-DryRun]
#pragma once
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "LPEditWidgetCommandlet.generated.h"
UCLASS()
class ULPEditWidgetCommandlet : public UCommandlet
{
GENERATED_BODY()
public:
ULPEditWidgetCommandlet();
virtual int32 Main(const FString& Params) override;
};

View file

@ -45,7 +45,9 @@ public class LifePlayEditorTools : ModuleRules
"SkeletalMeshUtilitiesCommon",
// Transfert de poids (FSkeletalMeshAttributes, FBoneWeight)
"SkeletalMeshDescription",
"AnimationCore"
"AnimationCore",
// UI14 : LPEditWidgetCommandlet lit sa specification JSON
"Json"
});
}
}

View file

@ -0,0 +1,16 @@
{
"bp": "/Game/UI/MainMenu",
"pins": [
{ "graph": "Get_Version_Text_0", "node": "K2Node_FormatText_0", "pin": "Format",
"expect": "LifePlay v{main}.{sub}{text}", "value": "LifePlay {text}" },
{ "graph": "EventGraph", "node": "K2Node_CallFunction_45", "pin": "URL",
"expect": "https://www.patreon.com/vinfamy", "value": "https://www.patreon.com/cw/edujiro5972" },
{ "graph": "EventGraph", "node": "K2Node_CallFunction_32", "pin": "URL",
"expect": "https://discord.gg/eUEUhc6", "value": "https://discord.gg/q4689pNSy" }
],
"widgets": [
{ "name": "TextBlock_19", "expect_text": "This project wouldn't be possible without the suppport from my generous patrons:", "text": "Original author:" },
{ "name": "DonateButton", "tooltip": "Support LifePlay Revival on Patreon" },
{ "name": "CommissionButton", "visibility": "Collapsed" }
]
}