diff --git a/SQL/database_changelog.md b/SQL/database_changelog.md index 1e146f4ebed..2725812f814 100644 --- a/SQL/database_changelog.md +++ b/SQL/database_changelog.md @@ -11,13 +11,27 @@ INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 33); or ```sql -INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 33); +INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 34); ``` In any query remember to add a prefix to the table names if you use one. --- +Version 5.34, godsnoknowswhen 2025, by Ghommie +Added `pda_themes_progress` as the second 'progress' subtype of 'datum/award/scores' + +```sql +CREATE TABLE `pda_themes_progress` ( + `ckey` VARCHAR(32) NOT NULL, + `progress_entry` VARCHAR(32) NOT NULL, + `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`ckey`,`progress_entry`) +) ENGINE=InnoDB; +``` + +--- + Version 5.33, 28 September 2025, by Atlanta-Ned Modifies manifest ckey column to be consistent with other ckey columns diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql index 570fb85ee81..ffebf71fa86 100644 --- a/SQL/tgstation_schema.sql +++ b/SQL/tgstation_schema.sql @@ -624,6 +624,14 @@ CREATE TABLE `fish_progress` ( PRIMARY KEY (`ckey`,`progress_entry`) ) ENGINE=InnoDB; +DROP TABLE IF EXISTS `pda_themes_progress`; +CREATE TABLE `pda_themes_progress` ( + `ckey` VARCHAR(32) NOT NULL, + `progress_entry` VARCHAR(32) NOT NULL, + `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`ckey`,`progress_entry`) +) ENGINE=InnoDB; + -- -- Table structure for table `ticket` -- diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql index 28d9fd9a7e5..af8afc4a8a2 100644 --- a/SQL/tgstation_schema_prefixed.sql +++ b/SQL/tgstation_schema_prefixed.sql @@ -616,7 +616,15 @@ CREATE TABLE `SS13_achievement_metadata` ( -- Table structure for table 'SS13_x_progress' DROP TABLE IF EXISTS `SS13_fish_progress`; -CREATE TABLE `fish_progress` ( +CREATE TABLE `SS13_fish_progress` ( + `ckey` VARCHAR(32) NOT NULL, + `progress_entry` VARCHAR(32) NOT NULL, + `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`ckey`,`progress_entry`) +) ENGINE=InnoDB; + +DROP TABLE IF EXISTS `SS13_pda_themes_progress`; +CREATE TABLE `SS13_pda_themes_progress` ( `ckey` VARCHAR(32) NOT NULL, `progress_entry` VARCHAR(32) NOT NULL, `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, diff --git a/code/__DEFINES/achievements.dm b/code/__DEFINES/achievements.dm index d026e2f795e..575efae4186 100644 --- a/code/__DEFINES/achievements.dm +++ b/code/__DEFINES/achievements.dm @@ -2,6 +2,7 @@ /// Achievements icon set #define ACHIEVEMENTS_SET 'icons/ui/achievements/achievements.dmi' +#define PDA_THEMES_PROGRESS_SET 'icons/ui/achievements/pda_themes.dmi' #define ACHIEVEMENT_DEFAULT "default" #define ACHIEVEMENT_SCORE "score" @@ -150,6 +151,9 @@ ///DB ID for all the different kinds of fish that you've caught so far. #define FISH_SCORE "Fish Score" +///Database ID for unlockable PDA themes, purely costmetic +#define PDA_THEMES_SCORE "PDA Themes" + // Tourist related achievements and scores //centcom grades (achievement) diff --git a/code/__DEFINES/dcs/signals/signals_modular_computer.dm b/code/__DEFINES/dcs/signals/signals_modular_computer.dm index dd325e41984..508c7fa439f 100644 --- a/code/__DEFINES/dcs/signals/signals_modular_computer.dm +++ b/code/__DEFINES/dcs/signals/signals_modular_computer.dm @@ -5,11 +5,11 @@ /// From /obj/item/modular_computer/proc/shutdown_computer: (loud) #define COMSIG_MODULAR_COMPUTER_SHUT_DOWN "comsig_modular_computer_shut_down" -/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_storing) +/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_storing, mob/user) #define COMSIG_MODULAR_COMPUTER_FILE_STORE "comsig_modular_computer_file_store" /// From /obj/item/modular_computer/proc/remove_file: (datum/computer_file/file_removing) #define COMSIG_MODULAR_COMPUTER_FILE_DELETE "comsig_modular_computer_file_delete" -/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_source, obj/item/modular_computer/host) +/// From /obj/item/modular_computer/proc/store_file: (datum/computer_file/file_source, obj/item/modular_computer/host, mob/user) #define COMSIG_COMPUTER_FILE_STORE "comsig_computer_file_store" /// From /obj/item/modular_computer/proc/store_file: () #define COMSIG_COMPUTER_FILE_DELETE "comsig_computer_file_delete" diff --git a/code/__DEFINES/modular_computer.dm b/code/__DEFINES/modular_computer.dm index c056f9a2a9c..dcae3bc689a 100644 --- a/code/__DEFINES/modular_computer.dm +++ b/code/__DEFINES/modular_computer.dm @@ -94,19 +94,30 @@ #define PDA_THEME_RETRO "ntOS95" #define PDA_THEME_SYNTH "ntos_synth" #define PDA_THEME_TERMINAL "ntos_terminal" +#define PDA_THEME_BIRD "neutral" ///Emagged/Syndicate NtOS (SyndiOS) PDA theme #define PDA_THEME_SYNDICATE "syndicate" -//Maintenance-loot themes +//Maintenance-loot themes: you only need to get them once for them to be available on future rounds #define PDA_THEME_CAT "ntos_cat" #define PDA_THEME_LIGHT_MODE "ntos_lightmode" #define PDA_THEME_SPOOKY "ntos_spooky" #define PDA_THEME_HACKERMAN "hackerman" #define PDA_THEME_ROULETTE "cardtable" #define PDA_THEME_ABDUCTOR "abductor" -#define PDA_THEME_BIRD "neutral" +/** + * Database IDs for unlockable themes (just maintenance-loot for now). + * Distinct from declarations for both theme and name because having to retroactively change DB IDs is bad + * So don't change the values of pre-existing IDs + */ +#define PDA_THEME_ID_CAT "ntos_cat" +#define PDA_THEME_ID_LIGHT_MODE "ntos_lightmode" +#define PDA_THEME_ID_SPOOKY "ntos_spooky" +#define PDA_THEME_ID_HACKERMAN "hackerman" +#define PDA_THEME_ID_ROULETTE "cardtable" +#define PDA_THEME_ID_ABDUCTOR "abductor" //Defines for the names of all the themes #define PDA_THEME_NTOS_NAME "NtOS" @@ -123,6 +134,7 @@ #define PDA_THEME_ABDUCTOR_NAME "Alien" #define PDA_THEME_BIRD_NAME "Bird" + ///List of PDA themes that are accessible to everyone by default. GLOBAL_LIST_INIT(default_pda_themes, list( PDA_THEME_NTOS_NAME = PDA_THEME_NTOS, @@ -149,3 +161,13 @@ GLOBAL_LIST_INIT(pda_name_to_theme, list( PDA_THEME_ABDUCTOR_NAME = PDA_THEME_ABDUCTOR, PDA_THEME_BIRD_NAME = PDA_THEME_BIRD, )) + +///List of unlockable PDA themes, keyed by their database ID. +GLOBAL_LIST_INIT(pda_id_to_name, list( + PDA_THEME_ID_CAT = PDA_THEME_CAT_NAME, + PDA_THEME_ID_LIGHT_MODE = PDA_THEME_LIGHT_MODE_NAME, + PDA_THEME_ID_SPOOKY = PDA_THEME_SPOOKY_NAME, + PDA_THEME_ID_HACKERMAN = PDA_THEME_HACKERMAN_NAME, + PDA_THEME_ID_ROULETTE = PDA_THEME_ROULETTE_NAME, + PDA_THEME_ABDUCTOR = PDA_THEME_ABDUCTOR_NAME, +)) diff --git a/code/datums/achievements/_achievement_data.dm b/code/datums/achievements/_achievement_data.dm index c8ca0abea87..9cfad0016de 100644 --- a/code/datums/achievements/_achievement_data.dm +++ b/code/datums/achievements/_achievement_data.dm @@ -60,14 +60,17 @@ award.load(src) ///Unlocks an achievement of a specific type. achievement type is a typepath to the award, user is the mob getting the award, and value is an optional value to be used for defining a score to add to the leaderboard -/datum/achievement_data/proc/unlock(achievement_type, mob/user, value = 1) +/datum/achievement_data/proc/unlock(achievement_type, mob/user, value = 1, ...) set waitfor = FALSE if(!SSachievements.achievements_enabled) return var/datum/award/award = SSachievements.awards[achievement_type] get_data(achievement_type) //Get the current status first if necessary - award.unlock(user, src, value) + var/list/unlock_args = list(user, src, value) + if(length(args) > 3) + unlock_args += args.Copy(4) + award.unlock(arglist(unlock_args)) update_static_data(user) ///Getter for the status/score of an achievement diff --git a/code/datums/achievements/_awards.dm b/code/datums/achievements/_awards.dm index 536b1fbcb99..1cd856d8636 100644 --- a/code/datums/achievements/_awards.dm +++ b/code/datums/achievements/_awards.dm @@ -72,10 +72,6 @@ /datum/award/proc/parse_value(raw_value) return default_value() -///Can be overridden for achievement specific events -/datum/award/proc/on_unlock(mob/user) - return - ///returns additional ui data for the Check Achievements menu /datum/award/proc/get_ui_data(list/award_data, datum/achievement_data/holder) return list( @@ -95,29 +91,7 @@ if(holder.data[type]) //You already unlocked it so don't bother running the unlock proc return holder.data[type] = TRUE - on_unlock(user) -/datum/award/achievement/get_metadata_row() - . = ..() - .["achievement_type"] = "achievement" - -/datum/award/achievement/get_ui_data(list/award_data, datum/achievement_data/holder) - . = ..() - .["achieve_info"] = "Unlocked by [times_achieved] players so far" - if(!SSachievements.most_unlocked_achievement) - .["achieve_tooltip"] = "No achievement has been unlocked yet. Be the first today!" - return - if(SSachievements.most_unlocked_achievement == src) - .["achieve_tooltip"] = "This is the most unlocked achievement" - return - var/percent = FLOOR(times_achieved / SSachievements.most_unlocked_achievement.times_achieved * 100, 0.01) - .["achieve_tooltip"] = "[(times_achieved && !percent) ? "Less than 0.01" : percent]% compared to the achievement unlocked by the most players: \"[SSachievements.most_unlocked_achievement.name])\"" - -/datum/award/achievement/parse_value(raw_value) - return raw_value > 0 - -/datum/award/achievement/on_unlock(mob/user) - . = ..() to_chat(user, span_greenannounce("Achievement unlocked: [name]!")) var/sound/sound_to_send = LAZYACCESS(GLOB.achievement_sounds, user.client.prefs.read_preference(/datum/preference/choiced/sound_achievement)) if(sound_to_send) @@ -140,6 +114,25 @@ GLOB.achievements_unlocked += new_report +/datum/award/achievement/get_metadata_row() + . = ..() + .["achievement_type"] = "achievement" + +/datum/award/achievement/get_ui_data(list/award_data, datum/achievement_data/holder) + . = ..() + .["achieve_info"] = "Unlocked by [times_achieved] players so far" + if(!SSachievements.most_unlocked_achievement) + .["achieve_tooltip"] = "No achievement has been unlocked yet. Be the first today!" + return + if(SSachievements.most_unlocked_achievement == src) + .["achieve_tooltip"] = "This is the most unlocked achievement" + return + var/percent = FLOOR(times_achieved / SSachievements.most_unlocked_achievement.times_achieved * 100, 0.01) + .["achieve_tooltip"] = "[(times_achieved && !percent) ? "Less than 0.01" : percent]% compared to the achievement unlocked by the most players: \"[SSachievements.most_unlocked_achievement.name])\"" + +/datum/award/achievement/parse_value(raw_value) + return raw_value > 0 + ///Scores are for leaderboarded things, such as killcount of a specific boss /datum/award/score desc = "you did it sooo many times." @@ -266,7 +259,7 @@ return FALSE return ..() -/datum/award/score/progress/unlock(mob/user, datum/achievement_data/holder, value) +/datum/award/score/progress/unlock(mob/user, datum/achievement_data/holder, value, progress_string) var/list/entries = holder.data[type] if(!value) CRASH("empty value used as argument to progress this score award.") @@ -279,6 +272,22 @@ holder.data[type] = entries entries |= value + if(!progress_string) + return + + to_chat(user, get_progress_string(progress_string)) + var/sound/sound_pref = LAZYACCESS(GLOB.achievement_sounds, user.client.prefs.read_preference(/datum/preference/choiced/sound_achievement)) + if(!sound_pref) + return + //send a copy cheevo sound but with its volume halved, since you're making a small progress and not unlocking an entire achievement. + var/sound/sound_to_send = sound(sound_pref.file, volume = sound_pref.volume / 2) + if(sound_to_send) + SEND_SOUND(user, sound_to_send) + +///Compose the string to send to the user's chat when the progress is made. This one here is generic, but you should override it really. +/datum/award/score/progress/proc/get_progress_string(progress_string) + return span_greenannounce("New progress made for [name]: [progress_string]!") + /datum/award/score/progress/load(datum/achievement_data/holder) var/list/results = ..() return validate_loaded_data(holder, results) @@ -291,8 +300,8 @@ holder.original_cached_data[type] = holder.data[type] = results if(!length(results)) return results - ///This list will be populated on validate_entries() - var/list/validated_results = list() + ///This list will be checked on validate_entries() to see if the entries are valid. + var/list/validated_results = unique_list(results) if(!validate_entries(results, validated_results)) holder.data[type] = validated_results return validated_results @@ -344,7 +353,6 @@ * ensuring that the original data and the new data aren't the same, allowing the new data will be saved. */ /datum/award/score/progress/proc/validate_entries(list/entries, list/validated_entries) - validated_entries = unique_list(entries) return length(validated_entries) == length(entries) ////Returns a list of data that we can use to make an index of contents that progress this award/score. diff --git a/code/datums/achievements/misc_scores.dm b/code/datums/achievements/misc_scores.dm index 63d8835e613..b91f46008de 100644 --- a/code/datums/achievements/misc_scores.dm +++ b/code/datums/achievements/misc_scores.dm @@ -21,74 +21,3 @@ name = "Style Score" desc = "You might not be a robot, but you were damn close." database_id = STYLE_SCORE - -/datum/award/score/progress/fish - name = "Fish Species Caught" - desc = "How many different species of fish you've caught so far. Gotta fish 'em all." - database_id = FISH_SCORE - var/list/early_entries_to_validate = list() - -/datum/award/score/progress/fish/New() - . = ..() - RegisterSignal(SSfishing, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(validate_early_joiners)) - -/datum/award/score/progress/fish/get_table() - return "fish_progress" - -/datum/award/score/progress/fish/proc/validate_early_joiners(datum/source) - for(var/client/client as anything in GLOB.clients) - var/datum/achievement_data/holder = client.persistent_client.achievements - if(!holder?.initialized) - continue - - var/list/entries = holder.data[/datum/award/score/progress/fish] - var/list_copied = FALSE - for(var/fish_id in entries) - if(SSfishing.catchable_fish[fish_id]) - continue - - //make a new list, unbound from the cached awards data, so that the score can be updated at the end of the round. - if(!list_copied) - entries = entries.Copy() - holder.data[/datum/award/score/progress/fish] = entries - list_copied = TRUE - entries -= fish_id - -/datum/award/score/progress/fish/validate_entries(list/entries, list/validated_entries) - . = ..() - if(!SSfishing.initialized) - return - for(var/fish_id in validated_entries) - if(!(SSfishing.catchable_fish[fish_id])) - validated_entries -= fish_id - . = FALSE - -/datum/award/score/progress/fish/get_progress(datum/achievement_data/holder) - var/list/data = list( - "name" = "Fishdex", - "percent" = 0, - "value_text" = "Subsystems still initializing...", - "entries" = list(), - ) - if(!SSfishing.initialized) - return data - var/list/catched_fish = holder.data[type] - var/catched_len = length(catched_fish) - var/catchable_len = length(SSfishing.catchable_fish) - data["percent"] = catched_len/catchable_len - data["value_text"] = "[catched_len] / [catchable_len]" - var/index = 1 - var/max_zeros = round(log(10, catchable_len)) - for(var/fish_id in SSfishing.catchable_fish) - var/obj/item/fish/fish = SSfishing.catchable_fish[fish_id] - var/catched = (fish_id in catched_fish) - var/entry_name = "◦[prefix_zeros_to_number(index, max_zeros)]◦ [catched ? full_capitalize(initial(fish.name)) : "??????" ]" - var/list/icon_dimensions = get_icon_dimensions(initial(fish.icon)) - data["entries"] += list(list( - "name" = entry_name, - "icon" = catched ? SSfishing.cached_fish_icons[fish] : SSfishing.cached_unknown_fish_icons[fish], - "height" = icon_dimensions["height"] * 2, - "width" = icon_dimensions["width"] * 2, - )) - index++ - return data diff --git a/code/datums/achievements/progress_scores.dm b/code/datums/achievements/progress_scores.dm new file mode 100644 index 00000000000..78de462af6f --- /dev/null +++ b/code/datums/achievements/progress_scores.dm @@ -0,0 +1,132 @@ + +/datum/award/score/progress/fish + name = "Fish Species Caught" + desc = "How many different species of fish you've caught so far. Gotta fish 'em all." + database_id = FISH_SCORE + var/list/early_entries_to_validate = list() + +/datum/award/score/progress/fish/New() + . = ..() + RegisterSignal(SSfishing, COMSIG_SUBSYSTEM_POST_INITIALIZE, PROC_REF(validate_early_joiners)) + +/datum/award/score/progress/fish/get_table() + return "fish_progress" + +/datum/award/score/progress/fish/proc/validate_early_joiners(datum/source) + for(var/client/client as anything in GLOB.clients) + var/datum/achievement_data/holder = client.persistent_client.achievements + if(!holder?.initialized) + continue + + var/list/entries = holder.data[/datum/award/score/progress/fish] + var/list_copied = FALSE + for(var/fish_id in entries) + if(SSfishing.catchable_fish[fish_id]) + continue + + //make a new list, unbound from the cached awards data, so that the score can be updated at the end of the round. + if(!list_copied) + entries = entries.Copy() + holder.data[/datum/award/score/progress/fish] = entries + list_copied = TRUE + entries -= fish_id + +/datum/award/score/progress/fish/validate_entries(list/entries, list/validated_entries) + . = ..() + if(!SSfishing.initialized) + return + for(var/fish_id in validated_entries) + if(!(SSfishing.catchable_fish[fish_id])) + validated_entries -= fish_id + . = FALSE + +/datum/award/score/progress/fish/get_progress(datum/achievement_data/holder) + var/list/data = list( + "name" = "Fishdex", + "percent" = 0, + "value_text" = "Subsystems still initializing...", + "entries" = list(), + ) + if(!SSfishing.initialized) + return data + var/list/catched_fish = holder.data[type] + var/catched_len = length(catched_fish) + var/catchable_len = length(SSfishing.catchable_fish) + data["percent"] = catched_len/catchable_len + data["value_text"] = "[catched_len] / [catchable_len]" + var/index = 1 + var/max_zeros = round(log(10, catchable_len)) + for(var/fish_id in SSfishing.catchable_fish) + var/obj/item/fish/fish = SSfishing.catchable_fish[fish_id] + var/catched = (fish_id in catched_fish) + var/entry_name = "◦[prefix_zeros_to_number(index, max_zeros)]◦ [catched ? full_capitalize(initial(fish.name)) : "??????" ]" + var/list/icon_dimensions = get_icon_dimensions(initial(fish.icon)) + data["entries"] += list(list( + "name" = entry_name, + "icon" = catched ? SSfishing.cached_fish_icons[fish] : SSfishing.cached_unknown_fish_icons[fish], + "height" = icon_dimensions["height"] * 2, + "width" = icon_dimensions["width"] * 2, + )) + index++ + return data + +/datum/award/score/progress/fish/get_progress_string(progress_string) + return span_greenannounce("This is the first time you've caught a [progress_string]!") + +/datum/award/score/progress/pda_themes + name = "Unlocked PDA Themes" + desc = "Any special PDA theme that you've installed on your PDA, which will then be added to your roundstart PDA on future rounds as well." + database_id = PDA_THEMES_SCORE + track_high_scores = FALSE //This is purely personal progress + var/list/cheevo_icons + var/list/unlockable_themes + +/datum/award/score/progress/pda_themes/New() + unlockable_themes = valid_subtypesof(/datum/computer_file/program/maintenance/theme) + + cheevo_icons = list() + for(var/datum/computer_file/program/maintenance/theme/theme as anything in unlockable_themes) + cheevo_icons[theme] = icon2base64(icon(theme::icon_file, theme::icon)) + cheevo_icons["unknown"] = icon2base64(icon(PDA_THEMES_PROGRESS_SET, "unknown")) + +/datum/award/score/progress/pda_themes/get_table() + return "pda_themes_progress" + +/datum/award/score/progress/pda_themes/get_progress_string(progress_string) + return span_greenannounce(span_tooltip("You can now select it on future rounds without having to install it again", "New PDA theme unlocked : [progress_string]!")) + +/datum/award/score/progress/pda_themes/get_progress(datum/achievement_data/holder) + var/list/data = list( + "name" = "PDA Themes", + "entries" = list(), + ) + + + var/list/unlocked_themes = holder.data[type] + var/unlocked_len = length(unlocked_themes) + var/unlockable_len = length(unlockable_themes) + data["percent"] = unlocked_len/unlockable_len + data["value_text"] = "[unlocked_len] / [unlockable_len]" + for(var/datum/computer_file/program/maintenance/theme/theme as anything in unlockable_themes) + var/unlocked = (theme::theme_id in unlocked_themes) + var/list/dimensions = get_icon_dimensions(unlocked ? theme::icon_file : PDA_THEMES_PROGRESS_SET) + var/entry_name = "[unlocked ? full_capitalize(theme::theme_name) : "??????" ]" + data["entries"] += list(list( + "name" = entry_name, + "icon" = unlocked ? cheevo_icons[theme] : cheevo_icons["unknown"], + "height" = dimensions["height"], + "width" = dimensions["width"], + )) + return data + +/datum/award/score/progress/pda_themes/validate_entries(list/entries, list/validated_entries) + . = ..() + var/static/list/valid_themes + if(!valid_themes) + valid_themes = list() + for(var/datum/computer_file/program/maintenance/theme/theme as anything in unlockable_themes) + valid_themes[theme::theme_id] = TRUE + for(var/id in validated_entries) + if(!valid_themes[id]) + validated_entries -= id + . = FALSE diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index a80bfce45dc..b08d26c19bb 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1074,11 +1074,11 @@ GLOBAL_LIST_INIT(unrecommended_builds, list( to_chat(src, announcement) ///Redirect proc that makes it easier to call the unlock achievement proc. Achievement type is the typepath to the award, user is the mob getting the award, and value is an optional variable used for leaderboard value increments -/client/proc/give_award(achievement_type, mob/user, value = 1) - return persistent_client.achievements.unlock(achievement_type, user, value) +/client/proc/give_award(achievement_type, mob/user, value = 1, ...) + return persistent_client.achievements.unlock(arglist(args)) ///Redirect proc that makes it easier to get the status of an achievement. Achievement type is the typepath to the award. -/client/proc/get_award_status(achievement_type, mob/user, value = 1) +/client/proc/get_award_status(achievement_type) return persistent_client.achievements.get_achievement_status(achievement_type) ///Gives someone hearted status for OOC, from behavior commendations diff --git a/code/modules/fishing/fishing_minigame.dm b/code/modules/fishing/fishing_minigame.dm index f45918e3309..707d9339f55 100644 --- a/code/modules/fishing/fishing_minigame.dm +++ b/code/modules/fishing/fishing_minigame.dm @@ -385,10 +385,10 @@ GLOBAL_LIST_EMPTY(fishing_challenges_by_user) if(valid_achievement_catch) var/obj/item/fish/fish_reward = reward_path - var/obj/item/fish/redirect_path = initial(fish_reward.fish_id_redirect_path) - var/fish_id = ispath(redirect_path, /obj/item/fish) ? initial(redirect_path.fish_id) : initial(fish_reward.fish_id) + var/obj/item/fish/path_to_use = initial(fish_reward.fish_id_redirect_path) || fish_reward + var/fish_id = initial(path_to_use.fish_id) if(fish_id) - user.client?.give_award(/datum/award/score/progress/fish, user, fish_id) + user.client?.give_award(/datum/award/score/progress/fish, user, fish_id, initial(path_to_use.name)) SEND_SIGNAL(user, COMSIG_MOB_COMPLETE_FISHING, src, TRUE) if(!QDELETED(src)) diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm index 5e7faa98e4f..0e26f82c657 100644 --- a/code/modules/modular_computers/computers/item/computer.dm +++ b/code/modules/modular_computers/computers/item/computer.dm @@ -966,7 +966,7 @@ return ITEM_INTERACT_SUCCESS /obj/item/modular_computer/proc/photo_act(mob/user, obj/item/photo/scanned_photo) - if(!store_file(new /datum/computer_file/picture(scanned_photo.picture))) + if(!store_file(new /datum/computer_file/picture(scanned_photo.picture), user)) balloon_alert(user, "no space!") return ITEM_INTERACT_BLOCKING balloon_alert(user, "photo scanned") diff --git a/code/modules/modular_computers/computers/item/computer_files.dm b/code/modules/modular_computers/computers/item/computer_files.dm index 62f06b131ba..027802c8cb9 100644 --- a/code/modules/modular_computers/computers/item/computer_files.dm +++ b/code/modules/modular_computers/computers/item/computer_files.dm @@ -3,8 +3,9 @@ * * Adds an already initialized file to the computer, checking if one already exists. * Returns TRUE if successfully stored, FALSE otherwise. + * user is optional: If set, the action was done by a mob/player */ -/obj/item/modular_computer/proc/store_file(datum/computer_file/file_storing) +/obj/item/modular_computer/proc/store_file(datum/computer_file/file_storing, mob/user) if(!file_storing || !istype(file_storing)) return FALSE if(!can_store_file(file_storing)) @@ -16,8 +17,8 @@ file_storing.computer = src used_capacity += file_storing.size - SEND_SIGNAL(file_storing, COMSIG_COMPUTER_FILE_STORE, src) - SEND_SIGNAL(src, COMSIG_MODULAR_COMPUTER_FILE_STORE, file_storing) + SEND_SIGNAL(file_storing, COMSIG_COMPUTER_FILE_STORE, src, user) + SEND_SIGNAL(src, COMSIG_MODULAR_COMPUTER_FILE_STORE, file_storing, user) return TRUE /** diff --git a/code/modules/modular_computers/computers/item/pda.dm b/code/modules/modular_computers/computers/item/pda.dm index 1df3722a50f..a2c83e5d6e7 100644 --- a/code/modules/modular_computers/computers/item/pda.dm +++ b/code/modules/modular_computers/computers/item/pda.dm @@ -247,6 +247,14 @@ if(new_ringtone && (new_ringtone != MESSENGER_RINGTONE_DEFAULT)) update_ringtone(new_ringtone) + var/datum/computer_file/program/themeify/theme_app = locate() in stored_files + if(theme_app) + var/list/unlocked_themes = owner_client.get_award_status(/datum/award/score/progress/pda_themes) + for(var/theme_id in unlocked_themes) + var/theme_name = GLOB.pda_id_to_name[theme_id] + if(theme_name) + LAZYOR(theme_app.imported_themes, theme_name) + var/new_theme = owner_client.prefs.read_preference(/datum/preference/choiced/pda_theme) if(new_theme) device_theme = GLOB.pda_name_to_theme[new_theme] diff --git a/code/modules/modular_computers/file_system/computer_file.dm b/code/modules/modular_computers/file_system/computer_file.dm index d719fe5bd36..768b2c29ebb 100644 --- a/code/modules/modular_computers/file_system/computer_file.dm +++ b/code/modules/modular_computers/file_system/computer_file.dm @@ -49,8 +49,8 @@ temp.filetype = filetype return temp -///Called post-installation of an application in a computer, after 'computer' var is set. -/datum/computer_file/proc/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +///Called post-installation of an application in a computer, after 'computer' var is set. Remember, the user is optional +/datum/computer_file/proc/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) SIGNAL_HANDLER SHOULD_CALL_PARENT(TRUE) computer_installing.stored_files.Add(src) diff --git a/code/modules/modular_computers/file_system/picture_file.dm b/code/modules/modular_computers/file_system/picture_file.dm index 4ba71aee3d7..8bd0cfb1171 100644 --- a/code/modules/modular_computers/file_system/picture_file.dm +++ b/code/modules/modular_computers/file_system/picture_file.dm @@ -19,7 +19,7 @@ src.stored_picture = stored_picture src.picture_name = picture_name -/datum/computer_file/picture/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/picture/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() assign_path() diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm index b7b8685750c..b3ca1b0990d 100644 --- a/code/modules/modular_computers/file_system/program.dm +++ b/code/modules/modular_computers/file_system/program.dm @@ -64,7 +64,7 @@ * It probably doesn't look badass, but it's a decent way of doing it without taining the component with * oddities like this. */ -/datum/computer_file/program/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() if(isnull(circuit_comp_type) || isnull(computer.shell)) return diff --git a/code/modules/modular_computers/file_system/programs/alarm.dm b/code/modules/modular_computers/file_system/programs/alarm.dm index 86452eb45d0..1bd70887d1c 100644 --- a/code/modules/modular_computers/file_system/programs/alarm.dm +++ b/code/modules/modular_computers/file_system/programs/alarm.dm @@ -14,7 +14,7 @@ /// Station alert datum for showing alerts UI var/datum/station_alert/alert_control -/datum/computer_file/program/alarm_monitor/on_install() +/datum/computer_file/program/alarm_monitor/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() //We want to send an alarm if we're in one of the mining home areas //Or if we're on station. Otherwise, die. diff --git a/code/modules/modular_computers/file_system/programs/card.dm b/code/modules/modular_computers/file_system/programs/card.dm index e8dd5320fcb..ad5e93008a0 100644 --- a/code/modules/modular_computers/file_system/programs/card.dm +++ b/code/modules/modular_computers/file_system/programs/card.dm @@ -26,7 +26,7 @@ /// Which departments this program has access to. See region defines. var/target_dept -/datum/computer_file/program/card_mod/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/card_mod/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() ADD_TRAIT(computer_installing, TRAIT_MODPC_TWO_ID_SLOTS, REF(src)) diff --git a/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm b/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm index 8045bcb1db0..f71d985197c 100644 --- a/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm +++ b/code/modules/modular_computers/file_system/programs/chatroom/ntnrc_client.dm @@ -34,7 +34,7 @@ ///Cooldown timer between pings. COOLDOWN_DECLARE(ping_cooldown) -/datum/computer_file/program/chatclient/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/chatclient/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() if(!username) username = "DefaultUser[rand(100, 999)]" diff --git a/code/modules/modular_computers/file_system/programs/coupon.dm b/code/modules/modular_computers/file_system/programs/coupon.dm index 53bc2b6fe23..cd4e64ef712 100644 --- a/code/modules/modular_computers/file_system/programs/coupon.dm +++ b/code/modules/modular_computers/file_system/programs/coupon.dm @@ -14,7 +14,7 @@ can_run_on_flags = PROGRAM_PDA //It relies on the PDA messenger to let you know of new codes detomatix_resistance = DETOMATIX_RESIST_MALUS -/datum/computer_file/program/coupon/on_install() +/datum/computer_file/program/coupon/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() ///set the discount_coupons list, which means SSmodular_computers will now begin to periodically produce new coupon codes. LAZYINITLIST(SSmodular_computers.discount_coupons) diff --git a/code/modules/modular_computers/file_system/programs/file_browser.dm b/code/modules/modular_computers/file_system/programs/file_browser.dm index 5c5d29d6672..7e0d0679b9d 100644 --- a/code/modules/modular_computers/file_system/programs/file_browser.dm +++ b/code/modules/modular_computers/file_system/programs/file_browser.dm @@ -71,7 +71,7 @@ if(!computer.can_store_file(F)) return FALSE var/datum/computer_file/C = F.clone(FALSE) - computer.store_file(C) + computer.store_file(C, usr) return TRUE if("PRG_togglesilence") var/datum/computer_file/program/binary = computer.find_file_by_name(params["name"]) diff --git a/code/modules/modular_computers/file_system/programs/frontier.dm b/code/modules/modular_computers/file_system/programs/frontier.dm index 34bcbca8027..7d270784c11 100644 --- a/code/modules/modular_computers/file_system/programs/frontier.dm +++ b/code/modules/modular_computers/file_system/programs/frontier.dm @@ -18,7 +18,7 @@ /// The file under consideration. var/datum/computer_file/data/ordnance/selected_file -/datum/computer_file/program/scipaper_program/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/scipaper_program/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() paper_to_be = new if(!CONFIG_GET(flag/no_default_techweb_link) && !linked_techweb) diff --git a/code/modules/modular_computers/file_system/programs/mafia_ntos.dm b/code/modules/modular_computers/file_system/programs/mafia_ntos.dm index 790f276ac67..ca9edd38ff3 100644 --- a/code/modules/modular_computers/file_system/programs/mafia_ntos.dm +++ b/code/modules/modular_computers/file_system/programs/mafia_ntos.dm @@ -9,7 +9,7 @@ program_icon = "user-secret" alert_able = TRUE -/datum/computer_file/program/mafia/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/mafia/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() RegisterSignal(SSdcs, COMSIG_MAFIA_GAME_START, PROC_REF(on_game_start)) diff --git a/code/modules/modular_computers/file_system/programs/maintenance/camera.dm b/code/modules/modular_computers/file_system/programs/maintenance/camera.dm index bb5641ef9ba..5615df56925 100644 --- a/code/modules/modular_computers/file_system/programs/maintenance/camera.dm +++ b/code/modules/modular_computers/file_system/programs/maintenance/camera.dm @@ -23,7 +23,7 @@ desc = "Specialized internal camera protected from the hellish depths of SSWardrobe. \ Yell at coders if you somehow manage to see this" -/datum/computer_file/program/maintenance/camera/on_install() +/datum/computer_file/program/maintenance/camera/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() internal_camera = new(computer) internal_camera.print_picture_on_snap = FALSE diff --git a/code/modules/modular_computers/file_system/programs/maintenance/cool_sword.dm b/code/modules/modular_computers/file_system/programs/maintenance/cool_sword.dm index 1a05bf2ceba..a7547b5bde9 100644 --- a/code/modules/modular_computers/file_system/programs/maintenance/cool_sword.dm +++ b/code/modules/modular_computers/file_system/programs/maintenance/cool_sword.dm @@ -17,7 +17,7 @@ . = ..() RegisterSignal(src, COMSIG_COMPUTER_FILE_DELETE, PROC_REF(on_delete)) -/datum/computer_file/program/maintenance/cool_sword/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/maintenance/cool_sword/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() RegisterSignal(computer_installing, COMSIG_ITEM_EQUIPPED, PROC_REF(host_equipped)) RegisterSignal(computer_installing, COMSIG_ITEM_DROPPED, PROC_REF(host_dropped)) diff --git a/code/modules/modular_computers/file_system/programs/maintenance/themes.dm b/code/modules/modular_computers/file_system/programs/maintenance/themes.dm index de52daa335e..a76ee44a0c9 100644 --- a/code/modules/modular_computers/file_system/programs/maintenance/themes.dm +++ b/code/modules/modular_computers/file_system/programs/maintenance/themes.dm @@ -1,11 +1,19 @@ +/// PDA themes that you can find in maintenance. Once installed by a player, it'll become available to them on future rounds as well. /datum/computer_file/program/maintenance/theme filename = "theme" filedesc = "Theme holder" extended_desc = "Holds a theme you can add to your Modular PC to set in the Themify application. Makes the application use more space" size = 2 + abstract_type = /datum/computer_file/program/maintenance/theme ///The type of theme we have var/theme_name + ///The Database ID of the theme. It's important that non-abstract types have it set. + var/theme_id + ///The icon file for this theme + var/icon_file = PDA_THEMES_PROGRESS_SET + ///The icon_state for this theme to show in the progress score tab + var/icon = "" /datum/computer_file/program/maintenance/theme/New() . = ..() @@ -25,29 +33,43 @@ return TRUE ///Called post-installation of an application in a computer, after 'computer' var is set. -/datum/computer_file/program/maintenance/theme/on_install() +/datum/computer_file/program/maintenance/theme/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) SHOULD_CALL_PARENT(FALSE) //add the theme to the computer and increase its size to match var/datum/computer_file/program/themeify/theme_app = locate() in computer.stored_files - if(theme_app) - LAZYADD(theme_app.imported_themes, theme_name) - theme_app.size += size - qdel(src) + if(!theme_app) + return + LAZYADD(theme_app.imported_themes, theme_name) + theme_app.size += size + user?.client?.give_award(/datum/award/score/progress/pda_themes, user, theme_id, theme_name) + qdel(src) /datum/computer_file/program/maintenance/theme/cat theme_name = PDA_THEME_CAT_NAME + theme_id = PDA_THEME_ID_CAT + icon = "cat" /datum/computer_file/program/maintenance/theme/lightmode theme_name = PDA_THEME_LIGHT_MODE_NAME + theme_id = PDA_THEME_ID_LIGHT_MODE + icon = "light_mode" /datum/computer_file/program/maintenance/theme/spooky theme_name = PDA_THEME_SPOOKY_NAME + theme_id = PDA_THEME_ID_SPOOKY + icon = "eldritch" /datum/computer_file/program/maintenance/theme/hacker theme_name = PDA_THEME_HACKERMAN_NAME + theme_id = PDA_THEME_ID_HACKERMAN + icon = "hacker" /datum/computer_file/program/maintenance/theme/roulette theme_name = PDA_THEME_ROULETTE_NAME + theme_id = PDA_THEME_ID_ROULETTE + icon = "roulette" /datum/computer_file/program/maintenance/theme/alien theme_name = PDA_THEME_ABDUCTOR_NAME + theme_id = PDA_THEME_ID_ABDUCTOR + icon = "alien" diff --git a/code/modules/modular_computers/file_system/programs/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/ntdownloader.dm index a26d9bd3d51..5e665962717 100644 --- a/code/modules/modular_computers/file_system/programs/ntdownloader.dm +++ b/code/modules/modular_computers/file_system/programs/ntdownloader.dm @@ -11,6 +11,8 @@ ///The program currently being downloaded. var/datum/computer_file/program/downloaded_file + ///weak ref to the user who began the download + var/datum/weakref/download_user ///Boolean on whether the `downloaded_file` is being downloaded from the Syndicate store, ///in which case it will appear as 'ENCRYPTED' in logs, rather than display file name. var/hacked_download = FALSE @@ -35,7 +37,7 @@ ui_header = null . = ..() -/datum/computer_file/program/ntnetdownload/proc/begin_file_download(filename) +/datum/computer_file/program/ntnetdownload/proc/begin_file_download(filename, mob/user) if(downloaded_file) return FALSE @@ -64,6 +66,7 @@ hacked_download = FALSE downloaded_file = PRG.clone() + download_user = WEAKREF(user) // If the filesize is 0 (or somehow lower), we instantly download to avoid invalid number issues with stepwise download. if(downloaded_file.size <= 0) @@ -76,17 +79,19 @@ downloaded_file = null download_completion = FALSE ui_header = null + download_user = null /datum/computer_file/program/ntnetdownload/proc/complete_file_download() if(!downloaded_file) return generate_network_log("Completed download of file [hacked_download ? "**ENCRYPTED**" : "[downloaded_file.filename].[downloaded_file.filetype]"].") - if(!computer || !computer.store_file(downloaded_file)) + if(!computer || !computer.store_file(downloaded_file, download_user?.resolve())) // The download failed downloaderror = "I/O ERROR - Unable to save file. Check whether you have enough free space on your hard drive and whether your hard drive is properly connected. If the issue persists contact your system administrator for assistance." downloaded_file = null download_completion = FALSE ui_header = "downloader_finished.gif" + download_user = null /datum/computer_file/program/ntnetdownload/process_tick(seconds_per_tick) if(!downloaded_file) @@ -117,7 +122,7 @@ switch(action) if("PRG_downloadfile") if(!downloaded_file) - begin_file_download(params["filename"]) + begin_file_download(params["filename"], usr) return TRUE if("PRG_reseterror") if(downloaderror) diff --git a/code/modules/modular_computers/file_system/programs/secureye.dm b/code/modules/modular_computers/file_system/programs/secureye.dm index 6d99a795cf6..7a91b9f00aa 100644 --- a/code/modules/modular_computers/file_system/programs/secureye.dm +++ b/code/modules/modular_computers/file_system/programs/secureye.dm @@ -69,7 +69,7 @@ ) spying = TRUE -/datum/computer_file/program/secureye/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/secureye/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() // Map name has to start and end with an A-Z character, // and definitely NOT with a square bracket or even a number. diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm index b3b255f0682..c01c865fbf7 100644 --- a/code/modules/modular_computers/file_system/programs/techweb.dm +++ b/code/modules/modular_computers/file_system/programs/techweb.dm @@ -21,7 +21,7 @@ /// Sequence var for the id cache var/id_cache_seq = 1 -/datum/computer_file/program/science/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing) +/datum/computer_file/program/science/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() if(!CONFIG_GET(flag/no_default_techweb_link) && !stored_research) CONNECT_TO_RND_SERVER_ROUNDSTART(stored_research, computer) diff --git a/code/modules/modular_computers/file_system/programs/virtual_pet.dm b/code/modules/modular_computers/file_system/programs/virtual_pet.dm index 78507331dad..3705c5d92a9 100644 --- a/code/modules/modular_computers/file_system/programs/virtual_pet.dm +++ b/code/modules/modular_computers/file_system/programs/virtual_pet.dm @@ -121,7 +121,7 @@ GLOBAL_LIST_EMPTY(virtual_pets_list) ///cooldown till we can alter our pet's appearance again COOLDOWN_DECLARE(alter_appearance_cooldown) -/datum/computer_file/program/virtual_pet/on_install() +/datum/computer_file/program/virtual_pet/on_install(datum/computer_file/source, obj/item/modular_computer/computer_installing, mob/user) . = ..() profile_picture = getFlatIcon(image(icon = 'icons/ui/virtualpet/pet_state.dmi', icon_state = "pet_preview")) GLOB.virtual_pets_list += src diff --git a/code/modules/unit_tests/achievements.dm b/code/modules/unit_tests/achievements.dm index decda52a2f5..484e0ca3aa8 100644 --- a/code/modules/unit_tests/achievements.dm +++ b/code/modules/unit_tests/achievements.dm @@ -2,15 +2,41 @@ /datum/unit_test/achievements /datum/unit_test/achievements/Run() + var/list/collected_ids = list() for(var/datum/award/award as anything in subtypesof(/datum/award)) if(!initial(award.name)) //Skip abstract achievements types continue - var/init_icon = initial(award.icon) - var/init_icon_state = initial(award.icon_state) - if(!init_icon_state || !icon_exists(init_icon, init_icon_state)) - TEST_FAIL("Award [initial(award.name)] has a non-existent icon in [init_icon]: \"[init_icon_state || "null"]\"") - if(length(initial(award.database_id)) > 32) //sql schema limit - TEST_FAIL("Award [initial(award.name)] database id is too long") - var/init_category = initial(award.category) - if(!(init_category in GLOB.achievement_categories)) - TEST_FAIL("Award [initial(award.name)] has unsupported category: \"[init_category || "null"]\". Update GLOB.achievement_categories") + if(!award::icon_state || !icon_exists(award::icon, award::icon_state)) + TEST_FAIL("[award] has a non-existent icon in [award::icon]: \"[award::icon_state || "null"]\"") + if(!(award::category in GLOB.achievement_categories)) + TEST_FAIL("[award] has unsupported category: \"[award::category || "null"]\". Update GLOB.achievement_categories") + if(length(award::database_id) > 32) //sql schema limit + TEST_FAIL("[award] database id is too long") + else if(!award::database_id) + TEST_FAIL("[award] doesn't have a database id") + continue + if(collected_ids[award::database_id]) + TEST_FAIL("There's already an award with same database id as [award]") + collected_ids[award::database_id] = TRUE + + +///Check that non-abstract maintenance themes have an id, name and icon +/datum/unit_test/unlockable_themes + +/datum/unit_test/unlockable_themes/Run() + var/list/collected_ids = list() + for(var/datum/computer_file/program/maintenance/theme/theme as anything in typesof(/datum/computer_file/program/maintenance/theme)) + if(theme::abstract_type == theme) + continue + if(!theme::theme_name) + TEST_FAIL("[theme] doesn't have a set name") + if(!theme::theme_id) + TEST_FAIL("[theme] doesn't have a set id") + if(length(theme::theme_id) > 32) //sql schema limit + TEST_FAIL("[theme] theme id is too long") + if(!theme::icon || !icon_exists(theme::icon_file, theme::icon)) + TEST_FAIL("[theme] has a non-existent icon in [theme::icon_file]: \"[theme::icon || "null"]\"") + continue + if(collected_ids[theme::theme_id]) + TEST_FAIL("There's already a theme with same id as [theme]") + collected_ids[theme::theme_id] = TRUE diff --git a/icons/ui/achievements/pda_themes.dmi b/icons/ui/achievements/pda_themes.dmi new file mode 100644 index 00000000000..9fe35eeda85 Binary files /dev/null and b/icons/ui/achievements/pda_themes.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 1d241718ce8..97d2f0d59fe 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -891,6 +891,7 @@ #include "code\datums\achievements\mafia_achievements.dm" #include "code\datums\achievements\misc_achievements.dm" #include "code\datums\achievements\misc_scores.dm" +#include "code\datums\achievements\progress_scores.dm" #include "code\datums\achievements\skill_achievements.dm" #include "code\datums\actions\action.dm" #include "code\datums\actions\cooldown_action.dm"