Fixed booleans not being saved

Fixed booleans not being saved

--
-- Copyright (C) 2017-2018 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.
--

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

for ffind in *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 type(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(type(value))
			buf = DLib.BytesBuffer()
			TagCompound\WriteFile(buf)
			file.Write('ppm2/' .. fTarget .. '.dat', buf\ToString())
	file.Delete('ppm2/' .. ffind)

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

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

	@FindInstances = =>
		output = [@(str\sub(1, #str - 4)) for str in *file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.txt')]
		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 *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, :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 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, ...)

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

	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: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		newData\SetEntity(ply)
		@ApplyDataToObject(newData, ...)
		return newData
	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		newData\SetEntity(LocalPlayer())
		@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 value.r and value.g and value.b and value.a
				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)

	SetupData: (data = @NBTTagCompound, force = false, doBackup = false) =>
		if type(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 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())}.dat"
				fRead = file.Read(@fpath, 'DATA')
				file.Write(bkName, 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"
		@fpathBackup = "#{@@DATA_DIR}#{filename}.bak.dat"
		@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)
		file.Write(path, buf\ToString())
		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 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) =>
		if doBackup and @exists
			fRead = file.Read(@fpath, 'DATA')
			file.Write(@fpathBackup, fRead)
		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-2018 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.
--

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

for ffind in *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 type(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(type(value))
			buf = DLib.BytesBuffer()
			TagCompound\WriteFile(buf)
			file.Write('ppm2/' .. fTarget .. '.dat', buf\ToString())
	file.Delete('ppm2/' .. ffind)

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

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

	@FindInstances = =>
		output = [@(str\sub(1, #str - 4)) for str in *file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.txt')]
		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 *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, :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 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, ...)

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

	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: (goingToNetwork = false, ply = LocalPlayer(), ...) =>
		newData = PPM2.NetworkedPonyData(nil, ply)
		newData\SetIsGoingToNetwork(goingToNetwork)
		newData\SetEntity(ply)
		@ApplyDataToObject(newData, ...)
		return newData
	CreateNetworkObject: (goingToNetwork = true, ...) =>
		newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
		newData\SetIsGoingToNetwork(goingToNetwork)
		newData\SetEntity(LocalPlayer())
		@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 value.r and value.g and value.b and value.a
				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)

	SetupData: (data = @NBTTagCompound, force = false, doBackup = false) =>
		if type(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 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())}.dat"
				fRead = file.Read(@fpath, 'DATA')
				file.Write(bkName, 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"
		@fpathBackup = "#{@@DATA_DIR}#{filename}.bak.dat"
		@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(tonumber(val))
				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)
		file.Write(path, buf\ToString())
		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 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) =>
		if doBackup and @exists
			fRead = file.Read(@fpath, 'DATA')
			file.Write(@fpathBackup, fRead)
		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


k: skip k itself

k: skip k itself

-- k, reimplemented from dbot's k by byte[]
history = {}

