Consolidated statement.

Consolidated statement.

rex = require "rex_pcre"
cli = require "cliargs"
lfs = require "lfs"

import concat, insert from table

contains = (t, item) ->
	return true if item == nil
	for k, v in pairs t
		return true if item == v
	false

file_ext = (str) ->
	return "." .. str\match(".([^%.]+)$")

local dir_list, dir_listing
dir_listing = (path, file) ->
	f = path .. '/' .. file
	attr = lfs.attributes(f).mode
	{file, attr, if attr == 'directory' then dir_list(f)}

dir_list = (path) ->
	[dir_listing(path, file) for file in lfs.dir(path) when not contains({'.', '..'}, file)]

map_listing = (structure, func) ->
	for k, v in pairs structure
		func(v, map_listing)

print_listing = (value, func) ->
	{file, mode, data} = value
	print file, mode
	func(data, print_listing) if data

process_files = (filedata, regex, parent) ->
	for k, {file, mode, data} in pairs filedata
		if rex.gsub(file, regex, '')
			if mode == 'directory'
				f = rex.gsub(file, regex, '')\match("^%s*(.-)%s*$")
				f = parent.out .. '/' .. f
				lfs.mkdir(f)
				process_files(data, regex, {in: parent.in .. '/' .. file, out: f})
			else
				ext = file_ext(file)
				newer_f = rex.gsub(file, regex, '')
				new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
				new_f = new_f\gsub('[ ]', '')
				new_f = rex.gsub(new_f, [[([^\d]*)(\d+)[^\d]*$]], [[%1_ep%2]], 1)
				old_f, new_f = parent.in .. '/' .. file, parent.out .. '/' .. new_f .. ext
				lfs.link(old_f, new_f) if not debug
				print "[D: " .. ((debug and "T") or "F") .. "] Link: " .. old_f .. " to: " .. new_f

		else
			print "ERR: ", file, mode

cli\set_name("regnamex.lua")
cli\add_argument("DIR", "directory to scan")
cli\add_argument("TARGET", "target directory")
cli\optarg("IGNORED", "ignored files/directories")
cli\add_flag("-t, --type", "action to take: cp, mv, symlink, defaults to symlink", "symlink")
cli\add_flag("-v, --version", "prints the program version")
cli\add_flag("-d, --debug", "script will simulate excution, print actions.")
args = cli\parse_args()
return if not args

ignored_files = switch type(args["IGNORED"])
	when "string"
		{}
	else
		args["IGNORED"]

export inputdir = args["DIR"]
export files = dir_list(args["DIR"])
export target = args["TARGET"]
export debug = args["d"]

regex = [==[\(.*?\)|\[.*?\]]==]

map_listing(files, print_listing)

processed = process_files(files, regex, {in: inputdir, out: target})
--print_listing(processed)

rex = require "rex_pcre"
cli = require "cliargs"
lfs = require "lfs"

import concat, insert from table

contains = (t, item) ->
	return true if item == nil
	for k, v in pairs t
		return true if item == v
	false

file_ext = (str) ->
	return "." .. str\match(".([^%.]+)$")

local dir_list, dir_listing
dir_listing = (path, file) ->
	f = path .. '/' .. file
	attr = lfs.attributes(f).mode
	{file, attr, if attr == 'directory' then dir_list(f)}

dir_list = (path) ->
	[dir_listing(path, file) for file in lfs.dir(path) when not contains({'.', '..'}, file)]

map_listing = (structure, func) ->
	for k, v in pairs structure
		func(v, map_listing)

print_listing = (value, func) ->
	{file, mode, data} = value
	print file, mode
	func(data, print_listing) if data

process_files = (filedata, regex, parent) ->
	for k, {file, mode, data} in pairs filedata
		if rex.gsub(file, regex, '')
			if mode == 'directory'
				f = rex.gsub(file, regex, '')
				f = f\match("^%s*(.-)%s*$")
				f = parent.out .. '/' .. f
				lfs.mkdir(f)
				process_files(data, regex, {in: parent.in .. '/' .. file, out: f})
			else
				ext = file_ext(file)
				newer_f = rex.gsub(file, regex, '')
				new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
				new_f = new_f\gsub('[ ]', '')
				new_f = rex.gsub(new_f, [[([^\d]*)(\d+)[^\d]*$]], [[%1_ep%2]], 1)
				old_f, new_f = parent.in .. '/' .. file, parent.out .. '/' .. new_f .. ext
				lfs.link(old_f, new_f) if not debug
				print "[D: " .. ((debug and "T") or "F") .. "] Link: " .. old_f .. " to: " .. new_f

		else
			print "ERR: ", file, mode

cli\set_name("regnamex.lua")
cli\add_argument("DIR", "directory to scan")
cli\add_argument("TARGET", "target directory")
cli\optarg("IGNORED", "ignored files/directories")
cli\add_flag("-t, --type", "action to take: cp, mv, symlink, defaults to symlink", "symlink")
cli\add_flag("-v, --version", "prints the program version")
cli\add_flag("-d, --debug", "script will simulate excution, print actions.")
args = cli\parse_args()
return if not args

ignored_files = switch type(args["IGNORED"])
	when "string"
		{}
	else
		args["IGNORED"]

export inputdir = args["DIR"]
export files = dir_list(args["DIR"])
export target = args["TARGET"]
export debug = args["d"]

regex = [==[\(.*?\)|\[.*?\]]==]

map_listing(files, print_listing)

processed = process_files(files, regex, {in: inputdir, out: target})
--print_listing(processed)


Add working render function to Image

Add working render function to Image

* This currently only works for loaded images though (passed into
  constructor via string filename)

require "LovePunk.Graphic"
require "LovePunk.RenderMode"
require "LovePunk.graphics.atlas.Atlas"
require "LovePunk.graphics.atlas.AtlasRegion"
require "LovePunk.graphics.atlas.TextureAtlas"
require "LovePunk.geometry.Rectangle"

export ^

-- Performance-optimized non-animated image. Can be drawn to the screen with transformations.
class Image extends Graphic
	new: (source, clipRect = nil, name = "") =>
		super!

		-- Init variables
		-- Rotation of the image, in degrees.
		@angle = 0

		-- Scale of the image, effects both x and y scale.
		@scale = 1

		-- X scale of the image.
		@scaleX = 1

		-- Y scale of the image.
		@scaleY = 1

		-- X origin of the image, determines transformation point.
		-- Defaults to top-left corner.
		@originX = 0

		-- Y origin of the image, determines transformation point.
		-- Defaults to top-left corner.
		@originY = 0

		-- Optional blend mode to use when drawing this image.
		-- Use constants from the flash.display.BlendMode class.
		@blend = nil

		@__alpha = 1
		@__flipped = false
		@__color = 0x00FFFFFF
		@__red, @__green, @__blue = 1, 1, 1
		@__matrix = LP.matrix

		-- check if the _source or _region were set in a higher class
		if @__source == nil and @__region == nil
			@__imgName = name
			if instance source, TextureAtlas
				@__setAtlasRegion source\getRegion name
			elseif instance source, AtlasRegion
				@__setAtlasRegion source
			elseif LP.renderMode == RenderMode.Hardware
				if type(source) == "string"
					@__imgName = source
				else if name == ""
					@__imgName = @@.__name

				@__setAtlasRegion Atlas.loadImageAsRegion source
			elseif source["type"] and source\type! == "Image" -- Löve Image
				@__setImageSource source
			else
				if type(source) == "string"
					@__imgName = source
				else if name == ""
					@__imgName = @@.__name

				@__setImageSource LP.getImage source

			assert false, "Invalid source image." if @__source == nil and @__region == nil

		if clipRect != nil
			clipRect.width = @__sourceRect\width! if clipRect\width! == 0
			clipRect.height = @__sourceRect\height! if clipRect\height! == 0
			@__region = @__region\clip clipRect if not @__blit -- creat a new clipped region
			@__sourceRect = clipRect

	__setAtlasRegion: (region) =>
		@__blit = false
		@__region = region
		@__sourceRect = Rectangle 0, 0, region\width!, region\height!

	__setImageSource: (img) =>
		@__blit = true
		@__sourceRect = Rectangle 0, 0, img\getWidth!, img\getHeight!
		@__source = img

	-- Renders the image.
	render: (point, camera) =>
		sx = @scale * @scaleX
		sy = @scale * @scaleY

		-- determine drawing location
-- 		@__point.x = point.x + @x - @originX - camera.x * @scrollY
-- 		@__point.y = point.y + @y - @originY - camera.y * @scrollY
		@__point.x = point.x + @x - camera.x * @scrollX
		@__point.y = point.y + @y - camera.y * @scrollY

		if @__blit
			if @angle == 0 and sx == 1 and sy == 1 and @blend == nil
				-- render without transformation
				love.graphics.draw @__source, @__point.x, @__point.y
			else
				-- TODO render from matrix
				-- render with transformation
