Moves pAI saves to the DB

This commit is contained in:
AffectedArc07
2021-10-04 19:15:53 +01:00
parent 2aa0abde72
commit c358a20498
12 changed files with 273 additions and 114 deletions
+15 -2
View File
@@ -597,5 +597,18 @@ CREATE TABLE `2fa_secrets` (
`date_setup` DATETIME NOT NULL DEFAULT current_timestamp(),
`last_time` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`ckey`) USING BTREE
)
COLLATE='utf8mb4_general_ci' ENGINE=InnoDB;
) COLLATE='utf8mb4_general_ci' ENGINE=InnoDB;
--
-- Table structure for table `pai_saves`
--
CREATE TABLE `pai_saves` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`ckey` VARCHAR(50) NOT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`pai_name` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`description` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`preferred_role` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`ooc_comments` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ckey` (`ckey`) USING BTREE
) COLLATE='utf8mb4_general_ci' ENGINE=InnoDB;
+13
View File
@@ -0,0 +1,13 @@
# Updates DB from 25-25, -AffectedArc07
# Adds a pAI saves table to the DB
CREATE TABLE `pai_saves` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`ckey` VARCHAR(50) NOT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`pai_name` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`description` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`preferred_role` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
`ooc_comments` LONGTEXT NULL DEFAULT '0' COLLATE 'utf8mb4_general_ci',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ckey` (`ckey`) USING BTREE
) COLLATE='utf8mb4_general_ci' ENGINE=InnoDB;
+1 -1
View File
@@ -365,7 +365,7 @@
#define INVESTIGATE_BOMB "bombs"
// The SQL version required by this version of the code
#define SQL_VERSION 25
#define SQL_VERSION 26
// Vending machine stuff
#define CAT_NORMAL 1
+2 -2
View File
@@ -357,8 +357,8 @@ GLOBAL_PROTECT(AdminProcCallSpamPrevention)
pai.real_name = pai.name
pai.key = choice.key
card.setPersonality(pai)
for(var/datum/paiCandidate/candidate in GLOB.paiController.pai_candidates)
if(candidate.key == choice.key)
for(var/datum/pai_save/candidate in GLOB.paiController.pai_candidates)
if(candidate.owner.ckey == choice.ckey)
GLOB.paiController.pai_candidates.Remove(candidate)
SSblackbox.record_feedback("tally", "admin_verb", 1, "Make pAI") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
+3
View File
@@ -114,6 +114,9 @@
/// Is the client watchlisted
var/watchlisted = FALSE
/// Client's pAI save
var/datum/pai_save/pai_save
/client/vv_edit_var(var_name, var_value)
switch(var_name)
// I know we will never be in a world where admins are editing client vars to let people bypass TOS
+3
View File
@@ -287,6 +287,8 @@
log_client_to_db(tdata) // Make sure our client exists in the DB
pai_save = new(src)
// This is where we load all of the clients stuff from the DB
if(SSdbcore.IsConnected())
// Load in all our client data from the DB
@@ -449,6 +451,7 @@
GLOB.directory -= ckey
GLOB.clients -= src
QDEL_NULL(chatOutput)
QDEL_NULL(pai_save)
if(movingmob)
movingmob.client_mobs_in_contents -= mob
UNSETEMPTY(movingmob.client_mobs_in_contents)
@@ -0,0 +1,8 @@
/datum/client_login_processor/pai_save
priority = 40
/datum/client_login_processor/pai_save/get_query(client/C)
return C.pai_save.get_query()
/datum/client_login_processor/pai_save/process_result(datum/db_query/Q, client/C)
C.pai_save.load_data(Q)
@@ -1,60 +1,65 @@
/*
name
key
description
role
comments
ready = 0
*/
/datum/pai_save
/// Client that owns the pAI
var/client/owner
/// pAI's name
var/pai_name
/// pAI's description
var/description
/// pAI's role
var/role
/// pAI's OOC comments
var/ooc_comments
/datum/paiCandidate/proc/savefile_path(mob/user)
return "data/player_saves/[copytext(user.ckey, 1, 2)]/[user.ckey]/pai.sav"
/datum/pai_save/New(client/C)
..()
owner = C
/datum/paiCandidate/proc/savefile_save(mob/user)
if(IsGuestKey(user.key))
return 0
/datum/pai_save/Destroy(force, ...)
owner = null
GLOB.paiController.pai_candidates -= src
return ..()
var/savefile/F = new /savefile(src.savefile_path(user))
// This proc seems useless but its used by client data loading
/datum/pai_save/proc/get_query()
var/datum/db_query/query = SSdbcore.NewQuery("SELECT pai_name, description, preferred_role, ooc_comments FROM pai_saves WHERE ckey=:ckey", list(
"ckey" = owner.ckey
))
return query
// Loads our data up
/datum/pai_save/proc/load_data(datum/db_query/Q)
while(Q.NextRow())
pai_name = Q.item[1]
description = Q.item[2]
role = Q.item[3]
ooc_comments = Q.item[4]
F["name"] << src.name
F["description"] << src.description
F["role"] << src.role
F["comments"] << src.comments
// Reload save from DB if the user edits it
/datum/pai_save/proc/reload_save()
var/datum/db_query/Q = get_query()
if(!Q.warn_execute())
qdel(Q)
return
load_data(Q)
qdel(Q)
F["version"] << 1
// Save their save to the DB
/datum/pai_save/proc/save_to_db()
var/datum/db_query/query = SSdbcore.NewQuery({"
INSERT INTO pai_saves (ckey, pai_name, description, preferred_role, ooc_comments)
VALUES (:ckey, :pai_name, :description, :preferred_role, :ooc_comments)
ON DUPLICATE KEY UPDATE pai_name=:pai_name2, description=:description2, preferred_role=:preferred_role2, ooc_comments=:ooc_comments2
"}, list(
"ckey" = owner.ckey,
"pai_name" = pai_name,
"description" = description,
"preferred_role" = role,
"ooc_comments" = ooc_comments,
"pai_name2" = pai_name,
"description2" = description,
"preferred_role2" = role,
"ooc_comments2" = ooc_comments
))
return 1
// loads the savefile corresponding to the mob's ckey
// if silent=true, report incompatible savefiles
// returns 1 if loaded (or file was incompatible)
// returns 0 if savefile did not exist
/datum/paiCandidate/proc/savefile_load(mob/user, silent = 1)
if(IsGuestKey(user.key))
return 0
var/path = savefile_path(user)
if(!fexists(path))
return 0
var/savefile/F = new /savefile(path)
if(!F) return //Not everyone has a pai savefile.
var/version = null
F["version"] >> version
if(isnull(version) || version != 1)
fdel(path)
if(!silent)
alert(user, "Your savefile was incompatible with this version and was deleted.")
return 0
F["name"] >> src.name
F["description"] >> src.description
F["role"] >> src.role
F["comments"] >> src.comments
return 1
query.warn_execute()
qdel(query)
+36 -55
View File
@@ -2,14 +2,6 @@
GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler for pAI candidates
/datum/paiCandidate
var/name
var/key
var/description
var/role
var/comments
var/ready = 0
/datum/paiController
var/list/pai_candidates = list()
var/list/asked = list()
@@ -18,7 +10,7 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
/datum/paiController/Topic(href, href_list[])
var/datum/paiCandidate/candidate = locateUID(href_list["candidate"])
var/datum/pai_save/candidate = locateUID(href_list["candidate"])
if(candidate)
if(!istype(candidate))
@@ -32,14 +24,14 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
return
if(usr.incapacitated() || isobserver(usr) || !card.Adjacent(usr))
return
if(istype(card, /obj/item/paicard) && istype(candidate, /datum/paiCandidate))
if(istype(card, /obj/item/paicard) && istype(candidate, /datum/pai_save))
var/mob/living/silicon/pai/pai = new(card)
if(!candidate.name)
if(!candidate.pai_name)
pai.name = pick(GLOB.ninja_names)
else
pai.name = candidate.name
pai.name = candidate.pai_name
pai.real_name = pai.name
pai.key = candidate.key
pai.key = candidate.owner.ckey
card.setPersonality(pai)
card.looking_for_personality = 0
@@ -64,7 +56,7 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
return
if(candidate)
if(candidate.key && usr.key && candidate.key != usr.key)
if(candidate.owner.ckey && usr.ckey && candidate.owner.ckey != usr.ckey)
message_admins("Warning: possible href exploit by [key_name_admin(usr)] (paiController/Topic, candidate and usr have different keys)")
log_debug("Warning: possible href exploit by [key_name(usr)] (paiController/Topic, candidate and usr have different keys)")
return
@@ -75,9 +67,9 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
switch(option)
if("name")
t = input("Enter a name for your pAI", "pAI Name", candidate.name) as text
t = input("Enter a name for your pAI", "pAI Name", candidate.pai_name) as text
if(t)
candidate.name = sanitize(copytext(t,1,MAX_NAME_LEN))
candidate.pai_name = sanitize(copytext(t,1,MAX_NAME_LEN))
if("desc")
t = input("Enter a description for your pAI", "pAI Description", candidate.description) as message
if(t)
@@ -87,26 +79,26 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
if(t)
candidate.role = sanitize(copytext(t,1,MAX_MESSAGE_LEN))
if("ooc")
t = input("Enter any OOC comments", "pAI OOC Comments", candidate.comments) as message
t = input("Enter any OOC comments", "pAI OOC Comments", candidate.ooc_comments) as message
if(t)
candidate.comments = sanitize(copytext(t,1,MAX_MESSAGE_LEN))
candidate.ooc_comments = sanitize(copytext(t,1,MAX_MESSAGE_LEN))
if("save")
candidate.savefile_save(usr)
if("load")
candidate.savefile_load(usr)
candidate.save_to_db(usr)
if("reload")
candidate.reload_save(usr)
//In case people have saved unsanitized stuff.
if(candidate.name)
candidate.name = sanitize(copytext(candidate.name,1,MAX_NAME_LEN))
if(candidate.pai_name)
candidate.pai_name = sanitize(copytext(candidate.pai_name, 1, MAX_NAME_LEN))
if(candidate.description)
candidate.description = sanitize(copytext(candidate.description,1,MAX_MESSAGE_LEN))
candidate.description = sanitize(copytext(candidate.description, 1, MAX_MESSAGE_LEN))
if(candidate.role)
candidate.role = sanitize(copytext(candidate.role,1,MAX_MESSAGE_LEN))
if(candidate.comments)
candidate.comments = sanitize(copytext(candidate.comments,1,MAX_MESSAGE_LEN))
candidate.role = sanitize(copytext(candidate.role, 1, MAX_MESSAGE_LEN))
if(candidate.ooc_comments)
candidate.ooc_comments = sanitize(copytext(candidate.ooc_comments, 1, MAX_MESSAGE_LEN))
if("submit")
if(candidate)
candidate.ready = 1
GLOB.paiController.pai_candidates |= candidate
for(var/obj/item/paicard/p in world)
if(p.looking_for_personality == 1)
p.alertUpdate()
@@ -114,18 +106,8 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
return
recruitWindow(usr)
/datum/paiController/proc/recruitWindow(mob/M as mob)
var/datum/paiCandidate/candidate
for(var/datum/paiCandidate/c in pai_candidates)
if(!istype(c) || !istype(M))
break
if(c.key == M.key)
candidate = c
if(!candidate)
candidate = new /datum/paiCandidate()
candidate.key = M.key
pai_candidates.Add(candidate)
/datum/paiController/proc/recruitWindow(mob/M)
var/datum/pai_save/candidate = M.client.pai_save
var/dat = ""
dat += {"
@@ -194,7 +176,7 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
<table>
<tr class="d0">
<th rowspan="2"><a href='byond://?src=[UID()];option=name;new=1;candidate=[candidate.UID()]'>Name</a>:</th>
<td class="desc">[candidate.name]&nbsp;</td>
<td class="desc">[candidate.pai_name]&nbsp;</td>
</tr>
<tr class="d1">
<td>What you plan to call yourself. Suggestions: Any character name you would choose for a station character OR an AI.</td>
@@ -215,7 +197,7 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
</tr>
<tr class="d0">
<th rowspan="2"><a href='byond://?src=[UID()];option=ooc;new=1;candidate=[candidate.UID()]'>OOC Comments</a>:</th>
<td class="desc">[candidate.comments]&nbsp;</td>
<td class="desc">[candidate.ooc_comments]&nbsp;</td>
</tr>
<tr class="d1">
<td>Anything you'd like to address specifically to the player reading this in an OOC manner. \"I prefer more serious RP.\", \"I'm still learning the interface!\", etc. Feel free to leave this blank if you want.</td>
@@ -230,7 +212,7 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
</tr>
<tr>
<td class="button">
<a href='byond://?src=[UID()];option=load;new=1;candidate=[candidate.UID()]' class="button">Load Personality</a>
<a href='byond://?src=[UID()];option=reload;new=1;candidate=[candidate.UID()]' class="button">Reload Personality</a>
</td>
</tr>
</table><br>
@@ -246,14 +228,13 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
/datum/paiController/proc/findPAI(obj/item/paicard/p, mob/user)
requestRecruits(p, user)
var/list/available = list()
for(var/datum/paiCandidate/c in GLOB.paiController.pai_candidates)
if(c.ready)
var/found = 0
for(var/mob/o in GLOB.respawnable_list)
if(o.key == c.key)
found = 1
if(found)
available.Add(c)
for(var/datum/pai_save/c in GLOB.paiController.pai_candidates)
var/found = 0
for(var/mob/o in GLOB.respawnable_list)
if(o.ckey == c.owner.ckey)
found = 1
if(found)
available.Add(c)
var/dat = ""
dat += {"
@@ -325,12 +306,12 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
"}
dat += "<p>Displaying available AI personalities from central database... If there are no entries, or if a suitable entry is not listed, check again later as more personalities may be added.</p>"
for(var/datum/paiCandidate/c in available)
for(var/datum/pai_save/c in available)
dat += {"
<table class="desc">
<tr class="d0">
<th>Name:</th>
<td>[c.name]</td>
<td>[c.pai_name]</td>
</tr>
<tr class="d1">
<th>Description:</th>
@@ -342,11 +323,11 @@ GLOBAL_DATUM_INIT(paiController, /datum/paiController, new) // Global handler fo
</tr>
<tr class="d1">
<th>OOC Comments:</th>
<td>[c.comments]</td>
<td>[c.ooc_comments]</td>
</tr>
</table>
<table class="download">
<td class="download"><a href='byond://?src=[UID()];download=1;candidate=[c.UID()];device=\ref[p]' class="button"><b>Download [c.name]</b></a>
<td class="download"><a href='byond://?src=[UID()];download=1;candidate=[c.UID()];device=\ref[p]' class="button"><b>Download [c.pai_name]</b></a>
</td>
</table>
<br>
+1 -1
View File
@@ -141,7 +141,7 @@ ipc_screens = [
# Enable/disable the database on a whole
sql_enabled = false
# SQL version. If this is a mismatch, round start will be delayed
sql_version = 25
sql_version = 26
# SQL server address. Can be an IP or DNS name
sql_address = "127.0.0.1"
# SQL server port
+1
View File
@@ -1384,6 +1384,7 @@
#include "code\modules\client\login_processing\37-alts_ip.dm"
#include "code\modules\client\login_processing\38-alts_cid.dm"
#include "code\modules\client\login_processing\39-cid_count.dm"
#include "code\modules\client\login_processing\40-pai_save.dm"
#include "code\modules\client\login_processing\__client_login_processor.dm"
#include "code\modules\client\preference\character.dm"
#include "code\modules\client\preference\link_processing.dm"
@@ -0,0 +1,132 @@
# AffectedArc07 2021-10-04
import struct, os, mysql.connector, argparse, html
from datetime import datetime
TYPE_NULL = 0x0
TYPE_STRING = 0x1
TYPE_NUMBER = 0x4
KEY_XOR_KEY = 0x53
DATA_XOR_KEY = 0x3A
KEY_LEN_OFFSET = 8
KEY_OFFSET = 9
# Might be shitcode but mehhhhhhhhhhh
class Decoder:
def __init__(self, data, key, jump=9):
self.generator = self.decode_data(data, key, jump)
def read_bytes(self, num):
return [next(self.generator) for _ in range(num)]
def read_all_bytes(self):
result = []
for val in self.generator:
result.append(val)
return result
def read_number(self):
return struct.unpack("<f", bytes(self.read_bytes(4)))[0]
def read_string(self, length):
return "".join([chr(c) for c in self.read_all_bytes()])
def decode_data(self, data, key, jump):
for byte in data:
yield byte ^ key
key = (key + jump) % 256
def decode_sav(path):
file = open(path, "rb")
is_file_header = True
result = {}
while True:
entry_header = file.read(5)
if len(entry_header) < 5:
break
entry_length = int.from_bytes(entry_header[:-1], byteorder='little')
entry_data = file.read(entry_length)
if entry_header[4] != 1:
continue
if is_file_header:
is_file_header = False
continue
key_length = entry_data[KEY_LEN_OFFSET]
key_data = entry_data[KEY_OFFSET:KEY_OFFSET+key_length]
key_data = Decoder(key_data, KEY_XOR_KEY).read_string(key_length)
if not key_data: continue
data_length_offset = KEY_OFFSET + key_length
data_length = int.from_bytes(entry_data[data_length_offset:data_length_offset+4], "little")
data_start = data_length_offset + 4
value = None
decoder = Decoder(entry_data[data_start:data_start+data_length], DATA_XOR_KEY)
typ = decoder.read_bytes(1)[0]
if typ == TYPE_NULL:
value = None
elif typ == TYPE_NUMBER:
value = decoder.read_number()
elif typ == TYPE_STRING:
length = int.from_bytes(decoder.read_bytes(2), "little")
value = decoder.read_string(length)
result[key_data] = value
return result
# Get outer dirs
basepath = "data/player_saves"
all_saves = []
start = datetime.now()
for dir in os.listdir(basepath):
for dir2 in os.listdir("{}/{}".format(basepath, dir)):
pai_data = decode_sav("{}/{}/{}/pai.sav".format(basepath, dir, dir2))
save_data = {}
save_data["name"] = pai_data["name"] if "name" in pai_data else None
save_data["description"] = pai_data["description"] if "description" in pai_data else None
save_data["role"] = pai_data["role"] if "role" in pai_data else None
save_data["comments"] = pai_data["comments"] if "comments" in pai_data else None
save_data["version"] = int(pai_data["version"]) if "version" in pai_data else None
save_data["ckey"] = dir2
all_saves.append(save_data)
parser = argparse.ArgumentParser()
parser.add_argument("host", help="SQL server host")
parser.add_argument("user", help="SQL login user")
parser.add_argument("pw", help="SQL login pass")
parser.add_argument("db", help="DB name")
args = parser.parse_args()
db = mysql.connector.connect(host=args.host, user=args.user, password=args.pw, database=args.db)
cursor = db.cursor()
stmt = "INSERT INTO pai_saves (ckey, pai_name, description, preferred_role, ooc_comments) VALUES (%s, %s, %s, %s, %s)"
for entry in all_saves:
if entry["name"]:
entry["name"] = html.unescape(entry["name"])
if entry["description"]:
entry["description"] = html.unescape(entry["description"])
if entry["role"]:
entry["role"] = html.unescape(entry["role"])
if entry["comments"]:
entry["comments"] = html.unescape(entry["comments"])
values = (entry["ckey"], entry["name"], entry["description"], entry["role"], entry["comments"])
cursor.execute(stmt, values)
db.commit()
duration = datetime.now() - start
print("Complete within {}.{}s".format(duration.seconds, duration.microseconds))