utf8: use more lib func

utf8: use more lib func

util = require'util'

an = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.?!"'`()[]{}<>&_]]
charmaps = {
ci: 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl: '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'
ud: [[∀BƆDƎℲפHIſK˥WNOԀQRS┴∩ΛMX⅄Zɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz0ƖᄅƐㄣϛ9ㄥ86'˙¿¡,,,)(][}{><⅋‾]]
nc: [[🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿]]
sq: [[🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789,⊡]]
ns: [[🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉]]
ds: [[𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡]]
bo: [[𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗]]
bi: [[𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛0123456789]]
bs: [[𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃]]
pt: [[⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵0⑴⑵⑶⑷⑸⑹⑺⑻⑼]]
tl: [[ค๒ς๔єŦﻮђเןкl๓ภ๏קợгรtยשฬץאzค๒ς๔єŦﻮђเןкl๓ภ๏קợгรtยשฬץאz0123456789,.؟!"'`()[]{}«»&_]]
ru: [[ДЬCDЗFGHIJКLMИФPQЯSTЦVШЖУZДЪCDЭFGHIJКLMЙФPQЯSTЦVЩЖЧZ0123456789,.?!"'`()[]{}<>&_]]
}

codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

mimic = {
  " ": {" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "},
  ["!"]: {"！", "ǃ", "ⵑ", "︕", "﹗", },
  ["\""]: {"＂", },
  ["#"]: {"＃", "﹟", },
  ["$"]: {"＄", "﹩", },
  ["%"]: {"％", "٪", "⁒", "﹪", },
  ["&"]: {"＆", "﹠", },
  ["'"]: {"＇", "ʹ", },
  ["("]: {"（", "⟮", "﹙", },
  [")"]: {"）", "⟯", "﹚", },
  ["*"]: {"＊", "⋆", "﹡", },
  ["+"]: {"＋", "᛭", "﹢", },
  [","]: {"，", "ˏ", "ᛧ", "‚", },
  ["-"]: {"－", "˗", " ", "ᝍ", "᠆", "ᱼ", "−", "⎯", "⎼", "╴", "ⲻ", "ⲻ", "ー", "ㄧ", "﹣", },
  ["."]: {"．", "․", },
  ["/"]: {"／", "᜵", "⁄", "∕", "⧸", "Ⳇ", "〳", },
  ["0"]: {"᱐", },
  ["2"]: {"ᒿ", },
  ["3"]: {"Ʒ", "ᢃ", "ℨ", "Ⳅ", "Ⳍ", "ⳍ", },
  ["4"]: {"Ꮞ", },
  ["6"]: {"Ꮾ", },
  ["9"]: {"Ꮽ", },
  [":"]: {"：", "ː", "˸", "։", "፡", "᛬", "᠄", "ᱺ", "⁚", "∶", "⠆", "︓", "︰", "﹕", },
  [";"]: {"；", ";", "︔", "﹔", },
  ["<"]: {"＜", "˂", "‹", "≺", "❮", "ⵦ", "〱", "ㄑ", "﹤", },
  ["="]: {"＝", "═", "⚌", "゠", "﹦", },
  [">"]: {"＞", "˃", "›", "≻", "❯", "﹥", },
  ["?"]: {"？", "︖", "﹖", },
  ["@"]: {"＠", "﹫", },
  ["A"]: {"Α", "А", "Ꭺ", },
  ["B"]: {"Β", "В", "Ᏼ", "ᗷ", "Ⲃ", },
  ["C"]: {"Ϲ", "С", "Ꮯ", "Ⅽ", "Ⲥ", },
  ["D"]: {"Ꭰ", "ᗪ", "Ⅾ", },
  ["E"]: {"Ε", "Е", "Ꭼ", },
  ["F"]: {"ᖴ", },
  ["G"]: {"Ԍ", "Ꮐ", },
  ["H"]: {"Η", "Н", "ዘ", "Ꮋ", "ᕼ", "Ⲏ", },
  ["I"]: {"Ι", "І", "Ⅰ", },
  ["J"]: {"Ј", "Ꭻ", "ᒍ", },
  ["K"]: {"Κ", "Κ", "Ꮶ", "ᛕ", "K", "Ⲕ", },
  ["L"]: {"Ꮮ", "ᒪ", "Ⅼ", "Ⳑ", },
  ["M"]: {"Μ", "Ϻ", "М", "Ꮇ", "Ⅿ", },
  ["N"]: {"Ν", "Ⲛ", },
  ["O"]: {"Ο", "О", "ᱛ", "Ⲟ", },
  ["P"]: {"Ρ", "Р", "Ꮲ", "Ⲣ", },
  ["Q"]: {"Ԛ", "Ⴓ", "ⵕ", },
  ["R"]: {"Ꭱ", "Ꮢ", "ᖇ", },
  ["S"]: {"Ѕ", "Ⴝ", "Ꮪ", },
  ["T"]: {"Τ", "Т", "Ꭲ", },
  ["V"]: {"Ꮩ", "Ⅴ", },
  ["W"]: {"Ꮃ", "Ꮤ", },
  ["X"]: {"Χ", "Х", "Ⅹ", "Ⲭ", },
  ["Y"]: {"Υ", "Ⲩ", },
  ["Z"]: {"Ζ", "Ꮓ", },
  ["["]: {"［", },
  ["\\"]: {"＼", "∖", "⧵", "⧹", "〵", "﹨", },
  ["]"]: {"］", },
  ["^"]: {"＾", "˄", "ˆ", "ᶺ", "⌃", },
  ["_"]: {"＿", "ˍ", "⚊", "﹘", },
  ["`"]: {"｀", "ˋ", "`", "‵", },
  ["a"]: {"ɑ", "а", },
  ["c"]: {"ϲ", "с", "ⅽ", },
  ["d"]: {"ԁ", "ⅾ", },
  ["e"]: {"е", "ᥱ", },
  ["g"]: {"ɡ", },
  ["h"]: {"һ", },
  ["i"]: {"і", "ⅰ", },
  ["j"]: {"ϳ", "ј", },
  ["l"]: {"ⅼ", },
  ["m"]: {"ⅿ", },
  ["n"]: {"ᥒ", },
  ["o"]: {"ο", "о", "೦", "ഠ", "൦", "ᦞ", "᧐", "ⲟ", },
  ["p"]: {"р", "ⲣ", },
  ["s"]: {"ѕ", },
  ["u"]: {"ᥙ", "∪", },
  ["v"]: {"ᴠ", "ⅴ", "∨", "⋁", },
  ["w"]: {"ᴡ", },
  ["x"]: {"х", "ⅹ", "ⲭ", },
  ["y"]: {"у", "ỿ", },
  ["z"]: {"ᤁ", "ᴢ", },
  ["{"]: {"｛", "﹛", },
  ["|"]: {"｜", "ǀ", "ᛁ", "⎜", "⎟", "⎢", "⎥", "⎪", "⎮", "⼁", "〡", "丨", "︱", "︳", "￨", },
  ["}"]: {"｝", "﹜", },
  ["~"]: {"～", "˜", "⁓", "∼", "〜", },
}

-- Construct a table which can be used for lookup replacement later
-- Iterate over normal ascii and find counterparts in the weirdo strings as substrings
maps = {}
for charmap, chars in pairs charmaps
  i = 1
  maps[charmap] = {}
  for uchar in util.utf8.chars(chars)
    maps[charmap][an\sub(i,i)] = uchar
    i = i +1

wireplace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      -- Replace space width ideographic space for fullwidth offset
      if bc == 32 and offset == 0xFEE0
        t[#t + 1] = '\227\128\128'
      elseif bc == 32
        t[#t + 1] = ' '
      elseif bc < 0x80 then
        t[#t + 1] = util.utf8.char(offset + bc)
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

remap = (map, s) ->
  table.concat [map[s\sub(i,i)] or s\sub(i,i) for i=1, #s], ''

zalgo = (text, intensity=50) ->
  -- Limit text to prevent DoS
  text = text\sub(1,512)
  zalgo_chars = {}
  for i=0x0300, 0x036f
    zalgo_chars[i-0x2ff] = util.utf8.char(i)

  zalgo_chars[#zalgo_chars + 1] = util.utf8.char(0x0488)
  zalgo_chars[#zalgo_chars + 0] = util.utf8.char(0x0489)

  zalgoized = {}
  for letter in codepoints(text)
    zalgoized[#zalgoized + 1] = letter
    zalgo_num = math.random(1, intensity)
    for i=1, zalgo_num
      zalgoized[#zalgoized + 1] = zalgo_chars[math.random(1, #zalgo_chars)]
  table.concat(zalgoized)

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    say wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    say remap(maps.bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    say remap(maps.ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    say zalgo(arg, 7)
  '^%pupsidedown (.+)$': (source, destination, arg) =>
    say remap(maps.ud, util.utf8.reverse(arg))
  '^%pflip (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pthrow (.+)$': (source, destination, arg) =>
    say "（╯°□°）╯︵ #{remap maps.ud, util.utf8.reverse(arg)}"
  '^%pparanthesized (.+)$': (source, destination, arg) =>
    say remap(maps.pt, arg)
  '^%pnegcircle (.+)$': (source, destination, arg) =>
    say remap(maps['nc'], arg)
  '^%psquare (.+)$': (source, destination, arg) =>
    say remap(maps.sq, arg)
  '^%pnegsquare (.+)$': (source, destination, arg) =>
    say remap(maps.ns, arg)
  '^%pdoublestruck (.+)$': (source, destination, arg) =>
    say remap(maps.ds, arg)
  '^%pubold (.+)$': (source, destination, arg) =>
    say remap(maps.bo, arg)
  '^%pbolditalic (.+)$': (source, destination, arg) =>
    say remap(maps.bi, arg)
  '^%pboldscript (.+)$': (source, destination, arg) =>
    say remap(maps.bs, arg)
  '^%pthai (.+)$': (source, destination, arg) =>
    say remap(maps.tl, arg)
  '^%prussian (.+)$': (source, destination, arg) =>
    say remap(maps.ru, arg)
  '^%putfuk (.+)$': (source, destination, arg) =>
    keys = [x for x,_ in pairs(maps)]
    say table.concat([remap(maps[keys[math.random(#keys)]], letter) for letter in codepoints(arg)])
  '^%pmimic (.+)$': (source, destination, arg) =>
      out = {}
      for uchar in util.utf8.chars(arg)
        mimictbl =  mimic[uchar]
        if mimictbl
          out[#out+1] = mimictbl[math.random(#mimictbl)]
        else
          out[#out+1] = uchar
      say table.concat(out)
  '^%pul (.+)$': (source, destination, arg) =>
    -- combining low line 0332 => CC B2
    say table.concat([letter .. util.utf8.char(0x0332) for letter in codepoints(arg)])

util = require'util'
html2unicode = require'html'

an = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.?!"'`()[]{}<>&_]]
charmaps = {
ci: 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl: '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'
ud: [[∀BƆDƎℲפHIſK˥WNOԀQRS┴∩ΛMX⅄Zɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz0ƖᄅƐㄣϛ9ㄥ86'˙¿¡,,,)(][}{><⅋‾]]
nc: [[🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿]]
sq: [[🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789,⊡]]
ns: [[🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉]]
ds: [[𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡]]
bo: [[𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗]]
bi: [[𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛0123456789]]
bs: [[𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃]]
pt: [[⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵0⑴⑵⑶⑷⑸⑹⑺⑻⑼]]
tl: [[ค๒ς๔єŦﻮђเןкl๓ภ๏קợгรtยשฬץאzค๒ς๔єŦﻮђเןкl๓ภ๏קợгรtยשฬץאz0123456789,.؟!"'`()[]{}«»&_]]
ru: [[ДЬCDЗFGHIJКLMИФPQЯSTЦVШЖУZДЪCDЭFGHIJКLMЙФPQЯSTЦVЩЖЧZ0123456789,.?!"'`()[]{}<>&_]]
}

codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

mimic = {
  " ": {" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "},
  ["!"]: {"！", "ǃ", "ⵑ", "︕", "﹗", },
  ["\""]: {"＂", },
  ["#"]: {"＃", "﹟", },
  ["$"]: {"＄", "﹩", },
  ["%"]: {"％", "٪", "⁒", "﹪", },
  ["&"]: {"＆", "﹠", },
  ["'"]: {"＇", "ʹ", },
  ["("]: {"（", "⟮", "﹙", },
  [")"]: {"）", "⟯", "﹚", },
  ["*"]: {"＊", "⋆", "﹡", },
  ["+"]: {"＋", "᛭", "﹢", },
  [","]: {"，", "ˏ", "ᛧ", "‚", },
  ["-"]: {"－", "˗", " ", "ᝍ", "᠆", "ᱼ", "−", "⎯", "⎼", "╴", "ⲻ", "ⲻ", "ー", "ㄧ", "﹣", },
  ["."]: {"．", "․", },
  ["/"]: {"／", "᜵", "⁄", "∕", "⧸", "Ⳇ", "〳", },
  ["0"]: {"᱐", },
  ["2"]: {"ᒿ", },
  ["3"]: {"Ʒ", "ᢃ", "ℨ", "Ⳅ", "Ⳍ", "ⳍ", },
  ["4"]: {"Ꮞ", },
  ["6"]: {"Ꮾ", },
  ["9"]: {"Ꮽ", },
  [":"]: {"：", "ː", "˸", "։", "፡", "᛬", "᠄", "ᱺ", "⁚", "∶", "⠆", "︓", "︰", "﹕", },
  [";"]: {"；", ";", "︔", "﹔", },
  ["<"]: {"＜", "˂", "‹", "≺", "❮", "ⵦ", "〱", "ㄑ", "﹤", },
  ["="]: {"＝", "═", "⚌", "゠", "﹦", },
  [">"]: {"＞", "˃", "›", "≻", "❯", "﹥", },
  ["?"]: {"？", "︖", "﹖", },
  ["@"]: {"＠", "﹫", },
  ["A"]: {"Α", "А", "Ꭺ", },
  ["B"]: {"Β", "В", "Ᏼ", "ᗷ", "Ⲃ", },
  ["C"]: {"Ϲ", "С", "Ꮯ", "Ⅽ", "Ⲥ", },
  ["D"]: {"Ꭰ", "ᗪ", "Ⅾ", },
  ["E"]: {"Ε", "Е", "Ꭼ", },
  ["F"]: {"ᖴ", },
  ["G"]: {"Ԍ", "Ꮐ", },
  ["H"]: {"Η", "Н", "ዘ", "Ꮋ", "ᕼ", "Ⲏ", },
  ["I"]: {"Ι", "І", "Ⅰ", },
  ["J"]: {"Ј", "Ꭻ", "ᒍ", },
  ["K"]: {"Κ", "Κ", "Ꮶ", "ᛕ", "K", "Ⲕ", },
  ["L"]: {"Ꮮ", "ᒪ", "Ⅼ", "Ⳑ", },
  ["M"]: {"Μ", "Ϻ", "М", "Ꮇ", "Ⅿ", },
  ["N"]: {"Ν", "Ⲛ", },
  ["O"]: {"Ο", "О", "ᱛ", "Ⲟ", },
  ["P"]: {"Ρ", "Р", "Ꮲ", "Ⲣ", },
  ["Q"]: {"Ԛ", "Ⴓ", "ⵕ", },
  ["R"]: {"Ꭱ", "Ꮢ", "ᖇ", },
  ["S"]: {"Ѕ", "Ⴝ", "Ꮪ", },
  ["T"]: {"Τ", "Т", "Ꭲ", },
  ["V"]: {"Ꮩ", "Ⅴ", },
  ["W"]: {"Ꮃ", "Ꮤ", },
  ["X"]: {"Χ", "Х", "Ⅹ", "Ⲭ", },
  ["Y"]: {"Υ", "Ⲩ", },
  ["Z"]: {"Ζ", "Ꮓ", },
  ["["]: {"［", },
  ["\\"]: {"＼", "∖", "⧵", "⧹", "〵", "﹨", },
  ["]"]: {"］", },
  ["^"]: {"＾", "˄", "ˆ", "ᶺ", "⌃", },
  ["_"]: {"＿", "ˍ", "⚊", "﹘", },
  ["`"]: {"｀", "ˋ", "`", "‵", },
  ["a"]: {"ɑ", "а", },
  ["c"]: {"ϲ", "с", "ⅽ", },
  ["d"]: {"ԁ", "ⅾ", },
  ["e"]: {"е", "ᥱ", },
  ["g"]: {"ɡ", },
  ["h"]: {"һ", },
  ["i"]: {"і", "ⅰ", },
  ["j"]: {"ϳ", "ј", },
  ["l"]: {"ⅼ", },
  ["m"]: {"ⅿ", },
  ["n"]: {"ᥒ", },
  ["o"]: {"ο", "о", "೦", "ഠ", "൦", "ᦞ", "᧐", "ⲟ", },
  ["p"]: {"р", "ⲣ", },
  ["s"]: {"ѕ", },
  ["u"]: {"ᥙ", "∪", },
  ["v"]: {"ᴠ", "ⅴ", "∨", "⋁", },
  ["w"]: {"ᴡ", },
  ["x"]: {"х", "ⅹ", "ⲭ", },
  ["y"]: {"у", "ỿ", },
  ["z"]: {"ᤁ", "ᴢ", },
  ["{"]: {"｛", "﹛", },
  ["|"]: {"｜", "ǀ", "ᛁ", "⎜", "⎟", "⎢", "⎥", "⎪", "⎮", "⼁", "〡", "丨", "︱", "︳", "￨", },
  ["}"]: {"｝", "﹜", },
  ["~"]: {"～", "˜", "⁓", "∼", "〜", },
}

-- Construct a table which can be used for lookup replacement later
-- Iterate over normal ascii and find counterparts in the weirdo strings as substrings
maps = {}
for charmap, chars in pairs charmaps
  i = 1
  maps[charmap] = {}
  for uchar in util.utf8.chars(chars)
    maps[charmap][an\sub(i,i)] = uchar
    i = i +1

unichr = (n) ->
  html2unicode('&#x%x;'\format(n))

wireplace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      -- Replace space width ideographic space for fullwidth offset
      if bc == 32 and offset == 0xFEE0
        t[#t + 1] = '\227\128\128'
      elseif bc == 32
        t[#t + 1] = ' '
      elseif bc < 0x80 then
        t[#t + 1] = html2unicode("&#" .. (offset + bc) .. ";")
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

remap = (map, s) ->
  table.concat [map[s\sub(i,i)] or s\sub(i,i) for i=1, #s], ''

zalgo = (text, intensity=50) ->
  -- Limit text to prevent DoS
  text = text\sub(1,512)
  zalgo_chars = {}
  for i=0x0300, 0x036f
    zalgo_chars[i-0x2ff] = unichr(i)

  zalgo_chars[#zalgo_chars + 1] = unichr(0x0488)
  zalgo_chars[#zalgo_chars + 0] = unichr(0x0489)

  zalgoized = {}
  for letter in codepoints(text)
    zalgoized[#zalgoized + 1] = letter
    zalgo_num = math.random(1, intensity)
    for i=1, zalgo_num
      zalgoized[#zalgoized + 1] = zalgo_chars[math.random(1, #zalgo_chars)]
  table.concat(zalgoized)

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    say wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    say remap(maps.bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    say remap(maps.ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    say zalgo(arg, 7)
  '^%pupsidedown (.+)$': (source, destination, arg) =>
    say remap(maps.ud, util.utf8.reverse(arg))
  '^%pflip (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pthrow (.+)$': (source, destination, arg) =>
    say "（╯°□°）╯︵ #{remap maps.ud, util.utf8.reverse(arg)}"
  '^%pparanthesized (.+)$': (source, destination, arg) =>
    say remap(maps.pt, arg)
  '^%pnegcircle (.+)$': (source, destination, arg) =>
    say remap(maps['nc'], arg)
  '^%psquare (.+)$': (source, destination, arg) =>
    say remap(maps.sq, arg)
  '^%pnegsquare (.+)$': (source, destination, arg) =>
    say remap(maps.ns, arg)
  '^%pdoublestruck (.+)$': (source, destination, arg) =>
    say remap(maps.ds, arg)
  '^%pubold (.+)$': (source, destination, arg) =>
    say remap(maps.bo, arg)
  '^%pbolditalic (.+)$': (source, destination, arg) =>
    say remap(maps.bi, arg)
  '^%pboldscript (.+)$': (source, destination, arg) =>
    say remap(maps.bs, arg)
  '^%pthai (.+)$': (source, destination, arg) =>
    say remap(maps.tl, arg)
  '^%prussian (.+)$': (source, destination, arg) =>
    say remap(maps.ru, arg)
  '^%putfuk (.+)$': (source, destination, arg) =>
    keys = [x for x,_ in pairs(maps)]
    say table.concat([remap(maps[keys[math.random(#keys)]], letter) for letter in codepoints(arg)])
  '^%pmimic (.+)$': (source, destination, arg) =>
      out = {}
      for uchar in util.utf8.chars(arg)
        mimictbl =  mimic[uchar]
        if mimictbl
          out[#out+1] = mimictbl[math.random(#mimictbl)]
        else
          out[#out+1] = uchar
      say table.concat(out)
  '^%pul (.+)$': (source, destination, arg) =>
    -- combining low line 0332 => CC B2
    say table.concat([letter .. string.char(0xCC) .. string.char(0xB2) for letter in codepoints(arg)])


fixed yubikey

fixed yubikey

http = require "lapis.nginx.http"
import render_html from require "lapis.html"
import from_json,to_json,slugify, escape from require "lapis.util"
import encode_base64,hmac_sha1 from require "lapis.util.encoding"

UsersModel=require "lazuli.modules.user_management.models.users"
YubiCloudModel=require "lazuli.modules.user_management.models.yubicloud"

frandom = nil
ranval=->
  frandom or= assert io.open "/dev/urandom", "rb"
  s = frandom\read 4
  assert s\len! == 4
  v = 0
  for i = 1, 4
    v = 256 * v + s\byte i
  return v

class YubiCloud
  new: (@app,@modulename,@config) =>
    @apiurl = @config.apiurl or "http://api.yubico.com/wsapi/2.0/verify?id=1&otp=$OTP$&nonce=$NONCE$&timestamp=1"
    @required = @config.required or false
    @takeControlOfForm = @takeControlOfForm or true

  fillUrl: (otp,nonce) =>
    @apiurl\gsub("%$OTP%$",escape otp)\gsub("%$NONCE%$",escape nonce)

  tryLogin: (params) =>
    if params.yubikey
      params.yubikey=params.yubikey\lower!
      nonce=@mkNonce!
      query=@fillUrl params.yubikey, nonce
      res=http.simple query
      if res and res\find "status=OK", 1, true and res\find "nonce="..nonce, 1, true
        entry=YubiCloudModel\find idstr: params.yubikey\sub(-32)
        if entry
          user=entry\get_user!
          if user
            return user, res
          return nil, "OTP -> user resulution failed."
        return nil, "OTP unknown. idstr: "..params.yubikey\sub(-32)
      return nil, "invalid OTP, details: "..query.." => "..res
    if @required
      return nil, res
    return false, res

  mkNonce: =>
    now=os.time!
    hash=slugify now..encode_base64 hmac_sha1 now..ranval!,ranval!..now
    hash=hash\sub 1,20
    hash\lower!

  getLoginOkHtml:    => ""
  getLoginErrorHtml: => ""

  getLoginHtml: =>
    render_html ->
      div class: "pure-control-group yubigroup", ->
        label for: "yubikey", "YubiCloud:"
        input id: "yubikey", type: "yubikey", name: "yubikey", autocomplete: "off", placeholder: "YubiKey OTP", autofocus: @takeControlOfForm and "autofocus" or nil
        if @takeControlOfForm
          input type: "button", value: "X", id: "closeYubiKeyBtn"
      if @takeControlOfForm
        script ->
          raw [[
            document.getElementsByClassName("usernamegroup")[0].hidden=true
            document.getElementsByClassName("passwordgroup")[0].hidden=true
            document.getElementById("closeYubiKeyBtn").onclick=function(){
              document.getElementsByClassName("usernamegroup")[0].hidden=false
              document.getElementsByClassName("passwordgroup")[0].hidden=false
              document.getElementsByClassName("yubigroup")[0].hidden=true
              document.getElementById("username").focus()
            }
          ]]

http = require "lapis.nginx.http"
import render_html from require "lapis.html"
import from_json,to_json,slugify, escape from require "lapis.util"
import encode_base64,hmac_sha1 from require "lapis.util.encoding"

UsersModel=require "lazuli.modules.user_management.models.users"
YubiCloudModel=require "lazuli.modules.user_management.models.yubicloud"

frandom = nil
ranval=->
  frandom or= assert io.open "/dev/urandom", "rb"
  s = frandom\read 4
  assert s\len! == 4
  v = 0
  for i = 1, 4
    v = 256 * v + s\byte i
  return v

class YubiCloud
  new: (@app,@modulename,@config) =>
    @apiurl = @config.apiurl or "http://api.yubico.com/wsapi/2.0/verify?id=1&otp=$OTP$&nonce=$NONCE$&timestamp=1"
    @required = @config.required or false
    @takeControlOfForm = @takeControlOfForm or true

  fillUrl: (otp,nonce) =>
    @apiurl\gsub("%$OTP%$",escape otp)\gsub("%$NONCE%$",escape nonce)

  tryLogin: (params) =>
    if params.yubikey
      params.yubikey=params.yubikey\lower!
      nonce=@mkNonce!
      query=@fillUrl params.yubikey, nonce
      res=http.simple query
      if res and res\find "status=OK", 1, true and res\find "nonce="..nonce, 1, true
        entry=YubiCloudModel\find idstr: params.yubikey\lower!\sub(-32)
        if entry
          user=entry\get_user!
          if user
            return user, res
          return nil, "OTP -> user resulution failed."
        return nil, "OTP unknown."
      return nil, "invalid OTP, details: "..query.." => "..res
    if @required
      return nil, res
    return false, res

  mkNonce: =>
    now=os.time!
    hash=slugify now..encode_base64 hmac_sha1 now..ranval!,ranval!..now
    hash=hash\sub 1,20
    hash\lower!

  getLoginOkHtml:    => ""
  getLoginErrorHtml: => ""

  getLoginHtml: =>
    render_html ->
      div class: "pure-control-group yubigroup", ->
        label for: "yubikey", "YubiCloud:"
        input id: "yubikey", type: "yubikey", name: "yubikey", autocomplete: "off", placeholder: "YubiKey OTP", autofocus: @takeControlOfForm and "autofocus" or nil
        if @takeControlOfForm
          input type: "button", value: "X", id: "closeYubiKeyBtn"
      if @takeControlOfForm
        script ->
          raw [[
            document.getElementsByClassName("usernamegroup")[0].hidden=true
            document.getElementsByClassName("passwordgroup")[0].hidden=true
            document.getElementById("closeYubiKeyBtn").onclick=function(){
              document.getElementsByClassName("usernamegroup")[0].hidden=false
              document.getElementsByClassName("passwordgroup")[0].hidden=false
              document.getElementsByClassName("yubigroup")[0].hidden=true
              document.getElementById("username").focus()
            }
          ]]


Remove useless functions

Remove useless functions

--
-- Copyright (C) 2017-2020 DBotThePony

-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:

-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

net = DLib.net

class PPM2.NetworkChangeState
	new: (key = '', keyValid = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@keyValid = keyValid
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTimeL()
		@rtime = RealTimeL()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
		@cantApply = false
		@networkChange = true
	GetPlayer: => @ply
	ChangedByClient: => not @networkChange or IsValid(@ply)
	ChangedByPlayer: => not @networkChange or IsValid(@ply)
	ChangedByServer: => not @networkChange or not IsValid(@ply)
	GetKey: => @keyValid
	GetVariable: => @keyValid
	GetVar: => @keyValid
	GetKeyInternal: => @key
	GetVariableInternal: => @key
	GetVarInternal: => @key
	GetNewValue: => @newValue
	GetValue: => @newValue
	GetCantApply: => @cantApply
	SetCantApply: (val) => @cantApply = val
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTimeL: => @rtime
	GetRealTimeL: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len
	ChangedByNetwork: => @networkChange

	Revert: => @obj[@key] = @oldValue if not @cantApply
	Apply: => @obj[@key] = @newValue if not @cantApply

rFloat = (min = 0, max = 255) ->
	return -> math.Clamp(net.ReadFloat(), min, max)

wFloat = net.WriteFloat
rBool = net.ReadBool
wBool = net.WriteBool

if PPM2.NetworkedPonyData and PPM2.NetworkedPonyData.REGISTRY
	REGISTRY = PPM2.NetworkedPonyData.REGISTRY

	nullify = ->
		for _, ent in ipairs ents.GetAll()
			if ent.__PPM2_PonyData and ent.__PPM2_PonyData.Remove
				ProtectedCall -> ent.__PPM2_PonyData\Remove()

		for _, ent in ipairs ents.GetAll()
			ent.__PPM2_PonyData = nil

		for key, obj in pairs(REGISTRY)
			if obj.Remove
				ProtectedCall -> obj\Remove()

			REGISTRY[key] = nil

	nullify()

class NetworkedPonyData extends PPM2.ModifierBase
	@REGISTRY = {}

	@AddNetworkVar = (getName = 'Var', readFunc = (->), writeFunc = (->), defValue, onSet = ((val) => val), networkByDefault = true) =>
		defFunc = defValue
		defFunc = (-> defValue) if type(defValue) ~= 'function'
		strName = "_NW_#{getName}"
		@NW_NextVarID += 1
		id = @NW_NextVarID
		tab = {:strName, :readFunc, :getName, :writeFunc, :defValue, :defFunc, :id, :onSet}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[id] = tab
		@__base[strName] = defFunc()

		@__base["Get#{getName}"] = => @[strName]
		@__base["Set#{getName}"] = (val = defFunc(), networkNow = networkByDefault) =>
			oldVal = @[strName]
			nevVal = onSet(@, val)
			@[strName] = nevVal
			state = PPM2.NetworkChangeState(strName, getName, nevVal, @)
			state.networkChange = false
			@SetLocalChange(state)
			return unless networkNow and @NETWORKED and (CLIENT and @ent == LocalPlayer() or SERVER)
			net.Start(@@NW_Modify)
			net.WriteUInt(@GetNetworkID(), 16)
			net.WriteUInt(id, 16)
			writeFunc(nevVal)
			net.SendToServer() if CLIENT
			net.Broadcast() if SERVER

	@NetworkVar = (...) => @AddNetworkVar(...)
	@GetSet = (fname, fvalue) =>
		@__base["Get#{fname}"] = => @[fvalue]
		@__base["Set#{fname}"] = (fnewValue = @[fvalue]) =>
			oldVal = @[fvalue]
			@[fvalue] = fnewValue
			state = PPM2.NetworkChangeState(fvalue, fname, fnewValue, @)
			state.networkChange = false
			@SetLocalChange(state)

	if CLIENT
		hook.Add 'OnEntityCreated', 'PPM2.NW_WAIT', (ent) -> timer.Simple 0, ->
			return if not IsValid(ent)
			return if not @REGISTRY[ent\EntIndex()]
			return if @REGISTRY[ent\EntIndex()].done_setup or IsValid(@REGISTRY[ent\EntIndex()].ent)
			@REGISTRY[ent\EntIndex()]\SetupEntity(ent)

		hook.Add 'NotifyShouldTransmit', 'PPM2.NW_WAIT', (ent, should) ->
			return if not should
			return if not @REGISTRY[ent\EntIndex()]
			return if @REGISTRY[ent\EntIndex()].done_setup or IsValid(@REGISTRY[ent\EntIndex()].ent)
			@REGISTRY[ent\EntIndex()]\SetupEntity(ent)

		-- clientside entities
		hook.Add 'EntityRemoved', 'PPM2.NW_WAIT', (ent) -> @REGISTRY[ent]\Remove() if @REGISTRY[ent]

	@OnNetworkedCreated = (ply = NULL, len = 0, nwobj) =>
		if CLIENT
			netID = net.ReadUInt16()
			entid = net.ReadUInt16()
			obj = @NW_Objects[netID] or @(netID, entid)
			@REGISTRY[entid] = obj
			obj.NETWORKED = true
			obj.CREATED_BY_SERVER = true
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
		else
			ply[@NW_CooldownTimer] = ply[@NW_CooldownTimer] or 0
			ply[@NW_CooldownTimerCount] = ply[@NW_CooldownTimerCount] or 0

			if ply[@NW_CooldownTimer] < RealTimeL()
				ply[@NW_CooldownTimerCount] = 1
				ply[@NW_CooldownTimer] = RealTimeL() + 10
			else
				ply[@NW_CooldownTimerCount] += 1

			if ply[@NW_CooldownTimerCount] >= 3
				ply[@NW_CooldownMessage] = ply[@NW_CooldownMessage] or 0
				if ply[@NW_CooldownMessage] < RealTimeL()
					PPM2.Message 'Player ', ply, " is creating #{@__name} too quickly!"
					ply[@NW_CooldownMessage] = RealTimeL() + 1
				return

			waitID = net.ReadUInt16()
			obj = @(nil, ply)
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			obj\Create()
			timer.Simple 0.5, ->
				return if not IsValid(ply)
				net.Start(@NW_ReceiveID)
				net.WriteUInt(waitID, 16)
				net.WriteUInt(obj.netID, 16)
				net.Send(ply)

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]

		unless obj
			return if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		if IsValid(ply) and obj.ent ~= ply
			error('Invalid realm for player being specified. If you are running on your own net.* library, check up your code') if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		varID = net.ReadUInt16()
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :getName, :readFunc, :writeFunc, :onSet} = varData
		newVal = onSet(obj, readFunc())
		return if newVal == obj["Get#{getName}"](obj)
		state = PPM2.NetworkChangeState(strName, getName, newVal, obj, len, ply)
		state\Apply()
		obj\NetworkDataChanges(state)
		if SERVER
			net.Start(@NW_Modify)
			net.WriteUInt(id, 16)
			net.WriteUInt(varID, 16)
			writeFunc(newVal)
			net.SendOmit(ply)

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove(true)

	@ReadNetworkData = =>
		output = {strName, {getName, readFunc()} for _, {:getName, :strName, :readFunc} in ipairs @NW_Vars}
		return output

	@RenderTasks = {}
	@CheckTasks = {}

	@NW_Vars = {}
	@NW_VarsTable = {}
	@NW_Objects = {}
	@O_Slots = {} if CLIENT
	@NW_Waiting = {}
	@NW_WaitID = -1
	@NW_NextVarID = -1
	@NW_Create = 'PPM2.NW.Created'
	@NW_Modify = 'PPM2.NW.Modified'
	@NW_Broadcast = 'PPM2.NW.ModifiedBroadcast'
	@NW_Remove = 'PPM2.NW.Removed'
	@NW_Rejected = 'PPM2.NW.Rejected'
	@NW_ReceiveID = 'PPM2.NW.ReceiveID'
	@NW_CooldownTimerCount = 'ppm2_NW_CooldownTimerCount'
	@NW_CooldownTimer = 'ppm2_NW_CooldownTimer'
	@NW_CooldownMessage = 'ppm2_NW_CooldownMessage'
	@NW_NextObjectID = 0
	@NW_NextObjectID_CL = 0x60000

	if SERVER
		net.pool(@NW_Create)
		net.pool(@NW_Modify)
		net.pool(@NW_Remove)
		net.pool(@NW_ReceiveID)
		net.pool(@NW_Rejected)
		net.pool(@NW_Broadcast)

	net.Receive @NW_Create, (len = 0, ply = NULL, obj) -> @OnNetworkedCreated(ply, len, obj)
	net.Receive @NW_Modify, (len = 0, ply = NULL, obj) -> @OnNetworkedModify(ply, len, obj)
	net.Receive @NW_Remove, (len = 0, ply = NULL, obj) -> @OnNetworkedDelete(ply, len, obj)

	if SERVER
		net.ReceiveAntispam(@NW_Create)
		net.ReceiveAntispam(@NW_Remove)

	net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
		return if SERVER
		waitID = net.ReadUInt16()
		netID = net.ReadUInt16()
		obj = @NW_Waiting[waitID]
		@NW_Waiting[waitID] = nil
		return unless obj
		obj.NETWORKED = true
		@NW_Objects[obj.netID] = nil
		obj.netID = netID
		obj.waitID = nil
		@NW_Objects[netID] = obj

	net.Receive @NW_Rejected, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		return if obj.__LastReject and obj.__LastReject > RealTimeL()
		obj.__LastReject = RealTimeL() + 3
		obj.NETWORKED = false
		obj\Create()

	net.Receive @NW_Broadcast, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		obj\ReadNetworkData(len, ply)

	@GetSet('UpperManeModel', 'm_upperManeModel')
	@GetSet('LowerManeModel', 'm_lowerManeModel')
	@GetSet('TailModel', 'm_tailModel')
	@GetSet('SocksModel', 'm_socksModel')
	@GetSet('HornModel', 'm_hornmodel')
	@GetSet('ClothesModel', 'm_clothesmodel')
	@GetSet('NewSocksModel', 'm_newSocksModel')

	-- @NetworkVar('Fly',                  rBool,   wBool,                 false)
	@NetworkVar('DisableTask',          rBool,   wBool,                 false)
	@NetworkVar('UseFlexLerp',          rBool,   wBool,                  true)
	@NetworkVar('FlexLerpMultiplier',   rFloat(0, 10),  wFloat,             1)

	@SetupModifiers: =>
		for key, value in pairs PPM2.PonyDataRegistry
			if value.modifiers
				@RegisterModifier(value.getFunc, 0, 0)
				@SetModifierMinMaxFinal(value.getFunc, value.min, value.max) if value.min or value.max
				@SetupLerpTables(value.getFunc)
				strName = '_NW_' .. value.getFunc
				funcLerp = 'Calculate' .. value.getFunc
				@__base['Get' .. value.getFunc] = => @[funcLerp](@, @[strName])

	for key, value in pairs PPM2.PonyDataRegistry
		@NetworkVar(value.getFunc, value.read, value.write, value.default)

	new: (netID, ent) =>
		super()

		@m_upperManeModel = Entity(-1)
		@m_lowerManeModel = Entity(-1)
		@m_tailModel = Entity(-1)
		@m_socksModel = Entity(-1)
		@m_newSocksModel = Entity(-1)

		@recomputeTextures = true
		@isValid = true
		@removed = false
		@valid = true
		@NETWORKED = false
		@NETWORKED_PREDICT = false

		@[data.strName] = data.defFunc() for _, data in ipairs @@NW_Vars when data.defFunc

		if SERVER
			@netID = @@NW_NextObjectID
			@@NW_NextObjectID += 1
		else
			netID = -1 if netID == nil
			@netID = netID

		@@NW_Objects[@netID] = @

		if CLIENT
			for i = 1, 1024
				if not @@O_Slots[i]
					@slotID = i
					@@O_Slots[i] = @
					break

			error('no more free pony data edicts') if not @slotID

		@entID = isnumber(ent) and ent or ent\EntIndex()
		ent = Entity(ent) if isnumber(ent)

		return if not IsValid(ent)
		@ent = ent
		@modelCached = ent\GetModel() if IsValid(ent)
		@SetupEntity(ent)

	GetEntity: => @ent or NULL
	IsValid: => @isValid
	GetModel: => @modelCached
	EntIndex: => @entID
	ObjectSlot: => @slotID
	GetObjectSlot: => @slotID

	Clone: (target = @ent) =>
		copy = @@(nil, target)
		@ApplyDataToObject(copy)
		return copy

	SetupEntity: (ent) =>
		if @@REGISTRY[ent]
			return if @@REGISTRY[ent]\GetOwner() and IsValid(@@REGISTRY[ent]\GetOwner()) and @@REGISTRY[ent]\GetOwner() ~= @GetOwner()
			@@REGISTRY[ent]\Remove() if @@REGISTRY[ent].Remove and @@REGISTRY[ent] ~= @

		return unless IsValid(ent)

		if @@REGISTRY[ent\EntIndex()]
			return if @@REGISTRY[ent\EntIndex()]\GetOwner() and IsValid(@@REGISTRY[ent\EntIndex()]\GetOwner()) and @@REGISTRY[ent\EntIndex()]\GetOwner() ~= @GetOwner()
			@@REGISTRY[ent\EntIndex()]\Remove() if @@REGISTRY[ent\EntIndex()].Remove and @@REGISTRY[ent\EntIndex()] ~= @

		@ent = ent
		@done_setup = true
		ent\SetPonyData(@)
		@entTable = @ent\GetTable()

		@modelCached = ent\GetModel()
		ent\PPMBonesModifier()
		@flightController = PPM2.PonyflyController(@) if not @flightController
		@entID = ent\EntIndex() if not @entID
		@lastLerpThink = RealTimeL() if not @lastLerpThink

		@ModelChanges(@modelCached, @modelCached)
		@Reset()

		timer.Simple(0, -> @GetRenderController()\CompileTextures() if @GetRenderController()) if CLIENT

		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	ModelChanges: (old = @ent\GetModel(), new = old) =>
		@modelCached = new

		@ent\SetNW2Bool('ppm2_fly', false)

		timer.Simple 0.5, ->
			return unless IsValid(@ent)
			@Reset()

	GenericDataChange: (state) =>
		hook.Run 'PPM2_PonyDataChanges', @ent, @, state

		if state\GetKey() == 'DisableTask'
			@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
			@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

		@GetSizeController()\DataChanges(state) if @ent and @GetBodygroupController()
		@GetBodygroupController()\DataChanges(state) if @ent and @GetBodygroupController()

		@GetWeightController()\DataChanges(state) if @ent and @GetWeightController()

		if CLIENT and @ent
			@GetRenderController()\DataChanges(state) if @GetRenderController()

	ResetScale: =>
		if scale = @GetSizeController()
			scale\ResetScale()

	ModifyScale: =>
		if scale = @GetSizeController()
			scale\ModifyScale()

	GetIsEditor: => @is_editor_data or false
	SetIsEditor: (value = false) => @is_editor_data = value

	Reset: =>
		if scale = @GetSizeController()
			scale\Reset()
		if CLIENT
			@GetWeightController()\Reset() if @GetWeightController() and @GetWeightController().Reset
			@GetRenderController()\Reset() if @GetRenderController() and @GetRenderController().Reset
			@GetBodygroupController()\Reset() if @GetBodygroupController() and @GetBodygroupController().Reset

	GetHoofstepVolume: =>
		return 0.8 if @ShouldMuffleHoosteps()
		return 1

	ShouldMuffleHoosteps: =>
		return @GetSocksAsModel() or @GetSocksAsNewModel()

	PlayerRespawn: =>
		return if not IsValid(@ent)
		@entTable.__cachedIsPony = @ent\IsPony()
		if not @entTable.__cachedIsPony
			return if @alreadyCalledRespawn
			@alreadyCalledRespawn = true
			@alreadyCalledDeath = true
		else
			@alreadyCalledRespawn = false
			@alreadyCalledDeath = false
		@ApplyBodygroups(CLIENT, true)

		@ent\SetNW2Bool('ppm2_fly', false)

		@ent\PPMBonesModifier()

		if scale = @GetSizeController()
			scale\PlayerRespawn()

		if weight = @GetWeightController()
			weight\PlayerRespawn()

		if CLIENT
			@deathRagdollMerged = false
			@GetRenderController()\PlayerRespawn() if @GetRenderController()
			@GetBodygroupController()\MergeModels(@ent) if IsValid(@ent) and @GetBodygroupController().MergeModels

	PlayerDeath: =>
		return if not IsValid(@ent)

		if @ent.__ppmBonesModifiers
			@ent.__ppmBonesModifiers\Remove()

		@entTable.__cachedIsPony = @ent\IsPony()

		if not @entTable.__cachedIsPony
			return if @alreadyCalledDeath
			@alreadyCalledDeath = true
		else
			@alreadyCalledDeath = false

		@ent\SetNW2Bool('ppm2_fly', false)

		if scale = @GetSizeController()
			scale\PlayerDeath()

		if weight = @GetWeightController()
			weight\PlayerDeath()

		if CLIENT
			@DoRagdollMerge()
			@GetRenderController()\PlayerDeath() if @GetRenderController()

	DoRagdollMerge: =>
		return if @deathRagdollMerged
		bgController = @GetBodygroupController()
		rag = @ent\GetRagdollEntity()

		if not bgController.MergeModels
			@deathRagdollMerged = true
		elseif IsValid(rag)
			@deathRagdollMerged = true
			bgController\MergeModels(rag)

	ApplyBodygroups: (updateModels = CLIENT) => @GetBodygroupController()\ApplyBodygroups(updateModels) if @ent
	SetLocalChange: (state) => @GenericDataChange(state)
	NetworkDataChanges: (state) => @GenericDataChange(state)

	GetPonyRaceFlags: =>
		switch @GetRace()
			when PPM2.RACE_EARTH
				return 0
			when PPM2.RACE_PEGASUS
				return PPM2.RACE_HAS_WINGS
			when PPM2.RACE_UNICORN
				return PPM2.RACE_HAS_HORN
			when PPM2.RACE_ALICORN
				return PPM2.RACE_HAS_HORN + PPM2.RACE_HAS_WINGS

	SlowUpdate: =>
		@GetBodygroupController()\SlowUpdate() if @GetBodygroupController()
		@GetWeightController()\SlowUpdate() if @GetWeightController()
		if scale = @GetSizeController()
			scale\SlowUpdate()

		if SERVER and IsValid(@ent) and @ent\IsPlayer()
			arms = @ent\GetHands()

			if IsValid(arms)
				cond = @GetPonyRaceFlags()\band(PPM2.RACE_HAS_HORN) ~= 0 and @ent\GetInfoBool('ppm2_cl_vm_magic_hands', true) and PPM2.HAND_BODYGROUP_MAGIC or PPM2.HAND_BODYGROUP_HOOVES
				arms\SetBodygroup(PPM2.HAND_BODYGROUP_ID, cond)

	Think: =>
	RenderScreenspaceEffects: =>
		time = RealTimeL()
		delta = time - @lastLerpThink
		@lastLerpThink = time
		if @isValid and IsValid(@ent)
			for _, change in ipairs @TriggerLerpAll(delta * 5)
				state = PPM2.NetworkChangeState('_NW_' .. change[1], change[1], change[2] + @['_NW_' .. change[1]], @)
				state\SetCantApply(true)
				@GenericDataChange(state)

	GetFlightController: => @flightController

	GetRenderController: =>
		return if SERVER
		return @renderController if not @isValid
		if not @renderController or @modelCached ~= @modelRender
			@modelRender = @modelCached
			cls = PPM2.GetRenderController(@modelCached)
			if @renderController and cls == @renderController.__class
				@renderController.ent = @ent
				PPM2.DebugPrint('Skipping render controller recreation for ', @ent, ' as part of ', @)
				return @renderController
			@renderController\Remove() if @renderController
			@renderController = cls(@)
		@renderController.ent = @ent
		return @renderController

	GetWeightController: =>
		return @weightController if not @isValid
		if not @weightController or @modelCached ~= @modelWeight
			@modelCached = @modelCached or @ent\GetModel()
			@modelWeight = @modelCached
			cls = PPM2.GetPonyWeightController(@modelCached)
			if @weightController and cls == @weightController.__class
				@weightController.ent = @ent
				PPM2.DebugPrint('Skipping weight controller recreation for ', @ent, ' as part of ', @)
				return @weightController
			@weightController\Remove() if @weightController
			@weightController = cls(@)
		@weightController.ent = @ent
		return @weightController

	GetSizeController: =>
		return @scaleController if not @isValid
		if not @scaleController or @modelCached ~= @modelScale
			@modelCached = @modelCached or @ent\GetModel()
			@modelScale = @modelCached
			cls = PPM2.GetSizeController(@modelCached)
			if @scaleController and cls == @scaleController.__class
				@scaleController.ent = @ent
				PPM2.DebugPrint('Skipping size controller recreation for ', @ent, ' as part of ', @)
				return @scaleController
			@scaleController\Remove() if @scaleController
			@scaleController = cls(@)
		@scaleController.ent = @ent
		return @scaleController
	GetScaleController: => @GetSizeController()

	GetBodygroupController: =>
		return @bodygroups if not @isValid
		if not @bodygroups or @modelBodygroups ~= @modelCached
			@modelCached = @modelCached or @ent\GetModel()
			@modelBodygroups = @modelCached
			cls = PPM2.GetBodygroupController(@modelCached)
			if @bodygroups and cls == @bodygroups.__class
				@bodygroups.ent = @ent
				PPM2.DebugPrint('Skipping bodygroup controller recreation for ', @ent, ' as part of ', @)
				return @bodygroups
			@bodygroups\Remove() if @bodygroups
			@bodygroups = cls(@)
		@bodygroups.ent = @ent
		return @bodygroups

	Remove: (byClient = false) =>
		@removed = true
		@@NW_Objects[@netID] = nil if SERVER or @NETWORKED
		@@O_Slots[@slotID] = nil if @slotID
		@isValid = false
		@ent = @GetEntity() if not IsValid(@ent)
		@@REGISTRY[@ent] = nil if IsValid(@ent) and @@REGISTRY[@ent] == @
		@@REGISTRY[@entID] = nil if @@REGISTRY[@entID] == @
		@GetWeightController()\Remove() if @GetWeightController()

		if CLIENT
			@GetRenderController()\Remove() if @GetRenderController()
			if IsValid(@ent) and @ent.__ppm2_task_hit
				@entTable.__ppm2_task_hit = false
				@ent\SetNoDraw(false)

		@GetBodygroupController()\Remove() if @GetBodygroupController()
		@GetSizeController()\Remove() if @GetSizeController()

		@flightController\Switch(false) if @flightController
		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	__tostring: => "[#{@@__name}:#{@netID}|#{@ent}]"

	GetOwner: => @ent
	IsNetworked: => @NETWORKED
	IsGoingToNetwork: => @NETWORKED_PREDICT
	SetIsGoingToNetwork: (val = @NETWORKED) => @NETWORKED_PREDICT = val
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID

	ComputeMagicColor: =>
		color = @GetHornMagicColor()

		if not @GetSeparateMagicColor()
			if not @GetSeparateEyes()
				color = @GetEyeIrisTop()\Lerp(0.5, @GetEyeIrisBottom())
			else
				lerpLeft = @GetEyeIrisTopLeft()\Lerp(0.5, @GetEyeIrisBottomLeft())
				lerpRight = @GetEyeIrisTopRight()\Lerp(0.5, @GetEyeIrisBottomRight())
				color = lerpLeft\Lerp(0.5, lerpRight)

		return color

	ReadNetworkData: (len = 24, ply = NULL, silent = false, applyEntities = true) =>
		data = @@ReadNetworkData()
		validPly = IsValid(ply)
		states = [PPM2.NetworkChangeState(key, keyValid, newVal, @, len, ply) for key, {keyValid, newVal} in pairs data]
		for _, state in ipairs states
			if not validPly or applyEntities or not isentity(state\GetValue())
				state\Apply()
				@NetworkDataChanges(state) unless silent

	NetworkedIterable: (grabEntities = true) =>
		data = [{getName, @[strName]} for _, {:strName, :getName} in ipairs @@NW_Vars when grabEntities or not isentity(@[strName])]
		return data

	ApplyDataToObject: (target, applyEntities = false) =>
		target["Set#{key}"](target, value) for {key, value} in *@NetworkedIterable(applyEntities) when target["Set#{key}"]
		return target

	WriteNetworkData: => writeFunc(@[strName]) for _, {:strName, :writeFunc} in ipairs @@NW_Vars

	ReBroadcast: =>
		return false if not @NETWORKED
		return false if CLIENT
		net.Start(@@NW_Broadcast)
		net.WriteUInt16(@netID)
		@WriteNetworkData()
		net.Broadcast()
		return true

	Create: =>
		return if @NETWORKED
		return if CLIENT and @CREATED_BY_SERVER -- wtf
		@NETWORKED = true if SERVER
		@NETWORKED_PREDICT = true

		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt16(@netID)
			net.WriteEntity(@ent)
			@WriteNetworkData()
			filter = RecipientFilter()
			filter\AddAllPlayers()
			filter\RemovePlayer(@ent) if IsValid(@ent) and @ent\IsPlayer()
			net.Send(filter)
		else
			@@NW_WaitID += 1
			@waitID = @@NW_WaitID

			net.Start(@@NW_Create)
			before = net.BytesWritten()

			net.WriteUInt16(@waitID)
			@WriteNetworkData()
			after = net.BytesWritten()

			net.SendToServer()
			@@NW_Waiting[@waitID] = @
			return after - before

	NetworkTo: (targets = {}) =>
		net.Start(@@NW_Create)
		net.WriteUInt16(@netID)
		net.WriteEntity(@ent)
		@WriteNetworkData()
		net.Send(targets)

PPM2.NetworkedPonyData = NetworkedPonyData

if CLIENT
	net.Receive 'PPM2.NotifyDisconnect', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()

	net.Receive 'PPM2.PonyDataRemove', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()
else
	hook.Add 'PlayerJoinTeam', 'PPM2.TeamWaypoint', (ply, new) ->
		ply.__ppm2_modified_jump = false
	hook.Add 'OnPlayerChangedTeam', 'PPM2.TeamWaypoint', (ply, old, new) ->
		ply.__ppm2_modified_jump = false

_G.LocalPonyData = () -> LocalPlayer()\GetPonyData()
_G.LocalPonydata = () -> LocalPlayer()\GetPonyData()

do
	REGISTRY = NetworkedPonyData.REGISTRY

	nullify = ->
		for _, ent in ipairs ents.GetAll()
			if ent.__PPM2_PonyData and ent.__PPM2_PonyData.Remove
				ProtectedCall -> ent.__PPM2_PonyData\Remove()

		for _, ent in ipairs ents.GetAll()
			ent.__PPM2_PonyData = nil

		for key, obj in pairs(REGISTRY)
			if obj.Remove
				ProtectedCall -> obj\Remove()

			REGISTRY[key] = nil

	nullify()

	entMeta = FindMetaTable('Entity')
	EntIndex = entMeta.EntIndex

	entMeta.GetPonyData = =>
		index = EntIndex(@)
		return REGISTRY[index] if index > 0
		return REGISTRY[@]

	entMeta.SetPonyData = (data) =>
		index = EntIndex(@)

		if index > 0
			REGISTRY[index] = data
			return

		REGISTRY[@] = data

--
-- Copyright (C) 2017-2020 DBotThePony

-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:

-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

net = DLib.net

class PPM2.NetworkChangeState
	new: (key = '', keyValid = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@keyValid = keyValid
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTimeL()
		@rtime = RealTimeL()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
		@cantApply = false
		@networkChange = true
	GetPlayer: => @ply
	ChangedByClient: => not @networkChange or IsValid(@ply)
	ChangedByPlayer: => not @networkChange or IsValid(@ply)
	ChangedByServer: => not @networkChange or not IsValid(@ply)
	GetKey: => @keyValid
	GetVariable: => @keyValid
	GetVar: => @keyValid
	GetKeyInternal: => @key
	GetVariableInternal: => @key
	GetVarInternal: => @key
	GetNewValue: => @newValue
	GetValue: => @newValue
	GetCantApply: => @cantApply
	SetCantApply: (val) => @cantApply = val
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTimeL: => @rtime
	GetRealTimeL: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len
	ChangedByNetwork: => @networkChange

	Revert: => @obj[@key] = @oldValue if not @cantApply
	Apply: => @obj[@key] = @newValue if not @cantApply

wUInt = (def = 0, size = 8) ->
	return (arg = def) -> net.WriteUInt(arg, size)

wInt = (def = 0, size = 8) ->
	return (arg = def) -> net.WriteInt(arg, size)

rUInt = (size = 8, min = 0, max = 255) ->
	return -> math.Clamp(net.ReadUInt(size), min, max)

rInt = (size = 8, min = -128, max = 127) ->
	return -> math.Clamp(net.ReadInt(size), min, max)

rFloat = (min = 0, max = 255) ->
	return -> math.Clamp(net.ReadFloat(), min, max)

wFloat = net.WriteFloat
rBool = net.ReadBool
wBool = net.WriteBool
rColor = net.ReadColor
wColor = net.WriteColor
rString = net.ReadString
wString = net.WriteString

if PPM2.NetworkedPonyData and PPM2.NetworkedPonyData.REGISTRY
	REGISTRY = PPM2.NetworkedPonyData.REGISTRY

	nullify = ->
		for _, ent in ipairs ents.GetAll()
			if ent.__PPM2_PonyData and ent.__PPM2_PonyData.Remove
				ProtectedCall -> ent.__PPM2_PonyData\Remove()

		for _, ent in ipairs ents.GetAll()
			ent.__PPM2_PonyData = nil

		for key, obj in pairs(REGISTRY)
			if obj.Remove
				ProtectedCall -> obj\Remove()

			REGISTRY[key] = nil

	nullify()

class NetworkedPonyData extends PPM2.ModifierBase
	@REGISTRY = {}

	@AddNetworkVar = (getName = 'Var', readFunc = (->), writeFunc = (->), defValue, onSet = ((val) => val), networkByDefault = true) =>
		defFunc = defValue
		defFunc = (-> defValue) if type(defValue) ~= 'function'
		strName = "_NW_#{getName}"
		@NW_NextVarID += 1
		id = @NW_NextVarID
		tab = {:strName, :readFunc, :getName, :writeFunc, :defValue, :defFunc, :id, :onSet}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[id] = tab
		@__base[strName] = defFunc()

		@__base["Get#{getName}"] = => @[strName]
		@__base["Set#{getName}"] = (val = defFunc(), networkNow = networkByDefault) =>
			oldVal = @[strName]
			nevVal = onSet(@, val)
			@[strName] = nevVal
			state = PPM2.NetworkChangeState(strName, getName, nevVal, @)
			state.networkChange = false
			@SetLocalChange(state)
			return unless networkNow and @NETWORKED and (CLIENT and @ent == LocalPlayer() or SERVER)
			net.Start(@@NW_Modify)
			net.WriteUInt(@GetNetworkID(), 16)
			net.WriteUInt(id, 16)
			writeFunc(nevVal)
			net.SendToServer() if CLIENT
			net.Broadcast() if SERVER

	@NetworkVar = (...) => @AddNetworkVar(...)
	@GetSet = (fname, fvalue) =>
		@__base["Get#{fname}"] = => @[fvalue]
		@__base["Set#{fname}"] = (fnewValue = @[fvalue]) =>
			oldVal = @[fvalue]
			@[fvalue] = fnewValue
			state = PPM2.NetworkChangeState(fvalue, fname, fnewValue, @)
			state.networkChange = false
			@SetLocalChange(state)

	if CLIENT
		hook.Add 'OnEntityCreated', 'PPM2.NW_WAIT', (ent) -> timer.Simple 0, ->
			return if not IsValid(ent)
			return if not @REGISTRY[ent\EntIndex()]
			return if @REGISTRY[ent\EntIndex()].done_setup or IsValid(@REGISTRY[ent\EntIndex()].ent)
			@REGISTRY[ent\EntIndex()]\SetupEntity(ent)

		hook.Add 'NotifyShouldTransmit', 'PPM2.NW_WAIT', (ent, should) ->
			return if not should
			return if not @REGISTRY[ent\EntIndex()]
			return if @REGISTRY[ent\EntIndex()].done_setup or IsValid(@REGISTRY[ent\EntIndex()].ent)
			@REGISTRY[ent\EntIndex()]\SetupEntity(ent)

		-- clientside entities
		hook.Add 'EntityRemoved', 'PPM2.NW_WAIT', (ent) -> @REGISTRY[ent]\Remove() if @REGISTRY[ent]

	@OnNetworkedCreated = (ply = NULL, len = 0, nwobj) =>
		if CLIENT
			netID = net.ReadUInt16()
			entid = net.ReadUInt16()
			obj = @NW_Objects[netID] or @(netID, entid)
			@REGISTRY[entid] = obj
			obj.NETWORKED = true
			obj.CREATED_BY_SERVER = true
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
		else
			ply[@NW_CooldownTimer] = ply[@NW_CooldownTimer] or 0
			ply[@NW_CooldownTimerCount] = ply[@NW_CooldownTimerCount] or 0

			if ply[@NW_CooldownTimer] < RealTimeL()
				ply[@NW_CooldownTimerCount] = 1
				ply[@NW_CooldownTimer] = RealTimeL() + 10
			else
				ply[@NW_CooldownTimerCount] += 1

			if ply[@NW_CooldownTimerCount] >= 3
				ply[@NW_CooldownMessage] = ply[@NW_CooldownMessage] or 0
				if ply[@NW_CooldownMessage] < RealTimeL()
					PPM2.Message 'Player ', ply, " is creating #{@__name} too quickly!"
					ply[@NW_CooldownMessage] = RealTimeL() + 1
				return

			waitID = net.ReadUInt16()
			obj = @(nil, ply)
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			obj\Create()
			timer.Simple 0.5, ->
				return if not IsValid(ply)
				net.Start(@NW_ReceiveID)
				net.WriteUInt(waitID, 16)
				net.WriteUInt(obj.netID, 16)
				net.Send(ply)

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]

		unless obj
			return if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		if IsValid(ply) and obj.ent ~= ply
			error('Invalid realm for player being specified. If you are running on your own net.* library, check up your code') if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		varID = net.ReadUInt16()
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :getName, :readFunc, :writeFunc, :onSet} = varData
		newVal = onSet(obj, readFunc())
		return if newVal == obj["Get#{getName}"](obj)
		state = PPM2.NetworkChangeState(strName, getName, newVal, obj, len, ply)
		state\Apply()
		obj\NetworkDataChanges(state)
		if SERVER
			net.Start(@NW_Modify)
			net.WriteUInt(id, 16)
			net.WriteUInt(varID, 16)
			writeFunc(newVal)
			net.SendOmit(ply)

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove(true)

	@ReadNetworkData = =>
		output = {strName, {getName, readFunc()} for _, {:getName, :strName, :readFunc} in ipairs @NW_Vars}
		return output

	@RenderTasks = {}
	@CheckTasks = {}

	@NW_Vars = {}
	@NW_VarsTable = {}
	@NW_Objects = {}
	@O_Slots = {} if CLIENT
	@NW_Waiting = {}
	@NW_WaitID = -1
	@NW_NextVarID = -1
	@NW_Create = 'PPM2.NW.Created'
	@NW_Modify = 'PPM2.NW.Modified'
	@NW_Broadcast = 'PPM2.NW.ModifiedBroadcast'
	@NW_Remove = 'PPM2.NW.Removed'
	@NW_Rejected = 'PPM2.NW.Rejected'
	@NW_ReceiveID = 'PPM2.NW.ReceiveID'
	@NW_CooldownTimerCount = 'ppm2_NW_CooldownTimerCount'
	@NW_CooldownTimer = 'ppm2_NW_CooldownTimer'
	@NW_CooldownMessage = 'ppm2_NW_CooldownMessage'
	@NW_NextObjectID = 0
	@NW_NextObjectID_CL = 0x60000

	if SERVER
		net.pool(@NW_Create)
		net.pool(@NW_Modify)
		net.pool(@NW_Remove)
		net.pool(@NW_ReceiveID)
		net.pool(@NW_Rejected)
		net.pool(@NW_Broadcast)

	net.Receive @NW_Create, (len = 0, ply = NULL, obj) -> @OnNetworkedCreated(ply, len, obj)
	net.Receive @NW_Modify, (len = 0, ply = NULL, obj) -> @OnNetworkedModify(ply, len, obj)
	net.Receive @NW_Remove, (len = 0, ply = NULL, obj) -> @OnNetworkedDelete(ply, len, obj)

	if SERVER
		net.ReceiveAntispam(@NW_Create)
		net.ReceiveAntispam(@NW_Remove)

	net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
		return if SERVER
		waitID = net.ReadUInt16()
		netID = net.ReadUInt16()
		obj = @NW_Waiting[waitID]
		@NW_Waiting[waitID] = nil
		return unless obj
		obj.NETWORKED = true
		@NW_Objects[obj.netID] = nil
		obj.netID = netID
		obj.waitID = nil
		@NW_Objects[netID] = obj

	net.Receive @NW_Rejected, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		return if obj.__LastReject and obj.__LastReject > RealTimeL()
		obj.__LastReject = RealTimeL() + 3
		obj.NETWORKED = false
		obj\Create()

	net.Receive @NW_Broadcast, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		obj\ReadNetworkData(len, ply)

	@GetSet('UpperManeModel', 'm_upperManeModel')
	@GetSet('LowerManeModel', 'm_lowerManeModel')
	@GetSet('TailModel', 'm_tailModel')
	@GetSet('SocksModel', 'm_socksModel')
	@GetSet('HornModel', 'm_hornmodel')
	@GetSet('ClothesModel', 'm_clothesmodel')
	@GetSet('NewSocksModel', 'm_newSocksModel')

	-- @NetworkVar('Fly',                  rBool,   wBool,                 false)
	@NetworkVar('DisableTask',          rBool,   wBool,                 false)
	@NetworkVar('UseFlexLerp',          rBool,   wBool,                  true)
	@NetworkVar('FlexLerpMultiplier',   rFloat(0, 10),  wFloat,             1)

	@SetupModifiers: =>
		for key, value in pairs PPM2.PonyDataRegistry
			if value.modifiers
				@RegisterModifier(value.getFunc, 0, 0)
				@SetModifierMinMaxFinal(value.getFunc, value.min, value.max) if value.min or value.max
				@SetupLerpTables(value.getFunc)
				strName = '_NW_' .. value.getFunc
				funcLerp = 'Calculate' .. value.getFunc
				@__base['Get' .. value.getFunc] = => @[funcLerp](@, @[strName])

	for key, value in pairs PPM2.PonyDataRegistry
		@NetworkVar(value.getFunc, value.read, value.write, value.default)

	new: (netID, ent) =>
		super()

		@m_upperManeModel = Entity(-1)
		@m_lowerManeModel = Entity(-1)
		@m_tailModel = Entity(-1)
		@m_socksModel = Entity(-1)
		@m_newSocksModel = Entity(-1)

		@recomputeTextures = true
		@isValid = true
		@removed = false
		@valid = true
		@NETWORKED = false
		@NETWORKED_PREDICT = false

		@[data.strName] = data.defFunc() for _, data in ipairs @@NW_Vars when data.defFunc

		if SERVER
			@netID = @@NW_NextObjectID
			@@NW_NextObjectID += 1
		else
			netID = -1 if netID == nil
			@netID = netID

		@@NW_Objects[@netID] = @

		if CLIENT
			for i = 1, 1024
				if not @@O_Slots[i]
					@slotID = i
					@@O_Slots[i] = @
					break

			error('no more free pony data edicts') if not @slotID

		@entID = isnumber(ent) and ent or ent\EntIndex()
		ent = Entity(ent) if isnumber(ent)

		return if not IsValid(ent)
		@ent = ent
		@modelCached = ent\GetModel() if IsValid(ent)
		@SetupEntity(ent)

	GetEntity: => @ent or NULL
	IsValid: => @isValid
	GetModel: => @modelCached
	EntIndex: => @entID
	ObjectSlot: => @slotID
	GetObjectSlot: => @slotID

	Clone: (target = @ent) =>
		copy = @@(nil, target)
		@ApplyDataToObject(copy)
		return copy

	SetupEntity: (ent) =>
		if @@REGISTRY[ent]
			return if @@REGISTRY[ent]\GetOwner() and IsValid(@@REGISTRY[ent]\GetOwner()) and @@REGISTRY[ent]\GetOwner() ~= @GetOwner()
			@@REGISTRY[ent]\Remove() if @@REGISTRY[ent].Remove and @@REGISTRY[ent] ~= @

		return unless IsValid(ent)

		if @@REGISTRY[ent\EntIndex()]
			return if @@REGISTRY[ent\EntIndex()]\GetOwner() and IsValid(@@REGISTRY[ent\EntIndex()]\GetOwner()) and @@REGISTRY[ent\EntIndex()]\GetOwner() ~= @GetOwner()
			@@REGISTRY[ent\EntIndex()]\Remove() if @@REGISTRY[ent\EntIndex()].Remove and @@REGISTRY[ent\EntIndex()] ~= @

		@ent = ent
		@done_setup = true
		ent\SetPonyData(@)
		@entTable = @ent\GetTable()

		@modelCached = ent\GetModel()
		ent\PPMBonesModifier()
		@flightController = PPM2.PonyflyController(@) if not @flightController
		@entID = ent\EntIndex() if not @entID
		@lastLerpThink = RealTimeL() if not @lastLerpThink

		@ModelChanges(@modelCached, @modelCached)
		@Reset()

		timer.Simple(0, -> @GetRenderController()\CompileTextures() if @GetRenderController()) if CLIENT

		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	ModelChanges: (old = @ent\GetModel(), new = old) =>
		@modelCached = new

		@ent\SetNW2Bool('ppm2_fly', false)

		timer.Simple 0.5, ->
			return unless IsValid(@ent)
			@Reset()

	GenericDataChange: (state) =>
		hook.Run 'PPM2_PonyDataChanges', @ent, @, state

		if state\GetKey() == 'DisableTask'
			@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
			@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

		@GetSizeController()\DataChanges(state) if @ent and @GetBodygroupController()
		@GetBodygroupController()\DataChanges(state) if @ent and @GetBodygroupController()

		@GetWeightController()\DataChanges(state) if @ent and @GetWeightController()

		if CLIENT and @ent
			@GetRenderController()\DataChanges(state) if @GetRenderController()

	ResetScale: =>
		if scale = @GetSizeController()
			scale\ResetScale()

	ModifyScale: =>
		if scale = @GetSizeController()
			scale\ModifyScale()

	GetIsEditor: => @is_editor_data or false
	SetIsEditor: (value = false) => @is_editor_data = value

	Reset: =>
		if scale = @GetSizeController()
			scale\Reset()
		if CLIENT
			@GetWeightController()\Reset() if @GetWeightController() and @GetWeightController().Reset
			@GetRenderController()\Reset() if @GetRenderController() and @GetRenderController().Reset
			@GetBodygroupController()\Reset() if @GetBodygroupController() and @GetBodygroupController().Reset

	GetHoofstepVolume: =>
		return 0.8 if @ShouldMuffleHoosteps()
		return 1

	ShouldMuffleHoosteps: =>
		return @GetSocksAsModel() or @GetSocksAsNewModel()

	PlayerRespawn: =>
		return if not IsValid(@ent)
		@entTable.__cachedIsPony = @ent\IsPony()
		if not @entTable.__cachedIsPony
			return if @alreadyCalledRespawn
			@alreadyCalledRespawn = true
			@alreadyCalledDeath = true
		else
			@alreadyCalledRespawn = false
			@alreadyCalledDeath = false
		@ApplyBodygroups(CLIENT, true)

		@ent\SetNW2Bool('ppm2_fly', false)

		@ent\PPMBonesModifier()

		if scale = @GetSizeController()
			scale\PlayerRespawn()

		if weight = @GetWeightController()
			weight\PlayerRespawn()

		if CLIENT
			@deathRagdollMerged = false
			@GetRenderController()\PlayerRespawn() if @GetRenderController()
			@GetBodygroupController()\MergeModels(@ent) if IsValid(@ent) and @GetBodygroupController().MergeModels

	PlayerDeath: =>
		return if not IsValid(@ent)

		if @ent.__ppmBonesModifiers
			@ent.__ppmBonesModifiers\Remove()

		@entTable.__cachedIsPony = @ent\IsPony()

		if not @entTable.__cachedIsPony
			return if @alreadyCalledDeath
			@alreadyCalledDeath = true
		else
			@alreadyCalledDeath = false

		@ent\SetNW2Bool('ppm2_fly', false)

		if scale = @GetSizeController()
			scale\PlayerDeath()

		if weight = @GetWeightController()
			weight\PlayerDeath()

		if CLIENT
			@DoRagdollMerge()
			@GetRenderController()\PlayerDeath() if @GetRenderController()

	DoRagdollMerge: =>
		return if @deathRagdollMerged
		bgController = @GetBodygroupController()
		rag = @ent\GetRagdollEntity()

		if not bgController.MergeModels
			@deathRagdollMerged = true
		elseif IsValid(rag)
			@deathRagdollMerged = true
			bgController\MergeModels(rag)

	ApplyBodygroups: (updateModels = CLIENT) => @GetBodygroupController()\ApplyBodygroups(updateModels) if @ent
	SetLocalChange: (state) => @GenericDataChange(state)
	NetworkDataChanges: (state) => @GenericDataChange(state)

	GetPonyRaceFlags: =>
		switch @GetRace()
			when PPM2.RACE_EARTH
				return 0
			when PPM2.RACE_PEGASUS
				return PPM2.RACE_HAS_WINGS
			when PPM2.RACE_UNICORN
				return PPM2.RACE_HAS_HORN
			when PPM2.RACE_ALICORN
				return PPM2.RACE_HAS_HORN + PPM2.RACE_HAS_WINGS

	SlowUpdate: =>
		@GetBodygroupController()\SlowUpdate() if @GetBodygroupController()
		@GetWeightController()\SlowUpdate() if @GetWeightController()
		if scale = @GetSizeController()
			scale\SlowUpdate()

		if SERVER and IsValid(@ent) and @ent\IsPlayer()
			arms = @ent\GetHands()

			if IsValid(arms)
				cond = @GetPonyRaceFlags()\band(PPM2.RACE_HAS_HORN) ~= 0 and @ent\GetInfoBool('ppm2_cl_vm_magic_hands', true) and PPM2.HAND_BODYGROUP_MAGIC or PPM2.HAND_BODYGROUP_HOOVES
				arms\SetBodygroup(PPM2.HAND_BODYGROUP_ID, cond)

	Think: =>
	RenderScreenspaceEffects: =>
		time = RealTimeL()
		delta = time - @lastLerpThink
		@lastLerpThink = time
		if @isValid and IsValid(@ent)
			for _, change in ipairs @TriggerLerpAll(delta * 5)
				state = PPM2.NetworkChangeState('_NW_' .. change[1], change[1], change[2] + @['_NW_' .. change[1]], @)
				state\SetCantApply(true)
				@GenericDataChange(state)

	GetFlightController: => @flightController

	GetRenderController: =>
		return if SERVER
		return @renderController if not @isValid
		if not @renderController or @modelCached ~= @modelRender
			@modelRender = @modelCached
			cls = PPM2.GetRenderController(@modelCached)
			if @renderController and cls == @renderController.__class
				@renderController.ent = @ent
				PPM2.DebugPrint('Skipping render controller recreation for ', @ent, ' as part of ', @)
				return @renderController
			@renderController\Remove() if @renderController
			@renderController = cls(@)
		@renderController.ent = @ent
		return @renderController

	GetWeightController: =>
		return @weightController if not @isValid
		if not @weightController or @modelCached ~= @modelWeight
			@modelCached = @modelCached or @ent\GetModel()
			@modelWeight = @modelCached
			cls = PPM2.GetPonyWeightController(@modelCached)
			if @weightController and cls == @weightController.__class
				@weightController.ent = @ent
				PPM2.DebugPrint('Skipping weight controller recreation for ', @ent, ' as part of ', @)
				return @weightController
			@weightController\Remove() if @weightController
			@weightController = cls(@)
		@weightController.ent = @ent
		return @weightController

	GetSizeController: =>
		return @scaleController if not @isValid
		if not @scaleController or @modelCached ~= @modelScale
			@modelCached = @modelCached or @ent\GetModel()
			@modelScale = @modelCached
			cls = PPM2.GetSizeController(@modelCached)
			if @scaleController and cls == @scaleController.__class
				@scaleController.ent = @ent
				PPM2.DebugPrint('Skipping size controller recreation for ', @ent, ' as part of ', @)
				return @scaleController
			@scaleController\Remove() if @scaleController
			@scaleController = cls(@)
		@scaleController.ent = @ent
		return @scaleController
	GetScaleController: => @GetSizeController()

	GetBodygroupController: =>
		return @bodygroups if not @isValid
		if not @bodygroups or @modelBodygroups ~= @modelCached
			@modelCached = @modelCached or @ent\GetModel()
			@modelBodygroups = @modelCached
			cls = PPM2.GetBodygroupController(@modelCached)
			if @bodygroups and cls == @bodygroups.__class
				@bodygroups.ent = @ent
				PPM2.DebugPrint('Skipping bodygroup controller recreation for ', @ent, ' as part of ', @)
				return @bodygroups
			@bodygroups\Remove() if @bodygroups
			@bodygroups = cls(@)
		@bodygroups.ent = @ent
		return @bodygroups

	Remove: (byClient = false) =>
		@removed = true
		@@NW_Objects[@netID] = nil if SERVER or @NETWORKED
		@@O_Slots[@slotID] = nil if @slotID
		@isValid = false
		@ent = @GetEntity() if not IsValid(@ent)
		@@REGISTRY[@ent] = nil if IsValid(@ent) and @@REGISTRY[@ent] == @
		@@REGISTRY[@entID] = nil if @@REGISTRY[@entID] == @
		@GetWeightController()\Remove() if @GetWeightController()

		if CLIENT
			@GetRenderController()\Remove() if @GetRenderController()
			if IsValid(@ent) and @ent.__ppm2_task_hit
				@entTable.__ppm2_task_hit = false
				@ent\SetNoDraw(false)

		@GetBodygroupController()\Remove() if @GetBodygroupController()
		@GetSizeController()\Remove() if @GetSizeController()

		@flightController\Switch(false) if @flightController
		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	__tostring: => "[#{@@__name}:#{@netID}|#{@ent}]"

	GetOwner: => @ent
	IsNetworked: => @NETWORKED
	IsGoingToNetwork: => @NETWORKED_PREDICT
	SetIsGoingToNetwork: (val = @NETWORKED) => @NETWORKED_PREDICT = val
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID

	ComputeMagicColor: =>
		color = @GetHornMagicColor()

		if not @GetSeparateMagicColor()
			if not @GetSeparateEyes()
				color = @GetEyeIrisTop()\Lerp(0.5, @GetEyeIrisBottom())
			else
				lerpLeft = @GetEyeIrisTopLeft()\Lerp(0.5, @GetEyeIrisBottomLeft())
				lerpRight = @GetEyeIrisTopRight()\Lerp(0.5, @GetEyeIrisBottomRight())
				color = lerpLeft\Lerp(0.5, lerpRight)

		return color

	ReadNetworkData: (len = 24, ply = NULL, silent = false, applyEntities = true) =>
		data = @@ReadNetworkData()
		validPly = IsValid(ply)
		states = [PPM2.NetworkChangeState(key, keyValid, newVal, @, len, ply) for key, {keyValid, newVal} in pairs data]
		for _, state in ipairs states
			if not validPly or applyEntities or not isentity(state\GetValue())
				state\Apply()
				@NetworkDataChanges(state) unless silent

	NetworkedIterable: (grabEntities = true) =>
		data = [{getName, @[strName]} for _, {:strName, :getName} in ipairs @@NW_Vars when grabEntities or not isentity(@[strName])]
		return data

	ApplyDataToObject: (target, applyEntities = false) =>
		target["Set#{key}"](target, value) for {key, value} in *@NetworkedIterable(applyEntities) when target["Set#{key}"]
		return target

	WriteNetworkData: => writeFunc(@[strName]) for _, {:strName, :writeFunc} in ipairs @@NW_Vars

	ReBroadcast: =>
		return false if not @NETWORKED
		return false if CLIENT
		net.Start(@@NW_Broadcast)
		net.WriteUInt16(@netID)
		@WriteNetworkData()
		net.Broadcast()
		return true

	Create: =>
		return if @NETWORKED
		return if CLIENT and @CREATED_BY_SERVER -- wtf
		@NETWORKED = true if SERVER
		@NETWORKED_PREDICT = true

		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt16(@netID)
			net.WriteEntity(@ent)
			@WriteNetworkData()
			filter = RecipientFilter()
			filter\AddAllPlayers()
			filter\RemovePlayer(@ent) if IsValid(@ent) and @ent\IsPlayer()
			net.Send(filter)
		else
			@@NW_WaitID += 1
			@waitID = @@NW_WaitID

			net.Start(@@NW_Create)
			before = net.BytesWritten()

			net.WriteUInt16(@waitID)
			@WriteNetworkData()
			after = net.BytesWritten()

			net.SendToServer()
			@@NW_Waiting[@waitID] = @
			return after - before

	NetworkTo: (targets = {}) =>
		net.Start(@@NW_Create)
		net.WriteUInt16(@netID)
		net.WriteEntity(@ent)
		@WriteNetworkData()
		net.Send(targets)

PPM2.NetworkedPonyData = NetworkedPonyData

if CLIENT
	net.Receive 'PPM2.NotifyDisconnect', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()

	net.Receive 'PPM2.PonyDataRemove', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()
else
	hook.Add 'PlayerJoinTeam', 'PPM2.TeamWaypoint', (ply, new) ->
		ply.__ppm2_modified_jump = false
	hook.Add 'OnPlayerChangedTeam', 'PPM2.TeamWaypoint', (ply, old, new) ->
		ply.__ppm2_modified_jump = false

_G.LocalPonyData = () -> LocalPlayer()\GetPonyData()
_G.LocalPonydata = () -> LocalPlayer()\GetPonyData()

do
	REGISTRY = NetworkedPonyData.REGISTRY

	nullify = ->
		for _, ent in ipairs ents.GetAll()
			if ent.__PPM2_PonyData and ent.__PPM2_PonyData.Remove
				ProtectedCall -> ent.__PPM2_PonyData\Remove()

		for _, ent in ipairs ents.GetAll()
			ent.__PPM2_PonyData = nil

		for key, obj in pairs(REGISTRY)
			if obj.Remove
				ProtectedCall -> obj\Remove()

			REGISTRY[key] = nil

	nullify()

	entMeta = FindMetaTable('Entity')
	EntIndex = entMeta.EntIndex

	entMeta.GetPonyData = =>
		index = EntIndex(@)
		return REGISTRY[index] if index > 0
		return REGISTRY[@]

	entMeta.SetPonyData = (data) =>
		index = EntIndex(@)

		if index > 0
			REGISTRY[index] = data
			return

		REGISTRY[@] = data


refactoring: abstracted temporary item drawing method

export ^

class Item
-- used for things on the map that minions can pick up
    color: {0, 0, 0}
    draw: =>
        love.graphics.setLineWidth(2)
        love.graphics.setColor(@color)
        love.graphics.rectangle("fill", -5, -5, 10, 10)
        love.graphics.setColor(0, 0, 0)
        love.graphics.rectangle("line", -5, -5, 10, 10)

class Energy extends Item
    color: {66, 145, 135}

class Material extends Item
    color: {243, 127, 183}

class Sludge extends Item
    color: {87, 119, 32}

class Food extends Item
    color: {159, 112, 19}

class Gun extends Item
    color: {0, 0, 0}

class ResearchEquipment extends Item
    color: {255, 255, 255}

export ^

class Item
-- used for things on the map that minions can pick up
    color: {0, 0, 0}

class Energy extends Item
    color: {66, 145, 135}
    draw: =>
        love.graphics.setColor(@color)
        love.graphics.rectangle("fill", -5, -5, 10, 10)

class Material extends Item
    color: {243, 127, 183}
    draw: =>
        love.graphics.setColor(@color)
        love.graphics.rectangle("fill", -5, -5, 10, 10)

class Sludge extends Item
    color: {87, 119, 32}
    draw: =>
        love.graphics.setColor(@color)
        love.graphics.rectangle("fill", -5, -5, 10, 10)

class Food extends Item
    color: {159, 112, 19}
    draw: =>
        love.graphics.setColor(@color)
        love.graphics.rectangle("fill", -5, -5, 10, 10)

class Gun extends Item
    color: {0, 0, 0}
    draw: =>
        love.graphics.setColor(@color)
        love.graphics.rectangle("fill", -5, -5, 10, 10)

class ResearchEquipment extends Item
    color: {255, 255, 255}
    draw: =>
        love.graphics.setColor(@color)
        love.graphics.rectangle("fill", -5, -5, 10, 10)


Inspector.moon: update library load routine to use package.path.

-- This library is unlicensed under CC0

DependencyControl = require "l0.DependencyControl"

versionRecord = DependencyControl( {
	name: "ASSInspector",
	version: "0.5.2",
	description: "Provides low level inspection and analysis of subtitles post-rasterization.",
	author: "torque",
	url: "https://github.com/TypesettingCartel/ASSInspector",
	moduleName: "ASSInspector.Inspector",
	feed: "https://raw.githubusercontent.com/TypesettingCartel/ASSInspector/master/DependencyControl.json",
	{ "ffi" }
} )

ASSIVersionCompat = DependencyControl( {
	moduleName: "ASSInspector.Compat",
	version: "0.4.1",
	virtual: true
} )

ffi = versionRecord\requireModules!

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
	uint32_t hash;
	uint8_t solid;
} ASSI_Rect;

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

packagePaths = ( namespace ) ->
	paths = { }
	-- fixedLibraryName = namespace .. "/" .. "#{(ffi.os != 'Windows') and 'lib' or ''}#{libraryName}.#{(OSX: 'dylib', Windows: 'dll')[ffi.os] or 'so'}"
	package.path\gsub "([^;]+)", ( path ) ->
		-- the init.lua paths are just dupes of other paths.
		if path\match "/%?/init%.lua$"
			return

		path = path\gsub "//?%?%.lua$", "/"
		table.insert paths, path .. namespace

	return paths

__name = "ASSInspector"
libraryPaths = packagePaths "#{__name}/Inspector/"
libraryName = "#{(ffi.os != 'Windows') and 'lib' or ''}#{__name}.#{(OSX: 'dylib', Windows: 'dll')[ffi.os] or 'so'}"
local ASSInspector, libraryPath
for path in *libraryPaths
	success, ASSInspector = pcall ffi.load, path .. libraryName
	if success
		libraryPath = path
		break

assert libraryPath, "Could not load #{__name} C library."

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	assiVer = DependencyControl moduleName: "ASSInspector.Lib", version: ASSIVersion, virtual: true
	unless ASSIVersionCompat\checkVersion assiVer, "major"
		return nil, ("Inspector.moon library is too old. Must be v%s.")\format assiVer\getVersionString nil, "major"
	unless assiVer\checkVersion ASSIVersionCompat, "minor"
		return nil, ("libASSInspector library is too old. Must be v%s compatible.")\format ASSIVersionCompat\getVersionString nil, "minor"

	return true

-- Inspector is not actually a property of the class but a property of
-- the script. Since we don't need to worry about concurrency or
-- anything silly like that, we can initialize ASSInspector once and
-- keep that reference alive until Aegisub is closed or scripts are all
-- reloaded.
state = {
	inspector:        nil
	fontconfigConfig: nil
	fontDir:          nil
	resX:             nil
	resY:             nil
}

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert( scriptHeader, .raw )

				if "PlayResX" == .key
					resX = tonumber( .value )
				elseif "PlayResY" == .key
					resY = tonumber( .value )

			elseif "style" == .class
				unless seenStyles
					table.insert( scriptHeader, "[V4+ Styles]\n" )
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size( )
	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat( scriptHeader, '\n' )
	@styles = styles

initializeInspector = ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) ->
	if nil == state.inspector
		success, message = looseVersionCompare( ASSInspector.assi_getVersion! )
		unless success
			return nil, message

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

		if nil == state.inspector
			return nil, "ASSInspector library initialization failed."

		state.resX = 1
		state.resY = 1
		state.fontDir = fontDirectory
		state.fontconfigConfig = fontconfigConfig

	return true

validateRect = ( rect ) ->
	bounds = {
		x: tonumber( rect.x )
		y: tonumber( rect.y )
		w: tonumber( rect.w )
		h: tonumber( rect.h )
		hash: tonumber( rect.hash )
		solid: (rect.solid == 1)
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms( 0 )

	if hasFrames
		for line in *lines
			with line
				for frame = ffms( .start_time ), true == .assi_exhaustive and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor( 0.5*( msff( frame ) + msff( frame + 1 ) ) )
					unless seenTimes[frameTime]
						table.insert( times, frameTime )
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert( times, .start_time )
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert( scriptText, @styles[line.style] )
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert( scriptText, @styles[styleName] )
				seenStyles[styleName] = true

class Inspector
	@version = versionRecord

	new: ( subtitles, fontconfigConfig = libraryPath .. "fonts.conf", fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		assert subtitles, "You must provide the subtitles object."

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		assert success, message

		success, message = @updateHeader( subtitles )
		assert success, message

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		return true

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

	forceLibraryReload: ( subtitles, fontconfigConfig, fontDirectory ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		state.inspector = nil
		-- let initializeInspector handle the default arguments.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.assi_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes( lines ) ) =>
		unless times
			return nil, "The render times table was empty."

		for i = 1,#lines
			line = lines[i]
			if nil == line.raw
				if line.createRaw
					line\createRaw!
				else
					return nil, "line.raw is missing from line #{i}."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert( scriptText, @styles.Default )

		for line in *lines
			addStyles( @, line, scriptText, seenStyles )

		table.insert( scriptText, '[Events]' )

		for line in *lines
			table.insert( scriptText, line.raw )

		scriptString = table.concat( scriptText, '\n' )
		if 0 < ASSInspector.assi_setScript( state.inspector, scriptString, #scriptString )
			return nil, "Could not set script" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		renderCount = #times
		cTimes = ffi.new( 'int32_t[?]', renderCount )
		cRects = ffi.new( 'ASSI_Rect[?]', renderCount )

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		rects = { }
		for i = 0, renderCount - 1
			table.insert( rects, validateRect( cRects[i] ) )

		return rects, times

return versionRecord\register Inspector

-- This library is unlicensed under CC0

DependencyControl = require "l0.DependencyControl"

versionRecord = DependencyControl( {
	name: "ASSInspector",
	version: "0.5.1",
	description: "Provides low level inspection and analysis of subtitles post-rasterization.",
	author: "torque",
	url: "https://github.com/TypesettingCartel/ASSInspector",
	moduleName: "ASSInspector.Inspector",
	feed: "https://raw.githubusercontent.com/TypesettingCartel/ASSInspector/master/DependencyControl.json",
	{ "ffi" }
} )

ASSIVersionCompat = DependencyControl( {
	moduleName: "ASSInspector.Compat",
	version: "0.4.0",
	virtual: true
} )

ffi = versionRecord\requireModules!

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
	uint32_t hash;
	uint8_t solid;
} ASSI_Rect;

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

loadName = "ASSInspector"
libraryName = "#{(ffi.os != 'Windows') and 'lib' or ''}#{loadName}.#{(OSX: 'dylib', Windows: 'dll')[ffi.os] or 'so'}"
pathExt = "/automation/include/ASSInspector/Inspector/"
libraryPaths = {
	aegisub.decode_path( "?user" .. pathExt ),
	aegisub.decode_path( "?data" .. pathExt )
}

local ASSInspector
libraryPath = false
for path in *libraryPaths
	success, ASSInspector = pcall ffi.load, path .. libraryName
	if success
		libraryPath = path
		break

assert( libraryPath, "Could not load required ASSInspector C library." )

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	assiVer = DependencyControl moduleName: "ASSInspector.Lib", version: ASSIVersion, virtual: true
	unless ASSIVersionCompat\checkVersion assiVer, "major"
		return nil, ("Inspector.moon library is too old. Must be v%s.")\format assiVer\getVersionString nil, "major"
	unless assiVer\checkVersion ASSIVersionCompat, "minor"
		return nil, ("libASSInspector library is too old. Must be v%s compatible.")\format ASSIVersionCompat\getVersionString nil, "minor"

	return true

-- Inspector is not actually a property of the class but a property of
-- the script. Since we don't need to worry about concurrency or
-- anything silly like that, we can initialize ASSInspector once and
-- keep that reference alive until Aegisub is closed or scripts are all
-- reloaded.
state = {
	inspector:        nil
	fontconfigConfig: nil
	fontDir:          nil
	resX:             nil
	resY:             nil
}

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert( scriptHeader, .raw )

				if "PlayResX" == .key
					resX = tonumber( .value )
				elseif "PlayResY" == .key
					resY = tonumber( .value )

			elseif "style" == .class
				unless seenStyles
					table.insert( scriptHeader, "[V4+ Styles]\n" )
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size( )
	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat( scriptHeader, '\n' )
	@styles = styles

initializeInspector = ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) ->
	if nil == state.inspector
		success, message = looseVersionCompare( ASSInspector.assi_getVersion! )
		unless success
			return nil, message

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

		if nil == state.inspector
			return nil, "ASSInspector library initialization failed."

		state.resX = 1
		state.resY = 1
		state.fontDir = fontDirectory
		state.fontconfigConfig = fontconfigConfig

	return true

validateRect = ( rect ) ->
	bounds = {
		x: tonumber( rect.x )
		y: tonumber( rect.y )
		w: tonumber( rect.w )
		h: tonumber( rect.h )
		hash: tonumber( rect.hash )
		solid: (rect.solid == 1)
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms( 0 )

	if hasFrames
		for line in *lines
			with line
				for frame = ffms( .start_time ), true == .assi_exhaustive and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor( 0.5*( msff( frame ) + msff( frame + 1 ) ) )
					unless seenTimes[frameTime]
						table.insert( times, frameTime )
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert( times, .start_time )
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert( scriptText, @styles[line.style] )
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert( scriptText, @styles[styleName] )
				seenStyles[styleName] = true

class Inspector
	@version = versionRecord

	new: ( subtitles, fontconfigConfig = aegisub.decode_path( libraryPath .. "fonts.conf" ), fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		assert subtitles, "You must provide the subtitles object."

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		assert success, message

		success, message = @updateHeader( subtitles )
		assert success, message

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		return true

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

	forceLibraryReload: ( subtitles, fontconfigConfig, fontDirectory ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		state.inspector = nil
		-- let initializeInspector handle the default arguments.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.assi_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes( lines ) ) =>
		unless times
			return nil, "The render times table was empty."

		for i = 1,#lines
			line = lines[i]
			if nil == line.raw
				if line.createRaw
					line\createRaw!
				else
					return nil, "line.raw is missing from line #{i}."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert( scriptText, @styles.Default )

		for line in *lines
			addStyles( @, line, scriptText, seenStyles )

		table.insert( scriptText, '[Events]' )

		for line in *lines
			table.insert( scriptText, line.raw )

		scriptString = table.concat( scriptText, '\n' )
		if 0 < ASSInspector.assi_setScript( state.inspector, scriptString, #scriptString )
			return nil, "Could not set script" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		renderCount = #times
		cTimes = ffi.new( 'int32_t[?]', renderCount )
		cRects = ffi.new( 'ASSI_Rect[?]', renderCount )

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		rects = { }
		for i = 0, renderCount - 1
			table.insert( rects, validateRect( cRects[i] ) )

		return rects, times

return versionRecord\register Inspector