debug

debug

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

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

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

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

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

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

  mkNonce: =>
    now=os.time!
    hash=now..encode_base64 hmac_sha1 now..ranval!,ranval!..now
    hash=hash\gsub "%/","_"
    hash\sub 1,20

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

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

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

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

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

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

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

  tryLogin: (params) =>
    if params.yubikey
      nonce=@mkNonce!
      res=http.simple @fillUrl params.yubikey, nonce
      if res\find "status=OK", 1, true and res\find "nonce="..nonce, 1, true
        entry=YubiCloudModel\find idstr: params.yubikey\lower!\sub(-33)
        if entry
          return entry\get_user!, res
      return nil, "invalid OTP or unregistered, details:<br>"..res
    if @required
      return nil, res
    else
      return false, res

  mkNonce: =>
    now=os.time!
    hash=now..encode_base64 hmac_sha1 now..ranval!,ranval!..now
    hash=hash\gsub "%/","_"
    hash\sub 1,20

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

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


Fixed invalid file extension

Fixed invalid file extension

--
-- 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)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()
	file.Delete('ppm2/' .. ffind)

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

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

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

	@PONY_DATA = PPM2.PonyDataRegistry

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

	for key, data in pairs @PONY_DATA
		continue unless data.enum
		data.enum = [arg\upper() for arg in *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)
		stream = file.Open(path, 'wb', 'DATA')
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

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

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

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

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

			\SetSequence(22)
			\FrameAdvance(0)

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

			buildingModel\DrawModel()

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

				if 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)
			stream = file.Open('ppm2/' .. fTarget .. '.dat', 'wb', 'DATA')
			buf\ToFileStream(stream)
			stream\Flush()
			stream\Close()
	file.Delete('ppm2/' .. ffind)

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

	@FindFiles = =>
		output = [str\sub(1, #str - 4) for str in *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)
		stream = file.Open(path, 'wb', 'DATA')
		buf\ToFileStream(stream)
		stream\Flush()
		stream\Close()
		return buf

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

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

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

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

			\SetSequence(22)
			\FrameAdvance(0)

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

			buildingModel\DrawModel()

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

				if 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


Don't draw hands (?) Fixes #40

Don't draw hands (?)
Fixes #40

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

timer.Create 'PPM2.ModelChecks', 1, 0, ->
    for ply in *player.GetAll()
        ply.__cachedIsPony = ply\IsPony()

        if ply.__cachedIsPony
            for wep in *ply\GetWeapons()
                continue if not wep
                wep\SetNoDraw(true)
                wep.__ppm2_weapon_hit = true
        else
            for wep in *ply\GetWeapons()
                continue if not wep
                continue if not wep.__ppm2_weapon_hit
                wep\SetNoDraw(false)
                ply.__ppm2_weapon_hit = false

PPM2.PostDrawOpaqueRenderables = (a, b) ->
    return if a or b
    lply = LocalPlayer()

    for ply in *player.GetAll()
        alive = ply\Alive()
        ply.__ppm2_last_dead = RealTime() + 2 if not alive
        if ply.__cachedIsPony
            if ply\GetPonyData() and not alive
                data = ply\GetPonyData()
                rag = ply\GetRagdollEntity()
                if IsValid(rag)
                    rag\SetNoDraw(true)
                    renderController = data\GetRenderController()
                    data\DoRagdollMerge()
                    if renderController
                        renderController\PreDraw(rag)
                        rag\DrawModel()
                        renderController\PostDraw(rag)

PPM2.PrePlayerDraw = =>
    return unless @GetPonyData()
    @__cachedIsPony = @IsPony()
    return if not @__cachedIsPony
    return if @__ppm2_last_draw == FrameNumber()
    @__ppm2_last_draw = FrameNumber()
    return if @IsDormant()
    @__ppm2_last_dead = @__ppm2_last_dead or 0
    return if @__ppm2_last_dead > RealTime()
    data = @GetPonyData()
    renderController = data\GetRenderController()
    status = renderController\PreDraw() if renderController
    data\GetWeightController()\UpdateWeight() if data\GetOverrideBones() and data\GetWeightController()
PPM2.PostPlayerDraw = =>
    return unless @GetPonyData()
    return unless @__cachedIsPony
    data = @GetPonyData()
    renderController = data\GetRenderController()
    renderController\PostDraw() if renderController

hook.Add 'PrePlayerDraw', 'PPM2.PlayerDraw', PPM2.PrePlayerDraw, 2
hook.Add 'PostPlayerDraw', 'PPM2.PostPlayerDraw', PPM2.PostPlayerDraw, 2
hook.Add 'PostDrawOpaqueRenderables', 'PPM2.PostDrawOpaqueRenderables', PPM2.PostDrawOpaqueRenderables, 2
hook.Add 'RenderScreenspaceEffects', 'PPM2.RenderScreenspaceEffects', ->
    self = LocalPlayer()

    if @__cachedIsPony and @GetPonyData() and @Alive()
        @GetPonyData()\GetRenderController()\DrawLegs()

SHOULD_DRAW_VIEWMODEL = CreateConVar('cl_ppm2_draw_hands', '1', {FCVAR_ARCHIVE}, 'Should draw hooves as viewmodel')

hook.Add 'PreDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
    return true unless SHOULD_DRAW_VIEWMODEL\GetBool()
    return unless IsValid(arms)
    return unless ply.__cachedIsPony
    return unless ply\Alive()
    wep = ply\GetActiveWeapon()
    if IsValid(wep) and wep.UseHands == false
        return true -- Dafuck?
    data = ply\GetPonyData()
    return unless data
    status = data\GetRenderController()\PreDrawArms(arms)
    return status if status ~= nil
    arms.__ppm2_draw = true

hook.Add 'PostDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
    return unless IsValid(arms)
    return unless arms.__ppm2_draw
    data = ply\GetPonyData()
    return unless data
    data\GetRenderController()\PostDrawArms(arms)
    arms.__ppm2_draw = false

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

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

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

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

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

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

hook.Add 'KeyPress', 'PPM2.RequireData', ->
    hook.Remove 'KeyPress', 'PPM2.RequireData'
    RunConsoleCommand('ppm2_reload')
    timer.Simple 3, -> RunConsoleCommand('ppm2_require')

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

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

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

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

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

timer.Create 'PPM2.ModelChecks', 1, 0, ->
    for ply in *player.GetAll()
        ply.__cachedIsPony = ply\IsPony()

        if ply.__cachedIsPony
            for wep in *ply\GetWeapons()
                continue if not wep
                wep\SetNoDraw(true)
                wep.__ppm2_weapon_hit = true
        else
            for wep in *ply\GetWeapons()
                continue if not wep
                continue if not wep.__ppm2_weapon_hit
                wep\SetNoDraw(false)
                ply.__ppm2_weapon_hit = false

PPM2.PostDrawOpaqueRenderables = (a, b) ->
    return if a or b
    lply = LocalPlayer()

    for ply in *player.GetAll()
        alive = ply\Alive()
        ply.__ppm2_last_dead = RealTime() + 2 if not alive
        if ply.__cachedIsPony
            if ply\GetPonyData() and not alive
                data = ply\GetPonyData()
                rag = ply\GetRagdollEntity()
                if IsValid(rag)
                    rag\SetNoDraw(true)
                    renderController = data\GetRenderController()
                    data\DoRagdollMerge()
                    if renderController
                        renderController\PreDraw(rag)
                        rag\DrawModel()
                        renderController\PostDraw(rag)

PPM2.PrePlayerDraw = =>
    return unless @GetPonyData()
    @__cachedIsPony = @IsPony()
    return if not @__cachedIsPony
    return if @__ppm2_last_draw == FrameNumber()
    @__ppm2_last_draw = FrameNumber()
    return if @IsDormant()
    @__ppm2_last_dead = @__ppm2_last_dead or 0
    return if @__ppm2_last_dead > RealTime()
    data = @GetPonyData()
    renderController = data\GetRenderController()
    status = renderController\PreDraw() if renderController
    data\GetWeightController()\UpdateWeight() if data\GetOverrideBones() and data\GetWeightController()
PPM2.PostPlayerDraw = =>
    return unless @GetPonyData()
    return unless @__cachedIsPony
    data = @GetPonyData()
    renderController = data\GetRenderController()
    renderController\PostDraw() if renderController

hook.Add 'PrePlayerDraw', 'PPM2.PlayerDraw', PPM2.PrePlayerDraw, 2
hook.Add 'PostPlayerDraw', 'PPM2.PostPlayerDraw', PPM2.PostPlayerDraw, 2
hook.Add 'PostDrawOpaqueRenderables', 'PPM2.PostDrawOpaqueRenderables', PPM2.PostDrawOpaqueRenderables, 2
hook.Add 'RenderScreenspaceEffects', 'PPM2.RenderScreenspaceEffects', ->
    self = LocalPlayer()

    if @__cachedIsPony and @GetPonyData() and @Alive()
        @GetPonyData()\GetRenderController()\DrawLegs()

SHOULD_DRAW_VIEWMODEL = CreateConVar('cl_ppm2_draw_hands', '1', {FCVAR_ARCHIVE}, 'Should draw hooves as viewmodel')

hook.Add 'PreDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
    return true unless SHOULD_DRAW_VIEWMODEL\GetBool()
    return unless IsValid(arms)
    return unless ply.__cachedIsPony
    return unless ply\Alive()
    data = ply\GetPonyData()
    return unless data
    status = data\GetRenderController()\PreDrawArms(arms)
    return status if status ~= nil
    arms.__ppm2_draw = true

hook.Add 'PostDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
    return unless IsValid(arms)
    return unless arms.__ppm2_draw
    data = ply\GetPonyData()
    return unless data
    data\GetRenderController()\PostDrawArms(arms)
    arms.__ppm2_draw = false

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

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

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

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

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

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

hook.Add 'KeyPress', 'PPM2.RequireData', ->
    hook.Remove 'KeyPress', 'PPM2.RequireData'
    RunConsoleCommand('ppm2_reload')
    timer.Simple 3, -> RunConsoleCommand('ppm2_require')

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

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

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

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


Added more tests for `merge` function

Added more tests for `merge` function


Add a resize stub to Engine

Add a resize stub to Engine

require "LovePunk.Entity"
require "LovePunk.RenderMode"
require "LovePunk.Screen"
require "LovePunk.geometry.Rectangle"
require "LovePunk.utils.Draw"

export ^

---
-- The main class which manages the game loop
class Engine
	---
	-- Constructor. Defines startup information about your game.
	-- @param width      The width of your game.
	-- @param height     The height of your game.
	-- @param frameRate  The game framerate, in frames per second.
	-- @param fixed      If a fixed-framerate should be used.
	-- @param renderMode Overrides the default render mode for this target
	new: (width = 0, height = 0, frameRate = 0, fixed = false, renderMode) =>
		LP.bounds = Rectangle(0, 0, width, height)
		LP.assignedFrameRate = frameRate
		LP.fixed = fixed

		LP.engine = @
		LP.screen = Screen!
		LP.width = width
		LP.height = height

		LP.randomizeSeed! unless LP.randomSeed

		LP.entity = Entity!
		LP.time = love.timer.getTime!

		---
		-- Cap on the elapsed time (default at 30 FPS). Raise this to allow for lower framerates (eg. 1 / 10).
		@maxElapsed = 0.0333
		---
		-- The max amount of frames that can be skipped in fixed framerate mode.
		@maxFrameSkip = 0
		---
		-- If the game should stop updating/rendering.
		@paused = false
		---
		-- The amount of milliseconds between ticks in fixed framerate mode.
		@tickRate = 4

		@__frameList = {}
		@__frameLast = 0
		@__frameListSum = 0

		-- Do something with the Löve functions...
		love.draw = @\render
		love.update = @\update
		if love._version_major >= 0 and love._version_minor >= 9
			love.resize = @\resize

	focusGained: =>
	focusLost: =>
	init: =>
	render: =>
		LP.resize LP.windowWidth, LP.windowHeight if LP.screen.needsResize

		-- timing stuff
		t = love.timer.getDelta!

		@__frameLast = t if @__frameLast == 0

		-- render loop
		if LP.renderMode == RenderMode.Buffer
			LP.screen.swap!
			LP.screen.refresh!

		Draw.resetTarget!

		LP.scene.render! if LP.scene.visible

		LP.screen.redraw! if LP.renderMode == RenderMode.Buffer

		-- more timing stuff
		t = love.timer.getTime!
		@__frameList[#@__frameList] = t - @__frameLast
		@__frameListSum += @__frameList[#@__frameList]
		if #@__frameList > 10
			@__frameListSum -= @__frameList[#@__frameList]
			@__frameList = [frame for frame in *@__frameList[2,]]

		LP.frameRate = 1000 / (@__frameListSum / #@__frameList)
		@__frameLast = t

	update: =>
		LP.scene.updateLists!
		checkScene! if LP\sceneIsNull!
		LP.tweener\updateTweens! if LP.tweener.active and LP.tweener\hasTween!

		if LP.scene.active
			LP.scene\updateTweens! if LP.scene\hasTween!
			LP.scene\update!

		LP.scene\updateLists false

	resize: =>

	checkScene = =>
		return if not LP\sceneIsNull!

		if LP.scene != nil
			LP.scene\endScene!
			LP.scene\updateLists!
			LP.scene\clearTweens! if LP.scene.autoClear and LP.scene\hasTween!
			-- TODO
			-- removeChild(LP.scene.sprite) if LP.scene.sprite
			LP\swapScene!
			-- TODO
			-- addChild LP.scene.sprite
			LP.camera = LP.scene.camera
			LP.scene\begin!
			LP.scene\updateLists!

require "LovePunk.Entity"
require "LovePunk.RenderMode"
require "LovePunk.Screen"
require "LovePunk.geometry.Rectangle"
require "LovePunk.utils.Draw"

export ^

---
-- The main class which manages the game loop
class Engine
	---
	-- Constructor. Defines startup information about your game.
	-- @param width      The width of your game.
	-- @param height     The height of your game.
	-- @param frameRate  The game framerate, in frames per second.
	-- @param fixed      If a fixed-framerate should be used.
	-- @param renderMode Overrides the default render mode for this target
	new: (width = 0, height = 0, frameRate = 0, fixed = false, renderMode) =>
		LP.bounds = Rectangle(0, 0, width, height)
		LP.assignedFrameRate = frameRate
		LP.fixed = fixed

		LP.engine = @
		LP.screen = Screen!
		LP.width = width
		LP.height = height

		LP.randomizeSeed! unless LP.randomSeed

		LP.entity = Entity!
		LP.time = love.timer.getTime!

		---
		-- Cap on the elapsed time (default at 30 FPS). Raise this to allow for lower framerates (eg. 1 / 10).
		@maxElapsed = 0.0333
		---
		-- The max amount of frames that can be skipped in fixed framerate mode.
		@maxFrameSkip = 0
		---
		-- If the game should stop updating/rendering.
		@paused = false
		---
		-- The amount of milliseconds between ticks in fixed framerate mode.
		@tickRate = 4

		@__frameList = {}
		@__frameLast = 0
		@__frameListSum = 0

		-- Do something with the Löve functions...
		love.draw = @\render
		love.update = @\update

	focusGained: =>
	focusLost: =>
	init: =>
	render: =>
		LP.resize LP.windowWidth, LP.windowHeight if LP.screen.needsResize

		-- timing stuff
		t = love.timer.getDelta!

		@__frameLast = t if @__frameLast == 0

		-- render loop
		if LP.renderMode == RenderMode.Buffer
			LP.screen.swap!
			LP.screen.refresh!

		Draw.resetTarget!

		LP.scene.render! if LP.scene.visible

		LP.screen.redraw! if LP.renderMode == RenderMode.Buffer

		-- more timing stuff
		t = love.timer.getTime!
		@__frameList[#@__frameList] = t - @__frameLast
		@__frameListSum += @__frameList[#@__frameList]
		if #@__frameList > 10
			@__frameListSum -= @__frameList[#@__frameList]
			@__frameList = [frame for frame in *@__frameList[2,]]

		LP.frameRate = 1000 / (@__frameListSum / #@__frameList)
		@__frameLast = t

	update: =>
		LP.scene.updateLists!
		checkScene! if LP\sceneIsNull!
		LP.tweener\updateTweens! if LP.tweener.active and LP.tweener\hasTween!

		if LP.scene.active
			LP.scene\updateTweens! if LP.scene\hasTween!
			LP.scene\update!

		LP.scene\updateLists false

	checkScene = =>
		return if not LP\sceneIsNull!

		if LP.scene != nil
			LP.scene\endScene!
			LP.scene\updateLists!
			LP.scene\clearTweens! if LP.scene.autoClear and LP.scene\hasTween!
			-- TODO
			-- removeChild(LP.scene.sprite) if LP.scene.sprite
			LP\swapScene!
			-- TODO
			-- addChild LP.scene.sprite
			LP.camera = LP.scene.camera
			LP.scene\begin!
			LP.scene\updateLists!


Placeholders.

Placeholders.

Class = require "class"

Tile = Class "Tile",
	__init: (arg) =>
		-- FIXME: Errors on missing parameters?

		@terrain = arg.terrain
		@structure = arg.structure

Class "Map",
	:Tile
	---
	-- @return (Tile)
	get: (x, y, z) =>

	---
	-- @return nil
	set: (x, y, z, arguments) =>

	---
	-- @return (string)
	toMoon: =>

	---
	-- @param str (string)
	fromMoon: (str) =>

Class = require "class"

Class "Map", {}


ircsnap: fix escaping

ircsnap: fix escaping

Former-commit-id: 8afc4bde35553d9ec8d29e573f2d3f44d0caf0cf
Former-commit-id: b6e5918105324d720d0b550c278d174b82fa2bdf

{:urlEncode} = require'util'
html2unicode = require 'html'
lfs = require'lfs'

hex_to_char = (x) ->
  string.char(tonumber(x, 16))

unescape = (url) ->
  url\gsub("%%(%x%x)", hex_to_char)

-- All URLs in this module is under this prefix
urlbase = '/image/'

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

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) =>
  url = req.url
  send = (body, code, content_type) ->
    if not code then code = "200"
    if not content_type then content_type = 'text/html'
    res\append ':status', code
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(#body)
    req\write_headers(res, false, 30)
    req\write_body_from_string(body, 30)
    return -- empty return

  file = url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = lfs.attributes(fn).size
    --body = fd\read '*a'
    content_type = 'image/jpeg'
    if file\lower!\match '.png'
      content_type = 'image/png'
    if file\lower!\match '.svg'
      content_type = 'image/svg'
    if file\lower!\match '.mp4'
      content_type = 'video/mp4'
    if file\lower!\match '.mov'
      content_type = 'video/quicktime'
    if file\lower!\match '.mp3'
      content_type = 'audio/mp3'

    res\append ':status', '200'
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(size)
    req\write_headers(res, false, 30)
    fd = io.open(fn, 'rb')
    req\write_body_from_file(fd, 5*60)
    fd\close!
    return

  -- Serve video player page
  video = url\match '/video/(.*)$'
  if video then
    send video_html(video)
    return

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

  channel = html2unicode channel
  unescaped_channel = unescape channel

  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 ]]..unescaped_channel..[[</title>

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

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

    <form>

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

        Your nickname

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

        Attach text, if you want

    </form>

Uploading. Percent complete: 0 %

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

      What is this?

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

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

Made by xt

  ]]
  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 = ->
        -- Move tempfile to real location
        os.rename req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{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.
lfs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."

{:urlEncode} = require'util'
html2unicode = require 'html'
lfs = require'lfs'

-- All URLs in this module is under this prefix
urlbase = '/image/'

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

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) =>
  url = req.url
  send = (body, code, content_type) ->
    if not code then code = "200"
    if not content_type then content_type = 'text/html'
    res\append ':status', code
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(#body)
    req\write_headers(res, false, 30)
    req\write_body_from_string(body, 30)

  file = url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = lfs.attributes(fn).size
    --body = fd\read '*a'
    content_type = 'image/jpeg'
    if file\lower!\match '.png'
      content_type = 'image/png'
    if file\lower!\match '.svg'
      content_type = 'image/svg'
    if file\lower!\match '.mp4'
      content_type = 'video/mp4'
    if file\lower!\match '.mov'
      content_type = 'video/quicktime'
    if file\lower!\match '.mp3'
      content_type = 'audio/mp3'

    res\append ':status', '200'
    res\append 'Content-Type', content_type
    res\append 'Content-Length', tostring(size)
    req\write_headers(res, false, 30)
    fd = io.open(fn, 'rb')
    req\write_body_from_file(fd, 5*60)
    fd\close!
    return

  -- Serve video player page
  video = url\match '/video/(.*)$'
  if video then
    return send video_html(video)

  channel = url\match('channel=(.+)%s*')
  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 ]]..unescaped_channel..[[</title>

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

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

    <form>

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

        Your nickname

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

        Attach text, if you want

    </form>

Uploading. Percent complete: 0 %

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

      What is this?

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

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

Made by xt

  ]]
  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 = ->
        -- Move tempfile to real location
        os.rename req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{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.
lfs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."


Test Mask collistions of hitbox (origin)

require "busted"
require "tests.mock_love"

require "LunoPunk.Mask"
require "LunoPunk.masks.Masklist"
require "LunoPunk.Entity"

describe "Mask", ->
	it "Construct", ->
		m = Mask!
		assert.is.Nil m.parent
		assert.are.same m.list, Masklist!

	it "assignTo", ->
		e = Entity!
		m = Mask!
		m\assignTo e
		assert.are.same e, m.parent

		m\assignTo nil
		assert.is.Nil m.parent

	it "collide", ->
		e = Entity!
		m = Mask!

		-- can't call collide before a parent is set
		assert.has.errors -> m\collide nil

		m\assignTo e
		-- Will collide with itself
		assert.is.True m\collide m

		e = with Entity!
			.width = 10
			.height = 10

		m\assignTo e

		m2 = Mask!
		m2\assignTo e

		-- Two masks collide with the same entity is true
		assert.is.True m\collide m2
		assert.is.True m2\collide m

		e2 = with Entity 10, 10
			.width = 10
			.height = 10

		m2\assignTo e2
		-- colliding at point (10, 10)
		assert.is.True m\collide m2
		assert.is.True m2\collide m

		e2 = with Entity 11, 11
			.width = 10
			.height = 10

		m2\assignTo e2
		-- Not colliding
		assert.is.False m\collide m2
		assert.is.False m2\collide m

		-- Test with hitbox colliding
		e = with Entity 0, 0
			.width = 10
			.hight = 10
			.originX = 10
			.originY = 10

		e2 = with Entity 0, 0
			.width = 10
			.height = 10
			.originX = 15
			.originY = 15

		m\assignTo e
		m2\assignTo e2

		-- Colliding
		assert.is.True m\collide m
		assert.is.True m\collide m2
		assert.is.True m2\collide m

		-- Corner colliding
		e2.originX = 20
		e2.originY = 20
		assert.is.True m\collide m2
		assert.is.True m2\collide m

		e2.originX = 21
		-- Not colliding
		assert.is.False m\collide m2
		assert.is.False m2\collide m


sylfest: add missing ending

sylfest: add missing ending

Former-commit-id: e65f60cdb29fc7d7ca8e30255ea3aef62afdbc8d [formerly d11f01bd0cd200455f8273e6ab42b302705768c0]
Former-commit-id: ba4ff030b6958b6c04498cde92d0042d17e0ebd6

lpeg = require 'lpeg'
import match, locale, P, R, V, S, Ct, C, Cg, Cf, Cc, Cp from lpeg
import concat from table

-- Helper for searching anywhere in string for patterns
anywhere = (p) ->
  P{ p + 1 * V(1) }
-- Helper for maybe pattern
maybe = (p) -> p^-1
-- Match norwegian words
word = R('az') + R('AZ') + S('æøåÆØÅ')
-- With an '
apo = P "'"
-- With bad endings
bend = P("ene") + P("en") + P("et") + P("ing") + P("ar") + P("ane") + P("er")
-- Separated by or line end
wend = S(',. ') + P(-1)
-- Linestart exceptions
bstart = maybe(P(' ')) * S[["'-]]

-- Capture word with bad endings followed by good word end
sylfestpatt = C(word^1 * apo * bend) * P(wend)
-- Search anywhere in string, capture all sylfestwords, do not match if line starts with excepted linestarts
fullpatt = Ct(anywhere(sylfestpatt)^1 - bstart)

-- run our handler on any message that contains '
PRIVMSG:
  ".*'.*": (source, destination, line) =>
    matches = fullpatt\match(line)
    if matches
      reply "Sylfest likar ikkje: %s", concat(matches, ', ')

lpeg = require 'lpeg'
import match, locale, P, R, V, S, Ct, C, Cg, Cf, Cc, Cp from lpeg
import concat from table

-- Helper for searching anywhere in string for patterns
anywhere = (p) ->
  P{ p + 1 * V(1) }
-- Helper for maybe pattern
maybe = (p) -> p^-1
-- Match norwegian words
word = R('az') + R('AZ') + S('æøåÆØÅ')
-- With an '
apo = P "'"
-- With bad endings
bend = P("en") + P("ene") + P("et") + P("ing") + P("ar") + P("ane")
-- Separated by or line end
wend = S(',. ') + P(-1)
-- Linestart exceptions
bstart = maybe(P(' ')) * S[["'-]]

-- Capture word with bad endings followed by good word end
sylfestpatt = C(word^1 * apo * bend) * P(wend)
-- Search anywhere in string, capture all sylfestwords, do not match if line starts with excepted linestarts
fullpatt = Ct(anywhere(sylfestpatt)^1 - bstart)

-- run our handler on any message that contains '
PRIVMSG:
  ".*'.*": (source, destination, line) =>
    matches = fullpatt\match(line)
    if matches
      reply "Sylfest likar ikkje: %s", concat(matches, ', ')


ircsnap: some cleanup

ircsnap: some cleanup

Former-commit-id: 2bcdab4bceff0a1c6203f5c199cac1cdf161d0d6 [formerly 93a0662e6fbeeb5f6135b0ebff96060cef77dc8d]
Former-commit-id: bb6e2c7f8c11fe5f233f1c6aee9aa5c96021a9fc

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

-- All URLs in this module is under this prefix
urlbase = '/image/'

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

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

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

  -- Serve video player page
  video = req.url\match '/video/(.*)$'
  if video then
    return send video_html(video)

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

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

  html = [[
  <!DOCTYPE html>

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

  <title>IRCSNAP</title>

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

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

    <form>

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

        Your name

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

        Attach text

    </form>

Uploading. Percent complete: 0 %

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

        What is this?

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

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

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        --fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        --written = fd\write file
        --fd\sync!
        --fd\close!
        -- Move tempfile to real location
        fs.move req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{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 "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."


remove trailing slash

remove trailing slash

db = require "lapis.db"
redis = require "resty.redis"
config = require("lapis.config").get!
os = require "os"
import assert_error from require "lapis.application"
import from_json, to_json from require "lapis.util"

import execute from require "os"

import Model from require "lapis.db.model"
import underscore, slugify from require "lapis.util"

local *

format_date = (time) ->
    os.date "!%Y-%m-%d %H:%M:%S", time

generate_token = do
  import random from math
  random_char = ->
    switch random 1,3
      when 1
        random 65, 90
      when 2
        random 97, 122
      when 3
        random 48, 57

  (length) ->
    string.char unpack [ random_char! for i=1,length ]

secure_filename = (str) ->
    (str\gsub("%s+", "-")\gsub("%.+", ".")\gsub("[^%w%-_%.]+", ""))

imagedatesql = do
  [[
        date_part('epoch',
          COALESCE(
            to_timestamp(metadata->'DateTimeOriginal', 'YYYY:MM:DD HH24:MI:SS'),
            to_timestamp(metadata->'CreateDate', 'YYYY:MM:DD HH24:MI:SS'),
            created_at
          ))*1000 AS date
  ]]

cache_session = (session) ->
  session_cache = ngx.shared.session_cache
  if session.email and session.id
    session_cache\set(session.email, to_json(session))

class Redis
  new: =>
    math.randomseed os.time!
    @prefix = config.redis.prefix
    @queuekey = @prefix .. ':upload:queue'
    @red = redis\new!
    @red\connect config.redis.host, config.redis.port

  queue: (token) =>
    @red\lpush @queuekey, token

  queue_length: =>
    @red\llen @queuekey

class Sessions extends Model
  @timestamp: true
  @create: (email) =>

class Users extends Model
  @timestamp: true

  @create: (email) =>
    if @check_unique_constraint "email", email
      return nil, "Email already taken"

    token = generate_token 6
    while @check_unique_constraint "token", token
        token = generate_token 6

    name = ""

    Model.create @, {
      :email, :token, :name
    }

  @login: =>
    nil
  @logout: =>
    nil

  @read_session: (r) =>
    if r.session.user
      -- First check session cache for user
      session_cache = ngx.shared.session_cache
      user = session_cache\get r.session.user.email
      if user
        return from_json user
      -- No cache hit, try database
      user = @find email: r.session.user.email
      -- No database hit, create a new user
      unless user
        user = @create r.session.user.email
      -- Write session to cache
      cache_session(user)
      user

  @write_session: (r, verification_data) =>
    session = {
      email: verification_data.email
    }
    -- Write the cookie
    r.session.user = session

class Albums extends Model
  @timestamp: true

  @create: (user_id, title) =>
    token = generate_token 6
    while @check_unique_constraint "token", token
        token = generate_token 6

    Model.create @, {
      :user_id, :token, :title
    }

  delete: =>

    -- Delete all images in album
    unless @id
      return nil, "Error: no id"
    images = Images\select "where album_id = ?", @id
    for image in *images
      image\delete!

    -- Delete folder
    execute 'rmdir ' .. table.concat { config.path.disk, @user_id, @id }, '/'

    super!

class Images extends Model
  @timestamp: true

  file_path: =>
    path = {
      config.path.image,
      @user_id,
      @album_id,
      @id
    }
    table.concat path, '/'

  real_file_path: =>
    path = {
      config.path.disk,
      @user_id,
      @album_id,
      @id
    }
    table.concat path, '/'

  get_url: =>
    '/'..@real_file_name!

  get_real_thumb_url: =>
    if @thumb_name
      '/'..@file_path!..'/'..@thumb_name
    else
      '/'..@real_file_name!

  get_real_huge_url: =>
    if @huge_name
      '/'..@file_path!..'/'..@huge_name
    else
      '/'..@real_file_name!

  get_huge_url: =>
    --XXX url_for ? config?
    unless @huge_name == ''
      '/img/'..@token..'/'..@huge_name
    else
      '/img/'..@token..'/'..@file_name

  get_thumb_url: =>
    --XXX url_for ? config?
    unless @thumb_name == ''
      '/img/'..@token..'/'..@thumb_name
    else
      '/img/'..@token..'/'..@file_name

  real_file_name: =>
    @real_file_path! .. '/' .. @file_name

  get_file_size: =>
      fp = io.open @real_file_name!
      if fp == nil then
        return 0
      filesize = fp\seek "end"
      fp\close()
      return filesize

  @create: (user_id, album_id, file_name) =>
    token = generate_token 6
    while @check_unique_constraint "token", token
        token = generate_token 6

    image = assert_error Model.create @, {
      :user_id, :token, :album_id, file_name:secure_filename(file_name), title:file_name, thumb_name:'', huge_name:''
    }
    if image
      execute "mkdir -p "..image\real_file_path!
    image

  delete: =>
    -- Remove file from filesystem
    execute "rm " .. @real_file_path! .. '/' .. @file_name
    execute "rm " .. @real_file_path! .. '/' .. @huge_name
    execute "rm " .. @real_file_path! .. '/' .. @thumb_name
    execute "rmdir " .. @real_file_path!

    super!

  get_coverimage: (album_id) =>
    Images\select "where album_id = ? order by views desc limit 1", album_id

  get_coverimages: (albums) =>
    flat_ids = table.concat [db.escape_literal a.id for a in *albums], ", "
    Images\select "where album_id IN ("..flat_ids..") order by album_id DESC, views DESC", flat_ids, fields:"distinct on (album_id) *"

class Accesstokens extends Model
  @timestamp: true

  @create: (user_id, album_id, name, expires_in) =>
    slug = slugify name
    expires_at = format_date ngx.now! + expires_in
    accesstoken = Model.create @, {
      :user_id, :album_id, :slug, :expires_at
    }
    accesstoken

  @for_slug: (slug) =>
    db.select "* from accesstokens where slug = ? and now() < expires_at", slug

  @validate_album: (slug, album_id) =>
    res = db.select "* from accesstokens where slug = ? and album_id = ? and now() < expires_at", slug, album_id
    if #res > 0
      db.update "accesstokens", {
        views: db.raw"views + 1"
        }, {
          album_id:album_id,
          slug:slug
        }
      return true
    else
      return false

{ :Redis, :Users, :Albums, :Images, :Sessions, :Accesstokens, :generate_token, :imagedatesql }


Slightly better.

Slightly better.

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

import concat, insert from table

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

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

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

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

print_listing = (filedata, regex) ->
	for k, {file, mode, data} in pairs filedata
		print file, mode
		print_listing(data, regex) if data

process_files = (filedata, regex, parent) ->
	ret = {}
	parent = parent or {'',''}
	for k, {file, mode, data} in pairs filedata
		return if not rex.match(file, regex)
		if mode == 'directory'
			f = rex.match(file, regex)
			f = f\gsub('[^aA-zZ|\\d| ]', '')
			f = f\match("^%s*(.-)%s*$")
			f = target .. '/' .. f
			lfs.mkdir(f)
			parent = {inputdir .. '/' .. file, f}
		else
			ext = file_ext(file)
			newer_f = rex.match(file, regex)
			new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
			new_f = new_f\gsub('[-,_, ]', '')
			new_f = rex.gsub(new_f, [[(\D*)(\d*)]], [[%1_ep%2]], 1)
			old_f, new_f = parent[1] .. '/' .. file, parent[2] .. '/' .. new_f .. ext
			lfs.link(old_f, new_f)
			print "Link: " .. old_f .. " to: " .. new_f
		item = {rex.match(file, regex), mode, data}
		item[3] = process_files(data, regex, parent) if data
		insert(ret, item)
	ret

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

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

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

regex = [==[(?:(?:\[[^\]]*\])|(?:\([^\)]*\)]))?([^\[|\]|\(|\)]*[^\[|\]|\(|\)])?]==]

processed = process_files(files, regex)
print_listing(processed)

-- ([\[]*[^\]]*[\]]*)([^\[|\]|\(|\)]*)([\[]*[^\]]*[\]]*c)
-- (?![^\[|\]*])?([^\[|\]|\(|\)]+\b)(?:[^\[|\]]*)?
-- ([\[][^\]]+[\]])+([^\[|\]|\(|\)]*)?([\[][^\]]+[\]])+
-- ([\[][^\]]*[\]])+([^\[|\]|\(|\)]*)?(([\[][^\]]+[\]])|([\(][^\)]*[\)]))+
-- ((\[[^\]]*\])|(\([^\)]*\)]))+([^\[|\]|\(|\)]*[^\[|\]|\(|\)])?
-- (?:(?:\[[^\]]*\])|(?:\([^\)]*\)]))+([^\[|\]|\(|\)]*[^\[|\]|\(|\)])?

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

