Import initial du projet Survival (UE 5.8)

Boucle de jeu complète : récolte, inventaire, consommation, stats de survie,
mort et respawn. Gameplay en C++, Blueprints réservés au câblage d'assets.

Assets binaires (.uasset, .umap, textures, audio) suivis via Git LFS.
Binaries/, Intermediate/, Saved/ et DerivedDataCache/ sont ignorés :
régénérés au build, ils pèsent 3 Go pour 1,3 Go de contenu utile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:29:07 +02:00
commit ed7c7fd991
2626 changed files with 14833 additions and 0 deletions
@@ -0,0 +1,8 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll
@@ -0,0 +1,27 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "Discord Rich Presence",
"Description": "Affiche le jeu dans le statut Discord (Rich Presence) via l'IPC local de Discord. Aucune dependance externe. Plugin made by Mathew Simon.",
"Category": "Online",
"CreatedBy": "Mathew Simon",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": false,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "DiscordRichPresence",
"Type": "Runtime",
"LoadingPhase": "Default",
"PlatformAllowList": [
"Win64"
]
}
]
}
+64
View File
@@ -0,0 +1,64 @@
# Discord Rich Presence
**Plugin made by Mathew Simon.**
Affiche le jeu dans le statut Discord quand on le lance. Aucune DLL ni SDK externe :
le plugin parle directement au named pipe local de Discord (`\\.\pipe\discord-ipc-N`).
Windows uniquement (`PlatformAllowList: Win64`).
## 1. Cote Discord
1. https://discord.com/developers/applications > **New Application**
2. Le **nom de l'application** = ce que Discord affichera comme nom du jeu ("joue a XXX").
3. Copier l'**Application ID** (= Client ID) sur la page *General Information*.
4. Optionnel, pour les images : *Rich Presence > Art Assets* > uploader une image et lui
donner un nom. Ce nom est la valeur a mettre dans `LargeImageKey` / `SmallImageKey`.
(Les uploads mettent quelques minutes a se propager.)
## 2. Cote Unreal
**Edit > Project Settings > Plugins > Discord Rich Presence**
| Reglage | Role |
|---|---|
| `Enabled` | Active/desactive tout le plugin |
| `Client Id` | L'Application ID copie a l'etape 1 |
| `Enable In Editor` | Presence active aussi en PIE (pratique pour tester) |
| `Default Activity` | Ce qui est affiche des le lancement |
| `Min Update Interval Seconds` | Throttle des envois (Discord limite a 5 / 20 s, ne pas descendre sous 4) |
| `Reconnect Interval Seconds` | Frequence des tentatives quand Discord n'est pas lance |
C'est tout : le sous-systeme demarre avec la GameInstance, se connecte, envoie
`Default Activity`, et se reconnecte tout seul si Discord est ferme puis relance.
## 3. Changer la presence en cours de jeu
En Blueprint, noeud **Get Discord Rich Presence**, puis :
- `Set Details And State` — les deux lignes de texte sous le nom du jeu
- `Set Activity` — remplace tout (textes + images + party)
- `Set Party` — le compteur "(2 sur 4)"
- `Reset Elapsed Time` — repart de zero sur le chrono de temps de jeu
- `Clear Activity` — retire le jeu du statut
- `Is Connected To Discord` — etat de la connexion
En C++ :
```cpp
#include "DiscordRichPresenceSubsystem.h"
if (UDiscordRichPresenceSubsystem* Discord = UDiscordRichPresenceSubsystem::Get(this))
{
Discord->SetDetailsAndState(TEXT("Foret du Nord"), TEXT("Jour 12 - En vie"));
}
```
## Notes
- Discord ignore les textes de **moins de 2 caracteres**, ils sont donc omis a l'envoi.
- Le chrono de temps de jeu demarre a l'`Initialize` du sous-systeme.
- Les changements sont accumules et envoyes au plus une fois par `Min Update Interval
Seconds` : appeler `SetDetailsAndState` a chaque frame ne spamme pas Discord.
- Si Discord n'est pas lance, il ne se passe rien (pas d'erreur, pas de blocage) et le
plugin retente periodiquement.
Binary file not shown.
@@ -0,0 +1,24 @@
// Discord Rich Presence - plugin made by Mathew Simon.
using UnrealBuildTool;
public class DiscordRichPresence : ModuleRules
{
public DiscordRichPresence(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"DeveloperSettings",
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"Json",
});
}
}
@@ -0,0 +1,387 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#include "DiscordIpcConnection.h"
#include "DiscordRichPresenceModule.h"
#include "DiscordRichPresenceTypes.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Policies/CondensedJsonPrintPolicy.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#if PLATFORM_WINDOWS
#include "Windows/AllowWindowsPlatformTypes.h"
#include <Windows.h>
#include "Windows/HideWindowsPlatformTypes.h"
#endif
namespace DiscordIpc
{
enum EOpCode : int32
{
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
/** Discord rejette les chaines de moins de 2 caracteres. */
static bool IsUsableText(const FString& Text)
{
return Text.Len() >= 2;
}
}
FDiscordIpcConnection::~FDiscordIpcConnection()
{
Disconnect();
}
bool FDiscordIpcConnection::Connect(const FString& InClientId)
{
#if PLATFORM_WINDOWS
Disconnect();
if (InClientId.IsEmpty())
{
return false;
}
ClientId = InClientId;
// Discord numerote ses pipes : la 1ere instance prend 0, la suivante 1, etc.
for (int32 PipeIndex = 0; PipeIndex < 10; ++PipeIndex)
{
const FString PipeName = FString::Printf(TEXT("\\\\.\\pipe\\discord-ipc-%d"), PipeIndex);
HANDLE Handle = ::CreateFileW(
*PipeName,
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (Handle == INVALID_HANDLE_VALUE)
{
continue;
}
PipeHandle = Handle;
bReady = false;
const FString Handshake = FString::Printf(TEXT("{\"v\":1,\"client_id\":\"%s\"}"), *ClientId);
if (!WriteFrame(DiscordIpc::Handshake, Handshake))
{
// Pipe occupe par un autre process : on tente le suivant.
Disconnect();
continue;
}
UE_LOG(LogDiscordRichPresence, Log, TEXT("Connecte a Discord sur %s"), *PipeName);
return true;
}
return false;
#else
return false;
#endif
}
void FDiscordIpcConnection::Disconnect()
{
#if PLATFORM_WINDOWS
if (PipeHandle)
{
::CloseHandle(static_cast<HANDLE>(PipeHandle));
PipeHandle = nullptr;
}
#endif
bReady = false;
}
bool FDiscordIpcConnection::WriteFrame(int32 OpCode, const FString& Payload)
{
#if PLATFORM_WINDOWS
if (!PipeHandle)
{
return false;
}
FTCHARToUTF8 Utf8Payload(*Payload);
const int32 PayloadLength = Utf8Payload.Length();
TArray<uint8> Frame;
Frame.Reserve(8 + PayloadLength);
Frame.Append(reinterpret_cast<const uint8*>(&OpCode), 4);
Frame.Append(reinterpret_cast<const uint8*>(&PayloadLength), 4);
Frame.Append(reinterpret_cast<const uint8*>(Utf8Payload.Get()), PayloadLength);
DWORD BytesWritten = 0;
const BOOL bOk = ::WriteFile(
static_cast<HANDLE>(PipeHandle),
Frame.GetData(),
static_cast<DWORD>(Frame.Num()),
&BytesWritten,
nullptr);
if (!bOk || BytesWritten != static_cast<DWORD>(Frame.Num()))
{
UE_LOG(LogDiscordRichPresence, Warning, TEXT("Ecriture vers Discord echouee, deconnexion."));
Disconnect();
return false;
}
return true;
#else
return false;
#endif
}
bool FDiscordIpcConnection::ReadExactly(void* Buffer, int32 NumBytes)
{
#if PLATFORM_WINDOWS
uint8* Cursor = static_cast<uint8*>(Buffer);
int32 Remaining = NumBytes;
while (Remaining > 0)
{
DWORD BytesRead = 0;
const BOOL bOk = ::ReadFile(
static_cast<HANDLE>(PipeHandle),
Cursor,
static_cast<DWORD>(Remaining),
&BytesRead,
nullptr);
if (!bOk || BytesRead == 0)
{
return false;
}
Cursor += BytesRead;
Remaining -= static_cast<int32>(BytesRead);
}
return true;
#else
return false;
#endif
}
bool FDiscordIpcConnection::Poll()
{
#if PLATFORM_WINDOWS
if (!PipeHandle)
{
return false;
}
// On ne lit que ce qui est deja arrive : ReadFile sur ce pipe est bloquant.
for (;;)
{
DWORD BytesAvailable = 0;
if (!::PeekNamedPipe(static_cast<HANDLE>(PipeHandle), nullptr, 0, nullptr, &BytesAvailable, nullptr))
{
UE_LOG(LogDiscordRichPresence, Log, TEXT("Discord a ferme la connexion."));
Disconnect();
return false;
}
if (BytesAvailable < 8)
{
return true;
}
uint8 Header[8];
if (!ReadExactly(Header, 8))
{
Disconnect();
return false;
}
int32 OpCode = 0;
int32 Length = 0;
FMemory::Memcpy(&OpCode, Header, 4);
FMemory::Memcpy(&Length, Header + 4, 4);
if (Length < 0 || Length > 64 * 1024)
{
UE_LOG(LogDiscordRichPresence, Warning, TEXT("Frame Discord invalide (taille %d)."), Length);
Disconnect();
return false;
}
FString Payload;
if (Length > 0)
{
TArray<uint8> PayloadBytes;
PayloadBytes.SetNumUninitialized(Length + 1);
if (!ReadExactly(PayloadBytes.GetData(), Length))
{
Disconnect();
return false;
}
PayloadBytes[Length] = 0;
Payload = UTF8_TO_TCHAR(reinterpret_cast<const ANSICHAR*>(PayloadBytes.GetData()));
}
HandleIncomingFrame(OpCode, Payload);
if (!PipeHandle)
{
return false;
}
}
#else
return false;
#endif
}
void FDiscordIpcConnection::HandleIncomingFrame(int32 OpCode, const FString& Payload)
{
switch (OpCode)
{
case DiscordIpc::Ping:
WriteFrame(DiscordIpc::Pong, Payload);
return;
case DiscordIpc::Close:
UE_LOG(LogDiscordRichPresence, Log, TEXT("Discord a demande la fermeture : %s"), *Payload);
Disconnect();
return;
case DiscordIpc::Frame:
break;
default:
return;
}
TSharedPtr<FJsonObject> Root;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Payload);
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
{
return;
}
FString Event;
if (!Root->TryGetStringField(TEXT("evt"), Event))
{
return;
}
if (Event == TEXT("READY"))
{
bReady = true;
UE_LOG(LogDiscordRichPresence, Log, TEXT("Rich Presence prete."));
}
else if (Event == TEXT("ERROR"))
{
UE_LOG(LogDiscordRichPresence, Warning, TEXT("Discord a renvoye une erreur : %s"), *Payload);
}
}
FString FDiscordIpcConnection::BuildActivityPayload(const FDiscordRichPresenceActivity* Activity, int64 StartUnixTime)
{
const TSharedRef<FJsonObject> Args = MakeShared<FJsonObject>();
Args->SetNumberField(TEXT("pid"), static_cast<double>(FPlatformProcess::GetCurrentProcessId()));
if (Activity == nullptr)
{
// activity: null => Discord retire le jeu du statut.
Args->SetField(TEXT("activity"), MakeShared<FJsonValueNull>());
}
else
{
const TSharedRef<FJsonObject> ActivityObject = MakeShared<FJsonObject>();
if (DiscordIpc::IsUsableText(Activity->Details))
{
ActivityObject->SetStringField(TEXT("details"), Activity->Details);
}
if (DiscordIpc::IsUsableText(Activity->State))
{
ActivityObject->SetStringField(TEXT("state"), Activity->State);
}
if (Activity->bShowElapsedTime && StartUnixTime > 0)
{
const TSharedRef<FJsonObject> Timestamps = MakeShared<FJsonObject>();
Timestamps->SetNumberField(TEXT("start"), static_cast<double>(StartUnixTime));
ActivityObject->SetObjectField(TEXT("timestamps"), Timestamps);
}
const bool bHasAssets = !Activity->LargeImageKey.IsEmpty() || !Activity->SmallImageKey.IsEmpty();
if (bHasAssets)
{
const TSharedRef<FJsonObject> Assets = MakeShared<FJsonObject>();
if (!Activity->LargeImageKey.IsEmpty())
{
Assets->SetStringField(TEXT("large_image"), Activity->LargeImageKey);
if (DiscordIpc::IsUsableText(Activity->LargeImageText))
{
Assets->SetStringField(TEXT("large_text"), Activity->LargeImageText);
}
}
if (!Activity->SmallImageKey.IsEmpty())
{
Assets->SetStringField(TEXT("small_image"), Activity->SmallImageKey);
if (DiscordIpc::IsUsableText(Activity->SmallImageText))
{
Assets->SetStringField(TEXT("small_text"), Activity->SmallImageText);
}
}
ActivityObject->SetObjectField(TEXT("assets"), Assets);
}
if (Activity->PartySize > 0)
{
TArray<TSharedPtr<FJsonValue>> Sizes;
Sizes.Add(MakeShared<FJsonValueNumber>(Activity->PartySize));
Sizes.Add(MakeShared<FJsonValueNumber>(FMath::Max(Activity->PartyMax, Activity->PartySize)));
const TSharedRef<FJsonObject> Party = MakeShared<FJsonObject>();
Party->SetArrayField(TEXT("size"), Sizes);
ActivityObject->SetObjectField(TEXT("party"), Party);
}
Args->SetObjectField(TEXT("activity"), ActivityObject);
}
const TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetStringField(TEXT("cmd"), TEXT("SET_ACTIVITY"));
Root->SetStringField(TEXT("nonce"), FString::Printf(TEXT("%d"), ++NonceCounter));
Root->SetObjectField(TEXT("args"), Args);
FString Output;
const TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> Writer =
TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Output);
FJsonSerializer::Serialize(Root, Writer);
return Output;
}
bool FDiscordIpcConnection::SendActivity(const FDiscordRichPresenceActivity& Activity, int64 StartUnixTime)
{
if (!IsConnected() || !bReady)
{
return false;
}
return WriteFrame(DiscordIpc::Frame, BuildActivityPayload(&Activity, StartUnixTime));
}
bool FDiscordIpcConnection::ClearActivity()
{
if (!IsConnected() || !bReady)
{
return false;
}
return WriteFrame(DiscordIpc::Frame, BuildActivityPayload(nullptr, 0));
}
@@ -0,0 +1,58 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#pragma once
#include "CoreMinimal.h"
struct FDiscordRichPresenceActivity;
/**
* Connexion brute au client Discord local.
*
* Discord ecoute sur un named pipe Windows : \\.\pipe\discord-ipc-N (N de 0 a 9,
* un par instance de Discord lancee). Le protocole est une suite de frames :
*
* [int32 opcode LE][int32 taille LE][payload JSON UTF-8]
*
* opcode 0 = HANDSHAKE, 1 = FRAME (commandes), 2 = CLOSE, 3 = PING, 4 = PONG.
*
* Aucune DLL ni SDK externe n'est necessaire.
*/
class FDiscordIpcConnection
{
public:
~FDiscordIpcConnection();
/** Ouvre le pipe et envoie le handshake. Ne bloque pas si Discord n'est pas lance. */
bool Connect(const FString& InClientId);
/** Ferme proprement le pipe. */
void Disconnect();
bool IsConnected() const { return PipeHandle != nullptr; }
/** Vrai une fois que Discord a repondu READY : avant ca, inutile d'envoyer une activite. */
bool IsReady() const { return bReady; }
/** Consomme les frames entrantes (READY, erreurs, PING). Retourne false si le pipe est mort. */
bool Poll();
/** @param StartUnixTime timestamp de debut de partie, ou 0 pour ne pas afficher le temps ecoule. */
bool SendActivity(const FDiscordRichPresenceActivity& Activity, int64 StartUnixTime);
/** Retire le jeu du statut Discord sans fermer la connexion. */
bool ClearActivity();
private:
bool WriteFrame(int32 OpCode, const FString& Payload);
bool ReadExactly(void* Buffer, int32 NumBytes);
void HandleIncomingFrame(int32 OpCode, const FString& Payload);
FString BuildActivityPayload(const FDiscordRichPresenceActivity* Activity, int64 StartUnixTime);
/** HANDLE Win32, garde en void* pour ne pas trainer <Windows.h> dans le header. */
void* PipeHandle = nullptr;
FString ClientId;
bool bReady = false;
int32 NonceCounter = 0;
};
@@ -0,0 +1,20 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#include "DiscordRichPresenceModule.h"
DEFINE_LOG_CATEGORY(LogDiscordRichPresence);
#define LOCTEXT_NAMESPACE "FDiscordRichPresenceModule"
void FDiscordRichPresenceModule::StartupModule()
{
UE_LOG(LogDiscordRichPresence, Log, TEXT("Discord Rich Presence plugin made by Mathew Simon."));
}
void FDiscordRichPresenceModule::ShutdownModule()
{
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FDiscordRichPresenceModule, DiscordRichPresence)
@@ -0,0 +1,11 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#include "DiscordRichPresenceSettings.h"
UDiscordRichPresenceSettings::UDiscordRichPresenceSettings()
{
DefaultActivity.Details = TEXT("En jeu");
DefaultActivity.State = TEXT("Exploration");
DefaultActivity.LargeImageKey = TEXT("logo");
DefaultActivity.bShowElapsedTime = true;
}
@@ -0,0 +1,205 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#include "DiscordRichPresenceSubsystem.h"
#include "DiscordIpcConnection.h"
#include "DiscordRichPresenceModule.h"
#include "DiscordRichPresenceSettings.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
bool UDiscordRichPresenceSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
if (!Super::ShouldCreateSubsystem(Outer))
{
return false;
}
// Rien a afficher pour un serveur dedie ou un commandlet.
if (IsRunningDedicatedServer() || IsRunningCommandlet())
{
return false;
}
#if PLATFORM_WINDOWS
return true;
#else
return false;
#endif
}
void UDiscordRichPresenceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
const UDiscordRichPresenceSettings* Settings = GetDefault<UDiscordRichPresenceSettings>();
if (!Settings->bEnabled)
{
return;
}
if (Settings->ClientId.IsEmpty())
{
UE_LOG(LogDiscordRichPresence, Warning,
TEXT("Aucun Client ID : renseigne-le dans Project Settings > Plugins > Discord Rich Presence."));
return;
}
#if WITH_EDITOR
if (GIsEditor && !Settings->bEnableInEditor)
{
return;
}
#endif
CurrentActivity = Settings->DefaultActivity;
StartUnixTime = FDateTime::UtcNow().ToUnixTimestamp();
bActivityDirty = true;
bActivityCleared = false;
Connection = MakeShared<FDiscordIpcConnection>();
TryConnect();
// 1 s suffit largement : les envois reels sont throttles plus bas.
TickerHandle = FTSTicker::GetCoreTicker().AddTicker(
FTickerDelegate::CreateUObject(this, &UDiscordRichPresenceSubsystem::Tick), 1.0f);
}
void UDiscordRichPresenceSubsystem::Deinitialize()
{
if (TickerHandle.IsValid())
{
FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle);
TickerHandle.Reset();
}
if (Connection.IsValid())
{
Connection->ClearActivity();
Connection->Disconnect();
Connection.Reset();
}
Super::Deinitialize();
}
UDiscordRichPresenceSubsystem* UDiscordRichPresenceSubsystem::Get(const UObject* WorldContextObject)
{
if (const UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr)
{
if (UGameInstance* GameInstance = World->GetGameInstance())
{
return GameInstance->GetSubsystem<UDiscordRichPresenceSubsystem>();
}
}
return nullptr;
}
bool UDiscordRichPresenceSubsystem::IsConnectedToDiscord() const
{
return Connection.IsValid() && Connection->IsConnected() && Connection->IsReady();
}
void UDiscordRichPresenceSubsystem::TryConnect()
{
if (!Connection.IsValid())
{
return;
}
LastConnectAttemptTime = FPlatformTime::Seconds();
if (Connection->Connect(GetDefault<UDiscordRichPresenceSettings>()->ClientId))
{
// La presence sera poussee des que Discord aura repondu READY.
bActivityDirty = !bActivityCleared;
}
}
void UDiscordRichPresenceSubsystem::SetActivity(const FDiscordRichPresenceActivity& NewActivity)
{
if (CurrentActivity == NewActivity && !bActivityCleared)
{
return;
}
CurrentActivity = NewActivity;
bActivityCleared = false;
bActivityDirty = true;
}
void UDiscordRichPresenceSubsystem::SetDetailsAndState(const FString& Details, const FString& State)
{
FDiscordRichPresenceActivity Updated = CurrentActivity;
Updated.Details = Details;
Updated.State = State;
SetActivity(Updated);
}
void UDiscordRichPresenceSubsystem::SetParty(int32 PartySize, int32 PartyMax)
{
FDiscordRichPresenceActivity Updated = CurrentActivity;
Updated.PartySize = FMath::Max(0, PartySize);
Updated.PartyMax = FMath::Max(0, PartyMax);
SetActivity(Updated);
}
void UDiscordRichPresenceSubsystem::ResetElapsedTime()
{
StartUnixTime = FDateTime::UtcNow().ToUnixTimestamp();
bActivityDirty = true;
}
void UDiscordRichPresenceSubsystem::ClearActivity()
{
bActivityCleared = true;
bActivityDirty = false;
if (Connection.IsValid())
{
Connection->ClearActivity();
}
}
bool UDiscordRichPresenceSubsystem::Tick(float DeltaTime)
{
if (!Connection.IsValid())
{
return true;
}
const UDiscordRichPresenceSettings* Settings = GetDefault<UDiscordRichPresenceSettings>();
const double Now = FPlatformTime::Seconds();
if (!Connection->IsConnected())
{
// Discord n'est pas lance, ou a ete ferme : on retente calmement.
if (Now - LastConnectAttemptTime >= Settings->ReconnectIntervalSeconds)
{
TryConnect();
}
return true;
}
// Traite READY / PING / erreurs. Peut fermer la connexion.
if (!Connection->Poll() || !Connection->IsReady())
{
return true;
}
if (bActivityDirty
&& !bActivityCleared
&& (LastSendTime == 0.0 || Now - LastSendTime >= Settings->MinUpdateIntervalSeconds))
{
if (Connection->SendActivity(CurrentActivity, StartUnixTime))
{
bActivityDirty = false;
LastSendTime = Now;
}
}
return true;
}
@@ -0,0 +1,15 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
DECLARE_LOG_CATEGORY_EXTERN(LogDiscordRichPresence, Log, All);
class FDiscordRichPresenceModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
@@ -0,0 +1,58 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "DiscordRichPresenceTypes.h"
#include "DiscordRichPresenceSettings.generated.h"
/**
* Discord Rich Presence - plugin made by Mathew Simon.
*
* Reglages ecrits dans Config/DefaultGame.ini.
*/
UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Discord Rich Presence"))
class DISCORDRICHPRESENCE_API UDiscordRichPresenceSettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
UDiscordRichPresenceSettings();
virtual FName GetCategoryName() const override { return TEXT("Plugins"); }
/** Auteur du plugin. Champ informatif, non modifiable. */
UPROPERTY(VisibleAnywhere, Transient, Category = "About", meta = (DisplayName = "Plugin"))
FString About = TEXT("Discord Rich Presence - plugin made by Mathew Simon");
/** Active la presence Discord. */
UPROPERTY(config, EditAnywhere, Category = "General")
bool bEnabled = true;
/**
* Application ID (= Client ID) recupere sur https://discord.com/developers/applications
* Le nom de l'application est ce que Discord affichera comme nom du jeu.
*/
UPROPERTY(config, EditAnywhere, Category = "General")
FString ClientId;
/** Active aussi la presence quand on joue dans l'editeur (PIE). Pratique pour tester. */
UPROPERTY(config, EditAnywhere, Category = "General")
bool bEnableInEditor = true;
/** Presence envoyee des le lancement du jeu. Modifiable ensuite en Blueprint / C++. */
UPROPERTY(config, EditAnywhere, Category = "Presence")
FDiscordRichPresenceActivity DefaultActivity;
/**
* Delai minimum entre deux envois a Discord.
* Discord limite a 5 mises a jour par 20 s : ne pas descendre sous 4 s.
*/
UPROPERTY(config, EditAnywhere, Category = "Advanced", meta = (ClampMin = "4.0", UIMin = "4.0"))
float MinUpdateIntervalSeconds = 4.0f;
/** Delai entre deux tentatives de connexion quand Discord n'est pas lance. */
UPROPERTY(config, EditAnywhere, Category = "Advanced", meta = (ClampMin = "1.0", UIMin = "1.0"))
float ReconnectIntervalSeconds = 15.0f;
};
@@ -0,0 +1,83 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#pragma once
#include "CoreMinimal.h"
#include "Containers/Ticker.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "DiscordRichPresenceTypes.h"
#include "DiscordRichPresenceSubsystem.generated.h"
class FDiscordIpcConnection;
/**
* Demarre tout seul avec le jeu : se connecte a Discord, envoie l'activite par defaut
* definie dans Project Settings > Plugins > Discord Rich Presence, puis se reconnecte
* automatiquement si Discord est ferme/relance en cours de partie.
*/
UCLASS()
class DISCORDRICHPRESENCE_API UDiscordRichPresenceSubsystem : public UGameInstanceSubsystem
{
GENERATED_BODY()
public:
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
/** Raccourci Blueprint : Get Discord Rich Presence. */
UFUNCTION(BlueprintPure, Category = "Discord", meta = (WorldContext = "WorldContextObject", DisplayName = "Get Discord Rich Presence"))
static UDiscordRichPresenceSubsystem* Get(const UObject* WorldContextObject);
/** Remplace toute la presence affichee. */
UFUNCTION(BlueprintCallable, Category = "Discord")
void SetActivity(const FDiscordRichPresenceActivity& NewActivity);
/** Cas courant : ne change que les deux lignes de texte, garde les images. */
UFUNCTION(BlueprintCallable, Category = "Discord")
void SetDetailsAndState(const FString& Details, const FString& State);
/** Met a jour le compteur "(x sur y)". PartySize a 0 masque le compteur. */
UFUNCTION(BlueprintCallable, Category = "Discord")
void SetParty(int32 PartySize, int32 PartyMax);
/** Repart de zero sur le chrono "temps ecoule" (nouvelle partie, respawn, etc.). */
UFUNCTION(BlueprintCallable, Category = "Discord")
void ResetElapsedTime();
/** Retire le jeu du statut Discord (la connexion reste ouverte). */
UFUNCTION(BlueprintCallable, Category = "Discord")
void ClearActivity();
/** Activite en cours (ou en attente d'envoi). */
UFUNCTION(BlueprintPure, Category = "Discord")
FDiscordRichPresenceActivity GetCurrentActivity() const { return CurrentActivity; }
/** Vrai quand Discord est connecte et a valide le handshake. */
UFUNCTION(BlueprintPure, Category = "Discord")
bool IsConnectedToDiscord() const;
private:
bool Tick(float DeltaTime);
void TryConnect();
// TSharedPtr et pas TUniquePtr : le deleter est efface a la construction, donc le code
// genere par UHT n'a pas besoin de la definition complete de FDiscordIpcConnection.
TSharedPtr<FDiscordIpcConnection> Connection;
FTSTicker::FDelegateHandle TickerHandle;
UPROPERTY()
FDiscordRichPresenceActivity CurrentActivity;
/** Timestamp Unix du debut de partie, pour le compteur de temps de jeu. */
int64 StartUnixTime = 0;
/** Une modification est en attente d'envoi (throttle par MinUpdateIntervalSeconds). */
bool bActivityDirty = false;
/** L'activite a ete effacee volontairement : ne pas la re-envoyer au prochain tick. */
bool bActivityCleared = false;
double LastSendTime = 0.0;
double LastConnectAttemptTime = 0.0;
};
@@ -0,0 +1,70 @@
// Discord Rich Presence - plugin made by Mathew Simon.
#pragma once
#include "CoreMinimal.h"
#include "DiscordRichPresenceTypes.generated.h"
/**
* Ce que Discord affiche sous le nom du jeu.
* Le nom du jeu lui-meme vient du Developer Portal (nom de l'application liee au Client ID).
*/
USTRUCT(BlueprintType)
struct DISCORDRICHPRESENCE_API FDiscordRichPresenceActivity
{
GENERATED_BODY()
/** 1ere ligne sous le nom du jeu. Discord ignore les textes de moins de 2 caracteres. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord")
FString Details;
/** 2eme ligne sous le nom du jeu. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord")
FString State;
/** Cle de l'image (Developer Portal > Rich Presence > Art Assets), ou une URL https directe. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord|Images")
FString LargeImageKey;
/** Tooltip au survol de la grande image. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord|Images")
FString LargeImageText;
/** Petite icone en bas a droite de la grande image. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord|Images")
FString SmallImageKey;
/** Tooltip au survol de la petite image. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord|Images")
FString SmallImageText;
/** Affiche "XX:XX ecoulees" — c'est le compteur de temps de jeu. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord")
bool bShowElapsedTime = true;
/** Nombre de joueurs actuels (affiche "(2 sur 4)"). Laisser a 0 pour ne rien afficher. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord|Party", meta = (ClampMin = "0"))
int32 PartySize = 0;
/** Nombre de joueurs max. Ignore si PartySize vaut 0. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Discord|Party", meta = (ClampMin = "0"))
int32 PartyMax = 0;
bool operator==(const FDiscordRichPresenceActivity& Other) const
{
return Details == Other.Details
&& State == Other.State
&& LargeImageKey == Other.LargeImageKey
&& LargeImageText == Other.LargeImageText
&& SmallImageKey == Other.SmallImageKey
&& SmallImageText == Other.SmallImageText
&& bShowElapsedTime == Other.bShowElapsedTime
&& PartySize == Other.PartySize
&& PartyMax == Other.PartyMax;
}
bool operator!=(const FDiscordRichPresenceActivity& Other) const
{
return !(*this == Other);
}
};