实现基本的载入/保存功能
This commit is contained in:
parent
c98fe4b6c8
commit
7f5b1c1900
@ -1,18 +1,34 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "AutoSave.h"
|
||||
#include "AutoSaveSubsystem.h"
|
||||
#include "Developer/Settings/Public/ISettingsModule.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "FAutoSaveModule"
|
||||
|
||||
void FAutoSaveModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
|
||||
|
||||
if (ISettingsModule* SettingModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
|
||||
{
|
||||
SettingModule->RegisterSettings("Project", "Plugins", "Auto Save",
|
||||
LOCTEXT("RuntimeSettingsName", "Auto Save"),
|
||||
LOCTEXT("RuntimeSettingsDescription", "Configure the Auto Save plugin"),
|
||||
GetMutableDefault<UAutoSaveSubsystem>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void FAutoSaveModule::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 (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
|
||||
{
|
||||
SettingsModule->UnregisterSettings("Project", "Plugins", "Auto Save");
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
3
Source/AutoSave/Private/AutoSaveLog.cpp
Normal file
3
Source/AutoSave/Private/AutoSaveLog.cpp
Normal file
@ -0,0 +1,3 @@
|
||||
#include "AutoSaveLog.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogAutoSave);
|
307
Source/AutoSave/Private/AutoSaveSubsystem.cpp
Normal file
307
Source/AutoSave/Private/AutoSaveSubsystem.cpp
Normal file
@ -0,0 +1,307 @@
|
||||
#include "AutoSaveSubsystem.h"
|
||||
|
||||
#include "AutoSaveLog.h"
|
||||
#include "Engine/UserDefinedStruct.h"
|
||||
#include "Serialization/MemoryReader.h"
|
||||
#include "Serialization/MemoryWriter.h"
|
||||
|
||||
UAutoSaveSubsystem::UAutoSaveSubsystem(const class FObjectInitializer & ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::GetSaveStructInfosWithoutData(TArray<FSaveStructInfo>& OutSaveStructInfos) const
|
||||
{
|
||||
OutSaveStructInfos.SetNum(StructInfos.Num());
|
||||
|
||||
int32 Index = 0;
|
||||
|
||||
for (const TPair<FString, TUniquePtr<FSaveStructInfo>>& Info : StructInfos)
|
||||
{
|
||||
OutSaveStructInfos[Index].Filename = Info.Value->Filename;
|
||||
OutSaveStructInfos[Index].Struct = Info.Value->Struct;
|
||||
OutSaveStructInfos[Index].State = Info.Value->State;
|
||||
OutSaveStructInfos[Index].RefConut = Info.Value->RefConut;
|
||||
OutSaveStructInfos[Index].LastRefConut = Info.Value->LastRefConut;
|
||||
OutSaveStructInfos[Index].LastSaveTime = Info.Value->LastSaveTime;
|
||||
|
||||
++Index;
|
||||
}
|
||||
}
|
||||
|
||||
FSaveStruct * UAutoSaveSubsystem::AddSaveStructRef(const FString& Filename, UScriptStruct * ScriptStruct)
|
||||
{
|
||||
const bool bIsCppStruct = ScriptStruct->IsChildOf(FSaveStruct::StaticStruct());
|
||||
const bool bIsBlueprintStruct = ScriptStruct->GetClass() == UUserDefinedStruct::StaticClass();
|
||||
|
||||
if (!bIsCppStruct && !bIsBlueprintStruct)
|
||||
return nullptr;
|
||||
|
||||
if (StructInfos.Contains(Filename))
|
||||
{
|
||||
FSaveStructInfo* StructInfo = StructInfos[Filename].Get();
|
||||
|
||||
// Increase the reference count of SaveStruct by one, and then decrease it accordingly in UAutoSaveSubsystem::RemoveSaveStructRef
|
||||
StructInfo->RefConut++;
|
||||
|
||||
return (FSaveStruct*)StructInfo->Data.GetData();
|
||||
}
|
||||
|
||||
TUniquePtr<FSaveStructInfo> NewStructInfo(new FSaveStructInfo());
|
||||
|
||||
if (FPaths::FileExists(Filename))
|
||||
{
|
||||
NewStructInfo->Filename = Filename;
|
||||
NewStructInfo->Struct = ScriptStruct;
|
||||
NewStructInfo->Data.SetNumUninitialized(ScriptStruct->GetStructureSize());
|
||||
NewStructInfo->State = ESaveStructState::Preload;
|
||||
NewStructInfo->RefConut = 1;
|
||||
NewStructInfo->LastRefConut = 0;
|
||||
NewStructInfo->LastSaveTime = FDateTime::Now();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if the target is writable
|
||||
if (!FFileHelper::SaveStringToFile(TEXT(""), *Filename))
|
||||
return nullptr;
|
||||
|
||||
NewStructInfo->Filename = Filename;
|
||||
NewStructInfo->Struct = ScriptStruct;
|
||||
NewStructInfo->Data.SetNumUninitialized(ScriptStruct->GetStructureSize());
|
||||
ScriptStruct->InitializeStruct(NewStructInfo->Data.GetData());
|
||||
NewStructInfo->State = ESaveStructState::Idle;
|
||||
NewStructInfo->RefConut = 1;
|
||||
NewStructInfo->LastRefConut = 0;
|
||||
NewStructInfo->LastSaveTime = FDateTime::Now();
|
||||
}
|
||||
|
||||
ScriptStructHooker.Add(Filename, ScriptStruct);
|
||||
|
||||
StructInfos.Add(Filename, nullptr);
|
||||
StructInfos[Filename].Reset(NewStructInfo.Release());
|
||||
|
||||
return (FSaveStruct*)StructInfos[Filename]->Data.GetData();
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::RemoveSaveStructRef(const FString& Filename)
|
||||
{
|
||||
if (StructInfos.Contains(Filename))
|
||||
{
|
||||
if (StructInfos[Filename]->RefConut > 0)
|
||||
{
|
||||
// Decrement the reference count of SaveStruct by one, and increase it accordingly in UAutoSaveSubsystem::AddSaveStructRef
|
||||
StructInfos[Filename]->RefConut--;
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogAutoSave, Warning, TEXT("Save Struct '%s' reference is negative, But was tried to remove the reference."), *Filename);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogAutoSave, Warning, TEXT("Save Struct '%s' is invalid, But was tried to remove the reference."), *Filename);
|
||||
}
|
||||
}
|
||||
|
||||
UAutoSaveSubsystem::FStructLoadOrSaveTask::FStructLoadOrSaveTask(FSaveStructInfo * InStructInfoPtr)
|
||||
: StructInfoPtr(InStructInfoPtr)
|
||||
{
|
||||
StructInfoPtr->LastRefConut = StructInfoPtr->RefConut;
|
||||
StructInfoPtr->LastSaveTime = FDateTime::Now();
|
||||
|
||||
switch (StructInfoPtr->State)
|
||||
{
|
||||
case ESaveStructState::Preload:
|
||||
StructInfoPtr->State = ESaveStructState::Loading;
|
||||
DataCopy.SetNumUninitialized(StructInfoPtr->Data.Num());
|
||||
break;
|
||||
|
||||
case ESaveStructState::Idle:
|
||||
StructInfoPtr->State = ESaveStructState::Saving;
|
||||
DataCopy = StructInfoPtr->Data;
|
||||
break;
|
||||
|
||||
default: checkNoEntry()
|
||||
}
|
||||
}
|
||||
|
||||
UAutoSaveSubsystem::FStructLoadOrSaveTask::~FStructLoadOrSaveTask()
|
||||
{
|
||||
switch (StructInfoPtr->State)
|
||||
{
|
||||
case ESaveStructState::Loading:
|
||||
StructInfoPtr->State = ESaveStructState::Idle;
|
||||
StructInfoPtr->Data = DataCopy;
|
||||
break;
|
||||
|
||||
case ESaveStructState::Saving:
|
||||
StructInfoPtr->State = ESaveStructState::Idle;
|
||||
break;
|
||||
|
||||
default: checkNoEntry()
|
||||
}
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::FStructLoadOrSaveTask::DoWork()
|
||||
{
|
||||
switch (StructInfoPtr->State)
|
||||
{
|
||||
case ESaveStructState::Loading:
|
||||
LoadWork();
|
||||
break;
|
||||
|
||||
case ESaveStructState::Saving:
|
||||
SaveWork();
|
||||
break;
|
||||
|
||||
default: checkNoEntry()
|
||||
}
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::FStructLoadOrSaveTask::LoadWork()
|
||||
{
|
||||
TArray<uint8> DataBuffer;
|
||||
|
||||
const bool bSuccessful = FFileHelper::LoadFileToArray(DataBuffer, *StructInfoPtr->Filename);
|
||||
|
||||
check(bSuccessful);
|
||||
|
||||
FMemoryReader MemoryReader(DataBuffer);
|
||||
|
||||
UScriptStruct* Struct = StructInfoPtr->Struct;
|
||||
|
||||
check(Struct);
|
||||
|
||||
Struct->SerializeItem(MemoryReader, DataCopy.GetData(), nullptr);
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::FStructLoadOrSaveTask::SaveWork()
|
||||
{
|
||||
TArray<uint8> DataBuffer;
|
||||
FMemoryWriter MemoryWriter(DataBuffer);
|
||||
|
||||
UScriptStruct* Struct = StructInfoPtr->Struct;
|
||||
|
||||
check(Struct);
|
||||
|
||||
Struct->SerializeItem(MemoryWriter, DataCopy.GetData(), nullptr);
|
||||
|
||||
const bool bSuccessful = FFileHelper::SaveArrayToFile(DataBuffer, *StructInfoPtr->Filename);
|
||||
|
||||
check(bSuccessful);
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::HandleTaskStart()
|
||||
{
|
||||
const FDateTime NowTime = FDateTime::Now();
|
||||
|
||||
TFunction<FSaveStructInfo*(void)> FindPreHandleStruct = [&]() -> FSaveStructInfo*
|
||||
{
|
||||
FSaveStructInfo* PreHandleStruct = nullptr;
|
||||
|
||||
for (const TPair<FString, TUniquePtr<FSaveStructInfo>>& Info : StructInfos)
|
||||
{
|
||||
check(Info.Value);
|
||||
|
||||
if (Info.Value->State == ESaveStructState::Preload)
|
||||
{
|
||||
PreHandleStruct = Info.Value.Get();
|
||||
break;
|
||||
}
|
||||
|
||||
if (Info.Value->State != ESaveStructState::Idle) continue;
|
||||
|
||||
if (Info.Value->RefConut == 0)
|
||||
{
|
||||
PreHandleStruct = Info.Value.Get();
|
||||
break;
|
||||
}
|
||||
|
||||
const bool bTimeout = PreHandleStruct
|
||||
? PreHandleStruct->LastSaveTime > Info.Value->LastSaveTime
|
||||
: NowTime - Info.Value->LastSaveTime > SaveWaitTime;
|
||||
|
||||
if (bTimeout)
|
||||
{
|
||||
PreHandleStruct = Info.Value.Get();
|
||||
}
|
||||
}
|
||||
|
||||
return PreHandleStruct;
|
||||
};
|
||||
|
||||
if (MaxThreadNum <= 0)
|
||||
{
|
||||
FSaveStructInfo* PreHandleStruct = FindPreHandleStruct();
|
||||
|
||||
if (PreHandleStruct)
|
||||
{
|
||||
FAsyncTask<FStructLoadOrSaveTask> Task(PreHandleStruct);
|
||||
Task.StartSynchronousTask();
|
||||
}
|
||||
}
|
||||
|
||||
for (TUniquePtr<FAsyncTask<FStructLoadOrSaveTask>>& Task : TaskThreads)
|
||||
{
|
||||
if (Task) continue;
|
||||
|
||||
FSaveStructInfo* PreHandleStruct = FindPreHandleStruct();
|
||||
|
||||
if (!PreHandleStruct) break;
|
||||
|
||||
Task.Reset(new FAsyncTask<FStructLoadOrSaveTask>(PreHandleStruct));
|
||||
Task->StartBackgroundTask();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::HandleTaskDone()
|
||||
{
|
||||
for (TUniquePtr<FAsyncTask<FStructLoadOrSaveTask>>& Task : TaskThreads)
|
||||
{
|
||||
if (!Task) continue;
|
||||
if (!Task->IsDone()) continue;
|
||||
Task = nullptr;
|
||||
}
|
||||
|
||||
TArray<FString> StructToRemove;
|
||||
|
||||
for (const TPair<FString, TUniquePtr<FSaveStructInfo>>& Info : StructInfos)
|
||||
{
|
||||
check(Info.Value);
|
||||
|
||||
if (Info.Value->State != ESaveStructState::Idle) continue;
|
||||
|
||||
if (Info.Value->RefConut <= 0 && Info.Value->LastRefConut <= 0)
|
||||
{
|
||||
StructToRemove.Add(Info.Value->Filename);
|
||||
}
|
||||
}
|
||||
|
||||
for (const FString& Filename : StructToRemove)
|
||||
{
|
||||
StructInfos.Remove(Filename);
|
||||
ScriptStructHooker.Remove(Filename);
|
||||
}
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::Initialize(FSubsystemCollectionBase & Collection)
|
||||
{
|
||||
if (MaxThreadNum > 0)
|
||||
TaskThreads.SetNum(MaxThreadNum);
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::Deinitialize()
|
||||
{
|
||||
for (TUniquePtr<FAsyncTask<FStructLoadOrSaveTask>>& Task : TaskThreads)
|
||||
{
|
||||
if (!Task) continue;
|
||||
Task->EnsureCompletion();
|
||||
Task = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void UAutoSaveSubsystem::Tick(float DeltaTime)
|
||||
{
|
||||
HandleTaskDone();
|
||||
HandleTaskStart();
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
#include "Blueprint/AutoSaveBlueprintLibrary.h"
|
||||
|
||||
#include "AutoSaveSubsystem.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
bool UAutoSaveBlueprintLibrary::AddSaveStructRef(UObject * WorldContextObject, const FString & Filename, UScriptStruct * ScriptStruct)
|
||||
{
|
||||
UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(WorldContextObject);
|
||||
|
||||
if (!GameInstance) return false;
|
||||
|
||||
UAutoSaveSubsystem* AutoSaveSubsystem = GameInstance->GetSubsystem<UAutoSaveSubsystem>();
|
||||
|
||||
if (!AutoSaveSubsystem) return false;
|
||||
|
||||
return AutoSaveSubsystem->AddSaveStructRef(Filename, ScriptStruct) != nullptr;
|
||||
}
|
||||
|
||||
void UAutoSaveBlueprintLibrary::RemoveSaveStructRef(UObject * WorldContextObject, const FString & Filename)
|
||||
{
|
||||
UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(WorldContextObject);
|
||||
|
||||
if (!GameInstance) return;
|
||||
|
||||
UAutoSaveSubsystem* AutoSaveSubsystem = GameInstance->GetSubsystem<UAutoSaveSubsystem>();
|
||||
|
||||
if (!AutoSaveSubsystem) return;
|
||||
|
||||
AutoSaveSubsystem->RemoveSaveStructRef(Filename);
|
||||
}
|
1
Source/AutoSave/Private/SaveStruct.cpp
Normal file
1
Source/AutoSave/Private/SaveStruct.cpp
Normal file
@ -0,0 +1 @@
|
||||
#include "SaveStruct.h"
|
5
Source/AutoSave/Public/AutoSaveLog.h
Normal file
5
Source/AutoSave/Public/AutoSaveLog.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "Logging/LogMacros.h"
|
||||
|
||||
AUTOSAVE_API DECLARE_LOG_CATEGORY_EXTERN(LogAutoSave, Log, All);
|
121
Source/AutoSave/Public/AutoSaveSubsystem.h
Normal file
121
Source/AutoSave/Public/AutoSaveSubsystem.h
Normal file
@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "AutoSaveSubsystem.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct AUTOSAVE_API FSaveStruct { GENERATED_BODY() };
|
||||
|
||||
UENUM(BlueprintType, Category = "AutoSave")
|
||||
enum class ESaveStructState : uint8
|
||||
{
|
||||
Preload,
|
||||
Loading,
|
||||
Idle,
|
||||
Saving,
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FSaveStructInfo
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "AutoSave")
|
||||
FString Filename;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "AutoSave")
|
||||
UScriptStruct* Struct;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "AutoSave")
|
||||
ESaveStructState State;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "AutoSave")
|
||||
int32 RefConut;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "AutoSave")
|
||||
int32 LastRefConut;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "AutoSave")
|
||||
FDateTime LastSaveTime;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<uint8> Data;
|
||||
// FSaveStruct* Data;
|
||||
|
||||
};
|
||||
|
||||
UCLASS(Config = Engine, DefaultConfig)
|
||||
class AUTOSAVE_API UAutoSaveSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
friend struct FSaveStructPtr;
|
||||
|
||||
public:
|
||||
|
||||
UAutoSaveSubsystem(const class FObjectInitializer& ObjectInitializer);
|
||||
|
||||
UPROPERTY(Config, EditAnywhere, Category = "AutoSave")
|
||||
int32 MaxThreadNum = 4;
|
||||
|
||||
UPROPERTY(Config, EditAnywhere, Category = "AutoSave")
|
||||
FTimespan SaveWaitTime = FTimespan(ETimespan::MaxTicks);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "AutoSave")
|
||||
void GetSaveStructInfosWithoutData(TArray<FSaveStructInfo>& OutSaveStructInfos) const;
|
||||
|
||||
FSaveStruct* AddSaveStructRef(const FString& Filename, UScriptStruct* ScriptStruct);
|
||||
|
||||
void RemoveSaveStructRef(const FString& Filename);
|
||||
|
||||
private:
|
||||
|
||||
UPROPERTY()
|
||||
TMap<FString, UScriptStruct*> ScriptStructHooker;
|
||||
|
||||
TMap<FString, TUniquePtr<FSaveStructInfo>> StructInfos;
|
||||
|
||||
class FStructLoadOrSaveTask : public FNonAbandonableTask
|
||||
{
|
||||
friend class FAsyncTask<FStructLoadOrSaveTask>;
|
||||
|
||||
TArray<uint8> DataCopy;
|
||||
|
||||
FSaveStructInfo* StructInfoPtr;
|
||||
|
||||
FStructLoadOrSaveTask(FSaveStructInfo* InStructInfoPtr);
|
||||
|
||||
~FStructLoadOrSaveTask();
|
||||
|
||||
void DoWork();
|
||||
|
||||
void LoadWork();
|
||||
|
||||
void SaveWork();
|
||||
|
||||
FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FStructLoadOrSaveTask, STATGROUP_ThreadPoolAsyncTasks); }
|
||||
|
||||
};
|
||||
|
||||
TArray<TUniquePtr<FAsyncTask<FStructLoadOrSaveTask>>> TaskThreads;
|
||||
|
||||
void HandleTaskStart();
|
||||
|
||||
void HandleTaskDone();
|
||||
|
||||
private:
|
||||
|
||||
//~ Begin USubsystem Interface
|
||||
virtual bool ShouldCreateSubsystem(UObject* Outer) const override { return true; }
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
//~ End USubsystem Interface
|
||||
|
||||
//~ Begin FTickableGameObject Interface
|
||||
virtual void Tick(float DeltaTime) override;
|
||||
virtual bool IsTickable() const override { return !IsTemplate(); }
|
||||
virtual TStatId GetStatId() const override { return GetStatID(); }
|
||||
//~ End FTickableGameObject Interface
|
||||
|
||||
};
|
20
Source/AutoSave/Public/Blueprint/AutoSaveBlueprintLibrary.h
Normal file
20
Source/AutoSave/Public/Blueprint/AutoSaveBlueprintLibrary.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "AutoSaveBlueprintLibrary.generated.h"
|
||||
|
||||
UCLASS()
|
||||
class AUTOSAVE_API UAutoSaveBlueprintLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "AutoSave", meta = (WorldContext = "WorldContextObject"))
|
||||
static bool AddSaveStructRef(UObject* WorldContextObject, const FString& Filename, UScriptStruct* ScriptStruct);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "AutoSave", meta = (WorldContext = "WorldContextObject"))
|
||||
static void RemoveSaveStructRef(UObject* WorldContextObject, const FString& Filename);
|
||||
|
||||
};
|
10
Source/AutoSave/Public/SaveStruct.h
Normal file
10
Source/AutoSave/Public/SaveStruct.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SaveStruct.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct AUTOSAVE_API FSaveStructPtr
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
Reference in New Issue
Block a user