import concat, insert from table

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

file_ext = (str) ->
	c1,c2,c3 = string.match(str, "(.-)([^\\]-([^%.]+))$")
	return "." .. c3

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

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

print_listing = (filedata, regex) ->
	for k, {file, mode, data} in pairs filedata
		print file, mode
		print_listing(data, regex) if data

process_files = (filedata, regex, parent) ->
	ret = {}
	parent = parent or {'',''}
	for k, {file, mode, data} in pairs filedata
		return if not rex.match(file, regex)
		if mode == 'directory'
			f = rex.match(file, regex)
			f = f\gsub('[^aA-zZ|\\d| ]', '')
			f = f\match("^%s*(.-)%s*$")
			f = target .. '/' .. f
			lfs.mkdir(f)
			parent = {inputdir .. '/' .. file, f}
		else
			ext = file_ext(file)
			newer_f = rex.match(file, regex)
			new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
			new_f = new_f\gsub('[-,_, ]', '')
			new_f = rex.gsub(new_f, [[(\D*)(\d*)]], [[%1_ep%2]], 1)
			old_f, new_f = parent[1] .. '/' .. file, parent[2] .. '/' .. new_f .. ext
			lfs.link(old_f, new_f)
			print "Link: " .. old_f .. " to: " .. new_f
		item = {rex.match(file, regex), mode, data}
		item[3] = process_files(data, regex, parent) if data
		insert(ret, item)
	ret

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

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

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

regex = [==[(?:(?:\[[^\]]*\])|(?:\([^\)]*\)]))?([^\[|\]|\(|\)]*[^\[|\]|\(|\)])?]==]

processed = process_files(files, regex)
print_listing(processed)

-- ([\[]*[^\]]*[\]]*)([^\[|\]|\(|\)]*)([\[]*[^\]]*[\]]*c)
-- (?![^\[|\]*])?([^\[|\]|\(|\)]+\b)(?:[^\[|\]]*)?
-- ([\[][^\]]+[\]])+([^\[|\]|\(|\)]*)?([\[][^\]]+[\]])+
-- ([\[][^\]]*[\]])+([^\[|\]|\(|\)]*)?(([\[][^\]]+[\]])|([\(][^\)]*[\)]))+
-- ((\[[^\]]*\])|(\([^\)]*\)]))+([^\[|\]|\(|\)]*[^\[|\]|\(|\)])?
-- (?:(?:\[[^\]]*\])|(?:\([^\)]*\)]))+([^\[|\]|\(|\)]*[^\[|\]|\(|\)])?