remove old bgfxgen

remove old bgfxgen

-- dev/bgfxgen.moon
--
-- generate bgfx auxilliary files
-- assumes truss has been built into "../build/"

sutil = require "util/string.t"
argparse = require "util/argparse.t"
genconstants = require "dev/genconstants.t"

BGFX_PATH = "/bgfx_EXTERNAL-prefix/src/bgfx_EXTERNAL"

os_exec = (cmd) ->
  f = io.popen cmd, "r"
  ret = f\read "*all"
  f\close!
  ret

os_copy = (srcfn, destfn) ->
  cmd = if truss.os == "Windows"
    "copy \"#{srcfn\gsub("/", "\\")}\" \"#{destfn\gsub("/", "\\")}\" /y"
  else
    "cp \"#{srcfn}\" \"#{destfn}\""
  print cmd
  print os_exec cmd

copy_files = (buildpath) ->
  srcpath = "#{buildpath}#{BGFX_PATH}"
  os_copy "#{srcpath}/include/bgfx/defines.h", "include/bgfxdefines.h"
  os_copy "#{srcpath}/examples/common/shaderlib.sh", "shaders/raw/common/shaderlib.sh"
  os_copy "#{srcpath}/src/bgfx_shader.sh", "shaders/raw/common/bgfx_shader.sh"
  os_copy "#{srcpath}/src/bgfx_compute.sh", "shaders/raw/common/bgfx_compute.sh"

rawload = (fn) -> (io.open fn, "rt")\read "*a"

DEFINE_PATT = "^#define%s*([%w_]*)[^%(%w_]"

get_define_names = () ->
  -- assumes `defines.h` has been copied to `include/bgfxdefines.h`
  defnames = {}
  for line in *(sutil.split_lines rawload "include/bgfxdefines.h")
    name = line\match DEFINE_PATT
    if name and name != ""
      table.insert defnames, name
  defnames

get_constants = () -> genconstants.gen_constants_file get_define_names!

to_snake_case = (s) ->
  s_first = s\sub(1,1)\upper!
  s_rem = s\sub 2, -1
  s = s_first .. s_rem
  table.concat [v\lower! for v in s\gmatch "[%u%d]+%l*"], "_"

lower_snake = (s) -> (to_snake_case s)\lower!

upper_snake = (s) -> (to_snake_case s)\upper!

remove_comment = (s) ->
  fpos = s\find "%-%-"
  if fpos
    s = s\sub 1, fpos-1
  sutil.strip s

