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:
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
+387
@@ -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;
|
||||
};
|
||||
+20
@@ -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)
|
||||
+11
@@ -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;
|
||||
}
|
||||
+205
@@ -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;
|
||||
}
|
||||
+15
@@ -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;
|
||||
};
|
||||
+58
@@ -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;
|
||||
};
|
||||
+83
@@ -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;
|
||||
};
|
||||
+70
@@ -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);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user