utf8: prevent zalgoDOS

utf8: prevent zalgoDOS

Former-commit-id: 677b48c28ee5b09771d7ee7854e6a3ee27f5570e [formerly 975853f3b381d395c4bfb2301babc525e8ec0670]
Former-commit-id: f6273b0634dab8bca21f8cef491e501f9b2268ad

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,.؟!"'`()[]{}«»&_]]
}
codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

-- 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)
  '^%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)])

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,.؟!"'`()[]{}«»&_]]
}
codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

-- 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) ->
  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)
  '^%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)])


utf8: fix zalgorithm

utf8: fix zalgorithm

Former-commit-id: f02b3b9e0451281b0b31620945f7bc41430f6130 [formerly 15cc9d1fd21f5065b541e26bee3d29baa7658251]
Former-commit-id: f3468ed6baed9ee0fa3e594f6bda2c084339a66e

html2unicode = require'html'
math = require'math'

an = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
ci = 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl = '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'

an2ci = {}
-- Circled letters are 3 bytes, just use sub string
for i=1, #an
  f = i*3+1-3
  t = i*3
  an2ci[an\sub(i,i)] = ci\sub(f, t)

an2bl = {}
i=1
-- Since blackletters have varying byte length, use the common lua pattern to find multibyte chars
for uchar in string.gfind(bl, "([%z\1-\127\194-\244][\128-\191]*)")
  an2bl[an\sub(i,i)] = uchar
  i = i +1

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

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) ->
  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) =>
    @Msg 'privmsg', destination, source, wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, zalgo(arg, 10)

html2unicode = require'html'
math = require'math'

an = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
ci = 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl = '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'

an2ci = {}
-- Circled letters are 3 bytes, just use sub string
for i=1, #an
  f = i*3+1-3
  t = i*3
  an2ci[an\sub(i,i)] = ci\sub(f, t)

an2bl = {}
i=1
-- Since blackletters have varying byte length, use the common lua pattern to find multibyte chars
for uchar in string.gfind(bl, "([%z\1-\127\194-\244][\128-\191]*)")
  an2bl[an\sub(i,i)] = uchar
  i = i +1

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

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) ->
  zalgo_threshold = intensity
  zalgo_chars = {}
  for i=0x0300, 0x036
    zalgo_chars[i-0x2ff] = unichr(i)

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

  source = text\upper()
  --source = _insert_randoms(source)

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

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, zalgo(arg, 10)


imageupload: add upload video file support

Former-commit-id: 34e9edb453d4636c0d9370b91734ed20d51029c8 [formerly d3b43675e0018094409f25f7371bc37a4544935f]
Former-commit-id: b579cd2187ad357181e2527561bc08ff42f1b70a

{:simplehttp, :json, :urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
fs = require'nixio.fs'
ltn12 = require'ltn12'

urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\match '.png'
      content_type = 'image/png'
    if file\match '.svg'
      content_type = 'image/svg'
    if file\match '.mp4'
      content_type = 'video/mp4'
    if file\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn

    return

  channel = req.url\match('channel=(.+)')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the yellow buttons to attach image. It will appear instantly on IRC!

    <form>

        <input id="sender" type="text" required>

        Your name

        <input id="text" type="text" required>

        Attach text

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">
      🎥 Upload video! <input type="file" accept="video/*" id="capturevf" style="visibility:hidden;">

        What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools -> Add to homescreen to create a convenient shortcut for easy and fast access to sharing

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        --fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        --written = fd\write file
        --fd\sync!
        --fd\close!
        -- Move tempfile to real location
        fs.move req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender}> "
        msg = "[IRCSNAP] #{sender}#{text\sub(1, 100)} http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}file/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}?channel=#{channel}"

{:simplehttp, :json, :urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
fs = require'nixio.fs'
ltn12 = require'ltn12'

urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\match '.png'
      content_type = 'image/png'
    if file\match '.svg'
      content_type = 'image/svg'
    if file\match '.mp4'
      content_type = 'video/mp4'
    if file\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn

    return

  channel = req.url\match('channel=(.+)')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the two yellow buttons to attach image. It will appear instantly on IRC!

    <form>

        <input id="sender" type="text" required>

        Your name

        <input id="text" type="text" required>

        Attach text

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">

        What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools -> Add to homescreen to create a convenient shortcut for easy and fast access to sharing

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        --fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        --written = fd\write file
        --fd\sync!
        --fd\close!
        -- Move tempfile to real location
        fs.move req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender}> "
        msg = "[IRCSNAP] #{sender}#{text\sub(1, 100)} http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}file/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}?channel=#{channel}"


adjust hammerspoon profile handling and hotkeys

require 'moonscript'

import HotkeyModal from require 'hotkey_modal'
import Grid from require 'grid'
import Profile from require 'profile'
Action = require 'action'

----------------------------------------------------------------------------------------------------
-- Profiles
----------------------------------------------------------------------------------------------------
grid1 = Grid(6, 6)

homeScreens = {69671680}
homeConfig = {
  "iTunes":     {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "Mail":       {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "Safari":     {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "SourceTree": {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "Spotify":    {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.8, h: 1.0})}
  "Terminal":   {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.5, y: 0.5, w: 0.5, h: 0.5})}
  "TextMate":   {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.5, y: 0.0, w: 0.5, h: 1.0})}
  "Xcode":      {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "_":          {Action.Snap(grid1)}
}

home = Profile('Home', homeScreens, homeConfig)

----------------------------------------------------------------------------------------------------