fix5p1 = (data) ->
  lines = sutil.split_lines data
  outlines = {}
  cur_enum = nil
  for linepos = 1, #lines
    curline = remove_comment sutil.strip lines[linepos]
    if (curline\sub 1,4) == "enum"
      cur_enum = curline\sub 5,-1
    elseif cur_enum and curline == ""
      if (outlines[#outlines]\sub -2,-1) != "()"
        print("Enum missing function call: #{cur_enum}")
        outlines[#outlines] = outlines[#outlines] .. "()"
      cur_enum = nil

    if (curline\sub 1,2) == "()"
      outlines[#outlines] = outlines[#outlines] .. curline
    else
      outlines[#outlines+1] = curline
  temp = io.open "bleh.lua", "wt"
  temp\write table.concat outlines, "\n"
  temp\close!
  table.concat outlines, "\n"

_flatten = (dest, list) ->
  for v in *list
    if type(v) == 'table' then
      _flatten dest, v
    else
      dest[#dest+1] = v
  dest

flatten = (lists) -> _flatten {}, lists

conflatten = (lists) -> table.concat (flatten lists), "\n"

ordered_concat = (t, keyorder, sep) ->
  table.concat [t[k] for k in *keyorder], (sep or "\n\n")

key_sorted_concat = (t, sep) ->
  sorted_keys = [k for k, v in pairs t]
  table.sort sorted_keys
  ordered_concat t, sorted_keys, sep

exec_in = (env, fn) ->
  src = fix5p1 rawload fn
  chunk, err = loadstring src
  if not chunk
    truss.error "Error parsing #{fn}: #{err}"
  setfenv chunk, env
  chunk! or env

load_idl = (buildpath) ->
  env = truss.extend_table {}, truss.clean_subenv
  path = buildpath .. BGFX_PATH
  print "Loading IDL from [#{path}]"
  idl = exec_in env, "#{path}/scripts/idl.lua"
  exec_in idl, "#{path}/scripts/bgfx.idl"

BANNED_TYPES = {"va_list"}

is_api_func = (line) ->
  parts = sutil.split " ", line
  if parts[1] != "BGFX_C_API" then return nil
  api_key = (parts[2] != "const" and parts[3]) or parts[4] or line
  api_key = (sutil.split "%(", api_key)[1]
  signature = table.concat [p for p in *parts[2,]], " "
  for bad_type in *BANNED_TYPES
    if line\find bad_type
      signature = "//" .. signature
  signature, api_key

get_functions = (buildpath) ->
  -- generating function signatures from the IDL is too much of a pain
  -- instead just read them from the C-api header
  path = "#{buildpath}#{BGFX_PATH}/include/bgfx/c99/bgfx.h"
  api_funcs = {}
  for line in *(sutil.split_lines rawload path)
    api_line, api_order_key = is_api_func line
    if api_line then api_funcs[api_order_key] = api_line
  key_sorted_concat api_funcs, "\n"

gen_enum = (e) ->
  format_val = if e.underscore
    (v) -> upper_snake v
  else
    (v) -> v\upper!
  name = "bgfx_#{lower_snake e.name}"
  conflatten {
    "typedef enum #{name} {",
    (table.concat ["    #{name\upper!}_#{format_val v.name}" for v in *e.enum], ",\n") .. ",",
    "    #{name\upper!}_COUNT",
    "} #{name}_t;"
  }

gen_enums = (idl) ->
  enums = {t.name, gen_enum t for t in *idl.types when t.enum}
  key_sorted_concat enums, "\n\n"

gen_handle = (handle) ->
  name =  "bgfx_#{lower_snake handle.name}"
  "typedef struct #{name} { uint16_t idx; } #{name}_t;"

gen_handles = (idl) ->
  handles = {t.name, gen_handle t for t in *idl.types when t.handle}
  key_sorted_concat handles, "\n"

is_pointer = (s) -> ((s\sub -1) == "*") and (s\sub 1, -2)

is_const = (s) -> ((s\sub 1, 5) == "const") and (s\sub 7)

is_enum = (s) ->
  parts = sutil.split "::", s
  if #parts == 2 and (parts[2]\sub 1, 4) == "Enum"
    parts[1]
  else
    false

is_array = (s) ->
  array_start = s\find "%["
  if not array_start then return nil, nil
  return (s\sub 1, array_start-1), (s\sub array_start+1, -2)

FIXED_TYPES = {
  "float": "float"
  "double": "double"
  "char": "char"
  "bool": "bool"
  "CallbackI": "bgfx_callback_interface_t"
  "bx::AllocatorI": "bgfx_allocator_interface_t"
  "void": "void"
}
for itype in *{8, 16, 32, 64}
  for qualifier in *{"uint", "int"}
    tname = "#{qualifier}#{itype}_t"
    FIXED_TYPES[tname] = tname

format_count = (count) ->
  parts = sutil.split "::", count
  if #parts == 2 -- assume ::Count
    "BGFX_#{upper_snake parts[1]}_COUNT"
  else
    count

namespaced_structs = {}

format_type = (t, parent) ->
  subtype, count = is_array t
  if subtype
    (format_type subtype, parent), (format_count count)
  else
    pointer_type = is_pointer t
    if pointer_type
      t = pointer_type
    const_type = is_const t
    if const_type
      t = const_type
    res = ""
    if FIXED_TYPES[t]
      res = FIXED_TYPES[t]
    else
      res = lower_snake ((is_enum t) or t)
      if parent and namespaced_structs["#{parent}_#{res}"]
        res = "#{parent}_#{res}"
      res = "bgfx_" .. res .. "_t"
    if pointer_type
      res = res .. "*"
    if const_type
      res = "const " .. res
    res

format_field = (f, parent) ->
  argtype, argcount = format_type f.fulltype, parent
  name = f.name
  if argcount
    name = "#{name}[#{argcount}]"
  "    #{argtype} #{name};"

gen_struct = (struct) ->
  name = lower_snake struct.name
  if struct.namespace
    name = "#{lower_snake struct.namespace}_#{name}"
    namespaced_structs[name] = true
  if #struct.struct == 0
    return name, "typedef struct bgfx_#{name}_s bgfx_#{name}_t;"
  name, conflatten {
    "typedef struct bgfx_#{name}_s {",
    [format_field f, name for f in *struct.struct]
    "} bgfx_#{name}_t;"
  }

gen_structs = (idl) ->
  structs = {}
  for t in *idl.types
    if not t.struct then continue
    name, struct = gen_struct t
    table.insert structs, struct
  table.concat structs, "\n\n"

PREAMBLE = [[
/*
 * BGFX Copyright 2011-2021 Branimir Karadzic. All rights reserved.
 * License: https://github.com/bkaradzic/bgfx/blob/master/LICENSE
 *
 * This header is slightly modified to make it easier for Terra
 * to digest; it is automatically generated from the 'real' BGFX
 * headers and IDL by `dev/bgfxgen.moon`.
 */

#ifndef BGFX_C99_H_HEADER_GUARD
#define BGFX_C99_H_HEADER_GUARD

//#include <stdarg.h>  // va_list
#include <stdbool.h> // bool
#include <stdint.h>  // uint32_t
#include <stdlib.h>  // size_t

#undef UINT32_MAX
#define UINT32_MAX 4294967295
#define BGFX_UINT32_MAX 4294967295

#define BGFX_INVALID_HANDLE 0xffff
typedef uint16_t bgfx_view_id_t;

typedef struct bgfx_interface_vtbl bgfx_interface_vtbl_t;
typedef struct bgfx_callback_interface bgfx_callback_interface_t;
typedef struct bgfx_callback_vtbl bgfx_callback_vtbl_t;
typedef struct bgfx_allocator_interface bgfx_allocator_interface_t;
typedef struct bgfx_allocator_vtbl bgfx_allocator_vtbl_t;

typedef void (*bgfx_release_fn_t)(void* _ptr, void* _userData);
]]

gen_header = (buildpath) ->
  idl = load_idl buildpath
  conflatten {
    PREAMBLE,
    "\n\n/* Enums: */\n",
    gen_enums idl,
    "\n\n/* Handle types: */\n",
    gen_handles idl,
    "\n\n/* Structs: */\n",
    gen_structs idl,
    "\n\n/* Functions: */\n",
    get_functions buildpath,
    "\n",
    "#endif // BGFX_C99_H_HEADER_GUARD"
  }

export init = ->
  bpath = truss.args[3] or "../build"
  print "Copying files from #{bpath}"
  copy_files bpath
  print "Generating include/bgfx_truss.c99.h"
  truss.save_string "include/bgfx_truss.c99.h", (gen_header bpath)
  print "Generating scripts/gfx/bgfx_constants.t"
  truss.save_string "scripts/gfx/bgfx_constants.t", get_constants!
  print "Done."
  truss.quit!

export update = ->
  truss.quit!


superroute

superroute

lapis = require "lapis"

import after_dispatch from require "lapis.nginx.context"
import to_json from require "lapis.util"
migrations=require "lapis.db.migrations"
config = (require "lapis.config").get!

class extends lapis.Application
  @before_filter =>
    if config.measure_performance
      after_dispatch ->
        print to_json ngx.ctx.performance
    @modules or={}
    @session.modules or={}
  "/_lazuli/console": if config.enable_console
    (require "lapis.console").make!
  else
    => "console disabled in config"
  "/_lazuli/migrate": if config.enable_web_migration
    =>
      migrations.create_migrations_table!
      migrations.run_migrations require "migrations"
  else
    => "web migration disabled in config"
  new: (...)=>
    @modules or={}
    super ...
  enable: (feature,forcelapis=false)=>
    if not forcelapis
      have,fn=pcall require, "lazuli.modules."..feature..".enabler"
      if have and type(fn)=="function"
        return fn @
    super feature
  superroute: (r)=>
    App=@__parent
    cache=nil
    name=next r
    cont=r[1]
    r[1]=nil
    @__base[r]= =>
      if not cache
        for app_route in pairs App.__base
          if type(app_route) == "table"
            app_route_name = next app_route
            if app_route_name == name
              cache=App.__base[app_route]
              break
      if cont
        cont @, cache(@)
      else
        cache(@)

lapis = require "lapis"

import after_dispatch from require "lapis.nginx.context"
import to_json from require "lapis.util"
migrations=require "lapis.db.migrations"
config = (require "lapis.config").get!

class extends lapis.Application
  @before_filter =>
    if config.measure_performance
      after_dispatch ->
        print to_json ngx.ctx.performance
    @modules or={}
    @session.modules or={}
  "/_lazuli/console": if config.enable_console
    (require "lapis.console").make!
  else
    => "console disabled in config"
  "/_lazuli/migrate": if config.enable_web_migration
    =>
      migrations.create_migrations_table!
      migrations.run_migrations require "migrations"
  else
    => "web migration disabled in config"
  new: (...)=>
    @modules or={}
    super ...
  enable: (feature,forcelapis=false)=>
    if not forcelapis
      have,fn=pcall require, "lazuli.modules."..feature..".enabler"
      if have and type(fn)=="function"
        return fn @
    super feature
  superroute: (r)=>
    App=@__parent
    cache=nil
    name=next r
    @__base[r]= =>
      if not cache
        for app_route in pairs App.__base
          if type(app_route) == "table"
            app_route_name = next app_route
            if app_route_name == name
              cache=App.__base[app_route]
              break
      if r[1]
        r[1] @, cache(@)
      else
        cache(@)


remove automatic sendname

remove automatic sendname

socket = require 'cqueues.socket'
Logger = require 'logger'

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

class IRCConnection
	new: (server, port=6697, config={})=>
		assert(server)
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs(config)
			@config[k] = v

		@handlers = {}
		@senders  = {}
		@server   = {}
		@hooks    = {}

	add_hook: (id, hook)=>
		if not @hooks[id]
			@hooks[id] = {hook}
		else
			table.insert @hooks[id], hook

	add_handler: (id, handler)=>
		if not @handlers[id]
			@handlers[id] = {handler}
		else
			table.insert @handlers[id], handler

	add_sender: (id, sender)=>
		assert not @senders[id], "Sender already exists: " .. id
		@senders[id] = sender

	load_modules: (modules)=>
		if modules.senders
			for id, sender in pairs modules.senders
				@add_sender id, sender
		if modules.handlers
			for id, handler in pairs modules.handlers
				@add_handler id, handler
		if modules.hooks
			for id, hook in pairs modules.hooks
				@add_hook id, hook

	connect: ()=>
		if @socket
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port
		---
		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname
		---
		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		@send_raw ('NICK %s')\format nick
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw ('PASS :%s')\format pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw ('USER %s * * :%s')\format user, real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	send_raw: (...)=>
		@socket\write table.concat({...}, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat {...}, ' '

	send: (name, pattern, ...)=>
		@senders[name] pattern\format ...

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	parse_time: (datestring)=>
		year, month, day, hour, min, sec, mil = datestring\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	parse_tags: (tag_message)=>
		local cur_name
		tags = {}
		charbuf = {}
		pos = 1
		while pos < #tag_message do
			if tag_message\match '^\\', pos
				lookahead = tag_message\sub pos+1, pos+1
				charbuf[#charbuf + 1] = escapers[lookahead] or lookahead
				pos += 2
			elseif cur_name
				if tag_message\match "^;", pos
					tags[cur_name] = table.concat charbuf
					cur_name = nil
					charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\;]+)()", pos
			else
				if tag_message\match "^=", pos
					if #charbuf > 0
						cur_name = table.concat charbuf
						charbuf = {}
					pos += 1
				elseif tag_message\match "^;", pos
					if #charbuf > 0
						tags[table.concat charbuf] = true
						charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\=;]+)()", pos
		return tags

	parse: (message_with_tags)=>
		local message, tags
		if message_with_tags\sub(1, 1) == '@'
			tags = @parse_tags message_with_tags\sub 2, message_with_tags\find(' ') - 1
			message = message_with_tags\sub((message_with_tags\find(' ') + 1))
		else
			message = message_with_tags
		prefix_end = 0
		prefix = nil
		if message\sub(1, 1) == ':'
			prefix_end = message\find ' '
			prefix = message\sub 2, message\find(' ') - 1

		trailing = nil
		tstart = message\find ' :'
		if tstart
			trailing = message\sub tstart + 2
		else
			tstart = #message

		rest = ((segment)->
			t = {}
			for word in segment\gmatch '%S+'
				table.insert t, word

			return t
		)(message\sub prefix_end + 1, tstart)

		command = rest[1]
		table.remove(rest, 1)
		return prefix, command, rest, trailing, tags

	fire_hook: (hook_name)=>
		if not @hooks[hook_name]
			return false
		for _, hook in pairs @hooks[hook_name]
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = pcall hook, @
			if not ok
				Logger.print Logger.level.error .. '*** ' .. err
		return true -- because it did fire off hooks

	process: (line)=>
		prefix, command, args, rest, tags = @parse line
		Logger.debug Logger.level.warn .. '--- | Line: ' .. line
		if not @handlers[command]
			return
		Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
		if prefix
			Logger.debug Logger.level.okay .. '--- |\\ Prefix: ' .. prefix
		if #args > 0
			Logger.debug Logger.level.okay .. '--- |\\ Arguments: ' .. table.concat(args, ', ')
		if rest
			Logger.debug Logger.level.okay .. '--- |\\ Trailing: ' .. rest
		for _, handler in pairs @handlers[command]
			ok, err = pcall handler, @, prefix, args, rest, tags
			if not ok
				Logger.print Logger.level.error .. '*** ' .. err

	loop: ()=>
		local line
		print_error =(err)->
			Logger.debug "Error: " .. err .. " (" .. line .. ")"

		for received_line in @socket\lines! do
			line = received_line
			xpcall @process, print_error, @, received_line

return IRCConnection

socket = require 'cqueues.socket'
Logger = require 'logger'

escapers =  {['s']: ' ', ['r']: '\r', ['n']: '\n', [';']: ';'}

class IRCConnection
	new: (server, port=6697, config={})=>
		assert(server)
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs(config)
			@config[k] = v

		@handlers = {}
		@senders  = {}
		@server   = {}
		@hooks    = {}

	add_hook: (id, hook)=>
		if not @hooks[id]
			@hooks[id] = {hook}
		else
			table.insert @hooks[id], hook

	add_handler: (id, handler)=>
		if not @handlers[id]
			@handlers[id] = {handler}
		else
			table.insert @handlers[id], handler

	add_sender: (id, sender)=>
		assert not @senders[id], "Sender already exists: " .. id
		@senders[id] = sender

	load_modules: (modules)=>
		if modules.senders
			for id, sender in pairs modules.senders
				@add_sender id, sender
		if modules.handlers
			for id, handler in pairs modules.handlers
				@add_handler id, handler
		if modules.hooks
			for id, hook in pairs modules.hooks
				@add_hook id, hook

	connect: ()=>
		if @socket
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port
		---
		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname
		---
		Logger.debug debug_msg, Logger.level.warn .. '--- Connecting...'
		@socket = assert socket.connect{:host, :port, sendname: true}
		if ssl
			Logger.debug 'Starting TLS exchange...'
			@socket\starttls!
			Logger.debug 'Started TLS exchange'
		Logger.print Logger.level.okay .. '--- Connected'
		@fire_hook 'CONNECT'
		nick = @config.nick
		user = @config.username
		real = @config.realname
		pass = @config.password
		Logger.print Logger.level.warn .. '--- Sending authentication data'
		@send_raw ('NICK %s')\format nick
		if pass and ssl
			Logger.debug '*** Sending password'
			@send_raw ('PASS :%s')\format pass
		elseif pass
			Logger.print Logger.level.error .. '*** Not sending password: TLS not enabled ***'
		@send_raw ('USER %s * * :%s')\format user, real
		debug_msg = ('Sent authentication data: {nickname: %s, username: %s, realname: %s}')\format nick, user, real
		Logger.debug debug_msg, Logger.level.okay .. '--- Sent authentication data'

	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	send_raw: (...)=>
		@socket\write table.concat({...}, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat {...}, ' '

	send: (name, pattern, ...)=>
		@senders[name] pattern\format ...

	date_pattern: "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z"

	parse_time: (datestring)=>
		year, month, day, hour, min, sec, mil = datestring\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	parse_tags: (tag_message)=>
		local cur_name
		tags = {}
		charbuf = {}
		pos = 1
		while pos < #tag_message do
			if tag_message\match '^\\', pos
				lookahead = tag_message\sub pos+1, pos+1
				charbuf[#charbuf + 1] = escapers[lookahead] or lookahead
				pos += 2
			elseif cur_name
				if tag_message\match "^;", pos
					tags[cur_name] = table.concat charbuf
					cur_name = nil
					charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\;]+)()", pos
			else
				if tag_message\match "^=", pos
					if #charbuf > 0
						cur_name = table.concat charbuf
						charbuf = {}
					pos += 1
				elseif tag_message\match "^;", pos
					if #charbuf > 0
						tags[table.concat charbuf] = true
						charbuf = {}
					pos += 1
				else
					charbuf[#charbuf + 1], pos = tag_message\match "([^\\=;]+)()", pos
		return tags

	parse: (message_with_tags)=>
		local message, tags
		if message_with_tags\sub(1, 1) == '@'
			tags = @parse_tags message_with_tags\sub 2, message_with_tags\find(' ') - 1
			message = message_with_tags\sub((message_with_tags\find(' ') + 1))
		else
			message = message_with_tags
		prefix_end = 0
		prefix = nil
		if message\sub(1, 1) == ':'
			prefix_end = message\find ' '
			prefix = message\sub 2, message\find(' ') - 1

		trailing = nil
		tstart = message\find ' :'
		if tstart
			trailing = message\sub tstart + 2
		else
			tstart = #message

		rest = ((segment)->
			t = {}
			for word in segment\gmatch '%S+'
				table.insert t, word

			return t
		)(message\sub prefix_end + 1, tstart)

		command = rest[1]
		table.remove(rest, 1)
		return prefix, command, rest, trailing, tags

	fire_hook: (hook_name)=>
		if not @hooks[hook_name]
			return false
		for _, hook in pairs @hooks[hook_name]
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = pcall hook, @
			if not ok
				Logger.print Logger.level.error .. '*** ' .. err
		return true -- because it did fire off hooks

	process: (line)=>
		prefix, command, args, rest, tags = @parse line
		Logger.debug Logger.level.warn .. '--- | Line: ' .. line
		if not @handlers[command]
			return
		Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
		if prefix
			Logger.debug Logger.level.okay .. '--- |\\ Prefix: ' .. prefix
		if #args > 0
			Logger.debug Logger.level.okay .. '--- |\\ Arguments: ' .. table.concat(args, ', ')
		if rest
			Logger.debug Logger.level.okay .. '--- |\\ Trailing: ' .. rest
		for _, handler in pairs @handlers[command]
			ok, err = pcall handler, @, prefix, args, rest, tags
			if not ok
				Logger.print Logger.level.error .. '*** ' .. err

	loop: ()=>
		local line
		print_error =(err)->
			Logger.debug "Error: " .. err .. " (" .. line .. ")"

		for received_line in @socket\lines! do
			line = received_line
			xpcall @process, print_error, @, received_line

return IRCConnection


Fixed networking

Fixed networking

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

PPM2.MIN_WEIGHT = 0.7
PPM2.MAX_WEIGHT = 1.3

PPM2.MIN_TAIL_SIZE = 0.6
PPM2.MAX_TAIL_SIZE = 1.7 -- i luv big tails

PPM2.MIN_IRIS = 0.7
PPM2.MAX_IRIS = 1.3

PPM2.MIN_PUPIL_SIZE = 0.2
PPM2.MAX_PUPIL_SIZE = 1

PPM2.AvaliableTails = {
    'MAILCALL'
    'FLOOFEH'
    'ADVENTUROUS'
    'SHOWBOAT'
    'ASSERTIVE'
    'BOLD'
    'STUMPY'
    'SPEEDSTER'
    'EDGY'
    'RADICAL'
    'BOOKWORM'
    'BUMPKIN'
    'POOFEH'
    'CURLY'
    'NONE'
}

PPM2.AvaliableUpperManes = {
    'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT', 'ASSERTIVE'
    'BOLD', 'STUMPY', 'SPEEDSTER', 'RADICAL', 'SPIKED'
    'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY', 'INSTRUCTOR', 'NONE'
}

PPM2.AvaliableLowerManes = {
    'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
    'ASSERTIVE', 'BOLD', 'STUMPY', 'HIPPIE', 'SPEEDSTER'
    'BOOKWORM', 'BUMPKIN', 'CURLY', 'NONE'
}

PPM2.EyelashTypes = {
    'Default', 'Double', 'Coy', 'Full', 'Mess', 'None'
}

PPM2.BodyDetails = {
    'None', 'Leg gradient', 'Lines', 'Stripes', 'Head stripes'
    'Freckles', 'Hooves big', 'Hooves small', 'Head layer'
    'Hooves big rnd', 'Hooves small rnd', 'Spots 1'
}

PPM2.BodyDetailsEnum = {
    'NONE', 'GRADIENT', 'LINES', 'STRIPES', 'HSTRIPES'
    'FRECKLES', 'HOOF_BIG', 'HOOF_SMALL', 'LAYER'
    'HOOF_BIG_ROUND', 'HOOF_SMALL_ROUND', 'SPOTS'
}

PPM2.MIN_EYELASHES = 0
PPM2.MAX_EYELASHES = #PPM2.EyelashTypes - 1

PPM2.MIN_TAILS = 0
PPM2.MAX_TAILS = #PPM2.AvaliableTails - 1

PPM2.MIN_UPPER_MANES = 0
PPM2.MAX_UPPER_MANES = #PPM2.AvaliableUpperManes - 1

PPM2.MIN_LOWER_MANES = 0
PPM2.MAX_LOWER_MANES = #PPM2.AvaliableLowerManes - 1

PPM2.MIN_DETAIL = 0
PPM2.MAX_DETAIL = #PPM2.BodyDetails - 1

PPM2.GENDER_FEMALE = 0
PPM2.GENDER_MALE = 1

PPM2.MAX_BODY_DETAILS = 8

PPM2.RACE_EARTH = 0
PPM2.RACE_PEGASUS = 1
PPM2.RACE_UNICORN = 2
PPM2.RACE_ALICORN = 3

PPM2.AGE_FILLY = 0
PPM2.AGE_ADULT = 1
PPM2.AGE_MATURE = 2

class NetworkedPonyData extends PPM2.NetworkedObject
    @NW_ClientsideCreation = true
    @Setup()
    @NetworkVar('Entity',           net.ReadEntity, net.WriteEntity, NULL)
    @NetworkVar('Race',             (-> math.Clamp(net.ReadUInt(4), 0, 3)), ((arg = PPM2.RACE_EARTH) -> net.WriteUInt(arg, 4)), PPM2.RACE_EARTH)
    @NetworkVar('Gender',           (-> math.Clamp(net.ReadUInt(4), 0, 1)), ((arg = PPM2.GENDER_FEMALE) -> net.WriteUInt(arg, 4)), PPM2.GENDER_FEMALE)
    @NetworkVar('Weight',           (-> math.Clamp(net.ReadFloat(), PPM2.MIN_WEIGHT, PPM2.MAX_WEIGHT)), net.WriteFloat, 1)

    -- Reserved - they can be accessed/used/changed, but they do not do anything
    @NetworkVar('Age',              (-> math.Clamp(net.ReadUInt(4), 0, 2)), ((arg = PPM2.AGE_ADULT) -> net.WriteUInt(arg, 4)), PPM2.AGE_ADULT)

    @NetworkVar('EyelashType',      (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_EYELASHES, PPM2.MAX_EYELASHES)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)

    @NetworkVar('TailType',         (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_TAILS, PPM2.MAX_TAILS)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)
    @NetworkVar('ManeType',         (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_UPPER_MANES, PPM2.MAX_UPPER_MANES)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)
    @NetworkVar('ManeTypeLower',    (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_LOWER_MANES, PPM2.MAX_LOWER_MANES)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)

    @NetworkVar('EyeBackground',    net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))
    @NetworkVar('EyeHole',          net.ReadColor, net.WriteColor, 	    Color(0,   0,   0  ))
    @NetworkVar('EyeIris',          net.ReadColor, net.WriteColor, 	    Color(200, 200, 200))
    @NetworkVar('EyeIris2',         net.ReadColor, net.WriteColor, 	    Color(200, 200, 200))
    @NetworkVar('EyeIrisLine1',     net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))
    @NetworkVar('EyeIrisLine2',     net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))
    @NetworkVar('BodyColor',        net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))

    for i = 1, 6
        @NetworkVar("TailColor#{i}",        net.ReadColor, net.WriteColor,     Color(255, 255, 255))
        @NetworkVar("LowerManeColor#{i}",        net.ReadColor, net.WriteColor,     Color(255, 255, 255))
        @NetworkVar("UpperManeColor#{i}",        net.ReadColor, net.WriteColor,     Color(255, 255, 255))

    @NetworkVar('EyeLines',         net.ReadBool, net.WriteBool,              true)
    @NetworkVar('IrisSize',         (-> math.Clamp(net.ReadFloat(), PPM2.MIN_IRIS, PPM2.MAX_IRIS)), net.WriteFloat, 1)
    @NetworkVar('EyeWidth',         (-> math.Clamp(net.ReadFloat(), PPM2.MIN_PUPIL_SIZE, PPM2.MAX_PUPIL_SIZE)), net.WriteFloat, 1)
    @NetworkVar('TailSize',         (-> math.Clamp(net.ReadFloat(), PPM2.MIN_TAIL_SIZE, PPM2.MAX_TAIL_SIZE)), net.WriteFloat, 1)

    for i = 1, PPM2.MAX_BODY_DETAILS
        @NetworkVar("BodyDetail#{i}", (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_DETAIL, PPM2.MAX_DETAIL)), ((arg = 0) -> net.WriteUInt(arg, 8)), 0)

    new: (netID, ent) =>
        if ent
            @SetEntity(ent)
            @SetupEntity(ent)
        super(netID)
    EntIndex: => @entID
    SetupEntity: (ent) =>
        return unless IsValid(ent)
        @ent = ent
        ent.__PPM2_PonyData = @
        @entID = ent\EntIndex()
    NetworkDataChanges: (state) =>
        if state\GetKey() == 'Entity' and IsValid(@GetEntity())
            @SetupEntity(@GetEntity())
    ApplyTextures: =>
        return if SERVER
        if @textureController
            @textureController\ApplyTextures()
            return
        @textureController = PPM2.PonyTextureController(@ent, @)

