diff --git a/SQL/paradise_schema.sql b/SQL/paradise_schema.sql
index 75d81c9eb03..87fbb3cd1c5 100644
--- a/SQL/paradise_schema.sql
+++ b/SQL/paradise_schema.sql
@@ -606,3 +606,19 @@ CREATE TABLE `tickets` (
CONSTRAINT `all_responses` CHECK (json_valid(`all_responses`)),
CONSTRAINT `awho` CHECK (json_valid(`awho`))
) COLLATE='utf8mb4_general_ci' ENGINE=InnoDB;
+
+--
+-- Table structure for table `json_datum_saves`
+--
+DROP TABLE IF EXISTS `json_datum_saves`;
+CREATE TABLE `json_datum_saves` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `ckey` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `slotname` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `slotjson` LONGTEXT NOT NULL COLLATE 'utf8mb4_general_ci',
+ `created` DATETIME NOT NULL DEFAULT current_timestamp(),
+ `updated` DATETIME NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `ckey_unique` (`ckey`, `slotname`) USING BTREE,
+ INDEX `ckey` (`ckey`) USING BTREE
+) COLLATE = 'utf8mb4_general_ci' ENGINE = InnoDB;
diff --git a/SQL/updates/49-50.sql b/SQL/updates/49-50.sql
new file mode 100644
index 00000000000..228827bc4ee
--- /dev/null
+++ b/SQL/updates/49-50.sql
@@ -0,0 +1,13 @@
+# Updating SQL from 49 to 50 -AffectedArc07
+# Add new JSON datum saves table
+CREATE TABLE `json_datum_saves` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `ckey` VARCHAR(64) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `slotname` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_general_ci',
+ `slotjson` LONGTEXT NOT NULL COLLATE 'utf8mb4_general_ci',
+ `created` DATETIME NOT NULL DEFAULT current_timestamp(),
+ `updated` DATETIME NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `ckey_unique` (`ckey`, `slotname`) USING BTREE,
+ INDEX `ckey` (`ckey`) USING BTREE
+) COLLATE = 'utf8mb4_general_ci' ENGINE = InnoDB;
diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm
index d9328e682ff..387a43cadf1 100644
--- a/code/__DEFINES/misc_defines.dm
+++ b/code/__DEFINES/misc_defines.dm
@@ -375,7 +375,7 @@
#define INVESTIGATE_BOMB "bombs"
// The SQL version required by this version of the code
-#define SQL_VERSION 49
+#define SQL_VERSION 50
// Vending machine stuff
#define CAT_NORMAL 1
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index c1c181791c3..b21e5b725b2 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -117,7 +117,8 @@ GLOBAL_LIST_INIT(admin_verbs_spawn, list(
/datum/admins/proc/spawn_atom, /*allows us to spawn instances*/
/client/proc/respawn_character,
/client/proc/admin_deserialize,
- /client/proc/create_crate
+ /client/proc/create_crate,
+ /client/proc/json_spawn_menu
))
GLOBAL_LIST_INIT(admin_verbs_server, list(
/client/proc/reload_admins,
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 7ba2806125f..9598439ac76 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -3360,6 +3360,73 @@
var/mob/about_to_be_banned = locateUID(href_list["adminalert"])
usr.client.cmd_admin_alert_message(about_to_be_banned)
+ else if(href_list["spawnjsondatum"])
+ // Get the name and JSON to spawn
+ var/datum/db_query/dbq = SSdbcore.NewQuery("SELECT slotname, slotjson FROM json_datum_saves WHERE ckey=:ckey AND id=:id", list(
+ "ckey" = usr.ckey,
+ "id" = href_list["spawnjsondatum"]
+ ))
+ if(!dbq.warn_execute())
+ qdel(dbq)
+ return
+
+ var/slot_name = null
+ var/slot_json = null
+
+ // Read it
+ while(dbq.NextRow())
+ slot_name = dbq.item[1]
+ slot_json = dbq.item[2]
+
+ qdel(dbq)
+
+ // Double check
+ var/spawn_choice = alert(usr, "Are you sure you wish to spawn '[slot_name]' at your current location?", "Warning", "Yes", "No")
+ if(spawn_choice != "Yes")
+ return
+
+ // Log this for gods sake
+ message_admins("[key_name_admin(usr)] spawned an atom from a JSON DB save.")
+ log_admin("[key_name(usr)] spawned an atom from a JSON DB save, JSON Text: [slot_json]")
+ json_to_object(slot_json, get_turf(usr))
+
+ else if(href_list["deletejsondatum"])
+ // Get the name
+ var/datum/db_query/dbq = SSdbcore.NewQuery("SELECT slotname FROM json_datum_saves WHERE ckey=:ckey AND id=:id", list(
+ "ckey" = usr.ckey,
+ "id" = href_list["deletejsondatum"]
+ ))
+ if(!dbq.warn_execute())
+ qdel(dbq)
+ return
+
+ var/slot_name = null
+
+ // Read it
+ while(dbq.NextRow())
+ slot_name = dbq.item[1]
+
+ qdel(dbq)
+
+ // Double check
+ var/delete_choice = alert(usr, "Are you sure you wish to delete '[slot_name]'? This cannot be undone!", "Warning", "Yes", "No")
+ if(delete_choice != "Yes")
+ return
+
+ var/datum/db_query/dbq2 = SSdbcore.NewQuery("DELETE FROM json_datum_saves WHERE ckey=:ckey AND id=:id", list(
+ "ckey" = usr.ckey,
+ "id" = href_list["deletejsondatum"]
+ ))
+
+ if(!dbq2.warn_execute())
+ qdel(dbq2)
+ return
+
+ qdel(dbq2)
+ owner.json_spawn_menu() // Refresh their menu
+ to_chat(usr, "Slot [slot_name] deleted.")
+
+
/client/proc/create_eventmob_for(mob/living/carbon/human/H, killthem = 0)
if(!check_rights(R_EVENT))
return
diff --git a/code/modules/admin/verbs/serialization.dm b/code/modules/admin/verbs/serialization.dm
index 863d15287ce..c60a072b6f7 100644
--- a/code/modules/admin/verbs/serialization.dm
+++ b/code/modules/admin/verbs/serialization.dm
@@ -11,7 +11,75 @@
return
var/atom/movable/AM = holder.marked_datum
- to_chat(src, json_encode(AM.serialize()))
+
+ var/json_data = json_encode(AM.serialize())
+
+ var/choice = alert(usr, "Would you like to store this on your PC or server side?", "Storage Location", "PC", "Server", "Cancel")
+ if(!choice || choice == "Cancel")
+ return
+
+ if(choice == "PC")
+ to_chat(src, json_data)
+
+ if(choice == "Server")
+ // Right, get their slot names
+ var/list/slots = list("--NEW--")
+ var/datum/db_query/dbq = SSdbcore.NewQuery("SELECT slotname FROM json_datum_saves WHERE ckey=:ckey", list("ckey" = usr.ckey))
+ if(!dbq.warn_execute())
+ qdel(dbq)
+ return
+
+ while(dbq.NextRow())
+ slots += dbq.item[1]
+
+ qdel(dbq)
+
+ var/slot_choice = input(usr, "Select a slot to update, or create a new one.", "Slot Selection") as null|anything in slots
+
+ if(!slot_choice)
+ return
+
+ if(slot_choice == "--NEW--")
+ // New slot, save to DB
+ var/chosen_slot_name = input(usr, "Name your slot", "Slot name") as null|text
+ if(!chosen_slot_name || length(chosen_slot_name) == 0)
+ return
+
+ // Sanitize the name
+ var/clean_name = sanitize(copytext(chosen_slot_name, 1, 32)) // 32 chars is your max
+
+ // And save
+ var/datum/db_query/dbq2 = SSdbcore.NewQuery("INSERT INTO json_datum_saves (ckey, slotname, slotjson) VALUES(:ckey, :slotname, :slotjson)", list(
+ "ckey" = usr.ckey,
+ "slotname" = clean_name,
+ "slotjson" = json_data
+ ))
+ if(!dbq2.warn_execute())
+ qdel(dbq2)
+ return
+
+ qdel(dbq2)
+ to_chat(usr, "Successfully saved [clean_name]. You can spawn it from Debug > Spawn Saved JSON Datum.")
+
+ else
+ // Existing slot, warn first
+ var/confirmation = alert(usr, "Are you sure you want to update '[slot_choice]'? This cannot be undone!", "You sure?", "Yes", "No")
+ if(confirmation != "Yes")
+ return
+
+ // Now update
+ var/datum/db_query/dbq2 = SSdbcore.NewQuery("UPDATE json_datum_saves SET slotjson=:slotjson WHERE slotname=:slotname AND ckey=:ckey", list(
+ "slotjson" = json_data,
+ "ckey" = usr.ckey,
+ "slotname" = slot_choice
+ ))
+ if(!dbq2.warn_execute())
+ qdel(dbq2)
+ return
+
+ qdel(dbq2)
+ to_chat(usr, "Successfully updated [slot_choice]. You can spawn it from Debug > Spawn Saved JSON Datum.")
+
/client/proc/admin_deserialize()
set name = "Deserialize JSON datum"
@@ -26,3 +94,41 @@
json_to_object(json_text, get_turf(usr))
message_admins("[key_name_admin(usr)] spawned an atom from a custom JSON object.")
log_admin("[key_name(usr)] spawned an atom from a custom JSON object, JSON Text: [json_text]")
+
+
+/client/proc/json_spawn_menu()
+ set name = "Spawn Saved JSON Datum"
+ set desc = "Spawns a JSON datums saved server side"
+ set category = "Debug"
+
+ // This needs a holder to function
+ if(!check_rights(R_SPAWN) || !holder)
+ return
+
+ // Right, get their slot names
+ var/list/slots = list()
+ var/datum/db_query/dbq = SSdbcore.NewQuery("SELECT slotname, id FROM json_datum_saves WHERE ckey=:ckey", list("ckey" = usr.ckey))
+ if(!dbq.warn_execute())
+ qdel(dbq)
+ return
+
+ while(dbq.NextRow())
+ slots[dbq.item[1]] += dbq.item[2]
+ qdel(dbq)
+
+
+ var/datum/browser/popup = new(usr, "jsonspawnmenu", "JSON Spawn Menu", 400, 300)
+
+ // Cache this to reduce proc jumps
+ var/holder_uid = holder.UID()
+
+ var/list/rows = list()
+ rows += "
| Slot | Actions |
|---|---|
| [slotname] | Spawn Delete |