-- 				@__matrix.b, @__matrix.c = 0, 0
-- 				@__matrix.a = sx
-- 				@__matrix.d = sy
-- 				@__matrix.tx = -@originX * sx
-- 				@__matrix.ty = -@originY * sy
-- 				@__matrix\rotate @angle * LP.RAD! if @angle != 0
-- 				@__matrix.tx += @originX + @__point.x
-- 				@__matrix.ty += @originY + @__point.y

				x, y = @__point.x, @__point.y
				angle = @angle * LP.RAD!
				ox = @originX
				oy = @originY
				love.graphics.draw @__source, x, y, angle, sx, sy, ox, oy
		else -- @__blit
			fsx = LP.screen\fullScaleX!
			fsy = LP.screen\fullScaleY!

			if @angle == 0
				-- recalculation of @__point for scaled origins
				if not (sx == 1 and sy == 1)
					@__point.x = point.x + @x - @originX * sx - camera.x * @scrollX
					@__point.y = point.y + @x - @originY * sy - camera.y * @scrollY

				@__point.x = math.floor @__point.x * fsx
				@__point.y = math.floor @__point.y * fsy

				-- render without rotation
				@__region\draw @__point.x, @__point.y, super.__layer,
					sx * fsx * (if @__flipped then -1 else 1), sy * fsy, @angle,
					@__red, @__green, @__blue, @__alpha
			else
				theta = @angle * LP.RAD!
				cos = math.cos theta
				sin = math.sin theta

				sx *= -1 if @__flipped

				-- optimized matrix math
				b = sx * fsx * sin
				a = sx * fsx * cos

				d = sy * fsy * cos
				a = sy * fsy * -sin

				tx = -@originX * sx
				ty = -@originY * sy
				tx1 = tx * cos - ty * sin
				ty = ((tx * sin + ty * cos) + @originY + @__point.y) * fsy
				tx = (tx1 + @originX + @__point.x) * fsx

				@__region\drawMatrix tx, ty, a, b, c, d, @__layer, @__red, @__green, @__blue, @__alpha

	width: => @__source\getWidth!
	height: => @__source\getHeight!

require "LovePunk.Graphic"
require "LovePunk.RenderMode"
require "LovePunk.graphics.atlas.Atlas"
require "LovePunk.graphics.atlas.AtlasRegion"
require "LovePunk.graphics.atlas.TextureAtlas"
require "LovePunk.geometry.Rectangle"

export ^

-- Performance-optimized non-animated image. Can be drawn to the screen with transformations.
class Image extends Graphic
	new: (source, clipRect = nil, name = "") =>
		super!

		-- Init variables
		-- Rotation of the image, in degrees.
		@angle = 0

		-- Scale of the image, effects both x and y scale.
		@scale = 1

		-- X scale of the image.
		@scaleX = 1

		-- Y scale of the image.
		@scaleY = 1

		-- X origin of the image, determines transformation point.
		-- Defaults to top-left corner.
		@originX = 0

		-- Y origin of the image, determines transformation point.
		-- Defaults to top-left corner.
		@originY = 0

		-- Optional blend mode to use when drawing this image.
		-- Use constants from the flash.display.BlendMode class.
		@blend = nil

		@__alpha = 1
		@__flipped = false
		@__color = 0x00FFFFFF
		@__red, @__green, @__blue = 1, 1, 1
		@__matrix = LP.matrix

		-- check if the _source or _region were set in a higher class
		if @__source == nil and @__region == nil
			@__imgName = name
			if instance source, TextureAtlas
				@__setAtlasRegion source\getRegion name
			elseif instance source, AtlasRegion
				@__setAtlasRegion source
			elseif LP.renderMode == RenderMode.Hardware
				if type(source) == "string"
					@__imgName = source
				else if name == ""
					@__imgName = @@.__name

				@__setAtlasRegion Atlas.loadImageAsRegion source
			elseif source["type"] and source\type! == "Image" -- Löve Image
				@__setImageSource source
			else
				if type(source) == "string"
					@__imgName = source
				else if name == ""
					@__imgName = @@.__name

				@__setImageSource LP.getImage source

			assert false, "Invalid source image." if @__source == nil and @__region == nil

		if clipRect != nil
			clipRect.width = @__sourceRect\width! if clipRect\width! == 0
			clipRect.height = @__sourceRect\height! if clipRect\height! == 0
			@__region = @__region\clip clipRect if not @__blit -- creat a new clipped region
			@__sourceRect = clipRect

	__setAtlasRegion: (region) =>
		@__blit = false
		@__region = region
		@__sourceRect = Rectangle 0, 0, region\width!, region\height!

	__setImageSource: (img) =>
		@__blit = false
		@__sourceRect = Rectangle 0, 0, img\getWidth!, img\getHeight!
		@__source = img

	-- Renders the image.
	render: (point, camera) =>


motivate: trim whitespace

motivate: trim whitespace