PRIVMSG: {
  '^%pk$': (source, destination) =>
    t = {}
    max = 5
    for i = 1, max
      h = history[destination][math.random(#history[destination])]
      msg = h
      want = math.random(2, 4)
      getting = false
      msg = msg\gsub("\003%d?%d?,?%d?%d?", "")
      for w in msg\gmatch("[%w%[%]%{%}%|`%^_%-%|'\194-\244\128-\191]+") do
        if w == "http" or w == "https"
          break
        if not getting
          -- Skip k itself
          if w == 'k'
            continue
          -- Less odds if not starting with letter.
          if w\find("^[%a']") or math.random(2) == 1
            if math.random(1, max + i + 1) <= i
              getting = true
        if getting
          if want > 0
            t[#t + 1] = w\lower()
          else
            break
          want = want - 1
    outline = table.concat(t, " ")
    @Msg 'privmsg', destination, source, outline
  (source, destination, argument) =>
    unless history[destination]
      history[destination] = {}
    table.insert history[destination], #history[destination]+1, argument
    if #history[destination] > 20
      table.remove history[destination], 1
}

-- k, reimplemented from dbot's k by byte[]
history = {}

PRIVMSG: {
  '^%pk$': (source, destination) =>
    t = {}
    max = 5
    for i = 1, max
      h = history[destination][math.random(#history[destination])]
      msg = h
      want = math.random(2, 4)
      getting = false
      msg = msg\gsub("\003%d?%d?,?%d?%d?", "")
      for w in msg\gmatch("[%w%[%]%{%}%|`%^_%-%|'\194-\244\128-\191]+") do
        if w == "http" or w == "https"
          break
        if not getting
          -- Less odds if not starting with letter.
          if w\find("^[%a']") or math.random(2) == 1
            if math.random(1, max + i + 1) <= i
              getting = true
        if getting
          if want > 0
            t[#t + 1] = w\lower()
          else
            break
          want = want - 1
    outline = table.concat(t, " ")
    @Msg 'privmsg', destination, source, outline
  (source, destination, argument) =>
    unless history[destination]
      history[destination] = {}
    table.insert history[destination], #history[destination]+1, argument
    if #history[destination] > 20
      table.remove history[destination], 1
}


`evalprint` does not handle(print) error

parse = require'moonscript.parse'
compile = require'moonscript.compile'
inspect = require'inspect'
ms = require'moonscript.base'
import remove, insert, concat from table

init_moonpath = ->
	moonpath = package.moonpath
	ms.insert_loader!

	moonpath != nil

deinit_moonpath = (has_moonpath) ->
	ms.remove_loader!
	unless has_moonpath
		package.moonpath = nil

printerr = (...) -> io.stderr\write "#{concat {...}, "\t"}\n"

to_lua = (code) ->
	tree, err = parse.string code

	return nil, err if err

	lua_code, err, pos = compile.tree tree

	unless lua_code
		nil, compile.format_error err, pos, code
	else lua_code

-- Lua evaluator & printer
fnwrap = (code) -> "return function(__newenv) local _ENV = setmetatable(__newenv, {__index = _ENV}) #{code} end"

evalprint = (env, lua_code, non_verbose) ->
	has_moonpath = init_moonpath!

	lua_code = if vardec = lua_code\match"^local%s+(.*)$"
			if exportFnCl = vardec\match "^%w+%s+(.*)$"
				if exportFnCl\match "^="
					"#{lua_code\match"local%s+(%w+)"} #{exportFnCl}"
				else exportFnCl
			else vardec
		elseif lua_code\match"__class"
			lua_code = lua_code\gsub "^local%s+", "export" , "1"
		else lua_code

	luafn, err = load fnwrap(lua_code), "tmp"

	return printerr err if err

	result = {pcall luafn!, env}

	deinit_moonpath has_moonpath

	ok = remove result, 1

	ok, unless ok then result[1]
	else
		if #result > 0
			print (inspect result)\match"^%s*{%s*(.*)%s*}%s*%n?%s*$" unless non_verbose
			table.unpack result

{:printerr, :to_lua, :fnwrap, :evalprint, :init_moonpath, :deinit_moonpath}

parse = require'moonscript.parse'
compile = require'moonscript.compile'
inspect = require'inspect'
import remove, insert, concat from table

printerr = (...) -> io.stderr\write "#{concat {...}, "\t"}\n"

to_lua = (code) ->
	tree, err = parse.string code

	return nil, err if err

	lua_code, err, pos = compile.tree tree

	unless lua_code
		nil, compile.format_error err, pos, code
	else lua_code

-- Lua evaluator & printer
fnwrap = (code) -> "return function(__newenv) local _ENV = setmetatable(__newenv, {__index = _ENV}) #{code} end"

evalprint = (env, lua_code) ->
	lua_code = if vardec = lua_code\match"^local%s+(.*)$"
			if exportFnCl = vardec\match "^%w+%s+(.*)$"
				if exportFnCl\match "^="
					"#{lua_code\match"local%s+(%w+)"} #{exportFnCl}"
				else exportFnCl
			else vardec
		elseif lua_code\match"__class"
			lua_code = lua_code\gsub "^local%s+", "export" , "1"
		else lua_code

	luafn, err = load fnwrap(lua_code), "tmp"

	return printerr err if err

	result = {pcall luafn!, env}

	ok = remove result, 1

	unless ok then printerr result[1]
	else
		if #result > 0
			print (inspect result)\match"^%s*{%s*(.*)%s*}%s*%n?%s*$"
			table.unpack result

{:printerr, :to_lua, :fnwrap, :evalprint}


Add update notice on group data change

Add update notice on group data change

-- Copyright (c) 2014 by Adam Hellberg <sharparam@sharparam.com>.
--
-- 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.

NAME, T = ...

import log, misc from T

{comm_manager: comm, event_manager: em} = T

local is_any_nil

{
    :GetExpansionLevel, :GetRealmName, :GetNumSubgroupMembers
    :IsInGroup
    :LE_PARTY_CATEGORY_HOME
    :UnitLevel, :UnitName, :UnitXP, :UnitXPMax
} = _G

T.xp_manager =
    max_levels:
        [0]: 60  -- Classic/Vanilla
        [1]: 70  -- TBC
        [2]: 80  -- WotLK
        [3]: 85  -- Cataclysm
        [4]: 90  -- MoP
        [5]: 100 -- WoD
    max_level: 255 -- Placeholder/fallback value replaced at load
    data: {}

    update: (name, current_level, max_level, current_xp, max_xp) =>
        name = misc.fix_name name
        current_level = tonumber current_level
        max_level = tonumber max_level
        current_xp = tonumber current_xp
        max_xp = tonumber max_xp

        if is_any_nil current_level, max_level, current_xp, max_xp
            return log\error 'Invalid data received from %s', name

        unless @data[name]
            @data[name] = {}
            realm = name\match '-(.+)$'
            local_realm = GetRealmName!\gsub '%s', ''
            @data[name].friendly_name = (realm == local_realm) and name\match '(.+)-' or name

        data = with @data[name]
            .current_level = current_level
            .max_level = max_level
            .current_xp = current_xp
            .max_xp = max_xp
        em\fire 'SHAREXP_XP_UPDATED'
        log\debug 'XP updated for %s', data.friendly_name

    send_xp: =>
        lvl = UnitLevel 'player'
        xp = UnitXP 'player'
        max_xp = UnitXPMax 'player'
        comm\send 'PARTY', 'xpupdate', lvl, @max_level, xp, max_xp
        log\debug 'Sent XP update to party'

    request_xp: =>
        comm\send 'PARTY', 'xprequest'
        log\debug 'Sent XP request to party'

    check_group: =>
        num = GetNumSubgroupMembers LE_PARTY_CATEGORY_HOME
        group = {}
        for i = 1, num
            unit = 'party' .. i
            name, realm = UnitName unit
            name = misc.fix_name name, realm
            group[name] = true

        for k, _ in pairs @data
            unless group[k]
                log\debug 'Removing %s from group, no longer present', k
                @data[k] = nil
                em\fire 'SHAREXP_XP_UPDATED'

is_any_nil = (...) ->
    for item in *{...}
        if type(item) == 'nil' then return true
    false

xp = T.xp_manager

comm\add 'xpupdate', (channel, sender, ...) ->
    -- ARGS:
    -- 1: current level
    -- 2: max level
    -- 3: current xp
    -- 4: max xp
    log\debug 'Received XP update from %s:%s', channel, sender
    xp\update sender, ...

comm\add 'xprequest', (channel, sender) ->
    log\debug 'Received XP request from %s:%s', channel, sender
    xp\send_xp!

T.PLAYER_ENTERING_WORLD = =>
    xp\send_xp!
    xp\request_xp! if IsInGroup!

T.PLAYER_XP_UPDATE = =>
    xp\send_xp!

T.GROUP_ROSTER_UPDATE = =>
    xp\send_xp!
    xp\check_group!

T.SHAREXP_LOADED = =>
    expansion_level = GetExpansionLevel!
    max_lvl = xp.max_levels[expansion_level]
    xp.max_level = max_lvl

-- Copyright (c) 2014 by Adam Hellberg <sharparam@sharparam.com>.
--
-- 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.

NAME, T = ...

import log, misc from T

comm = T.comm_manager

local is_any_nil

{
    :GetExpansionLevel, :GetRealmName, :GetNumSubgroupMembers
    :IsInGroup
    :LE_PARTY_CATEGORY_HOME
    :UnitLevel, :UnitName, :UnitXP, :UnitXPMax
} = _G

T.xp_manager =
    max_levels:
        [0]: 60  -- Classic/Vanilla
        [1]: 70  -- TBC
        [2]: 80  -- WotLK
        [3]: 85  -- Cataclysm
        [4]: 90  -- MoP
        [5]: 100 -- WoD
    max_level: 255 -- Placeholder/fallback value replaced at load
    data: {}

    update: (name, current_level, max_level, current_xp, max_xp) =>
        name = misc.fix_name name
        current_level = tonumber current_level
        max_level = tonumber max_level
        current_xp = tonumber current_xp
        max_xp = tonumber max_xp

        if is_any_nil current_level, max_level, current_xp, max_xp
            return log\error 'Invalid data received from %s', name

        unless @data[name]
            @data[name] = {}
            realm = name\match '-(.+)$'
            local_realm = GetRealmName!\gsub '%s', ''
            @data[name].friendly_name = (realm == local_realm) and name\match '(.+)-' or name

        data = with @data[name]
            .current_level = current_level
            .max_level = max_level
            .current_xp = current_xp
            .max_xp = max_xp
        T.event_manager\fire 'SHAREXP_XP_UPDATED'
        log\debug 'XP updated for %s', data.friendly_name

    send_xp: =>
        lvl = UnitLevel 'player'
        xp = UnitXP 'player'
        max_xp = UnitXPMax 'player'
        comm\send 'PARTY', 'xpupdate', lvl, @max_level, xp, max_xp
        log\debug 'Sent XP update to party'

    request_xp: =>
        comm\send 'PARTY', 'xprequest'
        log\debug 'Sent XP request to party'

    check_group: =>
        num = GetNumSubgroupMembers LE_PARTY_CATEGORY_HOME
        group = {}
        for i = 1, num
            unit = 'party' .. i
            name, realm = UnitName unit
            name = misc.fix_name name, realm
            group[name] = true

        for k, _ in pairs @data
            unless group[k]
                log\debug 'Removing %s from group, no longer present', k
                @data[k] = nil

is_any_nil = (...) ->
    for item in *{...}
        if type(item) == 'nil' then return true
    false

xp = T.xp_manager

comm\add 'xpupdate', (channel, sender, ...) ->
    -- ARGS:
    -- 1: current level
    -- 2: max level
    -- 3: current xp
    -- 4: max xp
    log\debug 'Received XP update from %s:%s', channel, sender
    xp\update sender, ...

comm\add 'xprequest', (channel, sender) ->
    log\debug 'Received XP request from %s:%s', channel, sender
    xp\send_xp!

T.PLAYER_ENTERING_WORLD = =>
    xp\send_xp!
    xp\request_xp! if IsInGroup!

T.PLAYER_XP_UPDATE = =>
    xp\send_xp!

T.GROUP_ROSTER_UPDATE = =>
    xp\send_xp!
    xp\check_group!

T.SHAREXP_LOADED = =>
    expansion_level = GetExpansionLevel!
    max_lvl = xp.max_levels[expansion_level]
    xp.max_level = max_lvl


imageupload: store name, fix encoding issue

Former-commit-id: ea70d71611c7b91fceab62f39352a5b1c54e5287 [formerly 310a86fb438f6900f15e075ef2eacb7f2571d93c]
Former-commit-id: c38a17f5832d0428fec4e104d3317ff48f150a56

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

urlbase = '/image/'

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

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

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

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

    return

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

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

  html = [[
  <!DOCTYPE html>

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

  <title>IRCSNAP</title>

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

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

    <form>

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

        Your name

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

        Attach text

    </form>

Uploading. Percent complete: 0 %

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

        What is this?

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

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

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

  send html

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

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

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

urlbase = '/image/'

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

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

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

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

    return

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

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

  html = [[
  <!DOCTYPE html>

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

  <title>IRCSNAP</title>

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

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

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

        Your name

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

        Attach text

    </form>

Uploading. Percent complete: 0 %

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

        What is this?

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

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

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

  send html

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

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


Add env.name

Add env.name


Use create date if exif date is missing

Use create date if exif date is missing

lapis = require "lapis"
json = require "cjson"
import capture_errors from require "lapis.application"
import respond_to, capture_errors, capture_errors_json, assert_error, yield_error from require "lapis.application"
import validate, assert_valid from require "lapis.validate"
import escape_pattern, trim_filter, to_json from require "lapis.util"
db = require "lapis.db"
import Redis, Users, Albums, Images, Accesstokens, generate_token from require "photongx.models"
config = require("lapis.config").get!

require_login = (fn) ->
  =>
    if @current_user
      fn @
    else
      redirect_to: @url_for "admin"

json_params = (fn) ->
  =>
    body = ngx.req.get_body_data!
    if body
      @json_params = json.decode body
      fn @
    else
      json:{error:"No parameters recieved"}

persona_verify = (assertion, audience) ->

    options = { method:ngx.HTTP_POST, body:json.encode {:assertion, :audience} }
    res, err = ngx.location.capture('/persona/', options)

    if not res then
      return { err: res }

    if res.status >= 200 and res.status < 300 then
      return json.decode(res.body)
    else
      return {
        status:res.status,
        body:res.body
      }

class extends lapis.Application
  layout: require "photongx.views.layout"
  views_prefix: "photongx.views"

  @before_filter =>
    @current_user = Users\read_session @

  [index: "/"]: =>
    if @current_user
      return redirect_to: @url_for "albums"
    render:true

  [albums: "/albums"]: require_login =>
    @albums = Albums\select "where user_id = ?", @current_user.id
    -- FIXME improve SQL to single statement
    for album in *@albums
      images = Images\select "where album_id = ? order by views desc limit 1", album.id
      album.image = images[1]
      album.url = @url_for("album", token:album.token, title:album.title)
    render: true

  [tokenalbums: "/albums/:slug/"]: capture_errors =>
    @accesstokens = Accesstokens\for_slug @params.slug
    unless #@accesstokens > 0
      return render: "error", status: 404
    album_ids = [a.album_id for a in *@accesstokens]
    @albums = Albums\find_all album_ids

    -- FIXME improve SQL to single statement
    for album in *@albums
      images = Images\select "where album_id = ? order by views desc limit 1", album.id
      album.image = images[1]
      -- Override the token with our given slug so the template generates the correct URLs
      album.slug = @params.slug
      album.url = @url_for("tokenalbum", slug:@params.slug, token:album.token, title:album.title)
    render: "albums"

  [album: "/album/:token/:title/"]: =>
    if @current_user
      @album = Albums\find token:@params.token
      unless @album
        return render:"error", status:404
      unless @current_user.id == @album.user_id
        return render:"error", status:403
    unless @album return render:"error", status:403
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ?", @album.id
    render: true

  [tokenalbum: "/album/:slug/:token/:title/"]: =>
    @album = Albums\find token:@params.token
    unless @album
      return render:"error", status:404
    valid_token = Accesstokens\validate_album @params.slug, @album.id
    unless valid_token
      return render:"error", status:410
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ?", @album.id
    render: "album"

--
-- This view gets called from a rewrite_by_lua handler so it exits
-- because: "Note that when calling ngx.exit(ngx.OK) within a rewrite_by_lua handler, the nginx request processing control flow will still continue to the content handler."
--
  [img: "/img/:token/:filename"]: =>

    -- Check if user has permission to see image or return 403
    -- Check if token is valid
    -- Set real url or return 404

    -- Simple auth check for now
    --unless @current_user
    --  ngx.exit(403)

    -- TODO maybe disallow original?

    @image = Images\find token:@params.token
    unless @image
      ngx.exit(404)

    -- Check access tokens and return status 410 if expired
    imguri = '/real' .. @image\file_path! .. '/' ..@params.filename
    ngx.req.set_uri imguri, true

  [persona_login: "/api/persona/login"]: respond_to {
    POST: capture_errors_json =>
      body = ngx.req.get_body_data!
      if body
        body = json.decode body

        verification_data = persona_verify body.assertion, config.site
        if verification_data.status == 'okay'
          Users\write_session @, verification_data
          return json:verification_data
      json: {email:false}
  }
  [persona_status: "/api/persona/status"]: respond_to {
    GET: =>
      render: true

    POST: capture_errors_json =>
      cu = @current_user
      if cu
        return json:{email:cu.email}
      json:{email:false}
  }
  [user_logout: "/api/persona/logout"]: =>
    @session.user = false
    json: {email:false}

-- ALBUMS API view - get albums or add new album
  [apialbums: "/api/albums"]: respond_to {
    GET: capture_errors_json require_login =>
      albums = assert_error Albums\select "where user_id = ?", @current_user.id
      json: {:albums}

    POST: capture_errors_json json_params require_login =>

      assert_valid @json_params, {
        { "name", exists: true, min_length: 1 }
      }
      -- Get or create
      album = Albums\find user_id: @current_user.id, title:@json_params.name
      unless album
        album = Albums\create @current_user.id, @json_params.name
      json: { album: album }
  }

  "/api/albums/:album_id": respond_to {
    PUT: capture_errors_json require_login json_params =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      album.title = @json_params.title
      album\update "title"
      json: {:album}
    DELETE: capture_errors_json require_login =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      unless album
        return render:"error", status:404
      album\delete!
      json:album
  }

  "/api/images/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      images = assert_error Images\select "where user_id = ? and album_id = ?", @current_user.id, @params.album_id
      --- TODO maybe use raw query to cast hstore to json?
      -- Examples found:https://gist.github.com/WoLpH/2318757
      for image in *images
        if image.metadata
          newstr, n, err = ngx.re.gsub(image.metadata, "=>", ":")
          image.metadata = json.decode '{'..newstr..'}'
      json: {:images}
  }

  "/api/accesstokens/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      accesstokens = Accesstokens\select "where user_id = ? and album_id = ?", @current_user.id, @params.album_id
      json: {:accesstokens}

    DELETE: =>
      accesstoken = Accesstokens\find id:@params.album_id, user_id:@current_user.id
      unless accesstoken
        return render:"error", status:404
      accesstoken\delete!
      json:{ :accesstoken }
  }

  [images: "/api/images"]: respond_to {
    GET:capture_errors_json require_login  =>
      images = assert_error Images\select "where user_id = ?", @current_user.id
      json: {:images}

    POST: capture_errors_json require_login =>
        assert_valid @params, {
            {'upload', file_exists: true}
            {'filename', exists: true}
            {'title', exists: true}
            {'token', exists: true}
            {'checksum', exists: true}
        }
        {:upload, :title, :filename, :token, :checksum} = @params
        pattern = '\\.(jpe?g|gif|png)$'
        unless ngx.re.match(filename, pattern, "i")
            return json:status:403, error:'Filename must be of image type'
        file = @params.upload
        album = assert_error Albums\find user_id: @current_user.id, token:token
        image = assert_error Images\create @current_user.id, album.id, filename
        content = file.content
        real_file_name = image\real_file_name!
        diskfile = io.open real_file_name, 'w'
        unless diskfile
          -- TODO delete created image from SQL
          return status:403, json:{status:403, error:"Permission denied"}
        diskfile\write file.content
        diskfile\flush!
        diskfile\close
        redis = Redis!
        queue = assert_error redis\queue image.token
        json: 'success'
  }

  [photostreamimages: "/api/photostreamimages"]: respond_to {
    GET:capture_errors_json require_login  =>
      photostreamimages = assert_error db.select [[
        *,
        date_part('epoch',
          COALESCE(
            to_timestamp(metadata->'CreateDate', 'YYYY:MM:DD HH24:MI:SS'),
            created_at
          ))*1000 AS createdate
        FROM images
        WHERE user_id = ?
        ORDER BY createdate DESC]], @current_user.id
      json: {:photostreamimages}
    }

  "/api/albumttl/:album_id": respond_to {
    POST: capture_errors_json require_login =>
      assert_valid @params, {
        { "name", exists: true}
        { "ttl", exists: true}
      }
      ttl = tonumber @params.ttl
      -- TTL not a number ? Assume forever.
      if ttl == nil
          ttl = 2^32 -- ~150 years

      name = @params.name
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      accesstoken = Accesstokens\create @current_user.id, album.id, name, ttl

      json: {:album,:accesstoken}
    }

  [admin: "/admin/"]: =>
    layout:'admin'

  "/api/tag": =>
    json: {token: generate_token 6}

  "/api/image/:image_id": respond_to {
    DELETE: capture_errors_json require_login =>
      image = Images\find id:@params.image_id, user_id: @current_user.id
      unless image
        return render:"error", status:"404"
      image\delete!
      json:image
  }

  "/api/img/click": =>
    assert_valid @params, {
      { "img", exists: true, min_length: 6 }
    }
    @image = Images\find token:@params.img
    unless @image
      return render: "error", status: 404
    @image.views = @image.views + 1
    @image\update "views"

    json: {views: @image.views}

  [adminapi: "/admin/api/all"]: =>
    ok = "test"
    json: { status: ok}

  "/api/queue": require_login capture_errors_json =>
    redis = Redis!
    queue = assert_error redis\queue_length!
    json: {counter:queue}

  "/db/make": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      schema = require "schema"
      schema.make_schema!
      return json: { status: "ok" }
    json: status: 403

  "/db/destroy": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      schema = require "schema"
      schema.destroy_schema!
      return json: { status: "ok" }
    json: status: 403

  "/db/migrate": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      import run_migrations from require "lapis.db.migrations"
      run_migrations require "migrations"
      return json: { status: "ok" }
    json: status: 403

  "/debug": =>
    json: config

lapis = require "lapis"
json = require "cjson"
import capture_errors from require "lapis.application"
import respond_to, capture_errors, capture_errors_json, assert_error, yield_error from require "lapis.application"
import validate, assert_valid from require "lapis.validate"
import escape_pattern, trim_filter, to_json from require "lapis.util"
db = require "lapis.db"
import Redis, Users, Albums, Images, Accesstokens, generate_token from require "photongx.models"
config = require("lapis.config").get!

require_login = (fn) ->
  =>
    if @current_user
      fn @
    else
      redirect_to: @url_for "admin"

json_params = (fn) ->
  =>
    body = ngx.req.get_body_data!
    if body
      @json_params = json.decode body
      fn @
    else
      json:{error:"No parameters recieved"}

persona_verify = (assertion, audience) ->

    options = { method:ngx.HTTP_POST, body:json.encode {:assertion, :audience} }
    res, err = ngx.location.capture('/persona/', options)

    if not res then
      return { err: res }

    if res.status >= 200 and res.status < 300 then
      return json.decode(res.body)
    else
      return {
        status:res.status,
        body:res.body
      }

class extends lapis.Application
  layout: require "photongx.views.layout"
  views_prefix: "photongx.views"

  @before_filter =>
    @current_user = Users\read_session @

  [index: "/"]: =>
    if @current_user
      return redirect_to: @url_for "albums"
    render:true

  [albums: "/albums"]: require_login =>
    @albums = Albums\select "where user_id = ?", @current_user.id
    -- FIXME improve SQL to single statement
    for album in *@albums
      images = Images\select "where album_id = ? order by views desc limit 1", album.id
      album.image = images[1]
      album.url = @url_for("album", token:album.token, title:album.title)
    render: true

  [tokenalbums: "/albums/:slug/"]: capture_errors =>
    @accesstokens = Accesstokens\for_slug @params.slug
    unless #@accesstokens > 0
      return render: "error", status: 404
    album_ids = [a.album_id for a in *@accesstokens]
    @albums = Albums\find_all album_ids

    -- FIXME improve SQL to single statement
    for album in *@albums
      images = Images\select "where album_id = ? order by views desc limit 1", album.id
      album.image = images[1]
      -- Override the token with our given slug so the template generates the correct URLs
      album.slug = @params.slug
      album.url = @url_for("tokenalbum", slug:@params.slug, token:album.token, title:album.title)
    render: "albums"

  [album: "/album/:token/:title/"]: =>
    if @current_user
      @album = Albums\find token:@params.token
      unless @album
        return render:"error", status:404
      unless @current_user.id == @album.user_id
        return render:"error", status:403
    unless @album return render:"error", status:403
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ?", @album.id
    render: true

  [tokenalbum: "/album/:slug/:token/:title/"]: =>
    @album = Albums\find token:@params.token
    unless @album
      return render:"error", status:404
    valid_token = Accesstokens\validate_album @params.slug, @album.id
    unless valid_token
      return render:"error", status:410
    @album.views = @album.views + 1
    @album\update "views"
    @images = Images\select "where album_id = ?", @album.id
    render: "album"

--
-- This view gets called from a rewrite_by_lua handler so it exits
-- because: "Note that when calling ngx.exit(ngx.OK) within a rewrite_by_lua handler, the nginx request processing control flow will still continue to the content handler."
--
  [img: "/img/:token/:filename"]: =>

    -- Check if user has permission to see image or return 403
    -- Check if token is valid
    -- Set real url or return 404

    -- Simple auth check for now
    --unless @current_user
    --  ngx.exit(403)

    -- TODO maybe disallow original?

    @image = Images\find token:@params.token
    unless @image
      ngx.exit(404)

    -- Check access tokens and return status 410 if expired
    imguri = '/real' .. @image\file_path! .. '/' ..@params.filename
    ngx.req.set_uri imguri, true

  [persona_login: "/api/persona/login"]: respond_to {
    POST: capture_errors_json =>
      body = ngx.req.get_body_data!
      if body
        body = json.decode body

        verification_data = persona_verify body.assertion, config.site
        if verification_data.status == 'okay'
          Users\write_session @, verification_data
          return json:verification_data
      json: {email:false}
  }
  [persona_status: "/api/persona/status"]: respond_to {
    GET: =>
      render: true

    POST: capture_errors_json =>
      cu = @current_user
      if cu
        return json:{email:cu.email}
      json:{email:false}
  }
  [user_logout: "/api/persona/logout"]: =>
    @session.user = false
    json: {email:false}

-- ALBUMS API view - get albums or add new album
  [apialbums: "/api/albums"]: respond_to {
    GET: capture_errors_json require_login =>
      albums = assert_error Albums\select "where user_id = ?", @current_user.id
      json: {:albums}

    POST: capture_errors_json json_params require_login =>

      assert_valid @json_params, {
        { "name", exists: true, min_length: 1 }
      }
      -- Get or create
      album = Albums\find user_id: @current_user.id, title:@json_params.name
      unless album
        album = Albums\create @current_user.id, @json_params.name
      json: { album: album }
  }

  "/api/albums/:album_id": respond_to {
    PUT: capture_errors_json require_login json_params =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      album.title = @json_params.title
      album\update "title"
      json: {:album}
    DELETE: capture_errors_json require_login =>
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      unless album
        return render:"error", status:404
      album\delete!
      json:album
  }

  "/api/images/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      images = assert_error Images\select "where user_id = ? and album_id = ?", @current_user.id, @params.album_id
      --- TODO maybe use raw query to cast hstore to json?
      -- Examples found:https://gist.github.com/WoLpH/2318757
      for image in *images
        if image.metadata
          newstr, n, err = ngx.re.gsub(image.metadata, "=>", ":")
          image.metadata = json.decode '{'..newstr..'}'
      json: {:images}
  }

  "/api/accesstokens/:album_id": respond_to {

    GET: capture_errors_json require_login =>
      accesstokens = Accesstokens\select "where user_id = ? and album_id = ?", @current_user.id, @params.album_id
      json: {:accesstokens}

    DELETE: =>
      accesstoken = Accesstokens\find id:@params.album_id, user_id:@current_user.id
      unless accesstoken
        return render:"error", status:404
      accesstoken\delete!
      json:{ :accesstoken }
  }

  [images: "/api/images"]: respond_to {
    GET:capture_errors_json require_login  =>
      images = assert_error Images\select "where user_id = ?", @current_user.id
      json: {:images}

    POST: capture_errors_json require_login =>
        assert_valid @params, {
            {'upload', file_exists: true}
            {'filename', exists: true}
            {'title', exists: true}
            {'token', exists: true}
            {'checksum', exists: true}
        }
        {:upload, :title, :filename, :token, :checksum} = @params
        pattern = '\\.(jpe?g|gif|png)$'
        unless ngx.re.match(filename, pattern, "i")
            return json:status:403, error:'Filename must be of image type'
        file = @params.upload
        album = assert_error Albums\find user_id: @current_user.id, token:token
        image = assert_error Images\create @current_user.id, album.id, filename
        content = file.content
        real_file_name = image\real_file_name!
        diskfile = io.open real_file_name, 'w'
        unless diskfile
          -- TODO delete created image from SQL
          return status:403, json:{status:403, error:"Permission denied"}
        diskfile\write file.content
        diskfile\flush!
        diskfile\close
        redis = Redis!
        queue = assert_error redis\queue image.token
        json: 'success'
  }

  [photostreamimages: "/api/photostreamimages"]: respond_to {
    GET:capture_errors_json require_login  =>
      photostreamimages = assert_error db.select "*, date_part('epoch', to_timestamp(metadata->'CreateDate', 'YYYY:MM:DD HH24:MI:SS'))*1000 AS createdate FROM images WHERE user_id = ? ORDER BY metadata->'CreateDate'", @current_user.id
      json: {:photostreamimages}
    }

  "/api/albumttl/:album_id": respond_to {
    POST: capture_errors_json require_login =>
      assert_valid @params, {
        { "name", exists: true}
        { "ttl", exists: true}
      }
      ttl = tonumber @params.ttl
      -- TTL not a number ? Assume forever.
      if ttl == nil
          ttl = 2^32 -- ~150 years

      name = @params.name
      album = Albums\find id:@params.album_id, user_id: @current_user.id
      accesstoken = Accesstokens\create @current_user.id, album.id, name, ttl

      json: {:album,:accesstoken}
    }

  [admin: "/admin/"]: =>
    layout:'admin'

  "/api/tag": =>
    json: {token: generate_token 6}

  "/api/image/:image_id": respond_to {
    DELETE: capture_errors_json require_login =>
      image = Images\find id:@params.image_id, user_id: @current_user.id
      unless image
        return render:"error", status:"404"
      image\delete!
      json:image
  }

  "/api/img/click": =>
    assert_valid @params, {
      { "img", exists: true, min_length: 6 }
    }
    @image = Images\find token:@params.img
    unless @image
      return render: "error", status: 404
    @image.views = @image.views + 1
    @image\update "views"

    json: {views: @image.views}

  [adminapi: "/admin/api/all"]: =>
    ok = "test"
    json: { status: ok}

  "/api/queue": require_login capture_errors_json =>
    redis = Redis!
    queue = assert_error redis\queue_length!
    json: {counter:queue}

  "/db/make": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      schema = require "schema"
      schema.make_schema!
      return json: { status: "ok" }
    json: status: 403

  "/db/destroy": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      schema = require "schema"
      schema.destroy_schema!
      return json: { status: "ok" }
    json: status: 403

  "/db/migrate": require_login =>
    -- Hard coded to first user for now
    if @current_user.id == 1
      import run_migrations from require "lapis.db.migrations"
      run_migrations require "migrations"
      return json: { status: "ok" }
    json: status: 403

  "/debug": =>
    json: config


Conditional remove chosen tasks from apply-upload

html = require "lapis.html"
import render_and_pass from require "utils"

class ApplyResult extends html.Widget
    content: =>
        chosen_tasks = @application\get_chosen_tasks!
        apply_content = capture ->
            h1 @m.subheading
            raw @m.text
            details ->
                div class: "left", @m.info_label
                div class: "right", ->
                    strong "#{@application.first_name} #{@application.last_name}"
                    if type(@application.school) == 'number'
                        raw ", #{@a.form.schools[@application.school]}, #{@a.form.classes[@application.class]} #{@a.form.class_suffix} ("
                    else
                        raw ", #{html.escape @application.school}, #{@a.form.classes[@application.class]} #{@a.form.class_suffix} ("

                    a href: "mailto:#{@application.email}",
                         @application.email
                    raw ")"

                if #@a.tasks > 0
                    div class: "left", @m.chosen_tasks_label
                div class: "right", ->
                    ul ->
                        for t in *chosen_tasks
                            t = @a.tasks[t.task]
                            li t.name

            form enctype: "multipart/form-data", method: "POST", ->
                with @m.form
                    input type: "hidden", name: "csrf-token", value: @csrf_token
                    label for: "pitch", id: "pitch-label", .pitch_label
                    textarea required: "required", id: "pitch", name: "pitch", placeholder: .pitch_placeholder
                    for task in *chosen_tasks
                        t = @a.tasks[task.task]
                        h2 t.name
                        raw t.prompt
                        p ->
                            a class: "upload", ->
                                span .select_files
                                input
                                    required: "required", type: "file", name: "tasks[#{task.task}]",
                                    accept: table.concat [".#{x}" for x in *t.filetypes], ','
                            span class: "filename"

                    div class: "status"

                    button type: "submit", id: "submit-button", .submit_text
                    div class: "spinner", style: "display: none", .please_wait

        render_and_pass widget, "views.apply-base", { :apply_content }

html = require "lapis.html"
import render_and_pass from require "utils"

class ApplyResult extends html.Widget
    content: =>
        chosen_tasks = @application\get_chosen_tasks!
        apply_content = capture ->
            h1 @m.subheading
            raw @m.text
            details ->
                div class: "left", @m.info_label
                div class: "right", ->
                    strong "#{@application.first_name} #{@application.last_name}"
                    if type(@application.school) == 'number'
                        raw ", #{@a.form.schools[@application.school]}, #{@a.form.classes[@application.class]} #{@a.form.class_suffix} ("
                    else
                        raw ", #{html.escape @application.school}, #{@a.form.classes[@application.class]} #{@a.form.class_suffix} ("

                    a href: "mailto:#{@application.email}",
                         @application.email
                    raw ")"

                div class: "left", @m.chosen_tasks_label
                div class: "right", ->
                    ul ->
                        for t in *chosen_tasks
                            t = @a.tasks[t.task]
                            li t.name

            form enctype: "multipart/form-data", method: "POST", ->
                with @m.form
                    input type: "hidden", name: "csrf-token", value: @csrf_token
                    label for: "pitch", id: "pitch-label", .pitch_label
                    textarea required: "required", id: "pitch", name: "pitch", placeholder: .pitch_placeholder
                    for task in *chosen_tasks
                        t = @a.tasks[task.task]
                        h2 t.name
                        raw t.prompt
                        p ->
                            a class: "upload", ->
                                span .select_files
                                input
                                    required: "required", type: "file", name: "tasks[#{task.task}]",
                                    accept: table.concat [".#{x}" for x in *t.filetypes], ','
                            span class: "filename"

                    div class: "status"

                    button type: "submit", id: "submit-button", .submit_text
                    div class: "spinner", style: "display: none", .please_wait

        render_and_pass widget, "views.apply-base", { :apply_content }


main: use MoonScript instead of Lua, fixes debugging

#!/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'
moonscript = require "moonscript"

import IRCClient from require 'irc'

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

pcall -> -- use StackTracePlus if available; it's much better than builtin
	debug.traceback = require("StackTracePlus").stacktrace

load_modules = (folder)->
	for file in lfs.dir folder
		if file\match "%.moon$"
			path = "#{folder}/#{file}"
			IRCClient\with_context path, assert moonscript.loadfile path

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 = {}
conf_home = os.getenv('XDG_CONFIG_HOME') or os.getenv('HOME') .. '/.config'
for file in lfs.dir "#{conf_home}/moonmoon"
	if file\match "%.lua$"
		local data
		env =
			bot: (name)->
				return (file_data)->
					file_data.name = name
					file_data.file = file
					data = file_data

		fn = assert loadfile("#{conf_home}/moonmoon/#{file}", nil, env)
		fn!
		unless data and data.server
			print "Missing `server` field: [#{file}]"
			continue
		if os.getenv 'DEBUG'
			for key, value in pairs data
				if type(value) == "string" then
					print ("%q: %q")\format key, value
				else
					print ("%q: %s")\format key, value
		bot = IRCClient data.server, data.port, data
		table.insert(bots, bot)

queue = cqueues.new!

package.loaded["queue"] = queue -- for use in modules

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.config.server
					Logger.debug Logger.level.error .. '*** ' .. err
				else
					break
			if not success
				Logger.print Logger.level.fatal .. '*** Not connecting anymore for: ' .. bot.config.file
				return

			ok, err = pcall -> bot\loop!
			if not ok then
				Logger.print Logger.level.error .. err

while not queue\empty!
	ok, err = queue\step!
	if not ok
		Logger.debug "#{Logger.level.error} *** #{err}"

#!/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'

pcall -> -- use StackTracePlus if available; it's much better than builtin
	debug.traceback = require("StackTracePlus").stacktrace

load_modules = (folder)->
	for file in lfs.dir folder
		if file\match "%.lua$"
			path = "#{folder}/#{file}"
			IRCClient\with_context path, loadfile path

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 = {}
conf_home = os.getenv('XDG_CONFIG_HOME') or os.getenv('HOME') .. '/.config'
for file in lfs.dir "#{conf_home}/moonmoon"
	if file\match "%.lua$"
		local data
		(loadfile "#{conf_home}/moonmoon/#{file}", nil, {
			bot: (name)->
				return (file_data)->
					file_data.name = name
					file_data.file = file
					data = file_data
		})()
		unless data and data.server
			print "Missing `server` field: [#{file}]"
			continue
		if os.getenv 'DEBUG'
			for key, value in pairs data
				if type(value) == "string" then
					print ("%q: %q")\format key, value
				else
					print ("%q: %s")\format key, value
		bot = IRCClient data.server, data.port, data
		table.insert(bots, bot)

queue = cqueues.new!

package.loaded["queue"] = queue -- for use in modules

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.config.server
					Logger.debug Logger.level.error .. '*** ' .. err
				else
					break
			if not success
				Logger.print Logger.level.fatal .. '*** Not connecting anymore for: ' .. bot.config.file
				return

			ok, err = pcall -> bot\loop!
			if not ok then
				Logger.print Logger.level.error .. err

while not queue\empty!
	ok, err = queue\loop!
	if not ok
		Logger.debug "#{Logger.level.error} *** #{err}"


Fixed flight collisions

Fixed flight collisions

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

ALLOW_FLIGHT = CreateConVar('ppm2_sv_flight', '1', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow flight for pegasus and alicorns. It obeys PlayerNoClip hook.')
FORCE_ALLOW_FLIGHT = CreateConVar('ppm2_sv_flight_force', '0', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Ignore PlayerNoClip hook')
SUPPRESS_CLIENTSIDE_CHECK = CreateConVar('ppm2_sv_flight_nocheck', '0', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Suppress PlayerNoClip clientside check (useful with bad coded addons. known are - ULX, Cinema, FAdmin)')
FLIGHT_DAMAGE = CreateConVar('ppm2_sv_flightdmg', '1', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Damage players in flight')

class PonyflyController
	new: (data) =>
		@controller = data
		@ent = data.ent
		@speedMult = 0.5
		@speedMultDirections = 1.25
		@dragMult = 0.95
		@obbCenter = Vector(0, 0, 0)
		@obbMins = Vector(0, 0, 0)
		@obbMaxs = Vector(0, 0, 0)
		@speedMultDiv = 2
		@speedMultLift = 2
		@pitch = 0
		@yaw = 0
		@roll = 0
		@angleLerp = 1
		@lastVelocity = Vector(0, 0, 0)
		@lastState = false

	GetEntity: => @ent
	GetData: => @controller
	GetController: => @controller

	Switch: (status = false) =>
		@ent = @controller.ent
		return if not IsValid(@ent)
		return if @lastState == status
		@lastState = status
		if not status
			{:p, :y, :r} = @ent\EyeAngles()
			newAng = Angle(p, y, 0)
			@ent\SetEyeAngles(newAng)
			@ent\SetMoveType(MOVETYPE_WALK)
			@roll = 0
			@pitch = 0
			@yaw = 0
			@ent\SetVelocity(@lastVelocity * 50)
			@lastVelocity = Vector(0, 0, 0)
		else
			@lastVelocity = Vector(0, 0, 0)
			@ent\SetVelocity(-@ent\GetVelocity() * .97)
			@ent\SetMoveType(MOVETYPE_CUSTOM)
			@obbCenter = @ent\OBBCenter()
			@obbMins = @ent\OBBMins()
			@obbMaxs = @ent\OBBMaxs()
			@roll = 0
			@pitch = 0
			@yaw = 0
	Think: (movedata) =>
		pos     = movedata\GetOrigin()
		ang     = movedata\GetAngles()
		fwd     = ang\Forward()
		bcwd    = -ang\Forward()
		right   = ang\Right()
		left    = -ang\Right()
		up      = ang\Up()
		down    = -ang\Up()
		W       = movedata\KeyDown(IN_FORWARD)
		S       = movedata\KeyDown(IN_BACK)
		D       = movedata\KeyDown(IN_MOVERIGHT)
		A       = movedata\KeyDown(IN_MOVELEFT)
		CTRL    = movedata\KeyDown(IN_DUCK)
		MULT    = FrameTime() * 66

		velocity = movedata\GetVelocity()
		cSpeed = velocity\Length()
		cSpeed = 1 if cSpeed < 1
		dragSqrt = math.min(math.sqrt(cSpeed) / cSpeed * 2, 0.99)
		cSpeedMult = @speedMultDiv / cSpeed
		cSpeedMult = @speedMultDiv if cSpeedMult ~= cSpeedMult
		cSpeedLiftMult = @speedMultLift / cSpeed
		cSpeedLiftMult = @speedMultLift if cSpeedLiftMult ~= cSpeedLiftMult

		dragCalc = math.Clamp(@dragMult / dragSqrt, 0, 0.99)
		pitch = 0
		yaw = 0
		roll = 0

		hit = false
		hitLift = false

		if W
			velocity += fwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			pitch += 20

		if S
			velocity += bcwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			pitch -= 20

		if A
			velocity += left * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			roll -= 20

		if D
			velocity += right * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			roll += 20

		if CTRL
			velocity += Vector(0, 0, -MULT * @speedMult * cSpeedLiftMult)
			hitLift = true

		if @isLiftingUp
			velocity += Vector(0, 0, MULT * @speedMult * cSpeedLiftMult)
			hitLift = true

		if CLIENT
			lerpMult = FrameTime() * @angleLerp
			{:p, :y, :r} = ang
			p -= @pitch
			y -= @yaw
			@pitch = Lerp(lerpMult, @pitch, pitch)
			@yaw = Lerp(lerpMult, @yaw, yaw)
			@roll = Lerp(lerpMult, @roll, roll)
			p += @pitch
			y += @yaw
			r = @roll + math.sin(RealTime()) * 2
			newAng = Angle(p, y, r)
			@ent\SetEyeAngles(newAng)

		if not hit
			velocity.x *= dragCalc
			velocity.y *= dragCalc

		if not hitLift
			velocity.z *= dragCalc
			velocity.z += math.sin(RealTime() * 2) * .01

		pos += velocity

		movedata\SetVelocity(velocity)
		movedata\SetOrigin(pos)
		return true

	SetupMove: (movedata, cmd) =>
		@isLiftingUp = movedata\KeyDown(IN_JUMP)
		if @isLiftingUp
			movedata\SetButtons(movedata\GetButtons() - IN_JUMP)
		cmd\SetButtons(cmd\GetButtons() - IN_JUMP) if cmd\KeyDown(IN_JUMP)

	FinishMove: (movedata) =>
		nativeEntity = @ent\GetEntity()
		mvPos = movedata\GetOrigin()
		pos = nativeEntity\GetPos()
		rpos = pos
		tryMove = util.TraceHull({
			filter: (ent) ->
				return false if nativeEntity == ent
				return true if not IsValid(ent)
				collision = ent\GetCollisionGroup()
				return collision ~= COLLISION_GROUP_WORLD and
					collision ~= COLLISION_GROUP_DEBRIS_TRIGGER and
					collision ~= COLLISION_GROUP_WEAPON and
					collision ~= COLLISION_GROUP_PASSABLE_DOOR and
					collision ~= COLLISION_GROUP_DEBRIS and
					ent\GetOwner() ~= nativeEntity and
					ent\GetParent() ~= nativeEntity and
					ent\IsSolid()
			mins: @obbMins
			maxs: @obbMaxs
			start: rpos
			endpos: mvPos
		})

		velocity = movedata\GetVelocity()
		newVelocity = velocity
		length = velocity\Length()

		if not tryMove.Hit
			nativeEntity\SetPos(mvPos)
		else
			if IsValid(tryMove.Entity)
				newVelocity = Vector(0, 0, 0)
				movedata\SetVelocity(newVelocity)
				newPos = tryMove.HitPos + tryMove.HitNormal
				nativeEntity\SetPos(newPos)
				movedata\SetOrigin(newPos)
			else
				newVelocity = velocity - tryMove.HitNormal * velocity\Dot(tryMove.HitNormal * 1.1)
				movedata\SetVelocity(newVelocity)
				newPos = tryMove.HitPos + tryMove.HitNormal
				nativeEntity\SetPos(newPos)
				movedata\SetOrigin(newPos)
			if length > 7 and SERVER and FLIGHT_DAMAGE\GetBool()
				dmgInfo = DamageInfo()
				dmgInfo\SetAttacker(nativeEntity)
				dmgInfo\SetInflictor(nativeEntity)
				dmgInfo\SetDamageType(DMG_CRUSH)
				calcDamage = math.Clamp((length / 4) ^ 2, 1, 100)
				if calcDamage >= 100
					nativeEntity\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
				elseif calcDamage > 70
					nativeEntity\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
				elseif calcDamage > 50
					nativeEntity\EmitSound("physics/body/body_medium_break#{math.random(2, 4)}.wav", 75)
				elseif calcDamage > 25
					nativeEntity\EmitSound("physics/body/body_medium_impact_hard#{math.random(1, 6)}.wav", 75)
				elseif calcDamage > 10
					nativeEntity\EmitSound("physics/body/body_medium_impact_soft#{math.random(1, 7)}.wav", 75)
				else
					nativeEntity\EmitSound("physics/flesh/flesh_impact_bullet#{math.random(1, 5)}.wav", 75)
				dmgInfo\SetDamage(calcDamage)
				nativeEntity\TakeDamageInfo(dmgInfo)
		@lastVelocity = newVelocity

PPM2.PonyflyController = PonyflyController

import IsPonyCached, GetPonyData, GetTable, SetIK, IsNewPonyCached from FindMetaTable('Entity')
import AnimRestartGesture, AnimResetGestureSlot from FindMetaTable('Player')

hook.Add 'SetupMove', 'PPM2.Ponyfly', (movedata, cmd) =>
	return if not IsPonyCached(@)
	data = GetPonyData(@)
	return if not data or not data\GetFly()
	flight = data\GetFlightController()
	return if not flight
	return flight\SetupMove(movedata, cmd)

hook.Add 'Move', 'PPM2.Ponyfly', (movedata) =>
	return if not IsPonyCached(@)
	data = GetPonyData(@)
	return if not data or not data\GetFly()
	flight = data\GetFlightController()
	return if not flight
	return flight\Think(movedata)

hook.Add 'FinishMove', 'PPM2.Ponyfly', (movedata) =>
	return if not IsPonyCached(@)
	data = GetPonyData(@)
	return if not data or not data\GetFly()
	flight = data\GetFlightController()
	return if not flight
	return flight\FinishMove(movedata)

hook.Add 'CalcMainActivity', 'PPM2.Ponyfly', (movedata) =>
	return if not IsNewPonyCached(@)
	if data = GetPonyData(@)
		if data\GetFly()
			if not @isPlayingPPM2Anim
				@isPlayingPPM2Anim = true
				AnimRestartGesture(@, GESTURE_SLOT_CUSTOM, ACT_GMOD_NOCLIP_LAYER, false)
				SetIK(@, false) if CLIENT
			return ACT_GMOD_NOCLIP_LAYER, 370
		else
			if @isPlayingPPM2Anim
				@isPlayingPPM2Anim = false
				AnimResetGestureSlot(@, GESTURE_SLOT_CUSTOM)
				SetIK(@, true) if CLIENT

if SERVER
	concommand.Add 'ppm2_fly', =>
		return if not ALLOW_FLIGHT\GetBool()
		return if not IsValid(@)
		return if not @IsPonyCached()
		data = @GetPonyData()
		return if not data
		if data\GetFly()
			return data\SetFly(false) if FORCE_ALLOW_FLIGHT\GetBool()
			can = hook.Run('PlayerNoClip', @, false) or hook.Run('PPM2Fly', @, false)
			data\SetFly(false) if can
		else
			return if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
			return data\SetFly(true) if FORCE_ALLOW_FLIGHT\GetBool()
			can = hook.Run('PlayerNoClip', @, true) or hook.Run('PPM2Fly', @, true)
			data\SetFly(true) if can
else
	lastDouble = 0
	lastMessage = 0
	lastMessage2 = 0
	hook.Add 'PlayerBindPress', 'PPM2.Ponyfly', (bind = '', pressed = false) =>
		return if not ALLOW_FLIGHT\GetBool()
		return if not pressed
		return if bind ~= '+jump' and bind ~= 'jump'
		if lastDouble > RealTime()
			return if not @IsPonyCached()
			data = @GetPonyData()
			return if not data
			if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
				if lastMessage < RealTime()
					lastMessage = RealTime() + 1
					PPM2.ChatPrint('You need to be a Pegasus or an Alicorn to fly!')
				return
			if not FORCE_ALLOW_FLIGHT\GetBool() and not SUPPRESS_CLIENTSIDE_CHECK\GetBool()
				can = hook.Run('PlayerNoClip', @, not data\GetFly()) or hook.Run('PPM2Fly', @, not data\GetFly())
				if not can
					if lastMessage2 < RealTime()
						lastMessage2 = RealTime() + 1
						PPM2.ChatPrint("You can not #{data\GetFly() and 'land' or 'fly'} right now.")
					return
			RunConsoleCommand('ppm2_fly')
			lastDouble = 0
			return
		lastDouble = RealTime() + 0.2

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

ALLOW_FLIGHT = CreateConVar('ppm2_sv_flight', '1', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow flight for pegasus and alicorns. It obeys PlayerNoClip hook.')
FORCE_ALLOW_FLIGHT = CreateConVar('ppm2_sv_flight_force', '0', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Ignore PlayerNoClip hook')
SUPPRESS_CLIENTSIDE_CHECK = CreateConVar('ppm2_sv_flight_nocheck', '0', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Suppress PlayerNoClip clientside check (useful with bad coded addons. known are - ULX, Cinema, FAdmin)')
FLIGHT_DAMAGE = CreateConVar('ppm2_sv_flightdmg', '1', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Damage players in flight')

class PonyflyController
	new: (data) =>
		@controller = data
		@ent = data.ent
		@speedMult = 0.5
		@speedMultDirections = 1.25
		@dragMult = 0.95
		@obbCenter = Vector(0, 0, 0)
		@obbMins = Vector(0, 0, 0)
		@obbMaxs = Vector(0, 0, 0)
		@speedMultDiv = 2
		@speedMultLift = 2
		@pitch = 0
		@yaw = 0
		@roll = 0
		@angleLerp = 1
		@lastVelocity = Vector(0, 0, 0)
		@lastState = false

	GetEntity: => @ent
	GetData: => @controller
	GetController: => @controller

	Switch: (status = false) =>
		@ent = @controller.ent
		return if not IsValid(@ent)
		return if @lastState == status
		@lastState = status
		if not status
			{:p, :y, :r} = @ent\EyeAngles()
			newAng = Angle(p, y, 0)
			@ent\SetEyeAngles(newAng)
			@ent\SetMoveType(MOVETYPE_WALK)
			@roll = 0
			@pitch = 0
			@yaw = 0
			@ent\SetVelocity(@lastVelocity * 50)
			@lastVelocity = Vector(0, 0, 0)
		else
			@lastVelocity = Vector(0, 0, 0)
			@ent\SetVelocity(-@ent\GetVelocity() * .97)
			@ent\SetMoveType(MOVETYPE_CUSTOM)
			@obbCenter = @ent\OBBCenter()
			@obbMins = @ent\OBBMins()
			@obbMaxs = @ent\OBBMaxs()
			@roll = 0
			@pitch = 0
			@yaw = 0
	Think: (movedata) =>
		pos     = movedata\GetOrigin()
		ang     = movedata\GetAngles()
		fwd     = ang\Forward()
		bcwd    = -ang\Forward()
		right   = ang\Right()
		left    = -ang\Right()
		up      = ang\Up()
		down    = -ang\Up()
		W       = movedata\KeyDown(IN_FORWARD)
		S       = movedata\KeyDown(IN_BACK)
		D       = movedata\KeyDown(IN_MOVERIGHT)
		A       = movedata\KeyDown(IN_MOVELEFT)
		CTRL    = movedata\KeyDown(IN_DUCK)
		MULT    = FrameTime() * 66

		velocity = movedata\GetVelocity()
		cSpeed = velocity\Length()
		cSpeed = 1 if cSpeed < 1
		dragSqrt = math.min(math.sqrt(cSpeed) / cSpeed * 2, 0.99)
		cSpeedMult = @speedMultDiv / cSpeed
		cSpeedMult = @speedMultDiv if cSpeedMult ~= cSpeedMult
		cSpeedLiftMult = @speedMultLift / cSpeed
		cSpeedLiftMult = @speedMultLift if cSpeedLiftMult ~= cSpeedLiftMult

		dragCalc = math.Clamp(@dragMult / dragSqrt, 0, 0.99)
		pitch = 0
		yaw = 0
		roll = 0

		hit = false
		hitLift = false

		if W
			velocity += fwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			pitch += 20

		if S
			velocity += bcwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			pitch -= 20

		if A
			velocity += left * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			roll -= 20

		if D
			velocity += right * MULT * @speedMult * cSpeedMult * @speedMultDirections
			hit = true
			roll += 20

		if CTRL
			velocity += Vector(0, 0, -MULT * @speedMult * cSpeedLiftMult)
			hitLift = true

		if @isLiftingUp
			velocity += Vector(0, 0, MULT * @speedMult * cSpeedLiftMult)
			hitLift = true

		if CLIENT
			lerpMult = FrameTime() * @angleLerp
			{:p, :y, :r} = ang
			p -= @pitch
			y -= @yaw
			@pitch = Lerp(lerpMult, @pitch, pitch)
			@yaw = Lerp(lerpMult, @yaw, yaw)
			@roll = Lerp(lerpMult, @roll, roll)
			p += @pitch
			y += @yaw
			r = @roll + math.sin(RealTime()) * 2
			newAng = Angle(p, y, r)
			@ent\SetEyeAngles(newAng)

		if not hit
			velocity.x *= dragCalc
			velocity.y *= dragCalc

		if not hitLift
			velocity.z *= dragCalc
			velocity.z += math.sin(RealTime() * 2) * .01

		pos += velocity

		movedata\SetVelocity(velocity)
		movedata\SetOrigin(pos)
		return true

	SetupMove: (movedata, cmd) =>
		@isLiftingUp = movedata\KeyDown(IN_JUMP)
		if @isLiftingUp
			movedata\SetButtons(movedata\GetButtons() - IN_JUMP)
		cmd\SetButtons(cmd\GetButtons() - IN_JUMP) if cmd\KeyDown(IN_JUMP)

	FinishMove: (movedata) =>
		nativeEntity = @ent\GetEntity()
		mvPos = movedata\GetOrigin()
		pos = nativeEntity\GetPos()
		rpos = pos
		tryMove = util.TraceHull({
			filter: (ent) ->
				return false if nativeEntity == ent
				return true if not IsValid(ent)
				collision = ent\GetCollisionGroup()
				return false if collision == COLLISION_GROUP_WORLD
				return false if collision == COLLISION_GROUP_DEBRIS
				return false if collision == COLLISION_GROUP_DEBRIS_TRIGGER
				return false if collision == COLLISION_GROUP_WEAPON
				return false if collision == COLLISION_GROUP_PASSABLE_DOOR
				return true
			mins: @obbMins
			maxs: @obbMaxs
			start: rpos
			endpos: mvPos
		})

		velocity = movedata\GetVelocity()
		newVelocity = velocity
		length = velocity\Length()

		if not tryMove.Hit
			nativeEntity\SetPos(mvPos)
		else
			if IsValid(tryMove.Entity)
				newVelocity = Vector(0, 0, 0)
				movedata\SetVelocity(newVelocity)
				newPos = tryMove.HitPos + tryMove.HitNormal
				nativeEntity\SetPos(newPos)
				movedata\SetOrigin(newPos)
			else
				newVelocity = velocity - tryMove.HitNormal * velocity\Dot(tryMove.HitNormal * 1.1)
				movedata\SetVelocity(newVelocity)
				newPos = tryMove.HitPos + tryMove.HitNormal
				nativeEntity\SetPos(newPos)
				movedata\SetOrigin(newPos)
			if length > 7 and SERVER and FLIGHT_DAMAGE\GetBool()
				dmgInfo = DamageInfo()
				dmgInfo\SetAttacker(nativeEntity)
				dmgInfo\SetInflictor(nativeEntity)
				dmgInfo\SetDamageType(DMG_CRUSH)
				calcDamage = math.Clamp((length / 4) ^ 2, 1, 100)
				if calcDamage >= 100
					nativeEntity\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
				elseif calcDamage > 70
					nativeEntity\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
				elseif calcDamage > 50
					nativeEntity\EmitSound("physics/body/body_medium_break#{math.random(2, 4)}.wav", 75)
				elseif calcDamage > 25
					nativeEntity\EmitSound("physics/body/body_medium_impact_hard#{math.random(1, 6)}.wav", 75)
				elseif calcDamage > 10
					nativeEntity\EmitSound("physics/body/body_medium_impact_soft#{math.random(1, 7)}.wav", 75)
				else
					nativeEntity\EmitSound("physics/flesh/flesh_impact_bullet#{math.random(1, 5)}.wav", 75)
				dmgInfo\SetDamage(calcDamage)
				nativeEntity\TakeDamageInfo(dmgInfo)
		@lastVelocity = newVelocity

PPM2.PonyflyController = PonyflyController

import IsPonyCached, GetPonyData, GetTable, SetIK, IsNewPonyCached from FindMetaTable('Entity')
import AnimRestartGesture, AnimResetGestureSlot from FindMetaTable('Player')

hook.Add 'SetupMove', 'PPM2.Ponyfly', (movedata, cmd) =>
	return if not IsPonyCached(@)
	data = GetPonyData(@)
	return if not data or not data\GetFly()
	flight = data\GetFlightController()
	return if not flight
	return flight\SetupMove(movedata, cmd)

hook.Add 'Move', 'PPM2.Ponyfly', (movedata) =>
	return if not IsPonyCached(@)
	data = GetPonyData(@)
	return if not data or not data\GetFly()
	flight = data\GetFlightController()
	return if not flight
	return flight\Think(movedata)

hook.Add 'FinishMove', 'PPM2.Ponyfly', (movedata) =>
	return if not IsPonyCached(@)
	data = GetPonyData(@)
	return if not data or not data\GetFly()
	flight = data\GetFlightController()
	return if not flight
	return flight\FinishMove(movedata)

hook.Add 'CalcMainActivity', 'PPM2.Ponyfly', (movedata) =>
	return if not IsNewPonyCached(@)
	if data = GetPonyData(@)
		if data\GetFly()
			if not @isPlayingPPM2Anim
				@isPlayingPPM2Anim = true
				AnimRestartGesture(@, GESTURE_SLOT_CUSTOM, ACT_GMOD_NOCLIP_LAYER, false)
				SetIK(@, false) if CLIENT
			return ACT_GMOD_NOCLIP_LAYER, 370
		else
			if @isPlayingPPM2Anim
				@isPlayingPPM2Anim = false
				AnimResetGestureSlot(@, GESTURE_SLOT_CUSTOM)
				SetIK(@, true) if CLIENT

if SERVER
	concommand.Add 'ppm2_fly', =>
		return if not ALLOW_FLIGHT\GetBool()
		return if not IsValid(@)
		return if not @IsPonyCached()
		data = @GetPonyData()
		return if not data
		if data\GetFly()
			return data\SetFly(false) if FORCE_ALLOW_FLIGHT\GetBool()
			can = hook.Run('PlayerNoClip', @, false) or hook.Run('PPM2Fly', @, false)
			data\SetFly(false) if can
		else
			return if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
			return data\SetFly(true) if FORCE_ALLOW_FLIGHT\GetBool()
			can = hook.Run('PlayerNoClip', @, true) or hook.Run('PPM2Fly', @, true)
			data\SetFly(true) if can
else
	lastDouble = 0
	lastMessage = 0
	lastMessage2 = 0
	hook.Add 'PlayerBindPress', 'PPM2.Ponyfly', (bind = '', pressed = false) =>
		return if not ALLOW_FLIGHT\GetBool()
		return if not pressed
		return if bind ~= '+jump' and bind ~= 'jump'
		if lastDouble > RealTime()
			return if not @IsPonyCached()
			data = @GetPonyData()
			return if not data
			if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
				if lastMessage < RealTime()
					lastMessage = RealTime() + 1
					PPM2.ChatPrint('You need to be a Pegasus or an Alicorn to fly!')
				return
			if not FORCE_ALLOW_FLIGHT\GetBool() and not SUPPRESS_CLIENTSIDE_CHECK\GetBool()
				can = hook.Run('PlayerNoClip', @, not data\GetFly()) or hook.Run('PPM2Fly', @, not data\GetFly())
				if not can
					if lastMessage2 < RealTime()
						lastMessage2 = RealTime() + 1
						PPM2.ChatPrint("You can not #{data\GetFly() and 'land' or 'fly'} right now.")
					return
			RunConsoleCommand('ppm2_fly')
			lastDouble = 0
			return
		lastDouble = RealTime() + 0.2