mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-28 01:51:46 +00:00
## About The Pull Request Fixes #89445 (well, technically. It fixes the bug associated but these `say`s should really be emotes.) Three things: 1. Reworks how language translation works. Rather than scrambling a sentence into a language entirely, sentences are now scrambled on a per-word basis. Additionally, the 1000 most common words of a language are *never* re-scrambled across the duration of a round. Once it's set it's set in stone. Example: (Sample / Old / New)  This allows for a number of things: - More consistent translations, making it (more) viable to actually "teach" someone words for something - Maintaining emphasis such as caps (but not `||`, `++`, or `__` - at least not yet) - The following: 2. Adds partial language understanding Some languages can understand portions of other languages.  This pr adds the following: - Those who understand Beachtongue can understand 50% of Common and 33% of Uncommon words. - Those who understand Common can understand 33% of Beachtongue and 20% of Uncommon words. - Those who understand Uncommon can understand 20% of Common and 20% of Beachtongue words. 3. Bilingual quirk has been expanded to accomodate these changes. There are now two more preferences: - Language Speakable - You can toggle this, so you only understand the language, rather than understand AND speak. - Language Skill - If you choose to be unable to speak the language, you can set how much of the language you can understand, down to 10%. ## Why It's Good For The Game Playing around languages is fun, but due to the way our translation works, ALL context is immediately lost for what the other person may be saying. If the other person is shouting in all caps? Output language is normal chatting. This is lame! Even if someone is unable to understand you, there's a LOT you can convey just by how you speak, and getting that across in game is quite difficult when all translations get mauled so badly. So this changes that. - Emphasis like caps lock is maintained, so you see someone shouting in caps in a foreign language you can probably intuit something is wrong (but not what is wrong!) - Some languages can gleam bits of other languages, so you MIGHT be able to pick out context if you pay close attention - "Brother" languages will now feel more like "brothers" and not completely divergent - You can even "teach" someone words in your language - at least the most common words! (Until next round) ## Changelog 🆑 Melbert add: Languages can now have partial understanding of other languages. More common English words are more likely to be mutually understood. add: Those who understand Beachtongue can understand 50% of Common and 33% of Uncommon words. add: Those who understand Common can understand 33% of Beachtongue and 20% of Uncommon words. add: Those who understand Uncommon can understand 20% of Common and 20% of Beachtongue words. add: Bilingual quirk: You can now choose between being able to speak or not speak the language add: Bilingual quirk: You can now choose to have partial understanding of your language, rather than full. qol: If you speak in ALL CAPS in a foreign language, the translated words will also be ALL CAPS. qol: Many more forms of punctuation are now conveyed across translations. qol: The 1000 most common English words will now never be scrambled when translating into other languages for the duration of the round. This means you can actually "learn" some words if you are especially attentive! (Until the next round at least) refactor: Refactored language translations. Report if you see any super odd looking translations. fix: Force-says forcing you to speak common (such as cult invocations) will now correctly force you to speak common (even if you don't know common) /🆑
299 lines
11 KiB
Plaintext
299 lines
11 KiB
Plaintext
/**
|
|
* # Discord Subsystem
|
|
*
|
|
* This subsystem handles some integrations with discord
|
|
*
|
|
*
|
|
* NOTES:
|
|
* * There is a DB table to track ckeys and associated discord IDs. (discord_link)
|
|
* * This system REQUIRES TGS for notifying users at end of the round
|
|
* * The SS uses fire() instead of just pure shutdown, so people can be notified if it comes back after a crash, where the SS wasn't properly shutdown
|
|
* * It only writes to the disk every 5 minutes, and it won't write to disk if the file is the same as it was the last time it was written. This is to save on disk writes
|
|
* * The system is kept per-server (EG: Terry will not notify people who pressed notify on Sybil), but the accounts are between servers so you dont have to relink on each server.
|
|
*
|
|
*
|
|
* ## HOW NOTIFYING WORKS
|
|
*
|
|
* ### ROUNDSTART:
|
|
* 1) The file is loaded and the discord IDs are extracted
|
|
* 2) A ping is sent to the discord with the IDs of people who wished to be notified
|
|
* 3) The file is emptied
|
|
*
|
|
* ### MIDROUND:
|
|
* 1) Someone usees the notify verb, it adds their discord ID to the list.
|
|
* 2) On fire, it will write that to the disk, as long as conditions above are correct
|
|
*
|
|
* ### END ROUND:
|
|
* 1) The file is force-saved, incase it hasn't fired at end round
|
|
*
|
|
* This is an absolute clusterfuck, but its my clusterfuck -aa07
|
|
*/
|
|
SUBSYSTEM_DEF(discord)
|
|
name = "Discord"
|
|
wait = 3000
|
|
init_stage = INITSTAGE_EARLY
|
|
|
|
/// People to save to notify file
|
|
var/list/notify_members = list()
|
|
/// Copy of previous list, so the SS doesnt have to fire if no new members have been added
|
|
var/list/notify_members_cache = list()
|
|
/// People to notify on roundstart
|
|
var/list/people_to_notify = list()
|
|
|
|
/// People who have tried to verify this round already
|
|
var/list/reverify_cache
|
|
|
|
/// The file where notification status is saved
|
|
var/notify_file = file("data/notify.json")
|
|
|
|
/// Is TGS enabled (If not we won't fire because otherwise this is useless)
|
|
var/enabled = FALSE
|
|
|
|
/datum/controller/subsystem/discord/Initialize()
|
|
reverify_cache = list()
|
|
// Check for if we are using TGS, otherwise return and disables firing
|
|
if(world.TgsAvailable())
|
|
enabled = TRUE // Allows other procs to use this (Account linking, etc)
|
|
else
|
|
can_fire = FALSE // We dont want excess firing
|
|
return SS_INIT_NO_NEED
|
|
|
|
try
|
|
people_to_notify = json_decode(file2text(notify_file))
|
|
catch
|
|
pass() // The list can just stay as its default (blank). Pass() exists because it needs a catch
|
|
var/notifymsg = jointext(people_to_notify, ", ")
|
|
if(notifymsg)
|
|
notifymsg += ", a new round is starting!"
|
|
for(var/channel_tag in CONFIG_GET(str_list/chat_new_game_notifications))
|
|
// Sends the message to the discord, using same config option as the roundstart notification
|
|
send2chat(new /datum/tgs_message_content(trim(notifymsg)), channel_tag)
|
|
fdel(notify_file) // Deletes the file
|
|
return SS_INIT_SUCCESS
|
|
|
|
/datum/controller/subsystem/discord/fire()
|
|
if(!enabled)
|
|
return // Dont do shit if its disabled
|
|
if(notify_members == notify_members_cache)
|
|
return // Dont re-write the file
|
|
// If we are all clear
|
|
write_notify_file()
|
|
|
|
/datum/controller/subsystem/discord/Shutdown()
|
|
write_notify_file() // Guaranteed force-write on server close
|
|
|
|
/datum/controller/subsystem/discord/proc/write_notify_file()
|
|
if(!enabled) // Dont do shit if its disabled
|
|
return
|
|
fdel(notify_file) // Deletes the file first to make sure it writes properly
|
|
WRITE_FILE(notify_file, json_encode(notify_members)) // Writes the file
|
|
notify_members_cache = notify_members // Updates the cache list
|
|
|
|
/**
|
|
* Given a ckey, look up the discord user id attached to the user, if any
|
|
*
|
|
* This gets the most recent entry from the discord link table that is associated with the given ckey
|
|
*
|
|
* Arguments:
|
|
* * lookup_ckey A string representing the ckey to search on
|
|
*/
|
|
/datum/controller/subsystem/discord/proc/lookup_id(lookup_ckey)
|
|
var/datum/discord_link_record/link = find_discord_link_by_ckey(lookup_ckey, only_valid = TRUE)
|
|
if(link)
|
|
return link.discord_id
|
|
|
|
/**
|
|
* Given a discord id as a string, look up the ckey attached to that account, if any
|
|
*
|
|
* This gets the most recent entry from the discord_link table that is associated with this discord id snowflake
|
|
*
|
|
* Arguments:
|
|
* * lookup_id The discord id as a string
|
|
*/
|
|
/datum/controller/subsystem/discord/proc/lookup_ckey(lookup_id)
|
|
var/datum/discord_link_record/link = find_discord_link_by_discord_id(lookup_id, only_valid = TRUE)
|
|
if(link)
|
|
return link.ckey
|
|
|
|
/datum/controller/subsystem/discord/proc/get_or_generate_one_time_token_for_ckey(ckey)
|
|
// Is there an existing valid one time token
|
|
var/datum/discord_link_record/link = find_discord_link_by_ckey(ckey, timebound = TRUE)
|
|
if(link)
|
|
return link.one_time_token
|
|
|
|
// Otherwise we make one
|
|
return generate_one_time_token(ckey)
|
|
|
|
/**
|
|
* Generate a timebound token for discord verification
|
|
*
|
|
* This uses the common word list to generate a six word random token, this token can then be fed to a discord bot that has access
|
|
* to the same database, and it can use it to link a ckey to a discord id, with minimal user effort
|
|
*
|
|
* It returns the token to the calling proc, after inserting an entry into the discord_link table of the following form
|
|
*
|
|
* ```
|
|
* (unique_id, ckey, null, the current time, the one time token generated)
|
|
* the null value will be filled out with the discord id by the integrated discord bot when a user verifies
|
|
* ```
|
|
*
|
|
* Notes:
|
|
* * The token is guaranteed to unique during its validity period
|
|
* * The validity period is currently set at 4 hours
|
|
* * a token may not be unique outside its validity window (to reduce conflicts)
|
|
*
|
|
* Arguments:
|
|
* * ckey_for a string representing the ckey this token is for
|
|
*
|
|
* Returns a string representing the one time token
|
|
*/
|
|
/datum/controller/subsystem/discord/proc/generate_one_time_token(ckey_for)
|
|
|
|
var/not_unique = TRUE
|
|
var/one_time_token = ""
|
|
// While there's a collision in the token, generate a new one (should rarely happen)
|
|
while(not_unique)
|
|
//Column is varchar 100, so we trim just in case someone does us the dirty later
|
|
one_time_token = trim("[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]-[pick(GLOB.most_common_words)]", 100)
|
|
|
|
not_unique = find_discord_link_by_token(one_time_token, timebound = TRUE)
|
|
|
|
// Insert into the table, null in the discord id, id and timestamp and valid fields so the db fills them out where needed
|
|
var/datum/db_query/query_insert_link_record = SSdbcore.NewQuery(
|
|
"INSERT INTO [format_table_name("discord_links")] (ckey, one_time_token) VALUES(:ckey, :token)",
|
|
list("ckey" = ckey_for, "token" = one_time_token)
|
|
)
|
|
|
|
if(!query_insert_link_record.Execute())
|
|
qdel(query_insert_link_record)
|
|
return ""
|
|
|
|
//Cleanup
|
|
qdel(query_insert_link_record)
|
|
return one_time_token
|
|
|
|
/**
|
|
* Find discord link entry by the passed in user token
|
|
*
|
|
* This will look into the discord link table and return the *first* entry that matches the given one time token
|
|
*
|
|
* Remember, multiple entries can exist, as they are only guaranteed to be unique for their validity period
|
|
*
|
|
* Arguments:
|
|
* * one_time_token the string of words representing the one time token
|
|
* * timebound A boolean flag, that specifies if it should only look for entries within the last 4 hours, off by default
|
|
*
|
|
* Returns a [/datum/discord_link_record]
|
|
*/
|
|
/datum/controller/subsystem/discord/proc/find_discord_link_by_token(one_time_token, timebound = FALSE)
|
|
var/timeboundsql = ""
|
|
if(timebound)
|
|
timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR"
|
|
var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE one_time_token = :one_time_token [timeboundsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1"
|
|
var/datum/db_query/query_get_discord_link_record = SSdbcore.NewQuery(
|
|
query,
|
|
list("one_time_token" = one_time_token)
|
|
)
|
|
if(!query_get_discord_link_record.Execute())
|
|
qdel(query_get_discord_link_record)
|
|
return
|
|
if(query_get_discord_link_record.NextRow())
|
|
var/result = query_get_discord_link_record.item
|
|
. = new /datum/discord_link_record(result[2], result[1], result[4], result[3])
|
|
|
|
//Make sure we clean up the query
|
|
qdel(query_get_discord_link_record)
|
|
|
|
/**
|
|
* Find discord link entry by the passed in user ckey
|
|
*
|
|
* This will look into the discord link table and return the *first* entry that matches the given ckey
|
|
*
|
|
* Remember, multiple entries can exist
|
|
*
|
|
* Arguments:
|
|
* * ckey the users ckey as a string
|
|
* * timebound should we search only in the last 4 hours
|
|
*
|
|
* Returns a [/datum/discord_link_record]
|
|
*/
|
|
/datum/controller/subsystem/discord/proc/find_discord_link_by_ckey(ckey, timebound = FALSE, only_valid = FALSE)
|
|
var/timeboundsql = ""
|
|
if(timebound)
|
|
timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR"
|
|
var/validsql = ""
|
|
if(only_valid)
|
|
validsql = "AND valid = 1"
|
|
|
|
var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE ckey = :ckey [timeboundsql] [validsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1"
|
|
var/datum/db_query/query_get_discord_link_record = SSdbcore.NewQuery(
|
|
query,
|
|
list("ckey" = ckey)
|
|
)
|
|
if(!query_get_discord_link_record.Execute())
|
|
qdel(query_get_discord_link_record)
|
|
return
|
|
|
|
if(query_get_discord_link_record.NextRow())
|
|
var/result = query_get_discord_link_record.item
|
|
. = new /datum/discord_link_record(result[2], result[1], result[4], result[3])
|
|
|
|
//Make sure we clean up the query
|
|
qdel(query_get_discord_link_record)
|
|
|
|
|
|
/**
|
|
* Find discord link entry by the passed in user ckey
|
|
*
|
|
* This will look into the discord link table and return the *first* entry that matches the given ckey
|
|
*
|
|
* Remember, multiple entries can exist
|
|
*
|
|
* Arguments:
|
|
* * discord_id The users discord id (string)
|
|
* * timebound should we search only in the last 4 hours
|
|
*
|
|
* Returns a [/datum/discord_link_record]
|
|
*/
|
|
/datum/controller/subsystem/discord/proc/find_discord_link_by_discord_id(discord_id, timebound = FALSE, only_valid = FALSE)
|
|
var/timeboundsql = ""
|
|
if(timebound)
|
|
timeboundsql = "AND timestamp >= Now() - INTERVAL 4 HOUR"
|
|
var/validsql = ""
|
|
if(only_valid)
|
|
validsql = "AND valid = 1"
|
|
|
|
var/query = "SELECT CAST(discord_id AS CHAR(25)), ckey, MAX(timestamp), one_time_token FROM [format_table_name("discord_links")] WHERE discord_id = :discord_id [timeboundsql] [validsql] GROUP BY ckey, discord_id, one_time_token LIMIT 1"
|
|
var/datum/db_query/query_get_discord_link_record = SSdbcore.NewQuery(
|
|
query,
|
|
list("discord_id" = discord_id)
|
|
)
|
|
if(!query_get_discord_link_record.Execute())
|
|
qdel(query_get_discord_link_record)
|
|
return
|
|
|
|
if(query_get_discord_link_record.NextRow())
|
|
var/result = query_get_discord_link_record.item
|
|
. = new /datum/discord_link_record(result[2], result[1], result[4], result[3])
|
|
|
|
//Make sure we clean up the query
|
|
qdel(query_get_discord_link_record)
|
|
|
|
|
|
/**
|
|
* Extract a discord id from a mention string
|
|
*
|
|
* This will regex out the mention <@num> block to extract the discord id
|
|
*
|
|
* Arguments:
|
|
* * discord_id The users discord mention string (string)
|
|
*
|
|
* Returns a text string with the discord id or null
|
|
*/
|
|
/datum/controller/subsystem/discord/proc/get_discord_id_from_mention(mention)
|
|
var/static/regex/discord_mention_extraction_regex = regex(@"<@([0-9]+)>")
|
|
discord_mention_extraction_regex.Find(mention)
|
|
if (length(discord_mention_extraction_regex.group) == 1)
|
|
return discord_mention_extraction_regex.group[1]
|
|
return null
|