PickOne = (l) ->
 l[math.random #l]

phrases = {
  "You're doing good work, %s"
  "%s, you are an inspiration to all of us"
  "Kudos, %s, kudos."
  "Keep up the great work, %s"
  "You're awesome, %s"
  "Keep on truckin', %s"
  "High five, %s, that's awesome"
  "%s: rock on!"
}

PRIVMSG:
  '^%pmotivate (.*)$': (s, d, n) => say PickOne(phrases), ivar2.util.trim(n)
  '^%^5 (.*)$': => reply '⁵!!'
  '^%^5$': => say '⁵!!'

PickOne = (l) ->
 l[math.random #l]

phrases = {
  "You're doing good work, %s"
  "%s, you are an inspiration to all of us"
  "Kudos, %s, kudos."
  "Keep up the great work, %s"
  "You're awesome, %s"
  "Keep on truckin', %s"
  "High five, %s, that's awesome"
  "%s: rock on!"
}

PRIVMSG:
  '^%pmotivate (.*)$': (s, d, n) => say PickOne(phrases), n
  '^%^5 (.*)$': => reply '⁵!!'
  '^%^5$': => say '⁵!!'


translate: Fix trailing whitespace.

translate: Fix trailing whitespace.

Former-commit-id: cf7e8b0bcfe45d8fa8ffa3c9e21438c229cebd58 [formerly d49fd050909cc5f340cfc6b1ef2d9c1b8ea7a378]
Former-commit-id: fc2e587cdb7772766f01ace5e98a8ecdc82c0761

-- google translate
json = require'json'
simplehttp = require'simplehttp'

languages =
  "af": "Afrikaans",
  "sq": "Albanian",
  "ar": "Arabic",
  "az": "Azerbaijani",
  "eu": "Basque",
  "bn": "Bengali",
  "be": "Belarusian",
  "bg": "Bulgarian",
  "ca": "Catalan",
  "zh-CN": "Simplified Chinese",
  "zh-TW": "Traditional Chinese",
  "hr": "Croatian",
  "cs": "Czech",
  "da": "Danish",
  "nl": "Dutch",
  "en": "English",
  "eo": "Esperanto",
  "et": "Estonian",
  "tl": "Filipino",
  "fi": "Finnish",
  "fr": "French",
  "gl": "Galician",
  "ka": "Georgian",
  "de": "German",
  "el": "Greek",
  "gu": "Gujarati",
  "ht": "Haitian Creole",
  "iw": "Hebrew",
  "hi": "Hindi",
  "hu": "Hungarian",
  "is": "Icelandic",
  "id": "Indonesian",
  "ga": "Irish",
  "it": "Italian",
  "ja": "Japanese",
  "kn": "Kannada",
  "ko": "Korean",
  "la": "Latin",
  "lv": "Latvian",
  "lt": "Lithuanian",
  "mk": "Macedonian",
  "ms": "Malay",
  "mt": "Maltese",
  "no": "Norwegian",
  "fa": "Persian",
  "pl": "Polish",
  "pt": "Portuguese",
  "ro": "Romanian",
  "ru": "Russian",
  "sr": "Serbian",
  "sk": "Slovak",
  "sl": "Slovenian",
  "es": "Spanish",
  "sw": "Swahili",
  "sv": "Swedish",
  "ta": "Tamil",
  "te": "Telugu",
  "th": "Thai",
  "tr": "Turkish",
  "uk": "Ukrainian",
  "ur": "Urdu",
  "vi": "Vietnamese",
  "cy": "Welsh",
  "yi": "Yiddish"

getCode = (language) ->
  for code, lang in pairs languages
    if lang\lower! == language\lower!
      return code
    else if language\lower! == code\lower!
      return code

urlEncode = (str, space) ->
  space = space or '+'
  str = tostring(str)

  str = str\gsub '([^%w ])', (c) ->
    string.format  "%%%02X", string.byte(c)
  return str\gsub(' ', space)

buildQueryStr = (tbl) ->
  out = {}
  for k,v in pairs tbl
    table.insert out, k..'='..urlEncode(v)
  table.concat out, '&'

translate = (origin, target, term, cb) ->
  origin = getCode(origin) or 'auto'
  target = getCode(target) or 'en'
  args =
    client: 'p'
    hl: 'en'
    multires: 1
    sc: 1
    sl: origin
    ssel: 0
    tl: target
    tsel: 0
    uptl: "en"
    text: term

  simplehttp {
    url:"http://translate.google.com/translate_a/t?" .. buildQueryStr(args)
    headers: {
      "User-Agent": "Mozilla/5.0"
      "Accept-Charset": "UTF-8"
    }
  }, (data) ->
    if #data > 4
      parsed = json.decode data
      language = parsed.src
      out = table.concat [k.trans for k in *parsed.sentences]
      if #out > 0
        cb(out)

PRIVMSG:
  '^%ptranslate ([%w%-]+) ([%w%-]+) (.*)$': (source, destination, origin, target, term) =>
    translate origin, target, term, (text) ->
      say text
  '^%ptr ([%w%-]+) ([%w%-]+) (.*)$': (source, destination, origin, target, term) =>
    translate origin, target, term, (text) ->
      say text
  '^%ptlolno (.*)$': (source, destination, text) =>
    translate 'auto', 'zh-TW', text, (text) ->
      translate 'zh-TW', 'no', text, (text) ->
        say text
  '^%ptlol (.*)$': (source, destination, text) =>
    translate 'auto', 'zh-CN', text, (text) ->
      translate 'zh-CN', 'no', text, (text) ->
        translate 'no', 'ja', text, (text) ->
          translate 'ja', 'en', text, (text) ->
            say text


Add Tween test stubs as a reminder of what still needs tests


Copy boolean type

Copy boolean type

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

class PonyDataInstance
    @DATA_DIR = "ppm2/"
    @DATA_DIR_BACKUP = "ppm2/backups/"
    @PONY_DATA = {
        'age': {
            default: -> PPM2.AGE_ADULT
            getFunc: 'Age'
            enum: {'FILLY', 'ADULT', 'MATURE'}
            fix: (arg = PPM2.AGE_ADULT) -> math.Clamp(tonumber(arg) or PPM2.AGE_ADULT, 0, 2)
            min: 0
            max: 2
        }

        'race': {
            default: -> PPM2.RACE_EARTH
            getFunc: 'Race'
            enum: [arg for arg in *PPM2.RACE_ENUMS]
            fix: (arg = PPM2.RACE_EARTH) -> math.Clamp(tonumber(arg) or PPM2.RACE_EARTH, 0, 3)
            min: 0
            max: 3
        }

        'gender': {
            default: -> PPM2.GENDER_FEMALE
            getFunc: 'Gender'
            enum: [arg for arg in *PPM2.AGE_ENUMS]
            fix: (arg = PPM2.GENDER_FEMALE) -> math.Clamp(tonumber(arg) or PPM2.GENDER_FEMALE, 0, 1)
            min: 0
            max: 1
        }

        'weight': {
            default: -> 1
            getFunc: 'Weight'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_WEIGHT, PPM2.MAX_WEIGHT)
            min: PPM2.MIN_WEIGHT
            max: PPM2.MAX_WEIGHT
        }

        'eyelash': {
            default: -> 0
            getFunc: 'EyelashType'
            enum: [arg for arg in *PPM2.EyelashTypes]
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_EYELASHES, PPM2.MAX_EYELASHES)
            min: PPM2.MIN_EYELASHES
            max: PPM2.MAX_EYELASHES
        }

        'tail': {
            default: -> 0
            getFunc: 'TailType'
            enum: [arg for arg in *PPM2.AvaliableTails] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_TAILS, PPM2.MAX_TAILS)
            min: PPM2.MIN_TAILS
            max: PPM2.MAX_TAILS
        }

        'tail_new': {
            default: -> 0
            getFunc: 'TailTypeNew'
            enum: [arg for arg in *PPM2.AvaliableTailsNew] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_TAILS_NEW, PPM2.MAX_TAILS_NEW)
            min: PPM2.MIN_TAILS_NEW
            max: PPM2.MAX_TAILS_NEW
        }

        'mane': {
            default: -> 0
            getFunc: 'ManeType'
            enum: [arg for arg in *PPM2.AvaliableUpperManes] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_UPPER_MANES, PPM2.MAX_UPPER_MANES)
            min: PPM2.MIN_UPPER_MANES
            max: PPM2.MAX_UPPER_MANES
        }

        'mane_new': {
            default: -> 0
            getFunc: 'ManeTypeNew'
            enum: [arg for arg in *PPM2.AvaliableUpperManesNew] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_UPPER_MANES_NEW, PPM2.MAX_UPPER_MANES_NEW)
            min: PPM2.MIN_UPPER_MANES_NEW
            max: PPM2.MAX_UPPER_MANES_NEW
        }

        'manelower': {
            default: -> 0
            getFunc: 'ManeTypeLower'
            enum: [arg for arg in *PPM2.AvaliableLowerManes] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_LOWER_MANES, PPM2.MAX_LOWER_MANES)
            min: PPM2.MIN_LOWER_MANES
            max: PPM2.MAX_LOWER_MANES
        }

        'manelower_new': {
            default: -> 0
            getFunc: 'ManeTypeLowerNew'
            enum: [arg for arg in *PPM2.AvaliableLowerManesNew] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_LOWER_MANES_NEW, PPM2.MAX_LOWER_MANES_NEW)
            min: PPM2.MIN_LOWER_MANES_NEW
            max: PPM2.MAX_LOWER_MANES_NEW
        }

        'tailsize': {
            default: -> 1
            getFunc: 'TailSize'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_TAIL_SIZE, PPM2.MAX_TAIL_SIZE)
            min: PPM2.MIN_TAIL_SIZE
            max: PPM2.MAX_TAIL_SIZE
        }

        'eye_iris_size': {
            default: -> 1
            getFunc: 'IrisSize'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_IRIS, PPM2.MAX_IRIS)
            min: PPM2.MIN_IRIS
            max: PPM2.MAX_IRIS
        }

        'hole_width': {
            default: -> 1
            getFunc: 'HoleWidth'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_PUPIL_SIZE, PPM2.MAX_PUPIL_SIZE)
            min: PPM2.MIN_PUPIL_SIZE
            max: PPM2.MAX_PUPIL_SIZE
        }

        'eye_hole_size': {
            default: -> .8
            getFunc: 'HoleSize'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or .8, PPM2.MIN_HOLE, PPM2.MAX_HOLE)
            min: PPM2.MIN_HOLE
            max: PPM2.MAX_HOLE
        }

        'cmark': {
            default: -> true
            getFunc: 'CMark'
            fix: (arg = true) -> tobool(arg)
        }

        'cmark_type': {
            default: -> 4
            getFunc: 'CMarkType'
            enum: [arg for arg in *PPM2.DefaultCutiemarks]
            fix: (arg = 4) -> math.Clamp(tonumber(arg) or 4, PPM2.MIN_CMARK, PPM2.MAX_CMARK)
            min: PPM2.MIN_CMARK
            max: PPM2.MAX_CMARK
        }

        'cmark_url': {
            default: -> ''
            getFunc: 'CMarkURL'
            fix: (arg = '') ->
                arg = tostring(arg)
                if arg\find('^https?://')
                    return arg
                else
                    return ''
        }

        'eye_derp': {
            default: -> false
            getFunc: 'DerpEyes'
            fix: (arg = true) -> tobool(arg)
        }

        'eye_derp_strength': {
            default: -> 1
            getFunc: 'DerpEyesStrength'
            fix: (arg = 1) -> math.Clamp(tonumber(arg), PPM2.MIN_DERP_STRENGTH, PPM2.MAX_DERP_STRENGTH)
            min: PPM2.MIN_DERP_STRENGTH
            max: PPM2.MAX_DERP_STRENGTH
        }

        'eye_bg': {
            default: -> Color(255, 255, 255)
            getFunc: 'EyeBackground'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'eye_hole': {
            default: -> Color(0, 0, 0)
            getFunc: 'EyeHole'
            fix: (arg = Color(0, 0, 0)) ->
                if type(arg) ~= 'table'
                    return Color(0, 0, 0)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(0, 0, 0)
        }

        'eye_iris1': {
            default: -> Color(200, 200, 200)
            getFunc: 'EyeIrisTop'
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        'eye_iris2': {
            default: -> Color(200, 200, 200)
            getFunc: 'EyeIrisBottom'
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        'eye_irisline1': {
            default: -> Color(255, 255, 255)
            getFunc: 'EyeIrisLine1'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'eye_irisline2': {
            default: -> Color(255, 255, 255)
            getFunc: 'EyeIrisLine2'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'body': {
            default: -> Color(255, 255, 255)
            getFunc: 'BodyColor'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'eye_lines': {
            default: -> true
            getFunc: 'EyeLines'
            fix: (arg = true) -> tobool(arg)
        }

        'separate_eyes': {
            default: -> false
            getFunc: 'SeparateEyes'
            fix: (arg = false) -> tobool(arg)
        }

        'separate_mane': {
            default: -> false
            getFunc: 'SeparateMane'
            fix: (arg = false) -> tobool(arg)
        }

        'socks': {
            default: -> false
            getFunc: 'Socks'
            fix: (arg = false) -> tobool(arg)
        }

        'suit': {
            default: -> 0
            getFunc: 'Bodysuit'
            enum: [arg for arg in *PPM2.AvaliablePonySuits]
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_SUIT, PPM2.MAX_SUIT)
            min: PPM2.MIN_SUIT
            max: PPM2.MAX_SUIT
        }
    }

    for {internal, publicName} in *{{'left', 'Left'}, {'right', 'Right'}}
        @PONY_DATA["eye_bg_#{internal}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "EyeBackground#{publicName}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["eye_hole_#{internal}"] = {
            default: -> Color(0, 0, 0)
            getFunc: "EyeHole#{publicName}"
            fix: (arg = Color(0, 0, 0)) ->
                if type(arg) ~= 'table'
                    return Color(0, 0, 0)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(0, 0, 0)
        }

        @PONY_DATA["eye_iris1_#{internal}"] = {
            default: -> Color(200, 200, 200)
            getFunc: "EyeIrisTop#{publicName}"
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        @PONY_DATA["eye_iris2_#{internal}"] = {
            default: -> Color(200, 200, 200)
            getFunc: "EyeIrisBottom#{publicName}"
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        @PONY_DATA["eye_irisline1_#{internal}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "EyeIrisLine1#{publicName}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["eye_irisline2_#{internal}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "EyeIrisLine2#{publicName}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["eye_lines_#{internal}"] = {
            default: -> true
            getFunc: "EyeLines#{publicName}"
            fix: (arg = true) -> tobool(arg)
        }

        @PONY_DATA["eye_iris_size_#{internal}"] = {
            default: -> 1
            getFunc: "IrisSize#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_IRIS, PPM2.MAX_IRIS)
            min: PPM2.MIN_IRIS
            max: PPM2.MAX_IRIS
        }

        @PONY_DATA["eye_derp_#{internal}"] = {
            default: -> false
            getFunc: "DerpEyes#{publicName}"
            fix: (arg = true) -> tobool(arg)
        }

        @PONY_DATA["eye_derp_strength_#{internal}"]= {
            default: -> 1
            getFunc: "DerpEyesStrength#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg), PPM2.MIN_DERP_STRENGTH, PPM2.MAX_DERP_STRENGTH)
            min: PPM2.MIN_DERP_STRENGTH
            max: PPM2.MAX_DERP_STRENGTH
        }

        @PONY_DATA["hole_width_#{internal}"] = {
            default: -> 1
            getFunc: "HoleWidth#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_PUPIL_SIZE, PPM2.MAX_PUPIL_SIZE)
            min: PPM2.MIN_PUPIL_SIZE
            max: PPM2.MAX_PUPIL_SIZE
        }

        @PONY_DATA["eye_hole_size#{internal}"] = {
            default: -> .8
            getFunc: "HoleSize#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or .8, PPM2.MIN_HOLE, PPM2.MAX_HOLE)
            min: PPM2.MIN_HOLE
            max: PPM2.MAX_HOLE
        }

    for i = 1, 6
        @PONY_DATA["mane_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "ManeColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["mane_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "ManeDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["tail_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "TailDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["tail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "TailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["lower_mane_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "LowerManeColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["upper_mane_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "UpperManeColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["lower_mane_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "LowerManeDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["upper_mane_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "UpperManeDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

    for i = 1, PPM2.MAX_BODY_DETAILS
        @PONY_DATA["body_detail_color_#{i}"] = {
            default: -> Color(140, 50, 100)
            getFunc: "BodyDetailColor#{i}"
            fix: (arg = Color(140, 50, 100)) ->
                if type(arg) ~= 'table'
                    return Color(140, 50, 100)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(140, 50, 100)
        }

        @PONY_DATA["body_detail_#{i}"] = {
            default: -> 0
            getFunc: "BodyDetail#{i}"
            enum: [arg for arg in *PPM2.BodyDetailsEnum]
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_DETAIL, PPM2.MAX_DETAIL)
            min: PPM2.MIN_DETAIL
            max: PPM2.MAX_DETAIL
        }

    @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 *data.enum]
        data.enumMapping = {}
        data.enumMappingBackward = {}
        i = -1
        for enumVal in *data.enum
            i += 1
            data.enumMapping[i] = enumVal
            data.enumMappingBackward[enumVal] = i
    for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max} 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
        @["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["Set#{getFunc}"] = (val = defValue, ...) =>
            if type(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, ...)

    Copy: (fileName = @filename) =>
        copyOfData = {}
        for key, val in pairs @dataTable
            switch type(val)
                when 'number'
                    copyOfData[key] = val
                when 'string'
                    copyOfData[key] = val
                when 'boolean'
                    copyOfData[key] = val
                when 'table'
                    {:r, :g, :b} = val
                    if r and g and b
                        copyOfData[key] = Color(r, g, b)
        newData = @@(fileName, copyOfData, false)
        return newData
    CreateCustomNetworkObject: (ply = LocalPlayer(), ...) =>
        newData = PPM2.NetworkedPonyData(nil, ply)
        newData\SetEntity(ply)
        @ApplyDataToObject(newData, ...)
        return newData
    CreateNetworkObject: (gointToNetwork = true, ...) =>
        newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
        newData\SetIsGoingToNetwork(gointToNetwork)
        newData\SetEntity(LocalPlayer())
        @ApplyDataToObject(newData, ...)
        return newData
    ApplyDataToObject: (target, ...) =>
        for key, value in pairs @GetAsNetworked()
            target["Set#{key}"](target, value, ...) if target["Get#{key}"](target, value) ~= value
    UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
    CreateController: (...) => @CreateNetworkObject(false, ...)
    CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

    new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
        @SetFilename(filename)
        @valid = @isOpen
        @rawData = data
        @dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
        @saveOnChange = true
        if data
            @SetupData(data, true)
        elseif @exists and readIfExists
            @ReadFromDisk(force, doBackup)

    @ERR_MISSING_PARAMETER = 4
    @ERR_MISSING_CONTENT = 5

    GetSaveOnChange: => @saveOnChange
    SaveOnChange: => @saveOnChange
    SetSaveOnChange: (val = true) => @saveOnChange = val
    SetupData: (data, force = false, doBackup = false) =>
        if doBackup or not force
            makeBackup = false
            for key, value in pairs data
                key = key\lower()
                map = @@PONY_DATA_MAPPING[key]
                if not map
                    return @@ERR_MISSING_PARAMETER if not force
                    makeBackup = true
                    break
                mapData = @@PONY_DATA[map]
                if mapData.enum
                    if type(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or type(value) == 'number' and not mapData.enumMapping[value]
                        return @@ERR_MISSING_CONTENT if not force
                        makeBackup = true
                        break
            if doBackup and makeBackup and @exists
                bkName = "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.txt"
                fRead = file.Read(@fpath, 'DATA')
                file.Write(bkName, fRead)

        for key, value in pairs data
            key = key\lower()
            map = @@PONY_DATA_MAPPING[key]
            continue unless map
            mapData = @@PONY_DATA[map]
            if mapData.enum and type(value) == 'string'
                @dataTable[key] = mapData.fix(mapData.enumMappingBackward[value\upper()])
            else
                @dataTable[key] = mapData.fix(value)
    ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
        if @nwObj
            {:getFunc} = @@PONY_DATA[key]
            @nwObj["Set#{getFunc}"](@nwObj, newVal)
        @Save() if saveNow
    SetFilename: (filename) =>
        @filename = filename
        @filenameFull = "#{filename}.txt"
        @fpath = "#{@@DATA_DIR}#{filename}.txt"
        @fpathBackup = "#{@@DATA_DIR}#{filename}.bak.txt"
        @fpathFull = "data/#{@@DATA_DIR}#{filename}.txt"
        @isOpen = @filename ~= nil
        @exists = file.Exists(@fpath, 'DATA')
        return @exists
    SetNetworkData: (nwObj) => @nwObj = nwObj
    SetPonyData: (nwObj) => @nwObj = nwObj
    SetPonyDataController: (nwObj) => @nwObj = nwObj
    SetPonyController: (nwObj) => @nwObj = nwObj
    SetController: (nwObj) => @nwObj = nwObj
    SetDataController: (nwObj) => @nwObj = nwObj

    GetNetworkData: => @nwObj
    GetPonyData: => @nwObj
    GetPonyDataController: => @nwObj
    GetPonyController: => @nwObj
    GetController: => @nwObj
    GetDataController: => @nwObj

    IsValid: => @valid
    Exists: => @exists
    FileExists: => @exists
    IsExists: => @exists
    GetFileName: => @filename
    GetFilename: => @filename
    GetFileNameFull: => @filenameFull
    GetFilenameFull: => @filenameFull
    GetFilePath: => @fpath
    GetFullFilePath: => @fpathFull
    SerealizeValue: (valID = '') =>
        map = @@PONY_DATA[valID]
        return unless map
        val = @dataTable[valID]
        if map.enum
            return map.enumMapping[val] or map.enumMapping[map.default()]
        elseif map.serealize
            return map.serealize(val)
        else
            return val
    Serealize: (prettyPrint = true) =>
        serTab = {key, @SerealizeValue(key) for key, val in pairs @dataTable}
        util.TableToJSON(serTab, prettyPrint)
    GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

    @READ_SUCCESS = 0
    @ERR_FILE_NOT_EXISTS = 1
    @ERR_FILE_EMPTY = 2
    @ERR_FILE_CORRUPT = 3
    ReadFromDisk: (force = false, doBackup = true) =>
        return @@ERR_FILE_NOT_EXISTS unless @exists
        fRead = file.Read(@fpath, 'DATA')
        return @@ERR_FILE_EMPTY if not fRead or fRead == ''
        readTable = util.JSONToTable(fRead)
        return @@ERR_FILE_CORRUPT unless readTable
        return @SetupData(readTable, force, doBackup) or @@READ_SUCCESS
    SaveAs: (path = @fpath) =>
        fCreate = @Serealize()
        file.Write(path, fCreate)
    Save: (doBackup = true) =>
        if doBackup and @exists
            fRead = file.Read(@fpath, 'DATA')
            file.Write(@fpathBackup, fRead)
        @SaveAs(@fpath)
        @exists = true

do
    PARSE_VECTOR = (str = '1.0 1.0 1.0', X = 1, Y = 1, Z = 1) ->
        return Vector(X, Y, Z) if str == ''
        x, y, z = str\match('([0-9.]+) ([0-9.]+) ([0-9.]+)')
        return Vector(tonumber(x) or X, tonumber(y) or Y, tonumber(z) or Z)

    PARSE_COLOR = (str = '1.0 1.0 1.0', r = 255, g = 255, b = 255) ->
        return Color(r, g, b) if str == ''
        {x, y, z} = PARSE_VECTOR(str, r / 255, g / 255, b / 255)
        return Color(x * 255, y * 255, z * 255)

    IMPORT_TABLE = {
        'gender': {
            name: 'Gender'
            func: (arg = 0) ->
                num = tonumber(arg)
                return num == 0 and 'MALE' or 'FEMALE'
        }

        'coatcolor': {
            name: 'BodyColor'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_bg': {
            name: 'EyeBackground'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_grad': {
            name: 'EyeIrisBottom'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_iris': {
            name: 'EyeIrisTop'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line1': {
            name: 'EyeIrisLine1'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line2': {
            name: 'EyeIrisLine2'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'haircolor1': {
            name: {'ManeColor1', 'TailColor1'}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'haircolor2': {
            name: {'ManeColor2', 'TailColor2'}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyejholerssize': {
            name: 'HoleWidth'
            func: (arg = '1') -> tonumber(arg) or 1
        }

        'eyeirissize': {
            name: 'IrisSize'
            func: (arg = '1') -> tonumber(arg) or 1
        }

        'eyeholesize': {
            name: 'HoleSize'
            func: (arg = '0.8') -> tonumber(arg) or 0.8
        }

        'bodyweight': {
            name: 'Weight'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'mane': {
            name: {'ManeType', 'ManeTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'manel': {
            name: {'ManeTypeLower', 'ManeTypeLowerNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tail': {
            name: {'TailType', 'TailTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tailsize': {
            name: 'TailSize'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'cmark': {
            name: 'CMarkType'
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        'cmark_enabled': {
            name: 'CMark'
            func: (arg = '1') -> arg == '1' or arg == '2'
        }
    }

    for i = 1, 8
        IMPORT_TABLE["bodydetail#{i}"] = {
            name: "BodyDetail#{i}"
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        IMPORT_TABLE["bodydetail#{i}_c"] = {
            name: "BodyDetailColor#{i}"
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    for i = 3, 6
        IMPORT_TABLE["haircolor#{i}"] = {
            name: {"ManeDetailColor#{i - 2}", "TailDetailColor#{i - 2}"}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    PPM2.ReadFromOldData = (filename = '_current') ->
        read = file.Read("ppm/#{filename}.txt", 'DATA')
        return false if read == ''
        split = [str\Trim() for str in *string.Explode('\n', read\Replace('\r', ''))]
        outputData = {}

        for line in *split
            varID = line\match('([a-zA-Z0-9_]+)')
            continue if not varID or varID == ''
            continue if not IMPORT_TABLE[varID]
            dt = IMPORT_TABLE[varID]
            value = line\sub(#varID + 2)
            if type(dt.name) ~= 'table'
                outputData[dt.name] = dt.func(value)
            else
                get = dt.func(value)
                outputData[name] = get for name in *dt.name

        data = PonyDataInstance("#{filename}_imported", nil, false)
        for key, value in pairs outputData
            data["Set#{key}"](data, value, false)
        return data, outputData

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

class PonyDataInstance
    @DATA_DIR = "ppm2/"
    @DATA_DIR_BACKUP = "ppm2/backups/"
    @PONY_DATA = {
        'age': {
            default: -> PPM2.AGE_ADULT
            getFunc: 'Age'
            enum: {'FILLY', 'ADULT', 'MATURE'}
            fix: (arg = PPM2.AGE_ADULT) -> math.Clamp(tonumber(arg) or PPM2.AGE_ADULT, 0, 2)
            min: 0
            max: 2
        }

        'race': {
            default: -> PPM2.RACE_EARTH
            getFunc: 'Race'
            enum: [arg for arg in *PPM2.RACE_ENUMS]
            fix: (arg = PPM2.RACE_EARTH) -> math.Clamp(tonumber(arg) or PPM2.RACE_EARTH, 0, 3)
            min: 0
            max: 3
        }

        'gender': {
            default: -> PPM2.GENDER_FEMALE
            getFunc: 'Gender'
            enum: [arg for arg in *PPM2.AGE_ENUMS]
            fix: (arg = PPM2.GENDER_FEMALE) -> math.Clamp(tonumber(arg) or PPM2.GENDER_FEMALE, 0, 1)
            min: 0
            max: 1
        }

        'weight': {
            default: -> 1
            getFunc: 'Weight'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_WEIGHT, PPM2.MAX_WEIGHT)
            min: PPM2.MIN_WEIGHT
            max: PPM2.MAX_WEIGHT
        }

        'eyelash': {
            default: -> 0
            getFunc: 'EyelashType'
            enum: [arg for arg in *PPM2.EyelashTypes]
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_EYELASHES, PPM2.MAX_EYELASHES)
            min: PPM2.MIN_EYELASHES
            max: PPM2.MAX_EYELASHES
        }

        'tail': {
            default: -> 0
            getFunc: 'TailType'
            enum: [arg for arg in *PPM2.AvaliableTails] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_TAILS, PPM2.MAX_TAILS)
            min: PPM2.MIN_TAILS
            max: PPM2.MAX_TAILS
        }

        'tail_new': {
            default: -> 0
            getFunc: 'TailTypeNew'
            enum: [arg for arg in *PPM2.AvaliableTailsNew] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_TAILS_NEW, PPM2.MAX_TAILS_NEW)
            min: PPM2.MIN_TAILS_NEW
            max: PPM2.MAX_TAILS_NEW
        }

        'mane': {
            default: -> 0
            getFunc: 'ManeType'
            enum: [arg for arg in *PPM2.AvaliableUpperManes] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_UPPER_MANES, PPM2.MAX_UPPER_MANES)
            min: PPM2.MIN_UPPER_MANES
            max: PPM2.MAX_UPPER_MANES
        }

        'mane_new': {
            default: -> 0
            getFunc: 'ManeTypeNew'
            enum: [arg for arg in *PPM2.AvaliableUpperManesNew] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_UPPER_MANES_NEW, PPM2.MAX_UPPER_MANES_NEW)
            min: PPM2.MIN_UPPER_MANES_NEW
            max: PPM2.MAX_UPPER_MANES_NEW
        }

        'manelower': {
            default: -> 0
            getFunc: 'ManeTypeLower'
            enum: [arg for arg in *PPM2.AvaliableLowerManes] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_LOWER_MANES, PPM2.MAX_LOWER_MANES)
            min: PPM2.MIN_LOWER_MANES
            max: PPM2.MAX_LOWER_MANES
        }

        'manelower_new': {
            default: -> 0
            getFunc: 'ManeTypeLowerNew'
            enum: [arg for arg in *PPM2.AvaliableLowerManesNew] -- fast copy
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_LOWER_MANES_NEW, PPM2.MAX_LOWER_MANES_NEW)
            min: PPM2.MIN_LOWER_MANES_NEW
            max: PPM2.MAX_LOWER_MANES_NEW
        }

        'tailsize': {
            default: -> 1
            getFunc: 'TailSize'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_TAIL_SIZE, PPM2.MAX_TAIL_SIZE)
            min: PPM2.MIN_TAIL_SIZE
            max: PPM2.MAX_TAIL_SIZE
        }

        'eye_iris_size': {
            default: -> 1
            getFunc: 'IrisSize'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_IRIS, PPM2.MAX_IRIS)
            min: PPM2.MIN_IRIS
            max: PPM2.MAX_IRIS
        }

        'hole_width': {
            default: -> 1
            getFunc: 'HoleWidth'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_PUPIL_SIZE, PPM2.MAX_PUPIL_SIZE)
            min: PPM2.MIN_PUPIL_SIZE
            max: PPM2.MAX_PUPIL_SIZE
        }

        'eye_hole_size': {
            default: -> .8
            getFunc: 'HoleSize'
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or .8, PPM2.MIN_HOLE, PPM2.MAX_HOLE)
            min: PPM2.MIN_HOLE
            max: PPM2.MAX_HOLE
        }

        'cmark': {
            default: -> true
            getFunc: 'CMark'
            fix: (arg = true) -> tobool(arg)
        }

        'cmark_type': {
            default: -> 4
            getFunc: 'CMarkType'
            enum: [arg for arg in *PPM2.DefaultCutiemarks]
            fix: (arg = 4) -> math.Clamp(tonumber(arg) or 4, PPM2.MIN_CMARK, PPM2.MAX_CMARK)
            min: PPM2.MIN_CMARK
            max: PPM2.MAX_CMARK
        }

        'cmark_url': {
            default: -> ''
            getFunc: 'CMarkURL'
            fix: (arg = '') ->
                arg = tostring(arg)
                if arg\find('^https?://')
                    return arg
                else
                    return ''
        }

        'eye_derp': {
            default: -> false
            getFunc: 'DerpEyes'
            fix: (arg = true) -> tobool(arg)
        }

        'eye_derp_strength': {
            default: -> 1
            getFunc: 'DerpEyesStrength'
            fix: (arg = 1) -> math.Clamp(tonumber(arg), PPM2.MIN_DERP_STRENGTH, PPM2.MAX_DERP_STRENGTH)
            min: PPM2.MIN_DERP_STRENGTH
            max: PPM2.MAX_DERP_STRENGTH
        }

        'eye_bg': {
            default: -> Color(255, 255, 255)
            getFunc: 'EyeBackground'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'eye_hole': {
            default: -> Color(0, 0, 0)
            getFunc: 'EyeHole'
            fix: (arg = Color(0, 0, 0)) ->
                if type(arg) ~= 'table'
                    return Color(0, 0, 0)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(0, 0, 0)
        }

        'eye_iris1': {
            default: -> Color(200, 200, 200)
            getFunc: 'EyeIrisTop'
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        'eye_iris2': {
            default: -> Color(200, 200, 200)
            getFunc: 'EyeIrisBottom'
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        'eye_irisline1': {
            default: -> Color(255, 255, 255)
            getFunc: 'EyeIrisLine1'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'eye_irisline2': {
            default: -> Color(255, 255, 255)
            getFunc: 'EyeIrisLine2'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'body': {
            default: -> Color(255, 255, 255)
            getFunc: 'BodyColor'
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        'eye_lines': {
            default: -> true
            getFunc: 'EyeLines'
            fix: (arg = true) -> tobool(arg)
        }

        'separate_eyes': {
            default: -> false
            getFunc: 'SeparateEyes'
            fix: (arg = false) -> tobool(arg)
        }

        'separate_mane': {
            default: -> false
            getFunc: 'SeparateMane'
            fix: (arg = false) -> tobool(arg)
        }

        'socks': {
            default: -> false
            getFunc: 'Socks'
            fix: (arg = false) -> tobool(arg)
        }

        'suit': {
            default: -> 0
            getFunc: 'Bodysuit'
            enum: [arg for arg in *PPM2.AvaliablePonySuits]
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_SUIT, PPM2.MAX_SUIT)
            min: PPM2.MIN_SUIT
            max: PPM2.MAX_SUIT
        }
    }

    for {internal, publicName} in *{{'left', 'Left'}, {'right', 'Right'}}
        @PONY_DATA["eye_bg_#{internal}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "EyeBackground#{publicName}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["eye_hole_#{internal}"] = {
            default: -> Color(0, 0, 0)
            getFunc: "EyeHole#{publicName}"
            fix: (arg = Color(0, 0, 0)) ->
                if type(arg) ~= 'table'
                    return Color(0, 0, 0)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(0, 0, 0)
        }

        @PONY_DATA["eye_iris1_#{internal}"] = {
            default: -> Color(200, 200, 200)
            getFunc: "EyeIrisTop#{publicName}"
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        @PONY_DATA["eye_iris2_#{internal}"] = {
            default: -> Color(200, 200, 200)
            getFunc: "EyeIrisBottom#{publicName}"
            fix: (arg = Color(200, 200, 200)) ->
                if type(arg) ~= 'table'
                    return Color(200, 200, 200)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(200, 200, 200)
        }

        @PONY_DATA["eye_irisline1_#{internal}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "EyeIrisLine1#{publicName}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["eye_irisline2_#{internal}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "EyeIrisLine2#{publicName}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["eye_lines_#{internal}"] = {
            default: -> true
            getFunc: "EyeLines#{publicName}"
            fix: (arg = true) -> tobool(arg)
        }

        @PONY_DATA["eye_iris_size_#{internal}"] = {
            default: -> 1
            getFunc: "IrisSize#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_IRIS, PPM2.MAX_IRIS)
            min: PPM2.MIN_IRIS
            max: PPM2.MAX_IRIS
        }

        @PONY_DATA["eye_derp_#{internal}"] = {
            default: -> false
            getFunc: "DerpEyes#{publicName}"
            fix: (arg = true) -> tobool(arg)
        }

        @PONY_DATA["eye_derp_strength_#{internal}"]= {
            default: -> 1
            getFunc: "DerpEyesStrength#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg), PPM2.MIN_DERP_STRENGTH, PPM2.MAX_DERP_STRENGTH)
            min: PPM2.MIN_DERP_STRENGTH
            max: PPM2.MAX_DERP_STRENGTH
        }

        @PONY_DATA["hole_width_#{internal}"] = {
            default: -> 1
            getFunc: "HoleWidth#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or 1, PPM2.MIN_PUPIL_SIZE, PPM2.MAX_PUPIL_SIZE)
            min: PPM2.MIN_PUPIL_SIZE
            max: PPM2.MAX_PUPIL_SIZE
        }

        @PONY_DATA["eye_hole_size#{internal}"] = {
            default: -> .8
            getFunc: "HoleSize#{publicName}"
            fix: (arg = 1) -> math.Clamp(tonumber(arg) or .8, PPM2.MIN_HOLE, PPM2.MAX_HOLE)
            min: PPM2.MIN_HOLE
            max: PPM2.MAX_HOLE
        }

    for i = 1, 6
        @PONY_DATA["mane_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "ManeColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["mane_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "ManeDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["tail_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "TailDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["tail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "TailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["lower_mane_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "LowerManeColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["upper_mane_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "UpperManeColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["lower_mane_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "LowerManeDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

        @PONY_DATA["upper_mane_detail_color_#{i}"] = {
            default: -> Color(255, 255, 255)
            getFunc: "UpperManeDetailColor#{i}"
            fix: (arg = Color(255, 255, 255)) ->
                if type(arg) ~= 'table'
                    return Color(255, 255, 255)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(255, 255, 255)
        }

    for i = 1, PPM2.MAX_BODY_DETAILS
        @PONY_DATA["body_detail_color_#{i}"] = {
            default: -> Color(140, 50, 100)
            getFunc: "BodyDetailColor#{i}"
            fix: (arg = Color(140, 50, 100)) ->
                if type(arg) ~= 'table'
                    return Color(140, 50, 100)
                else
                    {:r, :g, :b, :a} = arg
                    if r and g and b and a
                        return Color(r, g, b, a)
                    else
                        return Color(140, 50, 100)
        }

        @PONY_DATA["body_detail_#{i}"] = {
            default: -> 0
            getFunc: "BodyDetail#{i}"
            enum: [arg for arg in *PPM2.BodyDetailsEnum]
            fix: (arg = 0) -> math.Clamp(tonumber(arg) or 0, PPM2.MIN_DETAIL, PPM2.MAX_DETAIL)
            min: PPM2.MIN_DETAIL
            max: PPM2.MAX_DETAIL
        }

    @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 *data.enum]
        data.enumMapping = {}
        data.enumMappingBackward = {}
        i = -1
        for enumVal in *data.enum
            i += 1
            data.enumMapping[i] = enumVal
            data.enumMappingBackward[enumVal] = i
    for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max} 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
        @["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["Set#{getFunc}"] = (val = defValue, ...) =>
            if type(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, ...)

    Copy: (fileName = @filename) =>
        copyOfData = {}
        for key, val in pairs @dataTable
            switch type(val)
                when 'number'
                    copyOfData[key] = val
                when 'string'
                    copyOfData[key] = val
                when 'table'
                    {:r, :g, :b} = val
                    if r and g and b
                        copyOfData[key] = Color(r, g, b)
        newData = @@(fileName, copyOfData, false)
        return newData
    CreateCustomNetworkObject: (ply = LocalPlayer(), ...) =>
        newData = PPM2.NetworkedPonyData(nil, ply)
        newData\SetEntity(ply)
        @ApplyDataToObject(newData, ...)
        return newData
    CreateNetworkObject: (gointToNetwork = true, ...) =>
        newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
        newData\SetIsGoingToNetwork(gointToNetwork)
        newData\SetEntity(LocalPlayer())
        @ApplyDataToObject(newData, ...)
        return newData
    ApplyDataToObject: (target, ...) =>
        for key, value in pairs @GetAsNetworked()
            target["Set#{key}"](target, value, ...) if target["Get#{key}"](target, value) ~= value
    UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
    CreateController: (...) => @CreateNetworkObject(false, ...)
    CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

    new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
        @SetFilename(filename)
        @valid = @isOpen
        @rawData = data
        @dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
        @saveOnChange = true
        if data
            @SetupData(data, true)
        elseif @exists and readIfExists
            @ReadFromDisk(force, doBackup)

    @ERR_MISSING_PARAMETER = 4
    @ERR_MISSING_CONTENT = 5

    GetSaveOnChange: => @saveOnChange
    SaveOnChange: => @saveOnChange
    SetSaveOnChange: (val = true) => @saveOnChange = val
    SetupData: (data, force = false, doBackup = false) =>
        if doBackup or not force
            makeBackup = false
            for key, value in pairs data
                key = key\lower()
                map = @@PONY_DATA_MAPPING[key]
                if not map
                    return @@ERR_MISSING_PARAMETER if not force
                    makeBackup = true
                    break
                mapData = @@PONY_DATA[map]
                if mapData.enum
                    if type(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or type(value) == 'number' and not mapData.enumMapping[value]
                        return @@ERR_MISSING_CONTENT if not force
                        makeBackup = true
                        break
            if doBackup and makeBackup and @exists
                bkName = "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.txt"
                fRead = file.Read(@fpath, 'DATA')
                file.Write(bkName, fRead)

        for key, value in pairs data
            key = key\lower()
            map = @@PONY_DATA_MAPPING[key]
            continue unless map
            mapData = @@PONY_DATA[map]
            if mapData.enum and type(value) == 'string'
                @dataTable[key] = mapData.fix(mapData.enumMappingBackward[value\upper()])
            else
                @dataTable[key] = mapData.fix(value)
    ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
        if @nwObj
            {:getFunc} = @@PONY_DATA[key]
            @nwObj["Set#{getFunc}"](@nwObj, newVal)
        @Save() if saveNow
    SetFilename: (filename) =>
        @filename = filename
        @filenameFull = "#{filename}.txt"
        @fpath = "#{@@DATA_DIR}#{filename}.txt"
        @fpathBackup = "#{@@DATA_DIR}#{filename}.bak.txt"
        @fpathFull = "data/#{@@DATA_DIR}#{filename}.txt"
        @isOpen = @filename ~= nil
        @exists = file.Exists(@fpath, 'DATA')
        return @exists
    SetNetworkData: (nwObj) => @nwObj = nwObj
    SetPonyData: (nwObj) => @nwObj = nwObj
    SetPonyDataController: (nwObj) => @nwObj = nwObj
    SetPonyController: (nwObj) => @nwObj = nwObj
    SetController: (nwObj) => @nwObj = nwObj
    SetDataController: (nwObj) => @nwObj = nwObj

    GetNetworkData: => @nwObj
    GetPonyData: => @nwObj
    GetPonyDataController: => @nwObj
    GetPonyController: => @nwObj
    GetController: => @nwObj
    GetDataController: => @nwObj

    IsValid: => @valid
    Exists: => @exists
    FileExists: => @exists
    IsExists: => @exists
    GetFileName: => @filename
    GetFilename: => @filename
    GetFileNameFull: => @filenameFull
    GetFilenameFull: => @filenameFull
    GetFilePath: => @fpath
    GetFullFilePath: => @fpathFull
    SerealizeValue: (valID = '') =>
        map = @@PONY_DATA[valID]
        return unless map
        val = @dataTable[valID]
        if map.enum
            return map.enumMapping[val] or map.enumMapping[map.default()]
        elseif map.serealize
            return map.serealize(val)
        else
            return val
    Serealize: (prettyPrint = true) =>
        serTab = {key, @SerealizeValue(key) for key, val in pairs @dataTable}
        util.TableToJSON(serTab, prettyPrint)
    GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

    @READ_SUCCESS = 0
    @ERR_FILE_NOT_EXISTS = 1
    @ERR_FILE_EMPTY = 2
    @ERR_FILE_CORRUPT = 3
    ReadFromDisk: (force = false, doBackup = true) =>
        return @@ERR_FILE_NOT_EXISTS unless @exists
        fRead = file.Read(@fpath, 'DATA')
        return @@ERR_FILE_EMPTY if not fRead or fRead == ''
        readTable = util.JSONToTable(fRead)
        return @@ERR_FILE_CORRUPT unless readTable
        return @SetupData(readTable, force, doBackup) or @@READ_SUCCESS
    SaveAs: (path = @fpath) =>
        fCreate = @Serealize()
        file.Write(path, fCreate)
    Save: (doBackup = true) =>
        if doBackup and @exists
            fRead = file.Read(@fpath, 'DATA')
            file.Write(@fpathBackup, fRead)
        @SaveAs(@fpath)
        @exists = true

do
    PARSE_VECTOR = (str = '1.0 1.0 1.0', X = 1, Y = 1, Z = 1) ->
        return Vector(X, Y, Z) if str == ''
        x, y, z = str\match('([0-9.]+) ([0-9.]+) ([0-9.]+)')
        return Vector(tonumber(x) or X, tonumber(y) or Y, tonumber(z) or Z)

    PARSE_COLOR = (str = '1.0 1.0 1.0', r = 255, g = 255, b = 255) ->
        return Color(r, g, b) if str == ''
        {x, y, z} = PARSE_VECTOR(str, r / 255, g / 255, b / 255)
        return Color(x * 255, y * 255, z * 255)

    IMPORT_TABLE = {
        'gender': {
            name: 'Gender'
            func: (arg = 0) ->
                num = tonumber(arg)
                return num == 0 and 'MALE' or 'FEMALE'
        }

        'coatcolor': {
            name: 'BodyColor'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_bg': {
            name: 'EyeBackground'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_grad': {
            name: 'EyeIrisBottom'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_iris': {
            name: 'EyeIrisTop'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line1': {
            name: 'EyeIrisLine1'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line2': {
            name: 'EyeIrisLine2'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'haircolor1': {
            name: {'ManeColor1', 'TailColor1'}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'haircolor2': {
            name: {'ManeColor2', 'TailColor2'}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyejholerssize': {
            name: 'HoleWidth'
            func: (arg = '1') -> tonumber(arg) or 1
        }

        'eyeirissize': {
            name: 'IrisSize'
            func: (arg = '1') -> tonumber(arg) or 1
        }

        'eyeholesize': {
            name: 'HoleSize'
            func: (arg = '0.8') -> tonumber(arg) or 0.8
        }

        'bodyweight': {
            name: 'Weight'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'mane': {
            name: {'ManeType', 'ManeTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'manel': {
            name: {'ManeTypeLower', 'ManeTypeLowerNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tail': {
            name: {'TailType', 'TailTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tailsize': {
            name: 'TailSize'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'cmark': {
            name: 'CMarkType'
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        'cmark_enabled': {
            name: 'CMark'
            func: (arg = '1') -> arg == '1' or arg == '2'
        }
    }

    for i = 1, 8
        IMPORT_TABLE["bodydetail#{i}"] = {
            name: "BodyDetail#{i}"
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        IMPORT_TABLE["bodydetail#{i}_c"] = {
            name: "BodyDetailColor#{i}"
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    for i = 3, 6
        IMPORT_TABLE["haircolor#{i}"] = {
            name: {"ManeDetailColor#{i - 2}", "TailDetailColor#{i - 2}"}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    PPM2.ReadFromOldData = (filename = '_current') ->
        read = file.Read("ppm/#{filename}.txt", 'DATA')
        return false if read == ''
        split = [str\Trim() for str in *string.Explode('\n', read\Replace('\r', ''))]
        outputData = {}

        for line in *split
            varID = line\match('([a-zA-Z0-9_]+)')
            continue if not varID or varID == ''
            continue if not IMPORT_TABLE[varID]
            dt = IMPORT_TABLE[varID]
            value = line\sub(#varID + 2)
            if type(dt.name) ~= 'table'
                outputData[dt.name] = dt.func(value)
            else
                get = dt.func(value)
                outputData[name] = get for name in *dt.name

        data = PonyDataInstance("#{filename}_imported", nil, false)
        for key, value in pairs outputData
            data["Set#{key}"](data, value, false)
        return data, outputData

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


show variants 2x2

show variants 2x2

model = require('model')
quizs = require('quiz.all')

bgcolor = (task, i) ->
  form = task\quiz().state == model.ACTIVE
  bc = 'white'
  if not form and task\ans_i(i) == 1
    bc = 'green'
  if task\ans_i(i) == task.selected
    bc = 'yellow'
  return bc

class Helpers
  ans_form: (task, i) =>
    url = @url_for("answer", {id: task.id})
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input type: "hidden", name: "task", value: task.id
      input type: "hidden", name: "ans", value: i
      input type: "submit", value: @_('Select')

  cancel_form: =>
    element 'hr'
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for("cancel", {id: @quiz.id})
        form method: "POST", action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: "submit", value: @_('Cancel quiz'),
            style: 'background-color: pink'}
      element 'td', ->
        font color: 'red', @_("Dangerous zone")

  help_for: (task) =>
    quiz_name = task\quiz().name
    func = quizs[quiz_name][task.name]
    r, a1, a2, a3, a4, help = func(@)
    return help

  task_table: (task) =>
    form = task\quiz().state == model.ACTIVE
    element "table", border: 1, ->
      element "tr", ->
        element "td", colspan: 2, style: "padding:10px", ->
          if task\quiz().state == model.FINISHED
            b task.name
          p @help_for(task)
          pre task.text
      for i = 0, 1
        element "tr", ->
          for j = 0, 1
            k = i * 2 + j + 1
            element "td", {width: 100, style: "padding:10px",
                bgcolor: bgcolor(task, k)}, ->
              pre task\ans(k)
              if form
                @ans_form task, k

  tasks_number_switcher: (quiz) =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for('all-tasks', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[All tasks at one page]]}
      element 'td', ->
        url = @url_for('one-task', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[One task at one page]]}

  new_test_button: (name) =>
    url = @url_for("new-quiz")
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input {type: "hidden", name: "name", value: name}
      input type: "submit", value: name

  finish_quiz_button: =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for("finish", {id: @quiz.id})
        form method: "POST", action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input type: "submit", value: @_('Finish this quiz')
      element 'td', ->
        font color: 'red', @_([[Quiz is not finished untill
          you press the button]])

  detailed_results: (q, quizs) =>
    h1 @quiz_name
    print_header = ->
      element 'tr', ->
        element 'td' -- id
        element 'td', -> text @_("user")
        element 'td', -> text @_("right answers")
        element 'td', -> text @_("tasks")
        for task_name, func in pairs(q) do
          element 'td', ->
            div class: 'vertical-text', ->
              div class: 'vertical-text__inner', task_name
    element 'table', border: 1, ->
      print_header!
      name2score = {}
      for quiz in *quizs
        element 'tr', ->
          element 'td', ->
            a href: @url_for("prep-quiz", {id: quiz.id}), ->
              text '#'
          element 'td', ->
            url = @url_for("prep-quizs-of", {user: quiz.user})
            a href: url, ->
              text quiz.user
          element 'td', bgcolor: quiz\color!, ->
            text quiz.right_answers
          element 'td', ->
            text quiz.tasks
          tasks = quiz\all_tasks!
          name2task = {}
          for task in *tasks
            name2task[task.name] = task
          for task_name, func in pairs(q) do
            task = name2task[task_name]
            color = 'white'
            score = ''
            if task
              color = task\color!
              score = task\score!
              if task.selected == 1
                if not name2score[task_name]
                  name2score[task_name] = 0
                name2score[task_name] = name2score[task_name] + 1
            element 'td', bgcolor: color, ->
              text score
      element 'tr', ->
        element 'td' -- id
        element 'td' --, -> text @_("user")
        element 'td' --, -> text @_("right answers")
        element 'td' --, -> text @_("tasks")
        for task_name, func in pairs(q) do
          element 'td', ->
            if name2score[task_name]
              text name2score[task_name]
            else
              text 0
      print_header!

model = require('model')
quizs = require('quiz.all')

bgcolor = (task, i) ->
  form = task\quiz().state == model.ACTIVE
  bc = 'white'
  if not form and task\ans_i(i) == 1
    bc = 'green'
  if task\ans_i(i) == task.selected
    bc = 'yellow'
  return bc

class Helpers
  ans_form: (task, i) =>
    url = @url_for("answer", {id: task.id})
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input type: "hidden", name: "task", value: task.id
      input type: "hidden", name: "ans", value: i
      input type: "submit", value: @_('Select')

  cancel_form: =>
    element 'hr'
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for("cancel", {id: @quiz.id})
        form method: "POST", action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: "submit", value: @_('Cancel quiz'),
            style: 'background-color: pink'}
      element 'td', ->
        font color: 'red', @_("Dangerous zone")

  help_for: (task) =>
    quiz_name = task\quiz().name
    func = quizs[quiz_name][task.name]
    r, a1, a2, a3, a4, help = func(@)
    return help

  task_table: (task) =>
    form = task\quiz().state == model.ACTIVE
    element "table", border: 1, ->
      element "tr", ->
        element "td", colspan: 4, style: "padding:10px", ->
          if task\quiz().state == model.FINISHED
            b task.name
          p @help_for(task)
          pre task.text
      element "tr", ->
        for i = 1, 4
          element "td", {width: 100, style: "padding:10px",
              bgcolor: bgcolor(task, i)}, ->
            pre task\ans(i)
            if form
              @ans_form task, i

  tasks_number_switcher: (quiz) =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for('all-tasks', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[All tasks at one page]]}
      element 'td', ->
        url = @url_for('one-task', {id: quiz.id})
        form method: 'POST', action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input {type: 'submit',
            value: @_[[One task at one page]]}

  new_test_button: (name) =>
    url = @url_for("new-quiz")
    form method: "POST", action: url, ->
      input {type: "hidden", name: "csrf_token",
          value: @new_csrf}
      input {type: "hidden", name: "name", value: name}
      input type: "submit", value: name

  finish_quiz_button: =>
    element 'table', -> element 'tr', ->
      element 'td', ->
        url = @url_for("finish", {id: @quiz.id})
        form method: "POST", action: url, ->
          input {type: "hidden", name: "csrf_token",
              value: @new_csrf}
          input type: "submit", value: @_('Finish this quiz')
      element 'td', ->
        font color: 'red', @_([[Quiz is not finished untill
          you press the button]])

  detailed_results: (q, quizs) =>
    h1 @quiz_name
    print_header = ->
      element 'tr', ->
        element 'td' -- id
        element 'td', -> text @_("user")
        element 'td', -> text @_("right answers")
        element 'td', -> text @_("tasks")
        for task_name, func in pairs(q) do
          element 'td', ->
            div class: 'vertical-text', ->
              div class: 'vertical-text__inner', task_name
    element 'table', border: 1, ->
      print_header!
      name2score = {}
      for quiz in *quizs
        element 'tr', ->
          element 'td', ->
            a href: @url_for("prep-quiz", {id: quiz.id}), ->
              text '#'
          element 'td', ->
            url = @url_for("prep-quizs-of", {user: quiz.user})
            a href: url, ->
              text quiz.user
          element 'td', bgcolor: quiz\color!, ->
            text quiz.right_answers
          element 'td', ->
            text quiz.tasks
          tasks = quiz\all_tasks!
          name2task = {}
          for task in *tasks
            name2task[task.name] = task
          for task_name, func in pairs(q) do
            task = name2task[task_name]
            color = 'white'
            score = ''
            if task
              color = task\color!
              score = task\score!
              if task.selected == 1
                if not name2score[task_name]
                  name2score[task_name] = 0
                name2score[task_name] = name2score[task_name] + 1
            element 'td', bgcolor: color, ->
              text score
      element 'tr', ->
        element 'td' -- id
        element 'td' --, -> text @_("user")
        element 'td' --, -> text @_("right answers")
        element 'td' --, -> text @_("tasks")
        for task_name, func in pairs(q) do
          element 'td', ->
            if name2score[task_name]
              text name2score[task_name]
            else
              text 0
      print_header!


main.moon: add basic documentation and hashbang

#!/usr/bin/env moon
--- Create and manage IRC bots.
-- @script main.moon
-- @author Ryan "ChickenNuggers" <ryan@hashbang.sh>

Logger        = require 'logger' -- vim:set noet sts=0 sw=3 ts=3:
cqueues       = require 'cqueues'
lfs           = require 'lfs'

import IRCClient from require 'irc'

wd = lfs.currentdir()
Logger.set_debug true if os.getenv 'DEBUG'

load_modules = (folder)->
	for file in lfs.dir folder
		if file\match "%.lua$"
			dofile folder .. '/' .. file

load_modules_in_plugin_folders = ->
	for module_folder in *{'plugins', 'modules'}
		full_path = wd .. '/' .. module_folder
		load_modules full_path if lfs.attributes(full_path, 'mode') == 'directory'

load_modules_in_plugin_folders!

bots = {}
for file in lfs.dir 'configs'
	if file\match "%.ini$"
		data = {
			dir: wd
		}
		for line in io.lines('configs/' .. file)
			key, value = assert line\match "^(.-)=(.+)$"
			data[key] = value
		bot  = IRCClient data.host, data.port, data

		bot.user_data   = data
		bot.config_file = file\match("(.+).ini$")

		table.insert(bots, bot)

queue = cqueues.new!

for bot in *bots
	queue\wrap ->
		while true
			local success
			for i=1, 3 do -- three tries
				ok, err = pcall bot.connect, bot
				success = ok
				if not ok
					Logger.print Logger.level.error .. '*** Unable to connect: ' .. bot.user_data.host
					Logger.debug Logger.level.error .. '*** ' .. err
				else
					break

			if not success
				Logger.print Logger.level.fatal .. '*** Not connecting anymore for: ' .. bot.config_file
				return
			pcall -> bot\loop!

assert queue\loop!