// Fill out your copyright notice in the Description page of Project Settings. #include "SurvivalGameInstance.h" #include "ContentStreaming.h" #include "EnhancedInputSubsystems.h" #include "Engine/Engine.h" #include "Engine/GameViewportClient.h" #include "Engine/LocalPlayer.h" #include "GameMapsSettings.h" #include "InputMappingContext.h" #include "KeyboardLayout.h" #include "Kismet/GameplayStatics.h" #include "SessionSubsystem.h" #include "SurvivalUserSettings.h" #include "UserSettings/EnhancedInputUserSettings.h" void USurvivalGameInstance::Init() { Super::Init(); // Pas d'enregistrement des IMC ici : a ce stade il n'y a ni joueur local ni // PlayerController, donc pas encore de UserSettings a qui parler. // RegisterMappableContexts() est appelee par l'ecran de reglages. // Les reglages sont deja LUS depuis le .ini par le moteur, mais rien ne les // a encore APPLIQUES : sans cet appel, la resolution et la qualite // choisies au dernier lancement resteraient lettre morte jusqu'au premier // passage dans le menu. // // C'est le bon endroit : le GameInstance est le seul objet qui naisse avant // la premiere map et survive a tous les changements de niveau. if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) { Settings->ApplySettings(/*bCheckForCommandLineOverrides=*/false); } else { UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, aucun reglage ne sera pris en compte.")); } // Filet de securite reseau. On s'abonne ici, une fois pour toutes : ces // evenements arrivent au pire moment, typiquement quand le monde est en // train d'etre detruit, et un abonnement porte par un acteur mourrait // justement avec lui. if (GEngine) { NetworkFailureHandle = GEngine->OnNetworkFailure().AddUObject(this, &USurvivalGameInstance::HandleNetworkFailure); TravelFailureHandle = GEngine->OnTravelFailure().AddUObject(this, &USurvivalGameInstance::HandleTravelFailure); } PreLoadMapHandle = FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &USurvivalGameInstance::HandlePreLoadMap); } void USurvivalGameInstance::HandlePreLoadMap(const FString& MapName) { // On coupe AVANT que quoi que ce soit du nouveau monde n'existe. C'est le // premier controller pret qui rallumera, une fois son voile noir en place -- // et il naitra forcement apres nous, ce qui garantit le bon ordre. SetWorldRenderingSuspended(true); UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : chargement de [%s], rendu du monde suspendu."), *MapName); // Le timer du GameInstance et non celui du monde : celui-ci est justement en // train d'etre detruit. GetTimerManager().SetTimer( RenderingResumeTimeout, this, &USurvivalGameInstance::ResumeWorldRendering, 20.f, /*bLoop=*/false); } void USurvivalGameInstance::BeginSceneWarmup(FSimpleDelegate OnComplete) { SceneWarmupCompleted = OnComplete; RemainingWarmupFrames = FMath::Max(SceneWarmupFrames, 0); WarmupStartTime = FPlatformTime::Seconds(); if (RemainingWarmupFrames <= 0) { SceneWarmupCompleted.ExecuteIfBound(); SceneWarmupCompleted.Unbind(); return; } TickSceneWarmup(); } void USurvivalGameInstance::TickSceneWarmup() { const double Elapsed = FPlatformTime::Seconds() - WarmupStartTime; const bool bCapReached = Elapsed >= SceneWarmupMaxSeconds; if (RemainingWarmupFrames > 0 && !bCapReached) { --RemainingWarmupFrames; // SetTimerForNextTick et non un timer en secondes : une image rendue, // un tick. C'est l'unite dans laquelle Nanite et Lumen progressent. GetTimerManager().SetTimerForNextTick(this, &USurvivalGameInstance::TickSceneWarmup); return; } UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : chauffe terminee -- %d images rendues en %.0f ms%s."), SceneWarmupFrames - RemainingWarmupFrames, Elapsed * 1000.0, bCapReached ? TEXT(" (plafond atteint)") : TEXT("")); // Copie avant execution : le rappel declenche un fondu, qui pourrait // theoriquement relancer une chauffe et ecraser le delegue en pleine main. FSimpleDelegate Completed = SceneWarmupCompleted; SceneWarmupCompleted.Unbind(); Completed.ExecuteIfBound(); } void USurvivalGameInstance::SetWorldRenderingSuspended(bool bSuspended) { if (UGameViewportClient* Viewport = GetGameViewportClient()) { Viewport->bDisableWorldRendering = bSuspended; } } void USurvivalGameInstance::ResumeWorldRendering() { GetTimerManager().ClearTimer(RenderingResumeTimeout); // Streaming vide AVANT de rallumer. UpdateResourceStreaming(0, true) traite // tout sans avoir besoin d'un point de vue -- ce qui tombe bien, le rendu // est encore coupe donc personne ne lui en a fourni -- puis on bloque le // temps que les requetes aboutissent. // // Ce blocage a lieu ECRAN NOIR : c'est du temps de chargement, pas une // saccade. C'est exactement ce que le moteur fait lui-meme a l'ouverture // d'une map, et c'est ce qui evite de voir le terrain se raffiner et les // modeles apparaitre un a un. const double StartTime = FPlatformTime::Seconds(); const int32 PendingRequests = IStreamingManager::Get().StreamAllResources(FMath::Max(StreamingFlushTimeLimit, 0.f)); const double ElapsedMs = (FPlatformTime::Seconds() - StartTime) * 1000.0; SetWorldRenderingSuspended(false); UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : rendu du monde repris apres %.0f ms de streaming (%d requete(s) restante(s))."), ElapsedMs, PendingRequests); } void USurvivalGameInstance::Shutdown() { if (GEngine) { GEngine->OnNetworkFailure().Remove(NetworkFailureHandle); GEngine->OnTravelFailure().Remove(TravelFailureHandle); } FCoreUObjectDelegates::PreLoadMap.Remove(PreLoadMapHandle); Super::Shutdown(); } void USurvivalGameInstance::HandleNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString) { UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : echec reseau [%s] -- %s"), ENetworkFailure::ToString(FailureType), *ErrorString); // Depart deja en cours : c'est le cas NORMAL quand l'hote quitte, puisqu'il // a prevenu tout le monde avant de couper. On laisse les fondus finir. if (bReturningToMainMenu) { UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : depart deja en cours, echec ignore.")); return; } // Ici en revanche personne n'a prevenu : hote qui plante, cable arrache, // relais Steam qui lache. C'est le filet de secours, sans fondu -- mieux // vaut un retour brutal au menu qu'un monde mort. ReturnToMainMenu(); } void USurvivalGameInstance::HandleTravelFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ErrorString) { UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : echec de voyage [%s] -- %s"), ETravelFailure::ToString(FailureType), *ErrorString); if (bReturningToMainMenu) { return; } ReturnToMainMenu(); } void USurvivalGameInstance::ReturnToMainMenu() { bReturningToMainMenu = true; // La session est morte avec la connexion : la detruire ici evite qu'elle // reste annoncee a tes amis, et surtout qu'elle bloque la creation de la // suivante. if (USessionSubsystem* Sessions = GetSubsystem()) { Sessions->LeaveSession(); } // GetGameDefaultMap et pas un chemin en dur : c'est deja MenuScene dans // DefaultEngine.ini, et une seconde copie du chemin finirait par diverger. const FString MenuMap = UGameMapsSettings::GetGameDefaultMap(); if (MenuMap.IsEmpty()) { UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : aucune GameDefaultMap, impossible de revenir au menu.")); return; } UGameplayStatics::OpenLevel(this, FName(*MenuMap), /*bAbsolute=*/true); } void USurvivalGameInstance::RegisterMappableContexts() { ULocalPlayer* LocalPlayer = GetFirstGamePlayer(); if (!LocalPlayer) { UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : aucun joueur local, enregistrement des touches remis a plus tard.")); return; } if (MappableContexts.IsEmpty()) { UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : MappableContexts est vide, l'onglet Touches affichera 'Non assignee' partout. Ajoute IMC_Default dans le Blueprint de GameInstance.")); return; } UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem(); UEnhancedInputUserSettings* InputSettings = Subsystem ? Subsystem->GetUserSettings() : nullptr; if (!InputSettings) { UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : reglages d'input introuvables. Coche 'Enable User Settings' dans Project Settings > Enhanced Input.")); return; } bool bRegisteredAnyContext = false; for (const TObjectPtr& Context : MappableContexts) { if (Context) { bRegisteredAnyContext = true; // Idempotent : le moteur ignore un contexte deja connu, on peut donc // rappeler cette fonction sans precaution. InputSettings->RegisterInputMappingContext(Context); } } // APRES l'enregistrement et jamais avant : le profil de touches ne connait un // mapping -- donc sa touche par defaut -- qu'une fois son IMC declare. Appeler // la conversion plus tot la ferait tourner sur un profil vide, sans erreur ni // effet, ce qui est le pire des cas a diagnostiquer. // // UNE SEULE FOIS par session, en revanche. Cette fonction est aussi le point // d'entree du rafraichissement de l'onglet Touches, appele apres CHAQUE // remappage : y laisser la conversion reprendrait sa touche au joueur qui a // choisi celle de l'IMC -- la lettre W sur un AZERTY est identique au defaut, // donc indiscernable d'un mapping auquel personne n'a touche. if (bRegisteredAnyContext && !bKeyboardLayoutApplied) { bKeyboardLayoutApplied = true; ApplyKeyboardLayoutToDefaultKeys(); } } void USurvivalGameInstance::ApplyKeyboardLayoutToDefaultKeys() { const ULocalPlayer* LocalPlayer = GetFirstGamePlayer(); if (!LocalPlayer) { return; } UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem(); UEnhancedInputUserSettings* InputSettings = Subsystem ? Subsystem->GetUserSettings() : nullptr; const UEnhancedPlayerMappableKeyProfile* Profile = InputSettings ? InputSettings->GetActiveKeyProfile() : nullptr; if (!Profile) { return; } // On COLLECTE avant d'appliquer, au lieu de mapper au fil de la lecture. // MapPlayerKey ecrit dans les memes lignes que celles qu'on parcourt : muter un // conteneur pendant son iteration invalide les iterateurs des que le stockage // est reorganise. Le bug qui en sort est intermittent et ne ressemble a rien. struct FPendingRemap { FName MappingName; EPlayerMappableKeySlot Slot; FKey LayoutKey; }; TArray PendingRemaps; for (const TPair& Row : Profile->GetPlayerMappingRows()) { for (const FPlayerKeyMapping& Mapping : Row.Value.Mappings) { // Le joueur a deja choisi cette touche : son choix fait autorite, meme // s'il est incoherent avec sa disposition. C'est aussi ce qui rend la // fonction idempotente -- notre propre conversion compte comme un choix // des qu'elle est posee, donc on ne repasse jamais dessus. if (Mapping.IsCustomized()) { continue; } const FKey DefaultKey = Mapping.GetDefaultKey(); const FKey LayoutKey = KeyboardLayout::RemapKeyToActiveLayout(DefaultKey); // Clavier QWERTY, touche insensible a la disposition, ou manette : la // conversion rend la touche d'origine et il n'y a rien a ecrire. Sans ce // test on poserait une surcharge identique au defaut sur chaque mapping, // et "Reinitialiser" n'aurait plus rien a reinitialiser. if (LayoutKey == DefaultKey) { continue; } PendingRemaps.Add({ Row.Key, Mapping.GetSlot(), LayoutKey }); } } // Journalise AVANT de sortir, et donc meme quand il n'y a rien a faire. C'est // la seule trace lisible quand un testeur signale des touches inattendues, et // le cas "0 touche" est justement celui qu'il faut pouvoir distinguer du // silence : sans cette ligne, un clavier QWERTY et un systeme qui n'a pas // tourne du tout produisent exactement le meme log, c'est-a-dire aucun. // // Un KLID se lit en deux temps : "0000040C" est un AZERTY francais, "00000409" // un QWERTY US. Un utilisateur francais sur clavier QWERTY rend bien 00000409, // parce que c'est la DISPOSITION qui est journalisee, pas la langue -- et c'est // exactement la distinction sur laquelle une detection par pays se tromperait. UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : disposition clavier [%s], %d touche(s) a traduire depuis le QWERTY de reference."), *KeyboardLayout::GetActiveLayoutId(), PendingRemaps.Num()); if (PendingRemaps.IsEmpty()) { return; } for (const FPendingRemap& Remap : PendingRemaps) { FMapPlayerKeyArgs Args; Args.MappingName = Remap.MappingName; Args.Slot = Remap.Slot; Args.NewKey = Remap.LayoutKey; FGameplayTagContainer FailureReason; InputSettings->MapPlayerKey(Args, FailureReason); if (!FailureReason.IsEmpty()) { UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : conversion de '%s' vers '%s' refusee (%s)."), *Remap.MappingName.ToString(), *Remap.LayoutKey.ToString(), *FailureReason.ToString()); } } // Pousse les surcharges dans les contextes deja actifs : le pawn n'a rien a // rebrancher, les touches sont bonnes des la premiere image. InputSettings->ApplySettings(); }