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