Skip to content

Commit d9bb565

Browse files
committed
TGS Test Merge (PR 11660)
2 parents 8bb73bc + 9ad556b commit d9bb565

12 files changed

Lines changed: 363 additions & 0 deletions

File tree

code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,15 @@
9292

9393
/// From /mob/living/carbon/human/hud_set_holocard()
9494
#define COMSIG_HUMAN_TRIAGE_CARD_UPDATED "human_triage_card_updated"
95+
96+
/// From /datum/surgery_step/remove_larva/success()
97+
#define COMSIG_HUMAN_REMOVED_A_LARVA "human_removed_a_larva"
98+
99+
/// From /datum/species/proc/attempt_rock_paper_scissors
100+
#define COMSIG_HUMAN_WON_RPS "human_won_rps"
101+
102+
/// From /mob/living/carbon/human/help_shake_act
103+
#define COMSIG_HUMAN_HELPING_UP "human_helping_up"
104+
105+
/// /datum/game_mode/colonialmarines/declare_completion
106+
#define COMSIG_HUMAN_FINISHED_ROUND "human_finished_round"

code/__DEFINES/dcs/signals/atom/mob/living/signals_xeno.dm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,6 @@
9898

9999
/// From /mob/living/carbon/human/death(cause, gibbed)
100100
#define COMSIG_XENO_REVEAL_TACMAP "xeno_reveal_tacmap"
101+
102+
/// From /mob/living/carbon/xenomorph/larva/proc/chest_burst
103+
#define COMSIG_XENO_BURSTED "xeno_burst"

