diff --git a/code/__defines/admin.dm b/code/__defines/admin.dm
index d2405dc7f8..6dd6445c58 100644
--- a/code/__defines/admin.dm
+++ b/code/__defines/admin.dm
@@ -130,6 +130,9 @@
#define RANK_SOURCE_BACKUP "rank_backup"
#define RANK_SOURCE_TEMPORARY "rank_temp"
+//How many things you can spawn at once with spawn verb/create panel
+#define ADMIN_SPAWN_CAP 100
+
// LOG BROWSE TYPES
#define BROWSE_ROOT_ALL_LOGS 1
#define BROWSE_ROOT_RUNTIME_LOGS 2
diff --git a/code/__defines/dcs/signals/signals_atom/signals_atom_attack.dm b/code/__defines/dcs/signals/signals_atom/signals_atom_attack.dm
index 0219f795c3..c6dfe82da2 100644
--- a/code/__defines/dcs/signals/signals_atom/signals_atom_attack.dm
+++ b/code/__defines/dcs/signals/signals_atom/signals_atom_attack.dm
@@ -39,7 +39,7 @@
#define COMPONENT_CANCEL_ATTACK_CHAIN (1<<0)
///Skips the specific attack step, continuing for the next one to happen.
#define COMPONENT_SKIP_ATTACK (1<<1)
-///from base of atom/attack_ghost(): (mob/dead/observer/ghost)
+///from base of atom/attack_ghost(): (mob/observer/dead/ghost)
#define COMSIG_ATOM_ATTACK_GHOST "atom_attack_ghost"
///from base of atom/attack_hand(): (mob/user, list/modifiers)
#define COMSIG_ATOM_ATTACK_HAND "atom_attack_hand"
diff --git a/code/__defines/flags.dm b/code/__defines/flags.dm
index 3960b7c7e5..3f630bbecf 100644
--- a/code/__defines/flags.dm
+++ b/code/__defines/flags.dm
@@ -37,6 +37,7 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204
#define IS_BUSY (1<<8) // Atom has a TASK_TARGET_EXCLUSIVE do_after with it as the target.
#define REMOTEVIEW_ON_ENTER (1<<9) // Object starts a remoteview of itself for any mob that enters it with a client. Items will automatically handle their own remoteview, and ignore this.
#define WALL_ITEM (1<<10) // Wall mounted objects
+#define ADMIN_SPAWNED (1<<22) // Admin Spawned
#define ATOM_INITIALIZED (1<<23) // Atom has been initialized. Using a flag instead of a variable saves ~25mb total.
//Flags for items (equipment) - Used in /obj/item/var/item_flags
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index e9de1c0510..da972b253e 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -40,6 +40,9 @@
P.Click(params)
break
+ if(check_click_intercept(params,A))
+ return
+
if(stat)
return
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index e1f3960f79..5ce4dff908 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -43,9 +43,11 @@
* mob/RangedAttack(atom,params) - used only ranged, only used for tk and laser eyes but could be changed
*/
/mob/proc/ClickOn(atom/A, params)
- if(world.time <= next_click)
+ if(!checkClickCooldown()) return
+ setClickCooldown(1) //1/10 of a second, 10 clicks allowed per second.
+
+ if(check_click_intercept(params,A) || HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
return
- next_click = world.time + 1
if(client && client.buildmode)
build_click(src, client.buildmode, params, A)
@@ -372,3 +374,16 @@
/// MouseWheelOn
/mob/proc/MouseWheelOn(atom/A, delta_x, delta_y, params)
SEND_SIGNAL(src, COMSIG_MOUSE_SCROLL_ON, A, delta_x, delta_y, params)
+
+/mob/proc/check_click_intercept(params,A)
+ //Client level intercept
+ if(client?.click_intercept)
+ if(call(client.click_intercept, "InterceptClickOn")(src, params, A))
+ return TRUE
+
+ //Mob level intercept
+ if(click_intercept)
+ if(call(click_intercept, "InterceptClickOn")(src, params, A))
+ return TRUE
+
+ return FALSE
diff --git a/code/_onclick/cyborg.dm b/code/_onclick/cyborg.dm
index f3ce550f12..2a062667ad 100644
--- a/code/_onclick/cyborg.dm
+++ b/code/_onclick/cyborg.dm
@@ -10,6 +10,9 @@
if(!checkClickCooldown())
return
+ if(check_click_intercept(params,A))
+ return
+
setClickCooldown(1)
if(client.buildmode) // comes after object.Click to allow buildmode gui objects to be clicked
diff --git a/code/_onclick/hud/action/action_screen_objects.dm b/code/_onclick/hud/action/action_screen_objects.dm
index 8d44886052..a5693439b5 100644
--- a/code/_onclick/hud/action/action_screen_objects.dm
+++ b/code/_onclick/hud/action/action_screen_objects.dm
@@ -33,7 +33,7 @@
/atom/movable/screen/movable/action_button/proc/can_use(mob/user)
// if(isobserver(user))
- // var/mob/dead/observer/dead_mob = user
+ // var/mob/observer/dead/dead_mob = user
// if(dead_mob.observetarget) // Observers can only click on action buttons if they're not observing something
// return FALSE
@@ -327,7 +327,7 @@ GLOBAL_LIST_INIT(palette_removed_matrix, list(1.4,0,0,0, 0.7,0.4,0,0, 0.4,0,0.6,
/atom/movable/screen/button_palette/proc/can_use(mob/user)
if(isobserver(user))
- // var/mob/dead/observer/O = user
+ // var/mob/observer/dead/O = user
// return !O.observetarget
return TRUE
return TRUE
@@ -388,7 +388,7 @@ GLOBAL_LIST_INIT(palette_removed_matrix, list(1.4,0,0,0, 0.7,0.4,0,0, 0.4,0,0.6,
/atom/movable/screen/palette_scroll/proc/can_use(mob/user)
if(isobserver(user))
- // var/mob/dead/observer/O = user
+ // var/mob/observer/dead/O = user
// return !O.observetarget
return TRUE
return TRUE
diff --git a/code/_onclick/hud/alert.dm b/code/_onclick/hud/alert.dm
index 5bbbc60ddf..16f48e0ad1 100644
--- a/code/_onclick/hud/alert.dm
+++ b/code/_onclick/hud/alert.dm
@@ -414,7 +414,7 @@ so as to remain in compliance with the most up-to-date laws."
// /atom/movable/screen/alert/notify_jump/Click()
// if(!usr || !usr.client) return
// if(!jump_target) return
-// var/mob/dead/observer/G = usr
+// var/mob/observer/dead/G = usr
// if(!istype(G)) return
// if(attack_not_jump)
// jump_target.attack_ghost(G)
diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm
index ae2628b450..dbaf1d6874 100644
--- a/code/_onclick/hud/ghost.dm
+++ b/code/_onclick/hud/ghost.dm
@@ -192,7 +192,7 @@
/* I wish we had this. Not yet, though.
/datum/hud/ghost/show_hud(version = 0, mob/viewmob)
// don't show this HUD if observing; show the HUD of the observee
- var/mob/dead/observer/O = mymob
+ var/mob/observer/dead/O = mymob
if (istype(O) && O.observetarget)
plane_masters_update()
return FALSE
@@ -208,7 +208,7 @@
//We should only see observed mob alerts.
/datum/hud/ghost/reorganize_alerts(mob/viewmob)
- var/mob/dead/observer/O = mymob
+ var/mob/observer/dead/O = mymob
if (istype(O) && O.observetarget)
return
. = ..()
diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm
index d795cdd277..aad65fdcd6 100644
--- a/code/_onclick/observer.dm
+++ b/code/_onclick/observer.dm
@@ -11,6 +11,8 @@
to_chat(src, span_notice("You will no longer examine things you click on."))
/mob/observer/dead/DblClickOn(var/atom/A, var/params)
+ if(check_click_intercept(params,A))
+ return
if(client.buildmode)
build_click(src, client.buildmode, params, A)
return
@@ -28,21 +30,50 @@
stop_following()
forceMove(get_turf(A))
-/mob/observer/dead/ClickOn(var/atom/A, var/params)
- if(client.buildmode)
+/mob/observer/dead/ClickOn(atom/A, params)
+ if(!checkClickCooldown()) return
+ setClickCooldown(1)
+
+ if(check_click_intercept(params,A))
+ return
+
+ if(client && client.buildmode)
build_click(src, client.buildmode, params, A)
return
- if(!checkClickCooldown()) return
- setClickCooldown(4)
+
var/list/modifiers = params2list(params)
- if(modifiers["shift"])
- examinate(A)
+
+ if(LAZYACCESS(modifiers, BUTTON4) || LAZYACCESS(modifiers, BUTTON5))
return
- if(modifiers["alt"]) // alt and alt-gr (rightalt)
- var/turf/T = get_turf(A)
- if(T && TurfAdjacent(T))
- T.click_alt(src)
+
+ if(LAZYACCESS(modifiers, SHIFT_CLICK))
+ if(LAZYACCESS(modifiers, MIDDLE_CLICK))
+ ShiftMiddleClickOn(A)
return
+ if(LAZYACCESS(modifiers, CTRL_CLICK))
+ CtrlShiftClickOn(A)
+ return
+ if (LAZYACCESS(modifiers, ALT_CLICK))
+ alt_shift_click_on(A)
+ return
+ ShiftClickOn(A) //Should in most cases call examinate() unless we block things from examining us.
+ return
+ if(LAZYACCESS(modifiers, MIDDLE_CLICK))
+ if(LAZYACCESS(modifiers, CTRL_CLICK))
+ CtrlMiddleClickOn(A)
+ else
+ MiddleClickOn(A, params)
+ return
+ if(LAZYACCESS(modifiers, ALT_CLICK)) // alt and alt-gr (rightalt)
+ if(LAZYACCESS(modifiers, RIGHT_CLICK))
+ AltClickSecondaryOn(A)
+ else
+ AltClickOn(A)
+ return
+ if(LAZYACCESS(modifiers, CTRL_CLICK))
+ CtrlClickOn(A)
+ return
+
// You are responsible for checking config.ghost_interaction when you override this function
// Not all of them require checking, see below
A.attack_ghost(src)
diff --git a/code/datums/orbit.dm b/code/datums/orbit.dm
index a892f88c3d..64185ebf13 100644
--- a/code/datums/orbit.dm
+++ b/code/datums/orbit.dm
@@ -125,6 +125,6 @@
if(orbiters)
for(var/datum/orbit/O as anything in orbiters)
if(O.orbiter && isobserver(O.orbiter))
- var/mob/dead/observer/D = O.orbiter
+ var/mob/observer/dead/D = O.orbiter
D.ManualFollow(target)
*/
diff --git a/code/game/objects/structures/droppod.dm b/code/game/objects/structures/droppod.dm
index f2ad9f4c31..d0fcaa2d56 100644
--- a/code/game/objects/structures/droppod.dm
+++ b/code/game/objects/structures/droppod.dm
@@ -18,7 +18,7 @@
if(A)
A.forceMove(src) // helo
podfall(auto_open)
- air = new
+ air = new
/obj/structure/drop_pod/Destroy()
. = ..()
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index fe7010baa5..964488c356 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -49,12 +49,12 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_HOLDER, "Show Player Panel", m
var/body = "Options panel for " + span_bold("[player]")
if(player.client)
body += " played by " + span_bold("[player.client]")
- body += "\[[player.client.holder ? player.client.holder.rank_names() : "Player"] \]"
+ body += "\[[player.client.holder ? player.client.holder.rank_names() : "Player"] \]"
if(isnewplayer(player))
body += span_bold("Hasn't Entered Game")
else
- body += " \[Heal \] "
+ body += " \[Heal \] "
if(player.client)
body += " " + span_bold("First connection:") + "[player.client.player_age] days ago"
@@ -63,42 +63,42 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_HOLDER, "Show Player Panel", m
body += {"
\[
- VV -
- TP -
- PM -
- SM -
+ VV -
+ TP -
+ PM -
+ SM -
[admin_jump_link(player, src)]\]
"} + span_bold("Mob type:") + {"[player.type]
"} + span_bold("Inactivity time:") + {" [player.client ? "[player.client.inactivity/600] minutes" : "Logged out"]
- Kick |
- Warn |
- Ban |
- Jobban |
- Notes
+ Kick |
+ Warn |
+ Ban |
+ Jobban |
+ Notes
"}
if(player.client)
- body += "| Prison | "
- body += "\ Send back to Lobby | "
+ body += "| Prison | "
+ body += "\ Send back to Lobby | "
var/muted = player.client.prefs.muted
body += {" "} + span_bold("Mute: ") + {"
- \[[(muted & MUTE_IC) ? span_red("IC") : span_blue("IC")] |
- [(muted & MUTE_OOC) ? span_red("OOC") : span_blue("OOC")] |
- [(muted & MUTE_LOOC) ? span_red("LOOC") : span_blue("LOOC")] |
- [(muted & MUTE_PRAY) ? span_red("PRAY") : span_blue("PRAY")] |
- [(muted & MUTE_ADMINHELP) ? span_red("ADMINHELP") : span_blue("ADMINHELP")] |
- [(muted & MUTE_DEADCHAT) ? span_red("DEADCHAT") : span_blue("DEADCHAT")] \]
- ([(muted & MUTE_ALL) ? span_red("toggle all") : span_blue("toggle all")] )
+ \[[(muted & MUTE_IC) ? span_red("IC") : span_blue("IC")] |
+ [(muted & MUTE_OOC) ? span_red("OOC") : span_blue("OOC")] |
+ [(muted & MUTE_LOOC) ? span_red("LOOC") : span_blue("LOOC")] |
+ [(muted & MUTE_PRAY) ? span_red("PRAY") : span_blue("PRAY")] |
+ [(muted & MUTE_ADMINHELP) ? span_red("ADMINHELP") : span_blue("ADMINHELP")] |
+ [(muted & MUTE_DEADCHAT) ? span_red("DEADCHAT") : span_blue("DEADCHAT")] \]
+ ([(muted & MUTE_ALL) ? span_red("toggle all") : span_blue("toggle all")] )
"}
body += {"
- "} + span_bold("Jump to") + {" |
- Get |
- Send To
+ "} + span_bold("Jump to") + {" |
+ Get |
+ Send To
- [check_rights(R_ADMIN|R_MOD|R_EVENT,0) ? "Traitor panel | " : "" ]
- Narrate to |
- Subtle message
+ [check_rights(R_ADMIN|R_MOD|R_EVENT,0) ? "Traitor panel | " : "" ]
+ Narrate to |
+ Subtle message
"}
if (player.client)
@@ -111,30 +111,30 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_HOLDER, "Show Player Panel", m
if(issmall(player))
body += span_bold("Monkeyized") + " | "
else
- body += "Monkeyize | "
+ body += "Monkeyize | "
//Corgi
if(iscorgi(player))
body += span_bold("Corgized") + " | "
else
- body += "Corgize | "
+ body += "Corgize | "
//AI / Cyborg
if(isAI(player))
body += span_bold("Is an AI ")
else if(ishuman(player))
- body += {"Make AI |
- Make Robot |
- Make Alien
+ body += {"Make AI |
+ Make Robot |
+ Make Alien
"}
//Simple Animals
if(isanimal(player))
- body += "Re-Animalize | "
+ body += "Re-Animalize | "
else
- body += "Animalize | "
+ body += "Animalize | "
- body += "Respawn | "
+ body += "Respawn | "
// DNA2 - Admin Hax
if(player.dna && iscarbon(player))
@@ -170,7 +170,7 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_HOLDER, "Show Player Panel", m
bname = span_orange(bname)
else
bname = span_red(bname)
- body += "[bname] [block] " // Traitgenes edit - show trait linked names on mouseover
+ body += "[bname] [block] " // Traitgenes edit - show trait linked names on mouseover
else
body += "[block]"
body+=""
@@ -178,45 +178,45 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_HOLDER, "Show Player Panel", m
body += {"
"} + span_bold("Rudimentary transformation:") + span_normal(" These transformations only create a new mob type and copy stuff over. They do not take into account MMIs and similar mob-specific things. The buttons in 'Transformations' are preferred, when possible.") + {"
- Observer |
- \[ Xenos: Larva
- Drone
- Hunter
- Sentinel
- Queen \] |
- \[ Crew: Human
- Unathi
- Tajaran
- Skrell \] | \[
- Nymph
- Diona \] |
- \[ slime: Baby ,
- Adult \]
- Monkey |
- Cyborg |
- Cat |
- Runtime |
- Corgi |
- Ian |
- Crab |
- Coffee |
- \[ Construct: Armoured ,
- Builder ,
- Wraith \]
- Shade
+ Observer |
+ \[ Xenos: Larva
+ Drone
+ Hunter
+ Sentinel
+ Queen \] |
+ \[ Crew: Human
+ Unathi
+ Tajaran
+ Skrell \] | \[
+ Nymph
+ Diona \] |
+ \[ slime: Baby ,
+ Adult \]
+ Monkey |
+ Cyborg |
+ Cat |
+ Runtime |
+ Corgi |
+ Ian |
+ Crab |
+ Coffee |
+ \[ Construct: Armoured ,
+ Builder ,
+ Wraith \]
+ Shade
"}
body += {"
"} + span_bold("Other actions:") + {"
- Forcesay
+ Forcesay
"}
if (player.client)
body += {" |
- Thunderdome 1 |
- Thunderdome 2 |
- Thunderdome Admin |
- Thunderdome Observer |
+ Thunderdome 1 |
+ Thunderdome 2 |
+ Thunderdome Admin |
+ Thunderdome Observer |
"}
// language toggles
body += " " + span_bold("Languages:") + " "
@@ -227,9 +227,9 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_HOLDER, "Show Player Panel", m
if(!f) body += " | "
else f = 0
if(L in player.languages)
- body += "[span_green(k)] "
+ body += "[span_green(k)] "
else
- body += "[span_red(k)] "
+ body += "[span_red(k)] "
body += {" "}
@@ -279,28 +279,28 @@ ADMIN_VERB(access_news_network, R_ADMIN|R_EVENT, "Access Newscaster Network", "A
var/datum/admins/admin_holder = user.holder
switch(admin_holder.admincaster_screen)
if(0)
- dat += {"Welcome to the admin newscaster. Here you can add, edit and censor every newspiece on the network.
- Feed channels and stories entered through here will be uneditable and handled as official news by the rest of the units.
- Note that this panel allows full freedom over the news network, there are no constrictions except the few basic ones. Don't break things!
+ dat += {"Welcome to the admin newscaster. Here you can add, edit and censor every newspiece on the network.
+ Feed channels and stories entered through here will be uneditable and handled as official news by the rest of the units.
+ Note that this panel allows full freedom over the news network, there are no constrictions except the few basic ones. Don't break things!
"}
if(GLOB.news_network.wanted_issue)
- dat+= "
Read Wanted Issue "
+ dat+= "Read Wanted Issue "
- dat+= {"Create Feed Channel
- View Feed Channels
- Submit new Feed story
- Exit
+ dat+= {"Create Feed Channel
+ View Feed Channels
+ Submit new Feed story
+ Exit
"}
var/wanted_already = 0
if(GLOB.news_network.wanted_issue)
wanted_already = 1
- dat+={" "} + span_bold("Feed Security functions:") + {"
- [(wanted_already) ? ("Manage") : ("Publish")] \"Wanted\" Issue
- Censor Feed Stories
- Mark Feed Channel with [using_map.company_name] D-Notice (disables and locks the channel.
- The newscaster recognises you as: "} + span_green("[admin_holder.admincaster_signature]") + {"
+ dat+={" "} + span_bold("Feed Security functions:") + {"
+ [(wanted_already) ? ("Manage") : ("Publish")] \"Wanted\" Issue
+ Censor Feed Stories
+ Mark Feed Channel with [using_map.company_name] D-Notice (disables and locks the channel.
+ The newscaster recognises you as: "} + span_green("[admin_holder.admincaster_signature]") + {"
"}
if(1)
dat+= "Station Feed Channels "
@@ -309,97 +309,97 @@ ADMIN_VERB(access_news_network, R_ADMIN|R_EVENT, "Access Newscaster Network", "A
else
for(var/datum/feed_channel/CHANNEL in GLOB.news_network.network_channels)
if(CHANNEL.is_admin_channel)
- dat+=span_bold("[CHANNEL.channel_name] ") + " "
+ dat+=span_bold("[CHANNEL.channel_name] ") + " "
else
- dat+=span_bold("[CHANNEL.channel_name] [(CHANNEL.censored) ? (span_red("***")) : null] ")
- dat+={"Refresh
- Back
+ dat+=span_bold("[CHANNEL.channel_name] [(CHANNEL.censored) ? (span_red("***")) : null] ")
+ dat+={"Refresh
+ Back
"}
if(2)
dat+={"
Creating new Feed Channel...
- "} + span_bold("Channel Name :") + {" [admin_holder.admincaster_feed_channel.channel_name]
- "} + span_bold("Channel Author :") + {" "} + span_green("[admin_holder.admincaster_signature]") + {"
- "} + span_bold("Will Accept Public Feeds :") + {" [(admin_holder.admincaster_feed_channel.locked) ? ("NO") : ("YES")]
- Submit Cancel
+ "} + span_bold("Channel Name :") + {" [admin_holder.admincaster_feed_channel.channel_name]
+ "} + span_bold("Channel Author :") + {" "} + span_green("[admin_holder.admincaster_signature]") + {"
+ "} + span_bold("Will Accept Public Feeds :") + {" [(admin_holder.admincaster_feed_channel.locked) ? ("NO") : ("YES")]
+ Submit Cancel
"}
if(3)
dat+={"
Creating new Feed Message...
- "} + span_bold("Receiving Channel :") + {" [admin_holder.admincaster_feed_channel.channel_name]
- "} + span_bold("Message Author:") + {" "} + span_green("[admin_holder.admincaster_signature]") + {"
- "} + span_bold("Message Body :") + {" [admin_holder.admincaster_feed_message.body]
- Submit Cancel
+ "} + span_bold("Receiving Channel :") + {" [admin_holder.admincaster_feed_channel.channel_name]
+ "} + span_bold("Message Author:") + {" "} + span_green("[admin_holder.admincaster_signature]") + {"
+ "} + span_bold("Message Body :") + {" [admin_holder.admincaster_feed_message.body]
+ Submit Cancel
"}
if(4)
dat+={"
- Feed story successfully submitted to [admin_holder.admincaster_feed_channel.channel_name].
- Return
+ Feed story successfully submitted to [admin_holder.admincaster_feed_channel.channel_name].
+ Return
"}
if(5)
dat+={"
- Feed Channel [admin_holder.admincaster_feed_channel.channel_name] created successfully.
- Return
+ Feed Channel [admin_holder.admincaster_feed_channel.channel_name] created successfully.
+ Return
"}
if(6)
- dat+=span_bold(span_maroon("ERROR: Could not submit Feed story to Network.")) + " "
+ dat+=span_bold(span_maroon("ERROR: Could not submit Feed story to Network.")) + " "
if(admin_holder.admincaster_feed_channel.channel_name=="")
- dat+=span_maroon("Invalid receiving channel name.") + " "
+ dat+=span_maroon("Invalid receiving channel name.") + " "
if(admin_holder.admincaster_feed_message.body == "" || admin_holder.admincaster_feed_message.body == "\[REDACTED\]" || admin_holder.admincaster_feed_message.title == "")
- dat+=span_maroon("Invalid message body.") + " "
- dat+="Return "
+ dat+=span_maroon("Invalid message body.") + " "
+ dat+="Return "
if(7)
- dat+=span_bold(span_maroon("ERROR: Could not submit Feed Channel to Network.")) + " "
+ dat+=span_bold(span_maroon("ERROR: Could not submit Feed Channel to Network.")) + " "
if(admin_holder.admincaster_feed_channel.channel_name =="" || admin_holder.admincaster_feed_channel.channel_name == "\[REDACTED\]")
- dat+=span_maroon("Invalid channel name.") + " "
+ dat+=span_maroon("Invalid channel name.") + " "
var/check = 0
for(var/datum/feed_channel/FC in GLOB.news_network.network_channels)
if(FC.channel_name == admin_holder.admincaster_feed_channel.channel_name)
check = 1
break
if(check)
- dat+=span_maroon("Channel name already in use.") + " "
- dat+="Return "
+ dat+=span_maroon("Channel name already in use.") + " "
+ dat+="Return "
if(9)
dat+=span_bold("[admin_holder.admincaster_feed_channel.channel_name]: ") + span_small("\[created by: [span_maroon("[admin_holder.admincaster_feed_channel.author]")]\]") + " "
if(admin_holder.admincaster_feed_channel.censored)
dat+={"
- "} + span_red(span_bold("ATTENTION: ")) + {"This channel has been deemed as threatening to the welfare of the station, and marked with a [using_map.company_name] D-Notice.
- No further feed story additions are allowed while the D-Notice is in effect.
+ "} + span_red(span_bold("ATTENTION: ")) + {"This channel has been deemed as threatening to the welfare of the station, and marked with a [using_map.company_name] D-Notice.
+ No further feed story additions are allowed while the D-Notice is in effect.
"}
else
if( isemptylist(admin_holder.admincaster_feed_channel.messages) )
- dat+=span_italics("No feed messages found in channel...") + " "
+ dat+=span_italics("No feed messages found in channel...") + " "
else
var/i = 0
for(var/datum/feed_message/MESSAGE in admin_holder.admincaster_feed_channel.messages)
i++
- //dat+="-[MESSAGE.body] "
+ //dat+="-[MESSAGE.body] "
var/pic_data
if(MESSAGE.img)
user << browse_rsc(MESSAGE.img, "tmp_photo[i].png")
pic_data+=" "
dat+= get_newspaper_content(MESSAGE.title, MESSAGE.body, MESSAGE.author,"#d4cec1", pic_data)
- dat+=" "
- dat+=span_small("\[Story by [span_maroon("[MESSAGE.author] - [MESSAGE.time_stamp]")]\]") + " "
+ dat+=" "
+ dat+=span_small("\[Story by [span_maroon("[MESSAGE.author] - [MESSAGE.time_stamp]")]\]") + " "
dat+={"
- Refresh
- Back
+ Refresh
+ Back
"}
if(10)
dat+={"
- "} + span_bold("[using_map.company_name] Feed Censorship Tool") + {"
- "} + span_small("NOTE: Due to the nature of news Feeds, total deletion of a Feed Story is not possible. ") + {"
+ "} + span_bold("[using_map.company_name] Feed Censorship Tool") + {"
+ "} + span_small("NOTE: Due to the nature of news Feeds, total deletion of a Feed Story is not possible. ") + {"
"} + span_small("Keep in mind that users attempting to view a censored feed will instead see the \[REDACTED\] tag above it.") + {"
- Select Feed channel to get Stories from:
+ Select Feed channel to get Stories from:
"}
if(isemptylist(GLOB.news_network.network_channels))
- dat+=span_italics("No feed channels found active...") + " "
+ dat+=span_italics("No feed channels found active...") + " "
else
for(var/datum/feed_channel/CHANNEL in GLOB.news_network.network_channels)
- dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? (span_red("***")) : null] "
- dat+="Cancel "
+ dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? (span_red("***")) : null] "
+ dat+="Cancel "
if(11)
dat+={"
"} + span_bold("[using_map.company_name] D-Notice Handler") + {"
@@ -408,44 +408,44 @@ ADMIN_VERB(access_news_network, R_ADMIN|R_EVENT, "Access Newscaster Network", "A
"} + span_small("stories it might contain at the time. You can lift a D-Notice if you have the required access at any time.") + {"
"}
if(isemptylist(GLOB.news_network.network_channels))
- dat+=span_italics("No feed channels found active...") + " "
+ dat+=span_italics("No feed channels found active...") + " "
else
for(var/datum/feed_channel/CHANNEL in GLOB.news_network.network_channels)
- dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? (span_red("***")) : null] "
+ dat+="[CHANNEL.channel_name] [(CHANNEL.censored) ? (span_red("***")) : null] "
- dat+="Back "
+ dat+="Back "
if(12)
dat+={"
- "} + span_bold("[admin_holder.admincaster_feed_channel.channel_name]: ") + span_small("\[ created by: [span_maroon("[admin_holder.admincaster_feed_channel.author]")] \]") + {"
- "} + span_normal("[(admin_holder.admincaster_feed_channel.author=="\[REDACTED\]") ? ("Undo Author censorship") : ("Censor channel Author")] ") + {"
+ "} + span_bold("[admin_holder.admincaster_feed_channel.channel_name]: ") + span_small("\[ created by: [span_maroon("[admin_holder.admincaster_feed_channel.author]")] \]") + {"
+ "} + span_normal("[(admin_holder.admincaster_feed_channel.author=="\[REDACTED\]") ? ("Undo Author censorship") : ("Censor channel Author")] ") + {"
"}
if( isemptylist(admin_holder.admincaster_feed_channel.messages) )
- dat+=span_italics("No feed messages found in channel...") + " "
+ dat+=span_italics("No feed messages found in channel...") + " "
else
for(var/datum/feed_message/MESSAGE in admin_holder.admincaster_feed_channel.messages)
dat+={"
- -[MESSAGE.body] "} + span_small("\[Story by [span_maroon("[MESSAGE.author]")]\]") + {"
- "} + span_normal("[(MESSAGE.body == "\[REDACTED\]") ? ("Undo story censorship") : ("Censor story")] - [(MESSAGE.author == "\[REDACTED\]") ? ("Undo Author Censorship") : ("Censor message Author")] ") + {"
+ -[MESSAGE.body] "} + span_small("\[Story by [span_maroon("[MESSAGE.author]")]\]") + {"
+ "} + span_normal("[(MESSAGE.body == "\[REDACTED\]") ? ("Undo story censorship") : ("Censor story")] - [(MESSAGE.author == "\[REDACTED\]") ? ("Undo Author Censorship") : ("Censor message Author")] ") + {"
"}
- dat+="Back "
+ dat+="Back "
if(13)
dat+={"
- "} + span_bold("[admin_holder.admincaster_feed_channel.channel_name]: ") + span_small("\[ created by: [span_maroon("[admin_holder.admincaster_feed_channel.author]")] \]") + {"
- Channel messages listed below. If you deem them dangerous to the station, you can Bestow a D-Notice upon the channel .
+ "} + span_bold("[admin_holder.admincaster_feed_channel.channel_name]: ") + span_small("\[ created by: [span_maroon("[admin_holder.admincaster_feed_channel.author]")] \]") + {"
+ Channel messages listed below. If you deem them dangerous to the station, you can Bestow a D-Notice upon the channel .
"}
if(admin_holder.admincaster_feed_channel.censored)
dat+={"
- "} + span_red(span_bold("ATTENTION: ")) + {"This channel has been deemed as threatening to the welfare of the station, and marked with a [using_map.company_name] D-Notice.
- No further feed story additions are allowed while the D-Notice is in effect.
+ "} + span_red(span_bold("ATTENTION: ")) + {"This channel has been deemed as threatening to the welfare of the station, and marked with a [using_map.company_name] D-Notice.
+ No further feed story additions are allowed while the D-Notice is in effect.
"}
else
if( isemptylist(admin_holder.admincaster_feed_channel.messages) )
- dat+=span_italics("No feed messages found in channel...") + " "
+ dat+=span_italics("No feed messages found in channel...") + " "
else
for(var/datum/feed_message/MESSAGE in admin_holder.admincaster_feed_channel.messages)
- dat+="-[MESSAGE.body] " + span_small("\[Story by [span_maroon("[MESSAGE.author]")]\]") + " "
+ dat+="-[MESSAGE.body] " + span_small("\[Story by [span_maroon("[MESSAGE.author]")]\]") + " "
- dat+="Back "
+ dat+="Back "
if(14)
dat+=span_bold("Wanted Issue Handler:")
var/wanted_already = 0
@@ -454,54 +454,54 @@ ADMIN_VERB(access_news_network, R_ADMIN|R_EVENT, "Access Newscaster Network", "A
wanted_already = 1
end_param = 2
if(wanted_already)
- dat+=span_normal(span_italics(" A wanted issue is already in Feed Circulation. You can edit or cancel it below."))
+ dat+=span_normal(span_italics(" A wanted issue is already in Feed Circulation. You can edit or cancel it below."))
dat+={"
- Criminal Name : [admin_holder.admincaster_feed_message.author]
- Description : [admin_holder.admincaster_feed_message.body]
+ Criminal Name : [admin_holder.admincaster_feed_message.author]
+ Description : [admin_holder.admincaster_feed_message.body]
"}
if(wanted_already)
- dat+=span_bold("Wanted Issue created by:") + span_green(" [GLOB.news_network.wanted_issue.backup_author]") + " "
+ dat+=span_bold("Wanted Issue created by:") + span_green(" [GLOB.news_network.wanted_issue.backup_author]") + " "
else
- dat+=span_bold("Wanted Issue will be created under prosecutor:") + span_green(" [admin_holder.admincaster_signature]") + " "
- dat+="[(wanted_already) ? ("Edit Issue") : ("Submit")] "
+ dat+=span_bold("Wanted Issue will be created under prosecutor:") + span_green(" [admin_holder.admincaster_signature]") + " "
+ dat+="[(wanted_already) ? ("Edit Issue") : ("Submit")] "
if(wanted_already)
- dat+="Take down Issue "
- dat+="Cancel "
+ dat+="Take down Issue "
+ dat+="Cancel "
if(15)
dat+={"
- "} + span_green("Wanted issue for [admin_holder.admincaster_feed_message.author] is now in Network Circulation.") + {"
- Return
+ "} + span_green("Wanted issue for [admin_holder.admincaster_feed_message.author] is now in Network Circulation.") + {"
+ Return
"}
if(16)
- dat+=span_bold(span_maroon("ERROR: Wanted Issue rejected by Network.")) + " "
+ dat+=span_bold(span_maroon("ERROR: Wanted Issue rejected by Network.")) + " "
if(admin_holder.admincaster_feed_message.author =="" || admin_holder.admincaster_feed_message.author == "\[REDACTED\]")
- dat+=span_maroon("Invalid name for person wanted.") + " "
+ dat+=span_maroon("Invalid name for person wanted.") + " "
if(admin_holder.admincaster_feed_message.body == "" || admin_holder.admincaster_feed_message.body == "\[REDACTED\]")
- dat+=span_maroon("Invalid description.") + " "
- dat+="Return "
+ dat+=span_maroon("Invalid description.") + " "
+ dat+="Return "
if(17)
dat+={"
- "} + span_bold("Wanted Issue successfully deleted from Circulation") + {"
- Return
+ "} + span_bold("Wanted Issue successfully deleted from Circulation") + {"
+ Return
"}
if(18)
dat+={"
- "} + span_bold(span_maroon("-- STATIONWIDE WANTED ISSUE --")) + {" "} + span_normal("\[Submitted by: [span_green("[GLOB.news_network.wanted_issue.backup_author]")]\]") + {"
- "} + span_bold("Criminal") + {": [GLOB.news_network.wanted_issue.author]
- "} + span_bold("Description") + {": [GLOB.news_network.wanted_issue.body]
+ "} + span_bold(span_maroon("-- STATIONWIDE WANTED ISSUE --")) + {" "} + span_normal("\[Submitted by: [span_green("[GLOB.news_network.wanted_issue.backup_author]")]\]") + {"
+ "} + span_bold("Criminal") + {": [GLOB.news_network.wanted_issue.author]
+ "} + span_bold("Description") + {": [GLOB.news_network.wanted_issue.body]
"} + span_bold("Photo:") + {":
"}
if(GLOB.news_network.wanted_issue.img)
user << browse_rsc(GLOB.news_network.wanted_issue.img, "tmp_photow.png")
- dat+=" "
+ dat+=" "
else
dat+="None"
- dat+="Back "
+ dat+="Back "
if(19)
dat+={"
- "} + span_green("Wanted issue for [admin_holder.admincaster_feed_message.author] successfully edited.") + {"
- Return
+ "} + span_green("Wanted issue for [admin_holder.admincaster_feed_message.author] successfully edited.") + {"
+ Return
"}
else
dat+="I'm sorry to break your immersion. This shit's bugged. Report this bug to Agouri, polyxenitopalidou@gmail.com"
@@ -523,7 +523,7 @@ ADMIN_VERB(access_news_network, R_ADMIN|R_EVENT, "Access Newscaster Network", "A
var/r = t
if( findtext(r,"##") )
r = copytext( r, 1, findtext(r,"##") )//removes the description
- dat += text("[t] (unban ) ")
+ dat += text("[t] (unban ) ")
dat += ""
var/datum/browser/popup = new(owner, "ban", "Job Bans", 400, 400)
@@ -536,20 +536,17 @@ ADMIN_VERB(access_news_network, R_ADMIN|R_EVENT, "Access Newscaster Network", "A
var/dat = {"
"} + span_bold("Game Panel") + {" \n
- Change Game Mode
+ Change Game Mode
"}
if(GLOB.master_mode == "secret")
- dat += "(Force Secret Mode) "
+ dat += "(Force Secret Mode) "
dat += {"
-
- Create Object
- Quick Create Object
- Create Turf
- Create Mob
- Edit Airflow Settings
- Edit Phoron Settings
- Choose a default ZAS setting
+
+ Spawn Panel
+ Edit Airflow Settings
+ Edit Phoron Settings
+ Choose a default ZAS setting
"}
var/datum/browser/popup = new(owner, "admin2", "Game Panel", 220, 295)
@@ -565,19 +562,19 @@ ADMIN_VERB(access_news_network, R_ADMIN|R_EVENT, "Access Newscaster Network", "A
for(var/datum/admin_secret_category/category in admin_secrets.categories)
if(!category.can_view(usr))
continue
- dat += "[category.name] "
+ dat += "[category.name] "
dat += " "
// If a category is selected, print its description and then options
if(istype(active_category) && active_category.can_view(usr))
- dat += span_bold("[active_category.name]") + " "
+ dat += span_bold("[active_category.name]") + " "
if(active_category.desc)
- dat += span_italics("[active_category.desc]") + " "
+ dat += span_italics("[active_category.desc]") + " "
for(var/datum/admin_secret_item/item in active_category.items)
if(!item.can_view(usr))
continue
- dat += "[item.name()] "
- dat += " "
+ dat += "[item.name()] "
+ dat += " "
var/datum/browser/popup = new(usr, "secrets", "Secrets", 500, 500)
popup.set_content(dat)
@@ -1072,66 +1069,66 @@ ADMIN_VERB(show_game_mode, R_ADMIN|R_EVENT, "Show Game Mode", "Show the current
tgui_alert_async(user, "Not before roundstart!", "Alert")
return
- var/out = span_large(span_bold("Current mode: [SSticker.mode.name] ([SSticker.mode.config_tag] )")) + " "
+ var/out = span_large(span_bold("Current mode: [SSticker.mode.name] ([SSticker.mode.config_tag] )")) + " "
out += " "
if(SSticker.mode.ert_disabled)
- out += span_bold("Emergency Response Teams:") + "disabled "
+ out += span_bold("Emergency Response Teams:") + "disabled "
else
- out += span_bold("Emergency Response Teams:") + "enabled "
+ out += span_bold("Emergency Response Teams:") + "enabled "
out += " "
if(SSticker.mode.deny_respawn)
- out += span_bold("Respawning:") + "disallowed "
+ out += span_bold("Respawning:") + "disallowed "
else
- out += span_bold("Respawning:") + "allowed "
+ out += span_bold("Respawning:") + "allowed "
out += " "
- out += span_bold("Shuttle delay multiplier:") + " [SSticker.mode.shuttle_delay] "
+ out += span_bold("Shuttle delay multiplier:") + " [SSticker.mode.shuttle_delay] "
if(SSticker.mode.auto_recall_shuttle)
- out += span_bold("Shuttle auto-recall:") + " enabled "
+ out += span_bold("Shuttle auto-recall:") + " enabled "
else
- out += span_bold("Shuttle auto-recall:") + " disabled "
+ out += span_bold("Shuttle auto-recall:") + " disabled "
out += " "
if(SSticker.mode.event_delay_mod_moderate)
- out += span_bold("Moderate event time modifier:") + " [SSticker.mode.event_delay_mod_moderate] "
+ out += span_bold("Moderate event time modifier:") + " [SSticker.mode.event_delay_mod_moderate] "
else
- out += span_bold("Moderate event time modifier:") + " unset "
+ out += span_bold("Moderate event time modifier:") + " unset "
if(SSticker.mode.event_delay_mod_major)
- out += span_bold("Major event time modifier:") + " [SSticker.mode.event_delay_mod_major] "
+ out += span_bold("Major event time modifier:") + " [SSticker.mode.event_delay_mod_major] "
else
- out += span_bold("Major event time modifier:") + " unset "
+ out += span_bold("Major event time modifier:") + " unset "
out += " "
if(SSticker.mode.antag_tags && SSticker.mode.antag_tags.len)
out += span_bold("Core antag templates:") + ""
for(var/antag_tag in SSticker.mode.antag_tags)
- out += "[antag_tag] ."
+ out += "[antag_tag] ."
if(SSticker.mode.round_autoantag)
- out += span_bold("Autotraitor enabled .")
+ out += span_bold("Autotraitor enabled .")
if(SSticker.mode.antag_scaling_coeff > 0)
- out += " (scaling with [SSticker.mode.antag_scaling_coeff] )"
+ out += " (scaling with [SSticker.mode.antag_scaling_coeff] )"
else
- out += " (not currently scaling, set a coefficient )"
+ out += " (not currently scaling, set a coefficient )"
out += " "
else
- out += span_bold("Autotraitor disabled .") + " "
+ out += span_bold("Autotraitor disabled .") + " "
out += span_bold("All antag ids:")
if(SSticker.mode.antag_templates && SSticker.mode.antag_templates.len)
for(var/datum/antagonist/antag in SSticker.mode.antag_templates)
antag.update_current_antag_max()
- out += " [antag.id] "
+ out += " [antag.id] "
out += " ([antag.get_antag_count()]/[antag.cur_max]) "
- out += " \[-\] "
+ out += " \[-\] "
else
out += " None."
- out += " \[+\] "
+ out += " \[+\] "
var/datum/browser/popup = new(user, "edit_mode[user.holder]", "Edit Game Mode")
popup.set_content(out)
@@ -1188,19 +1185,19 @@ ADMIN_VERB(toggleguests, R_HOST, "Toggle guests", "Guests can't enter.", ADMIN_C
return span_bold("[key_name(C, link, name, highlight_special)]")
if(1) //Private Messages
- return span_bold("[key_name(C, link, name, highlight_special)](? )")
+ return span_bold("[key_name(C, link, name, highlight_special)](? )")
if(2) //Admins
- var/ref_mob = "\ref[M]"
- return span_bold("[key_name(C, link, name, highlight_special)](? ) (PP ) (VV ) (SM ) ([admin_jump_link(M)]) (CA ) (TAKE )")
+ var/ref_mob = "[REF(M)]"
+ return span_bold("[key_name(C, link, name, highlight_special)](? ) (PP ) (VV ) (SM ) ([admin_jump_link(M)]) (CA ) (TAKE )")
if(3) //Devs
- var/ref_mob = "\ref[M]"
- return span_bold("[key_name(C, link, name, highlight_special)](VV )([admin_jump_link(M)]) (TAKE )")
+ var/ref_mob = "[REF(M)]"
+ return span_bold("[key_name(C, link, name, highlight_special)](VV )([admin_jump_link(M)]) (TAKE )")
if(4) //Event Managers
- var/ref_mob = "\ref[M]"
- return span_bold("[key_name(C, link, name, highlight_special)] (? ) (PP ) (VV ) (SM ) ([admin_jump_link(M)]) (TAKE )")
+ var/ref_mob = "[REF(M)]"
+ return span_bold("[key_name(C, link, name, highlight_special)] (? ) (PP ) (VV ) (SM ) ([admin_jump_link(M)]) (TAKE )")
/proc/ishost(whom)
@@ -1369,12 +1366,12 @@ ADMIN_VERB(sendFax, R_ADMIN|R_MOD|R_EVENT, "Send Fax", "Sends a fax to this mach
log_admin("[key_name(src.owner)] replied to a fax message from [key_name(P.sender)]")
for(var/client/C in GLOB.admins)
if(check_rights_for(C, (R_ADMIN | R_MOD | R_EVENT)))
- to_chat(C, span_log_message("[span_prefix("FAX LOG:")][key_name_admin(src.owner)] replied to a fax message from [key_name_admin(P.sender)] (VIEW )"))
+ to_chat(C, span_log_message("[span_prefix("FAX LOG:")][key_name_admin(src.owner)] replied to a fax message from [key_name_admin(P.sender)] (VIEW )"))
else
log_admin("[key_name(src.owner)] has sent a fax message to [destination.department]")
for(var/client/C in GLOB.admins)
if(check_rights_for(C, (R_ADMIN | R_MOD | R_EVENT)))
- to_chat(C, span_log_message("[span_prefix("FAX LOG:")][key_name_admin(src.owner)] has sent a fax message to [destination.department] (VIEW )"))
+ to_chat(C, span_log_message("[span_prefix("FAX LOG:")][key_name_admin(src.owner)] has sent a fax message to [destination.department] (VIEW )"))
var/plaintext_title = P.sender ? "replied to [key_name(P.sender)]'s fax" : "sent a fax message to [destination.department]"
var/fax_text = paper_html_to_plaintext(P.info)
diff --git a/code/modules/admin/create_mob.dm b/code/modules/admin/create_mob.dm
deleted file mode 100644
index fa561d2e5a..0000000000
--- a/code/modules/admin/create_mob.dm
+++ /dev/null
@@ -1,9 +0,0 @@
-/var/create_mob_html = null
-/datum/admins/proc/create_mob(var/mob/user)
- if (!create_mob_html)
- var/mobjs = null
- mobjs = jointext(typesof(/mob), ";")
- create_mob_html = file2text('html/create_object.html')
- create_mob_html = replacetext(create_mob_html, "null /* object types */", "\"[mobjs]\"")
-
- user << browse(create_panel_helper(create_mob_html), "window=create_mob;size=680x600")
diff --git a/code/modules/admin/create_object.dm b/code/modules/admin/create_object.dm
deleted file mode 100644
index 8892232165..0000000000
--- a/code/modules/admin/create_object.dm
+++ /dev/null
@@ -1,47 +0,0 @@
-/datum/admins/proc/create_panel_helper(template)
- var/final_html = replacetext(template, "/* ref src */", "\ref[src];[HrefToken()]")
- final_html = replacetext(final_html,"/* hreftokenfield */","[HrefTokenFormField()]")
- return final_html
-
-/datum/admins/proc/create_object(var/mob/user)
- var/static/create_object_html = null
- if (!create_object_html)
- var/objectjs = null
- objectjs = jointext(typesof(/obj), ";")
- create_object_html = file2text('html/create_object.html')
- create_object_html = replacetext(create_object_html, "null /* object types */", "\"[objectjs]\"")
-
- user << browse(create_panel_helper(create_object_html), "window=create_object;size=680x600")
-
-
-/datum/admins/proc/quick_create_object(var/mob/user)
-
- var/quick_create_object_html = null
- var/pathtext = null
- var/list/choices = list("/obj",
- "/obj/structure",
- "/obj/item",
- "/obj/item/melee",
- "/obj/item/gun",
- "/obj/item/reagent_containers",
- "/obj/item/reagent_containers/food",
- "/obj/item/clothing",
- "/obj/item/storage/box/fluff", //VOREStation Edit,
- "/obj/machinery",
- "/obj/mecha",
- "/obj/item/mecha_parts",
- "/obj/item/mecha_parts/mecha_equipment")
-
- pathtext = tgui_input_list(usr, "Select the path of the object you wish to create.", "Path", choices, "/obj")
-
- if(!pathtext)
- return
- var path = text2path(pathtext)
-
- if (!quick_create_object_html)
- var/objectjs = null
- objectjs = jointext(typesof(path), ";")
- quick_create_object_html = file2text('html/create_object.html')
- quick_create_object_html = replacetext(quick_create_object_html, "null /* object types */", "\"[objectjs]\"")
-
- user << browse(create_panel_helper(quick_create_object_html), "window=quick_create_object;size=680x600")
diff --git a/code/modules/admin/create_turf.dm b/code/modules/admin/create_turf.dm
deleted file mode 100644
index 5a3efffd87..0000000000
--- a/code/modules/admin/create_turf.dm
+++ /dev/null
@@ -1,9 +0,0 @@
-/var/create_turf_html = null
-/datum/admins/proc/create_turf(var/mob/user)
- if (!create_turf_html)
- var/turfjs = null
- turfjs = jointext(typesof(/turf), ";")
- create_turf_html = file2text('html/create_object.html')
- create_turf_html = replacetext(create_turf_html, "null /* object types */", "\"[turfjs]\"")
-
- user << browse(create_panel_helper(create_turf_html), "window=create_turf;size=680x600")
diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm
index c41a236a17..17a8bdc9e9 100644
--- a/code/modules/admin/holder2.dm
+++ b/code/modules/admin/holder2.dm
@@ -32,6 +32,7 @@ GLOBAL_PROTECT(href_token)
var/datum/filter_editor/filteriffic
var/datum/particle_editor/particle_test
var/datum/whitelist_editor/whitelist_editor
+ var/datum/spawnpanel/spawn_panel
/// A lazylist of tagged datums, for quick reference with the View Tags verb
var/list/tagged_datums
diff --git a/code/modules/admin/modify_robot.dm b/code/modules/admin/modify_robot.dm
index b03eeae0af..6b80cf9d1b 100644
--- a/code/modules/admin/modify_robot.dm
+++ b/code/modules/admin/modify_robot.dm
@@ -219,63 +219,14 @@
target.module_reset(FALSE)
return TRUE
if("add_module")
- var/obj/item/add_item = locate(params["module"])
- if(!add_item)
+ var/obj/item/selected_item = locate(params["module"])
+ if(!selected_item)
return TRUE
- if(istype(add_item, /obj/item/card/id))
+ if(istype(selected_item, /obj/item/card/id))
source.idcard = null
- source.module.emag.Remove(add_item)
- source.module.modules.Remove(add_item)
- source.module.contents.Remove(add_item)
- if(istype(add_item, /obj/item/card/id))
- if(target.idcard)
- qdel(target.idcard)
- target.idcard = add_item
- target.module.modules.Add(add_item)
- target.module.contents.Add(add_item)
- spawn(0)
- SEND_SIGNAL(add_item, COMSIG_MOVABLE_ATTEMPTED_MOVE)
- target.hud_used?.update_robot_modules_display()
- if(istype(add_item, /obj/item/stack/))
- var/obj/item/stack/item_with_synth = add_item
- for(var/synth in item_with_synth.synths)
- var/found = target.module.synths.Find(synth)
- if(!found)
- source.module.synths.Remove(synth)
- target.module.synths.Add(synth)
- else
- item_with_synth.synths = list(target.module.synths[found])
- return TRUE
- if(istype(add_item, /obj/item/matter_decompiler/) || istype(add_item, /obj/item/dogborg/sleeper/compactor/decompiler/))
- var/obj/item/matter_decompiler/item_with_matter = add_item
- if(item_with_matter.metal)
- var/found = target.module.synths.Find(item_with_matter.metal)
- if(!found)
- source.module.synths.Remove(item_with_matter.metal)
- target.module.synths.Add(item_with_matter.metal)
- else
- item_with_matter.metal = target.module.synths[found]
- if(item_with_matter.glass)
- var/found = target.module.synths.Find(item_with_matter.glass)
- if(!found)
- source.module.synths.Remove(item_with_matter.glass)
- target.module.synths.Add(item_with_matter.glass)
- else
- item_with_matter.glass = target.module.synths[found]
- if(item_with_matter.wood)
- var/found = target.module.synths.Find(item_with_matter.wood)
- if(!found)
- source.module.synths.Remove(item_with_matter.wood)
- target.module.synths.Add(item_with_matter.wood)
- else
- item_with_matter.wood = target.module.synths[found]
- if(item_with_matter.plastic)
- var/found = target.module.synths.Find(item_with_matter.plastic)
- if(!found)
- source.module.synths.Remove(item_with_matter.plastic)
- target.module.synths.Add(item_with_matter.plastic)
- else
- item_with_matter.plastic = target.module.synths[found]
+ source.module.emag -= selected_item
+ source.module.modules -= selected_item
+ target.module.add_item(selected_item, target)
return TRUE
if("rem_module")
var/obj/item/rem_item = locate(params["module"])
diff --git a/code/modules/admin/spawn_panel/admin_hotkeys.dm b/code/modules/admin/spawn_panel/admin_hotkeys.dm
new file mode 100644
index 0000000000..bd5d165670
--- /dev/null
+++ b/code/modules/admin/spawn_panel/admin_hotkeys.dm
@@ -0,0 +1,9 @@
+// Open "View Variables" menu for target
+/mob/observer/dead/CtrlShiftClickOn(atom/target)
+ if(check_rights(R_DEBUG))
+ SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/debug_variables, target)
+
+// Open "Show Player Panel" menu for target mob
+/mob/observer/dead/CtrlClickOn(atom/target)
+ if(check_rights(R_ADMIN) && ismob(target))
+ SSadmin_verbs.dynamic_invoke_verb(client, /datum/admin_verb/show_player_panel, target)
diff --git a/code/modules/admin/spawn_panel/assets.dm b/code/modules/admin/spawn_panel/assets.dm
new file mode 100644
index 0000000000..ad8cd8623e
--- /dev/null
+++ b/code/modules/admin/spawn_panel/assets.dm
@@ -0,0 +1,51 @@
+/datum/asset/json/spawnpanel
+ name = "spawnpanel_atom_data"
+
+/datum/asset/json/spawnpanel/generate()
+ var/list/data = list()
+
+ var/static/list/mapping_objects = typecacheof(list(
+ /obj/effect/landmark,
+ /obj/effect/spawner,
+ /obj/effect/shuttle_landmark,
+ /obj/effect/floor_decal,
+ /obj/effect/decal,
+ /obj/effect/overlay,
+ /obj/effect/step_trigger,
+ /obj/effect/overmap,
+ /obj/effect/zone_divider,
+ ))
+
+ data["atoms"] = list()
+
+ for(var/obj/each_object as anything in typesof(/obj))
+ data["atoms"][each_object] = list(
+ "icon" = /* each_object?.icon_preview ||*/ each_object?.icon || "none",
+ "icon_state" = /* each_object?.icon_state_preview ||*/ each_object?.icon_state || "none",
+ "name" = each_object.name,
+ "description" = each_object.desc,
+ "mapping" = is_type_in_typecache(each_object, mapping_objects),
+ "type" = "Objects"
+ )
+
+ for(var/turf/each_turf as anything in typesof(/turf))
+ data["atoms"][each_turf] = list(
+ "icon" = each_turf?.icon || "noneturf",
+ "icon_state" = each_turf?.icon_state || "noneturf",
+ "name" = each_turf.name,
+ "description" = each_turf.desc,
+ "mapping" = is_type_in_typecache(each_turf, mapping_objects),
+ "type" = "Turfs"
+ )
+
+ for(var/mob/each_mob as anything in typesof(/mob))
+ data["atoms"][each_mob] = list(
+ "icon" = each_mob?.icon || "nonemob",
+ "icon_state" = each_mob?.icon_state || "nonemob",
+ "name" = each_mob.name,
+ "description" = each_mob.desc,
+ "mapping" = is_type_in_typecache(each_mob, mapping_objects),
+ "type" = "Mobs"
+ )
+
+ return data
diff --git a/code/modules/admin/spawn_panel/spawn_handling.dm b/code/modules/admin/spawn_panel/spawn_handling.dm
new file mode 100644
index 0000000000..d3e71ce057
--- /dev/null
+++ b/code/modules/admin/spawn_panel/spawn_handling.dm
@@ -0,0 +1,161 @@
+#define WHERE_FLOOR_BELOW_MOB "Current location"
+#define WHERE_SUPPLY_BELOW_MOB "Current location (droppod)"
+#define WHERE_MOB_HAND "In own mob's hand"
+#define WHERE_MARKED_OBJECT "At a marked object"
+#define WHERE_IN_MARKED_OBJECT "In the marked object"
+#define WHERE_TARGETED_LOCATION "Targeted location"
+#define WHERE_TARGETED_LOCATION_POD "Targeted location (droppod)"
+#define WHERE_TARGETED_MOB_HAND "In targeted mob's hand"
+
+#define OFFSET_ABSOLUTE "Absolute offset"
+#define OFFSET_RELATIVE "Relative offset"
+
+/*
+ Handles spawning an atom. See the call examples for the proper spawn parameters fetching.
+*/
+/datum/spawnpanel/proc/spawn_atom(list/spawn_params, mob/user)
+ if(!check_rights_for(user.client, R_SPAWN) || !spawn_params)
+ return
+
+ var/atom/atom_to_spawn = spawn_params["selected_atom"]
+
+ if(!atom_to_spawn || (!ispath(atom_to_spawn, /obj) && !ispath(atom_to_spawn, /turf) && !ispath(atom_to_spawn, /mob)))
+ return
+
+ var/amount = clamp(text2num(spawn_params["atom_amount"]), 1, ADMIN_SPAWN_CAP)
+
+ var/X = offset["X"] || 0
+ var/Y = offset["Y"] || 0
+ var/Z = offset["Z"] || 0
+
+ var/atom_dir = text2num(spawn_params["atom_dir"]) || 1
+ var/atom_name = sanitize(spawn_params["atom_name"])
+
+ var/where_target_type = spawn_params["where_target_type"]
+ var/atom/target = null
+
+ if(where_target_type == WHERE_MOB_HAND || where_target_type == WHERE_TARGETED_MOB_HAND)
+ target = (where_target_type == WHERE_TARGETED_MOB_HAND ? spawn_params["target"] : user)
+
+ if(!target)
+ to_chat(user, span_warning("No target specified."))
+ return
+
+ if(!ismob(target))
+ to_chat(user, span_warning("The targeted atom is not a mob."))
+ return
+
+ if(!iscarbon(target) && !isrobot(target))
+ to_chat(user, span_warning("Can only spawn in hand when the target is a carbon mob or a cyborg."))
+ where_target_type = WHERE_FLOOR_BELOW_MOB
+
+ else if(where_target_type == WHERE_MARKED_OBJECT || where_target_type == WHERE_IN_MARKED_OBJECT)
+ if(!user.client.holder.marked_datum)
+ to_chat(user, span_warning("You don't have any object marked."))
+ return
+ else if(!istype(user.client.holder.marked_datum, /atom))
+ to_chat(user, span_warning("The object you have marked cannot be used as a target. Target must be of type /atom."))
+ return
+ else
+ target = (where_target_type == WHERE_MARKED_OBJECT ? get_turf(user.client.holder.marked_datum) : user.client.holder.marked_datum)
+
+ else if(where_target_type == WHERE_TARGETED_LOCATION || where_target_type == WHERE_TARGETED_LOCATION_POD)
+ target = spawn_params["target"]
+ else
+ switch(spawn_params["offset_type"])
+ if(OFFSET_ABSOLUTE)
+ target = locate(X, Y, Z)
+
+ if(OFFSET_RELATIVE)
+ var/turf/relative_turf
+ var/atom/user_loc = user.loc
+
+ if (user_loc)
+ relative_turf = get_turf(user_loc)
+
+ if (!relative_turf)
+ if(isobserver(user))
+ var/mob/observer/dead/user_observer = user
+ relative_turf = get_turf(user_observer.client?.eye) || get_turf(user_observer)
+ if (!relative_turf)
+ relative_turf = locate(1, 1, 1)
+
+ if (!relative_turf)
+ to_chat(user, span_warning("Could not determine a valid relative location."))
+ return
+
+ target = locate(relative_turf.x + X, relative_turf.y + Y, relative_turf.z + Z)
+
+ if(!target)
+ return
+
+ var/use_droppod = where_target_type == WHERE_SUPPLY_BELOW_MOB || where_target_type == WHERE_TARGETED_LOCATION_POD
+
+ var/obj/structure/drop_pod/polite/pod
+ if(use_droppod)
+ pod = new(target)
+
+ for(var/i in 1 to amount)
+ if(istype(atom_to_spawn, /turf))
+ var/turf/original_turf = target
+ var/turf/created_turf = original_turf.ChangeTurf(atom_to_spawn.type)
+ if(created_turf && atom_name)
+ created_turf.name = atom_name
+ continue
+
+ var/atom/created_atom = new atom_to_spawn(use_droppod ? pod : target)
+
+ if(QDELETED(created_atom))
+ return
+
+ created_atom.flags |= ADMIN_SPAWNED
+
+ if(spawn_params["apply_icon_override"])
+ if(spawn_params["selected_atom_icon"])
+ created_atom.icon = file(spawn_params["selected_atom_icon"])
+
+ if(spawn_params["selected_atom_icon_state"])
+ created_atom.icon_state = spawn_params["selected_atom_icon_state"]
+
+ if(spawn_params["atom_icon_size"])
+ if(ismob(created_atom))
+ var/mob/living/created_mob = created_atom
+ created_mob.size_multiplier = spawn_params["atom_icon_size"] / 100
+
+ if(atom_dir)
+ created_atom.set_dir(atom_dir)
+
+ if(atom_name)
+ created_atom.name = atom_name
+ if(ismob(created_atom))
+ var/mob/created_mob = created_atom
+ created_mob.real_name = atom_name
+
+ if((where_target_type == WHERE_MOB_HAND || where_target_type == WHERE_TARGETED_MOB_HAND) && isliving(target) && isitem(created_atom))
+ var/mob/living/living_target = target
+ var/obj/item/created_item = created_atom
+ living_target.put_in_hands(created_item)
+
+ if(isrobot(living_target))
+ var/mob/living/silicon/robot/target_robot = living_target
+ if(target_robot.module)
+ target_robot.module.add_item(created_item, target_robot)
+ target_robot.activate_module(created_item)
+
+ if(pod)
+ pod.podfall()
+
+ log_admin("[key_name(user)] created [amount == 1 ? "an instance" : "[amount] instances"] of [atom_to_spawn.type]")
+ if(istype(atom_to_spawn, /mob))
+ message_admins("[key_name_admin(user)] created [amount == 1 ? "an instance" : "[amount] instances"] of [atom_to_spawn.type]")
+
+#undef WHERE_FLOOR_BELOW_MOB
+#undef WHERE_SUPPLY_BELOW_MOB
+#undef WHERE_MOB_HAND
+#undef WHERE_MARKED_OBJECT
+#undef WHERE_IN_MARKED_OBJECT
+#undef WHERE_TARGETED_LOCATION
+#undef WHERE_TARGETED_LOCATION_POD
+#undef WHERE_TARGETED_MOB_HAND
+#undef OFFSET_ABSOLUTE
+#undef OFFSET_RELATIVE
diff --git a/code/modules/admin/spawn_panel/spawn_panel.dm b/code/modules/admin/spawn_panel/spawn_panel.dm
new file mode 100644
index 0000000000..6190615184
--- /dev/null
+++ b/code/modules/admin/spawn_panel/spawn_panel.dm
@@ -0,0 +1,304 @@
+#define WHERE_FLOOR_BELOW_MOB "Current location"
+#define WHERE_SUPPLY_BELOW_MOB "Current location (droppod)"
+#define WHERE_MOB_HAND "In own mob's hand"
+#define WHERE_MARKED_OBJECT "At a marked object"
+#define WHERE_IN_MARKED_OBJECT "In the marked object"
+#define WHERE_TARGETED_LOCATION "Targeted location"
+#define WHERE_TARGETED_LOCATION_POD "Targeted location (droppod)"
+#define WHERE_TARGETED_MOB_HAND "In targeted mob's hand"
+
+#define PRECISE_MODE_OFF "Off"
+#define PRECISE_MODE_TARGET "Target"
+#define PRECISE_MODE_MARK "Mark"
+#define PRECISE_MODE_COPY "Copy"
+
+#define OFFSET_ABSOLUTE "Absolute offset"
+#define OFFSET_RELATIVE "Relative offset"
+
+/*
+ An instance of a /tg/UI™ Spawn Panel. Stores preferences, spawns things, controls the UI. Unique for each user (their ckey).
+*/
+/datum/spawnpanel
+ /// Where and how the atom should be spawned.
+ var/where_target_type = WHERE_FLOOR_BELOW_MOB
+ /// The atom selected from the panel.
+ var/atom/selected_atom = null
+ /// The icon selected for the atom from the panel.
+ var/selected_atom_icon = null
+ /// The icon state selected for the atom from the panel.
+ var/selected_atom_icon_state = null
+ /// Should selected icon/icon state override the initial ones? Added as an edge case to not replace animated GAGS icons.
+ var/apply_icon_override = FALSE
+ /// A list of icon states to display in preview panels.
+ var/list/available_icon_states = null
+ /// Override for the icon size of the spawned mob.
+ var/atom_icon_size = 100
+ /// How many atoms will be spawned at once.
+ var/atom_amount = 1
+ /// Custom atom name (leave `null` for initial).
+ var/atom_name = null
+ /// Custom atom description (leave `null` for initial).
+ var/atom_desc = null
+ /// Custom atom dir (leave `null` for `2`).
+ var/atom_dir = 1
+ /// An associative list of x-y-z offsets.
+ var/offset = list()
+ /// The pivot point for offsetting — relative or absolute.
+ var/offset_type = OFFSET_RELATIVE
+ /// Precise mode toggle. Used for build-mode-like spawning experience and targeting datums.
+ var/precise_mode = PRECISE_MODE_OFF
+
+/datum/spawnpanel/New()
+ . = ..()
+ offset = list("X" = 0, "Y" = 0, "Z" = 0)
+
+/datum/spawnpanel/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "SpawnPanel")
+ ui.open()
+
+/datum/spawnpanel/tgui_close(mob/user)
+ . = ..()
+ if (precise_mode && precise_mode != PRECISE_MODE_OFF)
+ toggle_precise_mode(PRECISE_MODE_OFF, user)
+
+/datum/spawnpanel/tgui_state(mob/user)
+ return ADMIN_STATE(R_SPAWN)
+
+/datum/spawnpanel/tgui_act(action, params, datum/tgui/ui)
+ if(..() || !check_rights_for(ui.user.client, R_SPAWN))
+ return FALSE
+
+ switch(action)
+ if("select-new-DMI")
+ var/icon/new_icon = input("Select a new icon file:", "Icon") as null|icon
+ if(new_icon)
+ selected_atom_icon = new_icon
+ available_icon_states = icon_states(selected_atom_icon)
+ if(!(selected_atom_icon_state in available_icon_states))
+ selected_atom_icon_state = available_icon_states[1]
+ return TRUE
+
+ if("set-apply-icon-override")
+ apply_icon_override = !!params["value"]
+ return TRUE
+
+ if("reset-DMI-icon")
+ selected_atom_icon = null
+ selected_atom_icon_state = null
+ if(selected_atom)
+ selected_atom_icon = initial(selected_atom.icon)
+ selected_atom_icon_state = initial(selected_atom.icon_state)
+ available_icon_states = icon_states(selected_atom_icon)
+ else
+ available_icon_states = list()
+ return TRUE
+
+ if("select-new-icon-state")
+ selected_atom_icon_state = params["new_state"]
+ return TRUE
+
+ if("reset-icon-state")
+ selected_atom_icon_state = null
+ if(selected_atom)
+ selected_atom_icon_state = initial(selected_atom.icon_state)
+ return TRUE
+
+ if("set-icon-size")
+ atom_icon_size = params["size"]
+ return TRUE
+
+ if("reset-icon-size")
+ atom_icon_size = 100
+ return TRUE
+
+ if("get-icon-states")
+ available_icon_states = icon_states(selected_atom_icon)
+ return TRUE
+
+ if("selected-atom-changed")
+ var/path = text2path(params["newObj"])
+ if(path)
+ var/atom/temp_atom = path
+ selected_atom_icon = initial(temp_atom.icon)
+ selected_atom_icon_state = initial(temp_atom.icon_state)
+ available_icon_states = icon_states(selected_atom_icon)
+ selected_atom = temp_atom
+ return TRUE
+
+ if("create-atom-action")
+ var/list/spawn_params = list(
+ "selected_atom" = selected_atom,
+ "offset" = params["offset"],
+ "atom_dir" = text2num(params["dir"]) || 1,
+ "atom_amount" = text2num(params["atom_amount"]) || 1,
+ "atom_name" = params["atom_name"],
+ "where_target_type" = params["where_target_type"] || WHERE_FLOOR_BELOW_MOB,
+ "atom_icon_size" = params["atom_icon_size"],
+ "offset_type" = params["offset_type"] || OFFSET_RELATIVE,
+ "apply_icon_override" = apply_icon_override,
+ )
+
+ if(apply_icon_override)
+ spawn_params["selected_atom_icon"] = selected_atom_icon
+ spawn_params["selected_atom_icon_state"] = selected_atom_icon_state
+
+ spawn_atom(spawn_params, ui.user)
+ return TRUE
+
+ if("toggle-precise-mode")
+ var/precise_type = params["newPreciseType"]
+ if(precise_type == PRECISE_MODE_TARGET && params["where_target_type"])
+ where_target_type = params["where_target_type"]
+ toggle_precise_mode(precise_type, ui.user)
+ return TRUE
+
+ if("update-settings")
+ if(params["atom_amount"])
+ atom_amount = text2num(params["atom_amount"])
+ if(params["atom_dir"])
+ atom_dir = text2num(params["atom_dir"])
+ if(params["offset"])
+ var/list/temp_offset = params["offset"]
+ offset["X"] = temp_offset[1]
+ offset["Y"] = temp_offset[2]
+ offset["Z"] = temp_offset[3]
+ if(params["atom_name"])
+ atom_name = params["atom_name"]
+ if(params["where_target_type"])
+ where_target_type = params["where_target_type"]
+ if(params["offset_type"])
+ offset_type = params["offset_type"]
+ if(params["atom_icon_size"])
+ atom_icon_size = text2num(params["atom_icon_size"])
+ if(params["selected_atom_icon"])
+ selected_atom_icon = params["selected_atom_icon"]
+ if(params["selected_atom_icon_state"])
+ selected_atom_icon_state = params["selected_atom_icon_state"]
+ return TRUE
+
+/datum/spawnpanel/proc/toggle_precise_mode(precise_type, mob/user)
+ precise_mode = precise_type
+ var/client/admin_client = user.client
+ if (!admin_client)
+ return
+
+ /* Unimplemented
+ admin_client.mouse_up_icon = null
+ admin_client.mouse_down_icon = null
+ admin_client.mouse_override_icon = null
+ */
+ admin_client.click_intercept = null
+
+ if (precise_mode != PRECISE_MODE_OFF)
+ /* Unimplemented
+ admin_client.mouse_up_icon = 'icons/effects/mouse_pointers/supplypod_pickturf.dmi'
+ admin_client.mouse_down_icon = 'icons/effects/mouse_pointers/supplypod_pickturf_down.dmi'
+ admin_client.mouse_override_icon = admin_client.mouse_up_icon
+ admin_client.mouse_pointer_icon = admin_client.mouse_override_icon
+ */
+ admin_client.click_intercept = src
+ winset(admin_client, "mapwindow.map", "right-click=true")
+ else
+ winset(admin_client, "mapwindow.map", "right-click=false")
+
+ /* Unimplemented
+ var/mob/holder_mob = admin_client.mob
+ holder_mob?.update_mouse_pointer()
+ */
+
+/datum/spawnpanel/proc/InterceptClickOn(mob/user, params, atom/target)
+ var/list/modifiers = params2list(params)
+ var/left_click = LAZYACCESS(modifiers, LEFT_CLICK)
+ var/right_click = LAZYACCESS(modifiers, RIGHT_CLICK)
+
+ if(right_click)
+ toggle_precise_mode(PRECISE_MODE_OFF, user)
+ SStgui.update_uis(src)
+ return TRUE
+
+ if(left_click)
+ if(istype(target,/atom/movable/screen))
+ return FALSE
+
+ var/turf/clicked_turf = get_turf(target)
+ if(!clicked_turf)
+ return FALSE
+
+ switch(precise_mode)
+ if(PRECISE_MODE_TARGET)
+ var/list/spawn_params = list(
+ "selected_atom" = selected_atom,
+ "atom_amount" = atom_amount,
+ "offset" = "0,0,0",
+ "atom_dir" = atom_dir,
+ "atom_name" = atom_name,
+ "atom_desc" = atom_desc,
+ "offset_type" = OFFSET_ABSOLUTE,
+ "where_target_type" = where_target_type,
+ "target" = target,
+ "atom_icon_size" = atom_icon_size,
+ "apply_icon_override" = apply_icon_override,
+ )
+
+ if(apply_icon_override)
+ spawn_params["selected_atom_icon"] = selected_atom_icon
+ spawn_params["selected_atom_icon_state"] = selected_atom_icon_state
+
+ if(where_target_type == WHERE_TARGETED_LOCATION || where_target_type == WHERE_TARGETED_LOCATION_POD)
+ spawn_params["X"] = clicked_turf.x
+ spawn_params["Y"] = clicked_turf.y
+ spawn_params["Z"] = clicked_turf.z
+
+ spawn_atom(spawn_params, user)
+
+ if(PRECISE_MODE_MARK)
+ var/client/admin_client = user.client
+ admin_client.mark_datum(target)
+ to_chat(user, span_notice("Marked object: [icon2html(target, user)] [span_bold("[target]")]"))
+ toggle_precise_mode(PRECISE_MODE_OFF, user)
+ SStgui.update_uis(src)
+
+ if(PRECISE_MODE_COPY)
+ to_chat(user, span_notice("Picked object: [icon2html(target, user)] [span_bold("[target]")]"))
+ selected_atom = target
+ toggle_precise_mode(PRECISE_MODE_OFF, user)
+ SStgui.update_uis(src)
+
+ return TRUE
+
+/datum/spawnpanel/tgui_data(mob/user)
+ var/data = list()
+ data["icon"] = selected_atom_icon
+ data["iconState"] = selected_atom_icon_state
+ data["iconSize"] = atom_icon_size
+ data["apply_icon_override"] = apply_icon_override
+ var/list/states = list()
+ if(available_icon_states)
+ for(var/state in available_icon_states)
+ states += state
+ data["iconStates"] = states
+ data["precise_mode"] = precise_mode
+ data["selected_object"] = selected_atom ? "[selected_atom.type]" : ""
+ return data
+
+/datum/spawnpanel/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/json/spawnpanel),
+ )
+
+#undef WHERE_FLOOR_BELOW_MOB
+#undef WHERE_SUPPLY_BELOW_MOB
+#undef WHERE_MOB_HAND
+#undef WHERE_MARKED_OBJECT
+#undef WHERE_IN_MARKED_OBJECT
+#undef WHERE_TARGETED_LOCATION
+#undef WHERE_TARGETED_LOCATION_POD
+#undef WHERE_TARGETED_MOB_HAND
+#undef PRECISE_MODE_OFF
+#undef PRECISE_MODE_TARGET
+#undef PRECISE_MODE_MARK
+#undef PRECISE_MODE_COPY
+#undef OFFSET_ABSOLUTE
+#undef OFFSET_RELATIVE
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index b8403e4c19..c44e2a0aa8 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1534,138 +1534,8 @@
return
SSadmin_verbs.dynamic_invoke_verb(usr.client, /datum/admin_verb/show_traitor_panel, M)
- else if(href_list["create_object"])
- if(!check_rights(R_SPAWN)) return
- return create_object(usr)
-
- else if(href_list["quick_create_object"])
- if(!check_rights(R_SPAWN)) return
- return quick_create_object(usr)
-
- else if(href_list["create_turf"])
- if(!check_rights(R_SPAWN)) return
- return create_turf(usr)
-
- else if(href_list["create_mob"])
- if(!check_rights(R_SPAWN)) return
- return create_mob(usr)
-
- else if(href_list["object_list"]) //this is the laggiest thing ever
- if(!check_rights(R_SPAWN)) return
-
- if(!CONFIG_GET(flag/allow_admin_spawning))
- to_chat(usr, span_filter_adminlog("Spawning of items is not allowed."))
- return
-
- var/atom/loc = usr.loc
-
- var/dirty_paths
- if (istext(href_list["object_list"]))
- dirty_paths = list(href_list["object_list"])
- else if (istype(href_list["object_list"], /list))
- dirty_paths = href_list["object_list"]
-
- var/paths = list()
- var/removed_paths = list()
-
- for(var/dirty_path in dirty_paths)
- var/path = text2path(dirty_path)
- if(!path)
- removed_paths += dirty_path
- continue
- else if(!ispath(path, /obj) && !ispath(path, /turf) && !ispath(path, /mob))
- removed_paths += dirty_path
- continue
- else if(ispath(path, /obj/item/gun/energy/pulse_rifle))
- if(!check_rights(R_FUN,0))
- removed_paths += dirty_path
- continue
- else if(ispath(path, /obj/item/melee/energy/blade))//Not an item one should be able to spawn./N
- if(!check_rights(R_FUN,0))
- removed_paths += dirty_path
- continue
- else if(ispath(path, /obj/effect/bhole))
- if(!check_rights(R_FUN,0))
- removed_paths += dirty_path
- continue
- paths += path
-
- if(!paths)
- tgui_alert(usr, "The path list you sent is empty")
- return
- if(length(paths) > 5)
- tgui_alert_async(usr, "Select fewer object types, (max 5)")
- return
- else if(length(removed_paths))
- tgui_alert_async(usr, "Removed:\n" + jointext(removed_paths, "\n"))
-
- var/list/offset = splittext(href_list["offset"],",")
- var/number = dd_range(1, 100, text2num(href_list["object_count"]))
- var/X = offset.len > 0 ? text2num(offset[1]) : 0
- var/Y = offset.len > 1 ? text2num(offset[2]) : 0
- var/Z = offset.len > 2 ? text2num(offset[3]) : 0
- var/tmp_dir = href_list["object_dir"]
- var/obj_dir = tmp_dir ? text2num(tmp_dir) : 2
- if(!obj_dir || !(obj_dir in list(1,2,4,8,5,6,9,10)))
- obj_dir = 2
- var/obj_name = sanitize(href_list["object_name"])
- var/where = href_list["object_where"]
- if (!( where in list("onfloor","inhand","inmarked") ))
- where = "onfloor"
-
- if( where == "inhand" )
- to_chat(usr, span_filter_adminlog("Support for inhand not available yet. Will spawn on floor."))
- where = "onfloor"
-
- if ( where == "inhand" ) //Can only give when human or monkey
- if ( !( ishuman(usr) || issmall(usr) ) )
- to_chat(usr, span_filter_adminlog("Can only spawn in hand when you're a human or a monkey."))
- where = "onfloor"
- else if ( usr.get_active_hand() )
- to_chat(usr, span_filter_adminlog("Your active hand is full. Spawning on floor."))
- where = "onfloor"
-
- if ( where == "inmarked" )
- if ( !marked_datum )
- to_chat(usr, span_filter_adminlog("You don't have any object marked. Abandoning spawn."))
- return
- else
- if ( !istype(marked_datum,/atom) )
- to_chat(usr, span_filter_adminlog("The object you have marked cannot be used as a target. Target must be of type /atom. Abandoning spawn."))
- return
-
- var/atom/target //Where the object will be spawned
- switch ( where )
- if ( "onfloor" )
- switch (href_list["offset_type"])
- if ("absolute")
- target = locate(0 + X,0 + Y,0 + Z)
- if ("relative")
- target = locate(loc.x + X,loc.y + Y,loc.z + Z)
- if ( "inmarked" )
- target = marked_datum
-
- if(target)
- for (var/path in paths)
- for (var/i = 0; i < number; i++)
- if(path in typesof(/turf))
- var/turf/O = target
- var/turf/N = O.ChangeTurf(path)
- if(N)
- if(obj_name)
- N.name = obj_name
- else
- var/atom/O = new path(target)
- if(O)
- O.set_dir(obj_dir)
- if(obj_name)
- O.name = obj_name
- if(istype(O,/mob))
- var/mob/M = O
- M.real_name = obj_name
-
- log_and_message_admins("created [number] [english_list(paths)]")
- return
+ else if(href_list["spawn_panel"])
+ SSadmin_verbs.dynamic_invoke_verb(usr, /datum/admin_verb/spawn_panel)
//else if(href_list["admin_secrets_panel"])
//var/datum/admin_secret_category/AC = locate(href_list["admin_secrets_panel"]) in admin_secrets.categories
diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm
new file mode 100644
index 0000000000..2698d451a7
--- /dev/null
+++ b/code/modules/admin/verbs/admingame.dm
@@ -0,0 +1,7 @@
+ADMIN_VERB(spawn_panel, R_SPAWN, "Spawn Panel", "Spawn Panel (TGUI).", ADMIN_CATEGORY_GAME)
+ var/datum/spawnpanel/panel = user.holder.spawn_panel
+ if(!panel)
+ panel = new()
+ user.holder.spawn_panel = panel
+ panel.tgui_interact(user.mob)
+ feedback_add_details("admin_verb","SP") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
diff --git a/code/modules/client/client defines.dm b/code/modules/client/client defines.dm
index dd1b4759cd..4524533948 100644
--- a/code/modules/client/client defines.dm
+++ b/code/modules/client/client defines.dm
@@ -40,6 +40,8 @@
show_verb_panel = FALSE
///Contains admin info. Null if client is not an admin.
var/datum/admins/holder = null
+ ///Needs to implement InterceptClickOn(user,params,atom) proc
+ var/datum/click_intercept = null
var/buildmode = 0
///Contains the last message sent by this client - used to protect against copy-paste spamming.
diff --git a/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm b/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm
index 93d67cbe1c..876d964510 100644
--- a/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm
+++ b/code/modules/mob/living/silicon/robot/dogborg/dog_sleeper.dm
@@ -31,10 +31,10 @@
var/max_item_count = 1
var/upgraded_capacity = FALSE
var/gulpsound = 'sound/vore/gulp.ogg'
- var/datum/matter_synth/metal = null
- var/datum/matter_synth/glass = null
- var/datum/matter_synth/wood = null
- var/datum/matter_synth/plastic = null
+ var/datum/matter_synth/metal/metal = null
+ var/datum/matter_synth/glass/glass = null
+ var/datum/matter_synth/wood/wood = null
+ var/datum/matter_synth/plastic/plastic = null
var/datum/matter_synth/water = null
var/digest_brute = 2
var/digest_burn = 3
diff --git a/code/modules/mob/living/silicon/robot/drone/drone_items.dm b/code/modules/mob/living/silicon/robot/drone/drone_items.dm
index 9c0dacbfe6..8cd25c7d84 100644
--- a/code/modules/mob/living/silicon/robot/drone/drone_items.dm
+++ b/code/modules/mob/living/silicon/robot/drone/drone_items.dm
@@ -7,10 +7,10 @@
icon_state = "decompiler"
//Metal, glass, wood, plastic.
- var/datum/matter_synth/metal = null
- var/datum/matter_synth/glass = null
- var/datum/matter_synth/wood = null
- var/datum/matter_synth/plastic = null
+ var/datum/matter_synth/metal/metal = null
+ var/datum/matter_synth/glass/glass = null
+ var/datum/matter_synth/wood/wood = null
+ var/datum/matter_synth/plastic/plastic = null
/obj/item/matter_decompiler/attack(mob/living/carbon/M as mob, mob/living/carbon/user as mob)
return
diff --git a/code/modules/mob/living/silicon/robot/robot_modules/station.dm b/code/modules/mob/living/silicon/robot/robot_modules/station.dm
index b571eaf3eb..72b7d9b591 100644
--- a/code/modules/mob/living/silicon/robot/robot_modules/station.dm
+++ b/code/modules/mob/living/silicon/robot/robot_modules/station.dm
@@ -184,6 +184,85 @@
CHANNEL_EXPLORATION = 1
)
+/obj/item/robot_module/proc/add_item_with_reagents(obj/item/stack/item_with_synth)
+ var/list/item_synths = list()
+ for(var/datum/matter_synth/matter_synth as anything in item_with_synth.synths)
+ var/found = FALSE
+ for(var/datum/matter_synth/synth as anything in synths)
+ if(matter_synth.type == synth.type)
+ item_synths += synth
+ break
+ if(!found)
+ var/datum/matter_synth/new_synth = new matter_synth.type(10000)
+ item_synths += new_synth
+ synths += new_synth
+ item_with_synth.synths = item_synths
+
+/obj/item/robot_module/proc/add_item(atom/movable/new_item, mob/living/silicon/robot/robot)
+ if(istype(new_item, /obj/item/card/id))
+ if(robot.idcard)
+ modules -= robot.idcard
+ QDEL_NULL(robot.idcard)
+ robot.idcard = new_item
+ modules += new_item
+ new_item.forceMove(src)
+ robot.hud_used?.update_robot_modules_display()
+
+ if(istype(new_item, /obj/item/robotic_multibelt/materials))
+ var/obj/item/robotic_multibelt/materials/mat_belt = new_item
+ for(var/obj/item/stack as anything in mat_belt.cyborg_integrated_tools)
+ add_item_with_reagents(stack)
+
+ if(istype(new_item, /obj/item/stack))
+ add_item_with_reagents(new_item)
+
+ if(istype(new_item, /obj/item/matter_decompiler) || istype(new_item, /obj/item/dogborg/sleeper/compactor/decompiler))
+ var/obj/item/matter_decompiler/item_with_matter = new_item
+ if(item_with_matter.metal)
+ var/found = FALSE
+ for(var/datum/matter_synth/synth as anything in synths)
+ if(item_with_matter.metal.type == synth.type)
+ item_with_matter.metal = synth
+ found = TRUE
+ break
+ if(!found)
+ var/datum/matter_synth/metal = new /datum/matter_synth/metal(40000)
+ item_with_matter.metal = metal
+ LAZYADD(synths, metal)
+ if(item_with_matter.glass)
+ var/found = FALSE
+ for(var/datum/matter_synth/synth as anything in synths)
+ if(item_with_matter.glass.type == synth.type)
+ item_with_matter.glass = synth
+ found = TRUE
+ break
+ if(!found)
+ var/datum/matter_synth/glass = new /datum/matter_synth/glass(40000)
+ item_with_matter.glass = glass
+ LAZYADD(synths, glass)
+ if(item_with_matter.wood)
+ var/found = FALSE
+ for(var/datum/matter_synth/synth as anything in synths)
+ if(item_with_matter.wood.type == synth.type)
+ item_with_matter.wood = synth
+ found = TRUE
+ break
+ if(!found)
+ var/datum/matter_synth/wood = new /datum/matter_synth/wood(40000)
+ item_with_matter.wood = wood
+ LAZYADD(synths, wood)
+ if(item_with_matter.plastic)
+ var/found = FALSE
+ for(var/datum/matter_synth/synth as anything in synths)
+ if(item_with_matter.plastic.type == synth.type)
+ item_with_matter.plastic = synth
+ found = TRUE
+ break
+ if(!found)
+ var/datum/matter_synth/plastic = new /datum/matter_synth/plastic(40000)
+ item_with_matter.plastic = plastic
+ LAZYADD(synths, plastic)
+
// Cyborgs (non-drones), default loadout. This will be given to every module.
/obj/item/robot_module/robot/create_equipment(var/mob/living/silicon/robot/robot)
..()
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index e26ce0966c..8413980fd5 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -269,6 +269,9 @@
///For storing what do_after's someone has, key = string, value = amount of interactions of that type happening.
var/list/do_afters
+ ///Allows a datum to intercept all click calls this mob is the source of
+ var/datum/click_intercept
+
var/datum/focus //What receives our keyboard inputs. src by default
/// dict of custom stat tabs with data
var/list/list/misc_tabs = list()
diff --git a/html/create_object.html b/html/create_object.html
deleted file mode 100644
index f202ea9983..0000000000
--- a/html/create_object.html
+++ /dev/null
@@ -1,150 +0,0 @@
-
-
- Create Object
-
-
-
-
-
-
-
-
-
diff --git a/tgui/packages/tgui-panel/settings/settingsImExport.ts b/tgui/packages/tgui-panel/settings/settingsImExport.ts
index 9c0596f51e..ca797ea82e 100644
--- a/tgui/packages/tgui-panel/settings/settingsImExport.ts
+++ b/tgui/packages/tgui-panel/settings/settingsImExport.ts
@@ -11,33 +11,17 @@ export function exportChatSettings(): void {
const chatPages = store.get(chatPagesRecordAtom);
const settings = store.get(storedSettingsAtom);
- const opts: SaveFilePickerOptions = {
- id: `ss13-chatprefs-${Date.now()}`,
- suggestedName: `ss13-chatsettings-${new Date().toJSON().slice(0, 10)}.json`,
- types: [
- {
- description: 'SS13 file',
- accept: { 'application/json': ['.json'] },
- },
- ],
- };
-
const exportObject = { ...settings, chatPages };
- window
- .showSaveFilePicker(opts)
- .then((fileHandle) => {
- fileHandle.createWritable().then((writableHandle) => {
- writableHandle.write(JSON.stringify(exportObject));
- writableHandle.close();
- });
- })
- .catch((e) => {
- // Log the error if the error has nothing to do with the user aborting the download
- if (e.name !== 'AbortError') {
- console.error(e);
- }
- });
+ const blob = new Blob([JSON.stringify(exportObject)], {
+ type: 'application/json',
+ });
+
+ Byond.saveBlob(
+ blob,
+ `ss13-chatsettings-${new Date().toJSON().slice(0, 10)}.json`,
+ '.json',
+ );
}
export function importChatSettings(settings: string | string[]): void {
diff --git a/tgui/packages/tgui/interfaces/SpawnPanel/CreateObject.tsx b/tgui/packages/tgui/interfaces/SpawnPanel/CreateObject.tsx
new file mode 100644
index 0000000000..ef93240850
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SpawnPanel/CreateObject.tsx
@@ -0,0 +1,475 @@
+import { storage } from 'common/storage';
+import { useCallback, useEffect, useState } from 'react';
+import {
+ Button,
+ DmIcon,
+ Input,
+ NoticeBox,
+ Section,
+ Stack,
+ VirtualList,
+} from 'tgui-core/components';
+import { useFuzzySearch } from 'tgui-core/fuzzysearch';
+
+import { useBackend } from '../../backend';
+import { CreateObjectSettings } from './CreateObjectSettings';
+import { listNames, listTypes } from './constants';
+import type {
+ AtomData,
+ CreateObjectProps,
+ SpawnPanelPreferences,
+} from './types';
+
+type SpawnPanelData = {
+ icon: string;
+ iconState: string;
+ selected_object?: string;
+ copied_type?: string;
+ preferences?: SpawnPanelPreferences;
+};
+
+interface SpawnPreferences {
+ hide_icons: boolean;
+ hide_mappings: boolean;
+ sort_by: string;
+ search_text: string;
+ search_by: string;
+ selected_atom?: string;
+}
+
+export function CreateObject(props: CreateObjectProps) {
+ const { act, data } = useBackend();
+ const { setAdvancedSettings, iconSettings, objList = { atoms: {} } } = props;
+
+ const [tooltipIcon, setTooltipIcon] = useState(false);
+ const [selectedObj, setSelectedObj] = useState(null);
+ const [searchBy, setSearchBy] = useState(false);
+ const [sortBy, setSortBy] = useState(listTypes.Objects);
+ const [hideMapping, setHideMapping] = useState(false);
+ const [showIcons, setshowIcons] = useState(false);
+ const [showPreview, setshowPreview] = useState(false);
+
+ // flattening the object lists
+ const allObjects = objList.atoms;
+
+ const currentList = objList;
+ const currentType = allObjects[data.copied_type ?? '']?.type || 'Objects';
+
+ const getSearchString = useCallback(
+ (key: string) => {
+ const item = allObjects[key];
+ if (!item) return key;
+ return searchBy ? key : `${key} ${item.name || ''}`;
+ },
+ [searchBy, allObjects],
+ );
+
+ const { query, setQuery, results } = useFuzzySearch({
+ searchArray: Object.keys(allObjects),
+ matchStrategy: 'smart',
+ getSearchString,
+ });
+
+ const filteredResults = results.filter((obj) => {
+ const item = allObjects[obj];
+ if (!item) return false;
+ if (sortBy !== listTypes[item.type]) return false;
+ if (hideMapping && item.mapping) return false;
+ return true;
+ });
+
+ useEffect(() => {
+ if (data.selected_object) {
+ setSelectedObj(data.selected_object);
+ if (currentList[data.selected_object]) {
+ props.onIconSettingsChange?.({
+ icon: currentList[data.selected_object].icon,
+ iconState: currentList[data.selected_object].icon_state,
+ });
+ }
+ }
+ }, [data.selected_object]);
+
+ useEffect(() => {
+ if (data.copied_type) {
+ setSelectedObj(data.copied_type);
+ setQuery(data.copied_type);
+
+ const copiedAtom: AtomData = objList.atoms[data.copied_type];
+ setSortBy(listTypes[copiedAtom.type]);
+ setSearchBy(true);
+
+ const list = objList.atoms;
+ if (list[data.copied_type]) {
+ props.onIconSettingsChange?.({
+ icon: list[data.copied_type].icon,
+ iconState: list[data.copied_type].icon_state,
+ });
+ }
+ }
+ }, [data.copied_type]);
+
+ useEffect(() => {
+ const loadStoredValues = async () => {
+ const storedSearchText = await storage.get('spawnpanel-searchText');
+ const storedSearchBy = await storage.get('spawnpanel-searchBy');
+ const storedSortBy = await storage.get('spawnpanel-sortBy');
+ const storedHideMapping = await storage.get('spawnpanel-hideMapping');
+ const storedShowIcons = await storage.get('spawnpanel-showIcons');
+ const storedShowPreview = await storage.get('spawnpanel-showPreview');
+ const storedSelectedObj = await storage.get('spawnpanel-selectedObj');
+
+ if (storedSearchText) setQuery(storedSearchText);
+ if (storedSearchBy !== undefined) setSearchBy(storedSearchBy);
+ if (storedSortBy) setSortBy(storedSortBy);
+ if (storedHideMapping !== undefined) setHideMapping(storedHideMapping);
+ if (storedShowIcons !== undefined) setshowIcons(storedShowIcons);
+ if (storedShowPreview !== undefined) setshowPreview(storedShowPreview);
+ if (storedSelectedObj && allObjects[storedSelectedObj]) {
+ setSelectedObj(storedSelectedObj);
+ props.onIconSettingsChange?.({
+ icon: allObjects[storedSelectedObj].icon,
+ iconState: allObjects[storedSelectedObj].icon_state,
+ });
+ }
+ };
+
+ loadStoredValues();
+ }, []);
+
+ useEffect(() => {
+ setSelectedObj(null);
+ }, [currentType]);
+
+ useEffect(() => {
+ if (selectedObj !== null) {
+ storage.set('spawnpanel-selectedObj', selectedObj);
+ }
+ }, [selectedObj]);
+
+ useEffect(() => {
+ if (data.selected_object) {
+ setSelectedObj(data.selected_object);
+ } else if (data.copied_type) {
+ setSelectedObj(data.copied_type);
+ }
+ }, [data.selected_object, data.copied_type]);
+
+ useEffect(() => {
+ if (data.iconState !== undefined) {
+ props.onIconSettingsChange({
+ iconState: data.iconState,
+ });
+ }
+ }, [data.iconState]);
+
+ const sendUpdatedSettings = (
+ changedSettings: Partial> = {},
+ ) => {
+ act('update-settings', changedSettings);
+ };
+
+ const updateSearchText = (value: string) => {
+ setQuery(value);
+ storage.set('spawnpanel-searchText', value);
+ };
+
+ const updateSearchBy = (value: boolean) => {
+ setSearchBy(value);
+ storage.set('spawnpanel-searchBy', value);
+ };
+
+ const updateSortBy = (value: string) => {
+ setSortBy(value);
+ storage.set('spawnpanel-sortBy', value);
+ };
+
+ const updateHideMapping = (value: boolean) => {
+ setHideMapping(value);
+ storage.set('spawnpanel-hideMapping', value);
+ };
+
+ const updateShowIcons = (value: boolean) => {
+ setshowIcons(value);
+ storage.set('spawnpanel-showIcons', value);
+ };
+
+ const updateShowPreview = (value: boolean) => {
+ setshowPreview(value);
+ storage.set('spawnpanel-showPreview', value);
+ };
+
+ const sendPreferences = (settings: Partial) => {
+ const parseOffset = (offsetStr: string): number[] => {
+ if (!offsetStr.trim()) return [0, 0, 0];
+
+ const parts = offsetStr.split(',').map((part) => {
+ return parseInt(part.trim(), 10);
+ });
+
+ while (parts.length < 3) {
+ parts.push(0);
+ }
+
+ return parts.slice(0, 3);
+ };
+
+ const prefsToSend = {
+ hide_icons: showIcons,
+ hide_mappings: hideMapping,
+ sort_by:
+ Object.keys(listTypes).find((key) => listTypes[key] === sortBy) ||
+ 'Objects',
+ search_text: query,
+ search_by: searchBy ? 'type' : 'name',
+ ...settings,
+ };
+
+ act('create-atom-action', prefsToSend);
+ };
+
+ const handleObjectSelect = (obj: string) => {
+ setSelectedObj(obj);
+ act('selected-atom-changed', {
+ newObj: obj,
+ });
+ if (allObjects[obj]) {
+ props.onIconSettingsChange?.({
+ icon: allObjects[obj].icon,
+ iconState: allObjects[obj].icon_state,
+ });
+ }
+ };
+
+ const handleResetIconState = () => {
+ props.onIconSettingsChange({
+ iconState: null,
+ icon: selectedObj ? allObjects[selectedObj]?.icon : null,
+ });
+ act('reset-icon-state');
+ };
+
+ return (
+
+
+
+
+
+ {showPreview && selectedObj && allObjects[selectedObj] && (
+
+
+
+
+
+
+
+
+
+
+ {allObjects[selectedObj].name}
+
+ {allObjects[selectedObj].description || 'no description'}
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ {query === '' ? (
+
+ Begin typing to search...
+
+ ) : !filteredResults.length ? (
+
+ Nothing found
+
+ ) : (
+
+ {filteredResults.map((obj, index) => (
+
+ )
+ }
+ tooltipPosition="top-start"
+ fluid
+ selected={selectedObj === obj}
+ style={{
+ backgroundColor:
+ selectedObj === obj
+ ? 'rgba(160, 200, 255, 0.1)'
+ : undefined,
+ color: selectedObj === obj ? '#fff' : undefined,
+ }}
+ onDoubleClick={() => {
+ if (selectedObj) {
+ sendPreferences({ selected_atom: selectedObj });
+ }
+ }}
+ onClick={() => handleObjectSelect(obj)}
+ >
+ {searchBy ? (
+ obj
+ ) : (
+ <>
+ {allObjects[obj]?.name}
+
+ {obj}
+
+ >
+ )}
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/tgui/packages/tgui/interfaces/SpawnPanel/CreateObjectAdvancedSettings.tsx b/tgui/packages/tgui/interfaces/SpawnPanel/CreateObjectAdvancedSettings.tsx
new file mode 100644
index 0000000000..d15fce9d7b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SpawnPanel/CreateObjectAdvancedSettings.tsx
@@ -0,0 +1,146 @@
+import { useEffect } from 'react';
+import { Button, Dropdown, Slider, Table } from 'tgui-core/components';
+
+import { useBackend } from '../../backend';
+import type { IconSettings } from './index';
+
+type SpawnPanelData = {
+ icon: string;
+ iconState: string;
+ iconStates: string[];
+ selected_object?: string;
+ apply_icon_override?: boolean;
+};
+
+interface CreateObjectAdvancedSettingsProps {
+ iconSettings: IconSettings;
+ onIconSettingsChange: (settings: Partial) => void;
+}
+
+export function CreateObjectAdvancedSettings(
+ props: CreateObjectAdvancedSettingsProps,
+) {
+ const { act, data } = useBackend();
+ const { iconSettings, onIconSettingsChange } = props;
+
+ const sendUpdatedSettings = (
+ changedSettings: Partial> = {},
+ ) => {
+ const currentSettings = {
+ selected_atom_icon: data.icon || iconSettings.icon,
+ selected_atom_icon_state: data.iconState || iconSettings.iconState,
+ atom_icon_size: iconSettings.iconSize,
+ ...changedSettings,
+ };
+ act('update-settings', currentSettings);
+ };
+
+ useEffect(() => {
+ act('get-icon-states');
+ }, []);
+
+ const iconStateOptions = (
+ Array.isArray(data.iconStates) ? data.iconStates : []
+ ).map((state) => ({
+ value: state,
+ displayText: state,
+ }));
+
+ return (
+
+
+
+ Icon:
+
+
+ act('select-new-DMI')}>
+ {data.icon || iconSettings.icon || 'Default'}
+
+
+
+ {
+ act('reset-DMI-icon');
+ sendUpdatedSettings();
+ }}
+ />
+
+
+
+ Icon state:
+
+ {
+ act('select-new-icon-state', {
+ new_state: value,
+ current_icon: data.icon || iconSettings.icon,
+ });
+ onIconSettingsChange({ iconState: value });
+ sendUpdatedSettings({ selected_atom_icon_state: value });
+ }}
+ width="100%"
+ />
+
+
+ {
+ onIconSettingsChange({ iconState: null });
+ act('reset-icon-state');
+ }}
+ />
+
+
+
+ Explicitly set icon:
+
+ {
+ const next = !iconSettings.applyIcon;
+ onIconSettingsChange({ applyIcon: next });
+ act('set-apply-icon-override', { value: next });
+ }}
+ >
+ Enabled
+
+
+
+
+
+ Icon scale:
+
+ {
+ onIconSettingsChange({ iconSize: value });
+ sendUpdatedSettings({ atom_icon_size: value });
+ }}
+ />
+
+
+ {
+ onIconSettingsChange({ iconSize: 100 });
+ act('reset-icon-size');
+ sendUpdatedSettings({ atom_icon_size: 100 });
+ }}
+ />
+
+
+
+ );
+}
diff --git a/tgui/packages/tgui/interfaces/SpawnPanel/CreateObjectSettings.tsx b/tgui/packages/tgui/interfaces/SpawnPanel/CreateObjectSettings.tsx
new file mode 100644
index 0000000000..9ac66f499c
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SpawnPanel/CreateObjectSettings.tsx
@@ -0,0 +1,492 @@
+import { storage } from 'common/storage';
+import { useEffect, useState } from 'react';
+import {
+ Button,
+ Dropdown,
+ Icon,
+ Input,
+ NumberInput,
+ Slider,
+ Stack,
+ Table,
+} from 'tgui-core/components';
+
+import { useBackend } from '../../backend';
+import {
+ directionNames,
+ directionRotation,
+ spawnLocationIcons,
+ spawnLocationOptions,
+} from './constants';
+import type { IconSettings } from './index';
+
+export type SpawnPanelData = {
+ icon: string;
+ iconState: string;
+ iconStates: string[];
+ selected_object?: string;
+ precise_mode: string;
+};
+
+interface CreateObjectSettingsProps {
+ onCreateObject?: (obj: Record) => void;
+ setAdvancedSettings: (value: boolean) => void;
+ iconSettings: IconSettings;
+}
+
+export function CreateObjectSettings(props: CreateObjectSettingsProps) {
+ const { onCreateObject, setAdvancedSettings, iconSettings } = props;
+ const { act, data } = useBackend();
+
+ const [amount, setAmount] = useState(1);
+ const [cordsType, setCordsType] = useState(0);
+ const [spawnLocation, setSpawnLocation] = useState('Current location');
+ const [direction, setDirection] = useState(0);
+ const [objectName, setObjectName] = useState('');
+ const [offset, setOffset] = useState('');
+
+ const dirValues = [1, 5, 4, 6, 2, 10, 8, 9];
+ const updateAmount = (value: number) => {
+ setAmount(value);
+ sendUpdatedSettings({ atom_amount: value });
+ };
+
+ const updateCordsType = (value: number) => {
+ setCordsType(value);
+ storage.set('spawnpanel-offset_type', value);
+ sendUpdatedSettings({
+ offset_type: value ? 'Absolute offset' : 'Relative offset',
+ });
+ };
+
+ const updateSpawnLocation = (value: string) => {
+ setSpawnLocation(value);
+ storage.set('spawnpanel-where_target_type', value);
+ sendUpdatedSettings({ where_target_type: value });
+ };
+
+ const updateDirection = (value: number) => {
+ setDirection(value);
+ storage.set('spawnpanel-direction', value);
+ sendUpdatedSettings({ atom_dir: dirValues[value] });
+ };
+
+ const updateObjectName = (value: string) => {
+ setObjectName(value);
+ storage.set('spawnpanel-atom_name', value);
+ sendUpdatedSettings({ atom_name: value });
+
+ if (isPreciseModeActive) {
+ sendUpdatedSettings();
+ }
+ };
+
+ const updateOffset = (value: string) => {
+ setOffset(value);
+ storage.set('spawnpanel-offset', value);
+
+ const parseOffset = (offsetStr: string): number[] => {
+ if (!offsetStr.trim()) return [0, 0, 0];
+
+ const parts = offsetStr.split(',').map((part) => {
+ return parseInt(part.trim(), 10);
+ });
+
+ while (parts.length < 3) {
+ parts.push(0);
+ }
+
+ return parts.slice(0, 3);
+ };
+
+ sendUpdatedSettings({ offset: parseOffset(value) });
+ };
+
+ const sendUpdatedSettings = (
+ changedSettings: Partial> = {},
+ ) => {
+ const parseOffset = (offsetStr: string): number[] => {
+ if (!offsetStr.trim()) return [0, 0, 0];
+
+ const parts = offsetStr.split(',').map((part) => {
+ return parseInt(part.trim(), 10);
+ });
+
+ while (parts.length < 3) {
+ parts.push(0);
+ }
+
+ return parts.slice(0, 3);
+ };
+
+ const currentSettings = {
+ atom_amount: amount,
+ offset_type: cordsType ? 'Absolute offset' : 'Relative offset',
+ where_target_type: spawnLocation,
+ atom_dir: dirValues[direction],
+ offset: parseOffset(offset),
+ atom_name: objectName,
+ atom_icon_size: iconSettings.iconSize,
+ apply_icon_override: iconSettings.applyIcon ?? false,
+ ...changedSettings,
+ };
+ act('update-settings', currentSettings);
+ };
+
+ const resetAdvancedSettings = () => {
+ const defaultAmount = 1;
+ const defaultCordsType = 0;
+ const defaultSpawnLocation = 'Current location';
+ const defaultDirection = 0;
+ const defaultObjectName = '';
+ const defaultOffset = '';
+
+ setAmount(defaultAmount);
+ setCordsType(defaultCordsType);
+ setSpawnLocation(defaultSpawnLocation);
+ setDirection(defaultDirection);
+ setObjectName(defaultObjectName);
+ setOffset(defaultOffset);
+
+ storage.set('spawnpanel-atom_amount', defaultAmount);
+ storage.set('spawnpanel-offset_type', defaultCordsType);
+ storage.set('spawnpanel-where_target_type', defaultSpawnLocation);
+ storage.set('spawnpanel-direction', defaultDirection);
+ storage.set('spawnpanel-atom_name', defaultObjectName);
+ storage.set('spawnpanel-offset', defaultOffset);
+
+ sendUpdatedSettings({
+ atom_amount: defaultAmount,
+ offset_type: defaultCordsType ? 'Absolute offset' : 'Relative offset',
+ where_target_type: defaultSpawnLocation,
+ atom_dir: dirValues[defaultDirection],
+ offset: defaultOffset,
+ atom_name: defaultObjectName,
+ });
+ };
+
+ useEffect(() => {
+ const loadStoredValues = async () => {
+ const storedCordsType = await storage.get('spawnpanel-offset_type');
+ const storedSpawnLocation = await storage.get(
+ 'spawnpanel-where_target_type',
+ );
+ const storedDirection = await storage.get('spawnpanel-direction');
+ const storedObjectName = await storage.get('spawnpanel-atom_name');
+ const storedOffset = await storage.get('spawnpanel-offset');
+
+ if (storedCordsType !== undefined) setCordsType(storedCordsType);
+ if (storedSpawnLocation) setSpawnLocation(storedSpawnLocation);
+ if (storedDirection !== undefined) setDirection(storedDirection);
+ if (storedObjectName !== undefined) setObjectName(storedObjectName);
+ if (storedOffset !== undefined) setOffset(storedOffset);
+ };
+
+ loadStoredValues();
+ }, []);
+
+ const isTargetMode =
+ spawnLocation === 'Targeted location' ||
+ spawnLocation === 'Targeted location (droppod)' ||
+ spawnLocation === "In targeted mob's hand";
+
+ const isPreciseModeActive = data?.precise_mode === 'Target';
+ const isMarkModeActive = data?.precise_mode === 'Mark';
+ const isCopyModeActive = data?.precise_mode === 'Copy';
+
+ const isAnyPreciseModeActive = !(data?.precise_mode === 'Off');
+
+ const disablePreciseMode = () => {
+ if (isPreciseModeActive) {
+ act('toggle-precise-mode', {
+ newPreciseType: 'Off',
+ });
+ }
+ };
+
+ const handleSpawn = () => {
+ const parseOffset = (offsetStr: string): number[] => {
+ if (!offsetStr.trim()) return [0, 0, 0];
+
+ const parts = offsetStr.split(',').map((part) => {
+ return parseInt(part.trim(), 10);
+ });
+
+ while (parts.length < 3) {
+ parts.push(0);
+ }
+
+ return parts.slice(0, 3);
+ };
+
+ const currentSettings = {
+ atom_amount: amount,
+ offset_type: cordsType ? 'Absolute offset' : 'Relative offset',
+ where_target_type: spawnLocation,
+ atom_dir: dirValues[direction],
+ offset: parseOffset(offset),
+ atom_name: objectName,
+ atom_icon_size: iconSettings.iconSize,
+ selected_atom: data.selected_object,
+ apply_icon_override: iconSettings.applyIcon ?? false,
+ };
+
+ act('update-settings', currentSettings);
+
+ if (!isTargetMode) {
+ if (onCreateObject) {
+ onCreateObject(currentSettings);
+ } else {
+ act('create-atom-action', currentSettings);
+ }
+ } else {
+ if (isPreciseModeActive) {
+ act('toggle-precise-mode', { newPreciseType: 'Off' });
+ } else {
+ act('toggle-precise-mode', {
+ newPreciseType: 'Target',
+ where_target_type: spawnLocation,
+ });
+ }
+ }
+ };
+
+ useEffect(() => {
+ if (!isTargetMode && isPreciseModeActive) {
+ disablePreciseMode();
+ }
+ }, [spawnLocation]);
+
+ useEffect(() => {
+ if (isPreciseModeActive) {
+ sendUpdatedSettings();
+ }
+ }, [iconSettings.icon, iconSettings.iconState, iconSettings.iconSize]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ setAdvancedSettings(true)}
+ style={{
+ height: '22px',
+ width: '22px',
+ lineHeight: '22px',
+ }}
+ tooltip="Advanced settings"
+ tooltipPosition="top"
+ disabled={isAnyPreciseModeActive}
+ />
+
+
+ resetAdvancedSettings()}
+ tooltip="Reset advanced settings"
+ tooltipPosition="top"
+ disabled={isAnyPreciseModeActive}
+ />
+
+
+ {
+ act('toggle-precise-mode', {
+ newPreciseType:
+ isMarkModeActive || isCopyModeActive
+ ? 'Off'
+ : spawnLocation === 'At a marked object' ||
+ spawnLocation === 'In the marked object'
+ ? 'Mark'
+ : 'Copy',
+ });
+ }}
+ selected={isMarkModeActive || isCopyModeActive}
+ tooltip={
+ spawnLocation === 'At a marked object' ||
+ spawnLocation === 'In the marked object'
+ ? 'Mark atom'
+ : 'Copy atom path'
+ }
+ tooltipPosition="top"
+ disabled={isAnyPreciseModeActive && !isMarkModeActive}
+ />
+
+
+
+
+
+
+
+ SPAWN
+
+
+
+ {
+ if (data?.precise_mode && data.precise_mode !== 'Off') {
+ act('toggle-precise-mode', {
+ newPreciseType: 'Off',
+ });
+ }
+ updateSpawnLocation(value);
+ }}
+ selected={spawnLocation}
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/tgui/packages/tgui/interfaces/SpawnPanel/constants.tsx b/tgui/packages/tgui/interfaces/SpawnPanel/constants.tsx
new file mode 100644
index 0000000000..f0707f757a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SpawnPanel/constants.tsx
@@ -0,0 +1,55 @@
+export const spawnLocationOptions = [
+ 'Current location',
+ 'Current location (droppod)',
+ "In own mob's hand",
+ 'At a marked object',
+ 'In the marked object',
+ 'Targeted location',
+ 'Targeted location (droppod)',
+ "In targeted mob's hand",
+];
+
+export const listTypes = {
+ Objects: 'cube',
+ Turfs: 'map',
+ Mobs: 'cat',
+};
+
+export const listNames = {
+ Objects: 'Search objects',
+ Turfs: 'Search turfs',
+ Mobs: 'Search mobs',
+};
+
+export const spawnLocationIcons = {
+ 'Current location': 'map-marker',
+ 'Current location (droppod)': 'parachute-box',
+ "In own mob's hand": 'hand-holding',
+ 'At a marked object': 'floppy-disk',
+ 'In the marked object': 'floppy-disk',
+ 'Targeted location': 'crosshairs',
+ 'Targeted location (droppod)': 'crosshairs',
+ "In targeted mob's hand": 'crosshairs',
+};
+
+export const directionRotation = {
+ 1: 0,
+ 5: 45,
+ 4: 90,
+ 6: 135,
+ 2: 180,
+ 10: 225,
+ 8: 270,
+ 9: 315,
+};
+
+export const directionNames = {
+ 1: 'NORTH',
+ 2: 'SOUTH',
+ 4: 'EAST',
+ 8: 'WEST',
+ 5: 'NORTHEAST',
+ 6: 'SOUTHEAST',
+ 9: 'NORTHWEST',
+ 10: 'SOUTHWEST',
+};
diff --git a/tgui/packages/tgui/interfaces/SpawnPanel/index.tsx b/tgui/packages/tgui/interfaces/SpawnPanel/index.tsx
new file mode 100644
index 0000000000..6cca2be9fb
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SpawnPanel/index.tsx
@@ -0,0 +1,91 @@
+import { useEffect, useState } from 'react';
+import { Button, Modal, Section, Stack } from 'tgui-core/components';
+import { fetchRetry } from 'tgui-core/http';
+
+import { resolveAsset } from '../../assets';
+import { Window } from '../../layouts';
+import { logger } from '../../logging';
+import { CreateObject } from './CreateObject';
+import { CreateObjectAdvancedSettings } from './CreateObjectAdvancedSettings';
+import type { CreateObjectData } from './types';
+
+export interface IconSettings {
+ icon: string | null;
+ iconState: string | null;
+ iconSize: number;
+ applyIcon?: boolean;
+}
+
+export function SpawnPanel() {
+ const [data, setData] = useState();
+ const [advancedSettings, setAdvancedSettings] = useState(false);
+ const [iconSettings, setIconSettings] = useState({
+ icon: null,
+ iconState: null,
+ iconSize: 100,
+ applyIcon: false,
+ });
+
+ useEffect(() => {
+ fetchRetry(resolveAsset('spawnpanel_atom_data.json'))
+ .then((response) => response.json())
+ .then(setData)
+ .catch((error) => {
+ logger.log(
+ 'Failed to fetch spawnpanel_atom_data.json',
+ JSON.stringify(error),
+ );
+ });
+ }, []);
+
+ const handleIconSettingsChange = (newSettings: Partial) => {
+ setIconSettings((current) => ({
+ ...current,
+ ...newSettings,
+ }));
+ };
+
+ return (
+
+
+ {advancedSettings && (
+
+ setAdvancedSettings(false)}
+ />
+ }
+ >
+
+
+
+ )}
+
+
+ {data && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/tgui/packages/tgui/interfaces/SpawnPanel/types.ts b/tgui/packages/tgui/interfaces/SpawnPanel/types.ts
new file mode 100644
index 0000000000..ae35c387e0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/SpawnPanel/types.ts
@@ -0,0 +1,53 @@
+import type { BooleanLike } from 'tgui-core/react';
+
+export interface SpawnPreferences {
+ hide_icons: boolean;
+ hide_mappings: boolean;
+ sort_by: string;
+ search_text: string;
+ search_by: string;
+ where_dropdown_value: string;
+ offset_type: string;
+ offset: string;
+ object_count: number;
+ dir: number;
+ object_name: string;
+}
+
+export interface SpawnPanelPreferences {
+ hide_icons: boolean;
+ hide_mappings: boolean;
+ sort_by: string;
+ search_text: string;
+ search_by: string;
+}
+
+export interface AtomData {
+ icon: string;
+ icon_state: string;
+ name: string;
+ description?: string;
+ mapping: BooleanLike;
+ type: 'Objects' | 'Turfs' | 'Mobs';
+}
+
+export interface CreateObjectData {
+ atoms: Record;
+}
+
+export interface CreateObjectProps {
+ objList: CreateObjectData;
+ setAdvancedSettings: (value: boolean) => void;
+ iconSettings: {
+ icon: string | null;
+ iconState: string | null;
+ iconSize: number;
+ };
+ onIconSettingsChange: (
+ settings: Partial<{
+ icon: string | null;
+ iconState: string | null;
+ iconSize: number;
+ }>,
+ ) => void;
+}
diff --git a/vorestation.dme b/vorestation.dme
index b925c90258..4b49945373 100644
--- a/vorestation.dme
+++ b/vorestation.dme
@@ -2181,9 +2181,6 @@
#include "code\modules\admin\admin_verbs.dm"
#include "code\modules\admin\banjob.dm"
#include "code\modules\admin\ckey_vr.dm"
-#include "code\modules\admin\create_mob.dm"
-#include "code\modules\admin\create_object.dm"
-#include "code\modules\admin\create_turf.dm"
#include "code\modules\admin\fix_player_notes_listing.dm"
#include "code\modules\admin\health_scan.dm"
#include "code\modules\admin\holder2.dm"
@@ -2204,8 +2201,13 @@
#include "code\modules\admin\callproc\callproc.dm"
#include "code\modules\admin\DB ban\ban_panle_ui.dm"
#include "code\modules\admin\DB ban\functions.dm"
+#include "code\modules\admin\spawn_panel\admin_hotkeys.dm"
+#include "code\modules\admin\spawn_panel\assets.dm"
+#include "code\modules\admin\spawn_panel\spawn_handling.dm"
+#include "code\modules\admin\spawn_panel\spawn_panel.dm"
#include "code\modules\admin\verb_datums\_admin_verb_datum.dm"
#include "code\modules\admin\verbs\adminfun.dm"
+#include "code\modules\admin\verbs\admingame.dm"
#include "code\modules\admin\verbs\adminjump.dm"
#include "code\modules\admin\verbs\adminpm.dm"
#include "code\modules\admin\verbs\adminsay.dm"