1#include <base/system.h>
2
3#include <engine/client.h>
4#include <engine/discord.h>
5
6#if defined(CONF_DISCORD)
7#include <discord_game_sdk.h>
8
9typedef enum EDiscordResult(DISCORD_API *FDiscordCreate)(DiscordVersion, struct DiscordCreateParams *, struct IDiscordCore **);
10
11#if defined(CONF_DISCORD_DYNAMIC)
12#include <dlfcn.h>
13FDiscordCreate GetDiscordCreate()
14{
15 void *pSdk = dlopen("discord_game_sdk.so", RTLD_NOW);
16 if(!pSdk)
17 {
18 return nullptr;
19 }
20 return (FDiscordCreate)dlsym(pSdk, "DiscordCreate");
21}
22#else
23FDiscordCreate GetDiscordCreate()
24{
25 return DiscordCreate;
26}
27#endif
28
29class CDiscord : public IDiscord
30{
31 DiscordActivity m_Activity;
32 bool m_UpdateActivity = false;
33 int64_t m_LastActivityUpdate = 0;
34
35 IDiscordCore *m_pCore;
36 IDiscordActivityEvents m_ActivityEvents;
37 IDiscordActivityManager *m_pActivityManager;
38
39public:
40 bool Init(FDiscordCreate pfnDiscordCreate)
41 {
42 m_pCore = 0;
43 mem_zero(&m_ActivityEvents, sizeof(m_ActivityEvents));
44
45 m_ActivityEvents.on_activity_join = &CDiscord::OnActivityJoin;
46 m_pActivityManager = 0;
47
48 DiscordCreateParams Params;
49 DiscordCreateParamsSetDefault(&Params);
50
51 Params.client_id = 752165779117441075; // DDNet
52 Params.flags = EDiscordCreateFlags::DiscordCreateFlags_NoRequireDiscord;
53 Params.event_data = this;
54 Params.activity_events = &m_ActivityEvents;
55
56 int Error = pfnDiscordCreate(DISCORD_VERSION, &Params, &m_pCore);
57 if(Error != DiscordResult_Ok)
58 {
59 dbg_msg("discord", "error initializing discord instance, error=%d", Error);
60 return true;
61 }
62
63 m_pActivityManager = m_pCore->get_activity_manager(m_pCore);
64
65 // which application to launch when joining activity
66 m_pActivityManager->register_command(m_pActivityManager, CONNECTLINK_DOUBLE_SLASH);
67 m_pActivityManager->register_steam(m_pActivityManager, 412220); // steam id
68
69 ClearGameInfo();
70
71 return false;
72 }
73
74 void Update() override
75 {
76 // update every 5 seconds, rate limit is 5 updates per 20 seconds
77 if(m_UpdateActivity && time_get() > m_LastActivityUpdate + time_freq() * 5)
78 {
79 m_UpdateActivity = false;
80 m_LastActivityUpdate = time_get();
81
82 m_pActivityManager->update_activity(m_pActivityManager, &m_Activity, 0, 0);
83 }
84
85 m_pCore->run_callbacks(m_pCore);
86 }
87
88 void ClearGameInfo() override
89 {
90 mem_zero(&m_Activity, sizeof(DiscordActivity));
91
92 str_copy(m_Activity.assets.large_image, "ddnet_logo", sizeof(m_Activity.assets.large_image));
93 str_copy(m_Activity.assets.large_text, "DDNet logo", sizeof(m_Activity.assets.large_text));
94 m_Activity.timestamps.start = time_timestamp();
95 str_copy(m_Activity.details, "Offline", sizeof(m_Activity.details));
96 m_Activity.instance = false;
97
98 m_UpdateActivity = true;
99 }
100
101 void SetGameInfo(const CServerInfo &ServerInfo, const char *pMapName, bool Registered) override
102 {
103 mem_zero(&m_Activity, sizeof(DiscordActivity));
104
105 str_copy(m_Activity.assets.large_image, "ddnet_logo", sizeof(m_Activity.assets.large_image));
106 str_copy(m_Activity.assets.large_text, "DDNet logo", sizeof(m_Activity.assets.large_text));
107 m_Activity.timestamps.start = time_timestamp();
108 str_copy(m_Activity.name, "Online", sizeof(m_Activity.name));
109 m_Activity.instance = true;
110
111 str_copy(m_Activity.details, ServerInfo.m_aName, sizeof(m_Activity.details));
112 str_copy(m_Activity.state, pMapName, sizeof(m_Activity.state));
113 m_Activity.party.size.current_size = ServerInfo.m_NumClients;
114 m_Activity.party.size.max_size = ServerInfo.m_MaxClients;
115 // private makes it so the game isn't public to join, but there's 'Ask to Join' button instead
116 m_Activity.party.privacy = Registered ? DiscordActivityPartyPrivacy_Public : DiscordActivityPartyPrivacy_Private;
117
118 if(!Registered)
119 {
120 // private parties have random id to not leak the server ip
121 char aPartyId[sizeof(m_Activity.party.id)];
122 secure_random_password(aPartyId, sizeof(aPartyId), 64);
123 str_copy(m_Activity.party.id, aPartyId, sizeof(m_Activity.party.id));
124 }
125 UpdateServerIp(ServerInfo);
126
127 m_UpdateActivity = true;
128 }
129
130 void UpdateServerInfo(const CServerInfo &ServerInfo, const char *pMapName) override
131 {
132 if(!m_Activity.instance)
133 return;
134
135 UpdateServerIp(ServerInfo);
136
137 str_copy(m_Activity.details, ServerInfo.m_aName, sizeof(m_Activity.details));
138 str_copy(m_Activity.state, pMapName, sizeof(m_Activity.state));
139 m_Activity.party.size.max_size = ServerInfo.m_MaxClients;
140 m_UpdateActivity = true;
141 }
142
143 void UpdatePlayerCount(int Count) override
144 {
145 if(!m_Activity.instance)
146 return;
147
148 if(m_Activity.party.size.current_size == Count)
149 return;
150
151 m_Activity.party.size.current_size = Count;
152 m_UpdateActivity = true;
153 }
154
155 void UpdateServerIp(const CServerInfo &ServerInfo)
156 {
157 if(!m_Activity.instance)
158 return;
159
160 // secret is only shared when player is joining the game, or when he's invited for private games
161 if(str_length(ServerInfo.m_aAddress) < (int)sizeof(m_Activity.secrets.join))
162 {
163 str_copy(m_Activity.secrets.join, ServerInfo.m_aAddress, sizeof(m_Activity.secrets.join));
164 }
165 else
166 {
167 char aAddr[NETADDR_MAXSTRSIZE];
168 net_addr_str(&ServerInfo.m_aAddresses[0], aAddr, sizeof(aAddr), true);
169 str_copy(m_Activity.secrets.join, aAddr, sizeof(m_Activity.secrets.join));
170 }
171
172 if(m_Activity.party.privacy == DiscordActivityPartyPrivacy_Public)
173 {
174 // id is sha256, because it didn't work with the ':' character
175 char aPartyId[SHA256_MAXSTRSIZE];
176 SHA256_DIGEST PartyIdSha256 = sha256(m_Activity.secrets.join, str_length(m_Activity.secrets.join));
177 sha256_str(PartyIdSha256, aPartyId, sizeof(aPartyId));
178 str_copy(m_Activity.party.id, aPartyId, sizeof(m_Activity.party.id));
179 }
180 }
181
182 static void DISCORD_CALLBACK OnActivityJoin(void *pEventData, const char *pSecret)
183 {
184 CDiscord *pSelf = static_cast<CDiscord *>(pEventData);
185 IClient *m_pClient = pSelf->Kernel()->RequestInterface<IClient>();
186 m_pClient->Connect(pSecret);
187 }
188};
189
190static IDiscord *CreateDiscordImpl()
191{
192 FDiscordCreate pfnDiscordCreate = GetDiscordCreate();
193 if(!pfnDiscordCreate)
194 {
195 return 0;
196 }
197 CDiscord *pDiscord = new CDiscord();
198 if(pDiscord->Init(pfnDiscordCreate))
199 {
200 delete pDiscord;
201 return 0;
202 }
203 return pDiscord;
204}
205#else
206static IDiscord *CreateDiscordImpl()
207{
208 return nullptr;
209}
210#endif
211
212class CDiscordStub : public IDiscord
213{
214 void Update() override {}
215 void ClearGameInfo() override {}
216 void SetGameInfo(const CServerInfo &ServerInfo, const char *pMapName, bool Registered) override {}
217 void UpdateServerInfo(const CServerInfo &ServerInfo, const char *pMapName) override {}
218 void UpdatePlayerCount(int Count) override {}
219};
220
221IDiscord *CreateDiscord()
222{
223 IDiscord *pDiscord = CreateDiscordImpl();
224 if(pDiscord)
225 {
226 return pDiscord;
227 }
228 return new CDiscordStub();
229}
230