diff --git a/Plugins/ProceduralDungeon/ProceduralDungeon.uplugin b/Plugins/ProceduralDungeon/ProceduralDungeon.uplugin new file mode 100644 index 0000000..57552ff --- /dev/null +++ b/Plugins/ProceduralDungeon/ProceduralDungeon.uplugin @@ -0,0 +1,26 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "2.0.0", + "FriendlyName": "ProceduralDungeon", + "Description": "Create procedural dungeons like \"The Binding of Isaac\" or \"Rogue Legacy\" but in 3D.\r\nYou can define your own generation rules.", + "Category": "Procedural", + "CreatedBy": "Ben Pyton", + "CreatedByURL": "https://github.com/BenPyton", + "DocsURL": "https://github.com/BenPyton/ProceduralDungeon/wiki", + "MarketplaceURL": "", + "SupportURL": "https://github.com/BenPyton/ProceduralDungeon/issues", + "EngineVersion": "4.27.0", + "CanContainContent": false, + "Installed": true, + "Modules": [ + { + "Name": "ProceduralDungeon", + "Type": "Runtime", + "LoadingPhase": "PreDefault", + "WhitelistPlatforms": [ + "Win64" + ] + } + ] +} \ No newline at end of file diff --git a/Plugins/ProceduralDungeon/Resources/Icon128.png b/Plugins/ProceduralDungeon/Resources/Icon128.png new file mode 100644 index 0000000..99e500e Binary files /dev/null and b/Plugins/ProceduralDungeon/Resources/Icon128.png differ diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/Door.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/Door.cpp new file mode 100644 index 0000000..43a6f00 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/Door.cpp @@ -0,0 +1,117 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "Door.h" +#include "Room.h" +#include "RoomLevel.h" +#include "DrawDebugHelpers.h" + +// Sets default values +ADoor::ADoor() +{ + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + + RootComponent = CreateDefaultSubobject(TEXT("SceneComponent")); +} + +// Called every frame +void ADoor::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + bLocked = !bAlwaysUnlocked && + ((RoomA == nullptr || (RoomA->GetLevelScript() != nullptr && RoomA->GetLevelScript()->IsLocked)) + || (RoomB == nullptr || (RoomB->GetLevelScript() != nullptr && RoomB->GetLevelScript()->IsLocked))); + + SetActorHiddenInGame( !bAlwaysVisible && + (RoomA == nullptr || (RoomA->GetLevelScript() != nullptr && RoomA->GetLevelScript()->IsHidden)) + && (RoomB == nullptr || (RoomB->GetLevelScript() != nullptr && RoomB->GetLevelScript()->IsHidden))); + + if (bLocked != bPrevLocked) + { + if (bLocked) + { + CloseDoor(); + OnDoorLock(); + OnDoorLock_BP(); + } + else + { + OnDoorUnlock(); + OnDoorUnlock_BP(); + } + } + + bPrevLocked = bLocked; + +#if WITH_EDITOR + DrawDebug(GetWorld()); +#endif +} + +void ADoor::OpenDoor() +{ + if (!bIsOpen && !bLocked) + { + bIsOpen = true; + OnDoorOpen(); + OnDoorOpen_BP(); + } +} + +void ADoor::CloseDoor() +{ + if (bIsOpen) + { + bIsOpen = false; + OnDoorClose(); + OnDoorClose_BP(); + } +} + +void ADoor::SetConnectingRooms(URoom * _RoomA, URoom * _RoomB) +{ + RoomA = _RoomA; + RoomB = _RoomB; +} + +void ADoor::DrawDebug(UWorld* World, FIntVector DoorCell, EDoorDirection DoorRot, FTransform Transform) +{ + if (URoom::DrawDebug()) + { + FVector DoorSize = URoom::DoorSize(); + FIntVector rot = URoom::GetDirection(DoorRot == EDoorDirection::NbDirection ? EDoorDirection::North : DoorRot); + FVector pos = URoom::GetRealDoorPosition(DoorCell, DoorRot) + FVector(0, 0, DoorSize.Z * 0.5f); + pos = Transform.TransformPosition(pos); + + // Arrow + DrawDebugDirectionalArrow(World, pos, pos + Transform.GetRotation() * FVector(rot) * 300, 300, FColor::Blue); + + // Door frame + FIntVector scale = URoom::Rotate(FIntVector(DoorSize * 0.5f), DoorRot); + DrawDebugBox(World, pos, FVector(scale), Transform.GetRotation(), FColor::Blue); + } +} + diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/DungeonGenerator.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/DungeonGenerator.cpp new file mode 100644 index 0000000..4f4becc --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/DungeonGenerator.cpp @@ -0,0 +1,546 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "DungeonGenerator.h" +#include "Engine/World.h" +#include "Engine.h" +#include "NavigationSystem.h" +#include "ProceduralLevelStreaming.h" +#include "RoomData.h" +#include "Room.h" +#include "Door.h" +#include "RoomLevel.h" +#include "ProceduralDungeon.h" +#include "ProceduralDungeonSettings.h" +#include "ProceduralDungeonLog.h" +#include "QueueOrStack.h" + +// Sets default values +ADungeonGenerator::ADungeonGenerator() +{ + // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. + PrimaryActorTick.bCanEverTick = true; + + GenerationType = EGenerationType::DFS; + SeedType = ESeedType::Random; + Seed = 123456789; // default Seed + + bAlwaysRelevant = true; + bReplicates = true; + NetPriority = 10.0f; + NetUpdateFrequency = 10; + } + +// Called when the game starts or when spawned +void ADungeonGenerator::BeginPlay() +{ + Super::BeginPlay(); +} + +void ADungeonGenerator::EndPlay(EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + UnloadAllRooms(); +} + +// Called every frame +void ADungeonGenerator::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + OnStateTick(CurrentState); +} + +void ADungeonGenerator::Generate() +{ + // Do it only on server, do nothing on clients + if (HasAuthority()) + { + if (SeedType == ESeedType::Random) + { + Random.GenerateNewSeed(); + Seed = Random.GetCurrentSeed(); + } + + BeginGeneration(Seed); + + if (SeedType == ESeedType::AutoIncrement) + { + Seed += 123456; + } + } +} + +void ADungeonGenerator::BeginGeneration_Implementation(uint32 GenerationSeed) +{ + Seed = GenerationSeed; + Random.Initialize(Seed); + LogInfo(FString::Printf(TEXT("Seed: %d"), Seed)); + SetState(EGenerationState::Unload); +} + +void ADungeonGenerator::CreateDungeon() +{ + IsInit = false; + NbInitRoom = 0; + int TriesLeft = MaxTry; + + // generate level until there IsValidDungeon return true + do { + TriesLeft--; + + // Reset generation data + UProceduralLevelStreaming::UniqueLevelInstanceId = 0; + ARoomLevel::Count = 0; + DispatchGenerationInit(); + + // Create the first room + RoomList.Empty(); + + URoomData* def = ChooseFirstRoomData(); + if(!IsValid(def)) + { + LogError("ChooseFirstRoomData returned null."); + continue; + } + + URoom* root = NewObject(); + root->Init(def); + RoomList.Add(root); + + // Create the list with the correct mode (depth or breadth) + TQueueOrStack::EMode listMode; + switch(GenerationType) + { + case EGenerationType::DFS: + listMode = TQueueOrStack::EMode::STACK; + break; + case EGenerationType::BFS: + listMode = TQueueOrStack::EMode::QUEUE; + break; + } + + // Build the list of rooms + TQueueOrStack roomStack(listMode); + roomStack.Push(root); + URoom* currentRoom = nullptr; + URoom* newRoom = nullptr; + while(ContinueToAddRoom() && !roomStack.IsEmpty()) + { + currentRoom = roomStack.Pop(); + check(IsValid(currentRoom)); // currentRoom should always be valid + for(URoom* room : AddNewRooms(*currentRoom)) + { + roomStack.Push(room); + } + } + + } while (TriesLeft > 0 && !IsValidDungeon()); +} + + +void ADungeonGenerator::InstantiateRoom(URoom* Room) +{ + // Instantiate room + Room->Instantiate(GetWorld()); + + for (int i = 0; i < Room->GetConnectionCount(); i++) + { + // Get next room + URoom* r = Room->GetConnection(i).Get(); + FIntVector DoorCell = Room->GetDoorWorldPosition(i); + EDoorDirection DoorRot = Room->GetDoorWorldOrientation(i); + int j = Room->GetOtherDoorIndex(i); + + // Don't instantiate door if it's the parent + if (!Room->IsDoorInstanced(i)) + { + TSubclassOf DoorClass = ChooseDoor(Room->GetRoomData(), nullptr != r ? r->GetRoomData() : nullptr); + + if (DoorClass != nullptr) + { + FVector InstanceDoorPos = URoom::GetRealDoorPosition(DoorCell, DoorRot); + FRotator InstanceDoorRot = FRotator(0, -90 * (int8)DoorRot, 0); + ADoor* Door = GetWorld()->SpawnActor(DoorClass, InstanceDoorPos, InstanceDoorRot); + + if (nullptr != Door) + { + DoorList.Add(Door); + Door->SetConnectingRooms(Room, r); + Room->SetDoorInstance(i, Door); + if(IsValid(r)) + { + r->SetDoorInstance(j, Door); + } + } + else + { + LogError("Failed to spawn Door, make sure you set door actor to always spawning."); + } + } + } + } +} + +TArray ADungeonGenerator::AddNewRooms(URoom& ParentRoom) +{ + TArray newRooms; + int nbDoor = ParentRoom.GetRoomData()->GetNbDoor(); + URoom* newRoom = nullptr; + for(int i = 0; i < nbDoor; ++i) + { + if(ParentRoom.IsConnected(i)) + continue; + + int nbTries = MaxRoomTry; + // Try to place a new room + do + { + nbTries--; + URoomData* def = ChooseNextRoomData(ParentRoom.GetRoomData()); + if(!IsValid(def)) + { + LogError("ChooseNextRoomData returned null."); + continue; + } + + // Create room from roomdef and set connections with current room + newRoom = NewObject(); + newRoom->Init(def); + int doorIndex = def->RandomDoor ? Random.RandRange(0, newRoom->GetRoomData()->GetNbDoor() - 1) : 0; + + // Place the room at its world position with the correct rotation + EDoorDirection parentDoorDir = ParentRoom.GetDoorWorldOrientation(i); + FIntVector newRoomPos = ParentRoom.GetDoorWorldPosition(i) + URoom::GetDirection(parentDoorDir); + newRoom->SetPositionAndRotationFromDoor(doorIndex, newRoomPos, URoom::Opposite(parentDoorDir)); + + // Test if it fit in the place + if(!URoom::Overlap(*newRoom, RoomList)) + { + // connect the doors to all possible existing rooms + URoom::Connect(*newRoom, doorIndex, ParentRoom, i); + if(URoom::CanLoop()) + { + newRoom->TryConnectToExistingDoors(RoomList); + } + RoomList.Add(newRoom); + newRooms.Add(newRoom); + DispatchRoomAdded(newRoom->GetRoomData()); + } + else + { + newRoom = nullptr; + } + } while(nbTries > 0 && newRoom == nullptr); + } + + return newRooms; +} + + +void ADungeonGenerator::LoadAllRooms() +{ + // When a level is correct, load all rooms + for (int i = 0; i < RoomList.Num(); i++) + { + InstantiateRoom(RoomList[i]); + } +} + +void ADungeonGenerator::UnloadAllRooms() +{ + for (int i = 0; i < DoorList.Num(); i++) + { + DoorList[i]->Destroy(); + } + DoorList.Empty(); + + for (int i = 0; i < RoomList.Num(); i++) + { + RoomList[i]->Destroy(GetWorld()); + } +} + +/* + * ======================================= + * State Machine + * ======================================= + */ +void ADungeonGenerator::SetState(EGenerationState NewState) +{ + OnStateEnd(CurrentState); + CurrentState = NewState; + OnStateBegin(CurrentState); +} + +void ADungeonGenerator::OnStateBegin(EGenerationState State) +{ + switch (State) + { + case EGenerationState::Unload: + LogInfo("======= Begin Unload All Levels ======="); + UnloadAllRooms(); + NbUnloadedRoom = 0; + break; + case EGenerationState::Generation: + DispatchPreGeneration(); + LogInfo("======= Begin Map Generation ======="); + CreateDungeon(); + break; + case EGenerationState::Load: + LogInfo("======= Begin Load All Levels ======="); + LoadAllRooms(); + NbLoadedRoom = 0; + break; + case EGenerationState::Initialization: + LogInfo("======= Begin Init All Levels ======="); + LogInfo(FString::Printf(TEXT("Nb Room To Initialize: %d"), RoomList.Num())); + break; + default: + break; + } +} + +void ADungeonGenerator::OnStateTick(EGenerationState State) +{ + int tmp = 0; + switch (State) + { + case EGenerationState::Unload: + + // Count nb level loaded + for (int i = 0; i < RoomList.Num(); i++) + { + if (RoomList[i]->IsInstanceUnloaded()) + { + tmp++; + } + } + // Change state when all levels are loaded + if (tmp == RoomList.Num()) + { + SetState(EGenerationState::Generation); + } + break; + case EGenerationState::Generation: + SetState(EGenerationState::Load); + break; + case EGenerationState::Load: + + // Count nb level loaded + for (int i = 0; i < RoomList.Num(); i++) + { + if (RoomList[i]->IsInstanceLoaded()) + { + tmp++; + } + } + // Change state when all levels are loaded + if (tmp == RoomList.Num()) + { + SetState(EGenerationState::Initialization); + } + + break; + case EGenerationState::Initialization: + // While initialization isn't done, try to initialize all rooms + if (!IsInit) + { + IsInit = true; + for (URoom* room : RoomList) + { + ARoomLevel* script = room->GetLevelScript(); + + if (nullptr != script) + { + IsInit &= script->IsInit; + if (!script->IsInit && !script->PendingInit) + { + NbInitRoom++; + script->Room = nullptr; + script->Init(room); + + LogInfo(FString::Printf(TEXT("Room Initialization: %d/%d"), NbInitRoom, RoomList.Num()), false); + } + } + else + { + IsInit = false; + } + } + + if (IsInit) + { + SetState(EGenerationState::None); + } + return; + } + break; + + default: + break; + } +} + +void ADungeonGenerator::OnStateEnd(EGenerationState State) +{ + FTimerHandle handle; + UNavigationSystemV1* nav = nullptr; + switch (State) + { + case EGenerationState::Unload: + RoomList.Empty(); + GetWorld()->FlushLevelStreaming(); + GEngine->ForceGarbageCollection(true); + LogInfo("======= End Unload All Levels ======="); + break; + case EGenerationState::Generation: + LogInfo("======= End Map Generation ======="); + break; + case EGenerationState::Load: + LogInfo("======= End Load All Levels ======="); + break; + case EGenerationState::Initialization: + LogInfo("======= End Init All Levels ======="); + + // Try to rebuild the navmesh + nav = UNavigationSystemV1::GetCurrent(GetWorld()); + if (nullptr != nav) + { + LogInfo("Rebuild navmesh"); + nav->CancelBuild(); + nav->Build(); + } + + // Invoke Post Generation Event when initialization is done + DispatchPostGeneration(); + break; + default: + break; + } +} + +URoomData* ADungeonGenerator::ChooseFirstRoomData_Implementation() +{ + LogError("Error: ChooseFirstRoomData not implemented"); + return nullptr; +} + +URoomData* ADungeonGenerator::ChooseNextRoomData_Implementation(URoomData* CurrentRoom) +{ + LogError("Error: ChooseNextRoomData not implemented"); + return nullptr; +} + +TSubclassOf ADungeonGenerator::ChooseDoor_Implementation(URoomData* CurrentRoom, URoomData* NextRoom) +{ + LogError("Error: ChooseDoor not implemented"); + return nullptr; +} + +bool ADungeonGenerator::IsValidDungeon_Implementation() +{ + LogError("Error: IsValidDungeon not implemented"); + return false; +} + +bool ADungeonGenerator::ContinueToAddRoom_Implementation() +{ + LogError("Error: ContinueToAddRoom not implemented"); + return false; +} + +void ADungeonGenerator::DispatchPreGeneration() +{ + OnPreGeneration(); + OnPreGeneration_BP(); + OnPreGenerationEvent.Broadcast(); +} + +void ADungeonGenerator::DispatchPostGeneration() +{ + OnPostGeneration(); + OnPostGeneration_BP(); + OnPostGenerationEvent.Broadcast(); +} + +void ADungeonGenerator::DispatchGenerationInit() +{ + OnGenerationInit(); + OnGenerationInit_BP(); + OnGenerationInitEvent.Broadcast(); +} + +void ADungeonGenerator::DispatchRoomAdded(URoomData* NewRoom) +{ + OnRoomAdded(NewRoom); + OnRoomAdded_BP(NewRoom); + OnRoomAddedEvent.Broadcast(NewRoom); +} + +URoomData* ADungeonGenerator::GetRandomRoomData(TArray RoomDataArray) +{ + int n = Random.RandRange(0, RoomDataArray.Num() - 1); + return RoomDataArray[n]; +} + +URoom* ADungeonGenerator::GetRoomAt(FIntVector RoomCell) +{ + return URoom::GetRoomAt(RoomCell, RoomList); +} + +bool ADungeonGenerator::HasAlreadyRoomData(URoomData* RoomData) +{ + return CountRoomData(RoomData) > 0; +} + +bool ADungeonGenerator::HasAlreadyOneRoomDataFrom(TArray RoomDataList) +{ + return CountTotalRoomData(RoomDataList) > 0; +} + +int ADungeonGenerator::CountRoomData(URoomData* RoomData) +{ + int count = 0; + for(int i = 0; i < RoomList.Num(); i++) + { + if(RoomList[i]->GetRoomData() == RoomData) + { + count++; + } + } + return count; +} + +int ADungeonGenerator::CountTotalRoomData(TArray RoomDataList) +{ + int count = 0; + for(int i = 0; i < RoomList.Num(); i++) + { + if(RoomDataList.Contains(RoomList[i]->GetRoomData())) + { + count++; + } + } + return count; +} diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeon.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeon.cpp new file mode 100644 index 0000000..77d5f50 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeon.cpp @@ -0,0 +1,102 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "ProceduralDungeon.h" +#include "Developer/Settings/Public/ISettingsModule.h" +#include "Developer/Settings/Public/ISettingsSection.h" +#include "ProceduralDungeonSettings.h" + +#define LOCTEXT_NAMESPACE "FProceduralDungeonModule" + +void FProceduralDungeonModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module + RegisterSettings(); +} + +void FProceduralDungeonModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. + if (UObjectInitialized()) + { + UnregisterSettings(); + } +} + +void FProceduralDungeonModule::RegisterSettings() +{ + // Registering some settings is just a matter of exposing the default UObject of + // your desired class, feel free to add here all those settings you want to expose + // to your LDs or artists. + + if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) + { + // Register the settings + ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "Procedural Dungeon", + LOCTEXT("RuntimeGeneralSettingsName", "Procedural Dungeon"), + LOCTEXT("RuntimeGeneralSettingsDescription", "Configuration for the Procedural Dungeon plugin"), + GetMutableDefault() + ); + + // Register the save handler to your settings, you might want to use it to + // validate those or just act to settings changes. + if (SettingsSection.IsValid()) + { + SettingsSection->OnModified().BindRaw(this, &FProceduralDungeonModule::HandleSettingsSaved); + } + } +} + +void FProceduralDungeonModule::UnregisterSettings() +{ + // Ensure to unregister all of your registered settings here, hot-reload would + // otherwise yield unexpected results. + + if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) + { + SettingsModule->UnregisterSettings("Project", "Plugins", "Procedural Dungeon"); + } +} + +// Callback for when the settings were saved. +bool FProceduralDungeonModule::HandleSettingsSaved() +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + bool ResaveSettings = false; + + // You can put any validation code in here and resave the settings in case an invalid + // value has been entered + + if (ResaveSettings) + { + Settings->SaveConfig(); + } + + return true; +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FProceduralDungeonModule, ProceduralDungeon) \ No newline at end of file diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonLog.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonLog.cpp new file mode 100644 index 0000000..024e835 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonLog.cpp @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "ProceduralDungeonLog.h" +#include "ProceduralDungeonSettings.h" + +DEFINE_LOG_CATEGORY(LogProceduralDungeon); + +bool ShowLogsOnScreen(float& _duration) +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + _duration = Settings->PrintDebugDuration; + return Settings->OnScreenPrintDebug; +} + +void LogInfo(FString message, bool showOnScreen) +{ + UE_LOG(LogProceduralDungeon, Log, TEXT("%s"), *message); + float duration; + if(showOnScreen && ShowLogsOnScreen(duration)) + { + GEngine->AddOnScreenDebugMessage(-1, duration, FColor::White, message); + } +} + +void LogWarning(FString message, bool showOnScreen) +{ + UE_LOG(LogProceduralDungeon, Warning, TEXT("%s"), *message); + float duration; + if(showOnScreen && ShowLogsOnScreen(duration)) + { + GEngine->AddOnScreenDebugMessage(-1, duration, FColor::Yellow, message); + } +} + +void LogError(FString message, bool showOnScreen) +{ + UE_LOG(LogProceduralDungeon, Error, TEXT("%s"), *message); + float duration; + if(showOnScreen && ShowLogsOnScreen(duration)) + { + GEngine->AddOnScreenDebugMessage(-1, duration, FColor::Red, message); + } +} \ No newline at end of file diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonSettings.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonSettings.cpp new file mode 100644 index 0000000..c1fcbb6 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonSettings.cpp @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "ProceduralDungeonSettings.h" + +UProceduralDungeonSettings::UProceduralDungeonSettings(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + RoomUnit = FVector(1000, 1000, 400); + DoorSize = FVector(40, 640, 400); + DoorOffset = 0.0f; + OcclusionCulling = true; + DrawDebug = true; + OnScreenPrintDebug = false; + PrintDebugDuration = 60.0f; + CanLoop = true; +} diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonTypes.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonTypes.cpp new file mode 100644 index 0000000..5a6bd8f --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralDungeonTypes.cpp @@ -0,0 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "ProceduralDungeonTypes.h" + diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralLevelStreaming.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralLevelStreaming.cpp new file mode 100644 index 0000000..72131e0 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/ProceduralLevelStreaming.cpp @@ -0,0 +1,172 @@ +#include "ProceduralLevelStreaming.h" +#include "Engine/World.h" +#include "Engine/Level.h" +#include "Engine.h" +#include "RoomData.h" +#include "ProceduralDungeon.h" + +#define LOCTEXT_NAMESPACE "World" + +int32 UProceduralLevelStreaming::UniqueLevelInstanceId = 0; + +UProceduralLevelStreaming::UProceduralLevelStreaming(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +void UProceduralLevelStreaming::PostLoad() +{ + Super::PostLoad(); + + // Initialize startup state of the streaming level + if (GetWorld()->IsGameWorld()) + { + bShouldBeLoaded = bInitiallyLoaded; + SetShouldBeVisible(bInitiallyVisible); + } +} + +void UProceduralLevelStreaming::SetShouldBeLoaded(const bool bInShouldBeLoaded) +{ + if (bInShouldBeLoaded != bShouldBeLoaded) + { + bShouldBeLoaded = bInShouldBeLoaded; + if (UWorld* World = GetWorld()) + { + World->UpdateStreamingLevelShouldBeConsidered(this); + } + } +} + + +void UProceduralLevelStreaming::OnLevelDynamicUnloaded() +{ + //UE_LOG(LogProceduralDungeon, Warning, TEXT("End unload level: %s"), *GetWorldAssetPackageName()); + UWorld* World = GetWorld(); + if (nullptr != World) + { + //UE_LOG(LogProceduralDungeon, Warning, TEXT("Remove instance from world")); + World->RemoveStreamingLevel(this); + } + bIsUnloaded = true; +} + +UProceduralLevelStreaming* UProceduralLevelStreaming::LoadLevelInstance(UObject* WorldContextObject, const FString LevelName, const FVector Location, const FRotator Rotation, bool& bOutSuccess) +{ + bOutSuccess = false; + UWorld* const World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + if (!World) + { + return nullptr; + } + + // Check whether requested map exists, this could be very slow if LevelName is a short package name + FString LongPackageName; + bOutSuccess = FPackageName::SearchForPackageOnDisk(LevelName, &LongPackageName); + if (!bOutSuccess) + { + return nullptr; + } + + return LoadLevelInstance_Internal(World, LongPackageName, Location, Rotation, bOutSuccess); +} + +UProceduralLevelStreaming* UProceduralLevelStreaming::LoadLevelInstanceBySoftObjectPtr(UObject* WorldContextObject, const TSoftObjectPtr Level, const FVector Location, const FRotator Rotation, bool& bOutSuccess) +{ + bOutSuccess = false; + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + if (!World) + { + return nullptr; + } + + // Check whether requested map exists, this could be very slow if LevelName is a short package name + if (Level.IsNull()) + { + return nullptr; + } + + return LoadLevelInstance_Internal(World, Level.GetLongPackageName(), Location, Rotation, bOutSuccess); +} + +UProceduralLevelStreaming * UProceduralLevelStreaming::Load(UObject * WorldContextObject, URoomData * Data, FVector Location, FRotator Rotation) +{ + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + if (nullptr == World) + { + UE_LOG(LogProceduralDungeon, Error, TEXT("Failed to load LevelStreamingDynamic: World is null")); + return nullptr; + } + if (nullptr == Data) + { + UE_LOG(LogProceduralDungeon, Error, TEXT("Failed to load LevelStreamingDynamic: Data is null")); + return nullptr; + } + + bool success = false; + UProceduralLevelStreaming* Instance = UProceduralLevelStreaming::LoadLevelInstanceBySoftObjectPtr(World, Data->Level, Location, Rotation, success); + + if (!success) + { + UE_LOG(LogProceduralDungeon, Error, TEXT("Failed to load LevelStreamingDynamic: Unknown reason")); + return nullptr; + } + + return Instance; +} + +void UProceduralLevelStreaming::Unload(UObject * WorldContextObject, UProceduralLevelStreaming * Instance) +{ + UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); + if (nullptr == World) + { + UE_LOG(LogProceduralDungeon, Error, TEXT("Failed to unload LevelStreamingDynamic: World is null")); + return; + } + if (nullptr == Instance) + { + UE_LOG(LogProceduralDungeon, Error, TEXT("Failed to unload LevelStreamingDynamic: Instance is null")); + return; + } + + // Prefix to remove + FString LevelName = Instance->GetWorldAssetPackageName(); + const FString PackagePath = FPackageName::GetLongPackagePath(LevelName); + FString LevelPackageName = PackagePath + TEXT("/") + World->StreamingLevelsPrefix; + + LevelName.RemoveFromStart(LevelPackageName, ESearchCase::IgnoreCase); + FLatentActionInfo LatentInfo(0, 2222, TEXT("OnLevelDynamicUnloaded"), Instance); + UGameplayStatics::UnloadStreamLevel(World, *LevelName, LatentInfo, false); +} + +UProceduralLevelStreaming* UProceduralLevelStreaming::LoadLevelInstance_Internal(UWorld* World, const FString& LongPackageName, const FVector Location, const FRotator Rotation, bool& bOutSuccess) +{ + // Create Unique Name for sub-level package + const FString ShortPackageName = FPackageName::GetShortName(LongPackageName); + const FString PackagePath = FPackageName::GetLongPackagePath(LongPackageName); + FString UniqueLevelPackageName = PackagePath + TEXT("/") + World->StreamingLevelsPrefix + ShortPackageName; + + //UE_LOG(LogProceduralDungeon, Warning, TEXT("Unique Id: %d"), UniqueLevelInstanceId); + + UniqueLevelPackageName += TEXT("_LevelInstance_") + FString::FromInt(++UniqueLevelInstanceId); + + // Setup streaming level object that will load specified map + UProceduralLevelStreaming* StreamingLevel = NewObject(World, UProceduralLevelStreaming::StaticClass(), NAME_None, RF_Transient, NULL); + StreamingLevel->SetWorldAssetByPackageName(FName(*UniqueLevelPackageName)); + StreamingLevel->LevelColor = FColor::MakeRandomColor(); + StreamingLevel->SetShouldBeLoaded(true); + StreamingLevel->SetShouldBeVisible(true); + StreamingLevel->bShouldBlockOnLoad = false; + StreamingLevel->bInitiallyLoaded = true; + StreamingLevel->bInitiallyVisible = true; + // Transform + StreamingLevel->LevelTransform = FTransform(Rotation, Location); + // Map to Load + StreamingLevel->PackageNameToLoad = FName(*LongPackageName); + + // Add the new level to world. + World->AddStreamingLevel(StreamingLevel); + + bOutSuccess = true; + return StreamingLevel; +} \ No newline at end of file diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/QueueOrStack.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/QueueOrStack.cpp new file mode 100644 index 0000000..bca2450 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/QueueOrStack.cpp @@ -0,0 +1,25 @@ +/* + * MIT License + * + * Copyright (c) 2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "QueueOrStack.h" diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/Room.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/Room.cpp new file mode 100644 index 0000000..7a3cb9d --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/Room.cpp @@ -0,0 +1,428 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "Room.h" +#include "Door.h" +#include "Engine/World.h" +#include "Engine.h" +#include "RoomData.h" +#include "RoomLevel.h" +#include "ProceduralDungeonSettings.h" +#include "ProceduralDungeonLog.h" + +void URoom::Init(URoomData* Data) +{ + RoomData = Data; + Instance = nullptr; + Position = FIntVector(0,0,0); + Direction = EDoorDirection::North; + + if (IsValid(RoomData)) + { + for (int i = 0; i < RoomData->GetNbDoor(); i++) + { + Connections.Add(FRoomConnection()); + } + } + else + { + LogError("No RoomData provided."); + } +} + +bool URoom::IsConnected(int Index) +{ + check(Index >= 0 && Index < Connections.Num()); + return Connections[Index].OtherRoom != nullptr; +} + +void URoom::SetConnection(int Index, URoom* Room, int OtherIndex) +{ + check(Index >= 0 && Index < Connections.Num()); + Connections[Index].OtherRoom = Room; + Connections[Index].OtherDoorIndex = OtherIndex; +} + +TWeakObjectPtr URoom::GetConnection(int Index) +{ + check(Index >= 0 && Index < Connections.Num()); + return Connections[Index].OtherRoom; +} + +int URoom::GetFirstEmptyConnection() +{ + for(int i = 0; i < Connections.Num(); ++i) + { + if(Connections[i].OtherRoom == nullptr) + { + return i; + } + } + return -1; +} + +void URoom::Instantiate(UWorld* World) +{ + if (Instance == nullptr) + { + if(!IsValid(RoomData)) + { + LogError("Failed to instantiate the room: it has no RoomData."); + return; + } + + Instance = UProceduralLevelStreaming::Load(World, RoomData, URoom::Unit() * FVector(Position), FRotator(0, -90 * (int)Direction, 0)); + UE_LOG(LogProceduralDungeon, Log, TEXT("Load room Instance: %s"), nullptr != Instance ? *Instance->GetWorldAssetPackageName() : TEXT("Null")); + } + else + { + LogError("Failed to instantiate the room: it is already instanciated."); + } +} + +void URoom::Destroy(UWorld* World) +{ + if (Instance != nullptr) + { + UE_LOG(LogProceduralDungeon, Log, TEXT("Unload room Instance: %s"), nullptr != Instance ? *Instance->GetWorldAssetPackageName() : TEXT("Null")); + + ARoomLevel* script = GetLevelScript(); + if (script != nullptr) + { + script->Room = nullptr; + script->Destroy(); + } + + UProceduralLevelStreaming::Unload(World, Instance); + } +} + +ARoomLevel* URoom::GetLevelScript() +{ + if (Instance == nullptr || !IsValid(Instance)) + { + return nullptr; + } + return Cast(Instance->GetLevelScriptActor()); +} + +bool URoom::IsInstanceLoaded() +{ + if(Instance == nullptr || !IsValid(Instance)) + { + return true; + } + return Instance->IsLevelLoaded(); +} + +bool URoom:: IsInstanceUnloaded() +{ + if(Instance == nullptr || !IsValid(Instance)) + { + return true; + } + return Instance->IsLevelUnloaded(); +} + +EDoorDirection URoom::GetDoorWorldOrientation(int DoorIndex) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + return Add(RoomData->Doors[DoorIndex].Direction, Direction); +} + +FIntVector URoom::GetDoorWorldPosition(int DoorIndex) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + return RoomToWorld(RoomData->Doors[DoorIndex].Position); +} + +int URoom::GetDoorIndexAt(FIntVector WorldPos, EDoorDirection WorldRot) +{ + FIntVector localPos = WorldToRoom(WorldPos); + EDoorDirection localRot = WorldToRoom(WorldRot); + + for(int i = 0; i < RoomData->Doors.Num(); ++i) + { + const FDoorDef door = RoomData->Doors[i]; + if(door.Position == localPos && door.Direction == localRot) + return i; + } + return -1; +} + +bool URoom::IsDoorInstanced(int DoorIndex) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + return IsValid(Connections[DoorIndex].DoorInstance); +} + +void URoom::SetDoorInstance(int DoorIndex, ADoor* Door) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + Connections[DoorIndex].DoorInstance = Door; +} + +int URoom::GetOtherDoorIndex(int DoorIndex) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + return Connections[DoorIndex].OtherDoorIndex; +} + +FIntVector URoom::WorldToRoom(FIntVector WorldPos) +{ + return Rotate(WorldPos - Position, Sub(EDoorDirection::North, Direction)); +} + +FIntVector URoom::RoomToWorld(FIntVector RoomPos) +{ + return Rotate(RoomPos, Direction) + Position; +} + +EDoorDirection URoom::WorldToRoom(EDoorDirection WorldRot) +{ + return Sub(WorldRot, Direction); +} + +EDoorDirection URoom::RoomToWorld(EDoorDirection RoomRot) +{ + return Add(RoomRot, Direction); +} + +void URoom::SetRotationFromDoor(int DoorIndex, EDoorDirection WorldRot) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + Direction = Add(Sub(WorldRot, RoomData->Doors[DoorIndex].Direction), EDoorDirection::South); +} + +void URoom::SetPositionFromDoor(int DoorIndex, FIntVector WorldPos) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + Position = WorldPos - RoomToWorld(RoomData->Doors[DoorIndex].Position); +} + +void URoom::SetPositionAndRotationFromDoor(int DoorIndex, FIntVector WorldPos, EDoorDirection WorldRot) +{ + check(DoorIndex >= 0 && DoorIndex < RoomData->Doors.Num()); + Direction = Sub(WorldRot, RoomData->Doors[DoorIndex].Direction); + Position = WorldPos - RoomToWorld(RoomData->Doors[DoorIndex].Position); +} + + +bool URoom::IsOccupied(FIntVector Cell) +{ + FIntVector local = WorldToRoom(Cell); + return local.X >= 0 && local.X < RoomData->Size.X + && local.Y >= 0 && local.Y < RoomData->Size.Y + && local.Z >= 0 && local.Z < RoomData->Size.Z; +} + +void URoom::TryConnectToExistingDoors(TArray& RoomList) +{ + for(int i = 0; i < RoomData->GetNbDoor(); ++i) + { + EDoorDirection dir = GetDoorWorldOrientation(i); + FIntVector pos = GetDoorWorldPosition(i) + URoom::GetDirection(dir); + URoom* otherRoom = GetRoomAt(pos, RoomList); + + if(IsValid(otherRoom)) + { + int j = otherRoom->GetDoorIndexAt(pos, URoom::Opposite(dir)); + if(j >= 0) // -1 if no door + { + Connect(*this, i, *otherRoom, j); + } + } + } +} + +FIntVector Max(const FIntVector& A, const FIntVector& B) +{ + return FIntVector(FMath::Max(A.X, B.X), FMath::Max(A.Y, B.Y), FMath::Max(A.Z, B.Z)); +} + +FIntVector Min(const FIntVector& A, const FIntVector& B) +{ + return FIntVector(FMath::Min(A.X, B.X), FMath::Min(A.Y, B.Y), FMath::Min(A.Z, B.Z)); +} + +// AABB Overlapping +bool URoom::Overlap(URoom& A, URoom& B) +{ + FIntVector A_firstPoint = A.Position; + FIntVector B_firstPoint = B.Position; + FIntVector A_secondPoint = A.RoomToWorld(A.RoomData->Size - FIntVector(1,1,1)); + FIntVector B_secondPoint = B.RoomToWorld(B.RoomData->Size - FIntVector(1,1,1)); + + FIntVector A_min = Min(A_firstPoint, A_secondPoint); + FIntVector A_max = Max(A_firstPoint, A_secondPoint); + FIntVector B_min = Min(B_firstPoint, B_secondPoint); + FIntVector B_max = Max(B_firstPoint, B_secondPoint); + + if (A_min.X > B_max.X) return false; + if (A_max.X < B_min.X) return false; + if (A_min.Y > B_max.Y) return false; + if (A_max.Y < B_min.Y) return false; + if (A_min.Z > B_max.Z) return false; + if (A_max.Z < B_min.Z) return false; + return true; +} + +bool URoom::Overlap(URoom & Room, TArray& RoomList) +{ + bool overlap = false; + for (int i = 0; i < RoomList.Num() && !overlap; i++) + { + if (Overlap(Room, *RoomList[i])) + { + overlap = true; + } + } + return overlap; +} + +EDoorDirection URoom::Add(EDoorDirection A, EDoorDirection B) +{ + int8 D = (int8)A + (int8)B; + while (D > 2) D -= 4; + while (D <= -2) D += 4; + return (EDoorDirection)D; +} + +EDoorDirection URoom::Sub(EDoorDirection A, EDoorDirection B) +{ + int8 D = (int8)A - (int8)B; + while (D > 2) D -= 4; + while (D <= -2) D += 4; + return (EDoorDirection)D; +} + +EDoorDirection URoom::Opposite(EDoorDirection O) +{ + return Add(O, EDoorDirection::South); +} + +FIntVector URoom::GetDirection(EDoorDirection O) +{ + FIntVector Dir = FIntVector::ZeroValue; + switch (O) + { + case EDoorDirection::North: + Dir.X = 1; + break; + case EDoorDirection::East: + Dir.Y = 1; + break; + case EDoorDirection::West: + Dir.Y = -1; + break; + case EDoorDirection::South: + Dir.X = -1; + break; + } + return Dir; +} + +FIntVector URoom::Rotate(FIntVector Pos, EDoorDirection Rot) +{ + FIntVector NewPos = Pos; + switch (Rot) + { + case EDoorDirection::North: + NewPos = Pos; + break; + case EDoorDirection::West: + NewPos.Y = -Pos.X; + NewPos.X = Pos.Y; + break; + case EDoorDirection::East: + NewPos.Y = Pos.X; + NewPos.X = -Pos.Y; + break; + case EDoorDirection::South: + NewPos.Y = -Pos.Y; + NewPos.X = -Pos.X; + break; + } + return NewPos; +} + +FVector URoom::GetRealDoorPosition(FIntVector DoorCell, EDoorDirection DoorRot) +{ + return URoom::Unit() * (FVector(DoorCell) + 0.5f * FVector(URoom::GetDirection(DoorRot)) + FVector(0, 0, URoom::DoorOffset())); +} + +void URoom::Connect(URoom& RoomA, int DoorA, URoom& RoomB, int DoorB) +{ + RoomA.SetConnection(DoorA, &RoomB, DoorB); + RoomB.SetConnection(DoorB, &RoomA, DoorA); +} + +URoom* URoom::GetRoomAt(FIntVector RoomCell, TArray& RoomList) +{ + for(auto it = RoomList.begin(); it != RoomList.end(); ++it) + { + if(IsValid(*it) && (*it)->IsOccupied(RoomCell)) + { + return *it; + } + } + return nullptr; +} + +FVector URoom::Unit() +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + return Settings->RoomUnit; +} + +FVector URoom::DoorSize() +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + return Settings->DoorSize; +} + +float URoom::DoorOffset() +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + return Settings->DoorOffset; +} + +bool URoom::OcclusionCulling() +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + return Settings->OcclusionCulling; +} + +bool URoom::DrawDebug() +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + return Settings->DrawDebug; +} + +bool URoom::CanLoop() +{ + UProceduralDungeonSettings* Settings = GetMutableDefault(); + return Settings->CanLoop; +} + diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomData.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomData.cpp new file mode 100644 index 0000000..4e4858e --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomData.cpp @@ -0,0 +1,35 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "RoomData.h" +#include "RoomLevel.h" +#include "ProceduralDungeonTypes.h" + +URoomData::URoomData() + : Super() +{ + Doors.Add(FDoorDef()); + Size = FIntVector(1, 1, 1); + RandomDoor = true; +} \ No newline at end of file diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomLevel.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomLevel.cpp new file mode 100644 index 0000000..d16db06 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomLevel.cpp @@ -0,0 +1,202 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "RoomLevel.h" +#include "CoreUObject.h" +#include "Engine/World.h" +#include "EngineUtils.h" +#include "Kismet/GameplayStatics.h" +#include "DrawDebugHelpers.h" +#include "GameFramework/GameState.h" +#include "GameFramework/Pawn.h" +#include "ProceduralDungeonTypes.h" +#include "Room.h" +#include "RoomData.h" +#include "Door.h" + + +uint32 ARoomLevel::Count = 0; + +// Use this for initialization +void ARoomLevel::Init(URoom* _Room) +{ + Id = Count; + Count++; + + IsInit = false; + Room = _Room; + PendingInit = true; +} + +ARoomLevel::ARoomLevel(const FObjectInitializer & ObjectInitializer) + : Super(ObjectInitializer) +{ + PrimaryActorTick.bCanEverTick = true; + IsInit = false; + PendingInit = false; + Room = nullptr; +} + +void ARoomLevel::BeginPlay() +{ + Super::BeginPlay(); +} + +void ARoomLevel::EndPlay(EEndPlayReason::Type EndPlayReason) +{ + for (AActor* Actor : ActorsInLevel) + { + if (IsValid(Actor)) + { + Actor->Destroy(); + } + } +} + +// Update is called once per frame +void ARoomLevel::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (!IsInit) + { + if (PendingInit && Room != nullptr) + { + Transform.SetLocation(FVector(Room->Position) * URoom::Unit()); + Transform.SetRotation(FRotator(0.0f, -90.0f * (int8)Room->Direction, 0.0f).Quaternion()); + + FIntVector forward = URoom::GetDirection(Room->Direction); + FIntVector right = URoom::GetDirection(URoom::Add(Room->Direction, EDoorDirection::East)); + + // Create triggerBox for occlusion culling + Center = 0.5f * (URoom::Unit() * FVector(Room->Position + Room->RoomToWorld(Room->GetRoomData()->Size) - forward - right)); + HalfExtents = 0.5f * (URoom::Unit() * FVector(Room->RoomToWorld(Room->GetRoomData()->Size) - Room->Position)); + HalfExtents = FVector(FMath::Abs(HalfExtents.X), FMath::Abs(HalfExtents.Y), FMath::Abs(HalfExtents.Z)); + + // Register All Actors in the level + for (TActorIterator ActorItr(GetWorld()); ActorItr; ++ActorItr) + { + ULevel *Level = ActorItr->GetLevel(); + if (Level->GetOuter() == GetLevel()->GetOuter()) + { + ActorsInLevel.Add(*ActorItr); + } + } + + PendingInit = false; + IsInit = true; + } + } + else + { + Display(); + } + + if (!IsValid(Data)) + return; + + FIntVector forward = URoom::GetDirection(EDoorDirection::North); + FIntVector right = URoom::GetDirection(URoom::Add(EDoorDirection::North, EDoorDirection::East)); + + Center = 0.5f * (URoom::Unit() * FVector(Data->Size - forward - right)); + HalfExtents = 0.5f * (URoom::Unit() * FVector(Data->Size)); + HalfExtents = FVector(FMath::Abs(HalfExtents.X), FMath::Abs(HalfExtents.Y), FMath::Abs(HalfExtents.Z)); + + Center = Transform.TransformPosition(Center); + +#if WITH_EDITOR + if (URoom::DrawDebug()) + { + // Pivot + DrawDebugSphere(GetWorld(), Transform.GetLocation(), 100.0f, 4, FColor::Magenta); + + // Room bounds + DrawDebugBox(GetWorld(), Center, HalfExtents, Transform.GetRotation(), FColor::Red); + + FVector DoorSize = URoom::DoorSize(); + + // Doors + for (int i = 0; i < Data->Doors.Num(); i++) + { + ADoor::DrawDebug(GetWorld(), Data->Doors[i].Position, Data->Doors[i].Direction, Transform); + } + } +#endif +} + +bool ARoomLevel::IsPlayerInside() +{ + bool inside = false; + + FCollisionShape box = FCollisionShape::MakeBox(HalfExtents); + + APawn* player = UGameplayStatics::GetPlayerController(GetWorld(), 0)->GetPawnOrSpectator(); + TArray overlappedActors; + + if (GetWorld()->OverlapMultiByObjectType( + overlappedActors, + Center, + Transform.GetRotation(), + FCollisionObjectQueryParams::AllDynamicObjects, + box)) + { + for (FOverlapResult result : overlappedActors) + { + if (player == result.GetActor()) + { + inside = true; + } + } + } + return inside; +} + +void ARoomLevel::Display() +{ + if (!IsPendingKill() && Room != nullptr && URoom::OcclusionCulling()) + { + PlayerInside = IsPlayerInside(); + IsHidden = !PlayerInside; + for (int i = 0; i < Room->GetConnectionCount(); i++) + { + if (Room->GetConnection(i) != nullptr + && IsValid(Room->GetConnection(i)->GetLevelScript()) + && Room->GetConnection(i)->GetLevelScript()->PlayerInside) + { + IsHidden = false; + } + } + + // force IsHidden to false if AlwaysVisible is true + IsHidden &= !AlwaysVisible; + + for (AActor* Actor : ActorsInLevel) + { + if (IsValid(Actor)) + { + Actor->SetActorHiddenInGame(IsHidden); + } + } + } +} diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomLockerBase.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomLockerBase.cpp new file mode 100644 index 0000000..91c61f1 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/RoomLockerBase.cpp @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "RoomLockerBase.h" +#include "RoomLevel.h" +#include "Room.h" +#include "RoomData.h" +#include "GameFramework/GameState.h" +#include "Engine/World.h" + +void ARoomLockerBase::SetLocked(bool Locked, bool Self, TSubclassOf RoomType) +{ + ARoomLevel* Script = GetRoomLevel(); + if (nullptr != Script) + { + if (Self) + { + Script->IsLocked = Locked; + } + + URoom* Room = Script->Room; + if (nullptr != Room && nullptr != RoomType) + { + for (int i = 0; i < Room->GetConnectionCount(); i++) + { + if (nullptr != Room->GetConnection(i) && nullptr != Room->GetConnection(i)->GetLevelScript()) + { + if (RoomType == Room->GetConnection(i)->GetRoomData()->GetClass()) + { + Room->GetConnection(i)->GetLevelScript()->IsLocked = Locked; + } + } + } + } + } +} + +ARoomLevel * ARoomLockerBase::GetRoomLevel() +{ + return Cast(GetLevel()->GetLevelScriptActor()); +} + diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/TriggerDoor.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/TriggerDoor.cpp new file mode 100644 index 0000000..a3c30e8 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/TriggerDoor.cpp @@ -0,0 +1,99 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "TriggerDoor.h" +#include "Components/BoxComponent.h" +#include "GameFramework/Character.h" +#include "Room.h" +#include "RoomLevel.h" + +ATriggerDoor::ATriggerDoor() +{ + BoxComponent = CreateDefaultSubobject(TEXT("BoxComponent")); + + if (RootComponent != nullptr) + { + BoxComponent->SetupAttachment(RootComponent); + } +} + +void ATriggerDoor::BeginPlay() +{ + Super::BeginPlay(); + + if (nullptr != BoxComponent) + { + BoxComponent->OnComponentBeginOverlap.AddUniqueDynamic(this, &ATriggerDoor::OnTriggerEnter); + BoxComponent->OnComponentEndOverlap.AddUniqueDynamic(this, &ATriggerDoor::OnTriggerExit); + } +} + +void ATriggerDoor::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (CharacterList.Num() > 0) + { + OpenDoor(); + } + else + { + CloseDoor(); + } +} + +void ATriggerDoor::SetRoomsAlwaysVisible(bool _visible) +{ + if (nullptr != RoomA && nullptr != RoomA->GetLevelScript()) + { + RoomA->GetLevelScript()->AlwaysVisible = _visible; + } + if (nullptr != RoomB && nullptr != RoomB->GetLevelScript()) + { + RoomB->GetLevelScript()->AlwaysVisible = _visible; + } +} + +void ATriggerDoor::OnTriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult) +{ + ACharacter* OtherCharacter = Cast(OtherActor); + UCapsuleComponent* OtherCapsule = Cast(OtherComp); + + if (OtherCharacter != nullptr && OtherCapsule != nullptr && OtherCapsule == OtherCharacter->GetCapsuleComponent() && !CharacterList.Contains(OtherCharacter)) + { + CharacterList.Add(OtherCharacter); + } + +} + +void ATriggerDoor::OnTriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) +{ + ACharacter* OtherCharacter = Cast(OtherActor); + UCapsuleComponent* OtherCapsule = Cast(OtherComp); + + if (OtherCharacter != nullptr && OtherCapsule != nullptr && OtherCapsule == OtherCharacter->GetCapsuleComponent() && CharacterList.Contains(OtherCharacter)) + { + CharacterList.Remove(OtherCharacter); + } +} diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/TriggerType.cpp b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/TriggerType.cpp new file mode 100644 index 0000000..c9b769c --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Private/TriggerType.cpp @@ -0,0 +1,114 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "TriggerType.h" +#include "TimerManager.h" +#include "ProceduralDungeonTypes.h" + +// Sets default values for this component's properties +UTriggerType::UTriggerType() +{ + // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features + // off to improve performance if you don't need them. + PrimaryComponentTick.bCanEverTick = false; + + TickDuration = 0.5f; + ActivationDelay = 0.0; +} + + +// Called when the game starts +void UTriggerType::BeginPlay() +{ + Super::BeginPlay(); + + if (GetNetMode() != ENetMode::NM_Client) + { + OnComponentBeginOverlap.AddUniqueDynamic(this, &UTriggerType::OnTriggerEnter); + OnComponentEndOverlap.AddUniqueDynamic(this, &UTriggerType::OnTriggerExit); + GetWorld()->GetTimerManager().SetTimer(TickTimer, this, &UTriggerType::TriggerTick, TickDuration, true); + } +} + +void UTriggerType::OnTriggerEnter(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult) +{ + if (ActorType == nullptr || (OtherActor!=nullptr && OtherActor->IsA(ActorType))) + { + if (!ActorList.Contains(OtherActor)) + { + ActorList.Add(OtherActor); + OnActorEnter.Broadcast(OtherActor); + + if (ActorList.Num() >= requiredActorCountToActivate) + { + if (ActivationDelay > 0) + { + GetWorld()->GetTimerManager().SetTimer(ActivationTimer, this, &UTriggerType::TriggerActivate, ActivationDelay, false); + } + else + { + TriggerActivate(); + } + } + } + } +} + +void UTriggerType::OnTriggerExit(UPrimitiveComponent * OverlappedComponent, AActor * OtherActor, UPrimitiveComponent * OtherComp, int32 OtherBodyIndex) +{ + if (ActorType == nullptr || (OtherActor != nullptr && OtherActor->IsA(ActorType))) + { + if (ActorList.Contains(OtherActor)) + { + ActorList.Remove(OtherActor); + OnActorExit.Broadcast(OtherActor); + + GetWorld()->GetTimerManager().ClearTimer(ActivationTimer); + TriggerDeactivate(); + } + } +} + +void UTriggerType::TriggerTick() +{ + OnTriggerTick.Broadcast(ActorList); +} + +void UTriggerType::TriggerActivate() +{ + if (!bIsActivated) + { + bIsActivated = true; + OnActivation.Broadcast(ActorList); + } +} + +void UTriggerType::TriggerDeactivate() +{ + if (bIsActivated) + { + bIsActivated = false; + OnDeactivation.Broadcast(ActorList); + } +} diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/ProceduralDungeon.Build.cs b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/ProceduralDungeon.Build.cs new file mode 100644 index 0000000..2246c4a --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/ProceduralDungeon.Build.cs @@ -0,0 +1,14 @@ +// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class ProceduralDungeon : ModuleRules +{ + public ProceduralDungeon(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange(new string[] { "Core", "NavigationSystem" }); + PrivateDependencyModuleNames.AddRange(new string[] { "CoreUObject", "Engine" }); + } +} diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/Door.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/Door.h new file mode 100644 index 0000000..c393baf --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/Door.h @@ -0,0 +1,102 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "ProceduralDungeonTypes.h" +#include "Door.generated.h" + +class URoom; + +UCLASS() +class PROCEDURALDUNGEON_API ADoor : public AActor +{ + GENERATED_BODY() + +public: + ADoor(); + +public: + virtual void Tick(float DeltaTime) override; + virtual bool ShouldTickIfViewportsOnly() const override { return true; } + +public: + UFUNCTION() + void OpenDoor(); + UFUNCTION() + void CloseDoor(); + +protected: + UFUNCTION() + virtual void OnDoorLock() {} + UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Locked")) + void OnDoorLock_BP(); + + UFUNCTION() + virtual void OnDoorUnlock() {} + UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Unlocked")) + void OnDoorUnlock_BP(); + + UFUNCTION() + virtual void OnDoorOpen() {} + UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Open")) + void OnDoorOpen_BP(); + + UFUNCTION() + virtual void OnDoorClose() {} + UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Close")) + void OnDoorClose_BP(); + +protected: + bool bLocked = false; + bool bIsOpen = false; + + // The two connected rooms to this door + UPROPERTY() + URoom* RoomA; + UPROPERTY() + URoom* RoomB; + + UPROPERTY(EditAnywhere, Category = "Door", meta = (DisplayName = "Always Visible")) + bool bAlwaysVisible = false; + + UPROPERTY(EditAnywhere, Category = "Door", meta = (DisplayName = "Always Unlocked")) + bool bAlwaysUnlocked = false; + +private: + bool bPrevLocked = false; + +public: + void SetConnectingRooms(URoom* RoomA, URoom* RoomB); + + UFUNCTION(BlueprintCallable, Category = "Door", meta = (DisplayName="Is Locked")) + bool IsLocked() { return bLocked; } + + UFUNCTION(BlueprintCallable, Category = "Door", meta = (DisplayName = "Is Open")) + bool IsOpen() { return bIsOpen; } + + static void DrawDebug(UWorld* World, FIntVector DoorCell = FIntVector::ZeroValue, EDoorDirection DoorRot = EDoorDirection::NbDirection, FTransform Transform = FTransform::Identity); +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/DungeonGenerator.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/DungeonGenerator.h new file mode 100644 index 0000000..818dd3a --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/DungeonGenerator.h @@ -0,0 +1,245 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "Math/RandomStream.h" +#include "ProceduralDungeonTypes.h" +#include "DungeonGenerator.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGenerationEvent); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FRoomEvent, URoomData*, NewRoom); + +class ADoor; +class URoom; + +UCLASS() +class PROCEDURALDUNGEON_API ADungeonGenerator : public AActor +{ + GENERATED_BODY() + +public: + // Sets default values for this actor's properties + ADungeonGenerator(); + +protected: + // Called when the game starts or when spawned + virtual void BeginPlay() override; + virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override; + virtual void Tick(float DeltaTime) override; + +public: + + // Update the seed and call the generation on all clients + // Do nothing when called on clients + UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Dungeon Generator") + void Generate(); + + // ===== Methods that should be overriden in blueprint ===== + + // Return the RoomData you want as root of the dungeon generation + UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose First Room")) + URoomData* ChooseFirstRoomData(); + + // Return the RoomData that will be connected to the Current Room + UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Next Room")) + URoomData* ChooseNextRoomData(URoomData* CurrentRoom); + + // Return the door which will be spawned between Current Room and Next Room + UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Door")) + TSubclassOf ChooseDoor(URoomData* CurrentRoom, URoomData* NextRoom); + + // Condition to validate a dungeon Generation + UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Is Valid Dungeon")) + bool IsValidDungeon(); + + // Condition to continue or stop adding room to the dungeon + UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Continue To Add Room")) + bool ContinueToAddRoom(); + + // ===== Optional events ===== + + // Called after unloading previous dungeon but before generating next dungeon. + UFUNCTION(BlueprintImplementableEvent, Category = "Dungeon Generator", meta = (DisplayName = "Pre Generation")) + void OnPreGeneration_BP(); + + // Called after all rooms are loaded and initialized + UFUNCTION(BlueprintImplementableEvent, Category = "Dungeon Generator", meta = (DisplayName = "Post Generation")) + void OnPostGeneration_BP(); + + // Called before generating a new dungeon and each time IsValidDungeon return false + UFUNCTION(BlueprintImplementableEvent, Category = "Dungeon Generator", meta = (DisplayName = "Generation Init")) + void OnGenerationInit_BP(); + + // Called when the room NewRoom is added in the generation (but not spawned yet) + UFUNCTION(BlueprintImplementableEvent, Category = "Dungeon Generator", meta = (DisplayName = "On Room Added")) + void OnRoomAdded_BP(URoomData* NewRoom); + + // ===== Utility functions you can use in blueprint ===== + + // Return true if a specific RoomData is already in the dungeon + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator") + bool HasAlreadyRoomData(URoomData* RoomData); + + // Return true if at least one of the RoomData from the list provided is already in the dungeon + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator") + bool HasAlreadyOneRoomDataFrom(TArray RoomDataList); + + // Return the number of a specific RoomData in the dungeon + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator") + int CountRoomData(URoomData* RoomData); + + // Return the total number of RoomData in the dungeon from the list provided + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator") + int CountTotalRoomData(TArray RoomDataList); + + // Return a random RoomData from the array provided + UFUNCTION(BlueprintCallable, Category = "Dungeon Generator") + URoomData* GetRandomRoomData(TArray RoomDataArray); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (CompactNodeTitle="Nb Room")) + int GetNbRoom() { return RoomList.Num(); } + + URoom* GetRoomAt(FIntVector RoomCell); + + // ===== Events ===== + + UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator") + FGenerationEvent OnPreGenerationEvent; + + UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator") + FGenerationEvent OnPostGenerationEvent; + + UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator") + FGenerationEvent OnGenerationInitEvent; + + UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator") + FRoomEvent OnRoomAddedEvent; + +protected: + + // ===== Implementation of blueprint native events ===== + + UFUNCTION() + virtual URoomData* ChooseFirstRoomData_Implementation(); + + UFUNCTION() + virtual URoomData* ChooseNextRoomData_Implementation(URoomData* CurrentRoom); + + UFUNCTION() + virtual TSubclassOf ChooseDoor_Implementation(URoomData* CurrentRoom, URoomData* NextRoom); + + UFUNCTION() + virtual bool IsValidDungeon_Implementation(); + + UFUNCTION() + virtual bool ContinueToAddRoom_Implementation(); + + // ===== Overridable events by native inheritance ===== + + UFUNCTION() + virtual void OnPreGeneration() {} + + UFUNCTION() + virtual void OnPostGeneration() {} + + UFUNCTION() + virtual void OnGenerationInit() {} + + UFUNCTION() + virtual void OnRoomAdded(URoomData* NewRoom) {} + +private: + // Launch the generation process of the dungeon + UFUNCTION(NetMulticast, Reliable, Category = "Dungeon Generator") + void BeginGeneration(uint32 GenerationSeed); + + // Create virtually the dungeon (no load nor initialization of rooms) + UFUNCTION() + void CreateDungeon(); + + // That add a room function to generate all rooms + TArray AddNewRooms(URoom& ParentRoom); + + // Instantiate a room in the scene + void InstantiateRoom(URoom* Room); + + // Load all room levels + UFUNCTION() + void LoadAllRooms(); + + // unload all room levels + UFUNCTION() + void UnloadAllRooms(); + + // ===== FSM ===== + + UFUNCTION() + void SetState(EGenerationState NewState); + UFUNCTION() + void OnStateBegin(EGenerationState State); + UFUNCTION() + void OnStateTick(EGenerationState State); + UFUNCTION() + void OnStateEnd(EGenerationState State); + + // ===== Dispatch optional events ===== + + UFUNCTION() + void DispatchPreGeneration(); + + UFUNCTION() + void DispatchPostGeneration(); + + UFUNCTION() + void DispatchGenerationInit(); + + UFUNCTION() + void DispatchRoomAdded(URoomData* NewRoom); + +private: + UPROPERTY(EditAnywhere, Category = "Procedural Generation") + EGenerationType GenerationType; + UPROPERTY(EditAnywhere, Category = "Procedural Generation") + ESeedType SeedType; + UPROPERTY(EditAnywhere, Category = "Procedural Generation") + uint32 Seed; + + static const int MaxTry = 500; + static const int MaxRoomTry = 10; + FRandomStream Random; + + UPROPERTY() + TArray RoomList; + UPROPERTY() + TArray DoorList; + + bool IsInit = false; + int NbInitRoom = 0; + int NbLoadedRoom = 0; + int NbUnloadedRoom = 0; + EGenerationState CurrentState = EGenerationState::None; +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeon.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeon.h new file mode 100644 index 0000000..523b71e --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeon.h @@ -0,0 +1,44 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FProceduralDungeonModule : public IModuleInterface +{ +public: + + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + virtual bool SupportsDynamicReloading() override { return true; } + +private: + void RegisterSettings(); + void UnregisterSettings(); + bool HandleSettingsSaved(); +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonLog.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonLog.h new file mode 100644 index 0000000..4f18ec9 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonLog.h @@ -0,0 +1,36 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "Containers/UnrealString.h" +#include "ProceduralDungeonSettings.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogProceduralDungeon, Log, All); + +bool ShowLogsOnScreen(float& _duration); +void LogInfo(FString message, bool showOnScreen = true); +void LogWarning(FString message, bool showOnScreen = true); +void LogError(FString message, bool showOnScreen = true); \ No newline at end of file diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonSettings.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonSettings.h new file mode 100644 index 0000000..5e21e7e --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonSettings.h @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "ProceduralDungeonSettings.generated.h" + +UCLASS(config = Game, defaultconfig) +class PROCEDURALDUNGEON_API UProceduralDungeonSettings : public UObject +{ + GENERATED_BODY() + +public: + UProceduralDungeonSettings(const FObjectInitializer& ObjectInitializer); + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + FVector RoomUnit; + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + FVector DoorSize; + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + float DoorOffset; + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + bool OcclusionCulling; + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + bool CanLoop; + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + bool DrawDebug; + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + bool OnScreenPrintDebug; + + UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon") + float PrintDebugDuration; +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonTypes.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonTypes.h new file mode 100644 index 0000000..3557ead --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralDungeonTypes.h @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "ProceduralDungeonTypes.generated.h" + +UENUM() +enum class EGenerationState : uint8 +{ + None UMETA(DisplayName = "None"), + Generation UMETA(DisplayName = "Generation"), + Load UMETA(DisplayName = "Load"), + Initialization UMETA(DisplayName = "Initialization"), + Unload UMETA(DisplayName = "Unload"), + NbState UMETA(Hidden) +}; + +UENUM(BlueprintType) +enum class EDoorDirection : uint8 +{ + North = 0 UMETA(DisplayName = "North"), // rotation = 0 (world forward) + East = 255 UMETA(DisplayName = "East"), // rotation = -90 (world right) + West = 1 UMETA(DisplayName = "West"), // rotation = 90 (world left) + South = 2 UMETA(DisplayName = "South"), // rotation = 180 (world backward) + NbDirection = 4 UMETA(Hidden) +}; + +UENUM() +enum class EGenerationType : uint8 +{ + DFS = 0 UMETA(DisplayName = "Depth First", Tooltip = "Make the dungeon more linear"), + BFS = 1 UMETA(DisplayName = "Breadth First", Tooltip = "Make the dungeon less linear"), + NbType = 2 UMETA(Hidden) +}; + +UENUM() +enum class ESeedType : uint8 +{ + Random = 0 UMETA(DisplayName = "Random", Tooltip = "Random seed at each generation"), + AutoIncrement = 1 UMETA(DisplayName = "Auto Increment", Tooltip = "Get the initial seed and increment at each generation"), + Fixed = 2 UMETA(DisplayName = "Fixed", Tooltip = "Always use initial seed (or you can set it manually via blueprint)"), + NbType = 3 UMETA(Hidden) +}; + +USTRUCT() +struct FDoorDef +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, Category = "DoorDef") + FIntVector Position; + UPROPERTY(EditAnywhere, Category = "DoorDef") + EDoorDirection Direction; +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralLevelStreaming.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralLevelStreaming.h new file mode 100644 index 0000000..3350d62 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/ProceduralLevelStreaming.h @@ -0,0 +1,76 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/ObjectMacros.h" +#include "Engine/LevelStreaming.h" +#include "ProceduralLevelStreaming.generated.h" + +/** + * This file was copied from the ULevelStreamingDynamic of the engine + * But an Unload methods is added and the UniqueLevelInstanceID is switch to public (accessed by the generator) + */ +UCLASS(BlueprintType) +class PROCEDURALDUNGEON_API UProceduralLevelStreaming : public ULevelStreaming +{ + GENERATED_UCLASS_BODY() + + /** Whether the level should be loaded at startup */ + UPROPERTY(Category = LevelStreaming, EditAnywhere) + uint32 bInitiallyLoaded : 1; + + /** Whether the level should be visible at startup if it is loaded */ + UPROPERTY(Category = LevelStreaming, EditAnywhere) + uint32 bInitiallyVisible : 1; + + /** + * Stream in a level with a specific location and rotation. You can create multiple instances of the same level! + * + * The level to be loaded does not have to be in the persistent map's Levels list, however to ensure that the .umap does get + * packaged, please be sure to include the .umap in your Packaging Settings: + * + * Project Settings -> Packaging -> List of Maps to Include in a Packaged Build (you may have to show advanced or type in filter) + * + * @param LevelName - Level package name, ex: /Game/Maps/MyMapName, specifying short name like MyMapName will force very slow search on disk + * @param Location - World space location where the level should be spawned + * @param Rotation - World space rotation for rotating the entire level + * @param bOutSuccess - Whether operation was successful (map was found and added to the sub-levels list) + * @return Streaming level object for a level instance + */ + UFUNCTION(BlueprintCallable, Category = LevelStreaming, meta = (DisplayName = "Load Level Instance (by Name)", WorldContext = "WorldContextObject")) + static UProceduralLevelStreaming* LoadLevelInstance(UObject* WorldContextObject, FString LevelName, FVector Location, FRotator Rotation, bool& bOutSuccess); + + UFUNCTION(BlueprintCallable, Category = LevelStreaming, meta = (DisplayName = "Load Level Instance (by Object Reference)", WorldContext = "WorldContextObject")) + static UProceduralLevelStreaming* LoadLevelInstanceBySoftObjectPtr(UObject* WorldContextObject, TSoftObjectPtr Level, FVector Location, FRotator Rotation, bool& bOutSuccess); + + + + UFUNCTION(BlueprintCallable, Category = LevelStreaming, meta = (DisplayName = "Load Level Instance (by Room Data)", WorldContext = "WorldContextObject")) + static UProceduralLevelStreaming* Load(UObject* WorldContextObject, class URoomData* Data, FVector Location, FRotator Rotation); + UFUNCTION(BlueprintCallable, Category = LevelStreaming, meta = (DisplayName = "Unload Level Instance", WorldContext = "WorldContextObject")) + static void Unload(UObject* WorldContextObject, UProceduralLevelStreaming* Instance); + + + //~ Begin UObject Interface + virtual void PostLoad() override; + //~ End UObject Interface + + //~ Begin ULevelStreaming Interface + virtual bool ShouldBeLoaded() const override { return bShouldBeLoaded; } + //~ End ULevelStreaming Interface + + virtual void SetShouldBeLoaded(bool bShouldBeLoaded) override; + + UFUNCTION(BlueprintCallable, Category = "Procedural Level Streaming") + void OnLevelDynamicUnloaded(); + + bool IsLevelUnloaded() { return bIsUnloaded; } + +public: + // Counter used by LoadLevelInstance to create unique level names + static int32 UniqueLevelInstanceId; +private: + + uint32 bIsUnloaded : 1; + + static UProceduralLevelStreaming* LoadLevelInstance_Internal(UWorld* World, const FString& LongPackageName, FVector Location, FRotator Rotation, bool& bOutSuccess); +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/QueueOrStack.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/QueueOrStack.h new file mode 100644 index 0000000..cee9e7d --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/QueueOrStack.h @@ -0,0 +1,97 @@ +/* + * MIT License + * + * Copyright (c) 2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "Containers/Queue.h" +#include "Containers/Array.h" + +template +class TQueueOrStack +{ +public: + enum class EMode { QUEUE, STACK }; + + TQueueOrStack(EMode _Mode) + : Mode(_Mode), Queue(), Stack() + { + } + + void Push(T& Element) + { + switch(Mode) + { + case EMode::QUEUE: + Queue.Enqueue(Element); + break; + case EMode::STACK: + Stack.Push(Element); + break; + } + } + + T Pop() + { + check(!IsEmpty()); + T item = T(); + switch(Mode) + { + case EMode::QUEUE: + Queue.Dequeue(item); + break; + case EMode::STACK: + item = Stack.Pop(); + break; + } + return item; + } + + int Num() + { + switch(Mode) + { + case EMode::QUEUE: + return Queue.Num(); + case EMode::STACK: + return Stack.Num(); + } + } + + bool IsEmpty() + { + switch(Mode) + { + case EMode::QUEUE: + return Queue.IsEmpty(); + case EMode::STACK: + return Stack.Num() <= 0; + } + return true; + } + +private: + EMode Mode; + TQueue Queue; + TArray Stack; +}; \ No newline at end of file diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/Room.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/Room.h new file mode 100644 index 0000000..1643c67 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/Room.h @@ -0,0 +1,122 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "ProceduralLevelStreaming.h" +#include "GameFramework/Actor.h" +#include "ProceduralDungeonTypes.h" +#include "Room.generated.h" + +class ARoomLevel; +class URoomData; +class ADoor; + +USTRUCT() +struct FRoomConnection +{ + GENERATED_BODY() + + UPROPERTY() + TWeakObjectPtr OtherRoom = nullptr; + int OtherDoorIndex = -1; + ADoor* DoorInstance = nullptr; +}; + +UCLASS() +class PROCEDURALDUNGEON_API URoom : public UObject +{ + GENERATED_BODY() +private: + UPROPERTY() + TArray Connections; + +public: + UPROPERTY() + UProceduralLevelStreaming* Instance; + UPROPERTY() + FIntVector Position; + EDoorDirection Direction; + + URoomData* GetRoomData() { return RoomData; } + +private: + UPROPERTY() + URoomData* RoomData; + +public: + void Init(URoomData* RoomData); + + bool IsConnected(int Index); + void SetConnection(int Index, URoom* Room, int OtherDoorIndex); + TWeakObjectPtr GetConnection(int Index); + int GetFirstEmptyConnection(); + + void Instantiate(UWorld* World); + void Destroy(UWorld* World); + ARoomLevel* GetLevelScript(); + bool IsInstanceLoaded(); + bool IsInstanceUnloaded(); + + EDoorDirection GetDoorWorldOrientation(int DoorIndex); + FIntVector GetDoorWorldPosition(int DoorIndex); + int GetConnectionCount() { return Connections.Num(); } + int GetDoorIndexAt(FIntVector WorldPos, EDoorDirection WorldRot); + bool IsDoorInstanced(int DoorIndex); + void SetDoorInstance(int DoorIndex, ADoor* Door); + int GetOtherDoorIndex(int DoorIndex); + void TryConnectToExistingDoors(TArray& RoomList); + + FIntVector WorldToRoom(FIntVector WorldPos); + FIntVector RoomToWorld(FIntVector RoomPos); + EDoorDirection WorldToRoom(EDoorDirection WorldRot); + EDoorDirection RoomToWorld(EDoorDirection RoomRot); + void SetRotationFromDoor(int DoorIndex, EDoorDirection WorldRot); + void SetPositionFromDoor(int DoorIndex, FIntVector WorldPos); + void SetPositionAndRotationFromDoor(int DoorIndex, FIntVector WorldPos, EDoorDirection WorldRot); + bool IsOccupied(FIntVector Cell); + + // AABB Overlapping + static bool Overlap(URoom& A, URoom& B); + static bool Overlap(URoom& Room, TArray& RoomList); + static EDoorDirection Add(EDoorDirection A, EDoorDirection B); + static EDoorDirection Sub(EDoorDirection A, EDoorDirection B); + static EDoorDirection Opposite(EDoorDirection O); + static FIntVector GetDirection(EDoorDirection O); + static FIntVector Rotate(FIntVector Pos, EDoorDirection Rot); + + static FVector GetRealDoorPosition(FIntVector DoorCell, EDoorDirection DoorRot); + + static void Connect(URoom& RoomA, int DoorA, URoom& RoomB, int DoorB); + static URoom* GetRoomAt(FIntVector RoomCell, TArray& RoomList); + + // Plugin Settings + static FVector Unit(); + static FVector DoorSize(); + static float DoorOffset(); + static bool OcclusionCulling(); + static bool DrawDebug(); + static bool CanLoop(); +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomData.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomData.h new file mode 100644 index 0000000..e7ca9a3 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomData.h @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "ProceduralDungeonTypes.h" +#include "RoomData.generated.h" + +UCLASS() +class PROCEDURALDUNGEON_API URoomData : public UPrimaryDataAsset +{ + GENERATED_BODY() + + friend class UProceduralLevelStreaming; + +private: + UPROPERTY(EditAnywhere, Category = "Level") + TSoftObjectPtr Level; + +public: + UPROPERTY(EditAnywhere, Category = "Door") + bool RandomDoor; + + UPROPERTY(EditAnywhere, Category = "Doors") + TArray Doors; + + UPROPERTY(EditAnywhere, Category = "Room") + FIntVector Size; + +public: + URoomData(); + + int GetNbDoor() const { return Doors.Num(); } +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomLevel.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomLevel.h new file mode 100644 index 0000000..74ac979 --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomLevel.h @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/LevelScriptActor.h" +#include "RoomLevel.generated.h" + +class URoom; + +UCLASS() +class PROCEDURALDUNGEON_API ARoomLevel : public ALevelScriptActor +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, Category = "Data", meta = ( BlueprintBaseOnly /* Doesn't work... */ ) ) + class URoomData* Data; + +public: + static uint32 Count; + + UPROPERTY() + URoom* Room = nullptr; + bool PlayerInside = false; + bool IsHidden = false; + bool IsInit = false; + bool PendingInit = false; + bool IsLocked = false; + + UPROPERTY(EditAnywhere, Category = "Room Level") + bool AlwaysVisible = false; + +public: + ARoomLevel(const FObjectInitializer& ObjectInitializer); + + virtual void BeginPlay() override; + virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override; + virtual void Tick(float DeltaTime) override; + virtual bool ShouldTickIfViewportsOnly() const override { return true; } + + void Init(URoom* Room); + uint32 GetId() { return Id; } + +private: + UPROPERTY(VisibleAnywhere, Category = "Room Level") + uint32 Id; + UPROPERTY() + TArray ActorsInLevel; + FTransform Transform; + FVector Center; + FVector HalfExtents; + + bool IsPlayerInside(); + void Display(); +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomLockerBase.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomLockerBase.h new file mode 100644 index 0000000..1c3808e --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/RoomLockerBase.h @@ -0,0 +1,46 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "ProceduralDungeonTypes.h" +#include "RoomLockerBase.generated.h" + +class URoomData; +class ARoomLevel; + +UCLASS() +class PROCEDURALDUNGEON_API ARoomLockerBase : public AActor +{ + GENERATED_BODY() + +public: + // Set the room where this actor is locked or not (with self parameter) and the neighbor rooms of RoomType. + void SetLocked(bool Locked, bool Self = true, TSubclassOf RoomType = nullptr); + +protected: + ARoomLevel* GetRoomLevel(); +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/TriggerDoor.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/TriggerDoor.h new file mode 100644 index 0000000..bc2e23e --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/TriggerDoor.h @@ -0,0 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "Door.h" +#include "TriggerDoor.generated.h" + +UCLASS() +class PROCEDURALDUNGEON_API ATriggerDoor : public ADoor +{ + GENERATED_BODY() + +private: + UPROPERTY(EditAnywhere, Category="Door Trigger") + class UBoxComponent* BoxComponent; + + UPROPERTY() + TArray CharacterList; + +public: + ATriggerDoor(); + virtual void BeginPlay() override; + virtual void Tick(float DeltaTime) override; + + UFUNCTION() + void OnTriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult); + + UFUNCTION() + void OnTriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); + + UFUNCTION(BlueprintCallable, Category = "Door") + void SetRoomsAlwaysVisible(bool Visible); +}; diff --git a/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/TriggerType.h b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/TriggerType.h new file mode 100644 index 0000000..d33a5be --- /dev/null +++ b/Plugins/ProceduralDungeon/Source/ProceduralDungeon/Public/TriggerType.h @@ -0,0 +1,103 @@ +/* + * MIT License + * + * Copyright (c) 2019-2021 Benoit Pelletier + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include "CoreMinimal.h" +#include "Components/BoxComponent.h" +#include "GameFramework/Actor.h" +#include "TriggerType.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTriggerEvent, AActor*, Actor); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FTriggerArrayEvent, TArray, Actor); + +UCLASS(BlueprintType, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) ) +class PROCEDURALDUNGEON_API UTriggerType : public UBoxComponent +{ + GENERATED_BODY() + +public: + UTriggerType(); + +protected: + virtual void BeginPlay() override; + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trigger Type") + float TickDuration; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trigger Type") + float ActivationDelay; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trigger Type") + uint8 requiredActorCountToActivate; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Trigger Type") + TSubclassOf ActorType; + + UPROPERTY(BlueprintAssignable, Meta = (DisplayName = "On Actor Enter")) + FTriggerEvent OnActorEnter; + UPROPERTY(BlueprintAssignable, Meta = (DisplayName = "On Actor Exit")) + FTriggerEvent OnActorExit; + + UPROPERTY(BlueprintAssignable, Meta = (DisplayName = "On Trigger Tick")) + FTriggerArrayEvent OnTriggerTick; + + UPROPERTY(BlueprintAssignable, Meta = (DisplayName = "On Trigger Activation")) + FTriggerArrayEvent OnActivation; + UPROPERTY(BlueprintAssignable, Meta = (DisplayName = "On Trigger Deactivation")) + FTriggerArrayEvent OnDeactivation; + + UFUNCTION() + bool IsActivated() { return bIsActivated; } + + UFUNCTION() + TArray GetActorList() { return ActorList; } + +protected: + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Trigger Type") + bool bIsActivated; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Trigger Type") + TArray ActorList; + +private: + UPROPERTY() + FTimerHandle TickTimer; + + UPROPERTY() + FTimerHandle ActivationTimer; + + UFUNCTION() + void OnTriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult); + UFUNCTION() + void OnTriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex); + + UFUNCTION() + void TriggerTick(); + + UFUNCTION() + void TriggerActivate(); + + UFUNCTION() + void TriggerDeactivate(); +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/.gitbugtraq b/Plugins/UE4GitPlugin-2.17-beta/.gitbugtraq new file mode 100644 index 0000000..e47406d --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/.gitbugtraq @@ -0,0 +1,7 @@ +# .gitbugtraq for Git GUIs (SmartGit/TortoiseGit) to show links to the Github issue tracker. +# Instead of the repository root directory, it could be added as an additional section to $GIT_DIR/config. +# (note that '\' need to be escaped). +[bugtraq] + url = https://github.com/SRombauts/UE4GitPlugin/issues/%BUGID% + loglinkregex = "#\\d+" + logregex = \\d+ diff --git a/Plugins/UE4GitPlugin-2.17-beta/.gitignore b/Plugins/UE4GitPlugin-2.17-beta/.gitignore new file mode 100644 index 0000000..2ca2cfb --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/.gitignore @@ -0,0 +1,5 @@ +/Binaries/*/*.pdb +/Binaries/*/*Debug* +/Binaries/*/*.dylib +/Binaries/*/*.modules +/Intermediate diff --git a/Plugins/UE4GitPlugin-2.17-beta/GitSourceControl.uplugin b/Plugins/UE4GitPlugin-2.17-beta/GitSourceControl.uplugin new file mode 100644 index 0000000..661c4b8 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/GitSourceControl.uplugin @@ -0,0 +1,25 @@ +{ + "FileVersion" : 3, + "Version" : 37, + "VersionName" : "2.17", + "FriendlyName" : "Git LFS 2", + "Description" : "Git source control management (dev)", + "Category" : "Source Control", + "CreatedBy" : "SRombauts", + "CreatedByURL" : "http://srombauts.github.com", + "DocsURL" : "", + "MarketplaceURL" : "", + "SupportURL" : "", + "EnabledByDefault" : true, + "CanContainContent" : false, + "IsBetaVersion" : true, + "Installed" : false, + "Modules" : + [ + { + "Name" : "GitSourceControl", + "Type" : "Editor", + "LoadingPhase" : "Default" + } + ] +} \ No newline at end of file diff --git a/Plugins/UE4GitPlugin-2.17-beta/LICENSE.txt b/Plugins/UE4GitPlugin-2.17-beta/LICENSE.txt new file mode 100644 index 0000000..4cdc13c --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Plugins/UE4GitPlugin-2.17-beta/README.md b/Plugins/UE4GitPlugin-2.17-beta/README.md new file mode 100644 index 0000000..4c0ce4d --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/README.md @@ -0,0 +1,201 @@ +Unreal Engine 4 Git Source Control Plugin +----------------------------------------- + +[![release](https://img.shields.io/github/release/SRombauts/UE4GitPlugin.svg)](https://github.com/SRombauts/UE4GitPlugin/releases) +[![Git Plugin issues](https://img.shields.io/github/issues/SRombauts/UE4GitPlugin.svg)](https://github.com/SRombauts/UE4GitPlugin/issues) +[![Join the chat at https://gitter.im/SRombauts/UE4GitPlugin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SRombauts/UE4GitPlugin) +UE4GitPlugin is a simple Git Source Control Plugin for **Unreal Engine 4.26**. + +Developed and contributed by Sébastien Rombauts 2014-2020 (sebastien.rombauts@gmail.com) + + +- First version of the plugin has been **integrated by default in UE4.7 in "beta version"**. +- This is a developement fork named "**Git LFS 2**" adding File Locks supported by Github. + +You need to install it into your Project **Plugins/** folder, and it will overwrite (replace) the default "Git (beta version)" Source Control Provider with the "Git LFS 2" plugin. + +Have a look at the [Git Plugin Tutorial on the Wiki](https://wiki.unrealengine.com/Git_source_control_%28Tutorial%29). ([alternate link](https://michaeljcole.github.io/wiki.unrealengine.com/Git_source_control_%28Tutorial%29/)) + +Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com) + +Source Control Login window to create a new workspace/a new repository: +![Source Control Login window - create a new repository](Screenshots/SourceControlLogin_Init.png) + +Source Control status tooltip, when hovering the Source Control icon in toolbar: +![Source Control Status Tooltip](Screenshots/SourceControlStatusTooltip.png) + +Source Control top Menu, extended with a few commands specific to Git: +![Source Control Status Tooltip](Screenshots/SourceControlMenu.png) + +Submit Files to Source Control window, to commit assets: +![Submit Files to Source Control](Screenshots/SubmitFiles.png) + +File History window, to see the changelog of an asset: +![History of a file](Screenshots/FileHistory.png) + +Visual Diffing of two revisions of a Blueprint: + + +Merge conflict of a Blueprint: + + +Status Icons: + +![New/Unsaved/Untracked](Screenshots/Icons/New.png) +![Added](Screenshots/Icons/Added.png) +![Unchanged](Screenshots/Icons/Unchanged.png) +![Modified](Screenshots/Icons/Modified.png) +![Moved/Renamed](Screenshots/Icons/Renamed.png) + +### Supported features +- initialize a new Git local repository ('git init') to manage your UE4 Game Project + - can also create an appropriate .gitignore file as part of initialization + - can also create a .gitattributes file to enable Git LFS (Large File System) as part of initialization + - can also enable Git LFS 2.x File Locks as part of initialization + - can also make the initial commit, with custom multi-line message +- display status icons to show modified/added/deleted/untracked files, not at head and conflicted +- show history of a file +- visual diff of a blueprint against depot or between previous versions of a file +- revert modifications of a file (works best with "Content Hot-Reload" experimental option of UE4.15, by default since 4.16) +- add, delete, rename a file +- checkin/commit a file (cannot handle atomically more than 50 files) +- migrate an asset between two projects if both are using Git +- solve a merge conflict on a blueprint +- show current branch name in status text +- Configure remote origin URL ('git remote add origin url') +- Sync to Pull (rebase) the current branch if there is no local modified files +- Push the current branch +- Git LFS (Github, Gitlab, Bitbucket), git-annex, git-fat and git-media are working with Git 2.10+ +- Git LFS 2 File Locks +- Windows, Mac and Linux + +### What *cannot* be done presently +- Branch/Merge are not in the current Editor workflow +- Amend a commit is not in the current Editor workflow +- Revert All (using either "Stash" or "reset --hard") +- Configure user name & email ('git config user.name' & git config user.email') +- Authentication is not managed if needed for Sync (Pull) + +### Known issues +- #34 "outside repository" fatal error +- #37 Rebase workflow: conflicts not detected! +- #41 UE-44637: Deleting an asset is unsuccessful if the asset is marked for add (since UE4.13) +- #46 Merge Conflicts - Accept Target - causes engine to crash bug +- #47 Git LFS conflict resolution not working +- #49 Git LFS 2: False error in logs after a successful push +- #51 Git LFS 2: cannot revert a modified/unchecked-out asset +- #53 Git LFS 2: document the configuration and workflow +- #54 Poor performances of 'lfs locks' on Windows command line +- #55 Git LFS 2: Unlocking a renamed asset + +- missing localisation for git specific messages +- displaying states of 'Engine' assets (also needs management of 'out of tree' files) +- renaming a Blueprint in Editor leaves a redirector file, AND modify too much the asset to enable git to track its history through renaming + +### Getting started + +Quick demo of the Git Plugin on Unreal Engine 4.12 (preview) +[![Git Plugin on Unreal Engine 4.12 (preview)](https://img.youtube.com/vi/rRhPl9vL58Q/0.jpg)](https://youtu.be/rRhPl9vL58Q) + +#### Install Git + +Under Windows 64bits, you should install the standard standalone Git for Windows +(now comming with Git LFS 2 with File Locking) with default parameters, +usually in "C:\Program Files\Git\bin\git.exe". + +Then you have to configure your name and e-mail that will appear in each of your commits: + +``` +git config --global user.name "Sébastien Rombauts" +git config --global user.email sebastien.rombauts@gmail.com +``` + +#### Install this Git Plugin (dev) into your Game Project + +Unreal Engine comes with a stable version of this plugin, so no need to install it. + +This alternate "Git development plugin" needs to be installed into a subfolder or your Game Project "Plugins" directory +(that is, you cannot install it into the Engine Plugins directory): + +``` +/Plugins +``` + +You will obviously only be able to use the plugin within this project. + +See also the [Plugins official Documentation](https://docs.unrealengine.com/latest/INT/Programming/Plugins/index.html) + +#### Activate Git Source Control for your Game Project + +Load your Game Project in Unreal Engine, then open: + +``` +File->Connect To Source Control... -> Git +``` + +##### Project already managed by Git + +If your project is already under Git (it contains a ".git" subfolder), just click on "Accept Settings". This connect the Editor to your local Git repository ("Depot"). + +##### Project not already under Git + +Otherwise, the Git Plugin is able to create (initialize) a new local Git Repository with your project Assets and Sources files: + + + +Click "Initialize project with Git" that will add all relevant files to source control and make the initial commit with the customizable message. +When everything is done, click on "Accept Settings". + +#### Using the Git Source Control Provider in the Unreal Engine Editor + +The plugin mostly interacts with you local Git repository ("Depot"), not much with the remote server (usually "origin"). + +It displays Git status icons on top of assets in the Asset Browser: +- No icon means that the file is under source control and unchanged since last commit, or ignored. +- A red mark is for "modified" assets, that is the one that needs to be committed (so not the same as "Check-out" in Perforce/SVN/Plastic SCM). +- A red cross is for "added" assets, that also needs to be committed +- A blue lightning means "renamed". +- A yellow exclamation point is for files in conflict after a merge, or is not at head (latest revision on the current remote branch). +- A yellow question mark is for files not in source control. + +TODO: +- specifics of rename and redirectors, and "Fix Up Redirector in Folder" command +- history / visual diff +- CheckIn = Commit +- CheckOut = Commit+Push+unlock (when using LFS 2) + +See also the [Source Control official Documentation](https://docs.unrealengine.com/latest/INT/Engine/UI/SourceControl/index.html) + +### License + +Copyright (c) 2014-2020 Sébastien Rombauts (sebastien.rombauts@gmail.com) + +Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +or copy at http://opensource.org/licenses/MIT) + +## How to contribute +### GitHub website +The most efficient way to help and contribute to this wrapper project is to +use the tools provided by GitHub: +- please fill bug reports and feature requests here: https://github.com/SRombauts/UE4GitPlugin/issues +- fork the repository, make some small changes and submit them with independent pull-requests + +### Contact +- You can use the Unreal Engine forums. +- You can also email me directly, I will answer any questions and requests. + +### Coding Style Guidelines +The source code follow the UnreaEngine official [Coding Standard](https://docs.unrealengine.com/latest/INT/Programming/Development/CodingStandard/index.html): +- CamelCase naming convention, with a prefix letter to differentiate classes ('F'), interfaces ('I'), templates ('T') +- files (.cpp/.h) are named like the class they contains +- Doxygen comments, documentation is located with declaration, on headers +- Use portable common features of C++11 like nullptr, auto, range based for, override keyword +- Braces on their own line +- Tabs to indent code, with a width of 4 characters + +## See also + +- [Git Source Control Tutorial on the Wikis](https://wiki.unrealengine.com/Git_source_control_(Tutorial)) +- [UE4 Git Plugin website](http://srombauts.github.com/UE4GitPlugin) + +- [ue4-hg-plugin for Mercurial (and bigfiles)](https://github.com/enlight/ue4-hg-plugin) diff --git a/Plugins/UE4GitPlugin-2.17-beta/Resources/Icon128.png b/Plugins/UE4GitPlugin-2.17-beta/Resources/Icon128.png new file mode 100644 index 0000000..3d9e864 Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Resources/Icon128.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/FileHistory.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/FileHistory.png new file mode 100644 index 0000000..2b9e0f5 Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/FileHistory.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Added.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Added.png new file mode 100644 index 0000000..fff4f19 Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Added.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Modified.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Modified.png new file mode 100644 index 0000000..a7f7789 Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Modified.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/New.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/New.png new file mode 100644 index 0000000..cfea784 Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/New.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Renamed.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Renamed.png new file mode 100644 index 0000000..d66b81b Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Renamed.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Unchanged.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Unchanged.png new file mode 100644 index 0000000..17307ad Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/Icons/Unchanged.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlLogin_Init.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlLogin_Init.png new file mode 100644 index 0000000..3b2f1c3 Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlLogin_Init.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlMenu.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlMenu.png new file mode 100644 index 0000000..d4f85b7 Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlMenu.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlStatusTooltip.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlStatusTooltip.png new file mode 100644 index 0000000..ef36f1f Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SourceControlStatusTooltip.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SubmitFiles.png b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SubmitFiles.png new file mode 100644 index 0000000..2545aae Binary files /dev/null and b/Plugins/UE4GitPlugin-2.17-beta/Screenshots/SubmitFiles.png differ diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/GitSourceControl.Build.cs b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/GitSourceControl.Build.cs new file mode 100644 index 0000000..d16ebb3 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/GitSourceControl.Build.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +using UnrealBuildTool; + +public class GitSourceControl : ModuleRules +{ + public GitSourceControl(ReadOnlyTargetRules Target) : base(Target) + { + // Enable the Include-What-You-Use (IWYU) UE4.15 policy (see https://docs.unrealengine.com/en-us/Programming/UnrealBuildSystem/IWYUReferenceGuide) + // "Shared PCHs may be used if an explicit private PCH is not set through PrivatePCHHeaderFile. In either case, none of the source files manually include a module PCH, and should include a matching header instead." + bEnforceIWYU = true; + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PrivatePCHHeaderFile = "Private/GitSourceControlPrivatePCH.h"; + + PrivateDependencyModuleNames.AddRange( + new string[] { + "Core", + "CoreUObject", + "Slate", + "SlateCore", + "InputCore", + "DesktopWidgets", + "EditorStyle", + "UnrealEd", + "SourceControl", + "Projects", + } + ); + } +} diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.cpp new file mode 100644 index 0000000..1bf25ec --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.cpp @@ -0,0 +1,65 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlCommand.h" + +#include "Modules/ModuleManager.h" +#include "GitSourceControlModule.h" + +FGitSourceControlCommand::FGitSourceControlCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate) + : Operation(InOperation) + , Worker(InWorker) + , OperationCompleteDelegate(InOperationCompleteDelegate) + , bExecuteProcessed(0) + , bCommandSuccessful(false) + , bConnectionDropped(false) + , bAutoDelete(true) + , Concurrency(EConcurrency::Synchronous) +{ + // grab the providers settings here, so we don't access them once the worker thread is launched + check(IsInGameThread()); + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" ); + PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); + PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot(); +} + +bool FGitSourceControlCommand::DoWork() +{ + bCommandSuccessful = Worker->Execute(*this); + FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1); + + return bCommandSuccessful; +} + +void FGitSourceControlCommand::Abandon() +{ + FPlatformAtomics::InterlockedExchange(&bExecuteProcessed, 1); +} + +void FGitSourceControlCommand::DoThreadedWork() +{ + Concurrency = EConcurrency::Asynchronous; + DoWork(); +} + +ECommandResult::Type FGitSourceControlCommand::ReturnResults() +{ + // Save any messages that have accumulated + for (FString& String : InfoMessages) + { + Operation->AddInfoMessge(FText::FromString(String)); + } + for (FString& String : ErrorMessages) + { + Operation->AddErrorMessge(FText::FromString(String)); + } + + // run the completion delegate if we have one bound + ECommandResult::Type Result = bCommandSuccessful ? ECommandResult::Succeeded : ECommandResult::Failed; + OperationCompleteDelegate.ExecuteIfBound(Operation, Result); + + return Result; +} diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.h new file mode 100644 index 0000000..42c1a89 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlCommand.h @@ -0,0 +1,92 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "ISourceControlProvider.h" +#include "Misc/IQueuedWork.h" + +/** + * Used to execute Git commands multi-threaded. + */ +class FGitSourceControlCommand : public IQueuedWork +{ +public: + + FGitSourceControlCommand(const TSharedRef& InOperation, const TSharedRef& InWorker, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete() ); + + /** + * This is where the real thread work is done. All work that is done for + * this queued object should be done from within the call to this function. + */ + bool DoWork(); + + /** + * Tells the queued work that it is being abandoned so that it can do + * per object clean up as needed. This will only be called if it is being + * abandoned before completion. NOTE: This requires the object to delete + * itself using whatever heap it was allocated in. + */ + virtual void Abandon() override; + + /** + * This method is also used to tell the object to cleanup but not before + * the object has finished it's work. + */ + virtual void DoThreadedWork() override; + + /** Save any results and call any registered callbacks. */ + ECommandResult::Type ReturnResults(); + +public: + /** Path to the Git binary */ + FString PathToGitBinary; + + /** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */ + FString PathToRepositoryRoot; + + /** Tell if using the Git LFS file Locking workflow */ + bool bUsingGitLfsLocking; + + /** Operation we want to perform - contains outward-facing parameters & results */ + TSharedRef Operation; + + /** The object that will actually do the work */ + TSharedRef Worker; + + /** Delegate to notify when this operation completes */ + FSourceControlOperationComplete OperationCompleteDelegate; + + /**If true, this command has been processed by the source control thread*/ + volatile int32 bExecuteProcessed; + + /**If true, the source control command succeeded*/ + bool bCommandSuccessful; + + /** TODO LFS If true, the source control connection was dropped while this command was being executed*/ + bool bConnectionDropped; + + /** Current Commit full SHA1 */ + FString CommitId; + + /** Current Commit description's Summary */ + FString CommitSummary; + + /** If true, this command will be automatically cleaned up in Tick() */ + bool bAutoDelete; + + /** Whether we are running multi-treaded or not*/ + EConcurrency::Type Concurrency; + + /** Files to perform this operation on */ + TArray Files; + + /**Info and/or warning message storage*/ + TArray InfoMessages; + + /**Potential error message storage*/ + TArray ErrorMessages; +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.cpp new file mode 100644 index 0000000..faeea9d --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.cpp @@ -0,0 +1,515 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlMenu.h" + +#include "GitSourceControlModule.h" +#include "GitSourceControlProvider.h" +#include "GitSourceControlOperations.h" +#include "GitSourceControlUtils.h" + +#include "ISourceControlModule.h" +#include "ISourceControlOperation.h" +#include "SourceControlOperations.h" + +#include "LevelEditor.h" +#include "Widgets/Notifications/SNotificationList.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Framework/MultiBox/MultiBoxBuilder.h" +#include "Misc/MessageDialog.h" +#include "EditorStyleSet.h" + +#include "PackageTools.h" +#include "FileHelpers.h" + +#include "Logging/MessageLog.h" + +static const FName GitSourceControlMenuTabName("GitSourceControlMenu"); + +#define LOCTEXT_NAMESPACE "GitSourceControl" + +void FGitSourceControlMenu::Register() +{ + // Register the extension with the level editor + FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr("LevelEditor"); + if (LevelEditorModule) + { + FLevelEditorModule::FLevelEditorMenuExtender ViewMenuExtender = FLevelEditorModule::FLevelEditorMenuExtender::CreateRaw(this, &FGitSourceControlMenu::OnExtendLevelEditorViewMenu); + auto& MenuExtenders = LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders(); + MenuExtenders.Add(ViewMenuExtender); + ViewMenuExtenderHandle = MenuExtenders.Last().GetHandle(); + } +} + +void FGitSourceControlMenu::Unregister() +{ + // Unregister the level editor extensions + FLevelEditorModule* LevelEditorModule = FModuleManager::GetModulePtr("LevelEditor"); + if (LevelEditorModule) + { + LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders().RemoveAll([=](const FLevelEditorModule::FLevelEditorMenuExtender& Extender) { return Extender.GetHandle() == ViewMenuExtenderHandle; }); + } +} + +bool FGitSourceControlMenu::HaveRemoteUrl() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl"); + const FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + return !Provider.GetRemoteUrl().IsEmpty(); +} + +/// Prompt to save or discard all packages +bool FGitSourceControlMenu::SaveDirtyPackages() +{ + const bool bPromptUserToSave = true; + const bool bSaveMapPackages = true; + const bool bSaveContentPackages = true; + const bool bFastSave = false; + const bool bNotifyNoPackagesSaved = false; + const bool bCanBeDeclined = true; // If the user clicks "don't save" this will continue and lose their changes + bool bHadPackagesToSave = false; + + bool bSaved = FEditorFileUtils::SaveDirtyPackages(bPromptUserToSave, bSaveMapPackages, bSaveContentPackages, bFastSave, bNotifyNoPackagesSaved, bCanBeDeclined, &bHadPackagesToSave); + + // bSaved can be true if the user selects to not save an asset by unchecking it and clicking "save" + if (bSaved) + { + TArray DirtyPackages; + FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages); + FEditorFileUtils::GetDirtyContentPackages(DirtyPackages); + bSaved = DirtyPackages.Num() == 0; + } + + return bSaved; +} + +/// Find all packages in Content directory +TArray FGitSourceControlMenu::ListAllPackages() +{ + TArray PackageRelativePaths; + FPackageName::FindPackagesInDirectory(PackageRelativePaths, *FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir())); + + TArray PackageNames; + PackageNames.Reserve(PackageRelativePaths.Num()); + for (const FString& Path : PackageRelativePaths) + { + FString PackageName; + FString FailureReason; + if (FPackageName::TryConvertFilenameToLongPackageName(Path, PackageName, &FailureReason)) + { + PackageNames.Add(PackageName); + } + else + { + FMessageLog("SourceControl").Error(FText::FromString(FailureReason)); + } + } + + return PackageNames; +} + +/// Unkink all loaded packages to allow to update them +TArray FGitSourceControlMenu::UnlinkPackages(const TArray& InPackageNames) +{ + TArray LoadedPackages; + + // Inspired from ContentBrowserUtils::SyncPathsFromSourceControl() + if (InPackageNames.Num() > 0) + { + // Form a list of loaded packages to reload... + LoadedPackages.Reserve(InPackageNames.Num()); + for (const FString& PackageName : InPackageNames) + { + UPackage* Package = FindPackage(nullptr, *PackageName); + if (Package) + { + LoadedPackages.Emplace(Package); + + // Detach the linkers of any loaded packages so that SCC can overwrite the files... + if (!Package->IsFullyLoaded()) + { + FlushAsyncLoading(); + Package->FullyLoad(); + } + ResetLoaders(Package); + } + } + UE_LOG(LogSourceControl, Log, TEXT("Reseted Loader for %d Packages"), LoadedPackages.Num()); + } + + return LoadedPackages; +} + +void FGitSourceControlMenu::ReloadPackages(TArray& InPackagesToReload) +{ + UE_LOG(LogSourceControl, Log, TEXT("Reloading %d Packages..."), InPackagesToReload.Num()); + + // Syncing may have deleted some packages, so we need to unload those rather than re-load them... + TArray PackagesToUnload; + InPackagesToReload.RemoveAll([&](UPackage* InPackage) -> bool + { + const FString PackageExtension = InPackage->ContainsMap() ? FPackageName::GetMapPackageExtension() : FPackageName::GetAssetPackageExtension(); + const FString PackageFilename = FPackageName::LongPackageNameToFilename(InPackage->GetName(), PackageExtension); + if (!FPaths::FileExists(PackageFilename)) + { + PackagesToUnload.Emplace(InPackage); + return true; // remove package + } + return false; // keep package + }); + + // Hot-reload the new packages... + UPackageTools::ReloadPackages(InPackagesToReload); + + // Unload any deleted packages... + UPackageTools::UnloadPackages(PackagesToUnload); +} + +// Ask the user if he wants to stash any modification and try to unstash them afterward, which could lead to conflicts +bool FGitSourceControlMenu::StashAwayAnyModifications() +{ + bool bStashOk = true; + + FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot(); + const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + const TArray ParametersStatus{"--porcelain --untracked-files=no"}; + TArray InfoMessages; + TArray ErrorMessages; + // Check if there is any modification to the working tree + const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), PathToGitBinary, PathToRespositoryRoot, ParametersStatus, TArray(), InfoMessages, ErrorMessages); + if ((bStatusOk) && (InfoMessages.Num() > 0)) + { + // Ask the user before stashing + const FText DialogText(LOCTEXT("SourceControlMenu_Stash_Ask", "Stash (save) all modifications of the working tree? Required to Sync/Pull!")); + const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText); + if (Choice == EAppReturnType::Ok) + { + const TArray ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" }; + bStashMadeBeforeSync = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray(), InfoMessages, ErrorMessages); + if (!bStashMadeBeforeSync) + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_StashFailed", "Stashing away modifications failed!")); + SourceControlLog.Notify(); + } + } + else + { + bStashOk = false; + } + } + + return bStashOk; +} + +// Unstash any modifications if a stash was made at the beginning of the Sync operation +void FGitSourceControlMenu::ReApplyStashedModifications() +{ + if (bStashMadeBeforeSync) + { + FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot(); + const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + const TArray ParametersStash{ "pop" }; + TArray InfoMessages; + TArray ErrorMessages; + const bool bUnstashOk = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray(), InfoMessages, ErrorMessages); + if (!bUnstashOk) + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_UnstashFailed", "Unstashing previously saved modifications failed!")); + SourceControlLog.Notify(); + } + } +} + +void FGitSourceControlMenu::SyncClicked() +{ + if (!OperationInProgressNotification.IsValid()) + { + // Ask the user to save any dirty assets opened in Editor + const bool bSaved = SaveDirtyPackages(); + if (bSaved) + { + // Find and Unlink all packages in Content directory to allow to update them + PackagesToReload = UnlinkPackages(ListAllPackages()); + + // Ask the user if he wants to stash any modification and try to unstash them afterward, which could lead to conflicts + const bool bStashed = StashAwayAnyModifications(); + if (bStashed) + { + // Launch a "Sync" operation + FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + TSharedRef SyncOperation = ISourceControlOperation::Create(); + const ECommandResult::Type Result = Provider.Execute(SyncOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + // Display an ongoing notification during the whole operation (packages will be reloaded at the completion of the operation) + DisplayInProgressNotification(SyncOperation->GetInProgressString()); + } + else + { + // Report failure with a notification and Reload all packages + DisplayFailureNotification(SyncOperation->GetName()); + ReloadPackages(PackagesToReload); + } + } + else + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_Sync_Unsaved", "Stash away all modifications before attempting to Sync!")); + SourceControlLog.Notify(); + } + } + else + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_Sync_Unsaved", "Save All Assets before attempting to Sync!")); + SourceControlLog.Notify(); + } + } + else + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress")); + SourceControlLog.Notify(); + } +} + +void FGitSourceControlMenu::PushClicked() +{ + if (!OperationInProgressNotification.IsValid()) + { + // Launch a "Push" Operation + FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + TSharedRef PushOperation = ISourceControlOperation::Create(); + const ECommandResult::Type Result = Provider.Execute(PushOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + // Display an ongoing notification during the whole operation + DisplayInProgressNotification(PushOperation->GetInProgressString()); + } + else + { + // Report failure with a notification + DisplayFailureNotification(PushOperation->GetName()); + } + } + else + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress")); + SourceControlLog.Notify(); + } +} + +void FGitSourceControlMenu::RevertClicked() +{ + if (!OperationInProgressNotification.IsValid()) + { + // Ask the user before reverting all! + const FText DialogText(LOCTEXT("SourceControlMenu_Revert_Ask", "Revert all modifications of the working tree?")); + const EAppReturnType::Type Choice = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText); + if (Choice == EAppReturnType::Ok) + { + // NOTE No need to force the user to SaveDirtyPackages(); since he will be presented with a choice by the Editor + + // Find and Unlink all packages in Content directory to allow to update them + PackagesToReload = UnlinkPackages(ListAllPackages()); + + // Launch a "Revert" Operation + FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + TSharedRef RevertOperation = ISourceControlOperation::Create(); + const ECommandResult::Type Result = Provider.Execute(RevertOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + // Display an ongoing notification during the whole operation + DisplayInProgressNotification(RevertOperation->GetInProgressString()); + } + else + { + // Report failure with a notification and Reload all packages + DisplayFailureNotification(RevertOperation->GetName()); + ReloadPackages(PackagesToReload); + } + } + } + else + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress")); + SourceControlLog.Notify(); + } +} + +void FGitSourceControlMenu::RefreshClicked() +{ + if (!OperationInProgressNotification.IsValid()) + { + // Launch an "UpdateStatus" Operation + FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + TSharedRef RefreshOperation = ISourceControlOperation::Create(); + RefreshOperation->SetCheckingAllFiles(true); + const ECommandResult::Type Result = Provider.Execute(RefreshOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateRaw(this, &FGitSourceControlMenu::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + // Display an ongoing notification during the whole operation + DisplayInProgressNotification(RefreshOperation->GetInProgressString()); + } + else + { + // Report failure with a notification + DisplayFailureNotification(RefreshOperation->GetName()); + } + } + else + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_InProgress", "Source control operation already in progress")); + SourceControlLog.Notify(); + } +} + +// Display an ongoing notification during the whole operation +void FGitSourceControlMenu::DisplayInProgressNotification(const FText& InOperationInProgressString) +{ + if (!OperationInProgressNotification.IsValid()) + { + FNotificationInfo Info(InOperationInProgressString); + Info.bFireAndForget = false; + Info.ExpireDuration = 0.0f; + Info.FadeOutDuration = 1.0f; + OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info); + if (OperationInProgressNotification.IsValid()) + { + OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); + } + } +} + +// Remove the ongoing notification at the end of the operation +void FGitSourceControlMenu::RemoveInProgressNotification() +{ + if (OperationInProgressNotification.IsValid()) + { + OperationInProgressNotification.Pin()->ExpireAndFadeout(); + OperationInProgressNotification.Reset(); + } +} + +// Display a temporary success notification at the end of the operation +void FGitSourceControlMenu::DisplaySucessNotification(const FName& InOperationName) +{ + const FText NotificationText = FText::Format( + LOCTEXT("SourceControlMenu_Success", "{0} operation was successful!"), + FText::FromName(InOperationName) + ); + FNotificationInfo Info(NotificationText); + Info.bUseSuccessFailIcons = true; + Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage")); + FSlateNotificationManager::Get().AddNotification(Info); + UE_LOG(LogSourceControl, Log, TEXT("%s"), *NotificationText.ToString()); +} + +// Display a temporary failure notification at the end of the operation +void FGitSourceControlMenu::DisplayFailureNotification(const FName& InOperationName) +{ + const FText NotificationText = FText::Format( + LOCTEXT("SourceControlMenu_Failure", "Error: {0} operation failed!"), + FText::FromName(InOperationName) + ); + FNotificationInfo Info(NotificationText); + Info.ExpireDuration = 8.0f; + FSlateNotificationManager::Get().AddNotification(Info); + UE_LOG(LogSourceControl, Error, TEXT("%s"), *NotificationText.ToString()); +} + +void FGitSourceControlMenu::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) +{ + RemoveInProgressNotification(); + + if ((InOperation->GetName() == "Sync") || (InOperation->GetName() == "Revert")) + { + // Unstash any modifications if a stash was made at the beginning of the Sync operation + ReApplyStashedModifications(); + // Reload packages that where unlinked at the beginning of the Sync/Revert operation + ReloadPackages(PackagesToReload); + } + + // Report result with a notification + if (InResult == ECommandResult::Succeeded) + { + DisplaySucessNotification(InOperation->GetName()); + } + else + { + DisplayFailureNotification(InOperation->GetName()); + } +} + +void FGitSourceControlMenu::AddMenuExtension(FMenuBuilder& Builder) +{ + Builder.AddMenuEntry( + LOCTEXT("GitPush", "Push"), + LOCTEXT("GitPushTooltip", "Push all local commits to the remote server."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Submit"), + FUIAction( + FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::PushClicked), + FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl) + ) + ); + + Builder.AddMenuEntry( + LOCTEXT("GitSync", "Sync/Pull"), + LOCTEXT("GitSyncTooltip", "Update all files in the local repository to the latest version of the remote server."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Sync"), + FUIAction( + FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::SyncClicked), + FCanExecuteAction::CreateRaw(this, &FGitSourceControlMenu::HaveRemoteUrl) + ) + ); + + Builder.AddMenuEntry( + LOCTEXT("GitRevert", "Revert"), + LOCTEXT("GitRevertTooltip", "Revert all files in the repository to their unchanged state."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Revert"), + FUIAction( + FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RevertClicked), + FCanExecuteAction() + ) + ); + + Builder.AddMenuEntry( + LOCTEXT("GitRefresh", "Refresh"), + LOCTEXT("GitRefreshTooltip", "Update the source control status of all files in the local repository."), + FSlateIcon(FEditorStyle::GetStyleSetName(), "SourceControl.Actions.Refresh"), + FUIAction( + FExecuteAction::CreateRaw(this, &FGitSourceControlMenu::RefreshClicked), + FCanExecuteAction() + ) + ); +} + +TSharedRef FGitSourceControlMenu::OnExtendLevelEditorViewMenu(const TSharedRef CommandList) +{ + TSharedRef Extender(new FExtender()); + + Extender->AddMenuExtension( + "SourceControlActions", + EExtensionHook::After, + nullptr, + FMenuExtensionDelegate::CreateRaw(this, &FGitSourceControlMenu::AddMenuExtension)); + + return Extender; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.h new file mode 100644 index 0000000..e7890d4 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlMenu.h @@ -0,0 +1,61 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "ISourceControlProvider.h" + +class FToolBarBuilder; +class FMenuBuilder; + +/** Git extension of the Source Control toolbar menu */ +class FGitSourceControlMenu +{ +public: + void Register(); + void Unregister(); + + /** This functions will be bound to appropriate Command. */ + void PushClicked(); + void SyncClicked(); + void RevertClicked(); + void RefreshClicked(); + +private: + bool HaveRemoteUrl() const; + + bool SaveDirtyPackages(); + TArray ListAllPackages(); + TArray UnlinkPackages(const TArray& InPackageNames); + void ReloadPackages(TArray& InPackagesToReload); + + bool StashAwayAnyModifications(); + void ReApplyStashedModifications(); + + void AddMenuExtension(FMenuBuilder& Builder); + + TSharedRef OnExtendLevelEditorViewMenu(const TSharedRef CommandList); + + void DisplayInProgressNotification(const FText& InOperationInProgressString); + void RemoveInProgressNotification(); + void DisplaySucessNotification(const FName& InOperationName); + void DisplayFailureNotification(const FName& InOperationName); + +private: + FDelegateHandle ViewMenuExtenderHandle; + + /** Was there a need to stash away modifications before Sync? */ + bool bStashMadeBeforeSync; + + /** Loaded packages to reload after a Sync or Revert operation */ + TArray PackagesToReload; + + /** Current source control operation from extended menu if any */ + TWeakPtr OperationInProgressNotification; + + /** Delegate called when a source control operation has completed */ + void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult); +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.cpp new file mode 100644 index 0000000..ce92181 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.cpp @@ -0,0 +1,65 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlModule.h" + +#include "Misc/App.h" +#include "Modules/ModuleManager.h" +#include "GitSourceControlOperations.h" +#include "Features/IModularFeatures.h" + +#define LOCTEXT_NAMESPACE "GitSourceControl" + +template +static TSharedRef CreateWorker() +{ + return MakeShareable( new Type() ); +} + +void FGitSourceControlModule::StartupModule() +{ + // Register our operations (implemented in GitSourceControlOperations.cpp by subclassing from Engine\Source\Developer\SourceControl\Public\SourceControlOperations.h) + GitSourceControlProvider.RegisterWorker( "Connect", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + // Note: this provider uses the "CheckOut" command only with Git LFS 2 "lock" command, since Git itself has no lock command (all tracked files in the working copy are always already checked-out). + GitSourceControlProvider.RegisterWorker( "CheckOut", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "UpdateStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "MarkForAdd", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "Delete", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "Revert", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "Sync", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "Push", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker ) ); + + // load our settings + GitSourceControlSettings.LoadSettings(); + + // Bind our source control provider to the editor + IModularFeatures::Get().RegisterModularFeature( "SourceControl", &GitSourceControlProvider ); +} + +void FGitSourceControlModule::ShutdownModule() +{ + // shut down the provider, as this module is going away + GitSourceControlProvider.Close(); + + // unbind provider from editor + IModularFeatures::Get().UnregisterModularFeature("SourceControl", &GitSourceControlProvider); +} + +void FGitSourceControlModule::SaveSettings() +{ + if (FApp::IsUnattended() || IsRunningCommandlet()) + { + return; + } + + GitSourceControlSettings.SaveSettings(); +} + +IMPLEMENT_MODULE(FGitSourceControlModule, GitSourceControl); + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.h new file mode 100644 index 0000000..161e3db --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlModule.h @@ -0,0 +1,114 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleInterface.h" +#include "GitSourceControlSettings.h" +#include "GitSourceControlProvider.h" + +/** + +UE4GitPlugin is a simple Git Source Control Plugin for Unreal Engine + +Written and contributed by Sebastien Rombauts (sebastien.rombauts@gmail.com) + +### Supported features +- initialize a new Git local repository ('git init') to manage your UE4 Game Project + - can also create an appropriate .gitignore file as part of initialization + - can also create a .gitattributes file to enable Git LFS (Large File System) as part of initialization + - can also make the initial commit, with custom multi-line message + - can also configure the default remote origin URL +- display status icons to show modified/added/deleted/untracked files +- show history of a file +- visual diff of a blueprint against depot or between previous versions of a file +- revert modifications of a file +- add, delete, rename a file +- checkin/commit a file (cannot handle atomically more than 50 files) +- migrate an asset between two projects if both are using Git +- solve a merge conflict on a blueprint +- show current branch name in status text +- Sync to Pull (rebase) the current branch +- Git LFS (Github, Gitlab, Bitbucket) is working with Git 2.10+ under Windows +- Git LFS 2 File Locking is working with Git 2.10+ and Git LFS 2.0.0 +- Windows, Mac and Linux + +### TODO +1. configure the name of the remote instead of default "origin" + +### TODO LFS 2.x File Locking + +Known issues: +0. False error logs after a successful push: +To https://github.com/SRombauts/UE4GitLfs2FileLocks.git + ee44ff5..59da15e HEAD -> master + +Use "TODO LFS" in the code to track things left to do/improve/refactor: +1. IsUsingGitLfsLocking() should be cached in the Provider to avoid calling AccessSettings() too frequently + it can not change without re-initializing (at least re-connect) the Provider! +2. Implement FGitSourceControlProvider::bWorkingOffline like the SubversionSourceControl plugin +3. Trying to deactivate Git LFS 2 file locking afterward on the "Login to Source Control" (Connect/Configure) screen + is not working after Git LFS 2 has switched "read-only" flag on files (which needs the Checkout operation to be editable)! + - temporarily deactivating locks may be required if we want to be able to work while not connected (do we really need this ???) + - does Git LFS have a command to do this deactivation ? + - perhaps should we rely on detection of such flags to detect LFS 2 usage (ie. the need to do a checkout) + - see SubversionSourceControl plugin that deals with such flags + - this would need a rework of the way the "bIsUsingFileLocking" si propagated, since this would no more be a configuration (or not only) but a file state + - else we should at least revert those read-only flags when going out of "Lock mode" +4. Optimize usage of "git lfs locks", ie reduce the use of UdpateStatus() in Operations + +### What *cannot* be done presently +- Branch/Merge are not in the current Editor workflow +- Fetch is not in the current Editor workflow +- Amend a commit is not in the current Editor workflow +- Configure user name & email ('git config user.name' & git config user.email') + +### Known issues +- the Editor does not show deleted files (only when deleted externally?) +- the Editor does not show missing files +- missing localization for git specific messages +- displaying states of 'Engine' assets (also needs management of 'out of tree' files) +- renaming a Blueprint in Editor leaves a redirector file, AND modify too much the asset to enable git to track its history through renaming +- standard Editor commit dialog asks if user wants to "Keep Files Checked Out" => no use for Git or Mercurial CanCheckOut()==false + + */ +class FGitSourceControlModule : public IModuleInterface +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; + + /** Access the Git source control settings */ + FGitSourceControlSettings& AccessSettings() + { + return GitSourceControlSettings; + } + const FGitSourceControlSettings& AccessSettings() const + { + return GitSourceControlSettings; + } + + /** Save the Git source control settings */ + void SaveSettings(); + + /** Access the Git source control provider */ + FGitSourceControlProvider& GetProvider() + { + return GitSourceControlProvider; + } + const FGitSourceControlProvider& GetProvider() const + { + return GitSourceControlProvider; + } + +private: + /** The Git source control provider */ + FGitSourceControlProvider GitSourceControlProvider; + + /** The settings for Git source control */ + FGitSourceControlSettings GitSourceControlSettings; +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.cpp new file mode 100644 index 0000000..0c48986 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.cpp @@ -0,0 +1,684 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlOperations.h" + +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "SourceControlOperations.h" +#include "ISourceControlModule.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlCommand.h" +#include "GitSourceControlUtils.h" +#include "Logging/MessageLog.h" + +#define LOCTEXT_NAMESPACE "GitSourceControl" + +FName FGitPush::GetName() const +{ + return "Push"; +} + +FText FGitPush::GetInProgressString() const +{ + // TODO Configure origin + return LOCTEXT("SourceControl_Push", "Pushing local commits to remote origin..."); +} + + +FName FGitConnectWorker::GetName() const +{ + return "Connect"; +} + +bool FGitConnectWorker::Execute(FGitSourceControlCommand& InCommand) +{ + check(InCommand.Operation->GetName() == GetName()); + TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); + + // Check Git Availability + if((InCommand.PathToGitBinary.Len() > 0) && GitSourceControlUtils::CheckGitAvailability(InCommand.PathToGitBinary)) + { + // Now update the status of assets in Content/ directory and also Config files + TArray ProjectDirs; + ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir())); + ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir())); + InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ErrorMessages, States); + if(!InCommand.bCommandSuccessful || InCommand.ErrorMessages.Num() > 0) + { + Operation->SetErrorText(LOCTEXT("NotAGitRepository", "Failed to enable Git source control. You need to initialize the project as a Git repository first.")); + InCommand.bCommandSuccessful = false; + } + else + { + GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); + + if(InCommand.bUsingGitLfsLocking) + { + // Check server connection by checking lock status (when using Git LFS file Locking worflow) + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs locks"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + } + } + } + else + { + Operation->SetErrorText(LOCTEXT("GitNotFound", "Failed to enable Git source control. You need to install Git and specify a valid path to git executable.")); + InCommand.bCommandSuccessful = false; + } + + return InCommand.bCommandSuccessful; +} + +bool FGitConnectWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +FName FGitCheckOutWorker::GetName() const +{ + return "CheckOut"; +} + +bool FGitCheckOutWorker::Execute(FGitSourceControlCommand& InCommand) +{ + check(InCommand.Operation->GetName() == GetName()); + + if(InCommand.bUsingGitLfsLocking) + { + // lock files: execute the LFS command on relative filenames + InCommand.bCommandSuccessful = true; + const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot); + for(const auto& RelativeFile : RelativeFiles) + { + TArray OneFile; + OneFile.Add(RelativeFile); + InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("lfs lock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages); + } + + // now update the status of our files + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States); + } + else + { + InCommand.bCommandSuccessful = false; + } + + return InCommand.bCommandSuccessful; +} + +bool FGitCheckOutWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +static FText ParseCommitResults(const TArray& InResults) +{ + if(InResults.Num() >= 1) + { + const FString& FirstLine = InResults[0]; + return FText::Format(LOCTEXT("CommitMessage", "Commited {0}."), FText::FromString(FirstLine)); + } + return LOCTEXT("CommitMessageUnknown", "Submitted revision."); +} + +// Get Locked Files (that is, CheckedOut files, not Added ones) +const TArray GetLockedFiles(const TArray& InFiles) +{ + TArray LockedFiles; + + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + + TArray> LocalStates; + Provider.GetState(InFiles, LocalStates, EStateCacheUsage::Use); + for(const auto& State : LocalStates) + { + if(State->IsCheckedOut()) + { + LockedFiles.Add(State->GetFilename()); + } + } + + return LockedFiles; +} + +FName FGitCheckInWorker::GetName() const +{ + return "CheckIn"; +} + +bool FGitCheckInWorker::Execute(FGitSourceControlCommand& InCommand) +{ + check(InCommand.Operation->GetName() == GetName()); + + TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); + + // make a temp file to place our commit message in + FGitScopedTempFile CommitMsgFile(Operation->GetDescription()); + if(CommitMsgFile.GetFilename().Len() > 0) + { + TArray Parameters; + FString ParamCommitMsgFilename = TEXT("--file=\""); + ParamCommitMsgFilename += FPaths::ConvertRelativePathToFull(CommitMsgFile.GetFilename()); + ParamCommitMsgFilename += TEXT("\""); + Parameters.Add(ParamCommitMsgFilename); + + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommit(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages); + if(InCommand.bCommandSuccessful) + { + // Remove any deleted files from status cache + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + + TArray> LocalStates; + Provider.GetState(InCommand.Files, LocalStates, EStateCacheUsage::Use); + for(const auto& State : LocalStates) + { + if(State->IsDeleted()) + { + Provider.RemoveFileFromCache(State->GetFilename()); + } + } + + Operation->SetSuccessMessage(ParseCommitResults(InCommand.InfoMessages)); + const FString Message = (InCommand.InfoMessages.Num() > 0) ? InCommand.InfoMessages[0] : TEXT(""); + UE_LOG(LogSourceControl, Log, TEXT("commit successful: %s"), *Message); + + // git-lfs: push and unlock files + if(InCommand.bUsingGitLfsLocking && InCommand.bCommandSuccessful) + { + TArray Parameters2; + // TODO Configure origin + Parameters2.Add(TEXT("origin")); + Parameters2.Add(TEXT("HEAD")); + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters2, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + if(!InCommand.bCommandSuccessful) + { + // if out of date, pull first, then try again + bool bWasOutOfDate = false; + for (const auto& PushError : InCommand.ErrorMessages) + { + if (PushError.Contains(TEXT("[rejected]")) && PushError.Contains(TEXT("non-fast-forward"))) + { + // Don't do it during iteration, want to append pull results to InCommand.ErrorMessages + bWasOutOfDate = true; + break; + } + } + if (bWasOutOfDate) + { + UE_LOG(LogSourceControl, Log, TEXT("Push failed because we're out of date, pulling automatically to try to resolve")); + // Use pull --rebase since that's what the pull command does by default + // This requires that we stash if dirty working copy though + bool bStashed = false; + bool bStashNeeded = false; + const TArray ParametersStatus{"--porcelain --untracked-files=no"}; + TArray StatusInfoMessages; + TArray StatusErrorMessages; + // Check if there is any modification to the working tree + const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStatus, TArray(), StatusInfoMessages, StatusErrorMessages); + if ((bStatusOk) && (StatusInfoMessages.Num() > 0)) + { + bStashNeeded = true; + const TArray ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" }; + bStashed = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStash, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + if (!bStashed) + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_StashFailed", "Stashing away modifications failed!")); + SourceControlLog.Notify(); + } + } + if (!bStashNeeded || bStashed) + { + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("pull --rebase"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + if (InCommand.bCommandSuccessful) + { + // Repeat the push + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push origin HEAD"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + } + + // Succeed or fail, restore the stash + if (bStashed) + { + const TArray ParametersStashPop{ "pop" }; + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStashPop, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + if (!InCommand.bCommandSuccessful) + { + FMessageLog SourceControlLog("SourceControl"); + SourceControlLog.Warning(LOCTEXT("SourceControlMenu_UnstashFailed", "Unstashing previously saved modifications failed!")); + SourceControlLog.Notify(); + } + } + } + } + } + if(InCommand.bCommandSuccessful) + { + // unlock files: execute the LFS command on relative filenames + // (unlock only locked files, that is, not Added files) + const TArray LockedFiles = GetLockedFiles(InCommand.Files); + if(LockedFiles.Num() > 0) + { + const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot); + for(const auto& RelativeFile : RelativeFiles) + { + TArray OneFile; + OneFile.Add(RelativeFile); + GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages); + } + } + } + } + } + } + + // now update the status of our files + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States); + GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); + + return InCommand.bCommandSuccessful; +} + +bool FGitCheckInWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +FName FGitMarkForAddWorker::GetName() const +{ + return "MarkForAdd"; +} + +bool FGitMarkForAddWorker::Execute(FGitSourceControlCommand& InCommand) +{ + check(InCommand.Operation->GetName() == GetName()); + + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages); + + // now update the status of our files + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States); + + return InCommand.bCommandSuccessful; +} + +bool FGitMarkForAddWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +FName FGitDeleteWorker::GetName() const +{ + return "Delete"; +} + +bool FGitDeleteWorker::Execute(FGitSourceControlCommand& InCommand) +{ + check(InCommand.Operation->GetName() == GetName()); + + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages); + + // now update the status of our files + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States); + + return InCommand.bCommandSuccessful; +} + +bool FGitDeleteWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + + +// Get lists of Missing files (ie "deleted"), Modified files, and "other than Added" Existing files +void GetMissingVsExistingFiles(const TArray& InFiles, TArray& OutMissingFiles, TArray& OutAllExistingFiles, TArray& OutOtherThanAddedExistingFiles) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + + const TArray Files = (InFiles.Num() > 0) ? (InFiles) : (Provider.GetFilesInCache()); + + TArray> LocalStates; + Provider.GetState(Files, LocalStates, EStateCacheUsage::Use); + for(const auto& State : LocalStates) + { + if(FPaths::FileExists(State->GetFilename())) + { + if(State->IsAdded()) + { + OutAllExistingFiles.Add(State->GetFilename()); + } + else if(State->IsModified()) + { + OutOtherThanAddedExistingFiles.Add(State->GetFilename()); + OutAllExistingFiles.Add(State->GetFilename()); + } + else if(State->CanRevert()) // for locked but unmodified files + { + OutOtherThanAddedExistingFiles.Add(State->GetFilename()); + } + } + else + { + if (State->IsSourceControlled()) + { + OutMissingFiles.Add(State->GetFilename()); + } + } + } +} + +FName FGitRevertWorker::GetName() const +{ + return "Revert"; +} + +bool FGitRevertWorker::Execute(FGitSourceControlCommand& InCommand) +{ + // Filter files by status to use the right "revert" commands on them + TArray MissingFiles; + TArray AllExistingFiles; + TArray OtherThanAddedExistingFiles; + GetMissingVsExistingFiles(InCommand.Files, MissingFiles, AllExistingFiles, OtherThanAddedExistingFiles); + + InCommand.bCommandSuccessful = true; + if(MissingFiles.Num() > 0) + { + // "Added" files that have been deleted needs to be removed from source control + InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("rm"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), MissingFiles, InCommand.InfoMessages, InCommand.ErrorMessages); + } + if(AllExistingFiles.Num() > 0) + { + // reset any changes already added to the index + InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("reset"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), AllExistingFiles, InCommand.InfoMessages, InCommand.ErrorMessages); + } + if(OtherThanAddedExistingFiles.Num() > 0) + { + // revert any changes in working copy (this would fails if the asset was in "Added" state, since after "reset" it is now "untracked") + InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("checkout"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OtherThanAddedExistingFiles, InCommand.InfoMessages, InCommand.ErrorMessages); + } + + if(InCommand.bUsingGitLfsLocking) + { + // unlock files: execute the LFS command on relative filenames + // (unlock only locked files, that is, not Added files) + const TArray LockedFiles = GetLockedFiles(OtherThanAddedExistingFiles); + if(LockedFiles.Num() > 0) + { + const TArray RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot); + for(const auto& RelativeFile : RelativeFiles) + { + TArray OneFile; + OneFile.Add(RelativeFile); + GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages); + } + } + } + + // If no files were specified (full revert), refresh all relevant files instead of the specified files (which is an empty list in full revert) + // This is required so that files that were "Marked for add" have their status updated after a full revert. + TArray FilesToUpdate = InCommand.Files; + if (InCommand.Files.Num() <= 0) + { + for (const auto& File : MissingFiles) FilesToUpdate.Add(File); + for (const auto& File : AllExistingFiles) FilesToUpdate.Add(File); + for (const auto& File : OtherThanAddedExistingFiles) FilesToUpdate.Add(File); + } + + // now update the status of our files + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, FilesToUpdate, InCommand.ErrorMessages, States); + + return InCommand.bCommandSuccessful; +} + +bool FGitRevertWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +FName FGitSyncWorker::GetName() const +{ + return "Sync"; +} + +bool FGitSyncWorker::Execute(FGitSourceControlCommand& InCommand) +{ + // pull the branch to get remote changes by rebasing any local commits (not merging them to avoid complex graphs) + TArray Parameters; + Parameters.Add(TEXT("--rebase")); + Parameters.Add(TEXT("--autostash")); + // TODO Configure origin + Parameters.Add(TEXT("origin")); + Parameters.Add(TEXT("HEAD")); + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("pull"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + + // now update the status of our files + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States); + GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); + + return InCommand.bCommandSuccessful; +} + +bool FGitSyncWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + + +FName FGitPushWorker::GetName() const +{ + return "Push"; +} + +bool FGitPushWorker::Execute(FGitSourceControlCommand& InCommand) +{ + + // If we have any locked files, check if we should unlock them + TArray FilesToUnlock; + if (InCommand.bUsingGitLfsLocking) + { + TMap Locks; + // Get locks as relative paths + GitSourceControlUtils::GetAllLocks(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, false, InCommand.ErrorMessages, Locks); + if(Locks.Num() > 0) + { + // test to see what lfs files we would push, and compare to locked files, unlock after if push OK + FString BranchName; + GitSourceControlUtils::GetBranchName(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, BranchName); + + TArray LfsPushParameters; + LfsPushParameters.Add(TEXT("push")); + LfsPushParameters.Add(TEXT("--dry-run")); + LfsPushParameters.Add(TEXT("origin")); + LfsPushParameters.Add(BranchName); + TArray LfsPushInfoMessages; + TArray LfsPushErrMessages; + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, LfsPushParameters, TArray(), LfsPushInfoMessages, LfsPushErrMessages); + + if(InCommand.bCommandSuccessful) + { + // Result format is of the form + // push f4ee401c063058a78842bb3ed98088e983c32aa447f346db54fa76f844a7e85e => Path/To/Asset.uasset + // With some potential informationals we can ignore + for (auto& Line : LfsPushInfoMessages) + { + if (Line.StartsWith(TEXT("push"))) + { + FString Prefix, Filename; + if (Line.Split(TEXT("=>"), &Prefix, &Filename)) + { + Filename = Filename.TrimStartAndEnd(); + if (Locks.Contains(Filename)) + { + // We do not need to check user or if the file has local modifications before attempting unlocking, git-lfs will reject the unlock if so + // No point duplicating effort here + FilesToUnlock.Add(Filename); + UE_LOG(LogSourceControl, Log, TEXT("Post-push will try to unlock: %s"), *Filename); + } + } + } + } + } + } + + } + // push the branch to its default remote + // (works only if the default remote "origin" is set and does not require authentication) + TArray Parameters; + Parameters.Add(TEXT("--set-upstream")); + // TODO Configure origin + Parameters.Add(TEXT("origin")); + Parameters.Add(TEXT("HEAD")); + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters, TArray(), InCommand.InfoMessages, InCommand.ErrorMessages); + + if(InCommand.bCommandSuccessful && InCommand.bUsingGitLfsLocking && FilesToUnlock.Num() > 0) + { + // unlock files: execute the LFS command on relative filenames + for(const auto& FileToUnlock : FilesToUnlock) + { + TArray OneFile; + OneFile.Add(FileToUnlock); + bool bUnlocked = GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), OneFile, InCommand.InfoMessages, InCommand.ErrorMessages); + if (!bUnlocked) + { + // Report but don't fail, it's not essential + UE_LOG(LogSourceControl, Log, TEXT("Unlock failed for %s"), *FileToUnlock); + } + } + + // We need to update status if we unlock + // This command needs absolute filenames + TArray AbsFilesToUnlock = GitSourceControlUtils::AbsoluteFilenames(FilesToUnlock, InCommand.PathToRepositoryRoot); + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, AbsFilesToUnlock, InCommand.ErrorMessages, States); + + } + + return InCommand.bCommandSuccessful; +} + +bool FGitPushWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +FName FGitUpdateStatusWorker::GetName() const +{ + return "UpdateStatus"; +} + +bool FGitUpdateStatusWorker::Execute(FGitSourceControlCommand& InCommand) +{ + check(InCommand.Operation->GetName() == GetName()); + + TSharedRef Operation = StaticCastSharedRef(InCommand.Operation); + + if(InCommand.Files.Num() > 0) + { + InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States); + GitSourceControlUtils::RemoveRedundantErrors(InCommand, TEXT("' is outside repository")); + + if(Operation->ShouldUpdateHistory()) + { + for(int32 Index = 0; Index < States.Num(); Index++) + { + FString& File = InCommand.Files[Index]; + TGitSourceControlHistory History; + + if(States[Index].IsConflicted()) + { + // In case of a merge conflict, we first need to get the tip of the "remote branch" (MERGE_HEAD) + GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, true, InCommand.ErrorMessages, History); + } + // Get the history of the file in the current branch + InCommand.bCommandSuccessful &= GitSourceControlUtils::RunGetHistory(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, File, false, InCommand.ErrorMessages, History); + Histories.Add(*File, History); + } + } + } + else + { + // no path provided: only update the status of assets in Content/ directory and also Config files + TArray ProjectDirs; + ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir())); + ProjectDirs.Add(FPaths::ConvertRelativePathToFull(FPaths::ProjectConfigDir())); + InCommand.bCommandSuccessful = GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, ProjectDirs, InCommand.ErrorMessages, States); + } + + GitSourceControlUtils::GetCommitInfo(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.CommitId, InCommand.CommitSummary); + + // don't use the ShouldUpdateModifiedState() hint here as it is specific to Perforce: the above normal Git status has already told us this information (like Git and Mercurial) + + return InCommand.bCommandSuccessful; +} + +bool FGitUpdateStatusWorker::UpdateStates() const +{ + bool bUpdated = GitSourceControlUtils::UpdateCachedStates(States); + + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" ); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + + const FDateTime Now = FDateTime::Now(); + + // add history, if any + for(const auto& History : Histories) + { + TSharedRef State = Provider.GetStateInternal(History.Key); + State->History = History.Value; + State->TimeStamp = Now; + bUpdated = true; + } + + return bUpdated; +} + +FName FGitCopyWorker::GetName() const +{ + return "Copy"; +} + +bool FGitCopyWorker::Execute(FGitSourceControlCommand& InCommand) +{ + check(InCommand.Operation->GetName() == GetName()); + + // Copy or Move operation on a single file : Git does not need an explicit copy nor move, + // but after a Move the Editor create a redirector file with the old asset name that points to the new asset. + // The redirector needs to be commited with the new asset to perform a real rename. + // => the following is to "MarkForAdd" the redirector, but it still need to be committed by selecting the whole directory and "check-in" + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, InCommand.InfoMessages, InCommand.ErrorMessages); + + return InCommand.bCommandSuccessful; +} + +bool FGitCopyWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +FName FGitResolveWorker::GetName() const +{ + return "Resolve"; +} + +bool FGitResolveWorker::Execute( class FGitSourceControlCommand& InCommand ) +{ + check(InCommand.Operation->GetName() == GetName()); + + // mark the conflicting files as resolved: + TArray Results; + InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray(), InCommand.Files, Results, InCommand.ErrorMessages); + + // now update the status of our files + GitSourceControlUtils::RunUpdateStatus(InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, InCommand.bUsingGitLfsLocking, InCommand.Files, InCommand.ErrorMessages, States); + + return InCommand.bCommandSuccessful; +} + +bool FGitResolveWorker::UpdateStates() const +{ + return GitSourceControlUtils::UpdateCachedStates(States); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.h new file mode 100644 index 0000000..5b9fb81 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlOperations.h @@ -0,0 +1,192 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "IGitSourceControlWorker.h" +#include "GitSourceControlState.h" + +#include "ISourceControlOperation.h" + +/** + * Internal operation used to push local commits to configured remote origin +*/ +class FGitPush : public ISourceControlOperation +{ +public: + // ISourceControlOperation interface + virtual FName GetName() const override; + + virtual FText GetInProgressString() const override; +}; + +/** Called when first activated on a project, and then at project load time. + * Look for the root directory of the git repository (where the ".git/" subdirectory is located). */ +class FGitConnectWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitConnectWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Lock (check-out) a set of files using Git LFS 2. */ +class FGitCheckOutWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitCheckOutWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Commit (check-in) a set of files to the local depot. */ +class FGitCheckInWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitCheckInWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Add an untraked file to source control (so only a subset of the git add command). */ +class FGitMarkForAddWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitMarkForAddWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Delete a file and remove it from source control. */ +class FGitDeleteWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitDeleteWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Revert any change to a file to its state on the local depot. */ +class FGitRevertWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitRevertWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Git pull --rebase to update branch from its configured remote */ +class FGitSyncWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitSyncWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Git push to publish branch for its configured remote */ +class FGitPushWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitPushWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** Get source control status of files on local working copy. */ +class FGitUpdateStatusWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitUpdateStatusWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; + + /** Map of filenames to history */ + TMap Histories; +}; + +/** Copy or Move operation on a single file */ +class FGitCopyWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitCopyWorker() {} + // IGitSourceControlWorker interface + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +public: + /** Temporary states for results */ + TArray States; +}; + +/** git add to mark a conflict as resolved */ +class FGitResolveWorker : public IGitSourceControlWorker +{ +public: + virtual ~FGitResolveWorker() {} + virtual FName GetName() const override; + virtual bool Execute(class FGitSourceControlCommand& InCommand) override; + virtual bool UpdateStates() const override; + +private: + /** Temporary states for results */ + TArray States; +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlPrivatePCH.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlPrivatePCH.h new file mode 100644 index 0000000..e5d9454 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlPrivatePCH.h @@ -0,0 +1,15 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "ISourceControlModule.h" +#include "ISourceControlOperation.h" +#include "ISourceControlProvider.h" +#include "ISourceControlRevision.h" +#include "ISourceControlState.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.cpp new file mode 100644 index 0000000..aa6c256 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.cpp @@ -0,0 +1,467 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlProvider.h" + +#include "HAL/PlatformProcess.h" +#include "Misc/Paths.h" +#include "Misc/QueuedThreadPool.h" +#include "Modules/ModuleManager.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "GitSourceControlCommand.h" +#include "ISourceControlModule.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlUtils.h" +#include "SGitSourceControlSettings.h" +#include "Logging/MessageLog.h" +#include "ScopedSourceControlProgress.h" +#include "SourceControlHelpers.h" +#include "SourceControlOperations.h" +#include "Interfaces/IPluginManager.h" + +#define LOCTEXT_NAMESPACE "GitSourceControl" + +static FName ProviderName("Git LFS 2"); + +void FGitSourceControlProvider::Init(bool bForceConnection) +{ + // Init() is called multiple times at startup: do not check git each time + if(!bGitAvailable) + { + const TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("GitSourceControl")); + if(Plugin.IsValid()) + { + UE_LOG(LogSourceControl, Log, TEXT("Git plugin '%s'"), *(Plugin->GetDescriptor().VersionName)); + } + + CheckGitAvailability(); + + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); + } + + // bForceConnection: not used anymore +} + +void FGitSourceControlProvider::CheckGitAvailability() +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + FString PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + if(PathToGitBinary.IsEmpty()) + { + // Try to find Git binary, and update settings accordingly + PathToGitBinary = GitSourceControlUtils::FindGitBinaryPath(); + if(!PathToGitBinary.IsEmpty()) + { + GitSourceControl.AccessSettings().SetBinaryPath(PathToGitBinary); + } + } + + if(!PathToGitBinary.IsEmpty()) + { + UE_LOG(LogSourceControl, Log, TEXT("Using '%s'"), *PathToGitBinary); + bGitAvailable = GitSourceControlUtils::CheckGitAvailability(PathToGitBinary, &GitVersion); + if(bGitAvailable) + { + CheckRepositoryStatus(PathToGitBinary); + } + } + else + { + bGitAvailable = false; + } +} + +void FGitSourceControlProvider::CheckRepositoryStatus(const FString& InPathToGitBinary) +{ + // Find the path to the root Git directory (if any, else uses the ProjectDir) + const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); + bGitRepositoryFound = GitSourceControlUtils::FindRootDirectory(PathToProjectDir, PathToRepositoryRoot); + if(bGitRepositoryFound) + { + GitSourceControlMenu.Register(); + + // Get branch name + bGitRepositoryFound = GitSourceControlUtils::GetBranchName(InPathToGitBinary, PathToRepositoryRoot, BranchName); + if(bGitRepositoryFound) + { + GitSourceControlUtils::GetRemoteUrl(InPathToGitBinary, PathToRepositoryRoot, RemoteUrl); + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("'%s' is not a valid Git repository"), *PathToRepositoryRoot); + } + } + else + { + UE_LOG(LogSourceControl, Warning, TEXT("'%s' is not part of a Git repository"), *FPaths::ProjectDir()); + } + + // Get user name & email (of the repository, else from the global Git config) + GitSourceControlUtils::GetUserConfig(InPathToGitBinary, PathToRepositoryRoot, UserName, UserEmail); +} + +void FGitSourceControlProvider::Close() +{ + // clear the cache + StateCache.Empty(); + // Remove all extensions to the "Source Control" menu in the Editor Toolbar + GitSourceControlMenu.Unregister(); + + bGitAvailable = false; + bGitRepositoryFound = false; + UserName.Empty(); + UserEmail.Empty(); +} + +TSharedRef FGitSourceControlProvider::GetStateInternal(const FString& Filename) +{ + TSharedRef* State = StateCache.Find(Filename); + if(State != NULL) + { + // found cached item + return (*State); + } + else + { + // cache an unknown state for this item + TSharedRef NewState = MakeShareable( new FGitSourceControlState(Filename, bUsingGitLfsLocking) ); + StateCache.Add(Filename, NewState); + return NewState; + } +} + +FText FGitSourceControlProvider::GetStatusText() const +{ + FFormatNamedArguments Args; + Args.Add( TEXT("RepositoryName"), FText::FromString(PathToRepositoryRoot) ); + Args.Add( TEXT("RemoteUrl"), FText::FromString(RemoteUrl) ); + Args.Add( TEXT("UserName"), FText::FromString(UserName) ); + Args.Add( TEXT("UserEmail"), FText::FromString(UserEmail) ); + Args.Add( TEXT("BranchName"), FText::FromString(BranchName) ); + Args.Add( TEXT("CommitId"), FText::FromString(CommitId.Left(8)) ); + Args.Add( TEXT("CommitSummary"), FText::FromString(CommitSummary) ); + + return FText::Format( NSLOCTEXT("Status", "Provider: Git\nEnabledLabel", "Local repository: {RepositoryName}\nRemote origin: {RemoteUrl}\nUser: {UserName}\nE-mail: {UserEmail}\n[{BranchName} {CommitId}] {CommitSummary}"), Args ); +} + +/** Quick check if source control is enabled */ +bool FGitSourceControlProvider::IsEnabled() const +{ + return bGitRepositoryFound; +} + +/** Quick check if source control is available for use (useful for server-based providers) */ +bool FGitSourceControlProvider::IsAvailable() const +{ + return bGitRepositoryFound; +} + +const FName& FGitSourceControlProvider::GetName(void) const +{ + return ProviderName; +} + +ECommandResult::Type FGitSourceControlProvider::GetState( const TArray& InFiles, TArray< TSharedRef >& OutState, EStateCacheUsage::Type InStateCacheUsage ) +{ + if(!IsEnabled()) + { + return ECommandResult::Failed; + } + + TArray AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles); + + if(InStateCacheUsage == EStateCacheUsage::ForceUpdate) + { + Execute(ISourceControlOperation::Create(), AbsoluteFiles); + } + + for(const auto& AbsoluteFile : AbsoluteFiles) + { + OutState.Add(GetStateInternal(*AbsoluteFile)); + } + + return ECommandResult::Succeeded; +} + +TArray FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef Predicate) const +{ + TArray Result; + for(const auto& CacheItem : StateCache) + { + FSourceControlStateRef State = CacheItem.Value; + if(Predicate(State)) + { + Result.Add(State); + } + } + return Result; +} + +bool FGitSourceControlProvider::RemoveFileFromCache(const FString& Filename) +{ + return StateCache.Remove(Filename) > 0; +} + +/** Get files in cache */ +TArray FGitSourceControlProvider::GetFilesInCache() +{ + TArray Files; + for (const auto& State : StateCache) + { + Files.Add(State.Key); + } + return Files; +} + +FDelegateHandle FGitSourceControlProvider::RegisterSourceControlStateChanged_Handle( const FSourceControlStateChanged::FDelegate& SourceControlStateChanged ) +{ + return OnSourceControlStateChanged.Add( SourceControlStateChanged ); +} + +void FGitSourceControlProvider::UnregisterSourceControlStateChanged_Handle( FDelegateHandle Handle ) +{ + OnSourceControlStateChanged.Remove( Handle ); +} + +ECommandResult::Type FGitSourceControlProvider::Execute( const TSharedRef& InOperation, const TArray& InFiles, EConcurrency::Type InConcurrency, const FSourceControlOperationComplete& InOperationCompleteDelegate ) +{ + if(!IsEnabled() && !(InOperation->GetName() == "Connect")) // Only Connect operation allowed while not Enabled (Repository found) + { + InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed); + return ECommandResult::Failed; + } + + TArray AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles); + + // Query to see if we allow this operation + TSharedPtr Worker = CreateWorker(InOperation->GetName()); + if(!Worker.IsValid()) + { + // this operation is unsupported by this source control provider + FFormatNamedArguments Arguments; + Arguments.Add( TEXT("OperationName"), FText::FromName(InOperation->GetName()) ); + Arguments.Add( TEXT("ProviderName"), FText::FromName(GetName()) ); + FText Message(FText::Format(LOCTEXT("UnsupportedOperation", "Operation '{OperationName}' not supported by source control provider '{ProviderName}'"), Arguments)); + FMessageLog("SourceControl").Error(Message); + InOperation->AddErrorMessge(Message); + + InOperationCompleteDelegate.ExecuteIfBound(InOperation, ECommandResult::Failed); + return ECommandResult::Failed; + } + + FGitSourceControlCommand* Command = new FGitSourceControlCommand(InOperation, Worker.ToSharedRef()); + Command->Files = AbsoluteFiles; + Command->OperationCompleteDelegate = InOperationCompleteDelegate; + + // fire off operation + if(InConcurrency == EConcurrency::Synchronous) + { + Command->bAutoDelete = false; + + UE_LOG(LogSourceControl, Log, TEXT("ExecuteSynchronousCommand(%s)"), *InOperation->GetName().ToString()); + return ExecuteSynchronousCommand(*Command, InOperation->GetInProgressString()); + } + else + { + Command->bAutoDelete = true; + + UE_LOG(LogSourceControl, Log, TEXT("IssueAsynchronousCommand(%s)"), *InOperation->GetName().ToString()); + return IssueCommand(*Command); + } +} + +bool FGitSourceControlProvider::CanCancelOperation( const TSharedRef& InOperation ) const +{ + return false; +} + +void FGitSourceControlProvider::CancelOperation( const TSharedRef& InOperation ) +{ +} + +bool FGitSourceControlProvider::UsesLocalReadOnlyState() const +{ + return bUsingGitLfsLocking; // Git LFS Lock uses read-only state +} + +bool FGitSourceControlProvider::UsesChangelists() const +{ + return false; +} + +bool FGitSourceControlProvider::UsesCheckout() const +{ + return bUsingGitLfsLocking; // Git LFS Lock uses read-only state +} + +TSharedPtr FGitSourceControlProvider::CreateWorker(const FName& InOperationName) const +{ + const FGetGitSourceControlWorker* Operation = WorkersMap.Find(InOperationName); + if(Operation != nullptr) + { + return Operation->Execute(); + } + + return nullptr; +} + +void FGitSourceControlProvider::RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate ) +{ + WorkersMap.Add( InName, InDelegate ); +} + +void FGitSourceControlProvider::OutputCommandMessages(const FGitSourceControlCommand& InCommand) const +{ + FMessageLog SourceControlLog("SourceControl"); + + for(int32 ErrorIndex = 0; ErrorIndex < InCommand.ErrorMessages.Num(); ++ErrorIndex) + { + SourceControlLog.Error(FText::FromString(InCommand.ErrorMessages[ErrorIndex])); + } + + for(int32 InfoIndex = 0; InfoIndex < InCommand.InfoMessages.Num(); ++InfoIndex) + { + SourceControlLog.Info(FText::FromString(InCommand.InfoMessages[InfoIndex])); + } +} + +void FGitSourceControlProvider::UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand) +{ + // For all operations running UpdateStatus, get Commit informations: + if (!InCommand.CommitId.IsEmpty()) + { + CommitId = InCommand.CommitId; + CommitSummary = InCommand.CommitSummary; + } +} + +void FGitSourceControlProvider::Tick() +{ + bool bStatesUpdated = false; + + for(int32 CommandIndex = 0; CommandIndex < CommandQueue.Num(); ++CommandIndex) + { + FGitSourceControlCommand& Command = *CommandQueue[CommandIndex]; + if(Command.bExecuteProcessed) + { + // Remove command from the queue + CommandQueue.RemoveAt(CommandIndex); + + // Update respository status on UpdateStatus operations + UpdateRepositoryStatus(Command); + + // let command update the states of any files + bStatesUpdated |= Command.Worker->UpdateStates(); + + // dump any messages to output log + OutputCommandMessages(Command); + + // run the completion delegate callback if we have one bound + Command.ReturnResults(); + + // commands that are left in the array during a tick need to be deleted + if(Command.bAutoDelete) + { + // Only delete commands that are not running 'synchronously' + delete &Command; + } + + // only do one command per tick loop, as we dont want concurrent modification + // of the command queue (which can happen in the completion delegate) + break; + } + } + + if(bStatesUpdated) + { + OnSourceControlStateChanged.Broadcast(); + } +} + +TArray< TSharedRef > FGitSourceControlProvider::GetLabels( const FString& InMatchingSpec ) const +{ + TArray< TSharedRef > Tags; + + // NOTE list labels. Called by CrashDebugHelper() (to remote debug Engine crash) + // and by SourceControlHelpers::AnnotateFile() (to add source file to report) + // Reserved for internal use by Epic Games with Perforce only + return Tags; +} + +#if SOURCE_CONTROL_WITH_SLATE +TSharedRef FGitSourceControlProvider::MakeSettingsWidget() const +{ + return SNew(SGitSourceControlSettings); +} +#endif + +ECommandResult::Type FGitSourceControlProvider::ExecuteSynchronousCommand(FGitSourceControlCommand& InCommand, const FText& Task) +{ + ECommandResult::Type Result = ECommandResult::Failed; + + // Display the progress dialog if a string was provided + { + FScopedSourceControlProgress Progress(Task); + + // Issue the command asynchronously... + IssueCommand( InCommand ); + + // ... then wait for its completion (thus making it synchronous) + while(!InCommand.bExecuteProcessed) + { + // Tick the command queue and update progress. + Tick(); + + Progress.Tick(); + + // Sleep so we don't busy-wait so much. + FPlatformProcess::Sleep(0.01f); + } + + // always do one more Tick() to make sure the command queue is cleaned up. + Tick(); + + if(InCommand.bCommandSuccessful) + { + Result = ECommandResult::Succeeded; + } + } + + // Delete the command now (asynchronous commands are deleted in the Tick() method) + check(!InCommand.bAutoDelete); + + // ensure commands that are not auto deleted do not end up in the command queue + if ( CommandQueue.Contains( &InCommand ) ) + { + CommandQueue.Remove( &InCommand ); + } + delete &InCommand; + + return Result; +} + +ECommandResult::Type FGitSourceControlProvider::IssueCommand(FGitSourceControlCommand& InCommand) +{ + if(GThreadPool != nullptr) + { + // Queue this to our worker thread(s) for resolving + GThreadPool->AddQueuedWork(&InCommand); + CommandQueue.Add(&InCommand); + return ECommandResult::Succeeded; + } + else + { + FText Message(LOCTEXT("NoSCCThreads", "There are no threads available to process the source control command.")); + + FMessageLog("SourceControl").Error(Message); + InCommand.bCommandSuccessful = false; + InCommand.Operation->AddErrorMessge(Message); + + return InCommand.ReturnResults(); + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.h new file mode 100644 index 0000000..97dce5b --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlProvider.h @@ -0,0 +1,210 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "ISourceControlOperation.h" +#include "ISourceControlState.h" +#include "ISourceControlProvider.h" +#include "IGitSourceControlWorker.h" +#include "GitSourceControlState.h" +#include "GitSourceControlMenu.h" + +class FGitSourceControlCommand; + +DECLARE_DELEGATE_RetVal(FGitSourceControlWorkerRef, FGetGitSourceControlWorker) + +/// Git version and capabilites extracted from the string "git version 2.11.0.windows.3" +struct FGitVersion +{ + // Git version extracted from the string "git version 2.11.0.windows.3" (Windows) or "git version 2.11.0" (Linux/Mac/Cygwin/WSL) + int Major; // 2 Major version number + int Minor; // 11 Minor version number + int Patch; // 0 Patch/bugfix number + int Windows; // 3 Windows specific revision number (under Windows only) + + uint32 bHasCatFileWithFilters : 1; + uint32 bHasGitLfs : 1; + uint32 bHasGitLfsLocking : 1; + + FGitVersion() + : Major(0) + , Minor(0) + , Patch(0) + , Windows(0) + , bHasCatFileWithFilters(false) + , bHasGitLfs(false) + , bHasGitLfsLocking(false) + { + } + + inline bool IsGreaterOrEqualThan(int InMajor, int InMinor) const + { + return (Major > InMajor) || (Major == InMajor && (Minor >= InMinor)); + } +}; + +class FGitSourceControlProvider : public ISourceControlProvider +{ +public: + /** Constructor */ + FGitSourceControlProvider() + : bGitAvailable(false) + , bGitRepositoryFound(false) + { + } + + /* ISourceControlProvider implementation */ + virtual void Init(bool bForceConnection = true) override; + virtual void Close() override; + virtual FText GetStatusText() const override; + virtual bool IsEnabled() const override; + virtual bool IsAvailable() const override; + virtual const FName& GetName(void) const override; + virtual bool QueryStateBranchConfig(const FString& ConfigSrc, const FString& ConfigDest) /* override UE4.20 */ { return false; } + virtual void RegisterStateBranches(const TArray& BranchNames, const FString& ContentRoot) /* override UE4.20 */ {} + virtual int32 GetStateBranchIndex(const FString& InBranchName) const /* override UE4.20 */ { return INDEX_NONE; } + virtual ECommandResult::Type GetState( const TArray& InFiles, TArray< TSharedRef >& OutState, EStateCacheUsage::Type InStateCacheUsage ) override; + virtual TArray GetCachedStateByPredicate(TFunctionRef Predicate) const override; + virtual FDelegateHandle RegisterSourceControlStateChanged_Handle(const FSourceControlStateChanged::FDelegate& SourceControlStateChanged) override; + virtual void UnregisterSourceControlStateChanged_Handle(FDelegateHandle Handle) override; + virtual ECommandResult::Type Execute(const TSharedRef& InOperation, const TArray& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override; + virtual bool CanCancelOperation( const TSharedRef& InOperation ) const override; + virtual void CancelOperation( const TSharedRef& InOperation ) override; + virtual bool UsesLocalReadOnlyState() const override; + virtual bool UsesChangelists() const override; + virtual bool UsesCheckout() const override; + virtual void Tick() override; + virtual TArray< TSharedRef > GetLabels( const FString& InMatchingSpec ) const override; +#if SOURCE_CONTROL_WITH_SLATE + virtual TSharedRef MakeSettingsWidget() const override; +#endif + + /** + * Check configuration, else standard paths, and run a Git "version" command to check the availability of the binary. + */ + void CheckGitAvailability(); + + /** + * Find the .git/ repository and check it's status. + */ + void CheckRepositoryStatus(const FString& InPathToGitBinary); + + /** Is git binary found and working. */ + inline bool IsGitAvailable() const + { + return bGitAvailable; + } + + /** Git version for feature checking */ + inline const FGitVersion& GetGitVersion() const + { + return GitVersion; + } + + /** Get the path to the root of the Git repository: can be the ProjectDir itself, or any parent directory */ + inline const FString& GetPathToRepositoryRoot() const + { + return PathToRepositoryRoot; + } + + /** Git config user.name */ + inline const FString& GetUserName() const + { + return UserName; + } + + /** Git config user.email */ + inline const FString& GetUserEmail() const + { + return UserEmail; + } + + /** Git remote origin url */ + inline const FString& GetRemoteUrl() const + { + return RemoteUrl; + } + + /** Helper function used to update state cache */ + TSharedRef GetStateInternal(const FString& Filename); + + /** + * Register a worker with the provider. + * This is used internally so the provider can maintain a map of all available operations. + */ + void RegisterWorker( const FName& InName, const FGetGitSourceControlWorker& InDelegate ); + + /** Remove a named file from the state cache */ + bool RemoveFileFromCache(const FString& Filename); + + /** Get files in cache */ + TArray GetFilesInCache(); + +private: + + /** Is git binary found and working. */ + bool bGitAvailable; + + /** Is git repository found. */ + bool bGitRepositoryFound; + + /** Is LFS File Locking enabled? */ + bool bUsingGitLfsLocking = false; + + /** Helper function for Execute() */ + TSharedPtr CreateWorker(const FName& InOperationName) const; + + /** Helper function for running command synchronously. */ + ECommandResult::Type ExecuteSynchronousCommand(class FGitSourceControlCommand& InCommand, const FText& Task); + /** Issue a command asynchronously if possible. */ + ECommandResult::Type IssueCommand(class FGitSourceControlCommand& InCommand); + + /** Output any messages this command holds */ + void OutputCommandMessages(const class FGitSourceControlCommand& InCommand) const; + + /** Update repository status on Connect and UpdateStatus operations */ + void UpdateRepositoryStatus(const class FGitSourceControlCommand& InCommand); + + /** Path to the root of the Git repository: can be the ProjectDir itself, or any parent directory (found by the "Connect" operation) */ + FString PathToRepositoryRoot; + + /** Git config user.name (from local repository, else globally) */ + FString UserName; + + /** Git config user.email (from local repository, else globally) */ + FString UserEmail; + + /** Name of the current branch */ + FString BranchName; + + /** URL of the "origin" defaut remote server */ + FString RemoteUrl; + + /** Current Commit full SHA1 */ + FString CommitId; + + /** Current Commit description's Summary */ + FString CommitSummary; + + /** State cache */ + TMap > StateCache; + + /** The currently registered source control operations */ + TMap WorkersMap; + + /** Queue for commands given by the main thread */ + TArray < FGitSourceControlCommand* > CommandQueue; + + /** For notifying when the source control states in the cache have changed */ + FSourceControlStateChanged OnSourceControlStateChanged; + + /** Git version for feature checking */ + FGitVersion GitVersion; + + /** Source Control Menu Extension */ + FGitSourceControlMenu GitSourceControlMenu; +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.cpp new file mode 100644 index 0000000..0858f7f --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.cpp @@ -0,0 +1,114 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlRevision.h" + +#include "HAL/FileManager.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlUtils.h" + +#define LOCTEXT_NAMESPACE "GitSourceControl" + +bool FGitSourceControlRevision::Get( FString& InOutFilename ) const +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FString PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + const FString PathToRepositoryRoot = GitSourceControl.GetProvider().GetPathToRepositoryRoot(); + + // if a filename for the temp file wasn't supplied generate a unique-ish one + if(InOutFilename.Len() == 0) + { + // create the diff dir if we don't already have it (Git wont) + IFileManager::Get().MakeDirectory(*FPaths::DiffDir(), true); + // create a unique temp file name based on the unique commit Id + const FString TempFileName = FString::Printf(TEXT("%stemp-%s-%s"), *FPaths::DiffDir(), *CommitId, *FPaths::GetCleanFilename(Filename)); + InOutFilename = FPaths::ConvertRelativePathToFull(TempFileName); + } + + // Diff against the revision + const FString Parameter = FString::Printf(TEXT("%s:%s"), *CommitId, *Filename); + + bool bCommandSuccessful; + if(FPaths::FileExists(InOutFilename)) + { + bCommandSuccessful = true; // if the temp file already exists, reuse it directly + } + else + { + bCommandSuccessful = GitSourceControlUtils::RunDumpToFile(PathToGitBinary, PathToRepositoryRoot, Parameter, InOutFilename); + } + return bCommandSuccessful; +} + +bool FGitSourceControlRevision::GetAnnotated( TArray& OutLines ) const +{ + return false; +} + +bool FGitSourceControlRevision::GetAnnotated( FString& InOutFilename ) const +{ + return false; +} + +const FString& FGitSourceControlRevision::GetFilename() const +{ + return Filename; +} + +int32 FGitSourceControlRevision::GetRevisionNumber() const +{ + return RevisionNumber; +} + +const FString& FGitSourceControlRevision::GetRevision() const +{ + return ShortCommitId; +} + +const FString& FGitSourceControlRevision::GetDescription() const +{ + return Description; +} + +const FString& FGitSourceControlRevision::GetUserName() const +{ + return UserName; +} + +const FString& FGitSourceControlRevision::GetClientSpec() const +{ + static FString EmptyString(TEXT("")); + return EmptyString; +} + +const FString& FGitSourceControlRevision::GetAction() const +{ + return Action; +} + +TSharedPtr FGitSourceControlRevision::GetBranchSource() const +{ + // if this revision was copied/moved from some other revision + return BranchSource; +} + +const FDateTime& FGitSourceControlRevision::GetDate() const +{ + return Date; +} + +int32 FGitSourceControlRevision::GetCheckInIdentifier() const +{ + return CommitIdNumber; +} + +int32 FGitSourceControlRevision::GetFileSize() const +{ + return FileSize; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.h new file mode 100644 index 0000000..d2cf7ab --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlRevision.h @@ -0,0 +1,76 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "ISourceControlRevision.h" + +/** Revision of a file, linked to a specific commit */ +class FGitSourceControlRevision : public ISourceControlRevision, public TSharedFromThis +{ +public: + FGitSourceControlRevision() + : RevisionNumber(0) + { + } + + /** ISourceControlRevision interface */ + virtual bool Get( FString& InOutFilename ) const override; + virtual bool GetAnnotated( TArray& OutLines ) const override; + virtual bool GetAnnotated( FString& InOutFilename ) const override; + virtual const FString& GetFilename() const override; + virtual int32 GetRevisionNumber() const override; + virtual const FString& GetRevision() const override; + virtual const FString& GetDescription() const override; + virtual const FString& GetUserName() const override; + virtual const FString& GetClientSpec() const override; + virtual const FString& GetAction() const override; + virtual TSharedPtr GetBranchSource() const override; + virtual const FDateTime& GetDate() const override; + virtual int32 GetCheckInIdentifier() const override; + virtual int32 GetFileSize() const override; + +public: + + /** The filename this revision refers to */ + FString Filename; + + /** The full hexadecimal SHA1 id of the commit this revision refers to */ + FString CommitId; + + /** The short hexadecimal SHA1 id (8 first hex char out of 40) of the commit: the string to display */ + FString ShortCommitId; + + /** The numeric value of the short SHA1 (8 first hex char out of 40) */ + int32 CommitIdNumber; + + /** The index of the revision in the history (SBlueprintRevisionMenu assumes order for the "Depot" label) */ + int32 RevisionNumber; + + /** The SHA1 identifier of the file at this revision */ + FString FileHash; + + /** The description of this revision */ + FString Description; + + /** The user that made the change */ + FString UserName; + + /** The action (add, edit, branch etc.) performed at this revision */ + FString Action; + + /** Source of move ("branch" in Perforce term) if any */ + TSharedPtr BranchSource; + + /** The date this revision was made */ + FDateTime Date; + + /** The size of the file at this revision */ + int32 FileSize; +}; + +/** History composed of the last 100 revisions of the file */ +typedef TArray< TSharedRef > TGitSourceControlHistory; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.cpp new file mode 100644 index 0000000..b03c6e5 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.cpp @@ -0,0 +1,90 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlSettings.h" + +#include "Misc/ScopeLock.h" +#include "Misc/ConfigCacheIni.h" +#include "Modules/ModuleManager.h" +#include "SourceControlHelpers.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlUtils.h" + +namespace GitSettingsConstants +{ + +/** The section of the ini file we load our settings from */ +static const FString SettingsSection = TEXT("GitSourceControl.GitSourceControlSettings"); + +} + +const FString FGitSourceControlSettings::GetBinaryPath() const +{ + FScopeLock ScopeLock(&CriticalSection); + return BinaryPath; // Return a copy to be thread-safe +} + +bool FGitSourceControlSettings::SetBinaryPath(const FString& InString) +{ + FScopeLock ScopeLock(&CriticalSection); + const bool bChanged = (BinaryPath != InString); + if(bChanged) + { + BinaryPath = InString; + } + return bChanged; +} + +/** Tell if using the Git LFS file Locking workflow */ +bool FGitSourceControlSettings::IsUsingGitLfsLocking() const +{ + FScopeLock ScopeLock(&CriticalSection); + return bUsingGitLfsLocking; +} + +/** Configure the usage of Git LFS file Locking workflow */ +bool FGitSourceControlSettings::SetUsingGitLfsLocking(const bool InUsingGitLfsLocking) +{ + FScopeLock ScopeLock(&CriticalSection); + const bool bChanged = (bUsingGitLfsLocking != InUsingGitLfsLocking); + bUsingGitLfsLocking = InUsingGitLfsLocking; + return bChanged; +} + +const FString FGitSourceControlSettings::GetLfsUserName() const +{ + FScopeLock ScopeLock(&CriticalSection); + return LfsUserName; // Return a copy to be thread-safe +} + +bool FGitSourceControlSettings::SetLfsUserName(const FString& InString) +{ + FScopeLock ScopeLock(&CriticalSection); + const bool bChanged = (LfsUserName != InString); + if (bChanged) + { + LfsUserName = InString; + } + return bChanged; +} + +// This is called at startup nearly before anything else in our module: BinaryPath will then be used by the provider +void FGitSourceControlSettings::LoadSettings() +{ + FScopeLock ScopeLock(&CriticalSection); + const FString& IniFile = SourceControlHelpers::GetSettingsIni(); + GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), BinaryPath, IniFile); + GConfig->GetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile); + GConfig->GetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), LfsUserName, IniFile); +} + +void FGitSourceControlSettings::SaveSettings() const +{ + FScopeLock ScopeLock(&CriticalSection); + const FString& IniFile = SourceControlHelpers::GetSettingsIni(); + GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("BinaryPath"), *BinaryPath, IniFile); + GConfig->SetBool(*GitSettingsConstants::SettingsSection, TEXT("UsingGitLfsLocking"), bUsingGitLfsLocking, IniFile); + GConfig->SetString(*GitSettingsConstants::SettingsSection, TEXT("LfsUserName"), *LfsUserName, IniFile); +} diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.h new file mode 100644 index 0000000..3a9549f --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlSettings.h @@ -0,0 +1,49 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" + +class FGitSourceControlSettings +{ +public: + /** Get the Git Binary Path */ + const FString GetBinaryPath() const; + + /** Set the Git Binary Path */ + bool SetBinaryPath(const FString& InString); + + /** Tell if using the Git LFS file Locking workflow */ + bool IsUsingGitLfsLocking() const; + + /** Configure the usage of Git LFS file Locking workflow */ + bool SetUsingGitLfsLocking(const bool InUsingGitLfsLocking); + + /** Get the username used by the Git LFS 2 File Locks server */ + const FString GetLfsUserName() const; + + /** Set the username used by the Git LFS 2 File Locks server */ + bool SetLfsUserName(const FString& InString); + + /** Load settings from ini file */ + void LoadSettings(); + + /** Save settings to ini file */ + void SaveSettings() const; + +private: + /** A critical section for settings access */ + mutable FCriticalSection CriticalSection; + + /** Git binary path */ + FString BinaryPath; + + /** Tells if using the Git LFS file Locking workflow */ + bool bUsingGitLfsLocking; + + /** Username used by the Git LFS 2 File Locks server */ + FString LfsUserName; +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.cpp new file mode 100644 index 0000000..8801ffd --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.cpp @@ -0,0 +1,386 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlState.h" + +#define LOCTEXT_NAMESPACE "GitSourceControl.State" + +int32 FGitSourceControlState::GetHistorySize() const +{ + return History.Num(); +} + +TSharedPtr FGitSourceControlState::GetHistoryItem( int32 HistoryIndex ) const +{ + check(History.IsValidIndex(HistoryIndex)); + return History[HistoryIndex]; +} + +TSharedPtr FGitSourceControlState::FindHistoryRevision( int32 RevisionNumber ) const +{ + for(const auto& Revision : History) + { + if(Revision->GetRevisionNumber() == RevisionNumber) + { + return Revision; + } + } + + return nullptr; +} + +TSharedPtr FGitSourceControlState::FindHistoryRevision(const FString& InRevision) const +{ + for(const auto& Revision : History) + { + if(Revision->GetRevision() == InRevision) + { + return Revision; + } + } + + return nullptr; +} + +TSharedPtr FGitSourceControlState::GetBaseRevForMerge() const +{ + for(const auto& Revision : History) + { + // look for the the SHA1 id of the file, not the commit id (revision) + if(Revision->FileHash == PendingMergeBaseFileHash) + { + return Revision; + } + } + + return nullptr; +} + +// @todo add Slate icons for git specific states (NotAtHead vs Conflicted...) +FName FGitSourceControlState::GetIconName() const +{ + if(LockState == ELockState::Locked) + { + return FName("Subversion.CheckedOut"); + } + else if(LockState == ELockState::LockedOther) + { + return FName("Subversion.CheckedOutByOtherUser"); + } + else if (!IsCurrent()) + { + return FName("Subversion.NotAtHeadRevision"); + } + + switch(WorkingCopyState) + { + case EWorkingCopyState::Modified: + if(bUsingGitLfsLocking) + { + return FName("Subversion.NotInDepot"); + } + else + { + return FName("Subversion.CheckedOut"); + } + case EWorkingCopyState::Added: + return FName("Subversion.OpenForAdd"); + case EWorkingCopyState::Renamed: + case EWorkingCopyState::Copied: + return FName("Subversion.Branched"); + case EWorkingCopyState::Deleted: // Deleted & Missing files does not show in Content Browser + case EWorkingCopyState::Missing: + return FName("Subversion.MarkedForDelete"); + case EWorkingCopyState::Conflicted: + return FName("Subversion.ModifiedOtherBranch"); + case EWorkingCopyState::NotControlled: + return FName("Subversion.NotInDepot"); + case EWorkingCopyState::Unknown: + case EWorkingCopyState::Unchanged: // Unchanged is the same as "Pristine" (not checked out) for Perforce, ie no icon + case EWorkingCopyState::Ignored: + default: + return NAME_None; + } + + return NAME_None; +} + +FName FGitSourceControlState::GetSmallIconName() const +{ + if(LockState == ELockState::Locked) + { + return FName("Subversion.CheckedOut_Small"); + } + else if(LockState == ELockState::LockedOther) + { + return FName("Subversion.CheckedOutByOtherUser_Small"); + } + else if (!IsCurrent()) + { + return FName("Subversion.NotAtHeadRevision_Small"); + } + + switch(WorkingCopyState) + { + case EWorkingCopyState::Modified: + if(bUsingGitLfsLocking) + { + return FName("Subversion.NotInDepot_Small"); + } + else + { + return FName("Subversion.CheckedOut_Small"); + } + case EWorkingCopyState::Added: + return FName("Subversion.OpenForAdd_Small"); + case EWorkingCopyState::Renamed: + case EWorkingCopyState::Copied: + return FName("Subversion.Branched_Small"); + case EWorkingCopyState::Deleted: // Deleted & Missing files can appear in the Submit to Source Control window + case EWorkingCopyState::Missing: + return FName("Subversion.MarkedForDelete_Small"); + case EWorkingCopyState::Conflicted: + return FName("Subversion.ModifiedOtherBranch_Small"); + case EWorkingCopyState::NotControlled: + return FName("Subversion.NotInDepot_Small"); + case EWorkingCopyState::Unknown: + case EWorkingCopyState::Unchanged: // Unchanged is the same as "Pristine" (not checked out) for Perforce, ie no icon + case EWorkingCopyState::Ignored: + default: + return NAME_None; + } + + return NAME_None; +} + +FText FGitSourceControlState::GetDisplayName() const +{ + if(LockState == ELockState::Locked) + { + return LOCTEXT("Locked", "Locked For Editing"); + } + else if(LockState == ELockState::LockedOther) + { + return FText::Format( LOCTEXT("LockedOther", "Locked by "), FText::FromString(LockUser) ); + } + else if (!IsCurrent()) + { + return LOCTEXT("NotCurrent", "Not current"); + } + + switch(WorkingCopyState) + { + case EWorkingCopyState::Unknown: + return LOCTEXT("Unknown", "Unknown"); + case EWorkingCopyState::Unchanged: + return LOCTEXT("Unchanged", "Unchanged"); + case EWorkingCopyState::Added: + return LOCTEXT("Added", "Added"); + case EWorkingCopyState::Deleted: + return LOCTEXT("Deleted", "Deleted"); + case EWorkingCopyState::Modified: + return LOCTEXT("Modified", "Modified"); + case EWorkingCopyState::Renamed: + return LOCTEXT("Renamed", "Renamed"); + case EWorkingCopyState::Copied: + return LOCTEXT("Copied", "Copied"); + case EWorkingCopyState::Conflicted: + return LOCTEXT("ContentsConflict", "Contents Conflict"); + case EWorkingCopyState::Ignored: + return LOCTEXT("Ignored", "Ignored"); + case EWorkingCopyState::NotControlled: + return LOCTEXT("NotControlled", "Not Under Source Control"); + case EWorkingCopyState::Missing: + return LOCTEXT("Missing", "Missing"); + } + + return FText(); +} + +FText FGitSourceControlState::GetDisplayTooltip() const +{ + if(LockState == ELockState::Locked) + { + return LOCTEXT("Locked_Tooltip", "Locked for editing by current user"); + } + else if(LockState == ELockState::LockedOther) + { + return FText::Format( LOCTEXT("LockedOther_Tooltip", "Locked for editing by: {0}"), FText::FromString(LockUser) ); + } + else if (!IsCurrent()) + { + return LOCTEXT("NotCurrent_Tooltip", "The file(s) are not at the head revision"); + } + + switch(WorkingCopyState) + { + case EWorkingCopyState::Unknown: + return LOCTEXT("Unknown_Tooltip", "Unknown source control state"); + case EWorkingCopyState::Unchanged: + return LOCTEXT("Pristine_Tooltip", "There are no modifications"); + case EWorkingCopyState::Added: + return LOCTEXT("Added_Tooltip", "Item is scheduled for addition"); + case EWorkingCopyState::Deleted: + return LOCTEXT("Deleted_Tooltip", "Item is scheduled for deletion"); + case EWorkingCopyState::Modified: + return LOCTEXT("Modified_Tooltip", "Item has been modified"); + case EWorkingCopyState::Renamed: + return LOCTEXT("Renamed_Tooltip", "Item has been renamed"); + case EWorkingCopyState::Copied: + return LOCTEXT("Copied_Tooltip", "Item has been copied"); + case EWorkingCopyState::Conflicted: + return LOCTEXT("ContentsConflict_Tooltip", "The contents of the item conflict with updates received from the repository."); + case EWorkingCopyState::Ignored: + return LOCTEXT("Ignored_Tooltip", "Item is being ignored."); + case EWorkingCopyState::NotControlled: + return LOCTEXT("NotControlled_Tooltip", "Item is not under version control."); + case EWorkingCopyState::Missing: + return LOCTEXT("Missing_Tooltip", "Item is missing (e.g., you moved or deleted it without using Git). This also indicates that a directory is incomplete (a checkout or update was interrupted)."); + } + + return FText(); +} + +const FString& FGitSourceControlState::GetFilename() const +{ + return LocalFilename; +} + +const FDateTime& FGitSourceControlState::GetTimeStamp() const +{ + return TimeStamp; +} + +// Deleted and Missing assets cannot appear in the Content Browser, but the do in the Submit files to Source Control window! +bool FGitSourceControlState::CanCheckIn() const +{ + if(bUsingGitLfsLocking) + { + return ( ( (LockState == ELockState::Locked) && !IsConflicted() ) || (WorkingCopyState == EWorkingCopyState::Added) ) && IsCurrent(); + } + else + { + return (WorkingCopyState == EWorkingCopyState::Added + || WorkingCopyState == EWorkingCopyState::Deleted + || WorkingCopyState == EWorkingCopyState::Missing + || WorkingCopyState == EWorkingCopyState::Modified + || WorkingCopyState == EWorkingCopyState::Renamed) && IsCurrent(); + } +} + +bool FGitSourceControlState::CanCheckout() const +{ + if(bUsingGitLfsLocking) + { + // We don't want to allow checkout if the file is out-of-date, as modifying an out-of-date binary file will most likely result in a merge conflict + return (WorkingCopyState == EWorkingCopyState::Unchanged || WorkingCopyState == EWorkingCopyState::Modified) && LockState == ELockState::NotLocked && IsCurrent(); + } + else + { + return false; // With Git all tracked files in the working copy are always already checked-out (as opposed to Perforce) + } +} + +bool FGitSourceControlState::IsCheckedOut() const +{ + if (bUsingGitLfsLocking) + { + return LockState == ELockState::Locked; + } + else + { + return IsSourceControlled(); // With Git all tracked files in the working copy are always checked-out (as opposed to Perforce) + } +} + +bool FGitSourceControlState::IsCheckedOutOther(FString* Who) const +{ + if (Who != NULL) + { + *Who = LockUser; + } + return LockState == ELockState::LockedOther; +} + +bool FGitSourceControlState::IsCurrent() const +{ + return !bNewerVersionOnServer; +} + +bool FGitSourceControlState::IsSourceControlled() const +{ + return WorkingCopyState != EWorkingCopyState::NotControlled && WorkingCopyState != EWorkingCopyState::Ignored && WorkingCopyState != EWorkingCopyState::Unknown; +} + +bool FGitSourceControlState::IsAdded() const +{ + return WorkingCopyState == EWorkingCopyState::Added; +} + +bool FGitSourceControlState::IsDeleted() const +{ + return WorkingCopyState == EWorkingCopyState::Deleted || WorkingCopyState == EWorkingCopyState::Missing; +} + +bool FGitSourceControlState::IsIgnored() const +{ + return WorkingCopyState == EWorkingCopyState::Ignored; +} + +bool FGitSourceControlState::CanEdit() const +{ + return IsCurrent(); // With Git all files in the working copy are always editable (as opposed to Perforce) +} + +bool FGitSourceControlState::CanDelete() const +{ + return !IsCheckedOutOther() && IsSourceControlled() && IsCurrent(); +} + +bool FGitSourceControlState::IsUnknown() const +{ + return WorkingCopyState == EWorkingCopyState::Unknown; +} + +bool FGitSourceControlState::IsModified() const +{ + // Warning: for Perforce, a checked-out file is locked for modification (whereas with Git all tracked files are checked-out), + // so for a clean "check-in" (commit) checked-out files unmodified should be removed from the changeset (the index) + // http://stackoverflow.com/questions/12357971/what-does-revert-unchanged-files-mean-in-perforce + // + // Thus, before check-in UE4 Editor call RevertUnchangedFiles() in PromptForCheckin() and CheckinFiles(). + // + // So here we must take care to enumerate all states that need to be commited, + // all other will be discarded : + // - Unknown + // - Unchanged + // - NotControlled + // - Ignored + return WorkingCopyState == EWorkingCopyState::Added + || WorkingCopyState == EWorkingCopyState::Deleted + || WorkingCopyState == EWorkingCopyState::Modified + || WorkingCopyState == EWorkingCopyState::Renamed + || WorkingCopyState == EWorkingCopyState::Copied + || WorkingCopyState == EWorkingCopyState::Missing + || WorkingCopyState == EWorkingCopyState::Conflicted; +} + + +bool FGitSourceControlState::CanAdd() const +{ + return WorkingCopyState == EWorkingCopyState::NotControlled; +} + +bool FGitSourceControlState::IsConflicted() const +{ + return WorkingCopyState == EWorkingCopyState::Conflicted; +} + +bool FGitSourceControlState::CanRevert() const +{ + return CanCheckIn(); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.h new file mode 100644 index 0000000..d1ea0fa --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlState.h @@ -0,0 +1,117 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "ISourceControlState.h" +#include "ISourceControlRevision.h" +#include "GitSourceControlRevision.h" + +namespace EWorkingCopyState +{ + enum Type + { + Unknown, + Unchanged, // called "clean" in SVN, "Pristine" in Perforce + Added, + Deleted, + Modified, + Renamed, + Copied, + Missing, + Conflicted, + NotControlled, + Ignored, + }; +} + +namespace ELockState +{ + enum Type + { + Unknown, + NotLocked, + Locked, + LockedOther, + }; +} + +class FGitSourceControlState : public ISourceControlState, public TSharedFromThis +{ +public: + FGitSourceControlState( const FString& InLocalFilename, const bool InUsingLfsLocking) + : LocalFilename(InLocalFilename) + , WorkingCopyState(EWorkingCopyState::Unknown) + , LockState(ELockState::Unknown) + , bUsingGitLfsLocking(InUsingLfsLocking) + , bNewerVersionOnServer(false) + , TimeStamp(0) + { + } + + /** ISourceControlState interface */ + virtual int32 GetHistorySize() const override; + virtual TSharedPtr GetHistoryItem(int32 HistoryIndex) const override; + virtual TSharedPtr FindHistoryRevision(int32 RevisionNumber) const override; + virtual TSharedPtr FindHistoryRevision(const FString& InRevision) const override; + virtual TSharedPtr GetBaseRevForMerge() const override; + virtual FName GetIconName() const override; + virtual FName GetSmallIconName() const override; + virtual FText GetDisplayName() const override; + virtual FText GetDisplayTooltip() const override; + virtual const FString& GetFilename() const override; + virtual const FDateTime& GetTimeStamp() const override; + virtual bool CanCheckIn() const override; + virtual bool CanCheckout() const override; + virtual bool IsCheckedOut() const override; + virtual bool IsCheckedOutOther(FString* Who = nullptr) const override; + virtual bool IsCheckedOutInOtherBranch(const FString& CurrentBranch = FString()) const /* UE4.20 override */ { return false; } + virtual bool IsModifiedInOtherBranch(const FString& CurrentBranch = FString()) const /* UE4.20 override */ { return false; } + virtual bool IsCheckedOutOrModifiedInOtherBranch(const FString& CurrentBranch = FString()) const /* UE4.20 override */ { return IsCheckedOutInOtherBranch(CurrentBranch) || IsModifiedInOtherBranch(CurrentBranch); } + virtual TArray GetCheckedOutBranches() const /* UE4.20 override */ { return TArray(); } + virtual FString GetOtherUserBranchCheckedOuts() const /* UE4.20 override */ { return FString(); } + virtual bool GetOtherBranchHeadModification(FString& HeadBranchOut, FString& ActionOut, int32& HeadChangeListOut) const /* UE4.20 override */ { return false; } + virtual bool IsCurrent() const override; + virtual bool IsSourceControlled() const override; + virtual bool IsAdded() const override; + virtual bool IsDeleted() const override; + virtual bool IsIgnored() const override; + virtual bool CanEdit() const override; + virtual bool CanDelete() const override; + virtual bool IsUnknown() const override; + virtual bool IsModified() const override; + virtual bool CanAdd() const override; + virtual bool IsConflicted() const override; + virtual bool CanRevert() const override; + +public: + /** History of the item, if any */ + TGitSourceControlHistory History; + + /** Filename on disk */ + FString LocalFilename; + + /** File Id with which our local revision diverged from the remote revision */ + FString PendingMergeBaseFileHash; + + /** State of the working copy */ + EWorkingCopyState::Type WorkingCopyState; + + /** Lock state */ + ELockState::Type LockState; + + /** Name of user who has locked the file */ + FString LockUser; + + /** Tells if using the Git LFS file Locking workflow */ + bool bUsingGitLfsLocking; + + /** Whether a newer version exists on the server */ + bool bNewerVersionOnServer; + + /** The timestamp of the last update */ + FDateTime TimeStamp; +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.cpp new file mode 100644 index 0000000..4201458 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.cpp @@ -0,0 +1,1556 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "GitSourceControlUtils.h" + +#include "GitSourceControlCommand.h" +#include "HAL/PlatformProcess.h" +#include "HAL/PlatformFilemanager.h" +#include "HAL/FileManager.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "ISourceControlModule.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlProvider.h" + +#if PLATFORM_LINUX +#include +#endif + + +namespace GitSourceControlConstants +{ + /** The maximum number of files we submit in a single Git command */ + const int32 MaxFilesPerBatch = 50; +} + +FGitScopedTempFile::FGitScopedTempFile(const FText& InText) +{ + Filename = FPaths::CreateTempFilename(*FPaths::ProjectLogDir(), TEXT("Git-Temp"), TEXT(".txt")); + if(!FFileHelper::SaveStringToFile(InText.ToString(), *Filename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogSourceControl, Error, TEXT("Failed to write to temp file: %s"), *Filename); + } +} + +FGitScopedTempFile::~FGitScopedTempFile() +{ + if(FPaths::FileExists(Filename)) + { + if(!FPlatformFileManager::Get().GetPlatformFile().DeleteFile(*Filename)) + { + UE_LOG(LogSourceControl, Error, TEXT("Failed to delete temp file: %s"), *Filename); + } + } +} + +const FString& FGitScopedTempFile::GetFilename() const +{ + return Filename; +} + + +namespace GitSourceControlUtils +{ + +// Launch the Git command line process and extract its results & errors +static bool RunCommandInternalRaw(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, FString& OutResults, FString& OutErrors, const int32 ExpectedReturnCode = 0) +{ + int32 ReturnCode = 0; + FString FullCommand; + FString LogableCommand; // short version of the command for logging purpose + + if(!InRepositoryRoot.IsEmpty()) + { + FString RepositoryRoot = InRepositoryRoot; + + // Detect a "migrate asset" scenario (a "git add" command is applied to files outside the current project) + if ( (InFiles.Num() > 0) && !FPaths::IsRelative(InFiles[0]) && !InFiles[0].StartsWith(InRepositoryRoot) ) + { + // in this case, find the git repository (if any) of the destination Project + FString DestinationRepositoryRoot; + if(FindRootDirectory(FPaths::GetPath(InFiles[0]), DestinationRepositoryRoot)) + { + RepositoryRoot = DestinationRepositoryRoot; // if found use it for the "add" command (else not, to avoid producing one more error in logs) + } + } + + // Specify the working copy (the root) of the git repository (before the command itself) + FullCommand = TEXT("-C \""); + FullCommand += RepositoryRoot; + FullCommand += TEXT("\" "); + } + // then the git command itself ("status", "log", "commit"...) + LogableCommand += InCommand; + + // Append to the command all parameters, and then finally the files + for(const auto& Parameter : InParameters) + { + LogableCommand += TEXT(" "); + LogableCommand += Parameter; + } + for(const auto& File : InFiles) + { + LogableCommand += TEXT(" \""); + LogableCommand += File; + LogableCommand += TEXT("\""); + } + // Also, Git does not have a "--non-interactive" option, as it auto-detects when there are no connected standard input/output streams + + FullCommand += LogableCommand; + + UE_LOG(LogSourceControl, Log, TEXT("RunCommand: 'git %s'"), *LogableCommand); + + FString PathToGitOrEnvBinary = InPathToGitBinary; +#if PLATFORM_MAC + // The Cocoa application does not inherit shell environment variables, so add the path expected to have git-lfs to PATH + FString PathEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); + FString GitInstallPath = FPaths::GetPath(InPathToGitBinary); + + TArray PathArray; + PathEnv.ParseIntoArray(PathArray, FPlatformMisc::GetPathVarDelimiter()); + bool bHasGitInstallPath = false; + for (auto Path : PathArray) + { + if (GitInstallPath.Equals(Path, ESearchCase::CaseSensitive)) + { + bHasGitInstallPath = true; + break; + } + } + + if (!bHasGitInstallPath) + { + PathToGitOrEnvBinary = FString("/usr/bin/env"); + FullCommand = FString::Printf(TEXT("PATH=\"%s%s%s\" \"%s\" %s"), *GitInstallPath, FPlatformMisc::GetPathVarDelimiter(), *PathEnv, *InPathToGitBinary, *FullCommand); + } +#endif + FPlatformProcess::ExecProcess(*PathToGitOrEnvBinary, *FullCommand, &ReturnCode, &OutResults, &OutErrors); + + // TODO: add a setting to easily enable Verbose logging + UE_LOG(LogSourceControl, Verbose, TEXT("RunCommand(%s):\n%s"), *InCommand, *OutResults); + if(ReturnCode != ExpectedReturnCode || OutErrors.Len() > 0) + { + UE_LOG(LogSourceControl, Warning, TEXT("RunCommand(%s) ReturnCode=%d:\n%s"), *InCommand, ReturnCode, *OutErrors); + } + + // Move push/pull progress information from the error stream to the info stream + if(ReturnCode == ExpectedReturnCode && OutErrors.Len() > 0) + { + OutResults.Append(OutErrors); + OutErrors.Empty(); + } + + return ReturnCode == ExpectedReturnCode; +} + +// Basic parsing or results & errors from the Git command line process +static bool RunCommandInternal(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) +{ + bool bResult; + FString Results; + FString Errors; + + bResult = RunCommandInternalRaw(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, Results, Errors); + Results.ParseIntoArray(OutResults, TEXT("\n"), true); + Errors.ParseIntoArray(OutErrorMessages, TEXT("\n"), true); + + return bResult; +} + +FString FindGitBinaryPath() +{ +#if PLATFORM_WINDOWS + // 1) First of all, look into standard install directories + // NOTE using only "git" (or "git.exe") relying on the "PATH" envvar does not always work as expected, depending on the installation: + // If the PATH is set with "git/cmd" instead of "git/bin", + // "git.exe" launch "git/cmd/git.exe" that redirect to "git/bin/git.exe" and ExecProcess() is unable to catch its outputs streams. + // First check the 64-bit program files directory: + FString GitBinaryPath(TEXT("C:/Program Files/Git/bin/git.exe")); + bool bFound = CheckGitAvailability(GitBinaryPath); + if(!bFound) + { + // otherwise check the 32-bit program files directory. + GitBinaryPath = TEXT("C:/Program Files (x86)/Git/bin/git.exe"); + bFound = CheckGitAvailability(GitBinaryPath); + } + if(!bFound) + { + // else the install dir for the current user: C:\Users\UserName\AppData\Local\Programs\Git\cmd + const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); + GitBinaryPath = FString::Printf(TEXT("%s/Programs/Git/cmd/git.exe"), *AppDataLocalPath); + bFound = CheckGitAvailability(GitBinaryPath); + } + + // 2) Else, look for the version of Git bundled with SmartGit "Installer with JRE" + if(!bFound) + { + GitBinaryPath = TEXT("C:/Program Files (x86)/SmartGit/git/bin/git.exe"); + bFound = CheckGitAvailability(GitBinaryPath); + if (!bFound) + { + // If git is not found in "git/bin/" subdirectory, try the "bin/" path that was in use before + GitBinaryPath = TEXT("C:/Program Files (x86)/SmartGit/bin/git.exe"); + bFound = CheckGitAvailability(GitBinaryPath); + } + } + + // 3) Else, look for the local_git provided by SourceTree + if(!bFound) + { + // C:\Users\UserName\AppData\Local\Atlassian\SourceTree\git_local\bin + const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); + GitBinaryPath = FString::Printf(TEXT("%s/Atlassian/SourceTree/git_local/bin/git.exe"), *AppDataLocalPath); + bFound = CheckGitAvailability(GitBinaryPath); + } + + // 4) Else, look for the PortableGit provided by GitHub Desktop + if(!bFound) + { + // The latest GitHub Desktop adds its binaries into the local appdata directory: + // C:\Users\UserName\AppData\Local\GitHub\PortableGit_c2ba306e536fdf878271f7fe636a147ff37326ad\cmd + const FString AppDataLocalPath = FPlatformMisc::GetEnvironmentVariable(TEXT("LOCALAPPDATA")); + const FString SearchPath = FString::Printf(TEXT("%s/GitHub/PortableGit_*"), *AppDataLocalPath); + TArray PortableGitFolders; + IFileManager::Get().FindFiles(PortableGitFolders, *SearchPath, false, true); + if(PortableGitFolders.Num() > 0) + { + // FindFiles just returns directory names, so we need to prepend the root path to get the full path. + GitBinaryPath = FString::Printf(TEXT("%s/GitHub/%s/cmd/git.exe"), *AppDataLocalPath, *(PortableGitFolders.Last())); // keep only the last PortableGit found + bFound = CheckGitAvailability(GitBinaryPath); + if (!bFound) + { + // If Portable git is not found in "cmd/" subdirectory, try the "bin/" path that was in use before + GitBinaryPath = FString::Printf(TEXT("%s/GitHub/%s/bin/git.exe"), *AppDataLocalPath, *(PortableGitFolders.Last())); // keep only the last PortableGit found + bFound = CheckGitAvailability(GitBinaryPath); + } + } + } + + // 5) Else, look for the version of Git bundled with Tower + if (!bFound) + { + GitBinaryPath = TEXT("C:/Program Files (x86)/fournova/Tower/vendor/Git/bin/git.exe"); + bFound = CheckGitAvailability(GitBinaryPath); + } + +#elif PLATFORM_MAC + // 1) First of all, look for the version of git provided by official git + FString GitBinaryPath = TEXT("/usr/local/git/bin/git"); + bool bFound = CheckGitAvailability(GitBinaryPath); + + // 2) Else, look for the version of git provided by Homebrew + if (!bFound) + { + GitBinaryPath = TEXT("/usr/local/bin/git"); + bFound = CheckGitAvailability(GitBinaryPath); + } + + // 3) Else, look for the version of git provided by MacPorts + if (!bFound) + { + GitBinaryPath = TEXT("/opt/local/bin/git"); + bFound = CheckGitAvailability(GitBinaryPath); + } + + // 4) Else, look for the version of git provided by Command Line Tools + if (!bFound) + { + GitBinaryPath = TEXT("/usr/bin/git"); + bFound = CheckGitAvailability(GitBinaryPath); + } + + { + SCOPED_AUTORELEASE_POOL; + NSWorkspace* SharedWorkspace = [NSWorkspace sharedWorkspace]; + + // 5) Else, look for the version of local_git provided by SmartGit + if (!bFound) + { + NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.syntevo.smartgit"]; + if (AppURL != nullptr) + { + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/git/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); + } + } + + // 6) Else, look for the version of local_git provided by SourceTree + if (!bFound) + { + NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.torusknot.SourceTreeNotMAS"]; + if (AppURL != nullptr) + { + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/git_local/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); + } + } + + // 7) Else, look for the version of local_git provided by GitHub Desktop + if (!bFound) + { + NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.github.GitHubClient"]; + if (AppURL != nullptr) + { + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/app/git/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); + } + } + + // 8) Else, look for the version of local_git provided by Tower2 + if (!bFound) + { + NSURL* AppURL = [SharedWorkspace URLForApplicationWithBundleIdentifier:@"com.fournova.Tower2"]; + if (AppURL != nullptr) + { + NSBundle* Bundle = [NSBundle bundleWithURL:AppURL]; + GitBinaryPath = FString::Printf(TEXT("%s/git/bin/git"), *FString([Bundle resourcePath])); + bFound = CheckGitAvailability(GitBinaryPath); + } + } + } + +#else + FString GitBinaryPath = TEXT("/usr/bin/git"); + bool bFound = CheckGitAvailability(GitBinaryPath); +#endif + + if(bFound) + { + FPaths::MakePlatformFilename(GitBinaryPath); + } + else + { + // If we did not find a path to Git, set it empty + GitBinaryPath.Empty(); + } + + return GitBinaryPath; +} + +bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion *OutVersion) +{ + FString InfoMessages; + FString ErrorMessages; + bool bGitAvailable = RunCommandInternalRaw(TEXT("version"), InPathToGitBinary, FString(), TArray(), TArray(), InfoMessages, ErrorMessages); + if(bGitAvailable) + { + if(!InfoMessages.Contains("git")) + { + bGitAvailable = false; + } + else if(OutVersion) + { + ParseGitVersion(InfoMessages, OutVersion); + FindGitCapabilities(InPathToGitBinary, OutVersion); + FindGitLfsCapabilities(InPathToGitBinary, OutVersion); + } + } + + return bGitAvailable; +} + +void ParseGitVersion(const FString& InVersionString, FGitVersion *OutVersion) +{ + // Parse "git version 2.11.0.windows.3" into the string tokens "git", "version", "2.11.0.windows.3" + TArray TokenizedString; + InVersionString.ParseIntoArrayWS(TokenizedString); + + // Select the string token containing the version "2.11.0.windows.3" + const FString* TokenVersionStringPtr = TokenizedString.FindByPredicate([](FString& s) { return TChar::IsDigit(s[0]); }); + if(TokenVersionStringPtr) + { + // Parse the version into its numerical components + TArray ParsedVersionString; + TokenVersionStringPtr->ParseIntoArray(ParsedVersionString, TEXT(".")); + if(ParsedVersionString.Num() >= 3) + { + if(ParsedVersionString[0].IsNumeric() && ParsedVersionString[1].IsNumeric() && ParsedVersionString[2].IsNumeric()) + { + OutVersion->Major = FCString::Atoi(*ParsedVersionString[0]); + OutVersion->Minor = FCString::Atoi(*ParsedVersionString[1]); + OutVersion->Patch = FCString::Atoi(*ParsedVersionString[2]); + if(ParsedVersionString.Num() >= 5) + { + if((ParsedVersionString[3] == TEXT("windows")) && ParsedVersionString[4].IsNumeric()) + { + OutVersion->Windows = FCString::Atoi(*ParsedVersionString[4]); + } + } + UE_LOG(LogSourceControl, Log, TEXT("Git version %d.%d.%d(%d)"), OutVersion->Major, OutVersion->Minor, OutVersion->Patch, OutVersion->Windows); + } + } + } +} + +void FindGitCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion) +{ + FString InfoMessages; + FString ErrorMessages; + RunCommandInternalRaw(TEXT("cat-file -h"), InPathToGitBinary, FString(), TArray(), TArray(), InfoMessages, ErrorMessages, 129); + if (InfoMessages.Contains("--filters")) + { + OutVersion->bHasCatFileWithFilters = true; + } +} + +void FindGitLfsCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion) +{ + FString InfoMessages; + FString ErrorMessages; + bool bGitLfsAvailable = RunCommandInternalRaw(TEXT("lfs version"), InPathToGitBinary, FString(), TArray(), TArray(), InfoMessages, ErrorMessages); + if(bGitLfsAvailable) + { + OutVersion->bHasGitLfs = true; + + if(InfoMessages.Compare(TEXT("git-lfs/2.0.0")) >= 0) + { + OutVersion->bHasGitLfsLocking = true; // Git LFS File Locking workflow introduced in "git-lfs/2.0.0" + } + UE_LOG(LogSourceControl, Log, TEXT("%s"), *InfoMessages); + } +} + +// Find the root of the Git repository, looking from the provided path and upward in its parent directories. +bool FindRootDirectory(const FString& InPath, FString& OutRepositoryRoot) +{ + bool bFound = false; + FString PathToGitSubdirectory; + OutRepositoryRoot = InPath; + + auto TrimTrailing = [](FString& Str, const TCHAR Char) + { + int32 Len = Str.Len(); + while(Len && Str[Len - 1] == Char) + { + Str = Str.LeftChop(1); + Len = Str.Len(); + } + }; + + TrimTrailing(OutRepositoryRoot, '\\'); + TrimTrailing(OutRepositoryRoot, '/'); + + while(!bFound && !OutRepositoryRoot.IsEmpty()) + { + // Look for the ".git" subdirectory (or file) present at the root of every Git repository + PathToGitSubdirectory = OutRepositoryRoot / TEXT(".git"); + bFound = IFileManager::Get().DirectoryExists(*PathToGitSubdirectory) || IFileManager::Get().FileExists(*PathToGitSubdirectory); + if(!bFound) + { + int32 LastSlashIndex; + if(OutRepositoryRoot.FindLastChar('/', LastSlashIndex)) + { + OutRepositoryRoot = OutRepositoryRoot.Left(LastSlashIndex); + } + else + { + OutRepositoryRoot.Empty(); + } + } + } + if(!bFound) + { + OutRepositoryRoot = InPath; // If not found, return the provided dir as best possible root. + } + return bFound; +} + +void GetUserConfig(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutUserName, FString& OutUserEmail) +{ + bool bResults; + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("user.name")); + bResults = RunCommandInternal(TEXT("config"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if(bResults && InfoMessages.Num() > 0) + { + OutUserName = InfoMessages[0]; + } + + Parameters.Reset(); + Parameters.Add(TEXT("user.email")); + InfoMessages.Reset(); + bResults &= RunCommandInternal(TEXT("config"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if(bResults && InfoMessages.Num() > 0) + { + OutUserEmail = InfoMessages[0]; + } +} + +bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName) +{ + bool bResults; + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("--short")); + Parameters.Add(TEXT("--quiet")); // no error message while in detached HEAD + Parameters.Add(TEXT("HEAD")); + bResults = RunCommandInternal(TEXT("symbolic-ref"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if(bResults && InfoMessages.Num() > 0) + { + OutBranchName = InfoMessages[0]; + } + else + { + Parameters.Reset(); + Parameters.Add(TEXT("-1")); + Parameters.Add(TEXT("--format=\"%h\"")); // no error message while in detached HEAD + bResults = RunCommandInternal(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if(bResults && InfoMessages.Num() > 0) + { + OutBranchName = "HEAD detached at "; + OutBranchName += InfoMessages[0]; + } + else + { + bResults = false; + } + } + + return bResults; +} + +bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary) +{ + bool bResults; + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("-1")); + Parameters.Add(TEXT("--format=\"%H %s\"")); + bResults = RunCommandInternal(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if(bResults && InfoMessages.Num() > 0) + { + OutCommitId = InfoMessages[0].Left(40); + OutCommitSummary = InfoMessages[0].RightChop(41); + } + + return bResults; +} + +bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl) +{ + TArray InfoMessages; + TArray ErrorMessages; + TArray Parameters; + Parameters.Add(TEXT("get-url")); + Parameters.Add(TEXT("origin")); + const bool bResults = RunCommandInternal(TEXT("remote"), InPathToGitBinary, InRepositoryRoot, Parameters, TArray(), InfoMessages, ErrorMessages); + if (bResults && InfoMessages.Num() > 0) + { + OutRemoteUrl = InfoMessages[0]; + } + + return bResults; +} + +bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) +{ + bool bResult = true; + + if(InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) + { + // Batch files up so we dont exceed command-line limits + int32 FileCount = 0; + while(FileCount < InFiles.Num()) + { + TArray FilesInBatch; + for(int32 FileIndex = 0; FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + + TArray BatchResults; + TArray BatchErrors; + bResult &= RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, FilesInBatch, BatchResults, BatchErrors); + OutResults += BatchResults; + OutErrorMessages += BatchErrors; + } + } + else + { + bResult &= RunCommandInternal(InCommand, InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, OutResults, OutErrorMessages); + } + + return bResult; +} + +// Run a Git "commit" command by batches +bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages) +{ + bool bResult = true; + + if(InFiles.Num() > GitSourceControlConstants::MaxFilesPerBatch) + { + // Batch files up so we dont exceed command-line limits + int32 FileCount = 0; + { + TArray FilesInBatch; + for(int32 FileIndex = 0; FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + // First batch is a simple "git commit" command with only the first files + bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, FilesInBatch, OutResults, OutErrorMessages); + } + + TArray Parameters; + for(const auto& Parameter : InParameters) + { + Parameters.Add(Parameter); + } + Parameters.Add(TEXT("--amend")); + + while(FileCount < InFiles.Num()) + { + TArray FilesInBatch; + for(int32 FileIndex = 0; FileCount < InFiles.Num() && FileIndex < GitSourceControlConstants::MaxFilesPerBatch; FileIndex++, FileCount++) + { + FilesInBatch.Add(InFiles[FileCount]); + } + // Next batches "amend" the commit with some more files + TArray BatchResults; + TArray BatchErrors; + bResult &= RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, Parameters, FilesInBatch, BatchResults, BatchErrors); + OutResults += BatchResults; + OutErrorMessages += BatchErrors; + } + } + else + { + bResult = RunCommandInternal(TEXT("commit"), InPathToGitBinary, InRepositoryRoot, InParameters, InFiles, OutResults, OutErrorMessages); + } + + return bResult; +} + +/** + * Parse informations on a file locked with Git LFS + * + * Example output of "git lfs locks" +Content\ThirdPersonBP\Blueprints\ThirdPersonCharacter.uasset SRombauts ID:891 +Content\ThirdPersonBP\Blueprints\ThirdPersonGameMode.uasset SRombauts ID:896 + */ +class FGitLfsLocksParser +{ +public: + FGitLfsLocksParser(const FString& InRepositoryRoot, const FString& InStatus, const bool bAbsolutePaths = true) + { + TArray Informations; + InStatus.ParseIntoArray(Informations, TEXT("\t"), true); + if(Informations.Num() >= 3) + { + Informations[0].TrimEndInline(); // Trim whitespace from the end of the filename + Informations[1].TrimEndInline(); // Trim whitespace from the end of the username + if (bAbsolutePaths) + LocalFilename = FPaths::ConvertRelativePathToFull(InRepositoryRoot, Informations[0]); + else + LocalFilename = Informations[0]; + LockUser = MoveTemp(Informations[1]); + } + } + + FString LocalFilename; ///< Filename on disk + FString LockUser; ///< Name of user who has file locked +}; + +/** + * @brief Extract the relative filename from a Git status result. + * + * Examples of status results: +M Content/Textures/T_Perlin_Noise_M.uasset +R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset +?? Content/Materials/M_Basic_Wall.uasset +!! BasicCode.sln + * + * @param[in] InResult One line of status + * @return Relative filename extracted from the line of status + * + * @see FGitStatusFileMatcher and StateFromGitStatus() + */ +static FString FilenameFromGitStatus(const FString& InResult) +{ + int32 RenameIndex; + if(InResult.FindLastChar('>', RenameIndex)) + { + // Extract only the second part of a rename "from -> to" + return InResult.RightChop(RenameIndex + 2); + } + else + { + // Extract the relative filename from the Git status result (after the 2 letters status and 1 space) + return InResult.RightChop(3); + } +} + +/** Match the relative filename of a Git status result with a provided absolute filename */ +class FGitStatusFileMatcher +{ +public: + FGitStatusFileMatcher(const FString& InAbsoluteFilename) + : AbsoluteFilename(InAbsoluteFilename) + { + } + + bool operator()(const FString& InResult) const + { + return AbsoluteFilename.Contains(FilenameFromGitStatus(InResult)); + } + +private: + const FString& AbsoluteFilename; +}; + +/** + * Extract and interpret the file state from the given Git status result. + * @see http://git-scm.com/docs/git-status + * ' ' = unmodified + * 'M' = modified + * 'A' = added + * 'D' = deleted + * 'R' = renamed + * 'C' = copied + * 'U' = updated but unmerged + * '?' = unknown/untracked + * '!' = ignored +*/ +class FGitStatusParser +{ +public: + FGitStatusParser(const FString& InResult) + { + TCHAR IndexState = InResult[0]; + TCHAR WCopyState = InResult[1]; + if( (IndexState == 'U' || WCopyState == 'U') + || (IndexState == 'A' && WCopyState == 'A') + || (IndexState == 'D' && WCopyState == 'D')) + { + // "Unmerged" conflict cases are generally marked with a "U", + // but there are also the special cases of both "A"dded, or both "D"eleted + State = EWorkingCopyState::Conflicted; + } + else if(IndexState == 'A') + { + State = EWorkingCopyState::Added; + } + else if(IndexState == 'D') + { + State = EWorkingCopyState::Deleted; + } + else if(WCopyState == 'D') + { + State = EWorkingCopyState::Missing; + } + else if(IndexState == 'M' || WCopyState == 'M') + { + State = EWorkingCopyState::Modified; + } + else if(IndexState == 'R') + { + State = EWorkingCopyState::Renamed; + } + else if(IndexState == 'C') + { + State = EWorkingCopyState::Copied; + } + else if(IndexState == '?' || WCopyState == '?') + { + State = EWorkingCopyState::NotControlled; + } + else if(IndexState == '!' || WCopyState == '!') + { + State = EWorkingCopyState::Ignored; + } + else + { + // Unmodified never yield a status + State = EWorkingCopyState::Unknown; + } + } + + EWorkingCopyState::Type State; +}; + +/** + * Extract the status of a unmerged (conflict) file + * + * Example output of git ls-files --unmerged Content/Blueprints/BP_Test.uasset +100644 d9b33098273547b57c0af314136f35b494e16dcb 1 Content/Blueprints/BP_Test.uasset +100644 a14347dc3b589b78fb19ba62a7e3982f343718bc 2 Content/Blueprints/BP_Test.uasset +100644 f3137a7167c840847cd7bd2bf07eefbfb2d9bcd2 3 Content/Blueprints/BP_Test.uasset + * + * 1: The "common ancestor" of the file (the version of the file that both the current and other branch originated from). + * 2: The version from the current branch (the master branch in this case). + * 3: The version from the other branch (the test branch) +*/ +class FGitConflictStatusParser +{ +public: + /** Parse the unmerge status: extract the base SHA1 identifier of the file */ + FGitConflictStatusParser(const TArray& InResults) + { + const FString& FirstResult = InResults[0]; // 1: The common ancestor of merged branches + CommonAncestorFileId = FirstResult.Mid(7, 40); + } + + FString CommonAncestorFileId; ///< SHA1 Id of the file (warning: not the commit Id) +}; + +/** Execute a command to get the details of a conflict */ +static void RunGetConflictStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, FGitSourceControlState& InOutFileState) +{ + TArray ErrorMessages; + TArray Results; + TArray Files; + Files.Add(InFile); + TArray Parameters; + Parameters.Add(TEXT("--unmerged")); + bool bResult = RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, ErrorMessages); + if(bResult && Results.Num() == 3) + { + // Parse the unmerge status: extract the base revision (or the other branch?) + FGitConflictStatusParser ConflictStatus(Results); + InOutFileState.PendingMergeBaseFileHash = ConflictStatus.CommonAncestorFileId; + } +} + +/// Convert filename relative to the repository root to absolute path (inplace) +void AbsoluteFilenames(const FString& InRepositoryRoot, TArray& InFileNames) +{ + for(auto& FileName : InFileNames) + { + FileName = FPaths::ConvertRelativePathToFull(InRepositoryRoot, FileName); + } +} + +/** Run a 'git ls-files' command to get all files tracked by Git recursively in a directory. + * + * Called in case of a "directory status" (no file listed in the command) when using the "Submit to Source Control" menu. +*/ +static bool ListFilesInDirectoryRecurse(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InDirectory, TArray& OutFiles) +{ + TArray ErrorMessages; + TArray Directory; + Directory.Add(InDirectory); + const bool bResult = RunCommandInternal(TEXT("ls-files"), InPathToGitBinary, InRepositoryRoot, TArray(), Directory, OutFiles, ErrorMessages); + AbsoluteFilenames(InRepositoryRoot, OutFiles); + return bResult; +} + +/** Parse the array of strings results of a 'git status' command for a provided list of files all in a common directory + * + * Called in case of a normal refresh of status on a list of assets in a the Content Browser (or user selected "Refresh" context menu). + * + * Example git status results: +M Content/Textures/T_Perlin_Noise_M.uasset +R Content/Textures/T_Perlin_Noise_M.uasset -> Content/Textures/T_Perlin_Noise_M2.uasset +?? Content/Materials/M_Basic_Wall.uasset +!! BasicCode.sln +*/ +static void ParseFileStatusResult(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, const TMap& InLockedFiles, const TArray& InResults, TArray& OutStates) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FString LfsUserName = GitSourceControl.AccessSettings().GetLfsUserName(); + const FDateTime Now = FDateTime::Now(); + + // Iterate on all files explicitly listed in the command + for(const auto& File : InFiles) + { + FGitSourceControlState FileState(File, InUsingLfsLocking); + // Search the file in the list of status + int32 IdxResult = InResults.IndexOfByPredicate(FGitStatusFileMatcher(File)); + if(IdxResult != INDEX_NONE) + { + // File found in status results; only the case for "changed" files + FGitStatusParser StatusParser(InResults[IdxResult]); + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) = '%s' => %d"), *File, *InResults[IdxResult], static_cast(StatusParser.State)); + + FileState.WorkingCopyState = StatusParser.State; + if(FileState.IsConflicted()) + { + // In case of a conflict (unmerged file) get the base revision to merge + RunGetConflictStatus(InPathToGitBinary, InRepositoryRoot, File, FileState); + } + } + else + { + // File not found in status + if(FPaths::FileExists(File)) + { + // usually means the file is unchanged, + FileState.WorkingCopyState = EWorkingCopyState::Unchanged; + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found but exists => unchanged"), *File); + } + else + { + // but also the case for newly created content: there is no file on disk until the content is saved for the first time + FileState.WorkingCopyState = EWorkingCopyState::NotControlled; + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) not found and does not exists => new/not controled"), *File); + } + } + if(InLockedFiles.Contains(File)) + { + FileState.LockUser = InLockedFiles[File]; + if(LfsUserName == FileState.LockUser) + { + FileState.LockState = ELockState::Locked; + } + else + { + FileState.LockState = ELockState::LockedOther; + } + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Locked by '%s'"), *File, *FileState.LockUser); + } + else + { + FileState.LockState = ELockState::NotLocked; + // TODO LFS Debug log + if (InUsingLfsLocking) + { + UE_LOG(LogSourceControl, Log, TEXT("Status(%s) Not Locked"), *File); + } + } + FileState.TimeStamp = Now; + OutStates.Add(FileState); + } +} + +/** Parse the array of strings results of a 'git status' command for a directory + * + * Called in case of a "directory status" (no file listed in the command) ONLY to detect Deleted/Missing/Untracked files + * since those files are not listed by the 'git ls-files' command. + * + * @see #ParseFileStatusResult() above for an example of a 'git status' results +*/ +static void ParseDirectoryStatusResult(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InResults, TArray& OutStates) +{ + // Iterate on each line of result of the status command + for(const FString& Result : InResults) + { + const FString RelativeFilename = FilenameFromGitStatus(Result); + const FString File = FPaths::ConvertRelativePathToFull(InRepositoryRoot, RelativeFilename); + + FGitSourceControlState FileState(File, InUsingLfsLocking); + FGitStatusParser StatusParser(Result); + if((EWorkingCopyState::Deleted == StatusParser.State) || (EWorkingCopyState::Missing == StatusParser.State) || (EWorkingCopyState::NotControlled == StatusParser.State)) + { + FileState.WorkingCopyState = StatusParser.State; + FileState.TimeStamp.Now(); + OutStates.Add(MoveTemp(FileState)); + } + } +} + +/** + * @brief Detects how to parse the result of a "status" command to get workspace file states + * + * It is either a command for a whole directory (ie. "Content/", in case of "Submit to Source Control" menu), + * or for one or more files all on a same directory (by design, since we group files by directory in RunUpdateStatus()) + * + * @param[in] InPathToGitBinary The path to the Git binary + * @param[in] InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param[in] InUsingLfsLocking Tells if using the Git LFS file Locking workflow + * @param[in] InFiles List of files in a directory, or the path to the directory itself (never empty). + * @param[out] InResults Results from the "status" command + * @param[out] OutStates States of files for witch the status has been gathered (distinct than InFiles in case of a "directory status") + */ +static void ParseStatusResults(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, const TMap& InLockedFiles, const TArray& InResults, TArray& OutStates) +{ + if((InFiles.Num() == 1) && FPaths::DirectoryExists(InFiles[0])) + { + // 1) Special case for "status" of a directory: requires to get the list of files by ourselves. + // (this is triggered by the "Submit to Source Control" menu) + // TODO LFS Debug Log + UE_LOG(LogSourceControl, Log, TEXT("ParseStatusResults: 1) Special case for status of a directory (%s)"), *InFiles[0]); + TArray Files; + const FString& Directory = InFiles[0]; + const bool bResult = ListFilesInDirectoryRecurse(InPathToGitBinary, InRepositoryRoot, Directory, Files); + if(bResult) + { + ParseFileStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, Files, InLockedFiles, InResults, OutStates); + } + // The above cannot detect deleted assets since there is no file left to enumerate (either by the Content Browser or by git ls-files) + // => so we also parse the status results to explicitly look for Deleted/Missing assets + ParseDirectoryStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, InResults, OutStates); + } + else + { + // 2) General case for one or more files in the same directory. + // TODO LFS Debug Log + UE_LOG(LogSourceControl, Log, TEXT("ParseStatusResults: 2) General case for one or more files (%s, ...)"), *InFiles[0]); + ParseFileStatusResult(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, InFiles, InLockedFiles, InResults, OutStates); + } +} + +bool GetAllLocks(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool bAbsolutePaths, TArray& OutErrorMessages, TMap& OutLocks) +{ + TArray Results; + TArray ErrorMessages; + const bool bResult = RunCommand(TEXT("lfs locks"), InPathToGitBinary, InRepositoryRoot, TArray(), TArray(), Results, ErrorMessages); + for(const FString& Result : Results) + { + FGitLfsLocksParser LockFile(InRepositoryRoot, Result, bAbsolutePaths); + // TODO LFS Debug log + UE_LOG(LogSourceControl, Log, TEXT("LockedFile(%s, %s)"), *LockFile.LocalFilename, *LockFile.LockUser); + OutLocks.Add(MoveTemp(LockFile.LocalFilename), MoveTemp(LockFile.LockUser)); + } + + return bResult; +} + +// Run a batch of Git "status" command to update status of given files and/or directories. +bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, TArray& OutErrorMessages, TArray& OutStates) +{ + bool bResults = true; + TMap LockedFiles; + + // 0) Issue a "git lfs locks" command at the root of the repository + if(InUsingLfsLocking) + { + TArray ErrorMessages; + GetAllLocks(InPathToGitBinary, InRepositoryRoot, true, ErrorMessages, LockedFiles); + } + + // Git status does not show any "untracked files" when called with files from different subdirectories! (issue #3) + // 1) So here we group files by path (ie. by subdirectory) + TMap> GroupOfFiles; + for(const auto& File : InFiles) + { + const FString Path = FPaths::GetPath(*File); + TArray* Group = GroupOfFiles.Find(Path); + if(Group != nullptr) + { + Group->Add(File); + } + else + { + TArray NewGroup; + NewGroup.Add(File); + GroupOfFiles.Add(Path, NewGroup); + } + } + + // Get the current branch name, since we need origin of current branch + FString BranchName; + GitSourceControlUtils::GetBranchName(InPathToGitBinary, InRepositoryRoot, BranchName); + + TArray Parameters; + Parameters.Add(TEXT("--porcelain")); + Parameters.Add(TEXT("--ignored")); + + // 2) then we can batch git status operation by subdirectory + for(const auto& Files : GroupOfFiles) + { + // "git status" can only detect renamed and deleted files when it operate on a folder, so use one folder path for all files in a directory + const FString Path = FPaths::GetPath(*Files.Value[0]); + TArray OnePath; + // Only one file: optim very useful for the .uproject file at the root to avoid parsing the whole repository + // (works only if the file exists) + if((Files.Value.Num() == 1) && (FPaths::FileExists(Files.Value[0]))) + { + OnePath.Add(Files.Value[0]); + } + else + { + OnePath.Add(Path); + } + { + TArray Results; + TArray ErrorMessages; + const bool bResult = RunCommand(TEXT("status"), InPathToGitBinary, InRepositoryRoot, Parameters, OnePath, Results, ErrorMessages); + OutErrorMessages.Append(ErrorMessages); + if(bResult) + { + ParseStatusResults(InPathToGitBinary, InRepositoryRoot, InUsingLfsLocking, Files.Value, LockedFiles, Results, OutStates); + } + } + + if (!BranchName.IsEmpty()) + { + // Using git diff, we can obtain a list of files that were modified between our current origin and HEAD. Assumes that fetch has been run to get accurate info. + // TODO: should do a fetch (at least periodically). + TArray Results; + TArray ErrorMessages; + TArray ParametersLsRemote; + ParametersLsRemote.Add(TEXT("origin")); + ParametersLsRemote.Add(BranchName); + const bool bResultLsRemote = RunCommand(TEXT("ls-remote"), InPathToGitBinary, InRepositoryRoot, ParametersLsRemote, OnePath, Results, ErrorMessages); + // If the command is successful and there is only 1 line on the output the branch exists on remote + const bool bDiffAgainstRemote = bResultLsRemote && Results.Num(); + + Results.Reset(); + ErrorMessages.Reset(); + TArray ParametersLog; + ParametersLog.Add(TEXT("--pretty=")); // this omits the commit lines, just gets us files + ParametersLog.Add(TEXT("--name-only")); + ParametersLog.Add(bDiffAgainstRemote ? TEXT("HEAD..HEAD@{upstream}") : BranchName); + const bool bResultDiff = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, ParametersLog, OnePath, Results, ErrorMessages); + OutErrorMessages.Append(ErrorMessages); + if (bResultDiff) + { + for (const FString& NewerFileName : Results) + { + const FString NewerFilePath = FPaths::ConvertRelativePathToFull(InRepositoryRoot, NewerFileName); + + // Find existing corresponding file state to update it (not found would mean new file or not in the current path) + if (FGitSourceControlState* FileStatePtr = OutStates.FindByPredicate([NewerFilePath](FGitSourceControlState& FileState) { return FileState.LocalFilename == NewerFilePath; })) + { + FileStatePtr->bNewerVersionOnServer = true; + } + } + } + } + } + + return bResults; +} + +// Run a Git `cat-file --filters` command to dump the binary content of a revision into a file. +bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName) +{ + int32 ReturnCode = -1; + FString FullCommand; + + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FGitVersion& GitVersion = GitSourceControl.GetProvider().GetGitVersion(); + + if(!InRepositoryRoot.IsEmpty()) + { + // Specify the working copy (the root) of the git repository (before the command itself) + FullCommand = TEXT("-C \""); + FullCommand += InRepositoryRoot; + FullCommand += TEXT("\" "); + } + + // then the git command itself + if(GitVersion.bHasCatFileWithFilters) + { + // Newer versions (2.9.3.windows.2) support smudge/clean filters used by Git LFS, git-fat, git-annex, etc + FullCommand += TEXT("cat-file --filters "); + } + else + { + // Previous versions fall-back on "git show" like before + FullCommand += TEXT("show "); + } + + // Append to the command the parameter + FullCommand += InParameter; + + const bool bLaunchDetached = false; + const bool bLaunchHidden = true; + const bool bLaunchReallyHidden = bLaunchHidden; + + void* PipeRead = nullptr; + void* PipeWrite = nullptr; + + verify(FPlatformProcess::CreatePipe(PipeRead, PipeWrite)); + + UE_LOG(LogSourceControl, Log, TEXT("RunDumpToFile: 'git %s'"), *FullCommand); + + FString PathToGitOrEnvBinary = InPathToGitBinary; + #if PLATFORM_MAC + // The Cocoa application does not inherit shell environment variables, so add the path expected to have git-lfs to PATH + FString PathEnv = FPlatformMisc::GetEnvironmentVariable(TEXT("PATH")); + FString GitInstallPath = FPaths::GetPath(InPathToGitBinary); + + TArray PathArray; + PathEnv.ParseIntoArray(PathArray, FPlatformMisc::GetPathVarDelimiter()); + bool bHasGitInstallPath = false; + for (auto Path : PathArray) + { + if (GitInstallPath.Equals(Path, ESearchCase::CaseSensitive)) + { + bHasGitInstallPath = true; + break; + } + } + + if (!bHasGitInstallPath) + { + PathToGitOrEnvBinary = FString("/usr/bin/env"); + FullCommand = FString::Printf(TEXT("PATH=\"%s%s%s\" \"%s\" %s"), *GitInstallPath, FPlatformMisc::GetPathVarDelimiter(), *PathEnv, *InPathToGitBinary, *FullCommand); + } + #endif + + FProcHandle ProcessHandle = FPlatformProcess::CreateProc(*PathToGitOrEnvBinary, *FullCommand, bLaunchDetached, bLaunchHidden, bLaunchReallyHidden, nullptr, 0, *InRepositoryRoot, PipeWrite); + if(ProcessHandle.IsValid()) + { + FPlatformProcess::Sleep(0.01); + + TArray BinaryFileContent; + while(FPlatformProcess::IsProcRunning(ProcessHandle)) + { + TArray BinaryData; + FPlatformProcess::ReadPipeToArray(PipeRead, BinaryData); + if(BinaryData.Num() > 0) + { + BinaryFileContent.Append(MoveTemp(BinaryData)); + } + } + TArray BinaryData; + FPlatformProcess::ReadPipeToArray(PipeRead, BinaryData); + if(BinaryData.Num() > 0) + { + BinaryFileContent.Append(MoveTemp(BinaryData)); + } + + FPlatformProcess::GetProcReturnCode(ProcessHandle, &ReturnCode); + if(ReturnCode == 0) + { + // Save buffer into temp file + if(FFileHelper::SaveArrayToFile(BinaryFileContent, *InDumpFileName)) + { + UE_LOG(LogSourceControl, Log, TEXT("Writed '%s' (%do)"), *InDumpFileName, BinaryFileContent.Num()); + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("Could not write %s"), *InDumpFileName); + ReturnCode = -1; + } + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("DumpToFile: ReturnCode=%d"), ReturnCode); + } + + FPlatformProcess::CloseProc(ProcessHandle); + } + else + { + UE_LOG(LogSourceControl, Error, TEXT("Failed to launch 'git cat-file'")); + } + + FPlatformProcess::ClosePipe(PipeRead, PipeWrite); + + return (ReturnCode == 0); +} + +/** + * Translate file actions from the given Git log --name-status command to keywords used by the Editor UI. + * + * @see https://www.kernel.org/pub/software/scm/git/docs/git-log.html + * ' ' = unmodified + * 'M' = modified + * 'A' = added + * 'D' = deleted + * 'R' = renamed + * 'C' = copied + * 'T' = type changed + * 'U' = updated but unmerged + * 'X' = unknown + * 'B' = broken pairing + * + * @see SHistoryRevisionListRowContent::GenerateWidgetForColumn(): "add", "edit", "delete", "branch" and "integrate" (everything else is taken like "edit") +*/ +static FString LogStatusToString(TCHAR InStatus) +{ + switch(InStatus) + { + case TEXT(' '): + return FString("unmodified"); + case TEXT('M'): + return FString("modified"); + case TEXT('A'): // added: keyword "add" to display a specific icon instead of the default "edit" action one + return FString("add"); + case TEXT('D'): // deleted: keyword "delete" to display a specific icon instead of the default "edit" action one + return FString("delete"); + case TEXT('R'): // renamed keyword "branch" to display a specific icon instead of the default "edit" action one + return FString("branch"); + case TEXT('C'): // copied keyword "branch" to display a specific icon instead of the default "edit" action one + return FString("branch"); + case TEXT('T'): + return FString("type changed"); + case TEXT('U'): + return FString("unmerged"); + case TEXT('X'): + return FString("unknown"); + case TEXT('B'): + return FString("broked pairing"); + } + + return FString(); +} + +/** + * Parse the array of strings results of a 'git log' command + * + * Example git log results: +commit 97a4e7626681895e073aaefd68b8ac087db81b0b +Author: Sébastien Rombauts +Date: 2014-2015-05-15 21:32:27 +0200 + + Another commit used to test History + + - with many lines + - some + - and strange characteres $*+ + +M Content/Blueprints/Blueprint_CeilingLight.uasset +R100 Content/Textures/T_Concrete_Poured_D.uasset Content/Textures/T_Concrete_Poured_D2.uasset + +commit 355f0df26ebd3888adbb558fd42bb8bd3e565000 +Author: Sébastien Rombauts +Date: 2014-2015-05-12 11:28:14 +0200 + + Testing git status, edit, and revert + +A Content/Blueprints/Blueprint_CeilingLight.uasset +C099 Content/Textures/T_Concrete_Poured_N.uasset Content/Textures/T_Concrete_Poured_N2.uasset +*/ +static void ParseLogResults(const TArray& InResults, TGitSourceControlHistory& OutHistory) +{ + TSharedRef SourceControlRevision = MakeShareable(new FGitSourceControlRevision); + for(const auto& Result : InResults) + { + if(Result.StartsWith(TEXT("commit "))) // Start of a new commit + { + // End of the previous commit + if(SourceControlRevision->RevisionNumber != 0) + { + OutHistory.Add(MoveTemp(SourceControlRevision)); + + SourceControlRevision = MakeShareable(new FGitSourceControlRevision); + } + SourceControlRevision->CommitId = Result.RightChop(7); // Full commit SHA1 hexadecimal string + SourceControlRevision->ShortCommitId = SourceControlRevision->CommitId.Left(8); // Short revision ; first 8 hex characters (max that can hold a 32 bit integer) + SourceControlRevision->CommitIdNumber = FParse::HexNumber(*SourceControlRevision->ShortCommitId); + SourceControlRevision->RevisionNumber = -1; // RevisionNumber will be set at the end, based off the index in the History + } + else if(Result.StartsWith(TEXT("Author: "))) // Author name & email + { + // Remove the 'email' part of the UserName + FString UserNameEmail = Result.RightChop(8); + int32 EmailIndex = 0; + if(UserNameEmail.FindLastChar('<', EmailIndex)) + { + SourceControlRevision->UserName = UserNameEmail.Left(EmailIndex - 1); + } + } + else if(Result.StartsWith(TEXT("Date: "))) // Commit date + { + FString Date = Result.RightChop(8); + SourceControlRevision->Date = FDateTime::FromUnixTimestamp(FCString::Atoi(*Date)); + } + // else if(Result.IsEmpty()) // empty line before/after commit message has already been taken care by FString::ParseIntoArray() + else if(Result.StartsWith(TEXT(" "))) // Multi-lines commit message + { + SourceControlRevision->Description += Result.RightChop(4); + SourceControlRevision->Description += TEXT("\n"); + } + else // Name of the file, starting with an uppercase status letter ("A"/"M"...) + { + const TCHAR Status = Result[0]; + SourceControlRevision->Action = LogStatusToString(Status); // Readable action string ("Added", Modified"...) instead of "A"/"M"... + // Take care of special case for Renamed/Copied file: extract the second filename after second tabulation + int32 IdxTab; + if(Result.FindLastChar('\t', IdxTab)) + { + SourceControlRevision->Filename = Result.RightChop(IdxTab + 1); // relative filename + } + } + } + // End of the last commit + if(SourceControlRevision->RevisionNumber != 0) + { + OutHistory.Add(MoveTemp(SourceControlRevision)); + } + + // Then set the revision number of each Revision based on its index (reverse order since the log starts with the most recent change) + for(int32 RevisionIndex = 0; RevisionIndex < OutHistory.Num(); RevisionIndex++) + { + const auto& SourceControlRevisionItem = OutHistory[RevisionIndex]; + SourceControlRevisionItem->RevisionNumber = OutHistory.Num() - RevisionIndex; + + // Special case of a move ("branch" in Perforce term): point to the previous change (so the next one in the order of the log) + if((SourceControlRevisionItem->Action == "branch") && (RevisionIndex < OutHistory.Num() - 1)) + { + SourceControlRevisionItem->BranchSource = OutHistory[RevisionIndex + 1]; + } + } +} + +/** + * Extract the SHA1 identifier and size of a blob (file) from a Git "ls-tree" command. + * + * Example output for the command git ls-tree --long 7fdaeb2 Content/Blueprints/BP_Test.uasset +100644 blob a14347dc3b589b78fb19ba62a7e3982f343718bc 70731 Content/Blueprints/BP_Test.uasset +*/ +class FGitLsTreeParser +{ +public: + /** Parse the unmerge status: extract the base SHA1 identifier of the file */ + FGitLsTreeParser(const TArray& InResults) + { + const FString& FirstResult = InResults[0]; + FileHash = FirstResult.Mid(12, 40); + int32 IdxTab; + if(FirstResult.FindChar('\t', IdxTab)) + { + const FString SizeString = FirstResult.Mid(53, IdxTab - 53); + FileSize = FCString::Atoi(*SizeString); + } + } + + FString FileHash; ///< SHA1 Id of the file (warning: not the commit Id) + int32 FileSize; ///< Size of the file (in bytes) +}; + +// Run a Git "log" command and parse it. +bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, TArray& OutErrorMessages, TGitSourceControlHistory& OutHistory) +{ + bool bResults; + { + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("--follow")); // follow file renames + Parameters.Add(TEXT("--date=raw")); + Parameters.Add(TEXT("--name-status")); // relative filename at this revision, preceded by a status character + Parameters.Add(TEXT("--pretty=medium")); // make sure format matches expected in ParseLogResults + if(bMergeConflict) + { + // In case of a merge conflict, we also need to get the tip of the "remote branch" (MERGE_HEAD) before the log of the "current branch" (HEAD) + // @todo does not work for a cherry-pick! Test for a rebase. + Parameters.Add(TEXT("MERGE_HEAD")); + Parameters.Add(TEXT("--max-count 1")); + } + TArray Files; + Files.Add(*InFile); + bResults = RunCommand(TEXT("log"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages); + if(bResults) + { + ParseLogResults(Results, OutHistory); + } + } + for(auto& Revision : OutHistory) + { + // Get file (blob) sha1 id and size + TArray Results; + TArray Parameters; + Parameters.Add(TEXT("--long")); // Show object size of blob (file) entries. + Parameters.Add(Revision->GetRevision()); + TArray Files; + Files.Add(*Revision->GetFilename()); + bResults &= RunCommand(TEXT("ls-tree"), InPathToGitBinary, InRepositoryRoot, Parameters, Files, Results, OutErrorMessages); + if(bResults && Results.Num()) + { + FGitLsTreeParser LsTree(Results); + Revision->FileHash = LsTree.FileHash; + Revision->FileSize = LsTree.FileSize; + } + } + + return bResults; +} + +TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo) +{ + TArray RelativeFiles; + FString RelativeTo = InRelativeTo; + + // Ensure that the path ends w/ '/' + if((RelativeTo.Len() > 0) && (RelativeTo.EndsWith(TEXT("/"), ESearchCase::CaseSensitive) == false) && (RelativeTo.EndsWith(TEXT("\\"), ESearchCase::CaseSensitive) == false)) + { + RelativeTo += TEXT("/"); + } + for(FString FileName : InFileNames) // string copy to be able to convert it inplace + { + if(FPaths::MakePathRelativeTo(FileName, *RelativeTo)) + { + RelativeFiles.Add(FileName); + } + } + + return RelativeFiles; +} + +TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo) +{ + TArray AbsFiles; + + for(FString FileName : InFileNames) // string copy to be able to convert it inplace + { + AbsFiles.Add(FPaths::Combine(InRelativeTo, FileName)); + } + + return AbsFiles; +} + +bool UpdateCachedStates(const TArray& InStates) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked( "GitSourceControl" ); + FGitSourceControlProvider& Provider = GitSourceControl.GetProvider(); + const bool bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); + + // TODO without LFS : Workaround a bug with the Source Control Module not updating file state after a simple "Save" with no "Checkout" (when not using File Lock) + const FDateTime Now = bUsingGitLfsLocking ? FDateTime::Now() : FDateTime(); + + for(const auto& InState : InStates) + { + TSharedRef State = Provider.GetStateInternal(InState.LocalFilename); + *State = InState; + State->TimeStamp = Now; + } + + return (InStates.Num() > 0); +} + +/** + * Helper struct for RemoveRedundantErrors() + */ +struct FRemoveRedundantErrors +{ + FRemoveRedundantErrors(const FString& InFilter) + : Filter(InFilter) + { + } + + bool operator()(const FString& String) const + { + if(String.Contains(Filter)) + { + return true; + } + + return false; + } + + /** The filter string we try to identify in the reported error */ + FString Filter; +}; + +void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter) +{ + bool bFoundRedundantError = false; + for(auto Iter(InCommand.ErrorMessages.CreateConstIterator()); Iter; Iter++) + { + if(Iter->Contains(InFilter)) + { + InCommand.InfoMessages.Add(*Iter); + bFoundRedundantError = true; + } + } + + InCommand.ErrorMessages.RemoveAll( FRemoveRedundantErrors(InFilter) ); + + // if we have no error messages now, assume success! + if(bFoundRedundantError && InCommand.ErrorMessages.Num() == 0 && !InCommand.bCommandSuccessful) + { + InCommand.bCommandSuccessful = true; + } +} + +} diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.h new file mode 100644 index 0000000..971b433 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/GitSourceControlUtils.h @@ -0,0 +1,220 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "GitSourceControlState.h" + +class FGitSourceControlCommand; + +/** + * Helper struct for maintaining temporary files for passing to commands + */ +class FGitScopedTempFile +{ +public: + + /** Constructor - open & write string to temp file */ + FGitScopedTempFile(const FText& InText); + + /** Destructor - delete temp file */ + ~FGitScopedTempFile(); + + /** Get the filename of this temp file - empty if it failed to be created */ + const FString& GetFilename() const; + +private: + /** The filename we are writing to */ + FString Filename; +}; + +struct FGitVersion; + +namespace GitSourceControlUtils +{ + +/** + * Find the path to the Git binary, looking into a few places (standalone Git install, and other common tools embedding Git) + * @returns the path to the Git binary if found, or an empty string. + */ +FString FindGitBinaryPath(); + +/** + * Run a Git "version" command to check the availability of the binary. + * @param InPathToGitBinary The path to the Git binary + * @param OutGitVersion If provided, populate with the git version parsed from "version" command + * @returns true if the command succeeded and returned no errors + */ +bool CheckGitAvailability(const FString& InPathToGitBinary, FGitVersion* OutVersion = nullptr); + +/** + * Parse the output from the "version" command into GitMajorVersion and GitMinorVersion. + * @param InVersionString The version string returned by `git --version` + * @param OutVersion The FGitVersion to populate + */ + void ParseGitVersion(const FString& InVersionString, FGitVersion* OutVersion); + +/** + * Check git for various optional capabilities by various means. + * @param InPathToGitBinary The path to the Git binary + * @param OutGitVersion If provided, populate with the git version parsed from "version" command + */ +void FindGitCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion); + +/** + * Run a Git "lfs" command to check the availability of the "Large File System" extension. + * @param InPathToGitBinary The path to the Git binary + * @param OutGitVersion If provided, populate with the git version parsed from "version" command + */ + void FindGitLfsCapabilities(const FString& InPathToGitBinary, FGitVersion *OutVersion); + +/** + * Find the root of the Git repository, looking from the provided path and upward in its parent directories + * @param InPath The path to the Game Directory (or any path or file in any git repository) + * @param OutRepositoryRoot The path to the root directory of the Git repository if found, else the path to the ProjectDir + * @returns true if the command succeeded and returned no errors + */ +bool FindRootDirectory(const FString& InPath, FString& OutRepositoryRoot); + +/** + * Get Git config user.name & user.email + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param OutUserName Name of the Git user configured for this repository (or globaly) + * @param OutEmailName E-mail of the Git user configured for this repository (or globaly) + */ +void GetUserConfig(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutUserName, FString& OutUserEmail); + +/** + * Get Git current checked-out branch + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param OutBranchName Name of the current checked-out branch (if any, ie. not in detached HEAD) + * @returns true if the command succeeded and returned no errors + */ +bool GetBranchName(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutBranchName); + +/** + * Get Git current commit details + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param OutCommitId Current Commit full SHA1 + * @param OutCommitSummary Current Commit description's Summary + * @returns true if the command succeeded and returned no errors + */ +bool GetCommitInfo(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutCommitId, FString& OutCommitSummary); + +/** + * Get the URL of the "origin" defaut remote server + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param OutRemoteUrl URL of "origin" defaut remote server + * @returns true if the command succeeded and returned no errors + */ +bool GetRemoteUrl(const FString& InPathToGitBinary, const FString& InRepositoryRoot, FString& OutRemoteUrl); + +/** + * Run a Git command - output is a string TArray. + * + * @param InCommand The Git command - e.g. commit + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param InParameters The parameters to the Git command + * @param InFiles The files to be operated on + * @param OutResults The results (from StdOut) as an array per-line + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @returns true if the command succeeded and returned no errors + */ +bool RunCommand(const FString& InCommand, const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages); + +/** + * Run a Git "commit" command by batches. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param InParameter The parameters to the Git commit command + * @param InFiles The files to be operated on + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @returns true if the command succeeded and returned no errors + */ +bool RunCommit(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const TArray& InParameters, const TArray& InFiles, TArray& OutResults, TArray& OutErrorMessages); + +/** + * Run a Git "status" command and parse it. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory (can be empty) + * @param InUsingLfsLocking Tells if using the Git LFS file Locking workflow + * @param InFiles The files to be operated on + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @returns true if the command succeeded and returned no errors + */ +bool RunUpdateStatus(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool InUsingLfsLocking, const TArray& InFiles, TArray& OutErrorMessages, TArray& OutStates); + +/** + * Run a Git "cat-file" command to dump the binary content of a revision into a file. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param InParameter The parameters to the Git show command (rev:path) + * @param InDumpFileName The temporary file to dump the revision + * @returns true if the command succeeded and returned no errors +*/ +bool RunDumpToFile(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InParameter, const FString& InDumpFileName); + +/** + * Run a Git "log" command and parse it. + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param InFile The file to be operated on + * @param bMergeConflict In case of a merge conflict, we also need to get the tip of the "remote branch" (MERGE_HEAD) before the log of the "current branch" (HEAD) + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @param OutHistory The history of the file + */ +bool RunGetHistory(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const FString& InFile, bool bMergeConflict, TArray& OutErrorMessages, TGitSourceControlHistory& OutHistory); + +/** + * Helper function to convert a filename array to relative paths. + * @param InFileNames The filename array + * @param InRelativeTo Path to the WorkspaceRoot + * @return an array of filenames, transformed into relative paths + */ +TArray RelativeFilenames(const TArray& InFileNames, const FString& InRelativeTo); + +/** + * Helper function to convert a filename array to absolute paths. + * @param InFileNames The filename array (relative paths) + * @param InRelativeTo Path to the WorkspaceRoot + * @return an array of filenames, transformed into absolute paths + */ +TArray AbsoluteFilenames(const TArray& InFileNames, const FString& InRelativeTo); + +/** + * Helper function for various commands to update cached states. + * @returns true if any states were updated + */ +bool UpdateCachedStates(const TArray& InStates); + +/** + * Remove redundant errors (that contain a particular string) and also + * update the commands success status if all errors were removed. + */ +void RemoveRedundantErrors(FGitSourceControlCommand& InCommand, const FString& InFilter); + +/** + * Run 'git lfs locks" to extract all lock information for all files in the repository + * + * @param InPathToGitBinary The path to the Git binary + * @param InRepositoryRoot The Git repository from where to run the command - usually the Game directory + * @param bAbsolutePaths Whether to report absolute filenames, false for repo-relative + * @param OutErrorMessages Any errors (from StdErr) as an array per-line + * @param OutLocks The lock results (file, username) + * @returns true if the command succeeded and returned no errors + */ +bool GetAllLocks(const FString& InPathToGitBinary, const FString& InRepositoryRoot, const bool bAbsolutePaths, TArray& OutErrorMessages, TMap& OutLocks); + +} diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/IGitSourceControlWorker.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/IGitSourceControlWorker.h new file mode 100644 index 0000000..fb2a01a --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/IGitSourceControlWorker.h @@ -0,0 +1,30 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" + +class IGitSourceControlWorker +{ +public: + /** + * Name describing the work that this worker does. Used for factory method hookup. + */ + virtual FName GetName() const = 0; + + /** + * Function that actually does the work. Can be executed on another thread. + */ + virtual bool Execute( class FGitSourceControlCommand& InCommand ) = 0; + + /** + * Updates the state of any items after completion (if necessary). This is always executed on the main thread. + * @returns true if states were updated + */ + virtual bool UpdateStates() const = 0; +}; + +typedef TSharedRef FGitSourceControlWorkerRef; diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp new file mode 100644 index 0000000..ce1d9ab --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.cpp @@ -0,0 +1,750 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#include "SGitSourceControlSettings.h" + +#include "Fonts/SlateFontInfo.h" +#include "Misc/App.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "Modules/ModuleManager.h" +#include "Styling/SlateTypes.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SFilePathPicker.h" +#include "Widgets/Input/SMultiLineEditableTextBox.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SSeparator.h" +#include "Widgets/Notifications/SNotificationList.h" +#include "Framework/Notifications/NotificationManager.h" +#include "EditorDirectories.h" +#include "EditorStyleSet.h" +#include "SourceControlOperations.h" +#include "GitSourceControlModule.h" +#include "GitSourceControlUtils.h" + +#define LOCTEXT_NAMESPACE "SGitSourceControlSettings" + +void SGitSourceControlSettings::Construct(const FArguments& InArgs) +{ + const FSlateFontInfo Font = FEditorStyle::GetFontStyle(TEXT("SourceControl.LoginWindow.Font")); + + bAutoCreateGitIgnore = true; + bAutoCreateReadme = true; + bAutoCreateGitAttributes = false; + bAutoInitialCommit = true; + + InitialCommitMessage = LOCTEXT("InitialCommitMessage", "Initial commit"); + + const FText FileFilterType = NSLOCTEXT("GitSourceControl", "Executables", "Executables"); +#if PLATFORM_WINDOWS + const FString FileFilterText = FString::Printf(TEXT("%s (*.exe)|*.exe"), *FileFilterType.ToString()); +#else + const FString FileFilterText = FString::Printf(TEXT("%s"), *FileFilterType.ToString()); +#endif + + ReadmeContent = FText::FromString(FString(TEXT("# ")) + FApp::GetProjectName() + "\n\nDeveloped with Unreal Engine 4\n"); + + ChildSlot + [ + SNew(SBorder) + .BorderImage( FEditorStyle::GetBrush("DetailsView.CategoryBottom")) + .Padding(FMargin(0.0f, 3.0f, 0.0f, 0.0f)) + [ + SNew(SVerticalBox) + // Path to the Git command line executable + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("BinaryPathLabel_Tooltip", "Path to Git binary")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("BinaryPathLabel", "Git Path")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(SFilePathPicker) + .BrowseButtonImage(FEditorStyle::GetBrush("PropertyWindow.Button_Ellipsis")) + .BrowseButtonStyle(FEditorStyle::Get(), "HoverHintOnly") + .BrowseDirectory(FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_OPEN)) + .BrowseTitle(LOCTEXT("BinaryPathBrowseTitle", "File picker...")) + .FilePath(this, &SGitSourceControlSettings::GetBinaryPathString) + .FileTypeFilter(FileFilterText) + .OnPathPicked(this, &SGitSourceControlSettings::OnBinaryPathPicked) + ] + ] + // Root of the local repository + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("RepositoryRootLabel_Tooltip", "Path to the root of the Git repository")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("RepositoryRootLabel", "Root of the repository")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(STextBlock) + .Text(this, &SGitSourceControlSettings::GetPathToRepositoryRoot) + .Font(Font) + ] + ] + // User Name + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("GitUserName_Tooltip", "User name configured for the Git repository")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("GitUserName", "User Name")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(STextBlock) + .Text(this, &SGitSourceControlSettings::GetUserName) + .Font(Font) + ] + ] + // User e-mail + +SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("GitUserEmail_Tooltip", "User e-mail configured for the Git repository")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("GitUserEmail", "E-Mail")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + [ + SNew(STextBlock) + .Text(this, &SGitSourceControlSettings::GetUserEmail) + .Font(Font) + ] + ] + // Separator + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SSeparator) + ] + // Explanation text + +SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + .HAlign(HAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("RepositoryNotFound", "Current Project is not contained in a Git Repository. Fill the form below to initialize a new Repository.")) + .ToolTipText(LOCTEXT("RepositoryNotFound_Tooltip", "No Repository found at the level or above the current Project")) + .Font(Font) + ] + ] + // Option to configure the URL of the default remote 'origin' + // TODO: option to configure the name of the remote instead of the default origin + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("ConfigureOrigin_Tooltip", "Configure the URL of the default remote 'origin'")) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(STextBlock) + .Text(LOCTEXT("ConfigureOrigin", "URL of the remote server 'origin'")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetRemoteUrl) + .OnTextCommitted(this, &SGitSourceControlSettings::OnRemoteUrlCommited) + .Font(Font) + ] + ] + // Option to add a proper .gitignore file (true by default) + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("CreateGitIgnore_Tooltip", "Create and add a standard '.gitignore' file")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Checked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitIgnore) + ] + +SHorizontalBox::Slot() + .FillWidth(2.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateGitIgnore", "Add a .gitignore file")) + .Font(Font) + ] + ] + // Option to add a README.md file with custom content + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("CreateReadme_Tooltip", "Add a README.md file")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Checked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateReadme) + ] + +SHorizontalBox::Slot() + .FillWidth(0.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateReadme", "Add a basic README.md file")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .Padding(2.0f) + [ + SNew(SMultiLineEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetReadmeContent) + .OnTextCommitted(this, &SGitSourceControlSettings::OnReadmeContentCommited) + .IsEnabled(this, &SGitSourceControlSettings::GetAutoCreateReadme) + .SelectAllTextWhenFocused(true) + .Font(Font) + ] + ] + // Option to add a proper .gitattributes file for Git LFS (false by default) + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("CreateGitAttributes_Tooltip", "Create and add a '.gitattributes' file to enable Git LFS for the whole 'Content/' directory (needs Git LFS extensions to be installed).")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Unchecked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedCreateGitAttributes) + .IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitLfs) + ] + +SHorizontalBox::Slot() + .FillWidth(2.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("CreateGitAttributes", "Add a .gitattributes file to enable Git LFS")) + .Font(Font) + ] + ] + // Option to use the Git LFS File Locking workflow (false by default) + // Enabled even after init to switch it off in case of no network + // TODO LFS turning it off afterwards does not work because all files are readonly ! + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .ToolTipText(LOCTEXT("UseGitLfsLocking_Tooltip", "Uses Git LFS 2 File Locking workflow (CheckOut and Commit/Push).")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(SGitSourceControlSettings::IsUsingGitLfsLocking()) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedUseGitLfsLocking) + .IsEnabled(this, &SGitSourceControlSettings::CanUseGitLfsLocking) + ] + +SHorizontalBox::Slot() + .FillWidth(0.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("UseGitLfsLocking", "Uses Git LFS 2 File Locking workflow")) + .Font(Font) + ] + // Username credential used to access the Git LFS 2 File Locks server + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetLfsUserName) + .OnTextCommitted(this, &SGitSourceControlSettings::OnLfsUserNameCommited) + .IsEnabled(this, &SGitSourceControlSettings::GetIsUsingGitLfsLocking) + .HintText(LOCTEXT("LfsUserName_Hint", "Username to lock files on the LFS server")) + .Font(Font) + ] + ] + // Option to Make the initial Git commit with custom message + +SVerticalBox::Slot() + .AutoHeight() + .Padding(2.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + .ToolTipText(LOCTEXT("InitialGitCommit_Tooltip", "Make the initial Git commit")) + +SHorizontalBox::Slot() + .FillWidth(0.1f) + [ + SNew(SCheckBox) + .IsChecked(ECheckBoxState::Checked) + .OnCheckStateChanged(this, &SGitSourceControlSettings::OnCheckedInitialCommit) + ] + +SHorizontalBox::Slot() + .FillWidth(0.9f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(LOCTEXT("InitialGitCommit", "Make the initial Git commit")) + .Font(Font) + ] + +SHorizontalBox::Slot() + .FillWidth(2.0f) + .Padding(2.0f) + [ + SNew(SMultiLineEditableTextBox) + .Text(this, &SGitSourceControlSettings::GetInitialCommitMessage) + .OnTextCommitted(this, &SGitSourceControlSettings::OnInitialCommitMessageCommited) + .IsEnabled(this, &SGitSourceControlSettings::GetAutoInitialCommit) + .SelectAllTextWhenFocused(true) + .Font(Font) + ] + ] + // Button to initialize the project with Git, create .gitignore/.gitattributes files, and make the first commit) + +SVerticalBox::Slot() + .FillHeight(2.5f) + .Padding(4.0f) + .VAlign(VAlign_Center) + [ + SNew(SHorizontalBox) + .Visibility(this, &SGitSourceControlSettings::MustInitializeGitRepository) + +SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SButton) + .Text(LOCTEXT("GitInitRepository", "Initialize project with Git")) + .ToolTipText(LOCTEXT("GitInitRepository_Tooltip", "Initialize current project as a new Git repository")) + .OnClicked(this, &SGitSourceControlSettings::OnClickedInitializeGitRepository) + .IsEnabled(this, &SGitSourceControlSettings::CanInitializeGitRepository) + .HAlign(HAlign_Center) + .ContentPadding(6) + ] + ] + ] + ]; +} + +SGitSourceControlSettings::~SGitSourceControlSettings() +{ + RemoveInProgressNotification(); +} + +FString SGitSourceControlSettings::GetBinaryPathString() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return GitSourceControl.AccessSettings().GetBinaryPath(); +} + +void SGitSourceControlSettings::OnBinaryPathPicked( const FString& PickedPath ) const +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + FString PickedFullPath = FPaths::ConvertRelativePathToFull(PickedPath); + const bool bChanged = GitSourceControl.AccessSettings().SetBinaryPath(PickedFullPath); + if(bChanged) + { + // Re-Check provided git binary path for each change + GitSourceControl.GetProvider().CheckGitAvailability(); + if(GitSourceControl.GetProvider().IsGitAvailable()) + { + GitSourceControl.SaveSettings(); + } + } +} + +FText SGitSourceControlSettings::GetPathToRepositoryRoot() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.GetProvider().GetPathToRepositoryRoot()); +} + +FText SGitSourceControlSettings::GetUserName() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.GetProvider().GetUserName()); +} + +FText SGitSourceControlSettings::GetUserEmail() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.GetProvider().GetUserEmail()); +} + +EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable(); + const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled(); + return (bGitAvailable && !bGitRepositoryFound) ? EVisibility::Visible : EVisibility::Collapsed; +} + +bool SGitSourceControlSettings::CanInitializeGitRepository() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const bool bGitAvailable = GitSourceControl.GetProvider().IsGitAvailable(); + const bool bGitRepositoryFound = GitSourceControl.GetProvider().IsEnabled(); + const FString LfsUserName = GitSourceControl.AccessSettings().GetLfsUserName(); + const bool bIsUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); + const bool bGitLfsConfigOk = !bIsUsingGitLfsLocking || !LfsUserName.IsEmpty(); + const bool bInitialCommitConfigOk = !bAutoInitialCommit || !InitialCommitMessage.IsEmpty(); + return (bGitAvailable && !bGitRepositoryFound && bGitLfsConfigOk && bInitialCommitConfigOk); +} + +bool SGitSourceControlSettings::CanInitializeGitLfs() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + const bool bGitLfsAvailable = GitSourceControl.GetProvider().GetGitVersion().bHasGitLfs; + return bGitLfsAvailable; +} + +bool SGitSourceControlSettings::CanUseGitLfsLocking() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const bool bGitLfsLockingAvailable = GitSourceControl.GetProvider().GetGitVersion().bHasGitLfsLocking; + // TODO LFS SRombauts : check if .gitattributes file is present and if Content/ is already tracked! + const bool bGitAttributesCreated = true; + return (bGitLfsLockingAvailable && (bAutoCreateGitAttributes || bGitAttributesCreated)); +} + +FReply SGitSourceControlSettings::OnClickedInitializeGitRepository() +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath(); + const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()); + TArray InfoMessages; + TArray ErrorMessages; + + // 1.a. Synchronous (very quick) "git init" operation: initialize a Git local repository with a .git/ subdirectory + GitSourceControlUtils::RunCommand(TEXT("init"), PathToGitBinary, PathToProjectDir, TArray(), TArray(), InfoMessages, ErrorMessages); + // 1.b. Synchronous (very quick) "git remote add" operation: configure the URL of the default remote server 'origin' if specified + if(!RemoteUrl.IsEmpty()) + { + TArray Parameters; + Parameters.Add(TEXT("add origin")); + Parameters.Add(RemoteUrl.ToString()); + GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, TArray(), InfoMessages, ErrorMessages); + } + + // Check the new repository status to enable connection (branch, user e-mail) + GitSourceControl.GetProvider().CheckRepositoryStatus(PathToGitBinary); + if(GitSourceControl.GetProvider().IsAvailable()) + { + // List of files to add to Source Control (.uproject, Config/, Content/, Source/ files and .gitignore/.gitattributes if any) + TArray ProjectFiles; + ProjectFiles.Add(FPaths::GetProjectFilePath()); + ProjectFiles.Add(FPaths::ProjectConfigDir()); + ProjectFiles.Add(FPaths::ProjectContentDir()); + if (FPaths::DirectoryExists(FPaths::GameSourceDir())) + { + ProjectFiles.Add(FPaths::GameSourceDir()); + } + if(bAutoCreateGitIgnore) + { + // 2.a. Create a standard ".gitignore" file with common patterns for a typical Blueprint & C++ project + const FString GitIgnoreFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitignore")); + const FString GitIgnoreContent = TEXT("Binaries\nDerivedDataCache\nIntermediate\nSaved\n.vscode\n.vs\n*.VC.db\n*.opensdf\n*.opendb\n*.sdf\n*.sln\n*.suo\n*.xcodeproj\n*.xcworkspace\n*.log"); + if(FFileHelper::SaveStringToFile(GitIgnoreContent, *GitIgnoreFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + ProjectFiles.Add(GitIgnoreFilename); + } + } + if(bAutoCreateReadme) + { + // 2.b. Create a "README.md" file with a custom description + const FString ReadmeFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT("README.md")); + if (FFileHelper::SaveStringToFile(ReadmeContent.ToString(), *ReadmeFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + ProjectFiles.Add(ReadmeFilename); + } + } + if(bAutoCreateGitAttributes) + { + // 2.c. Synchronous (very quick) "lfs install" operation: needs only to be run once by user + GitSourceControlUtils::RunCommand(TEXT("lfs install"), PathToGitBinary, PathToProjectDir, TArray(), TArray(), InfoMessages, ErrorMessages); + + // 2.d. Create a ".gitattributes" file to enable Git LFS (Large File System) for the whole "Content/" subdir + const FString GitAttributesFilename = FPaths::Combine(FPaths::ProjectDir(), TEXT(".gitattributes")); + FString GitAttributesContent; + if(GitSourceControl.AccessSettings().IsUsingGitLfsLocking()) + { + // Git LFS 2.x File Locking mechanism + GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text lockable\n"); + } + else + { + GitAttributesContent = TEXT("Content/** filter=lfs diff=lfs merge=lfs -text\n"); + } + if(FFileHelper::SaveStringToFile(GitAttributesContent, *GitAttributesFilename, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + ProjectFiles.Add(GitAttributesFilename); + } + } + + // 3. Add files to Source Control: launch an asynchronous MarkForAdd operation + LaunchMarkForAddOperation(ProjectFiles); + + // 4. The CheckIn will follow, at completion of the MarkForAdd operation + } + return FReply::Handled(); +} + +// Launch an asynchronous "MarkForAdd" operation and start an ongoing notification +void SGitSourceControlSettings::LaunchMarkForAddOperation(const TArray& InFiles) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + TSharedRef MarkForAddOperation = ISourceControlOperation::Create(); + ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(MarkForAddOperation, InFiles, EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + DisplayInProgressNotification(MarkForAddOperation); + } + else + { + DisplayFailureNotification(MarkForAddOperation); + } +} + +// Launch an asynchronous "CheckIn" operation and start another ongoing notification +void SGitSourceControlSettings::LaunchCheckInOperation() +{ + TSharedRef CheckInOperation = ISourceControlOperation::Create(); + CheckInOperation->SetDescription(InitialCommitMessage); + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, TArray(), EConcurrency::Asynchronous, FSourceControlOperationComplete::CreateSP(this, &SGitSourceControlSettings::OnSourceControlOperationComplete)); + if (Result == ECommandResult::Succeeded) + { + DisplayInProgressNotification(CheckInOperation); + } + else + { + DisplayFailureNotification(CheckInOperation); + } +} + +/// Delegate called when a source control operation has completed: launch the next one and manage notifications +void SGitSourceControlSettings::OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult) +{ + RemoveInProgressNotification(); + + // Report result with a notification + if (InResult == ECommandResult::Succeeded) + { + DisplaySuccessNotification(InOperation); + } + else + { + DisplayFailureNotification(InOperation); + } + + if ((InOperation->GetName() == "MarkForAdd") && (InResult == ECommandResult::Succeeded) && bAutoInitialCommit) + { + // 4. optional initial Asynchronous commit with custom message: launch a "CheckIn" Operation + LaunchCheckInOperation(); + } +} + + +// Display an ongoing notification during the whole operation +void SGitSourceControlSettings::DisplayInProgressNotification(const FSourceControlOperationRef& InOperation) +{ + FNotificationInfo Info(InOperation->GetInProgressString()); + Info.bFireAndForget = false; + Info.ExpireDuration = 0.0f; + Info.FadeOutDuration = 1.0f; + OperationInProgressNotification = FSlateNotificationManager::Get().AddNotification(Info); + if (OperationInProgressNotification.IsValid()) + { + OperationInProgressNotification.Pin()->SetCompletionState(SNotificationItem::CS_Pending); + } +} + +// Remove the ongoing notification at the end of the operation +void SGitSourceControlSettings::RemoveInProgressNotification() +{ + if (OperationInProgressNotification.IsValid()) + { + OperationInProgressNotification.Pin()->ExpireAndFadeout(); + OperationInProgressNotification.Reset(); + } +} + +// Display a temporary success notification at the end of the operation +void SGitSourceControlSettings::DisplaySuccessNotification(const FSourceControlOperationRef& InOperation) +{ + const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Success", "{0} operation was successfull!"), FText::FromName(InOperation->GetName())); + FNotificationInfo Info(NotificationText); + Info.bUseSuccessFailIcons = true; + Info.Image = FEditorStyle::GetBrush(TEXT("NotificationList.SuccessImage")); + FSlateNotificationManager::Get().AddNotification(Info); +} + +// Display a temporary failure notification at the end of the operation +void SGitSourceControlSettings::DisplayFailureNotification(const FSourceControlOperationRef& InOperation) +{ + const FText NotificationText = FText::Format(LOCTEXT("InitialCommit_Failure", "Error: {0} operation failed!"), FText::FromName(InOperation->GetName())); + FNotificationInfo Info(NotificationText); + Info.ExpireDuration = 8.0f; + FSlateNotificationManager::Get().AddNotification(Info); +} + +void SGitSourceControlSettings::OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState) +{ + bAutoCreateGitIgnore = (NewCheckedState == ECheckBoxState::Checked); +} + +void SGitSourceControlSettings::OnCheckedCreateReadme(ECheckBoxState NewCheckedState) +{ + bAutoCreateReadme = (NewCheckedState == ECheckBoxState::Checked); +} + +bool SGitSourceControlSettings::GetAutoCreateReadme() const +{ + return bAutoCreateReadme; +} + +void SGitSourceControlSettings::OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + ReadmeContent = InText; +} + +FText SGitSourceControlSettings::GetReadmeContent() const +{ + return ReadmeContent; +} + +void SGitSourceControlSettings::OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState) +{ + bAutoCreateGitAttributes = (NewCheckedState == ECheckBoxState::Checked); +} + +void SGitSourceControlSettings::OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + GitSourceControl.AccessSettings().SetUsingGitLfsLocking(NewCheckedState == ECheckBoxState::Checked); + GitSourceControl.AccessSettings().SaveSettings(); +} + +bool SGitSourceControlSettings::GetIsUsingGitLfsLocking() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return GitSourceControl.AccessSettings().IsUsingGitLfsLocking(); +} + +ECheckBoxState SGitSourceControlSettings::IsUsingGitLfsLocking() const +{ + return (GetIsUsingGitLfsLocking() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); +} + +void SGitSourceControlSettings::OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + GitSourceControl.AccessSettings().SetLfsUserName(InText.ToString()); + GitSourceControl.AccessSettings().SaveSettings(); +} + +FText SGitSourceControlSettings::GetLfsUserName() const +{ + const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked("GitSourceControl"); + return FText::FromString(GitSourceControl.AccessSettings().GetLfsUserName()); +} + +void SGitSourceControlSettings::OnCheckedInitialCommit(ECheckBoxState NewCheckedState) +{ + bAutoInitialCommit = (NewCheckedState == ECheckBoxState::Checked); +} + +bool SGitSourceControlSettings::GetAutoInitialCommit() const +{ + return bAutoInitialCommit; +} + +void SGitSourceControlSettings::OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + InitialCommitMessage = InText; +} + +FText SGitSourceControlSettings::GetInitialCommitMessage() const +{ + return InitialCommitMessage; +} + +void SGitSourceControlSettings::OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType) +{ + RemoteUrl = InText; +} + +FText SGitSourceControlSettings::GetRemoteUrl() const +{ + return RemoteUrl; +} + +#undef LOCTEXT_NAMESPACE diff --git a/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.h b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.h new file mode 100644 index 0000000..38d4855 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/Source/GitSourceControl/Private/SGitSourceControlSettings.h @@ -0,0 +1,98 @@ +// Copyright (c) 2014-2020 Sebastien Rombauts (sebastien.rombauts@gmail.com) +// +// Distributed under the MIT License (MIT) (See accompanying file LICENSE.txt +// or copy at http://opensource.org/licenses/MIT) + +#pragma once + +#include "CoreMinimal.h" +#include "Layout/Visibility.h" +#include "Input/Reply.h" +#include "Widgets/DeclarativeSyntaxSupport.h" +#include "Widgets/SCompoundWidget.h" +#include "SlateFwd.h" +#include "ISourceControlOperation.h" +#include "ISourceControlProvider.h" + +enum class ECheckBoxState : uint8; + +class SGitSourceControlSettings : public SCompoundWidget +{ +public: + + SLATE_BEGIN_ARGS(SGitSourceControlSettings) {} + + SLATE_END_ARGS() + +public: + + void Construct(const FArguments& InArgs); + + ~SGitSourceControlSettings(); + +private: + + /** Delegates to get Git binary path from/to settings */ + FString GetBinaryPathString() const; + void OnBinaryPathPicked(const FString & PickedPath) const; + + /** Delegate to get repository root, user name and email from provider */ + FText GetPathToRepositoryRoot() const; + FText GetUserName() const; + FText GetUserEmail() const; + + EVisibility MustInitializeGitRepository() const; + bool CanInitializeGitRepository() const; + bool CanInitializeGitLfs() const; + bool CanUseGitLfsLocking() const; + + /** Delegate to initialize a new Git repository */ + FReply OnClickedInitializeGitRepository(); + + void OnCheckedCreateGitIgnore(ECheckBoxState NewCheckedState); + bool bAutoCreateGitIgnore; + + /** Delegates to create a README.md file */ + void OnCheckedCreateReadme(ECheckBoxState NewCheckedState); + bool GetAutoCreateReadme() const; + bool bAutoCreateReadme; + void OnReadmeContentCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetReadmeContent() const; + FText ReadmeContent; + + void OnCheckedCreateGitAttributes(ECheckBoxState NewCheckedState); + bool bAutoCreateGitAttributes; + + void OnCheckedUseGitLfsLocking(ECheckBoxState NewCheckedState); + ECheckBoxState IsUsingGitLfsLocking() const; + bool GetIsUsingGitLfsLocking() const; + + void OnLfsUserNameCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetLfsUserName() const; + + void OnCheckedInitialCommit(ECheckBoxState NewCheckedState); + bool GetAutoInitialCommit() const; + bool bAutoInitialCommit; + void OnInitialCommitMessageCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetInitialCommitMessage() const; + FText InitialCommitMessage; + + void OnRemoteUrlCommited(const FText& InText, ETextCommit::Type InCommitType); + FText GetRemoteUrl() const; + FText RemoteUrl; + + /** Launch initial asynchronous add and commit operations */ + void LaunchMarkForAddOperation(const TArray& InFiles); + void LaunchCheckInOperation(); + + /** Delegate called when a source control operation has completed */ + void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult); + + /** Asynchronous operation progress notifications */ + TWeakPtr OperationInProgressNotification; + + void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation); + void RemoveInProgressNotification(); + void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation); + void DisplayFailureNotification(const FSourceControlOperationRef& InOperation); +}; diff --git a/Plugins/UE4GitPlugin-2.17-beta/_config.yml b/Plugins/UE4GitPlugin-2.17-beta/_config.yml new file mode 100644 index 0000000..f170406 --- /dev/null +++ b/Plugins/UE4GitPlugin-2.17-beta/_config.yml @@ -0,0 +1,2 @@ +show_downloads: true +theme: jekyll-theme-slate \ No newline at end of file