PPM2.NetworkedPonyData = NetworkedPonyData

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

PPM2.MIN_WEIGHT = 0.7
PPM2.MAX_WEIGHT = 1.3

PPM2.MIN_TAIL_SIZE = 0.6
PPM2.MAX_TAIL_SIZE = 1.7 -- i luv big tails

PPM2.MIN_IRIS = 0.7
PPM2.MAX_IRIS = 1.3

PPM2.MIN_PUPIL_SIZE = 0.2
PPM2.MAX_PUPIL_SIZE = 1

PPM2.AvaliableTails = {
    'MAILCALL'
    'FLOOFEH'
    'ADVENTUROUS'
    'SHOWBOAT'
    'ASSERTIVE'
    'BOLD'
    'STUMPY'
    'SPEEDSTER'
    'EDGY'
    'RADICAL'
    'BOOKWORM'
    'BUMPKIN'
    'POOFEH'
    'CURLY'
    'NONE'
}

PPM2.AvaliableUpperManes = {
    'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT', 'ASSERTIVE'
    'BOLD', 'STUMPY', 'SPEEDSTER', 'RADICAL', 'SPIKED'
    'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY', 'INSTRUCTOR', 'NONE'
}

PPM2.AvaliableLowerManes = {
    'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
    'ASSERTIVE', 'BOLD', 'STUMPY', 'HIPPIE', 'SPEEDSTER'
    'BOOKWORM', 'BUMPKIN', 'CURLY', 'NONE'
}

