Added the plugins to the GIT

This commit is contained in:
Jesse James Isler 2021-11-02 13:06:34 +01:00
parent 6ad7099708
commit 1600809ad1
71 changed files with 9533 additions and 0 deletions

View File

@ -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"
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -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<USceneComponent>(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);
}
}

View File

@ -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<URoom>();
root->Init(def);
RoomList.Add(root);
// Create the list with the correct mode (depth or breadth)
TQueueOrStack<URoom*>::EMode listMode;
switch(GenerationType)
{
case EGenerationType::DFS:
listMode = TQueueOrStack<URoom*>::EMode::STACK;
break;
case EGenerationType::BFS:
listMode = TQueueOrStack<URoom*>::EMode::QUEUE;
break;
}
// Build the list of rooms
TQueueOrStack<URoom*> 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<ADoor> 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<ADoor>(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<URoom*> ADungeonGenerator::AddNewRooms(URoom& ParentRoom)
{
TArray<URoom*> 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<URoom>();
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<ADoor> 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<URoomData*> 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<URoomData*> 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<URoomData*> RoomDataList)
{
int count = 0;
for(int i = 0; i < RoomList.Num(); i++)
{
if(RoomDataList.Contains(RoomList[i]->GetRoomData()))
{
count++;
}
}
return count;
}

View File

@ -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<ISettingsModule>("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<UProceduralDungeonSettings>()
);
// 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<ISettingsModule>("Settings"))
{
SettingsModule->UnregisterSettings("Project", "Plugins", "Procedural Dungeon");
}
}
// Callback for when the settings were saved.
bool FProceduralDungeonModule::HandleSettingsSaved()
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
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)

View File

@ -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<UProceduralDungeonSettings>();
_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);
}
}

View File

@ -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;
}

View File

@ -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"

View File

@ -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<UWorld> 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<UProceduralLevelStreaming>(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;
}

View File

@ -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"

View File

@ -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> 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<ARoomLevel>(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<URoom*>& 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<URoom*>& 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<URoom*>& 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<UProceduralDungeonSettings>();
return Settings->RoomUnit;
}
FVector URoom::DoorSize()
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
return Settings->DoorSize;
}
float URoom::DoorOffset()
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
return Settings->DoorOffset;
}
bool URoom::OcclusionCulling()
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
return Settings->OcclusionCulling;
}
bool URoom::DrawDebug()
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
return Settings->DrawDebug;
}
bool URoom::CanLoop()
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
return Settings->CanLoop;
}

View File

@ -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;
}

View File

@ -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<AActor> 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<FOverlapResult> 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);
}
}
}
}

View File

@ -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<URoomData> 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<ARoomLevel>(GetLevel()->GetLevelScriptActor());
}

View File

@ -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<UBoxComponent>(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<ACharacter>(OtherActor);
UCapsuleComponent* OtherCapsule = Cast<UCapsuleComponent>(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<ACharacter>(OtherActor);
UCapsuleComponent* OtherCapsule = Cast<UCapsuleComponent>(OtherComp);
if (OtherCharacter != nullptr && OtherCapsule != nullptr && OtherCapsule == OtherCharacter->GetCapsuleComponent() && CharacterList.Contains(OtherCharacter))
{
CharacterList.Remove(OtherCharacter);
}
}

View File

@ -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);
}
}

View File

@ -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" });
}
}

View File

@ -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);
};

View File

@ -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<ADoor> 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<URoomData*> 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<URoomData*> RoomDataList);
// Return a random RoomData from the array provided
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
URoomData* GetRandomRoomData(TArray<URoomData*> 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<ADoor> 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<URoom*> 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<URoom*> RoomList;
UPROPERTY()
TArray<class ADoor*> DoorList;
bool IsInit = false;
int NbInitRoom = 0;
int NbLoadedRoom = 0;
int NbUnloadedRoom = 0;
EGenerationState CurrentState = EGenerationState::None;
};

View File

@ -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();
};