workScreens = {188898833, 188898834}
workConfig = {
  "Dash":              {Action.MoveToScreen(2), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.5, h: 1.0})}
  "iTunes":            {Action.Close!}
  "Parallels Desktop": {Action.MoveToScreen(2), Action.FullScreen!}
  "Safari":            {Action.MoveToScreen(2), Action.MoveToUnit({x: 0.0, y: 0.0, w: 1.0, h: 1.0})}
  "SourceTree":        {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 1.0, h: 1.0})}
  "Terminal":          {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.5, y: 0.5, w: 0.5, h: 0.5})}
  "TextMate":          {Action.MoveToScreen(2), Action.MoveToUnit({x: 0.5, y: 0.0, w: 0.5, h: 1.0})}
  "Xcode":             {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 1.0, h: 1.0})}
  "_":                 {Action.Snap(grid1)}
}

work = Profile('Work', workScreens, workConfig)

----------------------------------------------------------------------------------------------------

profiles = {home, work}

activeProfile = ->
  for profile in *profiles
    if profile\isActive! then return profile
  return nil

checkKnownProfile = ->
  if activeProfile! == nil
    hs.alert.show("unknown profile, see console for screen information", 3)
    for screen in *hs.screen.allScreens!
      print("unknown screen: #{screen\id!}")

activateActiveProfile = ->
  profile = activeProfile!
  if profile then profile\activate!

launchOrFocusApp = (appName) -> hs.application.launchOrFocus(appName)

launchOrActivateApp = (appName) ->
  wasAlreadyRunning = hs.appfinder.appFromName(appName) != nil
  hs.application.launchOrFocus(appName)
  if wasAlreadyRunning then
    profile = activeProfile!
    if profile then profile\activateFor(appName)

----------------------------------------------------------------------------------------------------
-- Hotkey Bindings
----------------------------------------------------------------------------------------------------

splitModifiers = {'ctrl', 'alt', 'cmd'}
modalModifiers = {'cmd', 'alt'}
arrangementModifiers = {'cmd', 'shift'}

hs.hotkey.bind(splitModifiers, 'UP', Action.Maximize!\perform)
hs.hotkey.bind(splitModifiers, 'DOWN', Action.MoveToNextScreen!\perform)
hs.hotkey.bind(splitModifiers, 'LEFT', Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.5, h: 1.0})\perform)
hs.hotkey.bind(splitModifiers, 'RIGHT', Action.MoveToUnit({x: 0.5, y: 0.0, w: 0.5, h: 1.0})\perform)
hs.hotkey.bind(splitModifiers, 'SPACE', grid1\snapAll)

position = HotkeyModal('Position', modalModifiers, '1')
position\bind({}, 'UP', grid1\positionTopRight)
position\bind({}, 'DOWN', grid1\positionBottomLeft)
position\bind({}, 'LEFT', grid1\positionTopLeft)
position\bind({}, 'RIGHT', grid1\positionBottomRight)
position\bind({}, 'RETURN', position\exit)

resize = HotkeyModal('Resize', modalModifiers, '2')
resize\bind({}, 'UP', grid1\resizeShorter)
resize\bind({}, 'DOWN', grid1\resizeTaller)
resize\bind({}, 'LEFT', grid1\resizeThinner)
resize\bind({}, 'RIGHT', grid1\resizeWider)
resize\bind({}, 'RETURN', resize\exit)

move = HotkeyModal('Move', modalModifiers, '3')
move\bind({}, 'UP', grid1\moveUp)
move\bind({}, 'DOWN', grid1\moveDown)
move\bind({}, 'LEFT', grid1\moveLeft)
move\bind({}, 'RIGHT', grid1\moveRight)
move\bind({}, 'RETURN', move\exit)

appShortcuts = {
  'd': 'Dash'
  'e': 'TextMate'
  'f': 'Finder'
  'g': 'SourceTree'
  'i': 'iTunes'
  'm': 'Mail'
  'p': 'Parallels Desktop'
  's': 'Safari'
  't': 'Terminal'
  'x': 'Xcode'
}
for shortcut, appName in pairs(appShortcuts)
  hs.hotkey.bind(modalModifiers, shortcut, -> launchOrFocusApp(appName))
  hs.hotkey.bind(splitModifiers, shortcut, -> launchOrActivateApp(appName))

hs.hotkey.bind(arrangementModifiers, '1', -> activateActiveProfile!)
hs.hotkey.bind(arrangementModifiers, '2', -> home\activate!)
hs.hotkey.bind(arrangementModifiers, '3', -> work\activate!)

----------------------------------------------------------------------------------------------------
-- Settings and Watcher
----------------------------------------------------------------------------------------------------
hs.window.animationDuration = 0.1

hs.screen.watcher.new(-> activateActiveProfile!)\start!
hs.pathwatcher.new(hs.configdir, (files) -> hs.reload!)\start!
hs.alert.show("Hammerspoon loaded", 1)

checkKnownProfile!

require 'moonscript'

import HotkeyModal from require 'hotkey_modal'
import Grid from require 'grid'
import Profile from require 'profile'
Action = require 'action'

----------------------------------------------------------------------------------------------------
-- Profiles
----------------------------------------------------------------------------------------------------
grid1 = Grid(6, 6)

homeScreens = {69671680}
homeConfig = {
  "iTunes":     {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "Mail":       {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "Safari":     {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "SourceTree": {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "Spotify":    {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.8, h: 1.0})}
  "Terminal":   {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.5, y: 0.5, w: 0.5, h: 0.5})}
  "TextMate":   {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.5, y: 0.0, w: 0.5, h: 1.0})}
  "Xcode":      {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.7, h: 1.0})}
  "_":          {Action.Snap(grid1)}
}

home = Profile('Home', homeScreens, homeConfig)

----------------------------------------------------------------------------------------------------

