mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 18:22:39 +00:00
Adds an In-game Feedback System
This commit is contained in:
11
code/__defines/_protect.dm
Normal file
11
code/__defines/_protect.dm
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
///Protects a datum from being VV'd
|
||||||
|
#define GENERAL_PROTECT_DATUM(Path)\
|
||||||
|
##Path/can_vv_get(var_name){\
|
||||||
|
return FALSE;\
|
||||||
|
}\
|
||||||
|
##Path/vv_edit_var(var_name, var_value){\
|
||||||
|
return FALSE;\
|
||||||
|
}\
|
||||||
|
##Path/CanProcCall(procname){\
|
||||||
|
return FALSE;\
|
||||||
|
}
|
||||||
@@ -113,6 +113,7 @@
|
|||||||
#define MAX_RECORD_LENGTH 24576
|
#define MAX_RECORD_LENGTH 24576
|
||||||
#define MAX_LNAME_LEN 64
|
#define MAX_LNAME_LEN 64
|
||||||
#define MAX_NAME_LEN 52
|
#define MAX_NAME_LEN 52
|
||||||
|
#define MAX_FEEDBACK_LENGTH 4096
|
||||||
#define MAX_TEXTFILE_LENGTH 128000 // 512GQ file
|
#define MAX_TEXTFILE_LENGTH 128000 // 512GQ file
|
||||||
|
|
||||||
// Event defines.
|
// Event defines.
|
||||||
|
|||||||
7
code/__defines/sqlite_defines.dm
Normal file
7
code/__defines/sqlite_defines.dm
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#define SQLITE_TABLE_FEEDBACK "feedback"
|
||||||
|
|
||||||
|
#define SQLITE_FEEDBACK_COLUMN_ID "id"
|
||||||
|
#define SQLITE_FEEDBACK_COLUMN_AUTHOR "author"
|
||||||
|
#define SQLITE_FEEDBACK_COLUMN_TOPIC "topic"
|
||||||
|
#define SQLITE_FEEDBACK_COLUMN_CONTENT "content"
|
||||||
|
#define SQLITE_FEEDBACK_COLUMN_DATETIME "datetime"
|
||||||
@@ -52,6 +52,7 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G
|
|||||||
// Subsystem init_order, from highest priority to lowest priority
|
// Subsystem init_order, from highest priority to lowest priority
|
||||||
// Subsystems shutdown in the reverse of the order they initialize in
|
// Subsystems shutdown in the reverse of the order they initialize in
|
||||||
// The numbers just define the ordering, they are meaningless otherwise.
|
// The numbers just define the ordering, they are meaningless otherwise.
|
||||||
|
#define INIT_ORDER_SQLITE 19
|
||||||
#define INIT_ORDER_CHEMISTRY 18
|
#define INIT_ORDER_CHEMISTRY 18
|
||||||
#define INIT_ORDER_MAPPING 17
|
#define INIT_ORDER_MAPPING 17
|
||||||
#define INIT_ORDER_DECALS 16
|
#define INIT_ORDER_DECALS 16
|
||||||
|
|||||||
@@ -1577,3 +1577,11 @@ var/mob/dview/dview_mob = new
|
|||||||
else
|
else
|
||||||
return "\[[url_encode(thing.tag)]\]"
|
return "\[[url_encode(thing.tag)]\]"
|
||||||
return "\ref[input]"
|
return "\ref[input]"
|
||||||
|
|
||||||
|
// Painlessly creates an <a href=...> element.
|
||||||
|
// First argument is where to send the Topic call to when clicked. Should be a reference to an object. This is generally src, but not always.
|
||||||
|
// Second one is for all the params that will be sent. Uses an assoc list (e.g. "value" = "5").
|
||||||
|
// Note that object refs will be converted to text, as if \ref[thing] was done. To get the ref back on Topic() side, you will need to use locate().
|
||||||
|
// Third one is the text that will be clickable.
|
||||||
|
/proc/href(href_src, list/href_params, href_text)
|
||||||
|
return "<a href='?src=\ref[href_src];[list2params(href_params)]'>[href_text]</a>"
|
||||||
@@ -252,6 +252,17 @@ var/list/gamemode_cache = list()
|
|||||||
var/random_submap_orientation = FALSE // If true, submaps loaded automatically can be rotated.
|
var/random_submap_orientation = FALSE // If true, submaps loaded automatically can be rotated.
|
||||||
var/autostart_solars = FALSE // If true, specifically mapped in solar control computers will set themselves up when the round starts.
|
var/autostart_solars = FALSE // If true, specifically mapped in solar control computers will set themselves up when the round starts.
|
||||||
|
|
||||||
|
// New shiny SQLite stuff.
|
||||||
|
// The basics.
|
||||||
|
var/sqlite_enabled = FALSE // If it should even be active. SQLite can be ran alongside other databases but you should not have them do the same functions.
|
||||||
|
|
||||||
|
// In-Game Feedback.
|
||||||
|
var/sqlite_feedback = FALSE // Feedback cannot be submitted if this is false.
|
||||||
|
var/list/sqlite_feedback_topics = list("General") // A list of 'topics' that feedback can be catagorized under by the submitter.
|
||||||
|
var/sqlite_feedback_privacy = FALSE // If true, feedback submitted can have its author name be obfuscated. This is not 100% foolproof (it's md5 ffs) but can stop casual snooping.
|
||||||
|
var/sqlite_feedback_cooldown = 0 // How long one must wait, in days, to submit another feedback form. Used to help prevent spam, especially with privacy active. 0 = No limit.
|
||||||
|
var/sqlite_feedback_min_age = 0 // Used to block new people from giving feedback. This metric is very bad but it can help slow down spammers.
|
||||||
|
|
||||||
/datum/configuration/New()
|
/datum/configuration/New()
|
||||||
var/list/L = typesof(/datum/game_mode) - /datum/game_mode
|
var/list/L = typesof(/datum/game_mode) - /datum/game_mode
|
||||||
for (var/T in L)
|
for (var/T in L)
|
||||||
@@ -841,6 +852,23 @@ var/list/gamemode_cache = list()
|
|||||||
if("autostart_solars")
|
if("autostart_solars")
|
||||||
config.autostart_solars = TRUE
|
config.autostart_solars = TRUE
|
||||||
|
|
||||||
|
if("sqlite_enabled")
|
||||||
|
config.sqlite_enabled = TRUE
|
||||||
|
|
||||||
|
if("sqlite_feedback")
|
||||||
|
config.sqlite_feedback = TRUE
|
||||||
|
|
||||||
|
if("sqlite_feedback_topics")
|
||||||
|
config.sqlite_feedback_topics = splittext(value, ";")
|
||||||
|
if(!config.sqlite_feedback_topics.len)
|
||||||
|
config.sqlite_feedback_topics += "General"
|
||||||
|
|
||||||
|
if("sqlite_feedback_privacy")
|
||||||
|
config.sqlite_feedback_privacy = TRUE
|
||||||
|
|
||||||
|
if("sqlite_feedback_cooldown")
|
||||||
|
config.sqlite_feedback_cooldown = text2num(value)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|||||||
187
code/controllers/subsystems/sqlite.dm
Normal file
187
code/controllers/subsystems/sqlite.dm
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// This holds all the code needed to manage and use a SQLite database.
|
||||||
|
// It is merely a file sitting inside the data directory, as opposed to a full fledged DB service,
|
||||||
|
// however this makes it a lot easier to test, and it is natively supported by BYOND.
|
||||||
|
SUBSYSTEM_DEF(sqlite)
|
||||||
|
name = "SQLite"
|
||||||
|
init_order = INIT_ORDER_SQLITE
|
||||||
|
flags = SS_NO_FIRE
|
||||||
|
var/database/sqlite_db = null
|
||||||
|
|
||||||
|
/datum/controller/subsystem/sqlite/Initialize(timeofday)
|
||||||
|
connect()
|
||||||
|
if(sqlite_db)
|
||||||
|
init_schema(sqlite_db)
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/datum/controller/subsystem/sqlite/proc/connect()
|
||||||
|
if(!config.sqlite_enabled)
|
||||||
|
return
|
||||||
|
|
||||||
|
if(!sqlite_db)
|
||||||
|
sqlite_db = new("data/sqlite/sqlite.db") // The path has to be hardcoded or BYOND silently fails.
|
||||||
|
|
||||||
|
|
||||||
|
if(!sqlite_db)
|
||||||
|
to_world_log("Failed to load or create a SQLite database.")
|
||||||
|
log_debug("ERROR: SQLite database is active in config but failed to load.")
|
||||||
|
else
|
||||||
|
to_world_log("Sqlite database connected.")
|
||||||
|
|
||||||
|
// Makes the tables, if they do not already exist in the sqlite file.
|
||||||
|
/datum/controller/subsystem/sqlite/proc/init_schema(database/sqlite_object)
|
||||||
|
// Feedback table.
|
||||||
|
// Note that this is for direct feedback from players using the in-game feedback system and NOT for stat tracking.
|
||||||
|
// Player ckeys are not stored in this table as a unique key due to a config option to hash the keys to encourage more honest feedback.
|
||||||
|
/*
|
||||||
|
* id - Primary unique key to ID a specific piece of feedback.
|
||||||
|
NOT used to id people submitting feedback.
|
||||||
|
* author - The person who submitted it. Will be the ckey, or a hash of the ckey,
|
||||||
|
if both the config supports it, and the user wants it.
|
||||||
|
* topic - A specific category to organize feedback under. Options are defined in the config file.
|
||||||
|
* content - What the author decided to write to the staff. Limited to MAX_FEEDBACK_LENGTH.
|
||||||
|
* datetime - When the author submitted their feedback, acts as a timestamp.
|
||||||
|
*/
|
||||||
|
var/database/query/init_schema = new(
|
||||||
|
{"
|
||||||
|
CREATE TABLE IF NOT EXISTS [SQLITE_TABLE_FEEDBACK]
|
||||||
|
(
|
||||||
|
`[SQLITE_FEEDBACK_COLUMN_ID]` INTEGER NOT NULL UNIQUE,
|
||||||
|
`[SQLITE_FEEDBACK_COLUMN_AUTHOR]` TEXT NOT NULL,
|
||||||
|
`[SQLITE_FEEDBACK_COLUMN_TOPIC]` TEXT NOT NULL,
|
||||||
|
`[SQLITE_FEEDBACK_COLUMN_CONTENT]` TEXT NOT NULL,
|
||||||
|
`[SQLITE_FEEDBACK_COLUMN_DATETIME]` TEXT NOT NULL,
|
||||||
|
PRIMARY KEY(`[SQLITE_FEEDBACK_COLUMN_ID]`)
|
||||||
|
);
|
||||||
|
"}
|
||||||
|
)
|
||||||
|
init_schema.Execute(sqlite_object)
|
||||||
|
sqlite_check_for_errors(init_schema, "Feedback table creation")
|
||||||
|
|
||||||
|
// Add more schemas below this if the SQLite DB gets expanded for things like persistant news, polls, bans, deaths, etc.
|
||||||
|
|
||||||
|
// General error checking for SQLite.
|
||||||
|
// Returns true if something went wrong. Also writes a log.
|
||||||
|
// The desc parameter should be unique for each call, to make it easier to track down where the error occured.
|
||||||
|
/datum/controller/subsystem/sqlite/proc/sqlite_check_for_errors(var/database/query/query_used, var/desc)
|
||||||
|
if(query_used && query_used.ErrorMsg())
|
||||||
|
log_debug("SQLite Error: [desc] : [query_used.ErrorMsg()]")
|
||||||
|
return TRUE
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
|
||||||
|
/************
|
||||||
|
* Feedback *
|
||||||
|
************/
|
||||||
|
|
||||||
|
// Inserts data into the feedback table in a painless manner.
|
||||||
|
// Returns TRUE if no issues happened, FALSE otherwise.
|
||||||
|
/datum/controller/subsystem/sqlite/proc/insert_feedback(author, topic, content, database/sqlite_object)
|
||||||
|
if(!author || !topic || !content)
|
||||||
|
CRASH("One or more parameters was invalid.")
|
||||||
|
|
||||||
|
// Sanitize everything to avoid sneaky stuff.
|
||||||
|
var/sqlite_author = sql_sanitize_text(ckey(lowertext(author)))
|
||||||
|
var/sqlite_content = sql_sanitize_text(content)
|
||||||
|
var/sqlite_topic = sql_sanitize_text(topic)
|
||||||
|
|
||||||
|
var/database/query/query = new(
|
||||||
|
"INSERT INTO [SQLITE_TABLE_FEEDBACK] (\
|
||||||
|
[SQLITE_FEEDBACK_COLUMN_AUTHOR], \
|
||||||
|
[SQLITE_FEEDBACK_COLUMN_TOPIC], \
|
||||||
|
[SQLITE_FEEDBACK_COLUMN_CONTENT], \
|
||||||
|
[SQLITE_FEEDBACK_COLUMN_DATETIME]) \
|
||||||
|
\
|
||||||
|
VALUES (\
|
||||||
|
?,\
|
||||||
|
?,\
|
||||||
|
?,\
|
||||||
|
datetime('now'))",
|
||||||
|
sqlite_author,
|
||||||
|
sqlite_topic,
|
||||||
|
sqlite_content
|
||||||
|
)
|
||||||
|
query.Execute(sqlite_object)
|
||||||
|
return !sqlite_check_for_errors(query, "Insert Feedback")
|
||||||
|
|
||||||
|
/datum/controller/subsystem/sqlite/proc/can_submit_feedback(client/C)
|
||||||
|
if(!config.sqlite_enabled)
|
||||||
|
return FALSE
|
||||||
|
if(config.sqlite_feedback_min_age && !is_old_enough(C))
|
||||||
|
return FALSE
|
||||||
|
if(config.sqlite_feedback_cooldown > 0 && get_feedback_cooldown(C.key, config.sqlite_feedback_cooldown, sqlite_db) > 0)
|
||||||
|
return FALSE
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
// Returns TRUE if the player is 'old' enough, according to the config.
|
||||||
|
/datum/controller/subsystem/sqlite/proc/is_old_enough(client/C)
|
||||||
|
if(get_player_age(C.key) < config.sqlite_feedback_min_age)
|
||||||
|
return FALSE
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
|
||||||
|
// Returns how many days someone has to wait, to submit more feedback, or 0 if they can do so right now.
|
||||||
|
/datum/controller/subsystem/sqlite/proc/get_feedback_cooldown(player_ckey, cooldown, database/sqlite_object)
|
||||||
|
player_ckey = sql_sanitize_text(ckey(lowertext(player_ckey)))
|
||||||
|
var/potential_hashed_ckey = sql_sanitize_text(md5(player_ckey + SSsqlite.get_feedback_pepper()))
|
||||||
|
|
||||||
|
// First query is to get the most recent time the player has submitted feedback.
|
||||||
|
var/database/query/query = new({"
|
||||||
|
SELECT [SQLITE_FEEDBACK_COLUMN_DATETIME]
|
||||||
|
FROM [SQLITE_TABLE_FEEDBACK]
|
||||||
|
WHERE [SQLITE_FEEDBACK_COLUMN_AUTHOR] == ? OR [SQLITE_FEEDBACK_COLUMN_AUTHOR] == ?
|
||||||
|
ORDER BY [SQLITE_FEEDBACK_COLUMN_DATETIME]
|
||||||
|
DESC LIMIT 1;
|
||||||
|
"},
|
||||||
|
player_ckey,
|
||||||
|
potential_hashed_ckey
|
||||||
|
)
|
||||||
|
query.Execute(sqlite_object)
|
||||||
|
sqlite_check_for_errors(query, "Rate Limited Check 1")
|
||||||
|
|
||||||
|
// It is possible this is their first time, so there won't be a next row.
|
||||||
|
if(query.NextRow()) // If this is true, the user has submitted feedback at least once.
|
||||||
|
var/list/row_data = query.GetRowData()
|
||||||
|
var/last_submission_datetime = row_data[SQLITE_FEEDBACK_COLUMN_DATETIME]
|
||||||
|
|
||||||
|
// Now we have the datetime, we need to do something to compare it.
|
||||||
|
// Second query is to calculate the difference between two datetimes.
|
||||||
|
// This is done on the SQLite side because parsing datetimes with BYOND is probably a bad idea.
|
||||||
|
query = new(
|
||||||
|
"SELECT julianday('now') - julianday(?) \
|
||||||
|
AS 'datediff';",
|
||||||
|
last_submission_datetime
|
||||||
|
)
|
||||||
|
query.Execute(sqlite_object)
|
||||||
|
sqlite_check_for_errors(query, "Rate Limited Check 2")
|
||||||
|
|
||||||
|
query.NextRow()
|
||||||
|
row_data = query.GetRowData()
|
||||||
|
var/date_diff = row_data["datediff"]
|
||||||
|
|
||||||
|
// Now check if it's too soon to give more feedback.
|
||||||
|
if(text2num(date_diff) < cooldown) // Too soon.
|
||||||
|
return round(cooldown - date_diff, 0.1)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
// A Pepper is like a Salt but only one exists and is supposed to be outside of a database.
|
||||||
|
// If the file is properly protected, it can only be viewed/copied by sys-admins generating a log, which is much more conspicious than accessing/copying a DB.
|
||||||
|
// This stops mods/admins/etc from guessing the author by shoving names in an MD5 hasher until they pick the right one.
|
||||||
|
// Don't use this for things needing actual security.
|
||||||
|
/datum/controller/subsystem/sqlite/proc/get_feedback_pepper()
|
||||||
|
var/pepper_file = file2list("config/sqlite_feedback_pepper.txt")
|
||||||
|
var/pepper = null
|
||||||
|
for(var/line in pepper_file)
|
||||||
|
if(!line)
|
||||||
|
continue
|
||||||
|
if(length(line) == 0)
|
||||||
|
continue
|
||||||
|
else if(copytext(line, 1, 2) == "#")
|
||||||
|
continue
|
||||||
|
else
|
||||||
|
pepper = line
|
||||||
|
break
|
||||||
|
return pepper
|
||||||
|
|
||||||
|
/datum/controller/subsystem/sqlite/CanProcCall(procname)
|
||||||
|
return procname != "get_feedback_pepper"
|
||||||
52
code/datums/managed_browsers/_managed_browser.dm
Normal file
52
code/datums/managed_browsers/_managed_browser.dm
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
GLOBAL_VAR(managed_browser_id_ticker)
|
||||||
|
|
||||||
|
// This holds information on managing a /datum/browser object.
|
||||||
|
// Managing can include things like persisting the state of specific information inside of this object, receiving Topic() calls, or deleting itself when the window is closed.
|
||||||
|
// This is useful for browser windows to be able to stand 'on their own' instead of being tied to something in the game world, like an object or mob.
|
||||||
|
/datum/managed_browser
|
||||||
|
var/client/my_client = null
|
||||||
|
var/browser_id = null
|
||||||
|
var/base_browser_id = null
|
||||||
|
|
||||||
|
var/title = null
|
||||||
|
var/size_x = 200
|
||||||
|
var/size_y = 400
|
||||||
|
|
||||||
|
var/display_when_created = TRUE
|
||||||
|
|
||||||
|
/datum/managed_browser/New(client/new_client)
|
||||||
|
if(!new_client)
|
||||||
|
crash_with("Managed browser object was not given a client.")
|
||||||
|
return
|
||||||
|
if(!base_browser_id)
|
||||||
|
crash_with("Managed browser object does not have a base browser id defined in its type.")
|
||||||
|
return
|
||||||
|
|
||||||
|
my_client = new_client
|
||||||
|
browser_id = "[base_browser_id]-[GLOB.managed_browser_id_ticker++]"
|
||||||
|
|
||||||
|
if(display_when_created)
|
||||||
|
display()
|
||||||
|
|
||||||
|
/datum/managed_browser/Destroy()
|
||||||
|
my_client = null
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
// Override if you want to have the browser title change conditionally.
|
||||||
|
// Otherwise it's easier to just change the title variable directly.
|
||||||
|
/datum/managed_browser/proc/get_title()
|
||||||
|
return title
|
||||||
|
|
||||||
|
// Override to display the html information.
|
||||||
|
// It is suggested to build it with a list, and use list.Join() at the end.
|
||||||
|
// This helps prevent excessive concatination, which helps preserves BYOND's string tree from becoming a laggy mess.
|
||||||
|
/datum/managed_browser/proc/get_html()
|
||||||
|
return
|
||||||
|
|
||||||
|
/datum/managed_browser/proc/display()
|
||||||
|
interact(get_html(), get_title(), my_client)
|
||||||
|
|
||||||
|
/datum/managed_browser/proc/interact(html, title, client/C)
|
||||||
|
var/datum/browser/popup = new(C.mob, browser_id, title, size_x, size_y, src)
|
||||||
|
popup.set_content(html)
|
||||||
|
popup.open()
|
||||||
147
code/datums/managed_browsers/feedback_form.dm
Normal file
147
code/datums/managed_browsers/feedback_form.dm
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/client
|
||||||
|
var/datum/managed_browser/feedback_form/feedback_form = null
|
||||||
|
|
||||||
|
/client/can_vv_get(var_name)
|
||||||
|
return var_name != NAMEOF(src, feedback_form) // No snooping.
|
||||||
|
|
||||||
|
GENERAL_PROTECT_DATUM(datum/managed_browser/feedback_form)
|
||||||
|
|
||||||
|
// A fairly simple object to hold information about a player's feedback as it's being written.
|
||||||
|
// Having this be it's own object instead of being baked into /mob/new_player allows for it to be used
|
||||||
|
// from other places than just the lobby, and makes it a lot harder for people with dev powers to be naughty with it using VV/proccall.
|
||||||
|
/datum/managed_browser/feedback_form
|
||||||
|
base_browser_id = "feedback_form"
|
||||||
|
title = "Server Feedback"
|
||||||
|
size_x = 480
|
||||||
|
size_y = 520
|
||||||
|
var/feedback_topic = null
|
||||||
|
var/feedback_body = null
|
||||||
|
var/feedback_hide_author = FALSE
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_form/New(client/new_client)
|
||||||
|
feedback_topic = config.sqlite_feedback_topics[1]
|
||||||
|
..(new_client)
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_form/Destroy()
|
||||||
|
if(my_client)
|
||||||
|
my_client.feedback_form = null
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
// Privacy option is allowed if both the config allows it, and the pepper file exists and isn't blank.
|
||||||
|
/datum/managed_browser/feedback_form/proc/can_be_private()
|
||||||
|
return config.sqlite_feedback_privacy && SSsqlite.get_feedback_pepper()
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_form/display()
|
||||||
|
if(!my_client)
|
||||||
|
return
|
||||||
|
if(!SSsqlite.can_submit_feedback(my_client))
|
||||||
|
return
|
||||||
|
..()
|
||||||
|
|
||||||
|
// Builds the window for players to review their feedback.
|
||||||
|
/datum/managed_browser/feedback_form/get_html()
|
||||||
|
var/list/dat = list("<html><body>")
|
||||||
|
dat += "<center>"
|
||||||
|
dat += "<font size='2'>"
|
||||||
|
dat += "Here, you can write some feedback for the server.<br>"
|
||||||
|
dat += "Note that HTML is NOT supported!<br>"
|
||||||
|
dat += "Click the edit button to begin writing.<br>"
|
||||||
|
|
||||||
|
dat += "Your feedback is currently [length(feedback_body)]/[MAX_FEEDBACK_LENGTH] letters long."
|
||||||
|
dat += "</font>"
|
||||||
|
dat += "<hr>"
|
||||||
|
|
||||||
|
dat += "<h2>Preview</h2></center>"
|
||||||
|
|
||||||
|
dat += "Author: "
|
||||||
|
|
||||||
|
if(can_be_private())
|
||||||
|
if(!feedback_hide_author)
|
||||||
|
dat += "[my_client.ckey] "
|
||||||
|
dat += span("linkOn", "<b>Visible</b>")
|
||||||
|
dat += " | "
|
||||||
|
dat += href(src, list("feedback_hide_author" = 1), "Hashed")
|
||||||
|
else
|
||||||
|
dat += "[md5(ckey(lowertext(my_client.ckey + SSsqlite.get_feedback_pepper())))] "
|
||||||
|
dat += href(src, list("feedback_hide_author" = 0), "Visible")
|
||||||
|
dat += " | "
|
||||||
|
dat += span("linkOn", "<b>Hashed</b>")
|
||||||
|
else
|
||||||
|
dat += my_client.ckey
|
||||||
|
dat += "<br>"
|
||||||
|
|
||||||
|
if(config.sqlite_feedback_topics.len > 1)
|
||||||
|
dat += "Topic: [href(src, list("feedback_choose_topic" = 1), feedback_topic)]<br>"
|
||||||
|
else
|
||||||
|
dat += "Topic: [config.sqlite_feedback_topics[1]]<br>"
|
||||||
|
|
||||||
|
dat += "<br>"
|
||||||
|
if(feedback_body)
|
||||||
|
dat += replacetext(feedback_body, "\n", "<br>") // So newlines will look like they work in the preview.
|
||||||
|
else
|
||||||
|
dat += "<i>\[Feedback goes here...\]</i>"
|
||||||
|
dat += "<br>"
|
||||||
|
dat += href(src, list("feedback_edit_body" = 1), "Edit")
|
||||||
|
dat += "<hr>"
|
||||||
|
|
||||||
|
if(config.sqlite_feedback_cooldown)
|
||||||
|
dat += "<i>Please note that you will have to wait [config.sqlite_feedback_cooldown] day\s before \
|
||||||
|
being able to write more feedback after submitting.</i><br>"
|
||||||
|
|
||||||
|
dat += href(src, list("feedback_submit" = 1), "Submit")
|
||||||
|
dat += "</body></html>"
|
||||||
|
return dat.Join()
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_form/Topic(href, href_list[])
|
||||||
|
if(!my_client)
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
if(href_list["feedback_edit_body"])
|
||||||
|
// This is deliberately not sanitized here, and is instead checked when hitting the submission button,
|
||||||
|
// as we want to give the user a chance to fix it without needing to rewrite the whole thing.
|
||||||
|
feedback_body = input(my_client, "Please write your feedback here.", "Feedback Body", feedback_body) as null|message
|
||||||
|
display() // Refresh the window with new information.
|
||||||
|
return
|
||||||
|
|
||||||
|
if(href_list["feedback_hide_author"])
|
||||||
|
if(!can_be_private())
|
||||||
|
feedback_hide_author = FALSE
|
||||||
|
else
|
||||||
|
feedback_hide_author = text2num(href_list["feedback_hide_author"])
|
||||||
|
display()
|
||||||
|
return
|
||||||
|
|
||||||
|
if(href_list["feedback_choose_topic"])
|
||||||
|
feedback_topic = input(my_client, "Choose the topic you want to submit your feedback under.", "Feedback Topic", feedback_topic) in config.sqlite_feedback_topics
|
||||||
|
display()
|
||||||
|
return
|
||||||
|
|
||||||
|
if(href_list["feedback_submit"])
|
||||||
|
// Do some last minute validation, and tell the user if something goes wrong,
|
||||||
|
// so we don't wipe out their ten thousand page essay due to having a few too many characters.
|
||||||
|
if(length(feedback_body) > MAX_FEEDBACK_LENGTH)
|
||||||
|
to_chat(my_client, span("warning", "Your feedback is too long, at [length(feedback_body)] characters, where as the \
|
||||||
|
limit is [MAX_FEEDBACK_LENGTH]. Please shorten it and try again."))
|
||||||
|
return
|
||||||
|
|
||||||
|
var/text = sanitize(feedback_body, max_length = 0, encode = TRUE, trim = FALSE, extra = FALSE)
|
||||||
|
if(!text) // No text, or it was super invalid.
|
||||||
|
to_chat(my_client, span("warning", "It appears you didn't write anything, or it was invalid."))
|
||||||
|
return
|
||||||
|
|
||||||
|
if(alert(my_client, "Are you sure you want to submit your feedback?", "Confirm Submission", "No", "Yes") == "Yes")
|
||||||
|
var/author_text = my_client.ckey
|
||||||
|
if(can_be_private() && feedback_hide_author)
|
||||||
|
author_text = md5(my_client.ckey + SSsqlite.get_feedback_pepper())
|
||||||
|
|
||||||
|
var/success = SSsqlite.insert_feedback(author = author_text, topic = feedback_topic, content = feedback_body, sqlite_object = SSsqlite.sqlite_db)
|
||||||
|
if(!success)
|
||||||
|
to_chat(my_client, span("warning", "Something went wrong while inserting your feedback into the database. Please try again. \
|
||||||
|
If this happens again, you should contact a developer."))
|
||||||
|
return
|
||||||
|
|
||||||
|
my_client.mob << browse(null, "window=[browser_id]") // Closes the window.
|
||||||
|
if(istype(my_client.mob, /mob/new_player))
|
||||||
|
var/mob/new_player/NP = my_client.mob
|
||||||
|
NP.new_player_panel_proc() // So the feedback button goes away, if the user gets put on cooldown.
|
||||||
|
qdel(src)
|
||||||
162
code/datums/managed_browsers/feedback_viewer.dm
Normal file
162
code/datums/managed_browsers/feedback_viewer.dm
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/client
|
||||||
|
var/datum/managed_browser/feedback_viewer/feedback_viewer = null
|
||||||
|
|
||||||
|
/datum/admins/proc/view_feedback()
|
||||||
|
set category = "Admin"
|
||||||
|
set name = "View Feedback"
|
||||||
|
set desc = "Open the Feedback Viewer"
|
||||||
|
|
||||||
|
if(!check_rights(R_ADMIN))
|
||||||
|
return
|
||||||
|
|
||||||
|
if(usr.client.feedback_viewer)
|
||||||
|
usr.client.feedback_viewer.display()
|
||||||
|
else
|
||||||
|
usr.client.feedback_viewer = new(usr.client)
|
||||||
|
|
||||||
|
// This object holds the code to run the admin feedback viewer.
|
||||||
|
/datum/managed_browser/feedback_viewer
|
||||||
|
base_browser_id = "feedback_viewer"
|
||||||
|
title = "Submitted Feedback"
|
||||||
|
size_x = 900
|
||||||
|
size_y = 500
|
||||||
|
var/database/query/last_query = null
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_viewer/New(client/new_client)
|
||||||
|
if(!check_rights(R_ADMIN, new_client)) // Just in case someone figures out a way to spawn this as non-staff.
|
||||||
|
message_admins("[new_client] tried to view feedback with insufficent permissions.")
|
||||||
|
qdel(src)
|
||||||
|
|
||||||
|
..()
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_viewer/Destroy()
|
||||||
|
if(my_client)
|
||||||
|
my_client.feedback_viewer = null
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_viewer/proc/feedback_filter(row_name, thing_to_find, exact = FALSE)
|
||||||
|
var/database/query/query = null
|
||||||
|
if(exact) // Useful for ID searches, so searching for 'id 10' doesn't also get 'id 101'.
|
||||||
|
query = new({"
|
||||||
|
SELECT *
|
||||||
|
FROM [SQLITE_TABLE_FEEDBACK]
|
||||||
|
WHERE [row_name] == ?
|
||||||
|
ORDER BY [SQLITE_FEEDBACK_COLUMN_ID]
|
||||||
|
DESC LIMIT 50;
|
||||||
|
"},
|
||||||
|
thing_to_find
|
||||||
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
// Wrap the thing in %s so LIKE will work.
|
||||||
|
thing_to_find = "%[thing_to_find]%"
|
||||||
|
query = new({"
|
||||||
|
SELECT *
|
||||||
|
FROM [SQLITE_TABLE_FEEDBACK]
|
||||||
|
WHERE [row_name] LIKE ?
|
||||||
|
ORDER BY [SQLITE_FEEDBACK_COLUMN_ID]
|
||||||
|
DESC LIMIT 50;
|
||||||
|
"},
|
||||||
|
thing_to_find
|
||||||
|
)
|
||||||
|
query.Execute(SSsqlite.sqlite_db)
|
||||||
|
SSsqlite.sqlite_check_for_errors(query, "Admin Feedback Viewer - Filter by [row_name] to find [thing_to_find]")
|
||||||
|
return query
|
||||||
|
|
||||||
|
// Builds the window for players to review their feedback.
|
||||||
|
/datum/managed_browser/feedback_viewer/get_html()
|
||||||
|
var/list/dat = list("<html><body>")
|
||||||
|
if(!last_query) // If no query was done before, just show the most recent feedbacks.
|
||||||
|
var/database/query/query = new({"
|
||||||
|
SELECT *
|
||||||
|
FROM [SQLITE_TABLE_FEEDBACK]
|
||||||
|
ORDER BY [SQLITE_FEEDBACK_COLUMN_ID]
|
||||||
|
DESC LIMIT 50;
|
||||||
|
"}
|
||||||
|
)
|
||||||
|
query.Execute(SSsqlite.sqlite_db)
|
||||||
|
SSsqlite.sqlite_check_for_errors(query, "Admin Feedback Viewer")
|
||||||
|
last_query = query
|
||||||
|
|
||||||
|
dat += "<table border='1' style='width:100%'>"
|
||||||
|
dat += "<tr>"
|
||||||
|
dat += "<th>[href(src, list("filter_id" = 1), "ID")]</th>"
|
||||||
|
dat += "<th>[href(src, list("filter_topic" = 1), "Topic")]</th>"
|
||||||
|
dat += "<th>[href(src, list("filter_author" = 1), "Author")]</th>"
|
||||||
|
dat += "<th>[href(src, list("filter_content" = 1), "Content")]</th>"
|
||||||
|
dat += "<th>[href(src, list("filter_datetime" = 1), "Datetime")]</th>"
|
||||||
|
dat += "</tr>"
|
||||||
|
|
||||||
|
while(last_query.NextRow())
|
||||||
|
var/list/row_data = last_query.GetRowData()
|
||||||
|
dat += "<tr>"
|
||||||
|
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_ID]]</td>"
|
||||||
|
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_TOPIC]]</td>"
|
||||||
|
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR]]</td>" // TODO: Color this to make hashed keys more distinguishable.
|
||||||
|
var/text = row_data[SQLITE_FEEDBACK_COLUMN_CONTENT]
|
||||||
|
if(length(text) > 512)
|
||||||
|
text = href(src, list(
|
||||||
|
"show_full_feedback" = 1,
|
||||||
|
"feedback_author" = row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR],
|
||||||
|
"feedback_content" = row_data[SQLITE_FEEDBACK_COLUMN_CONTENT]
|
||||||
|
), "[copytext(text, 1, 64)]... ([length(text)])")
|
||||||
|
else
|
||||||
|
text = replacetext(text, "\n", "<br>")
|
||||||
|
dat += "<td>[text]</td>"
|
||||||
|
dat += "<td>[row_data[SQLITE_FEEDBACK_COLUMN_DATETIME]]</td>"
|
||||||
|
dat += "</tr>"
|
||||||
|
dat += "</table>"
|
||||||
|
|
||||||
|
dat += "</body></html>"
|
||||||
|
return dat.Join()
|
||||||
|
|
||||||
|
// Used to show the full version of feedback in a seperate window.
|
||||||
|
/datum/managed_browser/feedback_viewer/proc/display_big_feedback(author, text)
|
||||||
|
var/list/dat = list("<html><body>")
|
||||||
|
dat += replacetext(text, "\n", "<br>")
|
||||||
|
|
||||||
|
var/datum/browser/popup = new(my_client.mob, "feedback_big", "[author]'s Feedback", 480, 520, src)
|
||||||
|
popup.set_content(dat.Join())
|
||||||
|
popup.open()
|
||||||
|
|
||||||
|
|
||||||
|
/datum/managed_browser/feedback_viewer/Topic(href, href_list[])
|
||||||
|
if(!my_client)
|
||||||
|
return FALSE
|
||||||
|
|
||||||
|
if(href_list["close"]) // To avoid refreshing.
|
||||||
|
return
|
||||||
|
|
||||||
|
if(href_list["show_full_feedback"])
|
||||||
|
display_big_feedback(href_list["feedback_author"], href_list["feedback_content"])
|
||||||
|
return
|
||||||
|
|
||||||
|
if(href_list["filter_id"])
|
||||||
|
var/id_to_search = input(my_client, "Write feedback ID here.", "Filter by ID", null) as null|num
|
||||||
|
if(id_to_search)
|
||||||
|
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_ID, id_to_search, TRUE)
|
||||||
|
|
||||||
|
if(href_list["filter_author"])
|
||||||
|
var/author_to_search = input(my_client, "Write desired key or hash here. Partial keys/hashes are allowed.", "Filter by Author", null) as null|text
|
||||||
|
if(author_to_search)
|
||||||
|
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_AUTHOR, author_to_search)
|
||||||
|
|
||||||
|
if(href_list["filter_topic"])
|
||||||
|
var/topic_to_search = input(my_client, "Write desired topic here. Partial topics are allowed. \
|
||||||
|
\nThe current topics in the config are [english_list(config.sqlite_feedback_topics)].", "Filter by Topic", null) as null|text
|
||||||
|
if(topic_to_search)
|
||||||
|
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_TOPIC, topic_to_search)
|
||||||
|
|
||||||
|
if(href_list["filter_content"])
|
||||||
|
var/content_to_search = input(my_client, "Write desired content to find here. Partial matches are allowed.", "Filter by Content", null) as null|message
|
||||||
|
if(content_to_search)
|
||||||
|
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_CONTENT, content_to_search)
|
||||||
|
|
||||||
|
if(href_list["filter_datetime"])
|
||||||
|
var/datetime_to_search = input(my_client, "Write desired datetime. Partial matches are allowed.\n\
|
||||||
|
Format is 'YYYY-MM-DD HH:MM:SS'.", "Filter by Datetime", null) as null|text
|
||||||
|
if(datetime_to_search)
|
||||||
|
last_query = feedback_filter(SQLITE_FEEDBACK_COLUMN_DATETIME, datetime_to_search)
|
||||||
|
|
||||||
|
// Refresh.
|
||||||
|
display()
|
||||||
@@ -107,7 +107,8 @@ var/list/admin_verbs_admin = list(
|
|||||||
/client/proc/fixatmos,
|
/client/proc/fixatmos,
|
||||||
/datum/admins/proc/quick_nif, //VOREStation Add,
|
/datum/admins/proc/quick_nif, //VOREStation Add,
|
||||||
/datum/admins/proc/sendFax,
|
/datum/admins/proc/sendFax,
|
||||||
/client/proc/despawn_player
|
/client/proc/despawn_player,
|
||||||
|
/datum/admins/proc/view_feedback
|
||||||
)
|
)
|
||||||
|
|
||||||
var/list/admin_verbs_ban = list(
|
var/list/admin_verbs_ban = list(
|
||||||
|
|||||||
@@ -66,9 +66,11 @@
|
|||||||
else
|
else
|
||||||
output += "<p><a href='byond://?src=\ref[src];shownews=1'>Show News</A></p>"
|
output += "<p><a href='byond://?src=\ref[src];shownews=1'>Show News</A></p>"
|
||||||
|
|
||||||
|
if(SSsqlite.can_submit_feedback(client))
|
||||||
|
output += "<p>[href(src, list("give_feedback" = 1), "Give Feedback")]</p>"
|
||||||
output += "</div>"
|
output += "</div>"
|
||||||
|
|
||||||
panel = new(src, "Welcome","Welcome", 210, 280, src)
|
panel = new(src, "Welcome","Welcome", 210, 300, src)
|
||||||
panel.set_window_options("can_close=0")
|
panel.set_window_options("can_close=0")
|
||||||
panel.set_content(output)
|
panel.set_content(output)
|
||||||
panel.open()
|
panel.open()
|
||||||
@@ -170,7 +172,10 @@
|
|||||||
|
|
||||||
if(href_list["SelectedJob"])
|
if(href_list["SelectedJob"])
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
/* Vorestation Removal Start
|
/* Vorestation Removal Start
|
||||||
|
=======
|
||||||
|
>>>>>>> 97f8c1d... Merge pull request #6719 from Neerti/feedback_system
|
||||||
//Prevents people rejoining as same character.
|
//Prevents people rejoining as same character.
|
||||||
for (var/mob/living/carbon/human/C in mob_list)
|
for (var/mob/living/carbon/human/C in mob_list)
|
||||||
var/char_name = client.prefs.real_name
|
var/char_name = client.prefs.real_name
|
||||||
@@ -306,6 +311,15 @@
|
|||||||
show_hidden_jobs = !show_hidden_jobs
|
show_hidden_jobs = !show_hidden_jobs
|
||||||
LateChoices()
|
LateChoices()
|
||||||
|
|
||||||
|
if(href_list["give_feedback"])
|
||||||
|
if(!SSsqlite.can_submit_feedback(my_client))
|
||||||
|
return
|
||||||
|
|
||||||
|
if(client.feedback_form)
|
||||||
|
client.feedback_form.display() // In case they closed the form early.
|
||||||
|
else
|
||||||
|
client.feedback_form = new(client)
|
||||||
|
|
||||||
/mob/new_player/proc/handle_server_news()
|
/mob/new_player/proc/handle_server_news()
|
||||||
if(!client)
|
if(!client)
|
||||||
return
|
return
|
||||||
@@ -325,6 +339,7 @@
|
|||||||
popup.set_content(dat)
|
popup.set_content(dat)
|
||||||
popup.open()
|
popup.open()
|
||||||
|
|
||||||
|
|
||||||
/mob/new_player/proc/IsJobAvailable(rank)
|
/mob/new_player/proc/IsJobAvailable(rank)
|
||||||
var/datum/job/job = job_master.GetJob(rank)
|
var/datum/job/job = job_master.GetJob(rank)
|
||||||
if(!job) return 0
|
if(!job) return 0
|
||||||
|
|||||||
69
code/unit_tests/sqlite_tests.dm
Normal file
69
code/unit_tests/sqlite_tests.dm
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/datum/unit_test/sqlite
|
||||||
|
name = "SQLite template" // Template has to be in the name or this test will be ran, and fail.
|
||||||
|
var/database/stub_sqlite_db = null
|
||||||
|
|
||||||
|
/datum/unit_test/sqlite/proc/setup_stub_db()
|
||||||
|
fdel("data/sqlite/testing_[name].db") // In case any remain from a previous local test, so we can have a clean new database.
|
||||||
|
stub_sqlite_db = new("data/sqlite/testing_[name].db") // Unfortunately, byond doesn't like having working sqlite stuff w/o a file existing.
|
||||||
|
SSsqlite.init_schema(stub_sqlite_db)
|
||||||
|
|
||||||
|
// Feedback table tests.
|
||||||
|
/datum/unit_test/sqlite/feedback/insert
|
||||||
|
name = "SQLITE FEEDBACK: Insert and Retrieve Data"
|
||||||
|
|
||||||
|
/datum/unit_test/sqlite/feedback/insert/start_test()
|
||||||
|
// Arrange.
|
||||||
|
setup_stub_db()
|
||||||
|
var/test_author = "alice"
|
||||||
|
var/test_topic = "Test"
|
||||||
|
var/test_content = "Bob is lame."
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
SSsqlite.insert_feedback(author = test_author, topic = test_topic, content = test_content, sqlite_object = stub_sqlite_db)
|
||||||
|
var/database/query/Q = new("SELECT * FROM [SQLITE_TABLE_FEEDBACK]")
|
||||||
|
Q.Execute(stub_sqlite_db)
|
||||||
|
SSsqlite.sqlite_check_for_errors(Q, "Sqlite Insert Unit Test")
|
||||||
|
Q.NextRow()
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
var/list/row_data = Q.GetRowData()
|
||||||
|
if(row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR] == test_author && row_data[SQLITE_FEEDBACK_COLUMN_TOPIC] == test_topic && row_data[SQLITE_FEEDBACK_COLUMN_CONTENT] == test_content)
|
||||||
|
pass("No issues found.")
|
||||||
|
else
|
||||||
|
fail("Data insert and loading failed to have matching information.")
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
|
||||||
|
/datum/unit_test/sqlite/feedback/cooldown
|
||||||
|
name = "SQLITE FEEDBACK: Cooldown"
|
||||||
|
|
||||||
|
/datum/unit_test/sqlite/feedback/cooldown/start_test()
|
||||||
|
// Arrange.
|
||||||
|
setup_stub_db()
|
||||||
|
var/days_to_wait = 1
|
||||||
|
var/issues = 0
|
||||||
|
|
||||||
|
// Act.
|
||||||
|
SSsqlite.insert_feedback(author = "Alice", topic = "Testing", content = "This is a test.", sqlite_object = stub_sqlite_db)
|
||||||
|
|
||||||
|
var/alice_cooldown_block = SSsqlite.get_feedback_cooldown("Alice", days_to_wait, stub_sqlite_db)
|
||||||
|
var/bob_cooldown = SSsqlite.get_feedback_cooldown("Bob", days_to_wait, stub_sqlite_db)
|
||||||
|
days_to_wait = 0
|
||||||
|
var/alice_cooldown_allow = SSsqlite.get_feedback_cooldown("Alice", days_to_wait, stub_sqlite_db)
|
||||||
|
|
||||||
|
// Assert.
|
||||||
|
if(alice_cooldown_block <= 0)
|
||||||
|
issues++
|
||||||
|
log_unit_test("User 'Alice' did not receive a cooldown, when they were supposed to.")
|
||||||
|
if(bob_cooldown > 0)
|
||||||
|
issues++
|
||||||
|
log_unit_test("User 'Bob' did receive a cooldown, when they did not do anything.")
|
||||||
|
if(alice_cooldown_allow > 0)
|
||||||
|
issues++
|
||||||
|
log_unit_test("User 'Alice' did receive a cooldown, when no cooldown is supposed to be enforced.")
|
||||||
|
|
||||||
|
if(issues)
|
||||||
|
fail("[issues] issues were found.")
|
||||||
|
else
|
||||||
|
pass("No issues found.")
|
||||||
|
return TRUE
|
||||||
@@ -485,4 +485,41 @@ IPR_BLOCK_BAD_IPS
|
|||||||
IPR_ALLOW_EXISTING
|
IPR_ALLOW_EXISTING
|
||||||
|
|
||||||
# And what that age is (number)
|
# And what that age is (number)
|
||||||
IPR_MINIMUM_AGE 5
|
IPR_MINIMUM_AGE 5
|
||||||
|
|
||||||
|
|
||||||
|
## -SQLite Options-
|
||||||
|
## Uncomment to enable the use of SQLite. This does nothing by itself but other features that require SQLite will need this to be on.
|
||||||
|
## This can safely run alongside a MySQL/MariaDB database if they are powering seperate features.
|
||||||
|
# SQLITE_ENABLED
|
||||||
|
|
||||||
|
## Uncomment to enable a SQLite-powered in-game feedback system.
|
||||||
|
## SQLite must be enabled for this to function.
|
||||||
|
## It offers a means for players to be able to give feedback about the server.
|
||||||
|
## The benefit of doing so in-game is that the quality of feedback received will likely be superior, as it self-selects for people who care enough to join the game.
|
||||||
|
# SQLITE_FEEDBACK
|
||||||
|
|
||||||
|
## A list of 'topics' that can be used to categorize feedback submitted, chosen
|
||||||
|
## by the user. Have each topic seperated by a ';', as seen below.
|
||||||
|
## The first one in the list will be the default one used, if the user does not change it.
|
||||||
|
# SQLITE_FEEDBACK_TOPICS General; Suggestion; Complaint
|
||||||
|
|
||||||
|
## Uncomment to add a layer of privacy to player feedback, by hashing their key, if the user wants to.
|
||||||
|
## This is intended to encourage more honest feedback, while still allowing the ability to determine
|
||||||
|
## if its just one person submitting everything.
|
||||||
|
## A 'pepper.txt' containing a secret string must exist in the /config folder.
|
||||||
|
## If this is turned off, users won't have the option to obfuscate their key.
|
||||||
|
## Note that changing this does not retroactively change past submissions.
|
||||||
|
# SQLITE_FEEDBACK_PRIVACY
|
||||||
|
|
||||||
|
## Determines the 'cooldown' inbetween submissions, in days.
|
||||||
|
## This is recommended if privacy is active, to prevent spam floods.
|
||||||
|
## Less needed if feedback is not anonymous, since you can just ban spammers.
|
||||||
|
## Setting to zero means no rate limiting.
|
||||||
|
SQLITE_FEEDBACK_COOLDOWN 0
|
||||||
|
|
||||||
|
## Determines if feedback should be restricted based on how recently someone first joined.
|
||||||
|
## This is very unreliable due to how the age system works in general, but it might still be helpful.
|
||||||
|
## Set this to how many days you want someone to have to wait when they first join.
|
||||||
|
## Setting to zero will disable this restriction.
|
||||||
|
SQLITE_FEEDBACK_MIN_AGE 7
|
||||||
|
|||||||
11
config/example/sqlite_feedback_pepper.txt
Normal file
11
config/example/sqlite_feedback_pepper.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# This is used to alter the hash for in-game feedback authors.
|
||||||
|
# The reason for this is that doing this stops admins trying to
|
||||||
|
# guess who submitted a specific piece of feedback by feeding
|
||||||
|
# a list of ckeys into an MD5 hasher.
|
||||||
|
# In order for this to work, this file must contain a string, to be
|
||||||
|
# added onto the ckey before hashing, and ideally as few people as
|
||||||
|
# possible should know the string. It is also recommended to utilize
|
||||||
|
# file permissions (e.g. chown) to only allow the game to access
|
||||||
|
# this file, and forbid people with access to the server directly
|
||||||
|
# from just opening the file (even if they use sudo, it'll make a log).
|
||||||
|
# Make sure the string doesn't start with a #, or it'll be commented out.
|
||||||
36
html/changelogs/neerti-feedback_system.yml
Normal file
36
html/changelogs/neerti-feedback_system.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
################################
|
||||||
|
# Example Changelog File
|
||||||
|
#
|
||||||
|
# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
|
||||||
|
#
|
||||||
|
# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
|
||||||
|
# When it is, any changes listed below will disappear.
|
||||||
|
#
|
||||||
|
# Valid Prefixes:
|
||||||
|
# bugfix
|
||||||
|
# wip (For works in progress)
|
||||||
|
# tweak
|
||||||
|
# soundadd
|
||||||
|
# sounddel
|
||||||
|
# rscadd (general adding of nice things)
|
||||||
|
# rscdel (general deleting of nice things)
|
||||||
|
# imageadd
|
||||||
|
# imagedel
|
||||||
|
# maptweak
|
||||||
|
# spellcheck (typo fixes)
|
||||||
|
# experiment
|
||||||
|
#################################
|
||||||
|
|
||||||
|
# Your name.
|
||||||
|
author: Neerti
|
||||||
|
|
||||||
|
# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
|
||||||
|
delete-after: True
|
||||||
|
|
||||||
|
# Any changes you've made. See valid prefix list above.
|
||||||
|
# INDENT WITH TWO SPACES. NOT TABS. SPACES.
|
||||||
|
# SCREW THIS UP AND IT WON'T WORK.
|
||||||
|
# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
|
||||||
|
# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
|
||||||
|
changes:
|
||||||
|
- rscadd: "Adds an in-game feedback system that can be activated in the server configuration. Can be accessed from the lobby, with a button next to the other lobby buttons. Additional features and restrictions on usage are controlled by the server configuration."
|
||||||
@@ -27,7 +27,11 @@
|
|||||||
#include "code\__defines\_compile_options.dm"
|
#include "code\__defines\_compile_options.dm"
|
||||||
#include "code\__defines\_lists.dm"
|
#include "code\__defines\_lists.dm"
|
||||||
#include "code\__defines\_planes+layers.dm"
|
#include "code\__defines\_planes+layers.dm"
|
||||||
|
<<<<<<< HEAD:vorestation.dme
|
||||||
#include "code\__defines\_planes+layers_vr.dm"
|
#include "code\__defines\_planes+layers_vr.dm"
|
||||||
|
=======
|
||||||
|
#include "code\__defines\_protect.dm"
|
||||||
|
>>>>>>> 97f8c1d... Merge pull request #6719 from Neerti/feedback_system:polaris.dme
|
||||||
#include "code\__defines\_tick.dm"
|
#include "code\__defines\_tick.dm"
|
||||||
#include "code\__defines\admin.dm"
|
#include "code\__defines\admin.dm"
|
||||||
#include "code\__defines\admin_vr.dm"
|
#include "code\__defines\admin_vr.dm"
|
||||||
@@ -68,7 +72,11 @@
|
|||||||
#include "code\__defines\roguemining_vr.dm"
|
#include "code\__defines\roguemining_vr.dm"
|
||||||
#include "code\__defines\sound.dm"
|
#include "code\__defines\sound.dm"
|
||||||
#include "code\__defines\species_languages.dm"
|
#include "code\__defines\species_languages.dm"
|
||||||
|
<<<<<<< HEAD:vorestation.dme
|
||||||
#include "code\__defines\species_languages_vr.dm"
|
#include "code\__defines\species_languages_vr.dm"
|
||||||
|
=======
|
||||||
|
#include "code\__defines\sqlite_defines.dm"
|
||||||
|
>>>>>>> 97f8c1d... Merge pull request #6719 from Neerti/feedback_system:polaris.dme
|
||||||
#include "code\__defines\stat_tracking.dm"
|
#include "code\__defines\stat_tracking.dm"
|
||||||
#include "code\__defines\subsystems.dm"
|
#include "code\__defines\subsystems.dm"
|
||||||
#include "code\__defines\subsystems_vr.dm"
|
#include "code\__defines\subsystems_vr.dm"
|
||||||
@@ -246,6 +254,7 @@
|
|||||||
#include "code\controllers\subsystems\planets.dm"
|
#include "code\controllers\subsystems\planets.dm"
|
||||||
#include "code\controllers\subsystems\radiation.dm"
|
#include "code\controllers\subsystems\radiation.dm"
|
||||||
#include "code\controllers\subsystems\shuttles.dm"
|
#include "code\controllers\subsystems\shuttles.dm"
|
||||||
|
#include "code\controllers\subsystems\sqlite.dm"
|
||||||
#include "code\controllers\subsystems\sun.dm"
|
#include "code\controllers\subsystems\sun.dm"
|
||||||
#include "code\controllers\subsystems\time_track.dm"
|
#include "code\controllers\subsystems\time_track.dm"
|
||||||
#include "code\controllers\subsystems\timer.dm"
|
#include "code\controllers\subsystems\timer.dm"
|
||||||
@@ -320,6 +329,9 @@
|
|||||||
#include "code\datums\looping_sounds\machinery_sounds.dm"
|
#include "code\datums\looping_sounds\machinery_sounds.dm"
|
||||||
#include "code\datums\looping_sounds\sequence.dm"
|
#include "code\datums\looping_sounds\sequence.dm"
|
||||||
#include "code\datums\looping_sounds\weather_sounds.dm"
|
#include "code\datums\looping_sounds\weather_sounds.dm"
|
||||||
|
#include "code\datums\managed_browsers\_managed_browser.dm"
|
||||||
|
#include "code\datums\managed_browsers\feedback_form.dm"
|
||||||
|
#include "code\datums\managed_browsers\feedback_viewer.dm"
|
||||||
#include "code\datums\observation\_debug.dm"
|
#include "code\datums\observation\_debug.dm"
|
||||||
#include "code\datums\observation\_defines.dm"
|
#include "code\datums\observation\_defines.dm"
|
||||||
#include "code\datums\observation\destroyed.dm"
|
#include "code\datums\observation\destroyed.dm"
|
||||||
@@ -3373,6 +3385,7 @@
|
|||||||
#include "code\unit_tests\map_tests.dm"
|
#include "code\unit_tests\map_tests.dm"
|
||||||
#include "code\unit_tests\mob_tests.dm"
|
#include "code\unit_tests\mob_tests.dm"
|
||||||
#include "code\unit_tests\research_tests.dm"
|
#include "code\unit_tests\research_tests.dm"
|
||||||
|
#include "code\unit_tests\sqlite_tests.dm"
|
||||||
#include "code\unit_tests\unit_test.dm"
|
#include "code\unit_tests\unit_test.dm"
|
||||||
#include "code\unit_tests\unit_test_vr.dm"
|
#include "code\unit_tests\unit_test_vr.dm"
|
||||||
#include "code\unit_tests\vore_tests_vr.dm"
|
#include "code\unit_tests\vore_tests_vr.dm"
|
||||||
|
|||||||
Reference in New Issue
Block a user