View File

@ -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);

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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<UWorld> 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);
};

View File

@ -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<typename T>
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<T> Queue;
TArray<T> Stack;
};

View File

@ -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<URoom> OtherRoom = nullptr;
int OtherDoorIndex = -1;
ADoor* DoorInstance = nullptr;
};
UCLASS()
class PROCEDURALDUNGEON_API URoom : public UObject
{
GENERATED_BODY()
private:
UPROPERTY()
TArray<FRoomConnection> 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<URoom> 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<URoom*>& 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<URoom*>& 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<URoom*>& RoomList);
// Plugin Settings
static FVector Unit();
static FVector DoorSize();
static float DoorOffset();
static bool OcclusionCulling();
static bool DrawDebug();
static bool CanLoop();
};

View File

@ -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<UWorld> Level;
public:
UPROPERTY(EditAnywhere, Category = "Door")
bool RandomDoor;
UPROPERTY(EditAnywhere, Category = "Doors")
TArray<FDoorDef> Doors;
UPROPERTY(EditAnywhere, Category = "Room")
FIntVector Size;
public:
URoomData();
int GetNbDoor() const { return Doors.Num(); }
};

View File

@ -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<AActor*> ActorsInLevel;
FTransform Transform;
FVector Center;
FVector HalfExtents;
bool IsPlayerInside();
void Display();
};

View File

@ -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<URoomData> RoomType = nullptr);
protected:
ARoomLevel* GetRoomLevel();
};

View File

@ -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<class ACharacter*> 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);
};

View File

@ -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<AActor*>, 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<AActor> 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<AActor*> GetActorList() { return ActorList; }
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Trigger Type")
bool bIsActivated;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Trigger Type")
TArray<AActor*> 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();
};

View File

@ -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+

View File

@ -0,0 +1,5 @@
/Binaries/*/*.pdb
/Binaries/*/*Debug*
/Binaries/*/*.dylib
/Binaries/*/*.modules
/Intermediate

View File

@ -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"
}
]
}

View File

@ -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.

View File