PPM2.EyelashTypes = {
    'Default', 'Double', 'Coy', 'Full', 'Mess', 'None'
}

PPM2.BodyDetails = {
    'None', 'Leg gradient', 'Lines', 'Stripes', 'Head stripes'
    'Freckles', 'Hooves big', 'Hooves small', 'Head layer'
    'Hooves big rnd', 'Hooves small rnd', 'Spots 1'
}

PPM2.BodyDetailsEnum = {
    'NONE', 'GRADIENT', 'LINES', 'STRIPES', 'HSTRIPES'
    'FRECKLES', 'HOOF_BIG', 'HOOF_SMALL', 'LAYER'
    'HOOF_BIG_ROUND', 'HOOF_SMALL_ROUND', 'SPOTS'
}

PPM2.MIN_EYELASHES = 0
PPM2.MAX_EYELASHES = #PPM2.EyelashTypes - 1

PPM2.MIN_TAILS = 0
PPM2.MAX_TAILS = #PPM2.AvaliableTails - 1

PPM2.MIN_UPPER_MANES = 0
PPM2.MAX_UPPER_MANES = #PPM2.AvaliableUpperManes - 1

PPM2.MIN_LOWER_MANES = 0
PPM2.MAX_LOWER_MANES = #PPM2.AvaliableLowerManes - 1

