Files
Yogstation/code/modules/client/client_procs.dm
tattax 169d32074e fixes some amtag bugs (#19032)
* new

* 2

* e²

* e³

* e£

* glang

* e¢

* e¬

* e

* e12
2023-05-28 23:11:26 -05:00

1093 lines
42 KiB
Plaintext

////////////
//SECURITY//
////////////
#define UPLOAD_LIMIT 524288 //Restricts client uploads to the server to 0.5MB
#define UPLOAD_LIMIT_ADMIN 3145728 //Restricts admin client uploads to the server to 3MB
GLOBAL_LIST_INIT(blacklisted_builds, list(
"1407" = "bug preventing client display overrides from working leads to clients being able to see things/mobs they shouldn't be able to see",
"1408" = "bug preventing client display overrides from working leads to clients being able to see things/mobs they shouldn't be able to see",
"1428" = "bug causing right-click menus to show too many verbs that's been fixed in version 1429",
))
#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
if(src.prefs && src.afreeze && !href_list["priv_msg"] && href_list["_src_"] != "chat" && !src.holder) //yogs start - afreeze
to_chat(src, span_userdanger("You have been frozen by an administrator."))
return //yogs end
// asset_cache
var/asset_cache_job
if(href_list["asset_cache_confirm_arrival"])
asset_cache_job = asset_cache_confirm_arrival(href_list["asset_cache_confirm_arrival"])
if (!asset_cache_job)
return
if(href_list["__keydown"])
var/keydown = js_keycode_to_byond(href_list["__keydown"])
if(href_list["ctrlKey"] == "0" && keydown != "Ctrl")
keyUp("Ctrl")
if(href_list["ctrlKey"] == "1" && keydown != "Ctrl")
keyDown("Ctrl")
if(keydown)
keyDown(keydown)
return
if(href_list["__keyup"])
var/keyup = js_keycode_to_byond(href_list["__keyup"])
if(href_list["ctrlKey"] == "0" && keyup != "Ctrl")
keyUp("Ctrl")
if(href_list["ctrlKey"] == "1" && keyup != "Ctrl")
keyDown("Ctrl")
if(keyup)
keyUp(keyup)
return
//Rate limiting
var/mtl = CONFIG_GET(number/minute_topic_limit)
if (!holder && mtl)
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(src)] [ADMIN_KICK(usr)] Has hit the per-minute topic limit of [mtl] topic calls in a given game minute")
to_chat(src, span_danger("[msg]"))
return
var/stl = CONFIG_GET(number/second_topic_limit)
if (!holder && stl)
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, span_danger("Your previous action was ignored because you've done too many in a second"))
return
// Tgui Topic middleware
if(tgui_Topic(href_list))
return
if(href_list["reload_tguipanel"])
nuke_chat()
if(href_list["reload_statbrowser"])
src << browse(file('html/statbrowser.html'), "window=statbrowser")
// Log all hrefs
log_href("[src] (usr:[usr]\[[COORD(usr)]\]) : [hsrc ? "[hsrc] " : ""][href]")
//byond bug ID:2256651
if (asset_cache_job && (asset_cache_job in completed_asset_jobs))
to_chat(src, span_danger("An error has been detected in how your client is receiving resources. Attempting to correct.... (If you keep seeing these messages you might want to close byond and reconnect)"))
src << browse("...", "window=asset_cache_browser")
return
if (href_list["asset_cache_preload_data"])
asset_cache_preload_data(href_list["asset_cache_preload_data"])
return
// Admin PM
if(href_list["priv_msg"])
cmd_admin_pm(href_list["priv_msg"],null)
return
// YOGS START - Mentor PMs
if(yogs_client_procs(href_list))
return
// YOGS END
switch(href_list["_src_"])
if("holder")
hsrc = holder
if("usr")
hsrc = mob
if("mentor") // YOGS - Mentor stuff
hsrc = mentor_datum // YOGS - Mentor stuff
if("vars")
return view_var_Topic(href,href_list,hsrc)
switch(href_list["action"])
if("openLink")
src << link(href_list["link"])
//YOGS START: adds "refresh_admin_ticket_list" from another file.
if("refresh_admin_ticket_list")
var/client/C = usr.client
var/flag = href_list["flag"]
if(!flag)
flag = TICKET_FLAG_LIST_ALL
C.view_tickets_main(flag)
//YOGS END
if (hsrc)
var/datum/real_src = hsrc
if(QDELETED(real_src))
return
..() //redirect to hsrc.Topic()
/client/proc/handle_spam_prevention(message, mute_type)
//Increment message count
total_message_count += 1
//store the total to act on even after a reset
var/cache = total_message_count
if(total_count_reset <= world.time)
total_message_count = 0
total_count_reset = world.time + (5 SECONDS)
//If they're really going crazy, mute them
if(cache >= SPAM_TRIGGER_AUTOMUTE * 2)
total_message_count = 0
total_count_reset = 0
cmd_admin_mute(src, mute_type, 1)
return 1
//Otherwise just supress the message
else if(cache >= SPAM_TRIGGER_AUTOMUTE)
return 1
if(CONFIG_GET(flag/automute_on) && !holder && last_message == message)
src.last_message_count++
if(src.last_message_count >= SPAM_TRIGGER_AUTOMUTE)
to_chat(src, span_danger("You have exceeded the spam filter limit for identical messages. An auto-mute was applied."))
cmd_admin_mute(src, mute_type, 1)
return 1
if(src.last_message_count >= SPAM_TRIGGER_WARNING)
to_chat(src, span_danger("You are nearing the spam filter limit for identical messages."))
return 0
else
last_message = message
src.last_message_count = 0
return 0
//This stops files larger than UPLOAD_LIMIT being sent from client to server via input(), client.Import() etc.
/client/AllowUpload(filename, filelength)
if (holder)
if(filelength > UPLOAD_LIMIT_ADMIN)
to_chat(src, "<font color='red'>Error: AllowUpload(): File Upload too large. Upload Limit: [UPLOAD_LIMIT_ADMIN/1024]KiB.</font>")
return FALSE
else if(filelength > UPLOAD_LIMIT)
to_chat(src, "<font color='red'>Error: AllowUpload(): File Upload too large. Upload Limit: [UPLOAD_LIMIT/1024]KiB.</font>")
return 0
return 1
///////////
//CONNECT//
///////////
/client/Destroy()
SHOULD_CALL_PARENT(FALSE)
return QDEL_HINT_HARDDEL_NOW
/client/New(TopicData)
var/tdata = TopicData //save this for later use
//this is a scam, so sometimes the topicdata is set to /?key=value instead of key=value, this is a hack around that
if(copytext(tdata, 1, 3) == "/?")
tdata = copytext(tdata, 3)
TopicData = null //Prevent calls to client.Topic from connect
if(connection != "seeker" && connection != "web")//Invalid connection type.
return null
GLOB.clients += src
GLOB.directory[ckey] = src
// Instantiate tgui panel
tgui_panel = new(src)
//tgui_panel.send_connected()
GLOB.ahelp_tickets.ClientLogin(src)
var/connecting_admin = GLOB.permissions.load_permissions_for(src) //because de-admined admins connecting should be treated like admins.
if(connecting_admin && !holder)
stack_trace("[ckey] is an admin but has no holder")
// yogs start - mentor stuff
if(ckey in GLOB.mentor_datums)
var/datum/mentors/mentor = GLOB.mentor_datums[ckey]
src.mentor_datum = mentor
src.add_mentor_verbs()
if(!check_rights_for(src, R_ADMIN,0)) // don't add admins to mentor list.
GLOB.mentors |= src
// yogs end
//preferences datum - also holds some persistent data for the client (because we may as well keep these datums to a minimum)
prefs = GLOB.preferences_datums[ckey]
if(prefs)
prefs.parent = src
prefs.apply_all_client_preferences()
else
prefs = new /datum/preferences(src)
GLOB.preferences_datums[ckey] = prefs
prefs.last_ip = address //these are gonna be used for banning
prefs.last_id = computer_id //these are gonna be used for banning
if(fexists(roundend_report_file()))
add_verb(src, /client/proc/show_previous_roundend_report)
var/full_version = "[byond_version].[byond_build ? byond_build : "xxx"]"
log_access("Login: [key_name(src)] from [address ? address : "localhost"]-[computer_id] || BYOND v[full_version]")
var/alert_mob_dupe_login = FALSE
if(CONFIG_GET(flag/log_access))
for(var/I in GLOB.clients)
if(!I || I == src)
continue
var/client/C = I
if(C.key && (C.key != key) )
var/matches
if( (C.address == address) )
matches += "IP ([address])"
if( (C.computer_id == computer_id) )
if(matches)
matches += " and "
matches += "ID ([computer_id])"
alert_mob_dupe_login = TRUE
if(matches)
if(C)
message_admins("<span class='danger'><B>Notice: </B></span>[span_notice("[key_name_admin(src)] has the same [matches] as [key_name_admin(C)].")]")
log_access("Notice: [key_name(src)] has the same [matches] as [key_name(C)].")
else
message_admins("<span class='danger'><B>Notice: </B></span>[span_notice("[key_name_admin(src)] has the same [matches] as [key_name_admin(C)] (no longer logged in). ")]")
log_access("Notice: [key_name(src)] has the same [matches] as [key_name(C)] (no longer logged in).")
if(GLOB.player_details[ckey])
player_details = GLOB.player_details[ckey]
player_details.byond_version = full_version
else
player_details = new
player_details.byond_version = full_version
GLOB.player_details[ckey] = player_details
if (prefs.unlock_content & DONOR_YOGS)
src.add_donator_verbs()
else
if(prefs.yogtoggles & QUIET_ROUND)
prefs.yogtoggles &= ~QUIET_ROUND
prefs.save_preferences()
. = ..() //calls mob.Login()
if (byond_version >= 512)
if (!byond_build || byond_build < 1386)
message_admins(span_adminnotice("[key_name(src)] has been detected as spoofing their byond version. Connection rejected."))
add_system_note("Spoofed-Byond-Version", "Detected as using a spoofed byond version.")
log_access("Failed Login: [key] - Spoofed byond version")
qdel(src)
if (num2text(byond_build) in GLOB.blacklisted_builds)
log_access("Failed login: [key] - blacklisted byond version")
to_chat(src, span_userdanger("Your version of byond is blacklisted."))
to_chat(src, span_danger("Byond build [byond_build] ([byond_version].[byond_build]) has been blacklisted for the following reason: [GLOB.blacklisted_builds[num2text(byond_build)]]."))
to_chat(src, span_danger("Please download a new version of byond. If [byond_build] is the latest, you can go to <a href=\"https://secure.byond.com/download/build\">BYOND's website</a> to download other versions."))
if(connecting_admin)
to_chat(src, "As an admin, you are being allowed to continue using this version, but please consider changing byond versions")
else
qdel(src)
return
if(SSinput.initialized)
set_macros()
src << browse(file('html/statbrowser.html'), "window=statbrowser")
// Initialize tgui panel
tgui_panel.initialize()
src << browse(file('html/statbrowser.html'), "window=statbrowser")
addtimer(CALLBACK(src, PROC_REF(check_panel_loaded)), 5 SECONDS)
if(alert_mob_dupe_login)
spawn()
tgui_alert(mob, "You have logged in already with another key this round, please log out of this one NOW or risk being banned!")
connection_time = world.time
connection_realtime = world.realtime
connection_timeofday = world.timeofday
winset(src, null, "command=\".configure graphics-hwmode on\"")
var/cev = CONFIG_GET(number/client_error_version)
var/ceb = CONFIG_GET(number/client_error_build)
var/cwv = CONFIG_GET(number/client_warn_version)
if (byond_version < cev || byond_build < ceb) //Out of date client.
to_chat(src, span_danger("<b>Your version of BYOND is too old:</b>"))
to_chat(src, CONFIG_GET(string/client_error_message))
to_chat(src, "Your version: [byond_version].[byond_build]")
to_chat(src, "Required version: [cev].[ceb] or later")
to_chat(src, "Visit <a href=\"https://secure.byond.com/download\">BYOND's website</a> to get the latest version of BYOND.")
if (connecting_admin)
to_chat(src, "Because you are an admin, you are being allowed to walk past this limitation, But it is still STRONGLY suggested you upgrade")
else
qdel(src)
return 0
else if (byond_version < cwv) //We have words for this client.
if(CONFIG_GET(flag/client_warn_popup))
var/msg = "<HTML><HEAD><meta charset='UTF-8'></HEAD><BODY><b>Your version of byond may be getting out of date:</b><br>"
msg += CONFIG_GET(string/client_warn_message) + "<br><br>"
msg += "Your version: [byond_version]<br>"
msg += "Required version to remove this message: [cwv] or later<br>"
msg += "Visit <a href=\"https://secure.byond.com/download\">BYOND's website</a> to get the latest version of BYOND.<br>"
msg += "</BODY></HTML>"
src << browse(msg, "window=warning_popup")
else
to_chat(src, span_danger("<b>Your version of byond may be getting out of date:</b>"))
to_chat(src, CONFIG_GET(string/client_warn_message))
to_chat(src, "Your version: [byond_version]")
to_chat(src, "Required version to remove this message: [cwv] or later")
to_chat(src, "Visit <a href=\"https://secure.byond.com/download\">BYOND's website</a> to get the latest version of BYOND.")
if (connection == "web" && !connecting_admin)
if (!CONFIG_GET(flag/allow_webclient))
to_chat(src, "Web client is disabled")
qdel(src)
return 0
if (CONFIG_GET(flag/webclient_only_byond_members) && !IsByondMember())
to_chat(src, "Sorry, but the web client is restricted to byond members only.")
qdel(src)
return 0
if( (world.address == address || !address) && !GLOB.host )
GLOB.host = key
world.update_status()
if(holder)
add_admin_verbs()
to_chat(src, get_message_output("memo"))
adminGreet()
add_verbs_from_config()
var/cached_player_age = set_client_age_from_db(tdata) //we have to cache this because other shit may change it and we need it's current value now down below.
if (isnum(cached_player_age) && cached_player_age == -1) //first connection
player_age = 0
var/nnpa = CONFIG_GET(number/notify_new_player_age)
if (isnum(cached_player_age) && cached_player_age == -1) //first connection
if (nnpa >= 0)
message_admins("New user: [key_name_admin(src)] ([address]) is connecting here for the first time.")
if (CONFIG_GET(flag/irc_first_connection_alert))
send2irc_adminless_only("New-user", "[key_name(src)] is connecting for the first time!")
else if (isnum(cached_player_age) && cached_player_age < nnpa)
message_admins("New user: [key_name_admin(src)] just connected with an age of [cached_player_age] day[(player_age==1?"":"s")]")
if(CONFIG_GET(flag/use_account_age_for_jobs) && account_age >= 0)
player_age = account_age
if(account_age >= 0 && account_age < nnpa)
message_admins("[key_name_admin(src)] (IP: [address], ID: [computer_id]) is a new BYOND account [account_age] day[(account_age==1?"":"s")] old, created on [account_join_date].")
if (CONFIG_GET(flag/irc_first_connection_alert))
send2irc_adminless_only("new_byond_user", "[key_name(src)] (IP: [address], ID: [computer_id]) is a new BYOND account [account_age] day[(account_age==1?"":"s")] old, created on [account_join_date].")
get_message_output("watchlist entry", ckey)
check_ip_intel()
validate_key_in_db()
send_resources()
apply_clickcatcher()
if(prefs.lastchangelog != GLOB.changelog_hash) //bolds the changelog button on the interface so we know there are updates.
to_chat(src, span_info("You have unread updates in the changelog."))
if(CONFIG_GET(flag/aggressive_changelog))
changelog()
else
winset(src, "infowindow.changelog", "font-style=bold")
if(ckey in GLOB.clientmessages)
for(var/message in GLOB.clientmessages[ckey])
to_chat(src, message)
GLOB.clientmessages.Remove(ckey)
if(CONFIG_GET(flag/autoconvert_notes))
convert_notes_sql(ckey)
to_chat(src, get_message_output("message", ckey))
if(!winexists(src, "asset_cache_browser")) // The client is using a custom skin, tell them.
to_chat(src, span_warning("Unable to access asset cache browser, if you are using a custom skin file, please allow DS to download the updated version, if you are not, then make a bug report. This is not a critical issue but can cause issues with resource downloading, as it is impossible to know when extra resources arrived to you."))
//This is down here because of the browse() calls in tooltip/New()
if(!tooltips)
tooltips = new /datum/tooltip(src)
var/list/topmenus = GLOB.menulist[/datum/verbs/menu]
for (var/thing in topmenus)
var/datum/verbs/menu/topmenu = thing
var/topmenuname = "[topmenu]"
if (topmenuname == "[topmenu.type]")
var/list/tree = splittext(topmenuname, "/")
topmenuname = tree[tree.len]
winset(src, "[topmenu.type]", "parent=menu;name=[url_encode(topmenuname)]")
var/list/entries = topmenu.Generate_list(src)
for (var/child in entries)
winset(src, "[child]", "[entries[child]]")
if (!ispath(child, /datum/verbs/menu))
var/procpath/verbpath = child
if (verbpath.name[1] != "@")
new child(src)
view_size = new(src, getScreenSize(prefs.read_preference(/datum/preference/toggle/widescreen)))
view_size.resetFormat()
view_size.setZoomMode()
Master.UpdateTickRate()
//Client needs to exists for what follows
. = ..()
var/datum/connection_log/CL = GLOB.connection_logs[ckey]
if(CL)
CL.login()
else
GLOB.connection_logs[ckey] = new/datum/connection_log()
//////////////
//DISCONNECT//
//////////////
/client/Del()
//if(credits)
//QDEL_LIST(credits)
log_access("Logout: [key_name(src)]")
if(holder)
adminGreet(1)
holder.owner = null
GLOB.permissions.admins -= src
if (!GLOB.permissions.admins.len && SSticker.IsRoundInProgress()) //Only report this stuff if we are currently playing.
var/cheesy_message = pick(
"I have no admins online!",\
"I'm all alone :(",\
"I'm feeling lonely :(",\
"I'm so lonely :(",\
"Why does nobody love me? :(",\
"I want a man :(",\
"Where has everyone gone?",\
"I need a hug :(",\
"Someone come hold me :(",\
"I need someone on me :(",\
"What happened? Where has everyone gone?",\
"Forever alone :("\
)
send2irc("Server", "[cheesy_message] (No admins online)")
qdel(holder)
if(ckey in GLOB.permissions.deadmins)
qdel(GLOB.permissions.deadmins[ckey])
GLOB.ahelp_tickets.ClientLogout(src)
GLOB.directory -= ckey
GLOB.clients -= src
var/datum/connection_log/CL = GLOB.connection_logs[ckey]
if(CL)
CL.logout(mob)
QDEL_LIST_ASSOC_VAL(char_render_holders)
if(movingmob != null)
movingmob.client_mobs_in_contents -= mob
UNSETEMPTY(movingmob.client_mobs_in_contents)
seen_messages = null
Master.UpdateTickRate()
world.sync_logout_with_db(connection_number) // yogs - logout logging
return ..()
/client/proc/set_client_age_from_db(connectiontopic)
if (IsGuestKey(src.key))
return
if(!SSdbcore.Connect())
return
var/datum/DBQuery/query_get_related_ip = SSdbcore.NewQuery(
"SELECT ckey FROM [format_table_name("player")] WHERE ip = INET_ATON(:address) AND ckey != :ckey",
list("address" = address, "ckey" = ckey)
)
if(!query_get_related_ip.Execute())
qdel(query_get_related_ip)
return
related_accounts_ip = ""
while(query_get_related_ip.NextRow())
related_accounts_ip += "[query_get_related_ip.item[1]], "
qdel(query_get_related_ip)
var/datum/DBQuery/query_get_related_cid = SSdbcore.NewQuery(
"SELECT ckey FROM [format_table_name("player")] WHERE computerid = :computerid AND ckey != :ckey",
list("computerid" = computer_id, "ckey" = ckey)
)
if(!query_get_related_cid.Execute())
qdel(query_get_related_cid)
return
related_accounts_cid = ""
while (query_get_related_cid.NextRow())
related_accounts_cid += "[query_get_related_cid.item[1]], "
qdel(query_get_related_cid)
var/admin_rank = "Player"
if (src.holder)
admin_rank = src.holder.rank_name()
else
if (!GLOB.permissions.deadmins[ckey] && check_randomizer(connectiontopic))
return
var/new_player
var/datum/DBQuery/query_client_in_db = SSdbcore.NewQuery(
"SELECT 1 FROM [format_table_name("player")] WHERE ckey = :ckey",
list("ckey" = ckey)
)
if(!query_client_in_db.Execute())
qdel(query_client_in_db)
return
if(!query_client_in_db.NextRow())
if (CONFIG_GET(flag/panic_bunker) && !holder && !GLOB.permissions.deadmins[ckey])
log_access("Failed Login: [key] - New account attempting to connect during panic bunker")
message_admins(span_adminnotice("Failed Login: [key] - New account attempting to connect during panic bunker"))
to_chat(src, CONFIG_GET(string/panic_bunker_message))
var/list/connectiontopic_a = params2list(connectiontopic)
var/list/panic_addr = CONFIG_GET(string/panic_server_address)
if(panic_addr && !connectiontopic_a["redirect"])
var/panic_name = CONFIG_GET(string/panic_server_name)
to_chat(src, span_notice("Sending you to [panic_name ? panic_name : panic_addr]."))
winset(src, null, "command=.options")
src << link("[panic_addr]?redirect=1")
qdel(query_client_in_db)
qdel(src)
return
new_player = 1
account_join_date = findJoinDate()
var/datum/DBQuery/query_add_player = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("player")] (`ckey`, `byond_key`, `firstseen`, `firstseen_round_id`, `lastseen`, `lastseen_round_id`, `ip`, `computerid`, `lastadminrank`, `accountjoindate`)
VALUES (:ckey, :key, Now(), :round_id, Now(), :round_id, INET_ATON(:ip), :computerid, :adminrank, :account_join_date)
"}, list("ckey" = ckey, "key" = key, "round_id" = GLOB.round_id, "ip" = address, "computerid" = computer_id, "adminrank" = admin_rank, "account_join_date" = account_join_date || null))
if(!query_add_player.Execute())
qdel(query_client_in_db)
qdel(query_add_player)
return
qdel(query_add_player)
if(!account_join_date)
account_join_date = "Error"
account_age = -1
qdel(query_client_in_db)
var/datum/DBQuery/query_get_client_age = SSdbcore.NewQuery(
"SELECT firstseen, DATEDIFF(Now(),firstseen), accountjoindate, DATEDIFF(Now(),accountjoindate) FROM [format_table_name("player")] WHERE ckey = :ckey",
list("ckey" = ckey)
)
if(!query_get_client_age.Execute())
qdel(query_get_client_age)
return
if(query_get_client_age.NextRow())
player_join_date = query_get_client_age.item[1]
player_age = text2num(query_get_client_age.item[2])
if(!account_join_date)
account_join_date = query_get_client_age.item[3]
account_age = text2num(query_get_client_age.item[4])
if(!account_age)
account_join_date = findJoinDate()
if(!account_join_date)
account_age = -1
else
var/datum/DBQuery/query_datediff = SSdbcore.NewQuery(
"SELECT DATEDIFF(Now(), :account_join_date)",
list("account_join_date" = account_join_date)
)
if(!query_datediff.Execute())
qdel(query_datediff)
return
if(query_datediff.NextRow())
account_age = text2num(query_datediff.item[1])
qdel(query_datediff)
qdel(query_get_client_age)
if(!new_player)
var/datum/DBQuery/query_log_player = SSdbcore.NewQuery(
"UPDATE [format_table_name("player")] SET lastseen = Now(), lastseen_round_id = :round_id, ip = INET_ATON(:ip), computerid = :computerid, lastadminrank = :admin_rank, accountjoindate = :account_join_date WHERE ckey = :ckey",
list("round_id" = GLOB.round_id, "ip" = address, "computerid" = computer_id, "admin_rank" = admin_rank, "account_join_date" = account_join_date || null, "ckey" = ckey)
)
if(!query_log_player.Execute())
qdel(query_log_player)
return
qdel(query_log_player)
if(!account_join_date)
account_join_date = "Error"
sync_login_with_db()
if(new_player)
player_age = -1
. = player_age
/client/proc/findJoinDate()
var/list/http = world.Export("http://byond.com/members/[ckey]?format=text")
if(!http)
log_world("Failed to connect to byond member page to age check [ckey]")
return
var/F = file2text(http["CONTENT"])
if(F)
var/regex/R = regex("joined = \"(\\d{4}-\\d{2}-\\d{2})\"")
if(R.Find(F))
. = R.group[1]
else
CRASH("Age check regex failed for [src.ckey]")
/client/proc/validate_key_in_db()
var/sql_key
var/datum/DBQuery/query_check_byond_key = SSdbcore.NewQuery(
"SELECT byond_key FROM [format_table_name("player")] WHERE ckey = :ckey",
list("ckey" = ckey)
)
if(!query_check_byond_key.Execute())
qdel(query_check_byond_key)
return
if(query_check_byond_key.NextRow())
sql_key = query_check_byond_key.item[1]
qdel(query_check_byond_key)
if(key != sql_key)
var/list/http = world.Export("http://byond.com/members/[ckey]?format=text")
if(!http)
log_world("Failed to connect to byond member page to get changed key for [ckey]")
return
var/F = file2text(http["CONTENT"])
if(F)
var/regex/R = regex("\\tkey = \"(.+)\"")
if(R.Find(F))
var/web_key = R.group[1]
var/datum/DBQuery/query_update_byond_key = SSdbcore.NewQuery(
"UPDATE [format_table_name("player")] SET byond_key = :byond_key WHERE ckey = :ckey",
list("byond_key" = web_key, "ckey" = ckey)
)
query_update_byond_key.Execute()
qdel(query_update_byond_key)
else
CRASH("Key check regex failed for [ckey]")
/client/proc/check_randomizer(topic)
. = FALSE
if (connection != "seeker")
return
topic = params2list(topic)
if (!CONFIG_GET(flag/check_randomizer))
return
var/static/cidcheck = list()
var/static/tokens = list()
var/static/cidcheck_failedckeys = list() //to avoid spamming the admins if the same guy keeps trying.
var/static/cidcheck_spoofckeys = list()
var/datum/DBQuery/query_cidcheck = SSdbcore.NewQuery(
"SELECT computerid FROM [format_table_name("player")] WHERE ckey = :ckey",
list("ckey" = ckey)
)
query_cidcheck.Execute()
var/lastcid
if (query_cidcheck.NextRow())
lastcid = query_cidcheck.item[1]
qdel(query_cidcheck)
var/oldcid = cidcheck[ckey]
if (oldcid)
if (!topic || !topic["token"] || !tokens[ckey] || topic["token"] != tokens[ckey])
if (!cidcheck_spoofckeys[ckey])
message_admins(span_adminnotice("[key_name(src)] appears to have attempted to spoof a cid randomizer check."))
cidcheck_spoofckeys[ckey] = TRUE
cidcheck[ckey] = computer_id
tokens[ckey] = cid_check_reconnect()
sleep(15 SECONDS) //Longer sleep here since this would trigger if a client tries to reconnect manually because the inital reconnect failed
//we sleep after telling the client to reconnect, so if we still exist something is up
log_access("Forced disconnect: [key] [computer_id] [address] - CID randomizer check")
qdel(src)
return TRUE
if (oldcid != computer_id && computer_id != lastcid) //IT CHANGED!!!
cidcheck -= ckey //so they can try again after removing the cid randomizer.
to_chat(src, span_userdanger("Connection Error:"))
to_chat(src, span_danger("Invalid ComputerID(spoofed). Please remove the ComputerID spoofer from your byond installation and try again."))
if (!cidcheck_failedckeys[ckey])
message_admins(span_adminnotice("[key_name(src)] has been detected as using a cid randomizer. Connection rejected."))
send2irc_adminless_only("CidRandomizer", "[key_name(src)] has been detected as using a cid randomizer. Connection rejected.")
cidcheck_failedckeys[ckey] = TRUE
note_randomizer_user()
log_access("Failed Login: [key] [computer_id] [address] - CID randomizer confirmed (oldcid: [oldcid])")
qdel(src)
return TRUE
else
if (cidcheck_failedckeys[ckey])
message_admins(span_adminnotice("[key_name_admin(src)] has been allowed to connect after showing they removed their cid randomizer"))
send2irc_adminless_only("CidRandomizer", "[key_name(src)] has been allowed to connect after showing they removed their cid randomizer.")
cidcheck_failedckeys -= ckey
if (cidcheck_spoofckeys[ckey])
message_admins(span_adminnotice("[key_name_admin(src)] has been allowed to connect after appearing to have attempted to spoof a cid randomizer check because it <i>appears</i> they aren't spoofing one this time"))
cidcheck_spoofckeys -= ckey
cidcheck -= ckey
else if (computer_id != lastcid)
cidcheck[ckey] = computer_id
tokens[ckey] = cid_check_reconnect()
sleep(5 SECONDS) //browse is queued, we don't want them to disconnect before getting the browse() command.
//we sleep after telling the client to reconnect, so if we still exist something is up
log_access("Forced disconnect: [key] [computer_id] [address] - CID randomizer check")
qdel(src)
return TRUE
/client/proc/cid_check_reconnect()
var/token = md5("[rand(0,9999)][world.time][rand(0,9999)][ckey][rand(0,9999)][address][rand(0,9999)][computer_id][rand(0,9999)]")
. = token
log_access("Failed Login: [key] [computer_id] [address] - CID randomizer check")
var/url = winget(src, null, "url")
//special javascript to make them reconnect under a new window.
src << browse({"<a id='link' href="byond://[url]?token=[token]">byond://[url]?token=[token]</a><script type="text/javascript">document.getElementById("link").click();window.location="byond://winset?command=.quit"</script>"}, "border=0;titlebar=0;size=1x1;window=redirect")
to_chat(src, {"<a href="byond://[url]?token=[token]">You will be automatically taken to the game, if not, click here to be taken manually</a>"})
/client/proc/note_randomizer_user()
add_system_note("CID-Error", "Detected as using a cid randomizer.")
/**
* Makes the server note a player
*
* Automatically checks that we haven't noted them with the same system_ckey in the last day, and that their last note is not from that ckey either
* Arguments:
** system_ckey The ckey of the server giving the note
** message The actual message
** avoid_duplicate Should we NEVER add a note with the same message?
*/
/client/proc/add_system_note(system_ckey, message, avoid_duplicate = FALSE)
//check to see if we noted them in the last day.
var/datum/DBQuery/query_get_notes = SSdbcore.NewQuery(
"SELECT id FROM [format_table_name("messages")] WHERE type = 'note' AND targetckey = :targetckey AND adminckey = :adminckey AND timestamp + INTERVAL 1 DAY < NOW() AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)",
list("targetckey" = ckey, "adminckey" = system_ckey)
)
if(!query_get_notes.Execute())
qdel(query_get_notes)
return
if(query_get_notes.NextRow())
qdel(query_get_notes)
return
qdel(query_get_notes)
//regardless of above, make sure their last note is not from us, as no point in repeating the same note over and over.
query_get_notes = SSdbcore.NewQuery(
"SELECT adminckey FROM [format_table_name("messages")] WHERE targetckey = :targetckey AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL) ORDER BY timestamp DESC LIMIT 1",
list("targetckey" = ckey)
)
if(!query_get_notes.Execute())
qdel(query_get_notes)
return
if(query_get_notes.NextRow())
if (query_get_notes.item[1] == system_ckey)
qdel(query_get_notes)
return
qdel(query_get_notes)
if(avoid_duplicate)
query_get_notes = SSdbcore.NewQuery(
"SELECT adminckey FROM [format_table_name("messages")] WHERE targetckey = :targetckey AND deleted = 0 AND text = :message AND (expire_timestamp > NOW() OR expire_timestamp IS NULL) ORDER BY timestamp DESC",
list("targetckey" = ckey, "message" = message)
)
if(!query_get_notes.Execute())
qdel(query_get_notes)
return
if(query_get_notes.NextRow())
if (query_get_notes.item[1] == system_ckey)
qdel(query_get_notes)
return
//create_message("note", key, system_ckey, message, null, null, 0, 0, null, 0, 0)
create_message("note", key, system_ckey, message, null, null, 0, 0, null, 0) //yogs -
/client/proc/check_ip_intel()
set waitfor = 0 //we sleep when getting the intel, no need to hold up the client connection while we sleep
if (CONFIG_GET(string/ipintel_email))
var/datum/ipintel/res = get_ip_intel(address)
if (res.intel >= CONFIG_GET(number/ipintel_rating_bad))
message_admins(span_adminnotice("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a Proxy/VPN."))
ip_intel = res.intel
/client/Click(atom/object, atom/location, control, params)
if(click_intercept_time)
if(click_intercept_time >= world.time)
click_intercept_time = 0 //Reset and return. Next click should work, but not this one.
return
click_intercept_time = 0 //Just reset. Let's not keep re-checking forever.
if(src.afreeze)
to_chat(src, span_userdanger("You have been frozen by an administrator."))
return
var/ab = FALSE
var/list/L = params2list(params)
var/dragged = L["drag"]
if(dragged && !L[dragged])
return
if (object && IS_WEAKREF_OF(object, middle_drag_atom_ref) && LAZYACCESS(L, LEFT_CLICK))
ab = max(0, 5 SECONDS-(world.time-middragtime)*0.1)
var/mcl = CONFIG_GET(number/minute_click_limit)
if (!holder && mcl)
var/minute = round(world.time, 600)
if (!clicklimiter)
clicklimiter = new(LIMITER_SIZE)
if (minute != clicklimiter[CURRENT_MINUTE])
clicklimiter[CURRENT_MINUTE] = minute
clicklimiter[MINUTE_COUNT] = 0
clicklimiter[MINUTE_COUNT] += 1+(ab)
if (clicklimiter[MINUTE_COUNT] > mcl)
var/msg = "Your previous click was ignored because you've done too many in a minute."
if (minute != clicklimiter[ADMINSWARNED_AT]) //only one admin message per-minute. (if they spam the admins can just boot/ban them)
clicklimiter[ADMINSWARNED_AT] = minute
msg += " Administrators have been informed."
if (ab)
log_game("[key_name(src)] is using the middle click aimbot exploit")
message_admins("[ADMIN_LOOKUPFLW(src)] [ADMIN_KICK(usr)] is using the middle click aimbot exploit</span>")
add_system_note("aimbot", "Is using the middle click aimbot exploit")
log_game("[key_name(src)] Has hit the per-minute click limit of [mcl] clicks in a given game minute")
message_admins("[ADMIN_LOOKUPFLW(src)] [ADMIN_KICK(usr)] Has hit the per-minute click limit of [mcl] clicks in a given game minute")
to_chat(src, span_danger("[msg]"))
return
var/scl = CONFIG_GET(number/second_click_limit)
if (!holder && scl)
var/second = round(world.time, 10)
if (!clicklimiter)
clicklimiter = new(LIMITER_SIZE)
if (second != clicklimiter[CURRENT_SECOND])
clicklimiter[CURRENT_SECOND] = second
clicklimiter[SECOND_COUNT] = 0
clicklimiter[SECOND_COUNT] += 1+(!!ab)
if (clicklimiter[SECOND_COUNT] > scl)
to_chat(src, span_danger("Your previous click was ignored because you've done too many in a second"))
return
if (hotkeys)
// If hotkey mode is enabled, then clicking the map will automatically
// unfocus the text bar. This removes the red color from the text bar
// so that the visual focus indicator matches reality.
winset(src, null, "input.background-color=[COLOR_INPUT_DISABLED]")
else
winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED]")
SEND_SIGNAL(src, COMSIG_CLIENT_CLICK, object, location, control, params, usr)
..()
/client/proc/add_verbs_from_config()
if(CONFIG_GET(flag/see_own_notes))
add_verb(src, /client/proc/self_notes)
if(CONFIG_GET(flag/use_exp_tracking))
add_verb(src, /client/proc/self_playtime)
#undef UPLOAD_LIMIT
//checks if a client is afk
//3000 frames = 5 minutes
/client/proc/is_afk(duration = CONFIG_GET(number/inactivity_period))
if(inactivity > duration)
return inactivity
return FALSE
/// Send resources to the client.
/// Sends both game resources and browser assets.
/client/proc/send_resources()
#if (PRELOAD_RSC == 0)
var/static/next_external_rsc = 0
var/list/external_rsc_urls = CONFIG_GET(keyed_list/external_rsc_urls)
if(length(external_rsc_urls))
next_external_rsc = WRAP(next_external_rsc+1, 1, external_rsc_urls.len+1)
preload_rsc = external_rsc_urls[next_external_rsc]
#endif
spawn (10) //removing this spawn causes all clients to not get verbs.
//load info on what assets the client has
src << browse('code/modules/asset_cache/validate_assets.html', "window=asset_cache_browser")
//Precache the client with all other assets slowly, so as to not block other browse() calls
if (CONFIG_GET(flag/asset_simple_preload))
addtimer(CALLBACK(SSassets.transport, TYPE_PROC_REF(/datum/asset_transport, send_assets_slow), src, SSassets.transport.preload), 5 SECONDS)
#if (PRELOAD_RSC == 0)
for (var/name in GLOB.vox_sounds)
var/file = GLOB.vox_sounds[name]
Export("##action=load_rsc", file)
stoplag()
for (var/name in GLOB.vox_sounds_male) //YOGS start - male vox
var/file = GLOB.vox_sounds_male[name]
Export("##action=load_rsc", file)
stoplag() //YOGS end - male vox
for (var/name in GLOB.vox_sounds_military)
var/file = GLOB.vox_sounds_military[name]
Export("##action=load_rsc", file)
stoplag()
#endif
//Hook, override it to run code when dir changes
//Like for /atoms, but clients are their own snowflake FUCK
/client/proc/setDir(newdir)
dir = newdir
/client/vv_edit_var(var_name, var_value)
switch (var_name)
if ("holder")
return FALSE
if ("ckey")
return FALSE
if ("key")
return FALSE
if("view")
view_size.setDefault(var_value)
return TRUE
. = ..()
/client/proc/rescale_view(change, min, max)
view_size.setTo(clamp(change, min, max), clamp(change, min, max))
/**
* Updates the keybinds for special keys
*
* Handles adding macros for the keys that need it
* And adding movement keys to the clients movement_keys list
* At the time of writing this, communication(OOC, Say, IC) require macros
* Arguments:
* * direct_prefs - the preference we're going to get keybinds from
*/
/client/proc/update_special_keybinds(datum/preferences/direct_prefs)
var/datum/preferences/D = prefs || direct_prefs
if(!D?.key_bindings)
return
movement_keys = list()
for(var/kb_name in D.key_bindings)
for(var/key in D.key_bindings[kb_name])
switch(kb_name)
if("North")
movement_keys[key] = NORTH
if("East")
movement_keys[key] = EAST
if("West")
movement_keys[key] = WEST
if("South")
movement_keys[key] = SOUTH
if(SAY_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=.say")
if(ME_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=.me")
if(OOC_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=ooc")
if(LOOC_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=looc")
if(ASAY_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=asay")
if(MSAY_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=msay")
if(DONORSAY_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=.donorsay")
if(DEADSAY_CHANNEL)
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=dsay")
/client/proc/change_view(new_size)
if (isnull(new_size))
CRASH("change_view called without argument.")
view = new_size
apply_clickcatcher()
mob.reload_fullscreen()
if(mob && istype(mob.hud_used, /datum/hud/ai))
if(new_size == CONFIG_GET(string/default_view) || new_size == CONFIG_GET(string/default_view_square))
QDEL_NULL(mob.hud_used)
mob.create_mob_hud()
mob.hud_used.show_hud(mob.hud_used.hud_version)
mob.hud_used.update_ui_style(ui_style2icon(prefs.read_preference(/datum/preference/choiced/ui_style)))
if (isliving(mob))
var/mob/living/M = mob
M.update_damage_hud()
if (prefs.read_preference(/datum/preference/toggle/auto_fit_viewport))
addtimer(CALLBACK(src, VERB_REF(fit_viewport), 1 SECONDS)) //Delayed to avoid wingets from Login calls.
/client/proc/generate_clickcatcher()
if(!void)
void = new()
if(!(void in screen))
screen += void
/client/proc/apply_clickcatcher()
generate_clickcatcher()
var/list/actualview = getviewsize(view)
void.UpdateGreed(actualview[1],actualview[2])
/client/proc/AnnouncePR(announcement)
if(prefs && prefs.chat_toggles & CHAT_PULLR)
to_chat(src, announcement)
/// compiles a full list of verbs and sends it to the browser
/client/proc/init_verbs()
if(IsAdminAdvancedProcCall())
return
var/list/verblist = list()
var/list/verbstoprocess = verbs.Copy()
if(mob)
verbstoprocess += mob.verbs
for(var/atom/movable/thing as anything in mob.contents)
verbstoprocess += thing.verbs
panel_tabs.Cut() // panel_tabs get reset in init_verbs on JS side anyway
for(var/procpath/verb_to_init as anything in verbstoprocess)
if(!verb_to_init)
continue
if(verb_to_init.hidden)
continue
if(!istext(verb_to_init.category))
continue
panel_tabs |= verb_to_init.category
verblist[++verblist.len] = list(verb_to_init.category, verb_to_init.name)
src << output("[url_encode(json_encode(panel_tabs))];[url_encode(json_encode(verblist))]", "statbrowser:init_verbs")
/client/proc/check_panel_loaded()
if(statbrowser_ready)
return
to_chat(src, span_userdanger("Statpanel failed to load, click <a href='?src=[REF(src)];reload_statbrowser=1'>here</a> to reload the panel "))
tgui_panel.initialize()
/client/verb/stop_client_sounds()
set name = "Stop Sounds"
set category = "OOC"
set desc = "Stop Current Sounds"
SEND_SOUND(usr, sound(null))
tgui_panel?.stop_music()
SSblackbox.record_feedback("nested tally", "preferences_verb", 1, list("Stop Self Sounds"))