You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

768 lines
24 KiB

  1. /*
  2. * Holds procs designed to help with filtering text
  3. * Contains groups:
  4. * SQL sanitization/formating
  5. * Text sanitization
  6. * Text searches
  7. * Text modification
  8. * Misc
  9. */
  10. /*
  11. * SQL sanitization
  12. */
  13. // Run all strings to be used in an SQL query through this proc first to properly escape out injection attempts.
  14. /proc/sanitizeSQL(t)
  15. return SSdbcore.Quote("[t]")
  16. /proc/format_table_name(table as text)
  17. return CONFIG_GET(string/feedback_tableprefix) + table
  18. /*
  19. * Text sanitization
  20. */
  21. //Simply removes < and > and limits the length of the message
  22. /proc/strip_html_simple(t,limit=MAX_MESSAGE_LEN)
  23. var/list/strip_chars = list("<",">")
  24. t = copytext(t,1,limit)
  25. for(var/char in strip_chars)
  26. var/index = findtext(t, char)
  27. while(index)
  28. t = copytext(t, 1, index) + copytext(t, index+1)
  29. index = findtext(t, char)
  30. return t
  31. //Removes a few problematic characters
  32. /proc/sanitize_simple(t,list/repl_chars = list("\n"="#","\t"="#"))
  33. for(var/char in repl_chars)
  34. var/index = findtext(t, char)
  35. while(index)
  36. t = copytext(t, 1, index) + repl_chars[char] + copytext(t, index+1)
  37. index = findtext(t, char, index+1)
  38. return t
  39. /proc/sanitize_filename(t)
  40. return sanitize_simple(t, list("\n"="", "\t"="", "/"="", "\\"="", "?"="", "%"="", "*"="", ":"="", "|"="", "\""="", "<"="", ">"=""))
  41. //Runs byond's sanitization proc along-side sanitize_simple
  42. /proc/sanitize(t,list/repl_chars = null)
  43. return html_encode(sanitize_simple(t,repl_chars))
  44. //Runs sanitize and strip_html_simple
  45. //I believe strip_html_simple() is required to run first to prevent '<' from displaying as '&lt;' after sanitize() calls byond's html_encode()
  46. /proc/strip_html(t,limit=MAX_MESSAGE_LEN)
  47. return copytext((sanitize(strip_html_simple(t))),1,limit)
  48. //Runs byond's sanitization proc along-side strip_html_simple
  49. //I believe strip_html_simple() is required to run first to prevent '<' from displaying as '&lt;' that html_encode() would cause
  50. /proc/adminscrub(t,limit=MAX_MESSAGE_LEN)
  51. return copytext((html_encode(strip_html_simple(t))),1,limit)
  52. //Returns null if there is any bad text in the string
  53. /proc/reject_bad_text(text, max_length=512)
  54. if(length(text) > max_length)
  55. return //message too long
  56. var/non_whitespace = 0
  57. for(var/i=1, i<=length(text), i++)
  58. switch(text2ascii(text,i))
  59. if(62,60,92,47)
  60. return //rejects the text if it contains these bad characters: <, >, \ or /
  61. if(127 to 255)
  62. return //rejects weird letters like �
  63. if(0 to 31)
  64. return //more weird stuff
  65. if(32)
  66. continue //whitespace
  67. else
  68. non_whitespace = 1
  69. if(non_whitespace)
  70. return text //only accepts the text if it has some non-spaces
  71. // Used to get a properly sanitized input, of max_length
  72. // no_trim is self explanatory but it prevents the input from being trimed if you intend to parse newlines or whitespace.
  73. /proc/stripped_input(mob/user, message = "", title = "", default = "", max_length=MAX_MESSAGE_LEN, no_trim=FALSE)
  74. var/name = input(user, message, title, default) as text|null
  75. if(no_trim)
  76. return copytext(html_encode(name), 1, max_length)
  77. else
  78. return trim(html_encode(name), max_length) //trim is "outside" because html_encode can expand single symbols into multiple symbols (such as turning < into &lt;)
  79. // Used to get a properly sanitized multiline input, of max_length
  80. /proc/stripped_multiline_input(mob/user, message = "", title = "", default = "", max_length=MAX_MESSAGE_LEN, no_trim=FALSE)
  81. var/name = input(user, message, title, default) as message|null
  82. if(no_trim)
  83. return copytext(html_encode(name), 1, max_length)
  84. else
  85. return trim(html_encode(name), max_length)
  86. //Filters out undesirable characters from names
  87. /proc/reject_bad_name(t_in, allow_numbers=0, max_length=MAX_NAME_LEN)
  88. if(!t_in || length(t_in) > max_length)
  89. return //Rejects the input if it is null or if it is longer then the max length allowed
  90. var/number_of_alphanumeric = 0
  91. var/last_char_group = 0
  92. var/t_out = ""
  93. for(var/i=1, i<=length(t_in), i++)
  94. var/ascii_char = text2ascii(t_in,i)
  95. switch(ascii_char)
  96. // A .. Z
  97. if(65 to 90) //Uppercase Letters
  98. t_out += ascii2text(ascii_char)
  99. number_of_alphanumeric++
  100. last_char_group = 4
  101. // a .. z
  102. if(97 to 122) //Lowercase Letters
  103. if(last_char_group<2)
  104. t_out += ascii2text(ascii_char-32) //Force uppercase first character
  105. else
  106. t_out += ascii2text(ascii_char)
  107. number_of_alphanumeric++
  108. last_char_group = 4
  109. // 0 .. 9
  110. if(48 to 57) //Numbers
  111. if(!last_char_group)
  112. continue //suppress at start of string
  113. if(!allow_numbers)
  114. continue
  115. t_out += ascii2text(ascii_char)
  116. number_of_alphanumeric++
  117. last_char_group = 3
  118. // ' - .
  119. if(39,45,46) //Common name punctuation
  120. if(!last_char_group)
  121. continue
  122. t_out += ascii2text(ascii_char)
  123. last_char_group = 2
  124. // ~ | @ : # $ % & * +
  125. if(126,124,64,58,35,36,37,38,42,43) //Other symbols that we'll allow (mainly for AI)
  126. if(!last_char_group)
  127. continue //suppress at start of string
  128. if(!allow_numbers)
  129. continue
  130. t_out += ascii2text(ascii_char)
  131. last_char_group = 2
  132. //Space
  133. if(32)
  134. if(last_char_group <= 1)
  135. continue //suppress double-spaces and spaces at start of string
  136. t_out += ascii2text(ascii_char)
  137. last_char_group = 1
  138. else
  139. return
  140. if(number_of_alphanumeric < 2)
  141. return //protects against tiny names like "A" and also names like "' ' ' ' ' ' ' '"
  142. if(last_char_group == 1)
  143. t_out = copytext(t_out,1,length(t_out)) //removes the last character (in this case a space)
  144. for(var/bad_name in list("space","floor","wall","r-wall","monkey","unknown","inactive ai")) //prevents these common metagamey names
  145. if(cmptext(t_out,bad_name))
  146. return //(not case sensitive)
  147. return t_out
  148. //html_encode helper proc that returns the smallest non null of two numbers
  149. //or 0 if they're both null (needed because of findtext returning 0 when a value is not present)
  150. /proc/non_zero_min(a, b)
  151. if(!a)
  152. return b
  153. if(!b)
  154. return a
  155. return (a < b ? a : b)
  156. /*
  157. * Text searches
  158. */
  159. //Checks the beginning of a string for a specified sub-string
  160. //Returns the position of the substring or 0 if it was not found
  161. /proc/dd_hasprefix(text, prefix)
  162. var/start = 1
  163. var/end = length(prefix) + 1
  164. return findtext(text, prefix, start, end)
  165. //Checks the beginning of a string for a specified sub-string. This proc is case sensitive
  166. //Returns the position of the substring or 0 if it was not found
  167. /proc/dd_hasprefix_case(text, prefix)
  168. var/start = 1
  169. var/end = length(prefix) + 1
  170. return findtextEx(text, prefix, start, end)
  171. //Checks the end of a string for a specified substring.
  172. //Returns the position of the substring or 0 if it was not found
  173. /proc/dd_hassuffix(text, suffix)
  174. var/start = length(text) - length(suffix)
  175. if(start)
  176. return findtext(text, suffix, start, null)
  177. return
  178. //Checks the end of a string for a specified substring. This proc is case sensitive
  179. //Returns the position of the substring or 0 if it was not found
  180. /proc/dd_hassuffix_case(text, suffix)
  181. var/start = length(text) - length(suffix)
  182. if(start)
  183. return findtextEx(text, suffix, start, null)
  184. //Checks if any of a given list of needles is in the haystack
  185. /proc/text_in_list(haystack, list/needle_list, start=1, end=0)
  186. for(var/needle in needle_list)
  187. if(findtext(haystack, needle, start, end))
  188. return 1
  189. return 0
  190. //Like above, but case sensitive
  191. /proc/text_in_list_case(haystack, list/needle_list, start=1, end=0)
  192. for(var/needle in needle_list)
  193. if(findtextEx(haystack, needle, start, end))
  194. return 1
  195. return 0
  196. //Adds 'u' number of zeros ahead of the text 't'
  197. /proc/add_zero(t, u)
  198. while (length(t) < u)
  199. t = "0[t]"
  200. return t
  201. //Adds 'u' number of spaces ahead of the text 't'
  202. /proc/add_lspace(t, u)
  203. while(length(t) < u)
  204. t = " [t]"
  205. return t
  206. //Adds 'u' number of spaces behind the text 't'
  207. /proc/add_tspace(t, u)
  208. while(length(t) < u)
  209. t = "[t] "
  210. return t
  211. //Returns a string with reserved characters and spaces before the first letter removed
  212. /proc/trim_left(text)
  213. for (var/i = 1 to length(text))
  214. if (text2ascii(text, i) > 32)
  215. return copytext(text, i)
  216. return ""
  217. //Returns a string with reserved characters and spaces after the last letter removed
  218. /proc/trim_right(text)
  219. for (var/i = length(text), i > 0, i--)
  220. if (text2ascii(text, i) > 32)
  221. return copytext(text, 1, i + 1)
  222. return ""
  223. //Returns a string with reserved characters and spaces before the first word and after the last word removed.
  224. /proc/trim(text, max_length)
  225. if(max_length)
  226. text = copytext(text, 1, max_length)
  227. return trim_left(trim_right(text))
  228. //Returns a string with the first element of the string capitalized.
  229. /proc/capitalize(t as text)
  230. return uppertext(copytext(t, 1, 2)) + copytext(t, 2)
  231. //Centers text by adding spaces to either side of the string.
  232. /proc/dd_centertext(message, length)
  233. var/new_message = message
  234. var/size = length(message)
  235. var/delta = length - size
  236. if(size == length)
  237. return new_message
  238. if(size > length)
  239. return copytext(new_message, 1, length + 1)
  240. if(delta == 1)
  241. return new_message + " "
  242. if(delta % 2)
  243. new_message = " " + new_message
  244. delta--
  245. var/spaces = add_lspace("",delta/2-1)
  246. return spaces + new_message + spaces
  247. //Limits the length of the text. Note: MAX_MESSAGE_LEN and MAX_NAME_LEN are widely used for this purpose
  248. /proc/dd_limittext(message, length)
  249. var/size = length(message)
  250. if(size <= length)
  251. return message
  252. return copytext(message, 1, length + 1)
  253. /proc/stringmerge(text,compare,replace = "*")
  254. //This proc fills in all spaces with the "replace" var (* by default) with whatever
  255. //is in the other string at the same spot (assuming it is not a replace char).
  256. //This is used for fingerprints
  257. var/newtext = text
  258. if(lentext(text) != lentext(compare))
  259. return 0
  260. for(var/i = 1, i < lentext(text), i++)
  261. var/a = copytext(text,i,i+1)
  262. var/b = copytext(compare,i,i+1)
  263. //if it isn't both the same letter, or if they are both the replacement character
  264. //(no way to know what it was supposed to be)
  265. if(a != b)
  266. if(a == replace) //if A is the replacement char
  267. newtext = copytext(newtext,1,i) + b + copytext(newtext, i+1)
  268. else if(b == replace) //if B is the replacement char
  269. newtext = copytext(newtext,1,i) + a + copytext(newtext, i+1)
  270. else //The lists disagree, Uh-oh!
  271. return 0
  272. return newtext
  273. /proc/stringpercent(text,character = "*")
  274. //This proc returns the number of chars of the string that is the character
  275. //This is used for detective work to determine fingerprint completion.
  276. if(!text || !character)
  277. return 0
  278. var/count = 0
  279. for(var/i = 1, i <= lentext(text), i++)
  280. var/a = copytext(text,i,i+1)
  281. if(a == character)
  282. count++
  283. return count
  284. /proc/reverse_text(text = "")
  285. var/new_text = ""
  286. for(var/i = length(text); i > 0; i--)
  287. new_text += copytext(text, i, i+1)
  288. return new_text
  289. GLOBAL_LIST_INIT(zero_character_only, list("0"))
  290. GLOBAL_LIST_INIT(hex_characters, list("0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"))
  291. GLOBAL_LIST_INIT(alphabet, list("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"))
  292. GLOBAL_LIST_INIT(binary, list("0","1"))
  293. /proc/random_string(length, list/characters)
  294. . = ""
  295. for(var/i=1, i<=length, i++)
  296. . += pick(characters)
  297. /proc/repeat_string(times, string="")
  298. . = ""
  299. for(var/i=1, i<=times, i++)
  300. . += string
  301. /proc/random_short_color()
  302. return random_string(3, GLOB.hex_characters)
  303. /proc/random_color()
  304. return random_string(6, GLOB.hex_characters)
  305. /proc/add_zero2(t, u)
  306. var/temp1
  307. while (length(t) < u)
  308. t = "0[t]"
  309. temp1 = t
  310. if (length(t) > u)
  311. temp1 = copytext(t,2,u+1)
  312. return temp1
  313. //merges non-null characters (3rd argument) from "from" into "into". Returns result
  314. //e.g. into = "Hello World"
  315. // from = "Seeya______"
  316. // returns"Seeya World"
  317. //The returned text is always the same length as into
  318. //This was coded to handle DNA gene-splicing.
  319. /proc/merge_text(into, from, null_char="_")
  320. . = ""
  321. if(!istext(into))
  322. into = ""
  323. if(!istext(from))
  324. from = ""
  325. var/null_ascii = istext(null_char) ? text2ascii(null_char,1) : null_char
  326. var/previous = 0
  327. var/start = 1
  328. var/end = length(into) + 1
  329. for(var/i=1, i<end, i++)
  330. var/ascii = text2ascii(from, i)
  331. if(ascii == null_ascii)
  332. if(previous != 1)
  333. . += copytext(from, start, i)
  334. start = i
  335. previous = 1
  336. else
  337. if(previous != 0)
  338. . += copytext(into, start, i)
  339. start = i
  340. previous = 0
  341. if(previous == 0)
  342. . += copytext(from, start, end)
  343. else
  344. . += copytext(into, start, end)
  345. //finds the first occurrence of one of the characters from needles argument inside haystack
  346. //it may appear this can be optimised, but it really can't. findtext() is so much faster than anything you can do in byondcode.
  347. //stupid byond :(
  348. /proc/findchar(haystack, needles, start=1, end=0)
  349. var/temp
  350. var/len = length(needles)
  351. for(var/i=1, i<=len, i++)
  352. temp = findtextEx(haystack, ascii2text(text2ascii(needles,i)), start, end) //Note: ascii2text(text2ascii) is faster than copytext()
  353. if(temp)
  354. end = temp
  355. return end
  356. /proc/parsemarkdown_basic_step1(t, limited=FALSE)
  357. if(length(t) <= 0)
  358. return
  359. // This parses markdown with no custom rules
  360. // Escape backslashed
  361. t = replacetext(t, "$", "$-")
  362. t = replacetext(t, "\\\\", "$1")
  363. t = replacetext(t, "\\**", "$2")
  364. t = replacetext(t, "\\*", "$3")
  365. t = replacetext(t, "\\__", "$4")
  366. t = replacetext(t, "\\_", "$5")
  367. t = replacetext(t, "\\^", "$6")
  368. t = replacetext(t, "\\((", "$7")
  369. t = replacetext(t, "\\))", "$8")
  370. t = replacetext(t, "\\|", "$9")
  371. t = replacetext(t, "\\%", "$0")
  372. // Escape single characters that will be used
  373. t = replacetext(t, "!", "$a")
  374. // Parse hr and small
  375. if(!limited)
  376. t = replacetext(t, "((", "<font size=\"1\">")
  377. t = replacetext(t, "))", "</font>")
  378. t = replacetext(t, regex("(-){3,}", "gm"), "<hr>")
  379. t = replacetext(t, regex("^\\((-){3,}\\)$", "gm"), "$1")
  380. // Parse lists
  381. var/list/tlist = splittext(t, "\n")
  382. var/tlistlen = tlist.len
  383. var/listlevel = -1
  384. var/singlespace = -1 // if 0, double spaces are used before asterisks, if 1, single are
  385. for(var/i = 1, i <= tlistlen, i++)
  386. var/line = tlist[i]
  387. var/count_asterisk = length(replacetext(line, regex("\[^\\*\]+", "g"), ""))
  388. if(count_asterisk % 2 == 1 && findtext(line, regex("^\\s*\\*", "g"))) // there is an extra asterisk in the beggining
  389. var/count_w = length(replacetext(line, regex("^( *)\\*.*$", "g"), "$1")) // whitespace before asterisk
  390. line = replacetext(line, regex("^ *(\\*.*)$", "g"), "$1")
  391. if(singlespace == -1 && count_w == 2)
  392. if(listlevel == 0)
  393. singlespace = 0
  394. else
  395. singlespace = 1
  396. if(singlespace == 0)
  397. count_w = count_w % 2 ? round(count_w / 2 + 0.25) : count_w / 2
  398. line = replacetext(line, regex("\\*", ""), "<li>")
  399. while(listlevel < count_w)
  400. line = "<ul>" + line
  401. listlevel++
  402. while(listlevel > count_w)
  403. line = "</ul>" + line
  404. listlevel--
  405. else while(listlevel >= 0)
  406. line = "</ul>" + line
  407. listlevel--
  408. tlist[i] = line
  409. // end for
  410. t = tlist[1]
  411. for(var/i = 2, i <= tlistlen, i++)
  412. t += "\n" + tlist[i]
  413. while(listlevel >= 0)
  414. t += "</ul>"
  415. listlevel--
  416. else
  417. t = replacetext(t, "((", "")
  418. t = replacetext(t, "))", "")
  419. // Parse headers
  420. t = replacetext(t, regex("^#(?!#) ?(.+)$", "gm"), "<h2>$1</h2>")
  421. t = replacetext(t, regex("^##(?!#) ?(.+)$", "gm"), "<h3>$1</h3>")
  422. t = replacetext(t, regex("^###(?!#) ?(.+)$", "gm"), "<h4>$1</h4>")
  423. t = replacetext(t, regex("^#### ?(.+)$", "gm"), "<h5>$1</h5>")
  424. // Parse most rules
  425. t = replacetext(t, regex("\\*(\[^\\*\]*)\\*", "g"), "<i>$1</i>")
  426. t = replacetext(t, regex("_(\[^_\]*)_", "g"), "<i>$1</i>")
  427. t = replacetext(t, "<i></i>", "!")
  428. t = replacetext(t, "</i><i>", "!")
  429. t = replacetext(t, regex("\\!(\[^\\!\]+)\\!", "g"), "<b>$1</b>")
  430. t = replacetext(t, regex("\\^(\[^\\^\]+)\\^", "g"), "<font size=\"4\">$1</font>")
  431. t = replacetext(t, regex("\\|(\[^\\|\]+)\\|", "g"), "<center>$1</center>")
  432. t = replacetext(t, "!", "</i><i>")
  433. return t
  434. /proc/parsemarkdown_basic_step2(t)
  435. if(length(t) <= 0)
  436. return
  437. // Restore the single characters used
  438. t = replacetext(t, "$a", "!")
  439. // Redo the escaping
  440. t = replacetext(t, "$1", "\\")
  441. t = replacetext(t, "$2", "**")
  442. t = replacetext(t, "$3", "*")
  443. t = replacetext(t, "$4", "__")
  444. t = replacetext(t, "$5", "_")
  445. t = replacetext(t, "$6", "^")
  446. t = replacetext(t, "$7", "((")
  447. t = replacetext(t, "$8", "))")
  448. t = replacetext(t, "$9", "|")
  449. t = replacetext(t, "$0", "%")
  450. t = replacetext(t, "$-", "$")
  451. return t
  452. /proc/parsemarkdown_basic(t, limited=FALSE)
  453. t = parsemarkdown_basic_step1(t, limited)
  454. t = parsemarkdown_basic_step2(t)
  455. return t
  456. /proc/parsemarkdown(t, mob/user=null, limited=FALSE)
  457. if(length(t) <= 0)
  458. return
  459. // Premanage whitespace
  460. t = replacetext(t, regex("\[^\\S\\r\\n \]", "g"), " ")
  461. t = parsemarkdown_basic_step1(t)
  462. t = replacetext(t, regex("%s(?:ign)?(?=\\s|$)", "igm"), user ? "<font face=\"[SIGNFONT]\"><i>[user.real_name]</i></font>" : "<span class=\"paper_field\"></span>")
  463. t = replacetext(t, regex("%f(?:ield)?(?=\\s|$)", "igm"), "<span class=\"paper_field\"></span>")
  464. t = parsemarkdown_basic_step2(t)
  465. // Manage whitespace
  466. t = replacetext(t, regex("(?:\\r\\n?|\\n)", "g"), "<br>")
  467. t = replacetext(t, " ", "&nbsp;&nbsp;")
  468. // Done
  469. return t
  470. #define string2charlist(string) (splittext(string, regex("(.)")) - splittext(string, ""))
  471. /proc/rot13(text = "")
  472. var/list/textlist = string2charlist(text)
  473. var/list/result = list()
  474. for(var/c in textlist)
  475. var/ca = text2ascii(c)
  476. if(ca >= text2ascii("a") && ca <= text2ascii("m"))
  477. ca += 13
  478. else if(ca >= text2ascii("n") && ca <= text2ascii("z"))
  479. ca -= 13
  480. else if(ca >= text2ascii("A") && ca <= text2ascii("M"))
  481. ca += 13
  482. else if(ca >= text2ascii("N") && ca <= text2ascii("Z"))
  483. ca -= 13
  484. result += ascii2text(ca)
  485. return jointext(result, "")
  486. //Takes a list of values, sanitizes it down for readability and character count,
  487. //then exports it as a json file at data/npc_saves/[filename].json.
  488. //As far as SS13 is concerned this is write only data. You can't change something
  489. //in the json file and have it be reflected in the in game item/mob it came from.
  490. //(That's what things like savefiles are for) Note that this list is not shuffled.
  491. /proc/twitterize(list/proposed, filename, cullshort = 1, storemax = 1000)
  492. if(!islist(proposed) || !filename || !CONFIG_GET(flag/log_twitter))
  493. return
  494. //Regular expressions are, as usual, absolute magic
  495. var/regex/all_invalid_symbols = new("\[^ -~]+")
  496. var/list/accepted = list()
  497. for(var/string in proposed)
  498. if(findtext(string,GLOB.is_website) || findtext(string,GLOB.is_email) || findtext(string,all_invalid_symbols) || !findtext(string,GLOB.is_alphanumeric))
  499. continue
  500. var/buffer = ""
  501. var/early_culling = TRUE
  502. for(var/pos = 1, pos <= lentext(string), pos++)
  503. var/let = copytext(string, pos, (pos + 1) % lentext(string))
  504. if(early_culling && !findtext(let,GLOB.is_alphanumeric))
  505. continue
  506. early_culling = FALSE
  507. buffer += let
  508. if(!findtext(buffer,GLOB.is_alphanumeric))
  509. continue
  510. var/punctbuffer = ""
  511. var/cutoff = lentext(buffer)
  512. for(var/pos = lentext(buffer), pos >= 0, pos--)
  513. var/let = copytext(buffer, pos, (pos + 1) % lentext(buffer))
  514. if(findtext(let,GLOB.is_alphanumeric))
  515. break
  516. if(findtext(let,GLOB.is_punctuation))
  517. punctbuffer = let + punctbuffer //Note this isn't the same thing as using +=
  518. cutoff = pos
  519. if(punctbuffer) //We clip down excessive punctuation to get the letter count lower and reduce repeats. It's not perfect but it helps.
  520. var/exclaim = FALSE
  521. var/question = FALSE
  522. var/periods = 0
  523. for(var/pos = lentext(punctbuffer), pos >= 0, pos--)
  524. var/punct = copytext(punctbuffer, pos, (pos + 1) % lentext(punctbuffer))
  525. if(!exclaim && findtext(punct,"!"))
  526. exclaim = TRUE
  527. if(!question && findtext(punct,"?"))
  528. question = TRUE
  529. if(!exclaim && !question && findtext(punct,"."))
  530. periods += 1
  531. if(exclaim)
  532. if(question)
  533. punctbuffer = "?!"
  534. else
  535. punctbuffer = "!"
  536. else if(question)
  537. punctbuffer = "?"
  538. else if(periods)
  539. if(periods > 1)
  540. punctbuffer = "..."
  541. else
  542. punctbuffer = "" //Grammer nazis be damned
  543. buffer = copytext(buffer, 1, cutoff) + punctbuffer
  544. if(!findtext(buffer,GLOB.is_alphanumeric))
  545. continue
  546. if(!buffer || lentext(buffer) > 280 || lentext(buffer) <= cullshort || buffer in accepted)
  547. continue
  548. accepted += buffer
  549. var/log = file("data/npc_saves/[filename].json") //If this line ever shows up as changed in a PR be very careful you aren't being memed on
  550. var/list/oldjson = list()
  551. var/list/oldentries = list()
  552. if(fexists(log))
  553. oldjson = json_decode(file2text(log))
  554. oldentries = oldjson["data"]
  555. if(!isemptylist(oldentries))
  556. for(var/string in accepted)
  557. for(var/old in oldentries)
  558. if(string == old)
  559. oldentries.Remove(old) //Line's position in line is "refreshed" until it falls off the in game radar
  560. break
  561. var/list/finalized = list()
  562. finalized = accepted.Copy() + oldentries.Copy() //we keep old and unreferenced phrases near the bottom for culling
  563. listclearnulls(finalized)
  564. if(!isemptylist(finalized) && length(finalized) > storemax)
  565. finalized.Cut(storemax + 1)
  566. fdel(log)
  567. var/list/tosend = list()
  568. tosend["data"] = finalized
  569. WRITE_FILE(log, json_encode(tosend))
  570. //Used for applying byonds text macros to strings that are loaded at runtime
  571. /proc/apply_text_macros(string)
  572. var/next_backslash = findtext(string, "\\")
  573. if(!next_backslash)
  574. return string
  575. var/leng = length(string)
  576. var/next_space = findtext(string, " ", next_backslash + 1)
  577. if(!next_space)
  578. next_space = leng - next_backslash
  579. if(!next_space) //trailing bs
  580. return string
  581. var/base = next_backslash == 1 ? "" : copytext(string, 1, next_backslash)
  582. var/macro = lowertext(copytext(string, next_backslash + 1, next_space))
  583. var/rest = next_backslash > leng ? "" : copytext(string, next_space + 1)
  584. //See https://secure.byond.com/docs/ref/info.html#/DM/text/macros
  585. switch(macro)
  586. //prefixes/agnostic
  587. if("the")
  588. rest = text("\the []", rest)
  589. if("a")
  590. rest = text("\a []", rest)
  591. if("an")
  592. rest = text("\an []", rest)
  593. if("proper")
  594. rest = text("\proper []", rest)
  595. if("improper")
  596. rest = text("\improper []", rest)
  597. if("roman")
  598. rest = text("\roman []", rest)
  599. //postfixes
  600. if("th")
  601. base = text("[]\th", rest)
  602. if("s")
  603. base = text("[]\s", rest)
  604. if("he")
  605. base = text("[]\he", rest)
  606. if("she")
  607. base = text("[]\she", rest)
  608. if("his")
  609. base = text("[]\his", rest)
  610. if("himself")
  611. base = text("[]\himself", rest)
  612. if("herself")
  613. base = text("[]\herself", rest)
  614. if("hers")
  615. base = text("[]\hers", rest)
  616. . = base
  617. if(rest)
  618. . += .(rest)
  619. //Replacement for the \th macro when you want the whole word output as text (first instead of 1st)
  620. /proc/thtotext(number)
  621. if(!isnum(number))
  622. return
  623. switch(number)
  624. if(1)
  625. return "first"
  626. if(2)
  627. return "second"
  628. if(3)
  629. return "third"
  630. if(4)
  631. return "fourth"
  632. if(5)
  633. return "fifth"
  634. if(6)
  635. return "sixth"
  636. if(7)
  637. return "seventh"
  638. if(8)
  639. return "eighth"
  640. if(9)
  641. return "ninth"
  642. if(10)
  643. return "tenth"
  644. if(11)
  645. return "eleventh"
  646. if(12)
  647. return "twelfth"
  648. else
  649. return "[number]\th"