PPM2.MIN_DETAIL = 0
PPM2.MAX_DETAIL = #PPM2.BodyDetails - 1

PPM2.GENDER_FEMALE = 0
PPM2.GENDER_MALE = 1

PPM2.MAX_BODY_DETAILS = 8

PPM2.RACE_EARTH = 0
PPM2.RACE_PEGASUS = 1
PPM2.RACE_UNICORN = 2
PPM2.RACE_ALICORN = 3

PPM2.AGE_FILLY = 0
PPM2.AGE_ADULT = 1
PPM2.AGE_MATURE = 2

class NetworkedPonyData extends PPM2.NetworkedObject
    @NW_ClientsideCreation = true
    @Setup()
    @NetworkVar('Entity', net.ReadEntity, net.WriteEntity, NULL)
    @NetworkVar('Race',             (-> math.Clamp(net.ReadUInt(4), 0, 3)), ((arg = PPM2.RACE_EARTH) -> net.WriteUInt(arg, 4)), PPM2.RACE_EARTH)
    @NetworkVar('Gender',           (-> math.Clamp(net.ReadUInt(4), 0, 1)), ((arg = PPM2.GENDER_FEMALE) -> net.WriteUInt(arg, 4)), PPM2.GENDER_FEMALE)
    @NetworkVar('Weight',           (-> math.Clamp(net.ReadFloat(), PPM2.MIN_WEIGHT, PPM2.MAX_WEIGHT)), net.WriteFloat, 1)

    -- Reserved - they can be accessed/used/changed, but they do not do anything
    @NetworkVar('Age',              (-> math.Clamp(net.ReadUInt(4), 0, 2)), ((arg = PPM2.AGE_ADULT) -> net.WriteUInt(arg, 4)), PPM2.AGE_ADULT)

    @NetworkVar('EyelashType',      (-> math.Clamp(net.ReadFloat(), PPM2.MIN_EYELASHES, PPM2.MAX_EYELASHES)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)

    @NetworkVar('TailType',         (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_TAILS, PPM2.MAX_TAILS)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)
    @NetworkVar('ManeType',         (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_UPPER_MANES, PPM2.MAX_UPPER_MANES)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)
    @NetworkVar('ManeTypeLower',    (-> math.Clamp(net.ReadUInt(8), PPM2.MIN_LOWER_MANES, PPM2.MAX_LOWER_MANES)),     ((arg = 0) -> net.WriteUInt(arg, 8)), 0)

    @NetworkVar('EyeBackground',    net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))
    @NetworkVar('EyeHole',          net.ReadColor, net.WriteColor, 	    Color(0,   0,   0  ))
    @NetworkVar('EyeIris',          net.ReadColor, net.WriteColor, 	    Color(200, 200, 200))
    @NetworkVar('EyeIris2',         net.ReadColor, net.WriteColor, 	    Color(200, 200, 200))
    @NetworkVar('EyeIrisLine1',     net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))
    @NetworkVar('EyeIrisLine2',     net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))
    @NetworkVar('BodyColor',        net.ReadColor, net.WriteColor, 	    Color(255, 255, 255))

    for i = 1, 6
        @NetworkVar("TailColor#{i}",        net.ReadColor, net.WriteColor,     Color(255, 255, 255))
        @NetworkVar("LowerManeColor#{i}",        net.ReadColor, net.WriteColor,     Color(255, 255, 255))
        @NetworkVar("UpperManeColor#{i}",        net.ReadColor, net.WriteColor,     Color(255, 255, 255))

    @NetworkVar('EyeLines',         net.ReadBool, net.ReadBool,              true)
    @NetworkVar('IrisSize',         (-> math.Clamp(net.ReadFloat(), PPM2.MIN_IRIS, PPM2.MAX_IRIS)), net.WriteFloat, 1)
    @NetworkVar('EyeWidth',         (-> math.Clamp(net.ReadFloat(), PPM2.MIN_PUPIL_SIZE, PPM2.MAX_PUPIL_SIZE)), net.WriteFloat, 1)
    @NetworkVar('TailSize',         (-> math.Clamp(net.ReadFloat(), PPM2.MIN_TAIL_SIZE, PPM2.MAX_TAIL_SIZE)), net.WriteFloat, 1)

    for i = 1, PPM2.MAX_BODY_DETAILS
        @NetworkVar("BodyDetail#{i}", (-> math.Clamp(net.ReadFloat(), PPM2.MIN_DETAIL, PPM2.MAX_DETAIL)), ((arg = 0) -> net.WriteUInt(arg, 8)), 0)

    new: (netID, ent) =>
        if ent
            @SetEntity(ent)
            @SetupEntity(ent)
        super(netID)
    EntIndex: => @entID
    SetupEntity: (ent) =>
        return unless IsValid(ent)
        @ent = ent
        ent.__PPM2_PonyData = @
        @entID = ent\EntIndex()
    NetworkDataChanges: (state) =>
        if state\GetKey() == 'Entity' and IsValid(@GetEntity())
            @SetupEntity(@GetEntity())
    ApplyTextures: =>
        return if SERVER
        if @textureController
            @textureController\ApplyTextures()
            return
        @textureController = PPM2.PonyTextureController(@ent, @)

PPM2.NetworkedPonyData = NetworkedPonyData


Support spawn points.

Support spawn points.

STI = require('vendor/sti/sti')
bump = require('vendor/bump/bump')
State = require('lib/states/state')
MapLayer = require('lib/map-layer')
PauseState = require('lib/states/pause')
entities = require('lib/entities')
DrawSystem = require('lib/systems/draw')
ControlSystem = require('lib/systems/control')

class ZoneState extends State
	new: (@mapName, spawnPoint) =>
		super()

		@boxWorld = love.physics.newWorld(0, 0)
		@map = STI("assets/maps/#{@mapName}", { 'bump' })
		@world = bump.newWorld(@map.tilewidth)
		@map\bump_init(@world)

		for tile in *@map.layers.entities.objects
			entity = entities[tile.type](@world, tile)

			if tile.type == 'Player'
				@player = entity
				if spawnPoint
					if spawnPoint.x then tile.x = spawnPoint.x
					if spawnPoint.y then tile.y = spawnPoint.y

			@world\add(entity, tile.x, tile.y, tile.width, tile.height)

		entityLayer = MapLayer(@map, 'entities')
		entityLayer.engine\addSystem(DrawSystem())
		entityLayer.engine\addSystem(ControlSystem())
		entityLayer.engine\addEntity(@player)

	update: (dt) =>
		super(dt)
		@map\update(dt)

	draw: () =>
		scale = 2
		windowWidth = love.graphics.getWidth()
		windowHeight = love.graphics.getHeight()
		playerPosition = @player\get('Position')

		tx = math.floor(playerPosition.x - windowWidth / scale / 2)
		ty = math.floor(playerPosition.y - windowHeight / scale / 2)

		love.graphics.push()
		love.graphics.scale(scale)
		love.graphics.translate(-tx, -ty)
		@map\setDrawRange(tx, ty, windowWidth, windowHeight)
		@map\draw()

		if DEBUG
			love.graphics.setColor(255, 0, 0, 255)
			@map\bump_draw(@world)
			love.graphics.setColor(255, 255, 255, 255)

		love.graphics.pop()

		super()

	keypressed: (key) =>
		super(key)
		if key == 'escape' then @states.push(PauseState())