workScreens = {188898833, 188898834}
workConfig = {
  "Dash":              {Action.MoveToScreen(2), Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.5, h: 1.0})}
  "iTunes":            {Action.Close!}
  "Parallels Desktop": {Action.MoveToScreen(2), Action.FullScreen!}
  "Safari":            {Action.MoveToScreen(2), Action.MoveToUnit({x: 0.0, y: 0.0, w: 1.0, h: 1.0})}
  "SourceTree":        {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 1.0, h: 1.0})}
  "Terminal":          {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.5, y: 0.5, w: 0.5, h: 0.5})}
  "TextMate":          {Action.MoveToScreen(2), Action.MoveToUnit({x: 0.5, y: 0.0, w: 0.5, h: 1.0})}
  "Xcode":             {Action.MoveToScreen(1), Action.MoveToUnit({x: 0.0, y: 0.0, w: 1.0, h: 1.0})}
  "_":                 {Action.Snap(grid1)}
}

work = Profile('Work', workScreens, workConfig)

----------------------------------------------------------------------------------------------------

profiles = {home, work}

activateActiveProfile = ->
  for profile in *profiles
    if profile\isActive! then
      profile\activate!
      return
  hs.alert.show("unknown profile, see console for screen information")
  for screen in hs.screen.allScreens!
    print("unknown screen: #{screen\id!}")

----------------------------------------------------------------------------------------------------
-- Hotkey Bindings
----------------------------------------------------------------------------------------------------

modalModifiers = {'cmd', 'alt'}
arrangementModifiers = {'cmd', 'shift'}

split = HotkeyModal('Split', modalModifiers, '1')
split\bind({}, 'UP', -> Action.Maximize!\perform!)
split\bind({}, 'DOWN', -> Action.MoveToNextScreen!\perform!)
split\bind({}, 'LEFT', -> Action.MoveToUnit({x: 0.0, y: 0.0, w: 0.5, h: 1.0})\perform!)
split\bind({}, 'RIGHT', -> Action.MoveToUnit({x: 0.5, y: 0.0, w: 0.5, h: 1.0})\perform!)
split\bind({}, 'SPACE', grid1\snapAll)
split\bind({}, 'RETURN', split\exit)

position = HotkeyModal('Position', modalModifiers, '2')
position\bind({}, 'UP', grid1\positionTopRight)
position\bind({}, 'DOWN', grid1\positionBottomLeft)
position\bind({}, 'LEFT', grid1\positionTopLeft)
position\bind({}, 'RIGHT', grid1\positionBottomRight)
position\bind({}, 'RETURN', position\exit)

resize = HotkeyModal('Resize', modalModifiers, '3')
resize\bind({}, 'UP', grid1\resizeShorter)
resize\bind({}, 'DOWN', grid1\resizeTaller)
resize\bind({}, 'LEFT', grid1\resizeThinner)
resize\bind({}, 'RIGHT', grid1\resizeWider)
resize\bind({}, 'RETURN', resize\exit)

move = HotkeyModal('Move', modalModifiers, '4')
move\bind({}, 'UP', grid1\moveUp)
move\bind({}, 'DOWN', grid1\moveDown)
move\bind({}, 'LEFT', grid1\moveLeft)
move\bind({}, 'RIGHT', grid1\moveRight)
move\bind({}, 'RETURN', move\exit)

hs.hotkey.bind(arrangementModifiers, '1', -> activateActiveProfile!)
hs.hotkey.bind(arrangementModifiers, '2', -> home\activate!)
hs.hotkey.bind(arrangementModifiers, '3', -> work\activate!)

----------------------------------------------------------------------------------------------------
-- Settings and Watcher
----------------------------------------------------------------------------------------------------
hs.window.animationDuration = 0.1

hs.screen.watcher.new(-> activateActiveProfile!)\start!
hs.pathwatcher.new(hs.configdir, (files) -> hs.reload!)\start!
hs.alert.show("Hammerspoon loaded", 0.5)


меня заебало правит косяки гмода maybe fix render targets messup on join

--
-- Copyright (C) 2017 DBot
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

timer.Create 'PPM2.ModelChecks', 1, 0, ->
	for task in *PPM2.NetworkedPonyData.RenderTasks
		ent = task.ent
		if IsValid(ent)
			ent.__cachedIsPony = ent\IsPony()

	for ply in *player.GetAll()
		if not ply\IsDormant()
			ply.__cachedIsPony = ply\IsPony()
			ponydata = ply\GetPonyData()

			if ply.__cachedIsPony
				if (not ponydata or ponydata\GetHideWeapons()) and not hook.Run('SuppressPonyWeaponsHide', ply) and not ply.RenderOverride
					for wep in *ply\GetWeapons()
						if wep
							if not hook.Run('ShouldDrawPonyWeapon', ply, wep) and (not wep.ShouldPonyDraw or not wep\ShouldPonyDraw(ply))
								wep\SetNoDraw(true)
								wep.__ppm2_weapon_hit = true
							elseif wep.__ppm2_weapon_hit
								wep\SetNoDraw(false)
								ply.__ppm2_weapon_hit = false
				else
					for wep in *ply\GetWeapons()
						if wep and wep.__ppm2_weapon_hit
							wep\SetNoDraw(false)
							ply.__ppm2_weapon_hit = false

PlayerRespawn = ->
	ent = net.ReadEntity()
	return if not IsValid(ent)
	return if not ent\GetPonyData()
	ent\GetPonyData()\PlayerRespawn()

PlayerDeath = ->
	ent = net.ReadEntity()
	return if not IsValid(ent)
	return if not ent\GetPonyData()
	ent\GetPonyData()\PlayerDeath()

lastDataSend = 0
lastDataReceived = 0
net.Receive 'PPM2.RequestPonyData', ->
	return if lastDataReceived > RealTime()
	lastDataReceived = RealTime() + 10
	RunConsoleCommand('ppm2_reload')

net.Receive 'PPM2.PlayerRespawn', PlayerRespawn
net.Receive 'PPM2.PlayerDeath', PlayerDeath

concommand.Add 'ppm2_require', ->
	net.Start('PPM2.Require')
	net.SendToServer()
	PPM2.Message 'Requesting pony data...'