@ -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)
<a href="https://www.paypal.me/SRombauts" title="Pay Me a Beer! Donate with PayPal :)"><img src="https://www.paypalobjects.com/webstatic/paypalme/images/pp_logo_small.png" width="118"></a>
- 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:
<img src="https://cdn2.unrealengine.com/blog/DiffTool-1009x542-719850393.png" width="720">
Merge conflict of a Blueprint:
<img src="https://docs.unrealengine.com/latest/images/Support/Builds/ReleaseNotes/2015/4_7/BPmergeTool.jpg" width="720">
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):
```
<YourGameProject>/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:
<img src="Screenshots/SourceControlLogin_Init.png" width="720">
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -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",
}
);
}
}

View File

@ -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<class ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe>& 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<FGitSourceControlModule>( "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;
}

View File

@ -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<class ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe>& 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<class ISourceControlOperation, ESPMode::ThreadSafe> Operation;
/** The object that will actually do the work */
TSharedRef<class IGitSourceControlWorker, ESPMode::ThreadSafe> 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<FString> Files;
/**Info and/or warning message storage*/
TArray<FString> InfoMessages;
/**Potential error message storage*/
TArray<FString> ErrorMessages;
};

View File

@ -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<FLevelEditorModule>("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<FLevelEditorModule>("LevelEditor");
if (LevelEditorModule)
{
LevelEditorModule->GetAllLevelEditorToolbarSourceControlMenuExtenders().RemoveAll([=](const FLevelEditorModule::FLevelEditorMenuExtender& Extender) { return Extender.GetHandle() == ViewMenuExtenderHandle; });
}
}
bool FGitSourceControlMenu::HaveRemoteUrl() const
{
const FGitSourceControlModule& GitSourceControl = FModuleManager::LoadModuleChecked<FGitSourceControlModule>("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<UPackage*> DirtyPackages;
FEditorFileUtils::GetDirtyWorldPackages(DirtyPackages);
FEditorFileUtils::GetDirtyContentPackages(DirtyPackages);
bSaved = DirtyPackages.Num() == 0;
}
return bSaved;
}
/// Find all packages in Content directory
TArray<FString> FGitSourceControlMenu::ListAllPackages()
{
TArray<FString> PackageRelativePaths;
FPackageName::FindPackagesInDirectory(PackageRelativePaths, *FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir()));
TArray<FString> 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<UPackage*> FGitSourceControlMenu::UnlinkPackages(const TArray<FString>& InPackageNames)
{
TArray<UPackage*> 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<UPackage*>& 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<UPackage*> 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<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
const TArray<FString> ParametersStatus{"--porcelain --untracked-files=no"};
TArray<FString> InfoMessages;
TArray<FString> ErrorMessages;
// Check if there is any modification to the working tree
const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), PathToGitBinary, PathToRespositoryRoot, ParametersStatus, TArray<FString>(), 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<FString> ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" };
bStashMadeBeforeSync = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
const FString& PathToRespositoryRoot = Provider.GetPathToRepositoryRoot();
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
const TArray<FString> ParametersStash{ "pop" };
TArray<FString> InfoMessages;
TArray<FString> ErrorMessages;
const bool bUnstashOk = GitSourceControlUtils::RunCommand(TEXT("stash"), PathToGitBinary, PathToRespositoryRoot, ParametersStash, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
TSharedRef<FSync, ESPMode::ThreadSafe> SyncOperation = ISourceControlOperation::Create<FSync>();
const ECommandResult::Type Result = Provider.Execute(SyncOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
TSharedRef<FGitPush, ESPMode::ThreadSafe> PushOperation = ISourceControlOperation::Create<FGitPush>();
const ECommandResult::Type Result = Provider.Execute(PushOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
TSharedRef<FRevert, ESPMode::ThreadSafe> RevertOperation = ISourceControlOperation::Create<FRevert>();
const ECommandResult::Type Result = Provider.Execute(RevertOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
TSharedRef<FUpdateStatus, ESPMode::ThreadSafe> RefreshOperation = ISourceControlOperation::Create<FUpdateStatus>();
RefreshOperation->SetCheckingAllFiles(true);
const ECommandResult::Type Result = Provider.Execute(RefreshOperation, TArray<FString>(), 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<FExtender> FGitSourceControlMenu::OnExtendLevelEditorViewMenu(const TSharedRef<FUICommandList> CommandList)
{
TSharedRef<FExtender> Extender(new FExtender());
Extender->AddMenuExtension(
"SourceControlActions",
EExtensionHook::After,
nullptr,
FMenuExtensionDelegate::CreateRaw(this, &FGitSourceControlMenu::AddMenuExtension));
return Extender;
}
#undef LOCTEXT_NAMESPACE

View File

@ -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<FString> ListAllPackages();
TArray<UPackage*> UnlinkPackages(const TArray<FString>& InPackageNames);
void ReloadPackages(TArray<UPackage*>& InPackagesToReload);
bool StashAwayAnyModifications();
void ReApplyStashedModifications();
void AddMenuExtension(FMenuBuilder& Builder);
TSharedRef<class FExtender> OnExtendLevelEditorViewMenu(const TSharedRef<class FUICommandList> 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<UPackage*> PackagesToReload;
/** Current source control operation from extended menu if any */
TWeakPtr<class SNotificationItem> OperationInProgressNotification;
/** Delegate called when a source control operation has completed */
void OnSourceControlOperationComplete(const FSourceControlOperationRef& InOperation, ECommandResult::Type InResult);
};

View File

@ -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<typename Type>
static TSharedRef<IGitSourceControlWorker, ESPMode::ThreadSafe> 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<FGitConnectWorker> ) );
// 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<FGitCheckOutWorker> ) );
GitSourceControlProvider.RegisterWorker( "UpdateStatus", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitUpdateStatusWorker> ) );
GitSourceControlProvider.RegisterWorker( "MarkForAdd", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitMarkForAddWorker> ) );
GitSourceControlProvider.RegisterWorker( "Delete", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitDeleteWorker> ) );
GitSourceControlProvider.RegisterWorker( "Revert", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitRevertWorker> ) );
GitSourceControlProvider.RegisterWorker( "Sync", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitSyncWorker> ) );
GitSourceControlProvider.RegisterWorker( "Push", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitPushWorker> ) );
GitSourceControlProvider.RegisterWorker( "CheckIn", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCheckInWorker> ) );
GitSourceControlProvider.RegisterWorker( "Copy", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitCopyWorker> ) );
GitSourceControlProvider.RegisterWorker( "Resolve", FGetGitSourceControlWorker::CreateStatic( &CreateWorker<FGitResolveWorker> ) );
// 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

View File

@ -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;
};

View File

@ -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<FConnect, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FConnect>(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<FString> 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<FString>(), TArray<FString>(), 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<FString> RelativeFiles = GitSourceControlUtils::RelativeFilenames(InCommand.Files, InCommand.PathToRepositoryRoot);
for(const auto& RelativeFile : RelativeFiles)
{
TArray<FString> OneFile;
OneFile.Add(RelativeFile);
InCommand.bCommandSuccessful &= GitSourceControlUtils::RunCommand(TEXT("lfs lock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString>& 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<FString> GetLockedFiles(const TArray<FString>& InFiles)
{
TArray<FString> LockedFiles;
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> 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<FCheckIn, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FCheckIn>(InCommand.Operation);
// make a temp file to place our commit message in
FGitScopedTempFile CommitMsgFile(Operation->GetDescription());
if(CommitMsgFile.GetFilename().Len() > 0)
{
TArray<FString> 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<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> 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<FString> Parameters2;
// TODO Configure origin
Parameters2.Add(TEXT("origin"));
Parameters2.Add(TEXT("HEAD"));
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, Parameters2, TArray<FString>(), 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<FString> ParametersStatus{"--porcelain --untracked-files=no"};
TArray<FString> StatusInfoMessages;
TArray<FString> StatusErrorMessages;
// Check if there is any modification to the working tree
const bool bStatusOk = GitSourceControlUtils::RunCommand(TEXT("status"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStatus, TArray<FString>(), StatusInfoMessages, StatusErrorMessages);
if ((bStatusOk) && (StatusInfoMessages.Num() > 0))
{
bStashNeeded = true;
const TArray<FString> ParametersStash{ "save \"Stashed by Unreal Engine Git Plugin\"" };
bStashed = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStash, TArray<FString>(), 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<FString>(), TArray<FString>(), InCommand.InfoMessages, InCommand.ErrorMessages);
if (InCommand.bCommandSuccessful)
{
// Repeat the push
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("push origin HEAD"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), TArray<FString>(), InCommand.InfoMessages, InCommand.ErrorMessages);
}
// Succeed or fail, restore the stash
if (bStashed)
{
const TArray<FString> ParametersStashPop{ "pop" };
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("stash"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, ParametersStashPop, TArray<FString>(), 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<FString> LockedFiles = GetLockedFiles(InCommand.Files);
if(LockedFiles.Num() > 0)
{
const TArray<FString> RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
for(const auto& RelativeFile : RelativeFiles)
{
TArray<FString> OneFile;
OneFile.Add(RelativeFile);
GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString>(), 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<FString>(), 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<FString>& InFiles, TArray<FString>& OutMissingFiles, TArray<FString>& OutAllExistingFiles, TArray<FString>& OutOtherThanAddedExistingFiles)
{
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
const TArray<FString> Files = (InFiles.Num() > 0) ? (InFiles) : (Provider.GetFilesInCache());
TArray<TSharedRef<ISourceControlState, ESPMode::ThreadSafe>> 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<FString> MissingFiles;
TArray<FString> AllExistingFiles;
TArray<FString> 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<FString>(), 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<FString>(), 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<FString>(), 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<FString> LockedFiles = GetLockedFiles(OtherThanAddedExistingFiles);
if(LockedFiles.Num() > 0)
{
const TArray<FString> RelativeFiles = GitSourceControlUtils::RelativeFilenames(LockedFiles, InCommand.PathToRepositoryRoot);
for(const auto& RelativeFile : RelativeFiles)
{
TArray<FString> OneFile;
OneFile.Add(RelativeFile);
GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString> 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<FString> 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<FString>(), 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<FString> FilesToUnlock;
if (InCommand.bUsingGitLfsLocking)
{
TMap<FString, FString> 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<FString> LfsPushParameters;
LfsPushParameters.Add(TEXT("push"));
LfsPushParameters.Add(TEXT("--dry-run"));
LfsPushParameters.Add(TEXT("origin"));
LfsPushParameters.Add(BranchName);
TArray<FString> LfsPushInfoMessages;
TArray<FString> LfsPushErrMessages;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("lfs"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, LfsPushParameters, TArray<FString>(), 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<FString> 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<FString>(), 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<FString> OneFile;
OneFile.Add(FileToUnlock);
bool bUnlocked = GitSourceControlUtils::RunCommand(TEXT("lfs unlock"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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<FString> 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<FUpdateStatus, ESPMode::ThreadSafe> Operation = StaticCastSharedRef<FUpdateStatus>(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<FString> 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<FGitSourceControlModule>( "GitSourceControl" );
FGitSourceControlProvider& Provider = GitSourceControl.GetProvider();
const FDateTime Now = FDateTime::Now();
// add history, if any
for(const auto& History : Histories)
{
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> 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<FString>(), 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<FString> Results;
InCommand.bCommandSuccessful = GitSourceControlUtils::RunCommand(TEXT("add"), InCommand.PathToGitBinary, InCommand.PathToRepositoryRoot, TArray<FString>(), 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

View File

@ -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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> 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<FGitSourceControlState> States;
/** Map of filenames to history */
TMap<FString, TGitSourceControlHistory> 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<FGitSourceControlState> 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<FGitSourceControlState> States;
};

View File

@ -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"

View File

@ -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<IPlugin> 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<FGitSourceControlModule>("GitSourceControl");
bUsingGitLfsLocking = GitSourceControl.AccessSettings().IsUsingGitLfsLocking();
}
// bForceConnection: not used anymore
}
void FGitSourceControlProvider::CheckGitAvailability()
{
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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<FGitSourceControlState, ESPMode::ThreadSafe> FGitSourceControlProvider::GetStateInternal(const FString& Filename)
{
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe>* State = StateCache.Find(Filename);
if(State != NULL)
{
// found cached item
return (*State);
}
else
{
// cache an unknown state for this item
TSharedRef<FGitSourceControlState, ESPMode::ThreadSafe> 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<FString>& InFiles, TArray< TSharedRef<ISourceControlState, ESPMode::ThreadSafe> >& OutState, EStateCacheUsage::Type InStateCacheUsage )
{
if(!IsEnabled())
{
return ECommandResult::Failed;
}
TArray<FString> AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
if(InStateCacheUsage == EStateCacheUsage::ForceUpdate)
{
Execute(ISourceControlOperation::Create<FUpdateStatus>(), AbsoluteFiles);
}
for(const auto& AbsoluteFile : AbsoluteFiles)
{
OutState.Add(GetStateInternal(*AbsoluteFile));
}
return ECommandResult::Succeeded;
}
TArray<FSourceControlStateRef> FGitSourceControlProvider::GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> Predicate) const
{
TArray<FSourceControlStateRef> 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<FString> FGitSourceControlProvider::GetFilesInCache()
{
TArray<FString> 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<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TArray<FString>& 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<FString> AbsoluteFiles = SourceControlHelpers::AbsoluteFilenames(InFiles);
// Query to see if we allow this operation
TSharedPtr<IGitSourceControlWorker, ESPMode::ThreadSafe> 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<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation ) const
{
return false;
}
void FGitSourceControlProvider::CancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& 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<IGitSourceControlWorker, ESPMode::ThreadSafe> 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<ISourceControlLabel> > FGitSourceControlProvider::GetLabels( const FString& InMatchingSpec ) const
{
TArray< TSharedRef<ISourceControlLabel> > 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<class SWidget> 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

View File

@ -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<FString>& 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<FString>& InFiles, TArray< TSharedRef<ISourceControlState, ESPMode::ThreadSafe> >& OutState, EStateCacheUsage::Type InStateCacheUsage ) override;
virtual TArray<FSourceControlStateRef> GetCachedStateByPredicate(TFunctionRef<bool(const FSourceControlStateRef&)> 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<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation, const TArray<FString>& InFiles, EConcurrency::Type InConcurrency = EConcurrency::Synchronous, const FSourceControlOperationComplete& InOperationCompleteDelegate = FSourceControlOperationComplete()) override;
virtual bool CanCancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation ) const override;
virtual void CancelOperation( const TSharedRef<ISourceControlOperation, ESPMode::ThreadSafe>& InOperation ) override;
virtual bool UsesLocalReadOnlyState() const override;
virtual bool UsesChangelists() const override;
virtual bool UsesCheckout() const override;
virtual void Tick() override;
virtual TArray< TSharedRef<class ISourceControlLabel> > GetLabels( const FString& InMatchingSpec ) const override;
#if SOURCE_CONTROL_WITH_SLATE
virtual TSharedRef<class SWidget> 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<FGitSourceControlState, ESPMode::ThreadSafe> 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<FString> 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<class IGitSourceControlWorker, ESPMode::ThreadSafe> 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<FString, TSharedRef<class FGitSourceControlState, ESPMode::ThreadSafe> > StateCache;
/** The currently registered source control operations */
TMap<FName, FGetGitSourceControlWorker> 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;
};

View File

@ -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<FGitSourceControlModule>("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<FAnnotationLine>& 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<class ISourceControlRevision, ESPMode::ThreadSafe> 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

View File

@ -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<FGitSourceControlRevision, ESPMode::ThreadSafe>
{
public:
FGitSourceControlRevision()
: RevisionNumber(0)
{
}
/** ISourceControlRevision interface */
virtual bool Get( FString& InOutFilename ) const override;
virtual bool GetAnnotated( TArray<FAnnotationLine>& 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<class ISourceControlRevision, ESPMode::ThreadSafe> 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<FGitSourceControlRevision, ESPMode::ThreadSafe> 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<FGitSourceControlRevision, ESPMode::ThreadSafe> > TGitSourceControlHistory;

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::GetHistoryItem( int32 HistoryIndex ) const
{
check(History.IsValidIndex(HistoryIndex));
return History[HistoryIndex];
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision( int32 RevisionNumber ) const
{
for(const auto& Revision : History)
{
if(Revision->GetRevisionNumber() == RevisionNumber)
{
return Revision;
}
}
return nullptr;
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FGitSourceControlState::FindHistoryRevision(const FString& InRevision) const
{
for(const auto& Revision : History)
{
if(Revision->GetRevision() == InRevision)
{
return Revision;
}
}
return nullptr;
}
TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> 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

View File

@ -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<FGitSourceControlState, ESPMode::ThreadSafe>
{
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<class ISourceControlRevision, ESPMode::ThreadSafe> GetHistoryItem(int32 HistoryIndex) const override;
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FindHistoryRevision(int32 RevisionNumber) const override;
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> FindHistoryRevision(const FString& InRevision) const override;
virtual TSharedPtr<class ISourceControlRevision, ESPMode::ThreadSafe> 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<FString> GetCheckedOutBranches() const /* UE4.20 override */ { return TArray<FString>(); }
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;
};

View File

@ -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<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& 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<FString>& InParameters, const TArray<FString>& InFiles, TArray<FString>& OutResults, TArray<FString>& 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<FString>& InFiles, TArray<FString>& OutErrorMessages, TArray<FGitSourceControlState>& 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<FString>& 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<FString> RelativeFilenames(const TArray<FString>& 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<FString> AbsoluteFilenames(const TArray<FString>& InFileNames, const FString& InRelativeTo);
/**
* Helper function for various commands to update cached states.
* @returns true if any states were updated
*/
bool UpdateCachedStates(const TArray<FGitSourceControlState>& 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<FString>& OutErrorMessages, TMap<FString, FString>& OutLocks);
}

View File

@ -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<IGitSourceControlWorker, ESPMode::ThreadSafe> FGitSourceControlWorkerRef;

View File

@ -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<FGitSourceControlModule>("GitSourceControl");
return GitSourceControl.AccessSettings().GetBinaryPath();
}
void SGitSourceControlSettings::OnBinaryPathPicked( const FString& PickedPath ) const
{
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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<FGitSourceControlModule>("GitSourceControl");
return FText::FromString(GitSourceControl.GetProvider().GetPathToRepositoryRoot());
}
FText SGitSourceControlSettings::GetUserName() const
{
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
return FText::FromString(GitSourceControl.GetProvider().GetUserName());
}
FText SGitSourceControlSettings::GetUserEmail() const
{
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
return FText::FromString(GitSourceControl.GetProvider().GetUserEmail());
}
EVisibility SGitSourceControlSettings::MustInitializeGitRepository() const
{
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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<FGitSourceControlModule>("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<FGitSourceControlModule>("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<FGitSourceControlModule>("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<FGitSourceControlModule>("GitSourceControl");
const FString& PathToGitBinary = GitSourceControl.AccessSettings().GetBinaryPath();
const FString PathToProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
TArray<FString> InfoMessages;
TArray<FString> 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<FString>(), TArray<FString>(), 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<FString> Parameters;
Parameters.Add(TEXT("add origin"));
Parameters.Add(RemoteUrl.ToString());
GitSourceControlUtils::RunCommand(TEXT("remote"), PathToGitBinary, PathToProjectDir, Parameters, TArray<FString>(), 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<FString> 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<FString>(), TArray<FString>(), 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<FString>& InFiles)
{
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
TSharedRef<FMarkForAdd, ESPMode::ThreadSafe> MarkForAddOperation = ISourceControlOperation::Create<FMarkForAdd>();
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<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = ISourceControlOperation::Create<FCheckIn>();
CheckInOperation->SetDescription(InitialCommitMessage);
FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("GitSourceControl");
ECommandResult::Type Result = GitSourceControl.GetProvider().Execute(CheckInOperation, TArray<FString>(), 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<FGitSourceControlModule>("GitSourceControl");
GitSourceControl.AccessSettings().SetUsingGitLfsLocking(NewCheckedState == ECheckBoxState::Checked);
GitSourceControl.AccessSettings().SaveSettings();
}
bool SGitSourceControlSettings::GetIsUsingGitLfsLocking() const
{
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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<FGitSourceControlModule>("GitSourceControl");
GitSourceControl.AccessSettings().SetLfsUserName(InText.ToString());
GitSourceControl.AccessSettings().SaveSettings();
}
FText SGitSourceControlSettings::GetLfsUserName() const
{
const FGitSourceControlModule& GitSourceControl = FModuleManager::GetModuleChecked<FGitSourceControlModule>("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

View File

@ -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<FString>& 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<SNotificationItem> OperationInProgressNotification;
void DisplayInProgressNotification(const FSourceControlOperationRef& InOperation);
void RemoveInProgressNotification();
void DisplaySuccessNotification(const FSourceControlOperationRef& InOperation);
void DisplayFailureNotification(const FSourceControlOperationRef& InOperation);
};

View File

@ -0,0 +1,2 @@
show_downloads: true
theme: jekyll-theme-slate