STI = require('vendor/sti/sti')
bump = require('vendor/bump/bump')
State = require('lib/states/state')
MapLayer = require('lib/map-layer')
PauseState = require('lib/states/pause')
entities = require('lib/entities')
DrawSystem = require('lib/systems/draw')
ControlSystem = require('lib/systems/control')

class ZoneState extends State
	new: (@mapName) =>
		super()

		@boxWorld = love.physics.newWorld(0, 0)
		@map = STI("assets/maps/#{@mapName}", { 'bump' })
		@world = bump.newWorld(@map.tilewidth)
		@map\bump_init(@world)

		for object in *@map.layers.entities.objects
			entity = entities[object.type](@world, object)
			@world\add(entity, object.x, object.y, object.width, object.height)
			if object.type == 'Player'
				@player = entity

		entityLayer = MapLayer(@map, 'entities')
		entityLayer.engine\addSystem(DrawSystem())
		entityLayer.engine\addSystem(ControlSystem())
		entityLayer.engine\addEntity(@player)

	update: (dt) =>
		super(dt)
		@map\update(dt)

	draw: () =>
		scale = 2
		windowWidth = love.graphics.getWidth()
		windowHeight = love.graphics.getHeight()
		playerPosition = @player\get('Position')

		tx = math.floor(playerPosition.x - windowWidth / scale / 2)
		ty = math.floor(playerPosition.y - windowHeight / scale / 2)

		love.graphics.push()
		love.graphics.scale(scale)
		love.graphics.translate(-tx, -ty)
		@map\setDrawRange(tx, ty, windowWidth, windowHeight)
		@map\draw()

		if DEBUG
			love.graphics.setColor(255, 0, 0, 255)
			@map\bump_draw(@world)
			love.graphics.setColor(255, 255, 255, 255)

		love.graphics.pop()

		super()

	keypressed: (key) =>
		super(key)
		if key == 'escape' then @states.push(PauseState())


Fixed PonyDataInstance not resetting missing values to default ones on load Fixes https://gitlab.com/DBotThePony/PPM/issues/268

--
-- Copyright (C) 2017-2019 DBot

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

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

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

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