concommand.Add 'ppm2_reload', ->
	return if lastDataSend > RealTime()
	lastDataSend = RealTime() + 10
	instance = PPM2.GetMainData()
	newData = instance\CreateNetworkObject()
	newData\Create()
	instance\SetNetworkData(newData)
	PPM2.Message 'Sending pony data to server...'

if not IsValid(LocalPlayer())
	times = 0
	hook.Add 'Think', 'PPM2.RequireData', ->
		times += 1

		return if times < 200
		hook.Remove 'Think', 'PPM2.RequireData'
		hook.Add 'KeyPress', 'PPM2.RequireData', ->
			hook.Remove 'KeyPress', 'PPM2.RequireData'
			RunConsoleCommand('ppm2_reload')
			timer.Simple 3, -> RunConsoleCommand('ppm2_require')
else
	timer.Simple 0, ->
		RunConsoleCommand('ppm2_reload')
		timer.Simple 3, -> RunConsoleCommand('ppm2_require')

PPM_HINT_COLOR_FIRST = Color(255, 255, 255)
PPM_HINT_COLOR_SECOND = Color(0, 0, 0)

hook.Add 'HUDPaint', 'PPM2.EditorStatus', ->
	lply = LocalPlayer()
	lpos = lply\EyePos()
	for ply in *player.GetAll()
		if ply ~= lply
			if ply\GetNWBool('PPM2.InEditor')
				pos = ply\EyePos()
				dist = pos\Distance(lpos)
				if dist < 250
					pos.z += 10
					alpha = math.Clamp(1.3 - dist / 250, 0.1, 1) * 255
					{:x, :y} = pos\ToScreen()
					draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)
					draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)

concommand.Add 'ppm2_cleanup', ->
	for ent in *ents.GetAll()
		if ent.isPonyPropModel and not IsValid(ent.manePlayer)
			ent\Remove()
	PPM2.Message('All unused models were removed')

timer.Create 'PPM2.ModelCleanup', 60, 0, ->
	for ent in *ents.GetAll()
		if ent.isPonyPropModel and not IsValid(ent.manePlayer)
			ent\Remove()

cvars.AddChangeCallback('mat_picmip', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_generic', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_body', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

return

--
-- Copyright (C) 2017 DBot
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--     http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

timer.Create 'PPM2.ModelChecks', 1, 0, ->
	for task in *PPM2.NetworkedPonyData.RenderTasks
		ent = task.ent
		if IsValid(ent)
			ent.__cachedIsPony = ent\IsPony()

	for ply in *player.GetAll()
		if not ply\IsDormant()
			ply.__cachedIsPony = ply\IsPony()
			ponydata = ply\GetPonyData()

			if ply.__cachedIsPony
				if (not ponydata or ponydata\GetHideWeapons()) and not hook.Run('SuppressPonyWeaponsHide', ply) and not ply.RenderOverride
					for wep in *ply\GetWeapons()
						if wep
							if not hook.Run('ShouldDrawPonyWeapon', ply, wep) and (not wep.ShouldPonyDraw or not wep\ShouldPonyDraw(ply))
								wep\SetNoDraw(true)
								wep.__ppm2_weapon_hit = true
							elseif wep.__ppm2_weapon_hit
								wep\SetNoDraw(false)
								ply.__ppm2_weapon_hit = false
				else
					for wep in *ply\GetWeapons()
						if wep and wep.__ppm2_weapon_hit
							wep\SetNoDraw(false)
							ply.__ppm2_weapon_hit = false

PlayerRespawn = ->
	ent = net.ReadEntity()
	return if not IsValid(ent)
	return if not ent\GetPonyData()
	ent\GetPonyData()\PlayerRespawn()

PlayerDeath = ->
	ent = net.ReadEntity()
	return if not IsValid(ent)
	return if not ent\GetPonyData()
	ent\GetPonyData()\PlayerDeath()

lastDataSend = 0
lastDataReceived = 0
net.Receive 'PPM2.RequestPonyData', ->
	return if lastDataReceived > RealTime()
	lastDataReceived = RealTime() + 10
	RunConsoleCommand('ppm2_reload')

net.Receive 'PPM2.PlayerRespawn', PlayerRespawn
net.Receive 'PPM2.PlayerDeath', PlayerDeath

concommand.Add 'ppm2_require', ->
	net.Start('PPM2.Require')
	net.SendToServer()
	PPM2.Message 'Requesting pony data...'

concommand.Add 'ppm2_reload', ->
	return if lastDataSend > RealTime()
	lastDataSend = RealTime() + 10
	instance = PPM2.GetMainData()
	newData = instance\CreateNetworkObject()
	newData\Create()
	instance\SetNetworkData(newData)
	PPM2.Message 'Sending pony data to server...'

if not IsValid(LocalPlayer())
	hook.Add 'KeyPress', 'PPM2.RequireData', ->
		hook.Remove 'KeyPress', 'PPM2.RequireData'
		RunConsoleCommand('ppm2_reload')
		timer.Simple 3, -> RunConsoleCommand('ppm2_require')
else
	timer.Simple 0, ->
		RunConsoleCommand('ppm2_reload')
		timer.Simple 3, -> RunConsoleCommand('ppm2_require')

PPM_HINT_COLOR_FIRST = Color(255, 255, 255)
PPM_HINT_COLOR_SECOND = Color(0, 0, 0)

hook.Add 'HUDPaint', 'PPM2.EditorStatus', ->
	lply = LocalPlayer()
	lpos = lply\EyePos()
	for ply in *player.GetAll()
		if ply ~= lply
			if ply\GetNWBool('PPM2.InEditor')
				pos = ply\EyePos()
				dist = pos\Distance(lpos)
				if dist < 250
					pos.z += 10
					alpha = math.Clamp(1.3 - dist / 250, 0.1, 1) * 255
					{:x, :y} = pos\ToScreen()
					draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)
					draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)

concommand.Add 'ppm2_cleanup', ->
	for ent in *ents.GetAll()
		if ent.isPonyPropModel and not IsValid(ent.manePlayer)
			ent\Remove()
	PPM2.Message('All unused models were removed')