code/__DEFINES/subsystems.dm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
#define SS_INIT_CMTV 84
115115
#define SS_INIT_TOPIC 83
116116
#define SS_INIT_LOBBYART 82
117+
#define SS_INIT_ACHIEVEMENTS 27
117118
#define SS_INIT_INFLUXDRIVER 28
118119
#define SS_INIT_GARBAGE 24
119120
#define SS_INIT_EVENTS 23.5
@@ -211,6 +212,7 @@
211212
#define SS_PRIORITY_PING 10
212213
#define SS_PRIORITY_INFLUXMCSTATS 9
213214
#define SS_PRIORITY_INFLUXSTATS 8
215+
#define SS_PRIORITY_ACHIEVEMENTS 7
214216
#define SS_PRIORITY_PLAYTIME 5
215217
#define SS_PRIORITY_PERFLOGGING 4
216218
#define SS_PRIORITY_GARBAGE 2
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/// The base URL for the achievements API endpoint
2+
/datum/config_entry/string/achievements_api_url
3+
protection = CONFIG_ENTRY_HIDDEN
4+
5+
/// The API key for authenticating with the achievements service
6+
/datum/config_entry/string/achievements_api_key
7+
protection = CONFIG_ENTRY_HIDDEN|CONFIG_ENTRY_LOCKED
8+
9+
/// Additional parameter passed to the backend service
10+
/datum/config_entry/string/achievements_instance
11+
protection= CONFIG_ENTRY_LOCKED
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
/// Subsystem that queries an external API for player achievements
2+
SUBSYSTEM_DEF(achievements)
3+
name = "Achievements"
4+
flags = SS_NO_FIRE
5+
init_order = SS_INIT_ACHIEVEMENTS
6+
priority = SS_PRIORITY_ACHIEVEMENTS
7+
runlevels = RUNLEVELS_DEFAULT|RUNLEVEL_LOBBY
8+
9+
/// Associated list of key -> achievement datum
10+
var/list/datum/achievement/all_achivements = list()
11+
12+
/datum/controller/subsystem/achievements/Initialize()
13+
var/api_url = CONFIG_GET(string/achievements_api_url)
14+
var/api_key = CONFIG_GET(string/achievements_api_key)
15+
var/instance = CONFIG_GET(string/achievements_instance)
16+
17+
if(!api_url || !api_key || !instance)
18+
return SS_INIT_NO_NEED
19+
20+
RegisterSignal(SSdcs, COMSIG_GLOB_CLIENT_LOGGED_IN, PROC_REF(on_client_login))
21+
22+
for(var/datum/achievement/achievement_type as anything in subtypesof(/datum/achievement))
23+
if(!length(achievement_type::key))
24+
continue
25+
26+
var/datum/achievement/achievement = new achievement_type()
27+
all_achivements[achievement.key] = achievement
28+
29+
for(var/client/client in GLOB.clients)
30+
query_achievements_for_player(client.ckey)
31+
32+
return SS_INIT_SUCCESS
33+
34+
/// Signal handler for when a client logs in
35+
/datum/controller/subsystem/achievements/proc/on_client_login(source, client/new_client)
36+
SIGNAL_HANDLER
37+
38+
if(!new_client?.ckey)
39+
return
40+
41+
INVOKE_ASYNC(src, PROC_REF(query_achievements_for_player), new_client.ckey)
42+
43+
/// Queries the achievements API for a specific player
44+
/datum/controller/subsystem/achievements/proc/query_achievements_for_player(ckey)
45+
set waitfor = FALSE
46+
47+
var/api_url = CONFIG_GET(string/achievements_api_url)
48+
var/api_key = CONFIG_GET(string/achievements_api_key)
49+
var/instance = CONFIG_GET(string/achievements_instance)
50+
51+
if(!api_url)
52+
return
53+
54+
var/list/headers = list()
55+
headers["Content-Type"] = "application/json"
56+
headers["Accept"] = "application/json"
57+
if(api_key)
58+
headers["Authorization"] = "Bearer [api_key]"
59+
60+
var/datum/http_request/request = new
61+
request.prepare(RUSTG_HTTP_METHOD_GET, "[api_url]?ckey=[url_encode(ckey)]&instance=[instance]", "", headers)
62+
request.execute_blocking()
63+
64+
var/datum/http_response/response = request.into_response()
65+
66+
if(response.errored)
67+
log_debug("Achievements API error for [ckey]: [response.error]")
68+
return
69+
70+
if(response.status_code != 200)
71+
log_debug("Achievements API returned status [response.status_code] for [ckey]")
72+
return
73+
74+
handle_achievements_response(ckey, response.body)
75+
76+
/// Handle the API response and grant achievements to the player
77+
/datum/controller/subsystem/achievements/proc/handle_achievements_response(ckey, body)
78+
var/list/data
79+
try
80+
data = json_decode(body)
81+
catch
82+
log_debug("Achievements API returned invalid JSON for [ckey]")
83+
return
84+
85+
if(!islist(data))
86+
return
87+
88+
var/list/achievements = data["achievements"]
89+
if(!islist(achievements) || !length(achievements))
90+
return
91+
92+
var/client/client = GLOB.directory[ckey]
93+
if(!client)
94+
return
95+
96+
new /datum/achievement_manager(client, achievements)
97+
98+
/// Notify a client that they've unlocked an achievement
99+
/datum/controller/subsystem/achievements/proc/notify_achievement_unlocked(client/target, name, description)
100+
if(!target)
101+
return
102+
103+
to_chat(target, SPAN_BOLDNOTICE("Achievement Unlocked: [name]"))
104+
if(description)
105+
to_chat(target, SPAN_NOTICE("[description]"))
106+
107+
/// Report that a player has completed an achievement to the backend
108+
/datum/controller/subsystem/achievements/proc/report_achievement_completed(ckey, datum/achievement/achievement)
109+
set waitfor = FALSE
110+
111+
if(!ckey || !istype(achievement))
112+
return
113+
114+
var/api_url = CONFIG_GET(string/achievements_api_url)
115+
var/api_key = CONFIG_GET(string/achievements_api_key)
116+
var/instance = CONFIG_GET(string/achievements_instance)
117+
118+
if(!api_url)
119+
return
120+
121+
var/list/headers = list()
122+
headers["Content-Type"] = "application/json"
123+
headers["Accept"] = "application/json"
124+
if(api_key)
125+
headers["Authorization"] = "Bearer [api_key]"
126+
127+
var/list/request_body = list(
128+
"ckey" = ckey,
129+
"achievement" = achievement.key,
130+
"instance" = instance,
131+
)
132+
133+
var/datum/http_request/request = new
134+
request.prepare(RUSTG_HTTP_METHOD_POST, api_url, json_encode(request_body), headers)
135+
request.execute_blocking()
136+
137+
var/datum/http_response/response = request.into_response()
138+
139+
if(response.errored)
140+
log_debug("Achievements API error reporting completion for [ckey] ([achievement.key]): [response.error]")
141+
return
142+
143+
if(response.status_code < 200 || response.status_code >= 300)
144+
log_debug("Achievements API returned status [response.status_code] when reporting completion for [ckey] ([achievement.key])")
145+
return
146+
147+
log_debug("Successfully reported achievement '[achievement.key]' completion for [ckey]")
148+
149+
/datum/achievement_manager
150+
/// The client that we're managing
151+
var/client/owner
152+
153+
/// The mob we're tracking, to compare on logged in events
154+
var/mob/current_mob
155+
156+
var/list/datum/achievement/achievements = list()
157+
158+
/datum/achievement_manager/New(client/owner, list/achievements)
159+
if(!owner)
160+
qdel(src)
161+
return
162+
163+
src.owner = owner
164+
handle_mob_logged_in(new_mob = owner.mob)
165+
owner.achievement_manager = src
166+
167+
for(var/achievement in achievements)
168+
var/datum/achievement/achievement_datum = SSachievements.all_achivements[achievement]
169+
if(!istype(achievement_datum))
170+
continue
171+
172+
var/datum/achievement/my_achievement = new achievement_datum.type
173+
src.achievements += my_achievement
174+
175+
achievement_datum.register_mob(current_mob)
176+
177+
RegisterSignal(owner, COMSIG_PARENT_PREQDELETED, PROC_REF(handle_client_qdeleting))
178+
RegisterSignal(owner, COMSIG_CLIENT_MOB_LOGGED_IN, PROC_REF(handle_mob_logged_in))
179+
180+
/datum/achievement_manager/proc/handle_client_qdeleting()
181+
SIGNAL_HANDLER
182+
183+
for(var/datum/achievement/achievement as anything in achievements)
184+
achievement.unregister_mob(current_mob)
185+
186+
achievements = null
187+
owner.achievement_manager = null
188+
189+
/datum/achievement_manager/proc/handle_mob_logged_in(client/_owner, mob/new_mob)
190+
SIGNAL_HANDLER
191+
192+
if(current_mob == new_mob)
193+
return
194+
195+
for(var/datum/achievement/achievement as anything in achievements)
196+
achievement.unregister_mob(current_mob)
197+
achievement.register_mob(new_mob)
198+
199+
current_mob = new_mob
200+
201+
/datum/achievement
202+
/// The name of this achievement in the backend
203+
var/key
204+
205+
/// What signal should be tracked on the mob to trigger completion
206+
var/listen_signal
207+
208+
/// When the achievement has been accomplished and should be reported to the backend
209+
/datum/achievement/proc/achieved(client/achiever)
210+
SSachievements.report_achievement_completed(achiever.ckey, src)
211+
212+
unregister_mob(achiever.mob)
213+
214+
achiever.achievement_manager.achievements -= src
215+
216+
/datum/achievement/proc/register_mob(mob/registered)
217+
if(!key || !listen_signal)
218+
return
219+
220+
RegisterSignal(registered, listen_signal, PROC_REF(handle_achieved))
221+
222+
/datum/achievement/proc/unregister_mob(mob/unregistered)
223+
if(!key || !listen_signal)
224+
return
225+
226+
UnregisterSignal(unregistered, listen_signal)
227+
228+
/datum/achievement/proc/handle_achieved(mob/achieved)
229+
achieved(achieved.client)
230+
231+
/datum/achievement/squad
232+
var/squad
233+
234+
/datum/achievement/squad/register_mob(mob/registered)
235+
RegisterSignal(registered, COMSIG_SET_SQUAD, PROC_REF(handle_set_squad))
236+
237+
/datum/achievement/squad/unregister_mob(mob/unregistered)
238+
UnregisterSignal(unregistered, COMSIG_SET_SQUAD)
239+
240+
/datum/achievement/squad/proc/handle_set_squad(mob/living/carbon/human/set_squad)
241+
SIGNAL_HANDLER
242+
243+
if(set_squad.assigned_squad && set_squad.assigned_squad.name == src.squad)
244+
achieved(set_squad.client)
245+
246+
/datum/achievement/squad/alpha
247+
key = "join_as_alpha"
248+
squad = SQUAD_MARINE_1
249+
250+
/datum/achievement/squad/bravo
251+
key = "join_as_bravo"
252+
squad = SQUAD_MARINE_2
253+
254+
/datum/achievement/squad/charlie
255+
key = "join_as_charlie"
256+
squad = SQUAD_MARINE_3
257+
258+
/datum/achievement/squad/delta
259+
key = "join_as_delta"
260+
squad = SQUAD_MARINE_4
261+
262+
/datum/achievement/role
263+
var/role
264+
265+
/datum/achievement/role/register_mob(mob/registered)
266+
RegisterSignal(registered, COMSIG_POST_SPAWN_UPDATE, PROC_REF(handle_post_spawn))
267+
268+
/datum/achievement/role/unregister_mob(mob/unregistered)
269+
UnregisterSignal(unregistered, COMSIG_POST_SPAWN_UPDATE)
270+
271+
/datum/achievement/role/proc/handle_post_spawn(mob/post_spawned)
272+
SIGNAL_HANDLER
273+
274+
if(post_spawned.job == role)
275+
achieved(post_spawned.client)
276+
277+
/datum/achievement/role/doctor
278+
key = "join_as_doctor"
279+
role = JOB_DOCTOR
280+
281+
/datum/achievement/role/mp
282+
key = "join_as_mp"
283+
role = JOB_POLICE
284+
285+
/datum/achievement/remove_larva
286+
key = "surgery_remove_larva"
287+
listen_signal = COMSIG_HUMAN_REMOVED_A_LARVA
288+
289+
/datum/achievement/win_at_rps
290+
key = "win_at_rps"
291+
listen_signal = COMSIG_HUMAN_WON_RPS
292+
293+
/datum/achievement/help_ally_up
294+
key = "help_ally_up"
295+
listen_signal = COMSIG_HUMAN_HELPING_UP
296+
297+
/datum/achievement/enlist_as_marine
298+
key = "enlist_as_marine"
299+
300+
/datum/achievement/enlist_as_marine/register_mob(mob/registered)
301+
RegisterSignal(registered, COMSIG_POST_SPAWN_UPDATE, PROC_REF(handle_post_spawn))
302+
303+
/datum/achievement/enlist_as_marine/unregister_mob(mob/unregistered)
304+
UnregisterSignal(unregistered, COMSIG_POST_SPAWN_UPDATE)
305+
306+
/datum/achievement/enlist_as_marine/proc/handle_post_spawn(mob/post_spawned)
307+
if(post_spawned.faction == FACTION_MARINE)
308+
achieved(post_spawned.client)
309+
310+
/datum/achievement/complete_round_marine
311+
key = "complete_round_marine"
312+
listen_signal = COMSIG_HUMAN_FINISHED_ROUND
313+
314+
/datum/achievement/burst_as_xeno
315+
key = "burst_as_xeno"
316+
listen_signal = COMSIG_XENO_BURSTED

code/game/gamemodes/colonialmarines/colonialmarines.dm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,9 @@
728728

729729
GLOB.round_statistics.log_round_statistics()
730730

731+
for(var/mob/mob as anything in GLOB.alive_human_list)
732+
SEND_SIGNAL(mob, COMSIG_HUMAN_FINISHED_ROUND)
733+
731734
calculate_end_statistics()
732735
show_end_statistics(end_icon)
733736

code/modules/client/client_defines.dm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,6 @@
133133

134134
/// If this client has any windows scaling applied
135135
var/window_scaling
136+
137+
/// Controller for mob achievements. If present, the mob is eligible for achievements that are unattained
138+
var/datum/achievement_manager/achievement_manager

0 commit comments

Comments
 (0)