diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 4b1ab700c3d..5530858f1e1 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -127,6 +127,7 @@
#define INIT_ORDER_TIMER 1
#define INIT_ORDER_DEFAULT 0
#define INIT_ORDER_AIR -1
+#define INIT_ORDER_PERSISTENCE -2 //before assets because some assets take data from SSPersistence
#define INIT_ORDER_ASSETS -4
#define INIT_ORDER_ICON_SMOOTHING -5
#define INIT_ORDER_OVERLAY -6
@@ -138,7 +139,6 @@
#define INIT_ORDER_PATH -50
#define INIT_ORDER_DISCORD -60
#define INIT_ORDER_EXPLOSIONS -69
-#define INIT_ORDER_PERSISTENCE -95
#define INIT_ORDER_STATPANELS -98
#define INIT_ORDER_DEMO -99 // o avoid a bunch of changes related to initialization being written, do this last
#define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init.
diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm
index 3bac791241c..0ae00d47b2c 100644
--- a/code/_globalvars/lists/flavor_misc.dm
+++ b/code/_globalvars/lists/flavor_misc.dm
@@ -123,6 +123,7 @@ GLOBAL_LIST_INIT(ai_core_display_screens, sortList(list(
"Murica",
"Nanotrasen",
"Not Malf",
+ "Portrait",
"President",
"Random",
"Rainbow",
@@ -142,6 +143,10 @@ GLOBAL_LIST_INIT(ai_core_display_screens, sortList(list(
else
if(input == "Random")
input = pick(GLOB.ai_core_display_screens - "Random")
+ if(input == "Portrait")
+ var/datum/portrait_picker/tgui = new(usr)//create the datum
+ tgui.ui_interact(usr)//datum has a tgui component, here we open the window
+ return "ai-portrait" //just take this until they decide
return "ai-[lowertext(input)]"
GLOBAL_LIST_INIT(security_depts_prefs, sortList(list(SEC_DEPT_RANDOM, SEC_DEPT_NONE, SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, SEC_DEPT_SCIENCE, SEC_DEPT_SUPPLY)))
diff --git a/code/game/objects/structures/artstuff.dm b/code/game/objects/structures/artstuff.dm
index 5425f0a01de..b838b4c9a6e 100644
--- a/code/game/objects/structures/artstuff.dm
+++ b/code/game/objects/structures/artstuff.dm
@@ -47,7 +47,7 @@
var/list/grid
var/canvas_color = "#ffffff" //empty canvas color
var/used = FALSE
- var/painting_name //Painting name, this is set after framing.
+ var/painting_name = "Untitled Artwork" //Painting name, this is set after framing.
var/finalized = FALSE //Blocks edits
var/author_ckey
var/icon_generated = FALSE
@@ -185,7 +185,7 @@
/obj/item/canvas/proc/try_rename(mob/user)
var/new_name = stripped_input(user,"What do you want to name the painting?")
- if(!painting_name && new_name && user.canUseTopic(src,BE_CLOSE))
+ if(painting_name != initial(painting_name) && new_name && user.canUseTopic(src,BE_CLOSE))
painting_name = new_name
SStgui.update_uis(src)
@@ -216,6 +216,17 @@
framed_offset_x = 5
framed_offset_y = 6
+/obj/item/canvas/twentyfour_twentyfour
+ name = "ai universal standard canvas"
+ desc = "Besides being very large, the AI can accept these as a display from their internal database after you've hung it up."
+ icon_state = "24x24"
+ width = 24
+ height = 24
+ pixel_x = 2
+ pixel_y = 1
+ framed_offset_x = 4
+ framed_offset_y = 5
+
/obj/item/wallframe/painting
name = "painting frame"
desc = "The perfect showcase for your favorite deathtrap memories."
@@ -252,7 +263,7 @@
/obj/structure/sign/painting/attackby(obj/item/I, mob/user, params)
if(!C && istype(I, /obj/item/canvas))
frame_canvas(user,I)
- else if(C && !C.painting_name && istype(I,/obj/item/pen))
+ else if(C && C.painting_name == initial(C.painting_name) && istype(I,/obj/item/pen))
try_rename(user)
else
return ..()
@@ -280,7 +291,7 @@
update_icon()
/obj/structure/sign/painting/proc/try_rename(mob/user)
- if(!C.painting_name)
+ if(C.painting_name == initial(C.painting_name))
C.try_rename(user)
/obj/structure/sign/painting/update_icon_state()
@@ -312,6 +323,8 @@
var/title = chosen["title"]
var/author = chosen["ckey"]
var/png = "data/paintings/[persistence_id]/[chosen["md5"]].png"
+ if(!title)
+ message_admins("Painting with NO TITLE loaded on a [persistence_id] frame in [get_area(src)]. Please delete it, it is saved in the database with no name and will create bad assets.")
if(!fexists(png))
stack_trace("Persistent painting [chosen["md5"]].png was not found in [persistence_id] directory.")
return
@@ -339,6 +352,8 @@
if(sanitize_filename(persistence_id) != persistence_id)
stack_trace("Invalid persistence_id - [persistence_id]")
return
+ if(!C.painting_name)
+ return
var/data = C.get_data_string()
var/md5 = md5(lowertext(data))
var/list/current = SSpersistence.paintings[persistence_id]
diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm
index 06ee5a2977d..682a2920143 100644
--- a/code/modules/asset_cache/asset_list_items.dm
+++ b/code/modules/asset_cache/asset_list_items.dm
@@ -488,3 +488,26 @@
/datum/asset/spritesheet/mafia/register()
InsertAll("", 'icons/obj/mafia.dmi')
..()
+
+/datum/asset/simple/portraits
+ var/tab = "use subtypes of this please"
+ assets = list()
+
+/datum/asset/simple/portraits/New()
+ if(!SSpersistence.paintings || !SSpersistence.paintings[tab] || !length(SSpersistence.paintings[tab]))
+ return
+ for(var/p in SSpersistence.paintings[tab])
+ var/list/portrait = p
+ var/png = "data/paintings/[tab]/[portrait["md5"]].png"
+ if(fexists(png))
+ assets[portrait["title"]] = png
+ ..() //this is where it registers all these assets we added to the list
+
+/datum/asset/simple/portraits/library
+ tab = "library"
+
+/datum/asset/simple/portraits/library_secure
+ tab = "library_secure"
+
+/datum/asset/simple/portraits/library_private
+ tab = "library_private"
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index ccd3b171278..881c7fa65b7 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -1525,7 +1525,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
playtime_reward_cloak = !playtime_reward_cloak
if("ai_core_icon")
- var/ai_core_icon = input(user, "Choose your preferred AI core display screen:", "AI Core Display Screen Selection") as null|anything in GLOB.ai_core_display_screens
+ var/ai_core_icon = input(user, "Choose your preferred AI core display screen:", "AI Core Display Screen Selection") as null|anything in GLOB.ai_core_display_screens - "Portrait"
if(ai_core_icon)
preferred_ai_core_display = ai_core_icon
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 943bbcedce2..950d3e6ffcb 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -138,7 +138,8 @@
if(client)
INVOKE_ASYNC(src, .proc/apply_pref_name,"ai",client)
- set_core_display_icon()
+ INVOKE_ASYNC(src, .proc/set_core_display_icon)
+
holo_icon = getHologramIcon(icon('icons/mob/ai.dmi',"default"))
@@ -237,11 +238,17 @@
set name = "Set AI Core Display"
if(incapacitated())
return
+ icon = initial(icon)
+ icon_state = "ai"
+ cut_overlays()
var/list/iconstates = GLOB.ai_core_display_screens
for(var/option in iconstates)
if(option == "Random")
iconstates[option] = image(icon = src.icon, icon_state = "ai-random")
continue
+ if(option == "Portrait")
+ iconstates[option] = image(icon = src.icon, icon_state = "ai-portrait")
+ continue
iconstates[option] = image(icon = src.icon, icon_state = resolve_ai_icon(option))
view_core()
diff --git a/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm b/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm
new file mode 100644
index 00000000000..ba785251b77
--- /dev/null
+++ b/code/modules/mob/living/silicon/ai/ai_portrait_picker.dm
@@ -0,0 +1,78 @@
+
+//Portrait picker! It's a tgui window that lets you look through all the portraits, and choose one as your AI.
+
+//very similar to centcom_podlauncher in terms of how this is coded, so i kept a lot of comments from it
+//^ wow! it's the second time i've said this! i'm a real coder now, copying my statement of copying other people's stuff.
+
+
+#define TAB_LIBRARY 1
+#define TAB_SECURE 2
+#define TAB_PRIVATE 3
+
+/datum/portrait_picker
+ var/client/holder //client of whoever is using this datum
+
+/datum/portrait_picker/New(user)//user can either be a client or a mob due to byondcode(tm)
+ if (istype(user, /client))
+ var/client/user_client = user
+ holder = user_client //if its a client, assign it to holder
+ else
+ var/mob/user_mob = user
+ holder = user_mob.client //if its a mob, assign the mob's client to holder
+
+/datum/portrait_picker/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "PortraitPicker")
+ ui.open()
+
+/datum/portrait_picker/ui_close()
+ qdel(src)
+
+/datum/portrait_picker/ui_state(mob/user)
+ return GLOB.conscious_state
+
+/datum/portrait_picker/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/simple/portraits/library),
+ get_asset_datum(/datum/asset/simple/portraits/library_secure),
+ get_asset_datum(/datum/asset/simple/portraits/library_private)
+ )
+
+/datum/portrait_picker/ui_data(mob/user)
+ var/list/data = list()
+ data["library"] = SSpersistence.paintings["library"] ? SSpersistence.paintings["library"] : 0
+ data["library_secure"] = SSpersistence.paintings["library_secure"] ? SSpersistence.paintings["library_secure"] : 0
+ data["library_private"] = SSpersistence.paintings["library_private"] ? SSpersistence.paintings["library_private"] : 0 //i'm gonna regret this, won't i?
+ return data
+
+/datum/portrait_picker/ui_act(action, params)
+ . = ..()
+ if(.)
+ return
+ switch(action)
+ if("select")
+ var/list/tab2key = list(TAB_LIBRARY = "library", TAB_SECURE = "library_secure", TAB_PRIVATE = "library_private")
+ var/folder = tab2key[params["tab"]]
+ var/list/current_list = SSpersistence.paintings[folder]
+ var/list/chosen_portrait = current_list[params["selected"]]
+ var/png = "data/paintings/[folder]/[chosen_portrait["md5"]].png"
+ var/icon/portrait_icon = new(png)
+ var/mob/living/ai = holder.mob
+ var/w = portrait_icon.Width()
+ var/h = portrait_icon.Height()
+ var/mutable_appearance/MA = mutable_appearance(portrait_icon)
+ if(w == 23 || h == 23)
+ to_chat(ai, "Small note: 23x23 Portraits are accepted, but they do not fit perfectly inside the display frame.")
+ MA.pixel_x = 5
+ MA.pixel_y = 5
+ else if(w == 24 || h == 24)
+ to_chat(ai, "Portrait Accepted. Enjoy!")
+ MA.pixel_x = 4
+ MA.pixel_y = 4
+ else
+ to_chat(ai, "Sorry, only 23x23 and 24x24 Portraits are accepted.")
+ return
+ ai.cut_overlays() //so people can't keep repeatedly select portraits to add stacking overlays
+ ai.icon_state = "ai-portrait-active"//background
+ ai.add_overlay(MA)
diff --git a/code/modules/mob/living/silicon/ai/death.dm b/code/modules/mob/living/silicon/ai/death.dm
index 72262dcd133..53256dfb71e 100644
--- a/code/modules/mob/living/silicon/ai/death.dm
+++ b/code/modules/mob/living/silicon/ai/death.dm
@@ -4,6 +4,7 @@
. = ..()
+ cut_overlays() //remove portraits
var/old_icon = icon_state
if("[icon_state]_dead" in icon_states(icon))
icon_state = "[icon_state]_dead"
diff --git a/icons/mob/ai.dmi b/icons/mob/ai.dmi
index 25d6365adcb..6ccceff86d9 100644
Binary files a/icons/mob/ai.dmi and b/icons/mob/ai.dmi differ
diff --git a/icons/obj/artstuff.dmi b/icons/obj/artstuff.dmi
index 14970b76ef2..847ebd5ca61 100644
Binary files a/icons/obj/artstuff.dmi and b/icons/obj/artstuff.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index f5d557f7301..c55eb307390 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -2374,6 +2374,7 @@
#include "code\modules\mob\living\silicon\silicon_say.dm"
#include "code\modules\mob\living\silicon\ai\ai.dm"
#include "code\modules\mob\living\silicon\ai\ai_defense.dm"
+#include "code\modules\mob\living\silicon\ai\ai_portrait_picker.dm"
#include "code\modules\mob\living\silicon\ai\ai_say.dm"
#include "code\modules\mob\living\silicon\ai\death.dm"
#include "code\modules\mob\living\silicon\ai\examine.dm"
diff --git a/tgui/packages/tgui/interfaces/PortraitPicker.js b/tgui/packages/tgui/interfaces/PortraitPicker.js
new file mode 100644
index 00000000000..5f5b32e228d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PortraitPicker.js
@@ -0,0 +1,142 @@
+import { resolveAsset } from '../assets';
+import { useBackend, useLocalState } from '../backend';
+import { Box, Button, Flex, NoticeBox, Section, Tabs } from '../components';
+import { Window } from '../layouts';
+
+export const PortraitPicker = (props, context) => {
+ const { act, data } = useBackend(context);
+ const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
+ const [listIndex, setListIndex] = useLocalState(context, 'listIndex', 0);
+ const {
+ library,
+ library_secure,
+ library_private,
+ } = data;
+ const TABS = [
+ {
+ name: 'Common Portraits',
+ list: library,
+ },
+ {
+ name: 'Secure Portraits',
+ list: library_secure,
+ },
+ {
+ name: 'Private Portraits',
+ list: library_private,
+ },
+ ];
+ const tab2list = TABS[tabIndex].list;
+ const current_portrait = tab2list[listIndex]["title"];
+ return (
+ "+e+"
+
/gi,"\n").replace(/<\/?[a-z0-9-_]+[^>]*>/gi,"").replace(/&(nbsp|amp|quot|lt|gt|apos);/g,(function(e,n){return t[n]})).replace(/?([0-9]+);/gi,(function(e,t){var n=parseInt(t,10);return String.fromCharCode(n)})).replace(/?([0-9a-f]+);/gi,(function(e,t){var n=parseInt(t,16);return String.fromCharCode(n)}))};t.buildQueryString=function(e){return Object.keys(e).map((function(t){return encodeURIComponent(t)+"="+encodeURIComponent(e[t])})).join("&")}},function(e,t,n){"use strict";t.__esModule=!0,t.Box=t.computeBoxClassName=t.computeBoxProps=t.halfUnit=t.unit=void 0;var r=n(6),o=n(0),i=n(466),a=n(38);var u=function(e){return"string"==typeof e?e.endsWith("px")&&!Byond.IS_LTE_IE8?parseFloat(e)/12+"rem":e:"number"==typeof e?Byond.IS_LTE_IE8?12*e+"px":e+"rem":void 0};t.unit=u;var c=function(e){return"string"==typeof e?u(e):"number"==typeof e?u(.5*e):void 0};t.halfUnit=c;var s=function(e){return"string"==typeof e&&a.CSS_COLORS.includes(e)},l=function(e){return function(t,n){"number"!=typeof n&&"string"!=typeof n||(t[e]=n)}},f=function(e,t){return function(n,r){"number"!=typeof r&&"string"!=typeof r||(n[e]=t(r))}},d=function(e,t){return function(n,r){r&&(n[e]=t)}},p=function(e,t,n){return function(r,o){if("number"==typeof o||"string"==typeof o)for(var i=0;ic||n!=n?l*Infinity:l*n}},function(e,t,n){"use strict";var r=n(4),o=Math.hypot,i=Math.abs,a=Math.sqrt;r({target:"Math",stat:!0,forced:!!o&&o(Infinity,NaN)!==Infinity},{hypot:function(e,t){for(var n,r,o=0,u=0,c=arguments.length,s=0;u
\n":"'+(n?e:ee(e,!0))+"
\n"}return e}(),t.blockquote=function(){function e(e){return""+(n?e:ee(e,!0))+"\n"+e+"
\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
\n":"
\n"}return e}(),t.list=function(){function e(e,t,n){var r=t?"ol":"ul";return"<"+r+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+""+r+">\n"}return e}(),t.listitem=function(){function e(e){return"\n\n"+e+"\n"+t+"
\n"}return e}(),t.tablerow=function(){function e(e){return"\n"+e+" \n"}return e}(),t.tablecell=function(){function e(e,t){var n=t.header?"th":"td";return(t.align?"<"+n+' align="'+t.align+'">':"<"+n+">")+e+""+n+">\n"}return e}(),t.strong=function(){function e(e){return""+e+""}return e}(),t.em=function(){function e(e){return""+e+""}return e}(),t.codespan=function(){function e(e){return""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?"
":"
"}return e}(),t.del=function(){function e(e){return""+e+""}return e}(),t.link=function(){function e(e,t,n){if(null===(e=J(this.options.sanitize,this.options.baseUrl,e)))return n;var r='"+n+""}return e}(),t.image=function(){function e(e,t,n){if(null===(e=J(this.options.sanitize,this.options.baseUrl,e)))return n;var r='":">"}return e}(),t.text=function(){function e(e){return e}return e}(),e}(),ne=function(){function e(){}var t=e.prototype;return t.strong=function(){function e(e){return e}return e}(),t.em=function(){function e(e){return e}return e}(),t.codespan=function(){function e(e){return e}return e}(),t.del=function(){function e(e){return e}return e}(),t.html=function(){function e(e){return e}return e}(),t.text=function(){function e(e){return e}return e}(),t.link=function(){function e(e,t,n){return""+n}return e}(),t.image=function(){function e(e,t,n){return""+n}return e}(),t.br=function(){function e(){return""}return e}(),e}(),re=function(){function e(){this.seen={}}return e.prototype.slug=function(){function e(e){var t=e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-");if(this.seen.hasOwnProperty(t)){var n=t;do{this.seen[n]++,t=n+"-"+this.seen[n]}while(this.seen.hasOwnProperty(t))}return this.seen[t]=0,t}return e}(),e}(),oe=a.defaults,ie=I.unescape,ae=function(){function e(e){this.options=e||oe,this.options.renderer=this.options.renderer||new te,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new ne,this.slugger=new re}e.parse=function(){function t(t,n){return new e(n).parse(t)}return t}();var t=e.prototype;return t.parse=function(){function e(e,t){void 0===t&&(t=!0);var n,r,o,i,a,u,c,s,l,f,d,p,h,g,v,m,y,b,x="",w=e.length;for(n=0;n
"+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a