timer.Create 'PPM2.ModelCleanup', 60, 0, ->
	for ent in *ents.GetAll()
		if ent.isPonyPropModel and not IsValid(ent.manePlayer)
			ent\Remove()

cvars.AddChangeCallback('mat_picmip', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_generic', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_body', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

return


Fix PonyDataInstance:SetNetworkOnChange being ignored on PonyDataInstance:UpdateController

--
-- 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.

file.CreateDir('ppm2')
file.CreateDir('ppm2/backups')
file.CreateDir('ppm2/thumbnails')

if canon_presets = include('ppm2/client/canon_presets.lua')
	for presetname, payload in pairs(canon_presets)
		if not file.Exists('ppm2/' .. presetname .. '.dat', 'DATA')
			file.Write('ppm2/' .. presetname .. '.dat', payload)

for _, ffind in ipairs file.Find('ppm2/*.txt', 'DATA')
	fTarget = ffind\sub(1, -5)
	-- maybe joined server with old ppm2 and new clear _current was generated
	if not file.Exists('ppm2/' .. fTarget .. '.dat', 'DATA')
		fRead = file.Read('ppm2/' .. ffind, 'DATA')
		if json = util.JSONToTable(fRead)
			tab = {}

			for key, value in pairs json
				if luatype(value) == 'table'
					tab[key] = Color(value.r, value.g, value.b, value.a)
				else
					tab[key] = value

			buf = DLib.GON.Serialize(tab)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()

class PonyDataInstance
	@DATA_DIR = "ppm2/"
	@DATA_DIR_BACKUP = "ppm2/backups/"

	@FindFiles = =>
		output = [str\sub(1, #str - 4) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@FindInstances = =>
		output = [@(str\sub(1, #str - 4)) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@PONY_DATA = PPM2.PonyDataRegistry

	@PONY_DATA_MAPPING = {getFunc\lower(), key for key, {:getFunc} in pairs @PONY_DATA}
	@PONY_DATA_MAPPING[key] = key for key, value in pairs @PONY_DATA

	for key, data in pairs @PONY_DATA
		continue unless data.enum
		data.enum = [arg\upper() for _, arg in ipairs data.enum]
		data.enumMapping = {}
		data.enumMappingBackward = {}
		i = -1
		for _, enumVal in ipairs data.enum
			i += 1
			data.enumMapping[i] = enumVal
			data.enumMappingBackward[enumVal] = i
	for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max, :default} in pairs @PONY_DATA
		@__base["Get#{getFunc}"] = => @dataTable[key]
		@__base["GetMin#{getFunc}"] = => min if min
		@__base["GetMax#{getFunc}"] = => max if max
		@__base["Enum#{getFunc}"] = => enum if enum
		@__base["Get#{getFunc}Types"] = => enum if enum

		@["GetMin#{getFunc}"] = => min if min
		@["GetMax#{getFunc}"] = => max if max
		@["GetDefault#{getFunc}"] = default
		@["GetEnum#{getFunc}"] = => enum if enum
		@["Enum#{getFunc}"] = => enum if enum

		if enumMapping
			@__base["Get#{getFunc}Enum"] = => enumMapping[@dataTable[key]] or enumMapping[0] or @dataTable[key]
			@__base["GetEnum#{getFunc}"] = @__base["Get#{getFunc}Enum"]
		@__base["Reset#{getFunc}"] = => @["Set#{getFunc}"](@, default())
		@__base["Set#{getFunc}"] = (val = defValue, ...) =>
			if luatype(val) == 'string' and enumMappingBackward
				newVal = enumMappingBackward[val\upper()]
				val = newVal if newVal
			newVal = fix(val)
			oldVal = @dataTable[key]
			@dataTable[key] = newVal
			@ValueChanges(key, oldVal, newVal, ...) if oldVal ~= newVal

	new: (filename, data, readIfExists = true) =>
		@SetFilename(filename)

		@updateNWObject = true
		@networkNWObject = true
		@rawData = data
		@dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
		@saveOnChange = false

		if data
			@Deserialize(data)
		elseif @exists and readIfExists
			@ReadFromDisk()

	WriteNetworkData: =>
		for _, {:strName, :writeFunc, :getName, :defValue} in ipairs PPM2.NetworkedPonyData.NW_Vars
			if @["Get#{getName}"]
				writeFunc(@["Get#{getName}"](@))
			else
				writeFunc(defValue)

	Copy: (fileName = @filename) =>
		copyOfData = {}

		for key, val in pairs @dataTable
			switch luatype(val)
				when 'number', 'string', 'boolean'
					copyOfData[key] = val
				when 'table', 'Color'
					copyOfData[key] = IsColor(val) and Color(val) or Color()

		newData = @@(fileName, copyOfData, false)
		return newData

	CreateCustomNetworkObject: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData

	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData

	ApplyDataToObject: (target, ...) =>
		for key, value in pairs @GetAsNetworked()
			error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
			target["Set#{key}"](target, value, ...)

	UpdateController: (...) => @ApplyDataToObject(@nwObj, @networkNWObject, ...)
	CreateController: (...) => @CreateNetworkObject(false, ...)
	CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

	Reset: => @['Reset' .. getFunc](@) for k, {:getFunc} in pairs @@PONY_DATA

	@ERR_MISSING_PARAMETER = 4
	@ERR_MISSING_CONTENT = 5

	GetSaveOnChange: => @saveOnChange
	SaveOnChange: => @saveOnChange
	SetSaveOnChange: (val = false) => @saveOnChange = val

	ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
		if @nwObj and @updateNWObject
			{:getFunc} = @@PONY_DATA[key]
			@nwObj["Set#{getFunc}"](@nwObj, newVal, @networkNWObject)

		@Save() if saveNow

	SetFilename: (filename) =>
		@filename = filename
		@fullPath = "#{@@DATA_DIR}#{filename}.dat"
		@thumbnailPath = "#{@@DATA_DIR}thumbnails/#{filename}.png"
		@absolutePath = "data/#{@@DATA_DIR}#{filename}.dat"
		@exists = file.Exists(@fullPath, 'DATA')
		return @exists

	SetNetworkObject: (nwObj) => @nwObj = nwObj
	SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
	SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal
	GetNetworkOnChange: => @networkNWObject
	GetUpdateOnChange: => @updateNWObject
	GetNetworkObject: => @nwObj

	Exists: => @exists
	FileExists: => @exists
	IsExists: => @exists
	GetFilename: => @filename
	GetFilenameFull: => @filename .. '.dat'
	GetFullPath: => @fullPath
	GetAbsolutePath: => @absolutePath
	GetBackupPath: => "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.dat"

	GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

	Serialize: =>
		tab = {}

		for key, value in pairs(@dataTable)
			if map = @@PONY_DATA[key]
				if map.enum
					tab[key] = map.enumMapping[value] or map.enumMapping[map.default()]
				elseif map.serialize
					tab[key] = map.serialize(value)
				else
					tab[key] = value

		return DLib.GON.Serialize(tab)

	ReadFromDisk: =>
		return false if not @exists
		fRead = file.Read(@fullPath, 'DATA')
		return false if not fRead or fRead == ''

		buf = DLib.BytesBuffer(fRead)

		if buf\ReadUByte() == 10
			buf\Seek(0)
			tag = DLib.NBT.TagCompound()
			return false if not tag\ReadFile(buf)
			return @Deserialize(tag)
		else
			buf\Seek(0)
			return @Deserialize(DLib.GON.Deserialize(buf))

	FixNBTValue: (mapData, value) =>
		if mapData.enum and type(value) == 'string'
			mapData.fix(mapData.enumMappingBackward[value\upper()])
		elseif mapData.type == 'COLOR'
			if IsColor(value)
				mapData.fix(Color(value))
			else
				mapData.fix(Color(value[1] + 128, value[2] + 128, value[3] + 128, value[4] + 128))
		elseif mapData.type == 'BOOLEAN'
			if type(value) == 'boolean'
				mapData.fix(value)
			else
				mapData.fix(value == 1)
		else
			mapData.fix(value)

	DeserializeValue: (mapData, value) =>
		if mapData.enum and type(value) == 'string'
			return mapData.fix(mapData.enumMappingBackward[value\upper()])

		return mapData.fix(value)

	Deserialize: (data) =>
		fixNBT = false

		if luatype(data) == 'NBTCompound'
			data = data\GetValue()
			fixNBT = true

		dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}

		for key, value2 in pairs(data)
			key = key\lower()
			if map = @@PONY_DATA_MAPPING[key]
				if mapData = @@PONY_DATA[map]
					@dataTable[key] = @DeserializeValue(mapData, value2) if not fixNBT
					@dataTable[key] = @FixNBTValue(mapData, value2) if fixNBT
					dataTable[key] = nil

		@dataTable[k] = v for k, v in pairs(dataTable)

	SaveAs: (path = @fullPath) =>
		buf = @Serialize()
		stream = file.Open(path, 'wb', 'DATA')
		error('Unable to open ' .. path .. '!') if not stream
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

	WriteThumbnail: (path = @thumbnailPath) =>
		buildingModel = ClientsideModel('models/ppm/ppm2_stage.mdl', RENDERGROUP_OTHER)
		buildingModel\SetNoDraw(true)
		buildingModel\SetModelScale(0.9)
		model = ClientsideModel('models/ppm/player_default_base_new_nj.mdl')

		with model
			\SetNoDraw(true)
			data = @CreateCustomController(model)
			.__PPM2_PonyData = data
			ctrl = data\GetRenderController()

			if bg = data\GetBodygroupController()
				bg\ApplyBodygroups()

			with model\PPMBonesModifier()
				\ResetBones()
				hook.Call('PPM2.SetupBones', nil, model, data)
				\Think(true)

			\SetSequence(22)
			\FrameAdvance(0)

		timer.Simple 0.5, ->
			renderTarget = GetRenderTarget('ppm2_save_thumbnailPath_generate2', 1024, 1024, false)
			renderTarget\Download()
			render.PushRenderTarget(renderTarget)
			--render.SuppressEngineLighting(true)
			render.Clear(0, 0, 0, 255, true, true)
			cam.Start3D(Vector(49.373046875, -35.021484375, 58.332901000977), Angle(0, 141, 0), 90, 0, 0, 1024, 1024)

			buildingModel\DrawModel()

			with model
				data = .__PPM2_PonyData
				ctrl = data\GetRenderController()

				if textures = ctrl\GetTextureController()
					textures\CompileTextures()

				if bg = data\GetBodygroupController()
					bg\ApplyBodygroups()

				with model\PPMBonesModifier()
					\ResetBones()
					hook.Call('PPM2.SetupBones', nil, model, data)
					\Think(true)

				with ctrl
					\DrawModels()
					\HideModels(true)
					\PreDraw(model, true)

				\DrawModel()
				ctrl\PostDraw(model, true)

			cam.End3D()

			data = render.Capture({
				format: 'png'
				x: 0
				y: 0
				w: 1024
				h: 1024
				alpha: false
			})

			model\Remove()
			buildingModel\Remove()

			file.Write(path, data)

			--render.SuppressEngineLighting(false)
			render.PopRenderTarget()

	Save: (doBackup = true, saveThumbnail = true) =>
		file.Rename(@fullPath, @GetBackupPath()) if doBackup and @exists
		buf = @SaveAs(@fullPath)
		@WriteThumbnail(@thumbnailPath) if saveThumbnail
		@exists = true
		return buf

PPM2.PonyDataInstance = PonyDataInstance

PPM2.MainDataInstance = nil
PPM2.GetMainData = ->
	if not PPM2.MainDataInstance
		PPM2.MainDataInstance = PonyDataInstance('_current', nil, true, true)
		if not PPM2.MainDataInstance\FileExists()
			PPM2.MainDataInstance\Save()
	return PPM2.MainDataInstance

--
-- 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.

file.CreateDir('ppm2')
file.CreateDir('ppm2/backups')
file.CreateDir('ppm2/thumbnails')

if canon_presets = include('ppm2/client/canon_presets.lua')
	for presetname, payload in pairs(canon_presets)
		if not file.Exists('ppm2/' .. presetname .. '.dat', 'DATA')
			file.Write('ppm2/' .. presetname .. '.dat', payload)

for _, ffind in ipairs file.Find('ppm2/*.txt', 'DATA')
	fTarget = ffind\sub(1, -5)
	-- maybe joined server with old ppm2 and new clear _current was generated
	if not file.Exists('ppm2/' .. fTarget .. '.dat', 'DATA')
		fRead = file.Read('ppm2/' .. ffind, 'DATA')
		if json = util.JSONToTable(fRead)
			tab = {}

			for key, value in pairs json
				if luatype(value) == 'table'
					tab[key] = Color(value.r, value.g, value.b, value.a)
				else
					tab[key] = value

			buf = DLib.GON.Serialize(tab)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()

class PonyDataInstance
	@DATA_DIR = "ppm2/"
	@DATA_DIR_BACKUP = "ppm2/backups/"

	@FindFiles = =>
		output = [str\sub(1, #str - 4) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@FindInstances = =>
		output = [@(str\sub(1, #str - 4)) for _, str in ipairs file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.dat')]
		return output

	@PONY_DATA = PPM2.PonyDataRegistry

	@PONY_DATA_MAPPING = {getFunc\lower(), key for key, {:getFunc} in pairs @PONY_DATA}
	@PONY_DATA_MAPPING[key] = key for key, value in pairs @PONY_DATA

	for key, data in pairs @PONY_DATA
		continue unless data.enum
		data.enum = [arg\upper() for _, arg in ipairs data.enum]
		data.enumMapping = {}
		data.enumMappingBackward = {}
		i = -1
		for _, enumVal in ipairs data.enum
			i += 1
			data.enumMapping[i] = enumVal
			data.enumMappingBackward[enumVal] = i
	for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max, :default} in pairs @PONY_DATA
		@__base["Get#{getFunc}"] = => @dataTable[key]
		@__base["GetMin#{getFunc}"] = => min if min
		@__base["GetMax#{getFunc}"] = => max if max
		@__base["Enum#{getFunc}"] = => enum if enum
		@__base["Get#{getFunc}Types"] = => enum if enum

		@["GetMin#{getFunc}"] = => min if min
		@["GetMax#{getFunc}"] = => max if max
		@["GetDefault#{getFunc}"] = default
		@["GetEnum#{getFunc}"] = => enum if enum
		@["Enum#{getFunc}"] = => enum if enum

		if enumMapping
			@__base["Get#{getFunc}Enum"] = => enumMapping[@dataTable[key]] or enumMapping[0] or @dataTable[key]
			@__base["GetEnum#{getFunc}"] = @__base["Get#{getFunc}Enum"]
		@__base["Reset#{getFunc}"] = => @["Set#{getFunc}"](@, default())
		@__base["Set#{getFunc}"] = (val = defValue, ...) =>
			if luatype(val) == 'string' and enumMappingBackward
				newVal = enumMappingBackward[val\upper()]
				val = newVal if newVal
			newVal = fix(val)
			oldVal = @dataTable[key]
			@dataTable[key] = newVal
			@ValueChanges(key, oldVal, newVal, ...) if oldVal ~= newVal

	new: (filename, data, readIfExists = true) =>
		@SetFilename(filename)

		@updateNWObject = true
		@networkNWObject = true
		@rawData = data
		@dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
		@saveOnChange = false

		if data
			@Deserialize(data)
		elseif @exists and readIfExists
			@ReadFromDisk()

	WriteNetworkData: =>
		for _, {:strName, :writeFunc, :getName, :defValue} in ipairs PPM2.NetworkedPonyData.NW_Vars
			if @["Get#{getName}"]
				writeFunc(@["Get#{getName}"](@))
			else
				writeFunc(defValue)

	Copy: (fileName = @filename) =>
		copyOfData = {}

		for key, val in pairs @dataTable
			switch luatype(val)
				when 'number', 'string', 'boolean'
					copyOfData[key] = val
				when 'table', 'Color'
					copyOfData[key] = IsColor(val) and Color(val) or Color()

		newData = @@(fileName, copyOfData, false)
		return newData

	CreateCustomNetworkObject: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData

	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData

	ApplyDataToObject: (target, ...) =>
		for key, value in pairs @GetAsNetworked()
			error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
			target["Set#{key}"](target, value, ...)

	UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
	CreateController: (...) => @CreateNetworkObject(false, ...)
	CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

	Reset: => @['Reset' .. getFunc](@) for k, {:getFunc} in pairs @@PONY_DATA

	@ERR_MISSING_PARAMETER = 4
	@ERR_MISSING_CONTENT = 5

	GetSaveOnChange: => @saveOnChange
	SaveOnChange: => @saveOnChange
	SetSaveOnChange: (val = false) => @saveOnChange = val

	ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
		if @nwObj and @updateNWObject
			{:getFunc} = @@PONY_DATA[key]
			@nwObj["Set#{getFunc}"](@nwObj, newVal, @networkNWObject)

		@Save() if saveNow

	SetFilename: (filename) =>
		@filename = filename
		@fullPath = "#{@@DATA_DIR}#{filename}.dat"
		@thumbnailPath = "#{@@DATA_DIR}thumbnails/#{filename}.png"
		@absolutePath = "data/#{@@DATA_DIR}#{filename}.dat"
		@exists = file.Exists(@fullPath, 'DATA')
		return @exists

	SetNetworkObject: (nwObj) => @nwObj = nwObj
	SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
	SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal
	GetNetworkOnChange: => @networkNWObject
	GetUpdateOnChange: => @updateNWObject
	GetNetworkObject: => @nwObj

	Exists: => @exists
	FileExists: => @exists
	IsExists: => @exists
	GetFilename: => @filename
	GetFilenameFull: => @filename .. '.dat'
	GetFullPath: => @fullPath
	GetAbsolutePath: => @absolutePath
	GetBackupPath: => "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.dat"

	GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

	Serialize: =>
		tab = {}

		for key, value in pairs(@dataTable)
			if map = @@PONY_DATA[key]
				if map.enum
					tab[key] = map.enumMapping[value] or map.enumMapping[map.default()]
				elseif map.serialize
					tab[key] = map.serialize(value)
				else
					tab[key] = value

		return DLib.GON.Serialize(tab)

	ReadFromDisk: =>
		return false if not @exists
		fRead = file.Read(@fullPath, 'DATA')
		return false if not fRead or fRead == ''

		buf = DLib.BytesBuffer(fRead)

		if buf\ReadUByte() == 10
			buf\Seek(0)
			tag = DLib.NBT.TagCompound()
			return false if not tag\ReadFile(buf)
			return @Deserialize(tag)
		else
			buf\Seek(0)
			return @Deserialize(DLib.GON.Deserialize(buf))

	FixNBTValue: (mapData, value) =>
		if mapData.enum and type(value) == 'string'
			mapData.fix(mapData.enumMappingBackward[value\upper()])
		elseif mapData.type == 'COLOR'
			if IsColor(value)
				mapData.fix(Color(value))
			else
				mapData.fix(Color(value[1] + 128, value[2] + 128, value[3] + 128, value[4] + 128))
		elseif mapData.type == 'BOOLEAN'
			if type(value) == 'boolean'
				mapData.fix(value)
			else
				mapData.fix(value == 1)
		else
			mapData.fix(value)

	DeserializeValue: (mapData, value) =>
		if mapData.enum and type(value) == 'string'
			return mapData.fix(mapData.enumMappingBackward[value\upper()])

		return mapData.fix(value)

	Deserialize: (data) =>
		fixNBT = false

		if luatype(data) == 'NBTCompound'
			data = data\GetValue()
			fixNBT = true

		dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}

		for key, value2 in pairs(data)
			key = key\lower()
			if map = @@PONY_DATA_MAPPING[key]
				if mapData = @@PONY_DATA[map]
					@dataTable[key] = @DeserializeValue(mapData, value2) if not fixNBT
					@dataTable[key] = @FixNBTValue(mapData, value2) if fixNBT
					dataTable[key] = nil

		@dataTable[k] = v for k, v in pairs(dataTable)

	SaveAs: (path = @fullPath) =>
		buf = @Serialize()
		stream = file.Open(path, 'wb', 'DATA')
		error('Unable to open ' .. path .. '!') if not stream
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

	WriteThumbnail: (path = @thumbnailPath) =>
		buildingModel = ClientsideModel('models/ppm/ppm2_stage.mdl', RENDERGROUP_OTHER)
		buildingModel\SetNoDraw(true)
		buildingModel\SetModelScale(0.9)
		model = ClientsideModel('models/ppm/player_default_base_new_nj.mdl')

		with model
			\SetNoDraw(true)
			data = @CreateCustomController(model)
			.__PPM2_PonyData = data
			ctrl = data\GetRenderController()

			if bg = data\GetBodygroupController()
				bg\ApplyBodygroups()

			with model\PPMBonesModifier()
				\ResetBones()
				hook.Call('PPM2.SetupBones', nil, model, data)
				\Think(true)

			\SetSequence(22)
			\FrameAdvance(0)

		timer.Simple 0.5, ->
			renderTarget = GetRenderTarget('ppm2_save_thumbnailPath_generate2', 1024, 1024, false)
			renderTarget\Download()
			render.PushRenderTarget(renderTarget)
			--render.SuppressEngineLighting(true)
			render.Clear(0, 0, 0, 255, true, true)
			cam.Start3D(Vector(49.373046875, -35.021484375, 58.332901000977), Angle(0, 141, 0), 90, 0, 0, 1024, 1024)

			buildingModel\DrawModel()

			with model
				data = .__PPM2_PonyData
				ctrl = data\GetRenderController()

				if textures = ctrl\GetTextureController()
					textures\CompileTextures()

				if bg = data\GetBodygroupController()
					bg\ApplyBodygroups()

				with model\PPMBonesModifier()
					\ResetBones()
					hook.Call('PPM2.SetupBones', nil, model, data)
					\Think(true)

				with ctrl
					\DrawModels()
					\HideModels(true)
					\PreDraw(model, true)

				\DrawModel()
				ctrl\PostDraw(model, true)

			cam.End3D()

			data = render.Capture({
				format: 'png'
				x: 0
				y: 0
				w: 1024
				h: 1024
				alpha: false
			})

			model\Remove()
			buildingModel\Remove()

			file.Write(path, data)

			--render.SuppressEngineLighting(false)
			render.PopRenderTarget()

	Save: (doBackup = true, saveThumbnail = true) =>
		file.Rename(@fullPath, @GetBackupPath()) if doBackup and @exists
		buf = @SaveAs(@fullPath)
		@WriteThumbnail(@thumbnailPath) if saveThumbnail
		@exists = true
		return buf

PPM2.PonyDataInstance = PonyDataInstance

PPM2.MainDataInstance = nil
PPM2.GetMainData = ->
	if not PPM2.MainDataInstance
		PPM2.MainDataInstance = PonyDataInstance('_current', nil, true, true)
		if not PPM2.MainDataInstance\FileExists()
			PPM2.MainDataInstance\Save()
	return PPM2.MainDataInstance