////////////
//SECURITY//
////////////
//debugging, uncomment for viewing topic calls
//#define TOPIC_DEBUGGING 1
#define TOPIC_SPAM_DELAY 2 //2 ticks is about 2/10ths of a second; it was 4 ticks, but that caused too many clicks to be lost due to lag
#define UPLOAD_LIMIT 10485760 //Restricts client uploads to the server to 10MB //Boosted this thing. What's the worst that can happen?
#define MIN_CLIENT_VERSION 0 //Just an ambiguously low version for now, I don't want to suddenly stop people playing.
//I would just like the code ready should it ever need to be used.
#define SUGGESTED_CLIENT_VERSION 511 // only integers (e.g: 510, 511) useful here. Does not properly handle minor versions (e.g: 510.58, 511.848)
#define SSD_WARNING_TIMER 30 // cycles, not seconds, so 30=60s
#define LIMITER_SIZE 5
#define CURRENT_SECOND 1
#define SECOND_COUNT 2
#define CURRENT_MINUTE 3
#define MINUTE_COUNT 4
#define ADMINSWARNED_AT 5
/*
When somebody clicks a link in game, this Topic is called first.
It does the stuff in this proc and then is redirected to the Topic() proc for the src=[0xWhatever]
(if specified in the link). ie locate(hsrc).Topic()
Such links can be spoofed.
Because of this certain things MUST be considered whenever adding a Topic() for something:
- Can it be fed harmful values which could cause runtimes?
- Is the Topic call an admin-only thing?
- If so, does it have checks to see if the person who called it (usr.client) is an admin?
- Are the processes being called by Topic() particularly laggy?
- If so, is there any protection against somebody spam-clicking a link?
If you have any questions about this stuff feel free to ask. ~Carn
*/
/client/Topic(href, href_list, hsrc)
if(!usr || usr != mob) //stops us calling Topic for somebody else's client. Also helps prevent usr=null
return
// src should always be a UID; if it isn't, warn instead of failing entirely
if(href_list["src"])
hsrc = locateUID(href_list["src"])
// If there's a ]_ in the src, it's a UID, so don't try to locate it
if(!hsrc && !findtext(href_list["src"], "]_"))
hsrc = locate(href_list["src"])
if(hsrc)
var/hsrc_info = datum_info_line(hsrc) || "[hsrc]"
log_runtime(EXCEPTION("Got \\ref-based src in topic from [src] for [hsrc_info], should be UID: [href]"))
#if defined(TOPIC_DEBUGGING)
to_chat(world, "[src]'s Topic: [href] destined for [hsrc].")
#endif
if(href_list["nano_err"]) //nano throwing errors
if(topic_debugging)
to_chat(src, "## NanoUI: " + html_decode(href_list["nano_err"]))//NANO DEBUG HOOK
if(href_list["asset_cache_confirm_arrival"])
// to_chat(src, "ASSET JOB [href_list["asset_cache_confirm_arrival"]] ARRIVED.")
var/job = text2num(href_list["asset_cache_confirm_arrival"])
completed_asset_jobs += job
return
if(href_list["_src_"] == "chat")
return chatOutput.Topic(href, href_list)
// Rate limiting
var/mtl = 100 // 100 topics per minute
if (!holder) // Admins are allowed to spam click, deal with it.
var/minute = round(world.time, 600)
if (!topiclimiter)
topiclimiter = new(LIMITER_SIZE)
if (minute != topiclimiter[CURRENT_MINUTE])
topiclimiter[CURRENT_MINUTE] = minute
topiclimiter[MINUTE_COUNT] = 0
topiclimiter[MINUTE_COUNT] += 1
if (topiclimiter[MINUTE_COUNT] > mtl)
var/msg = "Your previous action was ignored because you've done too many in a minute."
if (minute != topiclimiter[ADMINSWARNED_AT]) //only one admin message per-minute. (if they spam the admins can just boot/ban them)
topiclimiter[ADMINSWARNED_AT] = minute
msg += " Administrators have been informed."
log_game("[key_name(src)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
message_admins("[ADMIN_LOOKUPFLW(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
to_chat(src, "[msg]")
return
var/stl = 10 // 10 topics a second
if (!holder) // Admins are allowed to spam click, deal with it.
var/second = round(world.time, 10)
if (!topiclimiter)
topiclimiter = new(LIMITER_SIZE)
if (second != topiclimiter[CURRENT_SECOND])
topiclimiter[CURRENT_SECOND] = second
topiclimiter[SECOND_COUNT] = 0
topiclimiter[SECOND_COUNT] += 1
if (topiclimiter[SECOND_COUNT] > stl)
to_chat(src, "Your previous action was ignored because you've done too many in a second")
return
//search the href for script injection
if( findtext(href,"",
"border=0;titlebar=0;size=1x1")
to_chat(src, "You will be automatically taken to the game, if not, click here to be taken manually. Except you can't, since the chat window doesn't exist yet.")
//checks if a client is afk
//3000 frames = 5 minutes
/client/proc/is_afk(duration=3000)
if(inactivity > duration) return inactivity
return 0
//Send resources to the client.
/client/proc/send_resources()
// Change the way they should download resources.
if(config.resource_urls)
preload_rsc = pick(config.resource_urls)
else
preload_rsc = 1 // If config.resource_urls is not set, preload like normal.
// Most assets are now handled through global_cache.dm
getFiles(
'html/search.js', // Used in various non-NanoUI HTML windows for search functionality
'html/panels.css' // Used for styling certain panels, such as in the new player panel
)
spawn (10) //removing this spawn causes all clients to not get verbs.
//Precache the client with all other assets slowly, so as to not block other browse() calls
getFilesSlow(src, SSassets.preload, register_asset = FALSE)
//For debugging purposes
/client/proc/list_all_languages()
for(var/L in GLOB.all_languages)
var/datum/language/lang = GLOB.all_languages[L]
var/message = "[lang.name] : [lang.type]"
if(lang.flags & RESTRICTED)
message += " (RESTRICTED)"
to_chat(world, "[message]")
/client/proc/colour_transition(list/colour_to = null, time = 10) //Call this with no parameters to reset to default.
animate(src, color = colour_to, time = time, easing = SINE_EASING)
/client/proc/on_varedit()
var_edited = TRUE
/////////////////
// DARKMODE UI //
/////////////////
// IF YOU CHANGE ANYTHING IN ACTIVATE, MAKE SURE IT HAS A DEACTIVATE METHOD, -AA07
/client/proc/activate_darkmode()
///// BUTTONS /////
SSchangelog.UpdatePlayerChangelogButton(src)
/* Rpane */
winset(src, "rpane.textb", "background-color=#40628a;text-color=#FFFFFF")
winset(src, "rpane.infob", "background-color=#40628a;text-color=#FFFFFF")
winset(src, "rpane.wikib", "background-color=#40628a;text-color=#FFFFFF")
winset(src, "rpane.forumb", "background-color=#40628a;text-color=#FFFFFF")
winset(src, "rpane.rulesb", "background-color=#40628a;text-color=#FFFFFF")
winset(src, "rpane.githubb", "background-color=#40628a;text-color=#FFFFFF")
/* Mainwindow */
winset(src, "mainwindow.saybutton", "background-color=#40628a;text-color=#FFFFFF")
winset(src, "mainwindow.mebutton", "background-color=#40628a;text-color=#FFFFFF")
///// UI ELEMENTS /////
/* Mainwindow */
winset(src, "mainwindow", "background-color=#272727")
winset(src, "mainwindow.mainvsplit", "background-color=#272727")
winset(src, "mainwindow.tooltip", "background-color=#272727")
/* Outputwindow */
winset(src, "outputwindow.browseroutput", "background-color=#272727")
/* Rpane */
winset(src, "rpane", "background-color=#272727")
winset(src, "rpane.rpanewindow", "background-color=#272727")
/* Browserwindow */
winset(src, "browserwindow", "background-color=#272727")
winset(src, "browserwindow.browser", "background-color=#272727")
/* Infowindow */
winset(src, "infowindow", "background-color=#272727;text-color=#FFFFFF")
winset(src, "infowindow.info", "background-color=#272727;text-color=#FFFFFF;highlight-color=#009900;tab-text-color=#FFFFFF;tab-background-color=#272727")
// NOTIFY USER
to_chat(src, "Darkmode Enabled")
/client/proc/deactivate_darkmode()
///// BUTTONS /////
SSchangelog.UpdatePlayerChangelogButton(src)
/* Rpane */
winset(src, "rpane.textb", "background-color=none;text-color=#000000")
winset(src, "rpane.infob", "background-color=none;text-color=#000000")
winset(src, "rpane.wikib", "background-color=none;text-color=#000000")
winset(src, "rpane.forumb", "background-color=none;text-color=#000000")
winset(src, "rpane.rulesb", "background-color=none;text-color=#000000")
winset(src, "rpane.githubb", "background-color=none;text-color=#000000")
/* Mainwindow */
winset(src, "mainwindow.saybutton", "background-color=none;text-color=#000000")
winset(src, "mainwindow.mebutton", "background-color=none;text-color=#000000")
///// UI ELEMENTS /////
/* Mainwindow */
winset(src, "mainwindow", "background-color=none")
winset(src, "mainwindow.mainvsplit", "background-color=none")
winset(src, "mainwindow.tooltip", "background-color=none")
/* Outputwindow */
winset(src, "outputwindow.browseroutput", "background-color=none")
/* Rpane */
winset(src, "rpane", "background-color=none")
winset(src, "rpane.rpanewindow", "background-color=none")
/* Browserwindow */
winset(src, "browserwindow", "background-color=none")
winset(src, "browserwindow.browser", "background-color=none")
/* Infowindow */
winset(src, "infowindow", "background-color=none;text-color=#000000")
winset(src, "infowindow.info", "background-color=none;text-color=#000000;highlight-color=#007700;tab-text-color=#000000;tab-background-color=none")
///// NOTIFY USER /////
to_chat(src, "Darkmode Disabled") // what a sick fuck
/client/proc/generate_clickcatcher()
if(!void)
void = new()
screen += void
/client/proc/apply_clickcatcher()
generate_clickcatcher()
var/list/actualview = getviewsize(view)
void.UpdateGreed(actualview[1],actualview[2])
/client/proc/send_ssd_warning(mob/M)
if(!config.ssd_warning)
return FALSE
if(ssd_warning_acknowledged)
return FALSE
if(M && M.player_logged < SSD_WARNING_TIMER)
return FALSE
to_chat(src, "Are you taking this person to cryo or giving them medical treatment? If you are, confirm that and proceed. Interacting with SSD players in other ways is against server rules unless you've ahelped first for permission.")
return TRUE
#undef SSD_WARNING_TIMER
/client/verb/resend_ui_resources()
set name = "Reload UI Resources"
set desc = "Reload your UI assets if they are not working"
set category = "Special Verbs"
if(last_ui_resource_send > world.time)
to_chat(usr, "You requested your UI resource files too quickly. Please try again in [(last_ui_resource_send - world.time)/10] seconds.")
return
var/choice = alert(usr, "This will reload your NanoUI and TGUI resources. If you have any open UIs this may break them. Are you sure?", "Resource Reloading", "Yes", "No")
if(choice == "Yes")
// 600 deciseconds = 1 minute
last_ui_resource_send = world.time + 60 SECONDS
// Close their open UIs
SSnanoui.close_user_uis(usr)
SStgui.close_user_uis(usr)
// Resend the resources
var/datum/asset/nano_assets = get_asset_datum(/datum/asset/nanoui)
nano_assets.register()
var/datum/asset/tgui_assets = get_asset_datum(/datum/asset/simple/tgui)
tgui_assets.register()
var/datum/asset/nanomaps = get_asset_datum(/datum/asset/simple/nanomaps)
nanomaps.register()
// Clear the user's cache so they get resent.
// This is not fully clearing their BYOND cache, just their assets sent from the server this round
cache = list()
to_chat(usr, "UI resource files resent successfully. If you are still having issues, please try manually clearing your BYOND cache. This can be achieved by opening your BYOND launcher, pressing the cog in the top right, selecting preferences, going to the Games tab, and pressing 'Clear Cache'.")
/**
* Retrieves the BYOND accounts data from the BYOND servers
*
* Makes a web request to byond.com to retrieve the details for the BYOND account associated with the clients ckey.
* Returns the data in a parsed, associative list
*/
/client/proc/retrieve_byondacc_data()
var/list/http[] = world.Export("http://www.byond.com/members/[ckey]?format=text")
if(http)
var/status = text2num(http["STATUS"])
if(status == 200)
// This is wrapped in try/catch because lummox could change the format on any day without informing anyone
try
var/list/lines = splittext(file2text(http["CONTENT"]), "\n")
var/list/initial_data = list()
var/current_index = ""
for(var/L in lines)
if(L == "")
continue
if(!findtext(L, "\t"))
current_index = L
initial_data[current_index] = list()
continue
initial_data[current_index] += replacetext(replacetext(L, "\t", ""), "\"", "")
var/list/parsed_data = list()
for(var/key in initial_data)
var/inner_list = list()
for(var/entry in initial_data[key])
var/list/split = splittext(entry, " = ")
var/inner_key = split[1]
var/inner_value = split[2]
inner_list[inner_key] = inner_value
parsed_data[key] = inner_list
// Main return is here
return parsed_data
catch
message_admins("Error parsing byond.com data for [ckey]. Please inform maintainers.")
return null
else
message_admins("Error retrieving data from byond.com for [ckey]. Invalid status code (Expected: 200 | Got: [status]).")
return null
else
message_admins("Failed to retrieve data from byond.com for [ckey]. Connection failed.")
return null
/**
* Sets the clients BYOND date up properly
*
* If the client does not have a saved BYOND account creation date, retrieve it from the website
* If they do have a saved date, use that from the DB, because this value will never change
* Arguments:
* * notify - Do we notify admins of this new accounts date
*/
/client/proc/get_byond_account_date(notify = FALSE)
// First we see if the client has a saved date in the DB
var/sql_ckey = sanitizeSQL(ckey)
var/DBQuery/query_date = GLOB.dbcon.NewQuery("SELECT byond_date, DATEDIFF(Now(), byond_date) FROM [format_table_name("player")] WHERE ckey = '[sql_ckey]'")
if(!query_date.Execute())
var/err = query_date.ErrorMsg()
log_game("SQL ERROR during get_byond_account_date (Line 1047). Error: \[[err]\]\n")
message_admins("SQL ERROR during get_byond_account_date (Line 1047). Error: \[[err]\]\n")
while(query_date.NextRow())
byondacc_date = query_date.item[1]
byondacc_age = max(text2num(query_date.item[2]), 0) // Ensure account isnt negative days old
// They have a date, lets bail
if(byondacc_date)
return
// They dont have a date, lets grab one
var/list/byond_data = retrieve_byondacc_data()
if(isnull(byond_data) || !(byond_data["general"]["joined"]))
message_admins("Failed to retrieve an account creation date for [ckey].")
return
byondacc_date = byond_data["general"]["joined"]
// Now save it
var/sql_date = sanitizeSQL(byondacc_date) // Yes, this is top level paranoia
var/DBQuery/query_update = GLOB.dbcon.NewQuery("UPDATE [format_table_name("player")] SET byond_date = '[sql_date]' WHERE ckey = '[sql_ckey]'")
if(!query_update.Execute())
var/err = query_update.ErrorMsg()
log_game("SQL ERROR during get_byond_account_date (Line 1071). Error: \[[err]\]\n")
message_admins("SQL ERROR during get_byond_account_date (Line 1071). Error: \[[err]\]\n")
// Now retrieve the age again because BYOND doesnt have native methods for this
var/DBQuery/query_age = GLOB.dbcon.NewQuery("SELECT DATEDIFF(Now(), byond_date) FROM [format_table_name("player")] WHERE ckey = '[sql_ckey]'")
if(!query_age.Execute())
var/err = query_age.ErrorMsg()
log_game("SQL ERROR during get_byond_account_date (Line 1078). Error: \[[err]\]\n")
message_admins("SQL ERROR during get_byond_account_date (Line 1078). Error: \[[err]\]\n")
while(query_age.NextRow())
byondacc_age = max(text2num(query_age.item[1]), 0) // Ensure account isnt negative days old
// Notify admins on new clients connecting, if the byond account age is less than a config value
if(notify && (byondacc_age < config.byond_account_age_threshold))
message_admins("[key] has just connected for the first time. BYOND account registered on [byondacc_date] ([byondacc_age] days old)")
#undef LIMITER_SIZE
#undef CURRENT_SECOND
#undef SECOND_COUNT
#undef CURRENT_MINUTE
#undef MINUTE_COUNT
#undef ADMINSWARNED_AT