for _, ffind in ipairs file.Find('ppm2/*.txt', 'DATA')
	fTarget = ffind\sub(1, -5)
	-- maybe joined server with old ppm2 and new clear _current was generated
	if not file.Exists('ppm2/' .. fTarget .. '.dat', 'DATA')
		fRead = file.Read('ppm2/' .. ffind, 'DATA')
		json = util.JSONToTable(fRead)
		if json
			TagCompound = DLib.NBT.TagCompound()
			for key, value in pairs json
				switch luatype(value)
					when 'string'
						TagCompound\AddString(key, value)
					when 'number'
						TagCompound\AddFloat(key, value)
					when 'boolean'
						TagCompound\AddByte(key, value and 1 or 0)
					when 'table'
						-- assume color
						TagCompound\AddByteArray(key, {value.r - 128, value.g - 128, value.b - 128, value.a - 128}) if value.r and value.g and value.b and value.a
					else
						error(luatype(value))
			buf = DLib.BytesBuffer()
			TagCompound\WriteFile(buf)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()
	file.Delete('ppm2/' .. ffind)

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

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

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

	@PONY_DATA = PPM2.PonyDataRegistry

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

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

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

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

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

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

		for key, val in pairs @dataTable
			switch luatype(val)
				when 'number', 'string', 'boolean'
					copyOfData[key] = val
				when 'table', 'Color'
					if IsColor(val)
						copyOfData[key] = Color(val)
					else
						copyOfData[key] = Color(255, 255, 255)
		newData = @@(fileName, copyOfData, false)
		return newData
	CreateCustomNetworkObject: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	ApplyDataToObject: (target, ...) =>
		for key, value in pairs @GetAsNetworked()
			error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
			target["Set#{key}"](target, value, ...)
	UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
	CreateController: (...) => @CreateNetworkObject(false, ...)
	CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

	new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
		@SetFilename(filename)
		@NBTTagCompound = DLib.NBT.TagCompound()
		@updateNWObject = true
		@networkNWObject = true
		@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)

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

	@ERR_MISSING_PARAMETER = 4
	@ERR_MISSING_CONTENT = 5

	GetSaveOnChange: => @saveOnChange
	SaveOnChange: => @saveOnChange
	SetSaveOnChange: (val = true) => @saveOnChange = val
	GetValueFromNBT: (mapData, value) =>
		if mapData.enum and type(value) == 'string'
			mapData.fix(mapData.enumMappingBackward[value\upper()])
		elseif mapData.type == 'COLOR'
			if IsColor(value)
				mapData.fix(Color(value))
			else
				mapData.fix(Color(value[1] + 128, value[2] + 128, value[3] + 128, value[4] + 128))
		elseif mapData.type == 'BOOLEAN'
			if type(value) == 'boolean'
				mapData.fix(value)
			else
				mapData.fix(value == 1)
		else
			mapData.fix(value)

	GetExtraBackupPath: => "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.dat"

	SetupData: (data = @NBTTagCompound, force = false, doBackup = false) =>
		if luatype(data) == 'NBTCompound'
			data = data\GetValue()

		if doBackup or not force
			makeBackup = false

			for key, value2 in pairs(data)
				key = key\lower()
				map = @@PONY_DATA_MAPPING[key]

				if map
					mapData = @@PONY_DATA[map]
					value = @GetValueFromNBT(mapData, value2)
					if mapData.enum
						if luatype(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or luatype(value) == 'number' and not mapData.enumMapping[value]
							return @@ERR_MISSING_CONTENT if not force
							makeBackup = true
							break

			if doBackup and makeBackup and @exists
				fRead = file.Read(@fpath, 'DATA')
				file.Write(@GetExtraBackupPath(), fRead)

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

		for key, value2 in pairs(data)
			key = key\lower()
			map = @@PONY_DATA_MAPPING[key]
			continue unless map
			mapData = @@PONY_DATA[map]
			@dataTable[key] = @GetValueFromNBT(mapData, value2)
			dataTable[key] = nil

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

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

	SetFilename: (filename) =>
		@filename = filename
		@filenameFull = "#{filename}.dat"
		@fpath = "#{@@DATA_DIR}#{filename}.dat"
		@preview = "#{@@DATA_DIR}thumbnails/#{filename}.png"
		@fpathFull = "data/#{@@DATA_DIR}#{filename}.dat"
		@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

	SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
	SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal

	GetNetworkOnChange: => @networkNWObject
	GetUpdateOnChange: => @updateNWObject

	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 DLib.NBT.TagString(map.enumMapping[val] or map.enumMapping[map.default()])
		elseif map.serealize
			return map.serealize(val)
		else
			switch map.type
				when 'INT'
					return DLib.NBT.TagInt(val)
				when 'FLOAT'
					return DLib.NBT.TagFloat(val)
				when 'URL'
					return DLib.NBT.TagString(val)
				when 'BOOLEAN'
					return DLib.NBT.TagByte(val and 1 or 0)
				when 'COLOR'
					return DLib.NBT.TagByteArray({val.r - 128, val.g - 128, val.b - 128, val.a - 128})
	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 == ''
		@NBTTagCompound\ReadFile(DLib.BytesBuffer(fRead))
		return @SetupData(@NBTTagCompound, force, doBackup) or @@READ_SUCCESS

	SaveAs: (path = @fpath) =>
		@NBTTagCompound\AddTag(key, @SerealizeValue(key)) for key, val in pairs @dataTable
		buf = DLib.BytesBuffer()
		@NBTTagCompound\WriteFile(buf)
		stream = file.Open(path, 'wb', 'DATA')
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

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

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

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

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

			\SetSequence(22)
			\FrameAdvance(0)

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

			buildingModel\DrawModel()

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

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

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

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

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

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

			cam.End3D()

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

			model\Remove()
			buildingModel\Remove()

			file.Write(path, data)

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

	Save: (doBackup = true, preview = true) =>
		file.Write(@GetExtraBackupPath(), file.Read(@fpath, 'DATA')) if doBackup and @exists
		buf = @SaveAs(@fpath)
		@SavePreview(@preview) if preview
		@exists = true
		return buf

PPM2.PonyDataInstance = PonyDataInstance

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

--
-- Copyright (C) 2017-2019 DBot

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

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

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

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

for _, ffind in ipairs file.Find('ppm2/*.txt', 'DATA')
	fTarget = ffind\sub(1, -5)
	-- maybe joined server with old ppm2 and new clear _current was generated
	if not file.Exists('ppm2/' .. fTarget .. '.dat', 'DATA')
		fRead = file.Read('ppm2/' .. ffind, 'DATA')
		json = util.JSONToTable(fRead)
		if json
			TagCompound = DLib.NBT.TagCompound()
			for key, value in pairs json
				switch luatype(value)
					when 'string'
						TagCompound\AddString(key, value)
					when 'number'
						TagCompound\AddFloat(key, value)
					when 'boolean'
						TagCompound\AddByte(key, value and 1 or 0)
					when 'table'
						-- assume color
						TagCompound\AddByteArray(key, {value.r - 128, value.g - 128, value.b - 128, value.a - 128}) if value.r and value.g and value.b and value.a
					else
						error(luatype(value))
			buf = DLib.BytesBuffer()
			TagCompound\WriteFile(buf)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()
	file.Delete('ppm2/' .. ffind)

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

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

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

	@PONY_DATA = PPM2.PonyDataRegistry

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

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

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

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

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

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

		for key, val in pairs @dataTable
			switch luatype(val)
				when 'number', 'string', 'boolean'
					copyOfData[key] = val
				when 'table', 'Color'
					if IsColor(val)
						copyOfData[key] = Color(val)
					else
						copyOfData[key] = Color(255, 255, 255)
		newData = @@(fileName, copyOfData, false)
		return newData
	CreateCustomNetworkObject: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		@ApplyDataToObject(newData, ...)
		return newData
	ApplyDataToObject: (target, ...) =>
		for key, value in pairs @GetAsNetworked()
			error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
			target["Set#{key}"](target, value, ...)
	UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
	CreateController: (...) => @CreateNetworkObject(false, ...)
	CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

	new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
		@SetFilename(filename)
		@NBTTagCompound = DLib.NBT.TagCompound()
		@updateNWObject = true
		@networkNWObject = true
		@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)

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

	@ERR_MISSING_PARAMETER = 4
	@ERR_MISSING_CONTENT = 5

	GetSaveOnChange: => @saveOnChange
	SaveOnChange: => @saveOnChange
	SetSaveOnChange: (val = true) => @saveOnChange = val
	GetValueFromNBT: (mapData, value) =>
		if mapData.enum and type(value) == 'string'
			mapData.fix(mapData.enumMappingBackward[value\upper()])
		elseif mapData.type == 'COLOR'
			if IsColor(value)
				mapData.fix(Color(value))
			else
				mapData.fix(Color(value[1] + 128, value[2] + 128, value[3] + 128, value[4] + 128))
		elseif mapData.type == 'BOOLEAN'
			if type(value) == 'boolean'
				mapData.fix(value)
			else
				mapData.fix(value == 1)
		else
			mapData.fix(value)

	GetExtraBackupPath: => "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.dat"

	SetupData: (data = @NBTTagCompound, force = false, doBackup = false) =>
		if luatype(data) == 'NBTCompound'
			data = data\GetValue()
		if doBackup or not force
			makeBackup = false
			for key, value2 in pairs(data)
				key = key\lower()
				map = @@PONY_DATA_MAPPING[key]
				if map
					mapData = @@PONY_DATA[map]
					value = @GetValueFromNBT(mapData, value2)
					if mapData.enum
						if luatype(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or luatype(value) == 'number' and not mapData.enumMapping[value]
							return @@ERR_MISSING_CONTENT if not force
							makeBackup = true
							break
			if doBackup and makeBackup and @exists
				fRead = file.Read(@fpath, 'DATA')
				file.Write(@GetExtraBackupPath(), fRead)

		for key, value2 in pairs(data)
			key = key\lower()
			map = @@PONY_DATA_MAPPING[key]
			continue unless map
			mapData = @@PONY_DATA[map]
			@dataTable[key] = @GetValueFromNBT(mapData, value2)
	ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
		if @nwObj and @updateNWObject
			{:getFunc} = @@PONY_DATA[key]
			@nwObj["Set#{getFunc}"](@nwObj, newVal, @networkNWObject)
		@Save() if saveNow
	SetFilename: (filename) =>
		@filename = filename
		@filenameFull = "#{filename}.dat"
		@fpath = "#{@@DATA_DIR}#{filename}.dat"
		@preview = "#{@@DATA_DIR}thumbnails/#{filename}.png"
		@fpathFull = "data/#{@@DATA_DIR}#{filename}.dat"
		@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

	SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
	SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal

	GetNetworkOnChange: => @networkNWObject
	GetUpdateOnChange: => @updateNWObject

	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 DLib.NBT.TagString(map.enumMapping[val] or map.enumMapping[map.default()])
		elseif map.serealize
			return map.serealize(val)
		else
			switch map.type
				when 'INT'
					return DLib.NBT.TagInt(val)
				when 'FLOAT'
					return DLib.NBT.TagFloat(val)
				when 'URL'
					return DLib.NBT.TagString(val)
				when 'BOOLEAN'
					return DLib.NBT.TagByte(val and 1 or 0)
				when 'COLOR'
					return DLib.NBT.TagByteArray({val.r - 128, val.g - 128, val.b - 128, val.a - 128})
	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 == ''
		@NBTTagCompound\ReadFile(DLib.BytesBuffer(fRead))
		return @SetupData(@NBTTagCompound, force, doBackup) or @@READ_SUCCESS

	SaveAs: (path = @fpath) =>
		@NBTTagCompound\AddTag(key, @SerealizeValue(key)) for key, val in pairs @dataTable
		buf = DLib.BytesBuffer()
		@NBTTagCompound\WriteFile(buf)
		stream = file.Open(path, 'wb', 'DATA')
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

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

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

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

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

			\SetSequence(22)
			\FrameAdvance(0)

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

			buildingModel\DrawModel()

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

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

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

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

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

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

			cam.End3D()

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

			model\Remove()
			buildingModel\Remove()

			file.Write(path, data)

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

	Save: (doBackup = true, preview = true) =>
		file.Write(@GetExtraBackupPath(), file.Read(@fpath, 'DATA')) if doBackup and @exists
		buf = @SaveAs(@fpath)
		@SavePreview(@preview) if preview
		@exists = true
		return buf

PPM2.PonyDataInstance = PonyDataInstance

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


refactor wall handling while turning

refactor wall handling while turning

require("lib/lgm/lgm")

export ^

defaultSpeed = 50 -- px/s
deltaSpeed = 50
maxSpeed = 200

class Player extends LGM.Entity
    new: (x, y, @dirX, @dirY, @color) =>
        super(x,y)
        @basePosition = {x, y}
        @baseDirection = {@dirX, @dirY}
        @score = 0
        @speed = defaultSpeed
        @linkedWallsID = {}
        @lastWall = None
        @resetLastWall()

    update: (dt) =>
        oldX = @x
        oldY = @y

        @x += @dirX * @speed * dt
        @y += @dirY * @speed * dt

        @lastWall.x2 = @x - @dirX
        @lastWall.y2 = @y - @dirY

        foundCollision = false
        moveSegment = LGM.Segment(LGM.Vector(oldX, oldY), LGM.Vector(@x, @y))
        -- collisions with walls
        for wall in wallSet\iter()
            if wall ~= @lastWall
                if moveSegment\intersect(wall\as_segment(), False)
                    foundCollision = wall\as_segment()

        if not foundCollision
            @speed = math.min(maxSpeed, @speed + deltaSpeed * dt)
        else
            @score -= 1
            @x, @y = @basePosition[1], @basePosition[2]
            @dirX, @dirY = @baseDirection[1], @baseDirection[2]
            @speed = 0
            for id in *@linkedWallsID
                wallSet\removeID(id)
            @linkedWallsID = {}
            @resetLastWall()

    draw: =>
        love.graphics.setColor(@color)
        love.graphics.circle("fill", @x, @y, 5)

    keypressed: (k) =>
        if k == "up" and @dirY == 0
            @dirX = 0
            @dirY = -1
            @hasTurned()
        if k == "right" and @dirX == 0
            @dirX = 1
            @dirY = 0
            @hasTurned()
        if k == "left" and @dirX == 0
            @dirX = -1
            @dirY = 0
            @hasTurned()
        if k == "down" and @dirY == 0
            @dirX = 0
            @dirY = 1
            @hasTurned()

    resetLastWall: =>
        @lastWall = Wall(@x, @y, @x, @y, @color, 0)
        wallSet\add(@lastWall)
        table.insert(@linkedWallsID, @lastWall.id)

    hasTurned: =>
        -- finalize lastWall (in global wallSet)
        @lastWall.time = 0
        @lastWall.x2 = @x
        @lastWall.y2 = @y

        -- create new wall for the new direction
        @resetLastWall()

        playSfxTurn()
        -- hotfix to not crash into its own wall
        @x += @dirX
        @y += @dirY

export playerSet = LGM.EntitySet()

require("lib/lgm/lgm")

export ^

defaultSpeed = 50 -- px/s
deltaSpeed = 50
maxSpeed = 200

class Player extends LGM.Entity
    new: (x, y, @dirX, @dirY, @color) =>
        super(x,y)
        @basePosition = {x, y}
        @baseDirection = {@dirX, @dirY}
        @score = 0
        @speed = defaultSpeed
        @linkedWallsID = {}
        @lastWall = Wall(x, y, x, y, @color, -1)
        wallSet\add(@lastWall)
        table.insert(@linkedWallsID, @lastWall.id)

    update: (dt) =>
        oldX = @x
        oldY = @y

        @x += @dirX * @speed * dt
        @y += @dirY * @speed * dt

        @lastWall.x2 = @x
        @lastWall.y2 = @y

        foundCollision = false
        moveSegment = LGM.Segment(LGM.Vector(oldX, oldY), LGM.Vector(@x, @y))
        -- collisions with walls
        for wall in wallSet\iter()
            if wall ~= @lastWall
                if moveSegment\intersect(wall\as_segment(), False)
                    foundCollision = wall\as_segment()

        if not foundCollision
            @speed = math.min(maxSpeed, @speed + deltaSpeed * dt)
        else
            @score -= 1
            @x, @y = @basePosition[1], @basePosition[2]
            @dirX, @dirY = @baseDirection[1], @baseDirection[2]
            @lastWallX, @lastWallY = @x, @y
            @speed = 0
            for id in *@linkedWallsID
                wallSet\removeID(id)
            @linkedWallsID = {}

    draw: =>
        love.graphics.setColor(@color)
        love.graphics.circle("fill", @x, @y, 5)

    keypressed: (k) =>
        if k == "up" and @dirY == 0
            @dirX = 0
            @dirY = -1
            @hasTurned()
        if k == "right" and @dirX == 0
            @dirX = 1
            @dirY = 0
            @hasTurned()
        if k == "left" and @dirX == 0
            @dirX = -1
            @dirY = 0
            @hasTurned()
        if k == "down" and @dirY == 0
            @dirX = 0
            @dirY = 1
            @hasTurned()

    hasTurned: () =>
        -- trigger last wall
        -- (@lastWall is also referenced in global wallSet)
        @lastWall.time = 0
        -- create new last wall
        @lastWall = Wall(@x, @y, @x, @y, @color, 0)
        wallSet\add(@lastWall)
        table.insert(@linkedWallsID, @lastWall.id)
        playSfxTurn()
        -- hotfix to not crash into its own wall
        @x += @dirX
        @y += @dirY

export playerSet = LGM.EntitySet()


plugins/cap: Support SASL, add CAP_ACK.<cap>, fix cap counting

import IRCClient from require 'irc'

caps = {'extended-join', 'multi-prefix', 'away-notify', 'account-notify',
	    'chghost', 'server-time', 'echo-message', 'invite-notify'
	    'draft/rename', 'sasl'}

for cap in *caps
	caps[cap] = true -- allow index-based lookup

IRCClient\add_hook 'CONNECT', =>
	@ircv3_caps = {}
	@set_caps = 0

IRCClient\add_hook 'CAP_ACK', =>
	@set_caps -= 1
	if @set_caps <= 0
		@send_raw 'CAP END'

PROCESS_OPTS = {LS: true, ACK: true, NAK: true, DEL: true, NEW: true}

IRCClient\add_handler 'CAP', (prefix, args, trailing)=>
	to_process = {} if PROCESS_OPTS[args[2]]

	-- Take all trailing caps and put into list if supported
	if PROCESS_OPTS[args[2]]
		for item in trailing\gmatch '%S+'
			if caps[item]
				table.insert to_process, item

	@log "Requesting: #{table.concat(to_process, ', ')}"

	-- Request all supported
	if args[2] == 'LS'
		if #to_process > 0
			@send_raw ('CAP REQ :%s')\format table.concat(to_process, ' ')
			@set_caps += #to_process

	-- Request new caps if supported
	elseif args[2] == 'NEW'
		to_send = {}
		for item in trailing\gmatch '%S+'
			for cap in *caps
				if item == cap
					to_send[#to_send + 1] = item
		@send_raw ('CAP REQ :%s')\format table.concat(to_send, ' ')

	-- Delete cap, server no longer supports (SASL, etc. if services go down)
	elseif args[2] == 'DEL'
		for item in trailing\gmatch '%S+'
			@ircv3_caps[item] = nil

	-- Run CAP_ACK hook for all succeeding caps
	elseif args[2] == 'ACK'
		for cap in *to_process
			key, value = cap\match '^(.-)=(.+)'
			cap_name = key or cap
			if value
				@server.ircv3_caps[key] = value
			else
				@server.ircv3_caps[cap] = true
			@fire_hook "CAP_ACK.#{cap_name}", value
			@fire_hook 'CAP_ACK'

	-- Run CAP_ACK hook for NAK'd caps
	elseif args[2] == 'NAK'
		for i=1, #to_process
			@fire_hook 'CAP_ACK'

IRCClient\add_command "list-caps", (prefix, channel)=>
	caps = [k for k in pairs(@server.ircv3_caps)]
	@send "COMMAND_OK", channel, "list-caps", "Caps: #{table.concat(caps, ', ')}"

import IRCClient from require 'irc'

caps = {'extended-join', 'multi-prefix', 'away-notify', 'account-notify',
	    'chghost', 'server-time', 'echo-message', 'invite-notify'
	    'draft/rename'}

for cap in *caps
	caps[cap] = true -- allow index-based lookup

IRCClient\add_hook 'CONNECT', =>
	@ircv3_caps = {}
	@set_caps = 0

IRCClient\add_hook 'CAP_ACK', =>
	@set_caps -= 1
	if @set_caps <= 0
		@send_raw 'CAP END'

PROCESS_OPTS = {LS: true, ACK: true, NAK: true, DEL: true, NEW: true}

IRCClient\add_handler 'CAP', (prefix, args, trailing)=>
	to_process = {} if PROCESS_OPTS[args[2]]

	-- Take all trailing caps and put into list if supported
	if PROCESS_OPTS[args[2]]
		for item in trailing\gmatch '%S+'
			if caps[item]
				table.insert to_process, item

	@log "Requesting: #{table.concat(to_process, ', ')}"

	-- Request all supported
	if args[2] == 'LS'
		if #to_process > 0
			@send_raw ('CAP REQ :%s')\format table.concat(to_process, ' ')
			@set_caps += 1

	-- Request new caps if supported
	elseif args[2] == 'NEW'
		to_send = {}
		for item in trailing\gmatch '%S+'
			for cap in *caps
				if item == cap
					to_send[#to_send + 1] = item
		@send_raw ('CAP REQ :%s')\format table.concat(to_send, ' ')

	-- Delete cap, server no longer supports (SASL, etc. if services go down)
	elseif args[2] == 'DEL'
		for item in trailing\gmatch '%S+'
			@ircv3_caps[item] = nil

	-- Run CAP_ACK hook for all succeeding caps
	elseif args[2] == 'ACK'
		for cap in *to_process
			key, value = cap\match '^(.-)=(.+)'
			if value
				@server.ircv3_caps[key] = value
			else
				@server.ircv3_caps[cap] = true
			@fire_hook 'CAP_ACK'

	-- Run CAP_ACK hook for NAK'd caps
	elseif args[2] == 'NAK'
		for i=1, #to_process
			@fire_hook 'CAP_ACK'

IRCClient\add_command "list-caps", (prefix, channel)=>
	caps = [k for k in pairs(@server.ircv3_caps)]
	@send "COMMAND_OK", channel, "list-caps", "Caps: #{table.concat(caps, ', ')}"