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