////////////
//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 513 // Minimum byond major version required to play.
//I would just like the code ready should it ever need to be used.
#define SUGGESTED_CLIENT_VERSION 514 // only integers (e.g: 513, 514) are useful here. This is the part BEFORE the ".", IE 513 out of 513.1542
#define SUGGESTED_CLIENT_BUILD 1566 // only integers (e.g: 1542, 1543) are useful here. This is the part AFTER the ".", IE 1542 out of 513.1542
#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["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(length(GLOB.configuration.url.rsc_urls))
preload_rsc = pick(GLOB.configuration.url.rsc_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-TGUI 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")
winset(src, "rpane.webmap", "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")
winset(src, "rpane.webmap", "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(!GLOB.configuration.general.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 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
SStgui.close_user_uis(usr)
// Resend the resources
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()
// Do not refactor this to use SShttp, because that requires the subsystem to be firing for requests to be made, and this will be triggered before the MC has finished loading
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
log_debug("Error parsing byond.com data for [ckey]. Please inform maintainers.")
return null
else
log_debug("Error retrieving data from byond.com for [ckey]. Invalid status code (Expected: 200 | Got: [status]).")
return null
else
log_debug("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/datum/db_query/query_date = SSdbcore.NewQuery("SELECT byond_date, DATEDIFF(Now(), byond_date) FROM player WHERE ckey=:ckey", list(
"ckey" = ckey
))
if(!query_date.warn_execute())
qdel(query_date)
return
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
qdel(query_date)
// They have a date already, 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"]))
log_debug("Failed to retrieve an account creation date for [ckey].")
return
byondacc_date = byond_data["general"]["joined"]
// Now save it
var/datum/db_query/query_update = SSdbcore.NewQuery("UPDATE player SET byond_date=:date WHERE ckey=:ckey", list(
"date" = byondacc_date,
"ckey" = ckey
))
if(!query_update.warn_execute())
qdel(query_update)
return
qdel(query_update)
// Now retrieve the age again because BYOND doesnt have native methods for this
var/datum/db_query/query_age = SSdbcore.NewQuery("SELECT DATEDIFF(Now(), byond_date) FROM player WHERE ckey=:ckey", list(
"ckey" = ckey
))
if(!query_age.warn_execute())
qdel(query_age)
return
while(query_age.NextRow())
byondacc_age = max(text2num(query_age.item[1]), 0) // Ensure account isnt negative days old
qdel(query_age)
// Notify admins on new clients connecting, if the byond account age is less than a config value
if(notify && (byondacc_age < GLOB.configuration.general.byond_account_age_threshold))
message_admins("[key] has just connected for the first time. BYOND account registered on [byondacc_date] ([byondacc_age] days old)")
/client/proc/show_update_notice()
to_chat(src, "Your BYOND client (v: [byond_version].[byond_build]) is out of date. This can cause glitches. We highly suggest you download the latest client from byond.com before playing. You can also update via the BYOND launcher application.")
/client/proc/update_ambience_pref()
if(prefs.sound & SOUND_AMBIENCE)
if(SSambience.ambience_listening_clients[src] > world.time)
return // If already properly set we don't want to reset the timer.
SSambience.ambience_listening_clients[src] = world.time + 10 SECONDS //Just wait 10 seconds before the next one aight mate? cheers.
else
SSambience.ambience_listening_clients -= src
#undef LIMITER_SIZE
#undef CURRENT_SECOND
#undef SECOND_COUNT
#undef CURRENT_MINUTE
#undef MINUTE_COUNT
#undef ADMINSWARNED_AT