Sfx\stop now works

Sfx\stop now works

* Fixed Sfx\playing. Forecd some other changes now that it works, such
  as commenting out @__channel.addEventLister
* addPlaying and removePlaying now do something that is closer to
  actually working (though, it still needs evaluated if it's going to
  work as it should.)

require "LunoPunk.LP"

export ^

-- Sound effect object used to play embedded sounds.
class Sfx
	-- Creates a sound effect from an embedded source. Store a reference to
	-- this object so that you can play the sound using play() or loop().
	-- @param	source		The embedded sound class to use.
	-- @param	complete	Optional callback function for when the sound finishes playing.
	new: (source, complete = nil) =>
		-- Optional callback function for when the sound finishes playing.
		@complete = complete
		-- @__transform = SoundTransform!
		@__volume = 1
		@__pan = 0
		@__position = 0
		@__type = ""

		-- Sound infromation.
		@__type
		@__volume = 1
		@__pan = 0
		@__filteredVol
		@__filteredPan
		@__sound
		@__channel
		-- @__transform
		@__position = 0
		@__looping

		-- Stored Sound objects.
		@@__sounds = {}
		@@__typePlaying = {}
		-- @@__typeTransforms = {}

		assert source != nil, "Invalid source Sound."

		if type(source) == "string"
			@__sound = love.audio.newSource source, "static"
			@@__sounds[source] = @__sound
		else
			className = source.__class.__name
			@__sound = @__sounds[className]
			if @__sound == nil
				@__sound = source
				@@__sounds[className] = source

	-- Plays the sound once.
	-- @param	vol	   Volume factor, a value from 0 to 1.
	-- @param	pan	   Panning factor, a value from -1 to 1.
	-- @param   loop   If the audio should loop infinitely
	play: (volume = 1, pan = 0, loop = false) =>
		return if @__sound == nil
		@stop! if @playing!
		@__pan = LP.clamp pan, -1, 1
		@__volume = (if volume < 0 then 0 else volume)
		@__filteredPan = LP.clamp @__pan + @getPan(@__type), -1, 1
		@__filteredVol = math.max 0, @__volume * @getVolume @_type
		--@__transform.pan = @__filteredPan
		--@__transform.volume = @__filteredVol
		@__sound\setLooping loop
		@__sound\setVolume @__volume
		@__sound\play!

		if @playing!
			addPlaying @
			--@__channel.addEventListener Event.SOUND_COMPLETE, @\onComplete

		@__looping = loop
		@__position = 0

	-- Plays the sound looping. Will loop continuously until you call stop(), play(), or loop() again.
	-- @param	vol		Volume factor, a value from 0 to 1.
	-- @param	pan		Panning factor, a value from -1 to 1.
	loop: (vol = 1, pan = 0) =>
		@play vol, pan, true

	-- Stops the sound if it is currently playing.
	--
	-- @return If the sound was stopped.
	stop: =>
		return false if not @playing!
		removePlaying @
		--@__position = @__channel.position
		--@__channel.removeEventListener(Event.SOUND_COMPLETE, @\onComplete)
		--@__channel.stop()
		--@__channel = nil
		@__sound\stop!
		true

	-- Resumes the sound from the position stop() was called on it.
	resume: =>
		@__channel = @__sound.play(@__position, if @__looping then -1 else 0, @__transform)
		if @playing!
			@addPlaying!
			@__channel.addEventListener(Event.SOUND_COMPLETE, onComplete)

		@__position = 0

	-- @private Event handler for sound completion.
	onComplete = (e = nil) =>
		if @__looping
			@loop @__volume, @__pan
		else
			@stop!

		@__position = 0
		@complete! if @complete != nil

	-- @private Add the sound to the global list.
	addPlaying = =>
		list = {}
		if not @@__typePlaying[@__type]
			@@__typePlaying[@__type] = list
		else
			list = @__typePlaying[@__type]

		table.insert list, @

	-- @private Remove the sound from the global list. */
	removePlaying = =>
		@@__typePlaying[@__type] = nil if (@@__typePlaying[@__type])

	-- Alter the volume factor (a value from 0 to 1) of the sound during playback.
	volume: (value) =>
		return @__volume if value == nil
		value = 0 if value < 0
		return value if @__channel == nil or @__volume == value
		@__volume = LP.clamp value, 0, 1
		filteredVol = value * @getVolume @__type
		filteredVol = 0 if filteredVol < 0
		return value if @__filteredVol == filteredVol
		@__filteredVol, @__transform.volume = filteredVol, filteredVol
		@__channel.soundTransform = @__transform
		@__volume

	-- Alter the panning factor (a value from -1 to 1) of the sound during playback.
	pan: (value) =>
		return @__pan if value == nil
		value = LP.clamp value, -1, 1
		return value if @__channel == nil or @__pan == value
		filteredPan = LP.clamp value + @getPan(@__type), -1, 1
		return value if @__filteredPan == filteredPan
		@__pan = value
		@__filteredPan, @__transform.pan = filteredPan, filteredPan
		@__channel.soundTransform = @__transform
		@__pan

	-- Change the sound type. This an arbitrary string you can use to group
	-- sounds to mute or pan en masse.
	type: (value) =>
		return @__type if value == nil or @__type == value
		if @playing!
			@removePlaying!
			@__type = value
			@addPlaying!
			-- reset, in case type has different global settings
			@pan @pan!
			@volume @volume!
		else
			@__type = value

		value

	-- If the sound is currently playing.
	playing: => @__sound.typeOf and @__sound\typeOf("Source") and not @__sound\isStopped!

	-- Position of the currently playing sound, in seconds.
	position: => (if @playing! then @__channel.position else @__position) / 1000

	-- Length of the sound, in seconds.
	length: => @__sound.length / 1000

	-- Return the global pan for a type.
	--
	-- @param	type	The type to get the pan from.
	--
	-- @return	The global pan for the type.
	getPan: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.pan else 0

		0

	-- Return the global volume for a type.
	--
	-- @param	type	The type to get the volume from.
	--
	-- @return	The global volume for the type.
	getVolume: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.volume else 1

		1

	-- Set the global pan for a type. Sfx instances of this type will add
	-- this pan to their own.
	--
	-- @param	type	The type to set.
	-- @param	pan		The pan value.
	setPan: (type, pan) ->
		transform = @__typeTransforms.get type
		if (transform == nil)
			transform = SoundTransform!
			@@__typeTransforms.set(type, transform)

		transform.pan = LP.clamp pan, -1, 1

		if @__typePlaying.exists type
			sfx.pan sfx.pan! for sfx in @@__typePlaying.get type

	-- Set the global volume for a type. Sfx instances of this type will
	-- multiply their volume by this value.
	--
	-- @param	type	The type to set.
	-- @param	volume	The volume value.
	setVolume: (type, volume) ->
		transform = @@__typeTransforms.get(type)

		if transform == nil
			transform = SoundTransform!
			@@__typeTransforms.set type, transform

		transform.volume = (if volume < 0 then 0 else volume)

		if @@__typePlaying.exists type
			sfx.volume sfx.volume! for sfx in @@__typePlaying.get type

require "LunoPunk.LP"

export ^

-- Sound effect object used to play embedded sounds.
class Sfx
	-- Creates a sound effect from an embedded source. Store a reference to
	-- this object so that you can play the sound using play() or loop().
	-- @param	source		The embedded sound class to use.
	-- @param	complete	Optional callback function for when the sound finishes playing.
	new: (source, complete = nil) =>
		-- Optional callback function for when the sound finishes playing.
		@complete = complete
		-- @__transform = SoundTransform!
		@__volume = 1
		@__pan = 0
		@__position = 0
		@__type = ""

		-- Sound infromation.
		@__type
		@__volume = 1
		@__pan = 0
		@__filteredVol
		@__filteredPan
		@__sound
		@__channel
		-- @__transform
		@__position = 0
		@__looping

		-- Stored Sound objects.
		@@__sounds = {}
		@@__typePlaying = {}
		-- @@__typeTransforms = {}

		assert source != nil, "Invalid source Sound."

		if type(source) == "string"
			@__sound = love.audio.newSource source, "static"
			@@__sounds[source] = @__sound
		else
			className = source.__class.__name
			@__sound = @__sounds[className]
			if @__sound == nil
				@__sound = source
				@@__sounds[className] = source

	-- Plays the sound once.
	-- @param	vol	   Volume factor, a value from 0 to 1.
	-- @param	pan	   Panning factor, a value from -1 to 1.
	-- @param   loop   If the audio should loop infinitely
	play: (volume = 1, pan = 0, loop = false) =>
		return if @__sound == nil
		@stop! if @playing!
		@__pan = LP.clamp pan, -1, 1
		@__volume = (if volume < 0 then 0 else volume)
		@__filteredPan = LP.clamp @__pan + @getPan(@__type), -1, 1
		@__filteredVol = math.max 0, @__volume * @getVolume @_type
		--@__transform.pan = @__filteredPan
		--@__transform.volume = @__filteredVol
		@__sound\setLooping loop
		@__sound\setVolume @__volume
		@__sound\play!

		if @playing!
			@addPlaying!
			@__channel.addEventListener Event.SOUND_COMPLETE, @\onComplete

		@__looping = loop
		@__position = 0

	-- Plays the sound looping. Will loop continuously until you call stop(), play(), or loop() again.
	-- @param	vol		Volume factor, a value from 0 to 1.
	-- @param	pan		Panning factor, a value from -1 to 1.
	loop: (vol = 1, pan = 0) =>
		@play vol, pan, true

	-- Stops the sound if it is currently playing.
	--
	-- @return If the sound was stopped.
	stop: =>
		return false if not @playing!
		@removePlaying!
		@__position = @__channel.position
		@__channel.removeEventListener(Event.SOUND_COMPLETE, onComplete)
		@__channel.stop()
		@__channel = nil
		true

	-- Resumes the sound from the position stop() was called on it.
	resume: =>
		@__channel = @__sound.play(@__position, if @__looping then -1 else 0, @__transform)
		if @playing!
			@addPlaying!
			@__channel.addEventListener(Event.SOUND_COMPLETE, onComplete)

		@__position = 0

	-- @private Event handler for sound completion.
	onComplete = (e = nil) =>
		if @__looping
			@loop @__volume, @__pan
		else
			@stop!

		@__position = 0
		@complete! if @complete != nil

	-- @private Add the sound to the global list.
	addPlaying = =>
		list = {}
		if not @@__typePlaying.exists(_type)
			@@__typePlaying.set(_type, list)
		else
			list = @__typePlaying.get(_type)

		list\append @

	-- @private Remove the sound from the global list. */
	removePlaying = =>
		@__typePlaying.get(@__type).remove @ if (@@__typePlaying.exists(@__type))

	-- Alter the volume factor (a value from 0 to 1) of the sound during playback.
	volume: (value) =>
		return @__volume if value == nil
		value = 0 if value < 0
		return value if @__channel == nil or @__volume == value
		@__volume = LP.clamp value, 0, 1
		filteredVol = value * @getVolume @__type
		filteredVol = 0 if filteredVol < 0
		return value if @__filteredVol == filteredVol
		@__filteredVol, @__transform.volume = filteredVol, filteredVol
		@__channel.soundTransform = @__transform
		@__volume

	-- Alter the panning factor (a value from -1 to 1) of the sound during playback.
	pan: (value) =>
		return @__pan if value == nil
		value = LP.clamp value, -1, 1
		return value if @__channel == nil or @__pan == value
		filteredPan = LP.clamp value + @getPan(@__type), -1, 1
		return value if @__filteredPan == filteredPan
		@__pan = value
		@__filteredPan, @__transform.pan = filteredPan, filteredPan
		@__channel.soundTransform = @__transform
		@__pan

	-- Change the sound type. This an arbitrary string you can use to group
	-- sounds to mute or pan en masse.
	type: (value) =>
		return @__type if value == nil or @__type == value
		if @playing!
			@removePlaying!
			@__type = value
			@addPlaying!
			-- reset, in case type has different global settings
			@pan @pan!
			@volume @volume!
		else
			@__type = value

		value

	-- If the sound is currently playing.
	playing: => @__channel != nil

	-- Position of the currently playing sound, in seconds.
	position: => (if @playing! then @__channel.position else @__position) / 1000

	-- Length of the sound, in seconds.
	length: => @__sound.length / 1000

	-- Return the global pan for a type.
	--
	-- @param	type	The type to get the pan from.
	--
	-- @return	The global pan for the type.
	getPan: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.pan else 0

		0

	-- Return the global volume for a type.
	--
	-- @param	type	The type to get the volume from.
	--
	-- @return	The global volume for the type.
	getVolume: (type) ->
--		if @@__typeTransforms.exists type
--			transform = @@__typeTransforms.get type
--			return if transform != nil then transform.volume else 1

		1

	-- Set the global pan for a type. Sfx instances of this type will add
	-- this pan to their own.
	--
	-- @param	type	The type to set.
	-- @param	pan		The pan value.
	setPan: (type, pan) ->
		transform = @__typeTransforms.get type
		if (transform == nil)
			transform = SoundTransform!
			@@__typeTransforms.set(type, transform)

		transform.pan = LP.clamp pan, -1, 1

		if @__typePlaying.exists type
			sfx.pan sfx.pan! for sfx in @@__typePlaying.get type

	-- Set the global volume for a type. Sfx instances of this type will
	-- multiply their volume by this value.
	--
	-- @param	type	The type to set.
	-- @param	volume	The volume value.
	setVolume: (type, volume) ->
		transform = @@__typeTransforms.get(type)

		if transform == nil
			transform = SoundTransform!
			@@__typeTransforms.set type, transform

		transform.volume = (if volume < 0 then 0 else volume)

		if @@__typePlaying.exists type
			sfx.volume sfx.volume! for sfx in @@__typePlaying.get type


lib/models/channel: Add a few functions

lib/models/channel: Add a few functions

import Map, Option from require 'lib.std'

class Channel
	new: (bot, channel_name, input={})=>
		@_bot = assert bot
		@name = assert channel_name
		@status = Option input.status -- Bot's status in that channel
		@users = Map input.users or {}
		@statuses = Map input.statuses or {}

	send: (message)=>
		@_bot\send "PRIVMSG", @name, message

	-- Channel Operations
	part: (channel, message)=>
		if message
			@_bot\send_raw "PART", channel, message
		else
			@_bot\send_raw "PART", channel

	-- Mode changing commands

	-- ::TODO:: optimize for multiple targets
	op: (target)=>
		@_bot\send "MODE", channel, "+o", target

	deop: (target)=>
		@_bot\send "MODE", channel, "-o", target

	voice: (target)=>
		@_bot\send "MODE", channel, "+v", target

	devoice: (target)=>
		@_bot\send "MODE", channel, "-v", target

	-- ::TODO:: invite

return :Channel

import Map, Option from require 'lib.std'

class Channel
	new: (bot, channel_name, input={})=>
		@_bot = assert bot
		@name = assert channel_name
		@status = Option input.status -- Bot's status in that channel
		@users = Map input.users or {}
		@statuses = Map input.statuses or {}

	send: (message)=>
		@_bot\send "PRIVMSG", @name, message

return :Channel


Debugprint

Debugprint

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

class NetworkChangeState
	new: (key = '', keyValid = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@keyValid = keyValid
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTime()
		@rtime = RealTime()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
		@networkChange = true
	GetPlayer: => @ply
	ChangedByClient: => not @networkChange or IsValid(@ply)
	ChangedByPlayer: => not @networkChange or IsValid(@ply)
	ChangedByServer: => not @networkChange or not IsValid(@ply)
	GetKey: => @keyValid
	GetVariable: => @keyValid
	GetVar: => @keyValid
	GetKeyInternal: => @key
	GetVariableInternal: => @key
	GetVarInternal: => @key
	GetNewValue: => @newValue
	GetValue: => @newValue
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTime: => @rtime
	GetRealTime: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len
	ChangedByNetwork: => @networkChange

	Revert: => @obj[@key] = @oldValue
	Apply: => @obj[@key] = @newValue

class NetworkedObject
	@Setup = =>
		@NW_Vars = {} if @NW_Vars == nil
		@NW_VarsTable = {}
		@NW_Objects = {}
		@NW_Waiting = {}
		@NW_WaitID = -1
		@NW_Setup = true
		@NW_NextVarID = -1 if @NW_NextVarID == nil
		@NW_Create = "PPM2.NW.Created.#{@__name}"
		@NW_Modify = "PPM2.NW.Modified.#{@__name}"
		@NW_Remove = "PPM2.NW.Removed.#{@__name}"
		@NW_Rejected = "PPM2.NW.Rejected.#{@__name}"
		@NW_ReceiveID = "PPM2.NW.ReceiveID.#{@__name}"
		@NW_CooldownTimerCount = "PPM2_NW_CooldownTimerCount_#{@__name}"
		@NW_CooldownTimer = "PPM2_NW_CooldownTimer_#{@__name}"
		@NW_CooldownMessage = "PPM2_NW_CooldownMessage_#{@__name}"
		@NW_NextObjectID = 0
		@NW_NextObjectID_CL = 2 ^ 28

		if SERVER
			util.AddNetworkString(@NW_Create)
			util.AddNetworkString(@NW_Modify)
			util.AddNetworkString(@NW_Remove)
			util.AddNetworkString(@NW_ReceiveID)
			util.AddNetworkString(@NW_Rejected)

		net.Receive @NW_Create, (len = 0, ply = NULL) -> @OnNetworkedCreated(ply, len)
		net.Receive @NW_Modify, (len = 0, ply = NULL) -> @OnNetworkedModify(ply, len)
		net.Receive @NW_Remove, (len = 0, ply = NULL) -> @OnNetworkedDelete(ply, len)
		net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
			return if SERVER
			waitID = net.ReadUInt(16)
			netID = net.ReadUInt(16)
			obj = @NW_Waiting[waitID]
			@NW_Waiting[waitID] = nil
			return unless obj
			obj.NETWORKED = true
			@NW_Objects[obj.netID] = nil
			obj.netID = netID
			obj.waitID = nil
			@NW_Objects[netID] = obj
		net.Receive @NW_Rejected, (len = 0, ply = NULL) ->
			return if SERVER
			netID = net.ReadUInt(16)
			obj = @NW_Objects[netID]
			return unless obj
			return if obj.__LastReject and obj.__LastReject > RealTime()
			obj.__LastReject = RealTime() + 3
			obj.NETWORKED = false
			obj\Create()
	-- @__inherited = (child) => child.Setup(child)

	@AddNetworkVar = (getName = 'Var', readFunc = (->), writeFunc = (->), defValue) =>
		strName = "_NW_#{getName}"
		error("No more free slots! Can't add #{getName} to the table") if @NW_NextVarID > 254
		@NW_NextVarID += 1
		id = @NW_NextVarID
		tab = {:strName, :readFunc, :getName, :writeFunc, :defValue, :id}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[id] = tab
		@__base[strName] = defValue
		@__base["Get#{getName}"] = => @[strName]
		@__base["Set#{getName}"] = (val = defValue, networkNow = true) =>
			oldVal = @[strName]
			@[strName] = val
			state = NetworkChangeState(strName, getName, val, @)
			state.networkChange = false
			@SetLocalChange(state)
			if networkNow and @NETWORKED and (CLIENT and @@NW_ClientsideCreation or SERVER)
				net.Start(@@NW_Modify)
				net.WriteUInt(@GetNetworkID(), 16)
				net.WriteUInt(id, 8)
				writeFunc(@[strName])
				if CLIENT
					net.SendToServer()
				else
					net.Broadcast()
	@NetworkVar = (...) => @AddNetworkVar(...)

	@NW_ClientsideCreation = false
	@NW_RemoveOnPlayerLeave = true
	@OnNetworkedCreated = (ply = NULL, len = 0) =>
		return if SERVER and not @NW_ClientsideCreation
		if CLIENT
			netID = net.ReadUInt(16)
			obj = @NW_Objects[netID] or @(netID)
			obj.NETWORKED = true
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			@OnNetworkedCreatedCallback(obj, ply, len)
		else
			ply[@NW_CooldownTimer] = ply[@NW_CooldownTimer] or 0
			ply[@NW_CooldownTimerCount] = ply[@NW_CooldownTimerCount] or 0

			if ply[@NW_CooldownTimer] < RealTime()
				ply[@NW_CooldownTimerCount] = 1
				ply[@NW_CooldownTimer] = RealTime() + 10
			else
				ply[@NW_CooldownTimerCount] += 1

			if ply[@NW_CooldownTimerCount] >= 3
				ply[@NW_CooldownMessage] = ply[@NW_CooldownMessage] or 0
				if ply[@NW_CooldownMessage] < RealTime()
					print "[PPM2] Player #{ply\Nick()}<#{ply\SteamID()}> is creating #{@__name} too quickly!"
					ply[@NW_CooldownMessage] = RealTime() + 1
				return

			waitID = net.ReadUInt(16)
			obj = @()
			obj.NW_Player = ply
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			obj\Create()
			timer.Simple 0.5, ->
				return if not IsValid(ply)
				net.Start(@NW_ReceiveID)
				net.WriteUInt(waitID, 16)
				net.WriteUInt(obj.netID, 16)
				net.Send(ply)
			@OnNetworkedCreatedCallback(obj, ply, len)
	@OnNetworkedCreatedCallback = (obj, ply = NULL, len = 0) => -- Override

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		unless obj
			if SERVER
				net.Start(@NW_Rejected)
				net.WriteUInt(id, 16)
				net.Send(ply)
			return
		return if IsValid(ply) and obj.NW_Player ~= ply
		varID = net.ReadUInt(8)
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :getName, :readFunc, :writeFunc} = varData
		newVal = readFunc()
		return if newVal == obj["Get#{getName}"](obj)
		state = NetworkChangeState(strName, getName, newVal, obj, len, ply)
		state\Apply()
		obj\NetworkDataChanges(state)
		if SERVER
			net.Start(@NW_Modify)
			net.WriteUInt(id, 16)
			net.WriteUInt(varID, 8)
			writeFunc(newVal)
			net.SendOmit(ply)
		@OnNetworkedModifyCallback(state)
	@OnNetworkedModifyCallback = (state) => -- Override

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove(true)
		@OnNetworkedDeleteCallback(obj, ply, len)
	@OnNetworkedDeleteCallback = (obj, ply = NULL, len = 0) => -- Override

	@ReadNetworkData = =>
		output = {strName, {getName, readFunc()} for {:getName, :strName, :readFunc} in *@NW_Vars}
		return output

	new: (netID, localObject = false) =>
		@valid = true
		@NETWORKED = false
		@NETWORKED_PREDICT = false

		if SERVER
			@netID = @@NW_NextObjectID
			@@NW_NextObjectID += 1
		else
			netID = -1 if netID == nil
			@netID = netID

		@@NW_Objects[@netID] = @
		@NW_Player = NULL
		@isLocal = localObject
		@NW_Player = LocalPlayer() if localObject

	IsValid: => @valid
	IsNetworked: => @NETWORKED
	IsGoingToNetwork: => @NETWORKED_PREDICT
	SetIsGoingToNetwork: (val = @NETWORKED) => @NETWORKED_PREDICT = val
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID
	Remove: (byClient = false) =>
		@@NW_Objects[@netID] = nil
		@valid = false
		if CLIENT and @isLocal and @NETWORKED and @@NW_ClientsideCreation
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID, 16)
			net.SendToServer()
		elseif SERVER and @NETWORKED
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID, 16)
			if not IsValid(@NW_Player) or not byClient
				net.Broadcast()
			else
				net.SendOmit(@NW_Player)

	NetworkDataChanges: (state) => -- Override
	SetLocalChange: (state) => -- Override
	ReadNetworkData: (len = 24, ply = NULL, silent = false) =>
		data = @@ReadNetworkData()
		states = [NetworkChangeState(key, keyValid, newVal, @, len, ply) for key, {keyValid, newVal} in pairs data]
		for state in *states
			state\Apply()
			@NetworkDataChanges(state) unless silent

	WriteNetworkData: => writeFunc(@[strName]) for {:strName, :writeFunc} in *@@NW_Vars

	SendVar: (Var = '') =>
		return if @[Var] == nil

	__tostring: => "[NetworkedObject:#{@netID}|#{@ent}]"

	Create: =>
		return if @NETWORKED
		return if CLIENT and not @@NW_ClientsideCreation
		@NETWORKED = true if SERVER
		@NETWORKED_PREDICT = true
		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt(@netID, 16)
			@WriteNetworkData()
			filter = RecipientFilter()
			filter\AddAllPlayers()
			filter\RemovePlayer(@NW_Player) if IsValid(@NW_Player)
			net.Send(filter)
		else
			@@NW_WaitID += 1
			@waitID = @@NW_WaitID
			net.Start(@@NW_Create)
			net.WriteUInt(@waitID, 16)
			@WriteNetworkData()
			net.SendToServer()
			@@NW_Waiting[@waitID] = @
	NetworkTo: (targets = {}) =>
		net.Start(@@NW_Create)
		net.WriteUInt(@netID, 16)
		@WriteNetworkData()
		net.Send(targets)

PPM2.NetworkedObject = NetworkedObject

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

class NetworkChangeState
	new: (key = '', keyValid = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@keyValid = keyValid
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTime()
		@rtime = RealTime()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
		@networkChange = true
	GetPlayer: => @ply
	ChangedByClient: => not @networkChange or IsValid(@ply)
	ChangedByPlayer: => not @networkChange or IsValid(@ply)
	ChangedByServer: => not @networkChange or not IsValid(@ply)
	GetKey: => @keyValid
	GetVariable: => @keyValid
	GetVar: => @keyValid
	GetKeyInternal: => @key
	GetVariableInternal: => @key
	GetVarInternal: => @key
	GetNewValue: => @newValue
	GetValue: => @newValue
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTime: => @rtime
	GetRealTime: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len
	ChangedByNetwork: => @networkChange

	Revert: => @obj[@key] = @oldValue
	Apply: => @obj[@key] = @newValue

class NetworkedObject
	@Setup = =>
		@NW_Vars = {} if @NW_Vars == nil
		@NW_VarsTable = {}
		@NW_Objects = {}
		@NW_Waiting = {}
		@NW_WaitID = -1
		@NW_Setup = true
		@NW_NextVarID = -1 if @NW_NextVarID == nil
		@NW_Create = "PPM2.NW.Created.#{@__name}"
		@NW_Modify = "PPM2.NW.Modified.#{@__name}"
		@NW_Remove = "PPM2.NW.Removed.#{@__name}"
		@NW_Rejected = "PPM2.NW.Rejected.#{@__name}"
		@NW_ReceiveID = "PPM2.NW.ReceiveID.#{@__name}"
		@NW_CooldownTimerCount = "PPM2_NW_CooldownTimerCount_#{@__name}"
		@NW_CooldownTimer = "PPM2_NW_CooldownTimer_#{@__name}"
		@NW_CooldownMessage = "PPM2_NW_CooldownMessage_#{@__name}"
		@NW_NextObjectID = 0
		@NW_NextObjectID_CL = 2 ^ 28

		if SERVER
			util.AddNetworkString(@NW_Create)
			util.AddNetworkString(@NW_Modify)
			util.AddNetworkString(@NW_Remove)
			util.AddNetworkString(@NW_ReceiveID)
			util.AddNetworkString(@NW_Rejected)

		net.Receive @NW_Create, (len = 0, ply = NULL) -> @OnNetworkedCreated(ply, len)
		net.Receive @NW_Modify, (len = 0, ply = NULL) -> @OnNetworkedModify(ply, len)
		net.Receive @NW_Remove, (len = 0, ply = NULL) -> @OnNetworkedDelete(ply, len)
		net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
			return if SERVER
			waitID = net.ReadUInt(16)
			netID = net.ReadUInt(16)
			obj = @NW_Waiting[waitID]
			@NW_Waiting[waitID] = nil
			return unless obj
			obj.NETWORKED = true
			@NW_Objects[obj.netID] = nil
			obj.netID = netID
			obj.waitID = nil
			@NW_Objects[netID] = obj
		net.Receive @NW_Rejected, (len = 0, ply = NULL) ->
			return if SERVER
			netID = net.ReadUInt(16)
			obj = @NW_Objects[netID]
			return unless obj
			return if obj.__LastReject and obj.__LastReject > RealTime()
			obj.__LastReject = RealTime() + 3
			obj.NETWORKED = false
			obj\Create()
	-- @__inherited = (child) => child.Setup(child)

	@AddNetworkVar = (getName = 'Var', readFunc = (->), writeFunc = (->), defValue) =>
		strName = "_NW_#{getName}"
		error("No more free slots! Can't add #{getName} to the table") if @NW_NextVarID > 254
		@NW_NextVarID += 1
		id = @NW_NextVarID
		tab = {:strName, :readFunc, :getName, :writeFunc, :defValue, :id}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[id] = tab
		@__base[strName] = defValue
		@__base["Get#{getName}"] = => @[strName]
		@__base["Set#{getName}"] = (val = defValue, networkNow = true) =>
			oldVal = @[strName]
			@[strName] = val
			state = NetworkChangeState(strName, getName, val, @)
			state.networkChange = false
			@SetLocalChange(state)
			if networkNow and @NETWORKED and (CLIENT and @@NW_ClientsideCreation or SERVER)
				net.Start(@@NW_Modify)
				net.WriteUInt(@GetNetworkID(), 16)
				net.WriteUInt(id, 8)
				writeFunc(@[strName])
				if CLIENT
					net.SendToServer()
				else
					net.Broadcast()
	@NetworkVar = (...) => @AddNetworkVar(...)

	@NW_ClientsideCreation = false
	@NW_RemoveOnPlayerLeave = true
	@OnNetworkedCreated = (ply = NULL, len = 0) =>
		return if SERVER and not @NW_ClientsideCreation
		if CLIENT
			netID = net.ReadUInt(16)
			obj = @NW_Objects[netID] or @(netID)
			obj.NETWORKED = true
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			@OnNetworkedCreatedCallback(obj, ply, len)
		else
			ply[@NW_CooldownTimer] = ply[@NW_CooldownTimer] or 0
			ply[@NW_CooldownTimerCount] = ply[@NW_CooldownTimerCount] or 0

			if ply[@NW_CooldownTimer] < RealTime()
				ply[@NW_CooldownTimerCount] = 1
				ply[@NW_CooldownTimer] = RealTime() + 10
			else
				ply[@NW_CooldownTimerCount] += 1

			if ply[@NW_CooldownTimerCount] >= 3
				ply[@NW_CooldownMessage] = ply[@NW_CooldownMessage] or 0
				if ply[@NW_CooldownMessage] < RealTime()
					print "[PPM2] Player #{ply\Nick()}<#{ply\SteamID()}> is creating #{@__name} too quickly!"
					ply[@NW_CooldownMessage] = RealTime() + 1
				return

			waitID = net.ReadUInt(16)
			obj = @()
			obj.NW_Player = ply
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			obj\Create()
			timer.Simple 0.5, ->
				return if not IsValid(ply)
				net.Start(@NW_ReceiveID)
				net.WriteUInt(waitID, 16)
				net.WriteUInt(obj.netID, 16)
				net.Send(ply)
				print waitID, obj.netID, ply
			@OnNetworkedCreatedCallback(obj, ply, len)
	@OnNetworkedCreatedCallback = (obj, ply = NULL, len = 0) => -- Override

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		unless obj
			if SERVER
				net.Start(@NW_Rejected)
				net.WriteUInt(id, 16)
				net.Send(ply)
			return
		return if IsValid(ply) and obj.NW_Player ~= ply
		varID = net.ReadUInt(8)
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :getName, :readFunc, :writeFunc} = varData
		newVal = readFunc()
		return if newVal == obj["Get#{getName}"](obj)
		state = NetworkChangeState(strName, getName, newVal, obj, len, ply)
		state\Apply()
		obj\NetworkDataChanges(state)
		if SERVER
			net.Start(@NW_Modify)
			net.WriteUInt(id, 16)
			net.WriteUInt(varID, 8)
			writeFunc(newVal)
			net.SendOmit(ply)
		@OnNetworkedModifyCallback(state)
	@OnNetworkedModifyCallback = (state) => -- Override

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove(true)
		@OnNetworkedDeleteCallback(obj, ply, len)
	@OnNetworkedDeleteCallback = (obj, ply = NULL, len = 0) => -- Override

	@ReadNetworkData = =>
		output = {strName, {getName, readFunc()} for {:getName, :strName, :readFunc} in *@NW_Vars}
		return output

	new: (netID, localObject = false) =>
		@valid = true
		@NETWORKED = false
		@NETWORKED_PREDICT = false

		if SERVER
			@netID = @@NW_NextObjectID
			@@NW_NextObjectID += 1
		else
			netID = -1 if netID == nil
			@netID = netID

		@@NW_Objects[@netID] = @
		@NW_Player = NULL
		@isLocal = localObject
		@NW_Player = LocalPlayer() if localObject

	IsValid: => @valid
	IsNetworked: => @NETWORKED
	IsGoingToNetwork: => @NETWORKED_PREDICT
	SetIsGoingToNetwork: (val = @NETWORKED) => @NETWORKED_PREDICT = val
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID
	Remove: (byClient = false) =>
		@@NW_Objects[@netID] = nil
		@valid = false
		if CLIENT and @isLocal and @NETWORKED and @@NW_ClientsideCreation
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID, 16)
			net.SendToServer()
		elseif SERVER and @NETWORKED
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID, 16)
			if not IsValid(@NW_Player) or not byClient
				net.Broadcast()
			else
				net.SendOmit(@NW_Player)

	NetworkDataChanges: (state) => -- Override
	SetLocalChange: (state) => -- Override
	ReadNetworkData: (len = 24, ply = NULL, silent = false) =>
		data = @@ReadNetworkData()
		states = [NetworkChangeState(key, keyValid, newVal, @, len, ply) for key, {keyValid, newVal} in pairs data]
		for state in *states
			state\Apply()
			@NetworkDataChanges(state) unless silent

	WriteNetworkData: => writeFunc(@[strName]) for {:strName, :writeFunc} in *@@NW_Vars

	SendVar: (Var = '') =>
		return if @[Var] == nil

	__tostring: => "[NetworkedObject:#{@netID}|#{@ent}]"

	Create: =>
		return if @NETWORKED
		return if CLIENT and not @@NW_ClientsideCreation
		@NETWORKED = true if SERVER
		@NETWORKED_PREDICT = true
		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt(@netID, 16)
			@WriteNetworkData()
			filter = RecipientFilter()
			filter\AddAllPlayers()
			filter\RemovePlayer(@NW_Player) if IsValid(@NW_Player)
			net.Send(filter)
		else
			@@NW_WaitID += 1
			@waitID = @@NW_WaitID
			net.Start(@@NW_Create)
			net.WriteUInt(@waitID, 16)
			@WriteNetworkData()
			net.SendToServer()
			@@NW_Waiting[@waitID] = @
	NetworkTo: (targets = {}) =>
		net.Start(@@NW_Create)
		net.WriteUInt(@netID, 16)
		@WriteNetworkData()
		net.Send(targets)

PPM2.NetworkedObject = NetworkedObject


Restore words coloring in Editor.ColorWord.moon


Remove some pointless whitespace

Remove some pointless whitespace


Don't draw legs if player view is far away from vehicle point

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

ENABLE_FLASHLIGHT_PASS = CreateConVar('ppm2_flasglight_pass', '0', {FCVAR_ARCHIVE}, 'Enable flashlight render pass. This kills FPS.')

class PonyRenderController
    @AVALIABLE_CONTROLLERS = {}
    @MODELS = {'models/ppm/player_default_base.mdl', 'models/ppm/player_default_base_nj.mdl', 'models/cppm/player_default_base.mdl', 'models/cppm/player_default_base_nj.mdl'}
    @__inherited: (child) =>
        child.MODELS_HASH = {mod, true for mod in *child.MODELS}
        @AVALIABLE_CONTROLLERS[mod] = child for mod in *child.MODELS
    @__inherited(@)
    @NEXT_OBJ_ID = 0

    CompileTextures: => @GetTextureController()\CompileTextures() if @GetTextureController and @GetTextureController()
    new: (data) =>
        @objID = @@NEXT_OBJ_ID
        @@NEXT_OBJ_ID += 1
        @isValid = true
        @networkedData = data
        @ent = data.ent
        @modelCached = data\GetModel()
        @IGNORE_DRAW = false
        @CompileTextures() if @ent
        @CreateLegs() if @ent == LocalPlayer()
        @socksModel = data\GetSocksModel()
        @socksModel\SetNoDraw(false) if IsValid(@socksModel)
        @CreateFlexController() if @ent
    __tostring: => "[#{@@__name}:#{@objID}|#{@GetData()}]"
    GetEntity: => @ent
    GetData: => @networkedData
    GetModel: => @networkedData\GetModel()

    GetLegs: =>
        return NULL if not @isValid
        return NULL if @ent ~= LocalPlayer()
        @CreateLegs() if not IsValid()
        return @legsModel
    CreateLegs: =>
        return NULL if not @isValid
        return NULL if @ent ~= LocalPlayer()
        for ent in *ents.GetAll()
            if ent.isPonyLegsModel
                ent\Remove()
        @legsModel = ClientsideModel(@modelCached)
        with @legsModel
            .isPonyLegsModel = true
            \SetNoDraw(true)
            .__PPM2_PonyData = @GetData()

        @GetData()\GetWeightController()\UpdateWeight(@legsModel)

        @lastLegUpdate = CurTime()
        @legClipPlanePos = Vector(0, 0, 0)
        @legBGSetup = CurTime()
        @legUpdateFrame = 0
        @legClipDot = 0
        @duckOffsetHack = @@LEG_CLIP_OFFSET_STAND
        @legsClipPlane = @@LEG_CLIP_VECTOR
        return @legsModel

    @LEG_SHIFT_CONST = 24
    @LEG_SHIFT_CONST_VEHICLE = 14
    @LEG_Z_CONST = 0
    @LEG_Z_CONST_VEHICLE = 20
    @LEG_ANIM_SPEED_CONST = 1
    @LEG_CLIP_OFFSET_STAND = 28
    @LEG_CLIP_OFFSET_DUCK = 12
    @LEG_CLIP_OFFSET_VEHICLE = 11

    UpdateLegs: =>
        return if not @isValid
        return unless IsValid(@legsModel)
        return if @legUpdateFrame == FrameNumber()
        @legUpdateFrame = FrameNumber()
        ctime = CurTime()
        ply = @ent
        seq = ply\GetSequence()

        if seq ~= @legSeq
            @legSeq = seq
            @legsModel\ResetSequence(seq)

        if @legBGSetup < ctime
            @legBGSetup = ctime + 1
            for group in *ply\GetBodyGroups()
                @legsModel\SetBodygroup(group.id, ply\GetBodygroup(group.id))

        with @legsModel
            \FrameAdvance(ctime - @lastLegUpdate)
            \SetPlaybackRate(@@LEG_ANIM_SPEED_CONST * ply\GetPlaybackRate())
            @lastLegUpdate = ctime
            \SetPoseParameter('move_x',       (ply\GetPoseParameter('move_x')     * 2) - 1)
            \SetPoseParameter('move_y',       (ply\GetPoseParameter('move_y')     * 2) - 1)
            \SetPoseParameter('move_yaw',     (ply\GetPoseParameter('move_yaw')   * 360) - 180)
            \SetPoseParameter('body_yaw',     (ply\GetPoseParameter('body_yaw')   * 180) - 90)
            \SetPoseParameter('spine_yaw',    (ply\GetPoseParameter('spine_yaw')  * 180) - 90)

        if ply\InVehicle()
            veh = ply\GetVehicle()
            vehAng = veh\GetAngles()
            eyepos = EyePos()
            vehAng\RotateAroundAxis(vehAng\Up(), 90)

            clipAng = Angle(vehAng.p, vehAng.y, vehAng.r)
            clipAng\RotateAroundAxis(clipAng\Right(), -90)

            @legsClipPlane = clipAng\Forward()
            @legsModel\SetRenderAngles(vehAng)

            legClipPlanePos = Vector(0, 0, @@LEG_CLIP_OFFSET_VEHICLE)
            legClipPlanePos\Rotate(vehAng)
            @legClipPlanePos = eyepos - legClipPlanePos

            drawPos = Vector(@@LEG_SHIFT_CONST_VEHICLE, 0, @@LEG_Z_CONST_VEHICLE)
            drawPos\Rotate(vehAng)
            @legsModel\SetRenderOrigin(eyepos - drawPos)
        else
            @legsClipPlane = @@LEG_CLIP_VECTOR
            eangles = EyeAngles()
            yaw = eangles.y - ply\GetPoseParameter('head_yaw') * 180 + 90
            newAng = Angle(0, yaw, 0)
            rad = math.rad(yaw)
            sin, cos = math.sin(rad), math.cos(rad)
            pos = ply\GetPos()
            {:x, :y, :z} = pos
            newPos = Vector(x - cos * @@LEG_SHIFT_CONST, y - sin * @@LEG_SHIFT_CONST, z + @@LEG_Z_CONST)
            if ply\Crouching()
                @duckOffsetHack = @@LEG_CLIP_OFFSET_DUCK
            else
                @duckOffsetHack = Lerp(0.1, @duckOffsetHack, @@LEG_CLIP_OFFSET_STAND)

            @legClipPlanePos = Vector(x, y, z + @duckOffsetHack)
            @legsModel\SetRenderAngles(newAng)
            @legsModel\SetRenderOrigin(newPos)
        @legClipDot = @legsClipPlane\Dot(@legClipPlanePos)

    @LEG_CLIP_VECTOR = Vector(0, 0, -1)
    @LEGS_MAX_DISTANCE = 60 ^ 2
    DrawLegs: (start3D = true) =>
        return if not @isValid
        @CreateLegs() unless IsValid(@legsModel)
        return unless IsValid(@legsModel)
        return if @ent\ShouldDrawLocalPlayer()
        return if @ent\GetPos()\DistToSqr(EyePos()) > @@LEGS_MAX_DISTANCE
        @UpdateLegs()

        oldClip = render.EnableClipping(true)
        render.PushCustomClipPlane(@legsClipPlane, @legClipDot)
        cam.Start3D() if start3D

        if ENABLE_FLASHLIGHT_PASS\GetBool()
            render.PushFlashlightMode(true)
            @GetTextureController()\PreDrawLegs()
            @legsModel\DrawModel()
            @GetTextureController()\PostDrawLegs()
            render.PopFlashlightMode()

        @GetTextureController()\PreDrawLegs()
        @legsModel\DrawModel()
        @GetTextureController()\PostDrawLegs()

        render.PopCustomClipPlane()
        cam.End3D() if start3D
        render.EnableClipping(oldClip)

    IsValid: => IsValid(@ent) and @isValid
    Remove: =>
        @flexes\Remove() if @flexes
        @GetTextureController()\Remove() if @GetTextureController and @GetTextureController()
        @isValid = false

    PlayerRespawn: =>
        return if not @isValid
        @flexes\PlayerRespawn() if @flexes
        @GetTextureController()\ResetTextures() if @GetTextureController()

    DrawModels: =>
        @socksModel\DrawModel() if IsValid(@socksModel)

    PreDraw: (ent = @ent) =>
        return if not @isValid
        @GetTextureController()\PreDraw(ent)
        @flexes\Think(ent) if @flexes
    PostDraw: (ent = @ent) =>
        return if not @isValid
        @GetTextureController()\PostDraw(ent)

    @ARMS_MATERIAL_INDEX = 0
    PreDrawArms: (ent) =>
        return if not @isValid
        if ent and not @armsWeightSetup
            @armsWeightSetup = true
            weight = @GetData()\GetWeight()
            vec = Vector(weight, weight, weight)
            for i = 1, 13
                ent\ManipulateBoneScale(i, vec)
        render.MaterialOverrideByIndex(@@ARMS_MATERIAL_INDEX, @GetTextureController()\GetBody())
    PostDrawArms: (ent) =>
        return if not @isValid
        render.MaterialOverrideByIndex(@@ARMS_MATERIAL_INDEX)

    DataChanges: (state) =>
        return if not @isValid
        return if not @ent
        @GetTextureController()\DataChanges(state)
        @flexes\DataChanges(state) if @flexes
        switch state\GetKey()
            when 'Weight'
                @armsWeightSetup = false
                @GetData()\GetWeightController()\UpdateWeight(@legsModel) if IsValid(@legsModel)
            when 'SocksModel'
                @socksModel = @GetData()\GetSocksModel()
                @socksModel\SetNoDraw(false) if IsValid(@socksModel)
                @GetTextureController()\UpdateSocks(@ent, @socksModel) if @GetTextureController() and IsValid(@socksModel)
            when 'NoFlex'
                if state\GetValue()
                    @flexes\ResetSequences() if @flexes
                    @flexes = nil
                else
                    @CreateFlexController()
    GetTextureController: =>
        return @renderController if not @isValid
        if not @renderController
            cls = PPM2.GetTextureController(@modelCached)
            @renderController = cls(@)
        @renderController.ent = @ent
        return @renderController
    CreateFlexController: =>
        return if not @isValid
        return if not @ent\IsPlayer()
        return if @GetData()\GetNoFlex()
        if not @flexes
            cls = PPM2.GetFlexController(@modelCached)
            return if not cls
            @flexes = cls(@)
        @flexes.ent = @ent
        return @flexes
    GetFlexController: => @flexes

class NewPonyRenderController extends PonyRenderController
    @MODELS = {'models/ppm/player_default_base_new.mdl'}

    new: (data) =>
        @upperManeModel = data\GetUpperManeModel()
        @lowerManeModel = data\GetLowerManeModel()
        @tailModel = data\GetTailModel()
        @upperManeModel\SetNoDraw(false) if IsValid(@upperManeModel)
        @lowerManeModel\SetNoDraw(false) if IsValid(@lowerManeModel)
        @tailModel\SetNoDraw(false) if IsValid(@tailModel)
        super(data)
    __tostring: => "[#{@@__name}:#{@objID}|#{@GetData()}]"

    DataChanges: (state) =>
        return if not @ent
        return if not @isValid
        switch state\GetKey()
            when 'UpperManeModel'
                @upperManeModel = @GetData()\GetUpperManeModel()
                @upperManeModel\SetNoDraw(false) if IsValid(@upperManeModel)
                @GetTextureController()\UpdateUpperMane(@ent, @upperManeModel) if @GetTextureController() and IsValid(@upperManeModel)
            when 'LowerManeModel'
                @lowerManeModel = @GetData()\GetLowerManeModel()
                @lowerManeModel\SetNoDraw(false) if IsValid(@lowerManeModel)
                @GetTextureController()\UpdateLowerMane(@ent, @lowerManeModel) if @GetTextureController() and IsValid(@lowerManeModel)
            when 'TailModel'
                @tailModel = @GetData()\GetTailModel()
                @tailModel\SetNoDraw(false) if IsValid(@tailModel)
                @GetTextureController()\UpdateTail(@ent, @tailModel) if @GetTextureController() and IsValid(@tailModel)
        super(state)

    DrawModels: =>
        @upperManeModel\DrawModel() if IsValid(@upperManeModel)
        @lowerManeModel\DrawModel() if IsValid(@lowerManeModel)
        @tailModel\DrawModel() if IsValid(@tailModel)
        super()

PPM2.PonyRenderController = PonyRenderController
PPM2.NewPonyRenderController = NewPonyRenderController
PPM2.GetPonyRenderController = (model = 'models/ppm/player_default_base.mdl') -> PonyRenderController.AVALIABLE_CONTROLLERS[model\lower()] or PonyRenderController
PPM2.GetPonyRendererController = PPM2.GetPonyRenderController
PPM2.GetRenderController = PPM2.GetPonyRenderController
PPM2.GetRendererController = PPM2.GetPonyRenderController

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

ENABLE_FLASHLIGHT_PASS = CreateConVar('ppm2_flasglight_pass', '0', {FCVAR_ARCHIVE}, 'Enable flashlight render pass. This kills FPS.')

class PonyRenderController
    @AVALIABLE_CONTROLLERS = {}
    @MODELS = {'models/ppm/player_default_base.mdl', 'models/ppm/player_default_base_nj.mdl', 'models/cppm/player_default_base.mdl', 'models/cppm/player_default_base_nj.mdl'}
    @__inherited: (child) =>
        child.MODELS_HASH = {mod, true for mod in *child.MODELS}
        @AVALIABLE_CONTROLLERS[mod] = child for mod in *child.MODELS
    @__inherited(@)
    @NEXT_OBJ_ID = 0

    CompileTextures: => @GetTextureController()\CompileTextures() if @GetTextureController and @GetTextureController()
    new: (data) =>
        @objID = @@NEXT_OBJ_ID
        @@NEXT_OBJ_ID += 1
        @isValid = true
        @networkedData = data
        @ent = data.ent
        @modelCached = data\GetModel()
        @IGNORE_DRAW = false
        @CompileTextures() if @ent
        @CreateLegs() if @ent == LocalPlayer()
        @socksModel = data\GetSocksModel()
        @socksModel\SetNoDraw(false) if IsValid(@socksModel)
        @CreateFlexController() if @ent
    __tostring: => "[#{@@__name}:#{@objID}|#{@GetData()}]"
    GetEntity: => @ent
    GetData: => @networkedData
    GetModel: => @networkedData\GetModel()

    GetLegs: =>
        return NULL if not @isValid
        return NULL if @ent ~= LocalPlayer()
        @CreateLegs() if not IsValid()
        return @legsModel
    CreateLegs: =>
        return NULL if not @isValid
        return NULL if @ent ~= LocalPlayer()
        for ent in *ents.GetAll()
            if ent.isPonyLegsModel
                ent\Remove()
        @legsModel = ClientsideModel(@modelCached)
        with @legsModel
            .isPonyLegsModel = true
            \SetNoDraw(true)
            .__PPM2_PonyData = @GetData()

        @GetData()\GetWeightController()\UpdateWeight(@legsModel)

        @lastLegUpdate = CurTime()
        @legClipPlanePos = Vector(0, 0, 0)
        @legBGSetup = CurTime()
        @legUpdateFrame = 0
        @legClipDot = 0
        @duckOffsetHack = @@LEG_CLIP_OFFSET_STAND
        @legsClipPlane = @@LEG_CLIP_VECTOR
        return @legsModel

    @LEG_SHIFT_CONST = 24
    @LEG_SHIFT_CONST_VEHICLE = 14
    @LEG_Z_CONST = 0
    @LEG_Z_CONST_VEHICLE = 20
    @LEG_ANIM_SPEED_CONST = 1
    @LEG_CLIP_OFFSET_STAND = 28
    @LEG_CLIP_OFFSET_DUCK = 12
    @LEG_CLIP_OFFSET_VEHICLE = 11

    UpdateLegs: =>
        return if not @isValid
        return unless IsValid(@legsModel)
        return if @legUpdateFrame == FrameNumber()
        @legUpdateFrame = FrameNumber()
        ctime = CurTime()
        ply = @ent
        seq = ply\GetSequence()

        if seq ~= @legSeq
            @legSeq = seq
            @legsModel\ResetSequence(seq)

        if @legBGSetup < ctime
            @legBGSetup = ctime + 1
            for group in *ply\GetBodyGroups()
                @legsModel\SetBodygroup(group.id, ply\GetBodygroup(group.id))

        with @legsModel
            \FrameAdvance(ctime - @lastLegUpdate)
            \SetPlaybackRate(@@LEG_ANIM_SPEED_CONST * ply\GetPlaybackRate())
            @lastLegUpdate = ctime
            \SetPoseParameter('move_x',       (ply\GetPoseParameter('move_x')     * 2) - 1)
            \SetPoseParameter('move_y',       (ply\GetPoseParameter('move_y')     * 2) - 1)
            \SetPoseParameter('move_yaw',     (ply\GetPoseParameter('move_yaw')   * 360) - 180)
            \SetPoseParameter('body_yaw',     (ply\GetPoseParameter('body_yaw')   * 180) - 90)
            \SetPoseParameter('spine_yaw',    (ply\GetPoseParameter('spine_yaw')  * 180) - 90)

        if ply\InVehicle()
            veh = ply\GetVehicle()
            vehAng = veh\GetAngles()
            eyepos = EyePos()
            vehAng\RotateAroundAxis(vehAng\Up(), 90)

            clipAng = Angle(vehAng.p, vehAng.y, vehAng.r)
            clipAng\RotateAroundAxis(clipAng\Right(), -90)

            @legsClipPlane = clipAng\Forward()
            @legsModel\SetRenderAngles(vehAng)

            legClipPlanePos = Vector(0, 0, @@LEG_CLIP_OFFSET_VEHICLE)
            legClipPlanePos\Rotate(vehAng)
            @legClipPlanePos = eyepos - legClipPlanePos

            drawPos = Vector(@@LEG_SHIFT_CONST_VEHICLE, 0, @@LEG_Z_CONST_VEHICLE)
            drawPos\Rotate(vehAng)
            @legsModel\SetRenderOrigin(eyepos - drawPos)
        else
            @legsClipPlane = @@LEG_CLIP_VECTOR
            eangles = EyeAngles()
            yaw = eangles.y - ply\GetPoseParameter('head_yaw') * 180 + 90
            newAng = Angle(0, yaw, 0)
            rad = math.rad(yaw)
            sin, cos = math.sin(rad), math.cos(rad)
            pos = ply\GetPos()
            {:x, :y, :z} = pos
            newPos = Vector(x - cos * @@LEG_SHIFT_CONST, y - sin * @@LEG_SHIFT_CONST, z + @@LEG_Z_CONST)
            if ply\Crouching()
                @duckOffsetHack = @@LEG_CLIP_OFFSET_DUCK
            else
                @duckOffsetHack = Lerp(0.1, @duckOffsetHack, @@LEG_CLIP_OFFSET_STAND)

            @legClipPlanePos = Vector(x, y, z + @duckOffsetHack)
            @legsModel\SetRenderAngles(newAng)
            @legsModel\SetRenderOrigin(newPos)
        @legClipDot = @legsClipPlane\Dot(@legClipPlanePos)

    @LEG_CLIP_VECTOR = Vector(0, 0, -1)
    DrawLegs: (start3D = true) =>
        return if not @isValid
        @CreateLegs() unless IsValid(@legsModel)
        return unless IsValid(@legsModel)
        return if @ent\ShouldDrawLocalPlayer()
        @UpdateLegs()

        oldClip = render.EnableClipping(true)
        render.PushCustomClipPlane(@legsClipPlane, @legClipDot)
        cam.Start3D() if start3D

        if ENABLE_FLASHLIGHT_PASS\GetBool()
            render.PushFlashlightMode(true)
            @GetTextureController()\PreDrawLegs()
            @legsModel\DrawModel()
            @GetTextureController()\PostDrawLegs()
            render.PopFlashlightMode()

        @GetTextureController()\PreDrawLegs()
        @legsModel\DrawModel()
        @GetTextureController()\PostDrawLegs()

        render.PopCustomClipPlane()
        cam.End3D() if start3D
        render.EnableClipping(oldClip)

    IsValid: => IsValid(@ent) and @isValid
    Remove: =>
        @flexes\Remove() if @flexes
        @GetTextureController()\Remove() if @GetTextureController and @GetTextureController()
        @isValid = false

    PlayerRespawn: =>
        return if not @isValid
        @flexes\PlayerRespawn() if @flexes
        @GetTextureController()\ResetTextures() if @GetTextureController()

    DrawModels: =>
        @socksModel\DrawModel() if IsValid(@socksModel)

    PreDraw: (ent = @ent) =>
        return if not @isValid
        @GetTextureController()\PreDraw(ent)
        @flexes\Think(ent) if @flexes
    PostDraw: (ent = @ent) =>
        return if not @isValid
        @GetTextureController()\PostDraw(ent)

    @ARMS_MATERIAL_INDEX = 0
    PreDrawArms: (ent) =>
        return if not @isValid
        if ent and not @armsWeightSetup
            @armsWeightSetup = true
            weight = @GetData()\GetWeight()
            vec = Vector(weight, weight, weight)
            for i = 1, 13
                ent\ManipulateBoneScale(i, vec)
        render.MaterialOverrideByIndex(@@ARMS_MATERIAL_INDEX, @GetTextureController()\GetBody())
    PostDrawArms: (ent) =>
        return if not @isValid
        render.MaterialOverrideByIndex(@@ARMS_MATERIAL_INDEX)

    DataChanges: (state) =>
        return if not @isValid
        return if not @ent
        @GetTextureController()\DataChanges(state)
        @flexes\DataChanges(state) if @flexes
        switch state\GetKey()
            when 'Weight'
                @armsWeightSetup = false
                @GetData()\GetWeightController()\UpdateWeight(@legsModel) if IsValid(@legsModel)
            when 'SocksModel'
                @socksModel = @GetData()\GetSocksModel()
                @socksModel\SetNoDraw(false) if IsValid(@socksModel)
                @GetTextureController()\UpdateSocks(@ent, @socksModel) if @GetTextureController() and IsValid(@socksModel)
            when 'NoFlex'
                if state\GetValue()
                    @flexes\ResetSequences() if @flexes
                    @flexes = nil
                else
                    @CreateFlexController()
    GetTextureController: =>
        return @renderController if not @isValid
        if not @renderController
            cls = PPM2.GetTextureController(@modelCached)
            @renderController = cls(@)
        @renderController.ent = @ent
        return @renderController
    CreateFlexController: =>
        return if not @isValid
        return if not @ent\IsPlayer()
        return if @GetData()\GetNoFlex()
        if not @flexes
            cls = PPM2.GetFlexController(@modelCached)
            return if not cls
            @flexes = cls(@)
        @flexes.ent = @ent
        return @flexes
    GetFlexController: => @flexes

class NewPonyRenderController extends PonyRenderController
    @MODELS = {'models/ppm/player_default_base_new.mdl'}

    new: (data) =>
        @upperManeModel = data\GetUpperManeModel()
        @lowerManeModel = data\GetLowerManeModel()
        @tailModel = data\GetTailModel()
        @upperManeModel\SetNoDraw(false) if IsValid(@upperManeModel)
        @lowerManeModel\SetNoDraw(false) if IsValid(@lowerManeModel)
        @tailModel\SetNoDraw(false) if IsValid(@tailModel)
        super(data)
    __tostring: => "[#{@@__name}:#{@objID}|#{@GetData()}]"

    DataChanges: (state) =>
        return if not @ent
        return if not @isValid
        switch state\GetKey()
            when 'UpperManeModel'
                @upperManeModel = @GetData()\GetUpperManeModel()
                @upperManeModel\SetNoDraw(false) if IsValid(@upperManeModel)
                @GetTextureController()\UpdateUpperMane(@ent, @upperManeModel) if @GetTextureController() and IsValid(@upperManeModel)
            when 'LowerManeModel'
                @lowerManeModel = @GetData()\GetLowerManeModel()
                @lowerManeModel\SetNoDraw(false) if IsValid(@lowerManeModel)
                @GetTextureController()\UpdateLowerMane(@ent, @lowerManeModel) if @GetTextureController() and IsValid(@lowerManeModel)
            when 'TailModel'
                @tailModel = @GetData()\GetTailModel()
                @tailModel\SetNoDraw(false) if IsValid(@tailModel)
                @GetTextureController()\UpdateTail(@ent, @tailModel) if @GetTextureController() and IsValid(@tailModel)
        super(state)

    DrawModels: =>
        @upperManeModel\DrawModel() if IsValid(@upperManeModel)
        @lowerManeModel\DrawModel() if IsValid(@lowerManeModel)
        @tailModel\DrawModel() if IsValid(@tailModel)
        super()

PPM2.PonyRenderController = PonyRenderController
PPM2.NewPonyRenderController = NewPonyRenderController
PPM2.GetPonyRenderController = (model = 'models/ppm/player_default_base.mdl') -> PonyRenderController.AVALIABLE_CONTROLLERS[model\lower()] or PonyRenderController
PPM2.GetPonyRendererController = PPM2.GetPonyRenderController
PPM2.GetRenderController = PPM2.GetPonyRenderController
PPM2.GetRendererController = PPM2.GetPonyRenderController


clue: add silly "bankid" style word generator

Former-commit-id: 4c5299f2adaf8e929d2265ee259e29cd20d74ef5 [formerly 9285e91cea34c0969045d8b1f4dd0f028a60c7c3]
Former-commit-id: c8f946ca4f72e7a9057c9d8c12d74948fb15f312

-- Clue module requires clue data files and tosql.py from emnh/clue2

dbi = require 'DBI'
require'logging.console'
log = logging.console()
conn = false

attrs = {'grammar','reference', 'country', 'context', 'text'}

connect = ->
  conn, err = DBI.Connect('PostgreSQL', ivar2.config.dbname, ivar2.config.dbuser, ivar2.config.dbpass, ivar2.config.dbhost, ivar2.config.dbport)
  unless conn
    log\error "Unable to connect to DB: #{err}"
    return

  conn\autocommit(true)

dbh = ->
  connect! unless conn

  -- Check if connection is alive
  alive = conn\ping!
  connect! unless alive

  success, err = DBI.Do(conn, 'SELECT NOW()')
  unless success
    log\error "SQL Connection :#{err}"
    connect!

  return conn

lookup = (source, destination, lookup) =>
  stmt = dbh!\prepare [[
    SELECT
      tablename
    from
      pg_catalog.pg_tables
    where
      tablename like 'cl%'
    ]]
  stmt\execute!
  tables = {}
  for row in stmt\rows(true) -- true for column names
    tables[#tables+1] = row.tablename

  out = {}
  for dict in *tables
    out[dict] = {}
    stmt = dbh!\prepare [[
      SELECT
        *
      FROM
        ]]..dict..[[
      WHERE
        word = ?
      ]]
    stmt\execute lookup
    for row in stmt\rows(true) -- true for column names
      table.insert(out[dict], row)

  i = 0
  for dict, rows in pairs out
    res = {}
    dfrom = dict\sub(3, 4)
    to = dict\sub(5, 6)
    first = true
    for row in *rows
      i += 1
      if first
        table.insert(res, "[#{ivar2.util.bold row.word}]")
        table.insert(res, "(#{ivar2.util.italic dfrom}")
        table.insert(res, "-> #{ivar2.util.italic to})")
        first = false
      for attr in *attrs
        if row[attr] != nil and row[attr] != ''
          if attr == 'grammar'
            table.insert(res, "(#{row[attr]}. )")
          else
            table.insert(res, row[attr])
    say table.concat(res, ' ')
  if i == 0
    reply 'Nope.'

upper = (s) ->
  (string.upper(s)\gsub('æ','Æ')\gsub('ø','Ø')\gsub('å','Å'))

getWord = (wordclass) ->
  stmt = dbh!\prepare [[
    SELECT
     *
    FROM
      clnono
    WHERE
      grammar = ?
    AND
      length(word) < 8
    ORDER BY
      random()
    LIMIT 1
  ]]
  stmt\execute wordclass
  out = {}
  for row in stmt\rows(true) -- true for column names
    table.insert(out, row)
  return out

bankid = (source, destination) =>

  adj = getWord 'adj'
  sub = getWord 's'

  say "#{upper adj[1].word} #{upper sub[1].word}"

PRIVMSG:
  '^%pclue (.+)$': lookup
  '^%pbankid$': bankid

-- Clue module requires clue data files and tosql.py from emnh/clue2

dbi = require 'DBI'
require'logging.console'
log = logging.console()
conn = false

attrs = {'grammar','reference', 'country', 'context', 'text'}

connect = ->
  conn, err = DBI.Connect('PostgreSQL', ivar2.config.dbname, ivar2.config.dbuser, ivar2.config.dbpass, ivar2.config.dbhost, ivar2.config.dbport)
  unless conn
    log\error "Unable to connect to DB: #{err}"
    return

  conn\autocommit(true)

dbh = ->
  connect! unless conn

  -- Check if connection is alive
  alive = conn\ping!
  connect! unless alive

  success, err = DBI.Do(conn, 'SELECT NOW()')
  unless success
    log\error "SQL Connection :#{err}"
    connect!

  return conn

lookup = (source, destination, lookup) =>
  stmt = dbh!\prepare [[
    SELECT
      tablename
    from
      pg_catalog.pg_tables
    where
      tablename like 'cl%'
    ]]
  stmt\execute!
  tables = {}
  for row in stmt\rows(true) -- true for column names
    tables[#tables+1] = row.tablename

  out = {}
  for dict in *tables
    out[dict] = {}
    stmt = dbh!\prepare [[
      SELECT
        *
      FROM
        ]]..dict..[[
      WHERE
        word = ?
      ]]
    stmt\execute lookup
    for row in stmt\rows(true) -- true for column names
      table.insert(out[dict], row)

  i = 0
  for dict, rows in pairs out
    res = {}
    dfrom = dict\sub(3, 4)
    to = dict\sub(5, 6)
    first = true
    for row in *rows
      i += 1
      if first
        table.insert(res, "[#{ivar2.util.bold row.word}]")
        table.insert(res, "(#{ivar2.util.italic dfrom}")
        table.insert(res, "-> #{ivar2.util.italic to})")
        first = false
      for attr in *attrs
        if row[attr] != nil and row[attr] != ''
          if attr == 'grammar'
            table.insert(res, "(#{row[attr]}. )")
          else
            table.insert(res, row[attr])
    say table.concat(res, ' ')
  if i == 0
    reply 'Nope.'

PRIVMSG:
  '^%pclue (.+)$': lookup


github: add event review

github: add event review

util = require 'util'
hmac = require "openssl.hmac" -- from luaossl

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

tohex = (b) ->
  x = ""
  for i = 1, #b do
    x = x .. string.format("%.2x", string.byte(b, i))
  return 'sha1='..x

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

acolor = (action) ->
  if action == 'opened'
    return util.green action
  if action == 'closed'
    return util.red action
  if action == 'deleted'
    return util.red action
  if action == 'synchronize'
    return util.purple action
  if action == 'labeled'
    return util.yellow action

  return action

strip_email = (text) ->
  out = {}
  for line in text\gmatch'[^\r\n]+'
    unless line\match'^%s*>'
      out[#out+1] = line
  table.concat out

handlers = {
  push: (repo, destination, json) ->
    branch = util.bold(json.ref\gsub 'refs/heads/', '')
    out = {"[#{repo}]: #{branch}"}
    lastauthor = ''
    for i, c in ipairs json.commits
      if i > 3
        break
      message = c.message\gsub '\n.*', ''
      if i == 1
        message ..= ' ' .. json.compare
      author = ''
      if c.author.username ~= lastauthor
        author = "<#{util.nonickalertword c.author.username}>"
      out[#out+1] = "#{author} #{message}\n"
      lastauthor = c.author.username
    if #json.commits >= 4
      out[#out+1] = "#{#json.commits-3} more commits not displayed."
    table.concat out, ' '
  watch: (repo, destination, json) ->
    action = json.action
    if action == 'started'
      "[#{repo}]: #{util.nonickalertword json.sender.login}: +1 #{util.yellow '★'} (Total: #{json.repository.stargazers_count})"
    else
      "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} watching"
  ping: (repo, destination, json) ->
    "[#{repo}]: ping"
  commit_comment: (repo, destination, json) ->
    --commenter = json.comment.user.login
    body = json.comment.body
    "[#{repo}]: <#{util.nonickalertword json.sender.login}> #{body}"
  pull_request: (repo, destination, json) ->
    action = acolor json.action
    number = util.bold json.number
    "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} PR ##{number}: #{json.pull_request.title} #{json.pull_request.html_url}"
  pull_request_review_comment: (repo, destination, json) ->
    body = json.comment.body
    number = util.bold json.pull_request.number
    "[#{repo}]: PR ##{number} <#{util.nonickalertword json.sender.login}> #{body}"
  pull_request_review: (repo, destinatino, json) ->
    body = json.review.body
    number = util.bold json.pull_request.number
    state = json.review.state\gsub('_', ' ')
    "[#{repo}]: PR ##{number} Review #{state} <#{util.nonickalertword json.sender.login}> #{body}"
  issues: (repo, destination, json) ->
    action = acolor json.action
    nr = util.bold json.issue.number
    extra = ''
    if json.action == 'labeled' -- Add label and color if action is label
      extra = "#{json.label.name}, ##{json.label.color} "
    "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} issue ##{nr}: #{extra}#{json.issue.title} #{json.issue.html_url}"
  issue_comment: (repo, destination, json) ->
    action = acolor json.action
    nr = util.bold json.issue.number
    "[#{repo}]: Issue ##{nr} <#{util.nonickalertword json.sender.login}> #{strip_email json.comment.body} #{json.issue.html_url}"
  fork: (repo, destination, json) ->
    "[#{repo}]: #{util.nonickalertword json.sender.login}: forked to #{json.forkee.html_url}"
  create: (repo, destination, json) ->
    "[#{repo}]: #{util.nonickalertword json.sender.login}: created #{json.ref_type} #{util.bold json.ref}"
  delete: (repo, destination, json) ->
    "[#{repo}]: #{util.nonickalertword json.sender.login}: deleted #{json.ref_type} #{util.bold json.ref}"
  gollum: (repo, destination, json) ->
    return
  release: (repo, destination, json) ->
    return
  status: (repo, destination, json) -> -- Travis CI event
    -- NYI
    return

}

ivar2.webserver.regUrl '/github/(.*)', (req, res) =>
  destination = req.url\match('/github/(.+)/?$')
  destination = unescape(destination)
  json = util.json.decode(req.body)

  repo = json.repository.full_name

  secret = req.headers['x-hub-signature']
  if secret
    confsecret = ivar2.config.channels[destination].githubSecret
    if not confsecret
      ivar2\Log('error', 'github event, no secret configured for destination: %s', destination)
      return
    mysum = tohex(hmac.new(confsecret)\final(req.body))
    if tostring(mysum) != tostring(secret)
      ivar2\Log('error', 'github event with invalid signature for destination: %s', destination)
      res\append ':status', "403"
      res\append 'Content-Type', 'text/plain'
      req\write_headers(res, false, 30)
      req\write_body_from_string('invalid secret', 30)
      return

  event = req.headers['x-github-event']
  handler = handlers[event]
  if handler
    if message = handler(repo, destination, json)
      ivar2\Msg 'privmsg', destination, nil, message
  else
    ivar2\Log('error', 'Unknown github event: %s', event)

  res\append ':status', "200"
  res\append 'Content-Type', 'text/plain'
  req\write_headers(res, false, 30)
  req\write_body_from_string('ok', 30)

util = require 'util'
hmac = require "openssl.hmac" -- from luaossl

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

tohex = (b) ->
  x = ""
  for i = 1, #b do
    x = x .. string.format("%.2x", string.byte(b, i))
  return 'sha1='..x

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

acolor = (action) ->
  if action == 'opened'
    return util.green action
  if action == 'closed'
    return util.red action
  if action == 'deleted'
    return util.red action
  if action == 'synchronize'
    return util.purple action
  if action == 'labeled'
    return util.yellow action

  return action

strip_email = (text) ->
  out = {}
  for line in text\gmatch'[^\r\n]+'
    unless line\match'^%s*>'
      out[#out+1] = line
  table.concat out

handlers = {
  push: (repo, destination, json) ->
    branch = util.bold(json.ref\gsub 'refs/heads/', '')
    out = {"[#{repo}]: #{branch}"}
    lastauthor = ''
    for i, c in ipairs json.commits
      if i > 3
        break
      message = c.message\gsub '\n.*', ''
      if i == 1
        message ..= ' ' .. json.compare
      author = ''
      if c.author.username ~= lastauthor
        author = "<#{util.nonickalertword c.author.username}>"
      out[#out+1] = "#{author} #{message}\n"
      lastauthor = c.author.username
    if #json.commits >= 4
      out[#out+1] = "#{#json.commits-3} more commits not displayed."
    table.concat out, ' '
  watch: (repo, destination, json) ->
    action = json.action
    if action == 'started'
      "[#{repo}]: #{util.nonickalertword json.sender.login}: +1 #{util.yellow '★'} (Total: #{json.repository.stargazers_count})"
    else
      "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} watching"
  ping: (repo, destination, json) ->
    "[#{repo}]: ping"
  commit_comment: (repo, destination, json) ->
    --commenter = json.comment.user.login
    body = json.comment.body
    "[#{repo}]: <#{util.nonickalertword json.sender.login}> #{body}"
  pull_request: (repo, destination, json) ->
    action = acolor json.action
    number = util.bold json.number
    "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} PR ##{number}: #{json.pull_request.title} #{json.pull_request.html_url}"
  pull_request_review_comment: (repo, destination, json) ->
    body = json.comment.body
    number = util.bold json.pull_request.number
    "[#{repo}]: PR ##{number} <#{util.nonickalertword json.sender.login}> #{body}"
  issues: (repo, destination, json) ->
    action = acolor json.action
    nr = util.bold json.issue.number
    extra = ''
    if json.action == 'labeled' -- Add label and color if action is label
      extra = "#{json.label.name}, ##{json.label.color} "
    "[#{repo}]: #{util.nonickalertword json.sender.login}: #{action} issue ##{nr}: #{extra}#{json.issue.title} #{json.issue.html_url}"
  issue_comment: (repo, destination, json) ->
    action = acolor json.action
    nr = util.bold json.issue.number
    "[#{repo}]: Issue ##{nr} <#{util.nonickalertword json.sender.login}> #{strip_email json.comment.body} #{json.issue.html_url}"
  fork: (repo, destination, json) ->
    "[#{repo}]: #{util.nonickalertword json.sender.login}: forked to #{json.forkee.html_url}"
  create: (repo, destination, json) ->
    "[#{repo}]: #{util.nonickalertword json.sender.login}: created #{json.ref_type} #{util.bold json.ref}"
  delete: (repo, destination, json) ->
    "[#{repo}]: #{util.nonickalertword json.sender.login}: deleted #{json.ref_type} #{util.bold json.ref}"
  gollum: (repo, destination, json) ->
    return
  release: (repo, destination, json) ->
    return
  status: (repo, destination, json) -> -- Travis CI event
    -- NYI
    return

}

ivar2.webserver.regUrl '/github/(.*)', (req, res) =>
  destination = req.url\match('/github/(.+)/?$')
  destination = unescape(destination)
  json = util.json.decode(req.body)

  repo = json.repository.full_name

  secret = req.headers['x-hub-signature']
  if secret
    confsecret = ivar2.config.channels[destination].githubSecret
    if not confsecret
      ivar2\Log('error', 'github event, no secret configured for destination: %s', destination)
      return
    mysum = tohex(hmac.new(confsecret)\final(req.body))
    if tostring(mysum) != tostring(secret)
      ivar2\Log('error', 'github event with invalid signature for destination: %s', destination)
      res\append ':status', "403"
      res\append 'Content-Type', 'text/plain'
      req\write_headers(res, false, 30)
      req\write_body_from_string('invalid secret', 30)
      return

  event = req.headers['x-github-event']
  handler = handlers[event]
  if handler
    if message = handler(repo, destination, json)
      ivar2\Msg 'privmsg', destination, nil, message
  else
    ivar2\Log('error', 'Unknown github event: %s', event)

  res\append ':status', "200"
  res\append 'Content-Type', 'text/plain'
  req\write_headers(res, false, 30)
  req\write_body_from_string('ok', 30)


Fix TriggerLerpAll generate garbage and tank FPS when lerp suddenly return NaN

--
-- Copyright (C) 2017-2020 DBotThePony

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

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

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

-- uuuuuuuuugh
Lerp = (byVal, fromValue, intoValue) ->
	delta = intoValue - fromValue
	if delta < 0 and delta > -0.025
		return intoValue
	elseif delta > 0 and delta < 0.025
		return intoValue
	return fromValue + delta * byVal

class PPM2.ModifierBase
	@MODIFIERS = {}
	@SetupModifiers: =>
	@__inherited: (child) =>
		child.MODIFIERS = {}
		child\SetupModifiers()

	@RegisterModifier: (modifName = 'MyModifier', def = 0, calculateStart = 0) =>
		iName = modifName .. 'Modifiers'
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.def = def
				return

		targetTable = {
			name: modifName
			:def, :iName
			clamp: (val) -> val
			clampFinal: (val) -> val
			lerpFunc: Lerp
			:calculateStart
		}

		targetTable.def = (-> def) if type(def) ~= 'function'
		targetTable.calculateStart = (-> calculateStart) if type(calculateStart) ~= 'function'

		table.insert(@MODIFIERS, targetTable)

		@__base['SetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			return if not @[iName][modifID]
			@[iName][modifID] = targetTable.clamp(val)

		@__base['GetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			if targetTable.isLerped
				return @[targetTable.iNameLerp][modifID]
			else
				return @[iName][modifID]

		@__base['Calculate' .. modifName] = (inputAdd) =>
			calc = targetTable.calculateStart()
			calc += inputAdd if inputAdd
			if targetTable.isLerped and @[targetTable.iNameLerp]
				calc += modif for _, modif in ipairs @[targetTable.iNameLerp]
			elseif @[iName]
				calc += modif for _, modif in ipairs @[iName]
			return targetTable.clampFinal(calc)

	@SetModifierMinMax: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.mins = mins
				data.maxs = maxs
				if not mins and not maxs
					data.clamp = (val) -> val
				elseif not mins
					data.clamp = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clamp = (val) -> math.max(val, mins)
				else
					data.clamp = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	@SetModifierMinMaxFinal: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.minsFinal = mins
				data.maxsFinal = maxs
				if not mins and not maxs
					data.clampFinal = (val) -> val
				elseif not mins
					data.clampFinal = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clampFinal = (val) -> math.max(val, mins)
				else
					data.clampFinal = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	RegisterModifier: (modifName = 'MyModifier', def = 0, calculateStart = 0) =>
		iName = modifName .. 'Modifiers'
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.def = def
				return

		targetTable = {
			name: modifName
			:def, :iName
			clamp: (val) -> val
			clampFinal: (val) -> val
			lerpFunc: Lerp
			:calculateStart
		}

		targetTable.def = (-> def) if type(def) ~= 'function'
		targetTable.calculateStart = (-> calculateStart) if type(calculateStart) ~= 'function'

		table.insert(@CUSTOM_MODIFIERS, targetTable)
		@[iName] = {}

		@['SetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			return if not @[iName][modifID]
			@[iName][modifID] = targetTable.clamp(val)

		@['GetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			if targetTable.isLerped
				return @[targetTable.iNameLerp][modifID]
			else
				return @[iName][modifID]

		@['GetRawModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			return @[iName][modifID]

		@['Calculate' .. modifName] = (inputAdd) =>
			calc = targetTable.calculateStart()
			calc += inputAdd if inputAdd
			if targetTable.isLerped and @[targetTable.iNameLerp]
				calc += modif for i, modif in ipairs @[targetTable.iNameLerp]
			elseif @[iName]
				calc += modif for i, modif in ipairs @[iName]
			return targetTable.clampFinal(calc)

	SetModifierMinMax: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.mins = mins
				data.maxs = maxs
				if not mins and not maxs
					data.clamp = (val) -> val
				elseif not mins
					data.clamp = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clamp = (val) -> math.max(val, mins)
				else
					data.clamp = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	SetModifierMinMaxFinal: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.minsFinal = mins
				data.maxsFinal = maxs
				if not mins and not maxs
					data.clampFinal = (val) -> val
				elseif not mins
					data.clampFinal = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clampFinal = (val) -> math.max(val, mins)
				else
					data.clamp = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	@SetupLerpTables: (modifName = 'MyModifier') =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.isLerped = true
				data.iNameLerp = data.iName .. 'Lerp'
				return true
		return false

	SetupLerpTables: (modifName = 'MyModifier') =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.isLerped = true
				data.lerpTable = {k, v for k, v in pairs @[data.iName]}
				data.iNameLerp = data.iName .. 'Lerp'
				@[data.iNameLerp] = data.lerpTable
				return true, data.lerpTable
		return false

	@SetLerpFunc: (modifName = 'MyModifier', func = Lerp) =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.lerpFunc = func
				return true
		return false

	SetLerpFunc: (modifName = 'MyModifier', func = Lerp) =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.lerpFunc = func
				return true
		return false

	TriggerLerp: (modifName = 'MyModifier', lerpBy = 0.5) =>
		for _, modif in ipairs @CUSTOM_MODIFIERS
			if modif.name == modifName
				@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id]) for id = 1, #@[modif.iNameLerp] when @[modif.iNameLerp][id] ~= @[modif.iName][id]
				return true
		for _, modif in ipairs @@MODIFIERS
			if modif.name == modifName
				@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id]) for id = 1, #@[modif.iNameLerp] when @[modif.iNameLerp][id] ~= @[modif.iName][id]
				return true
		return false

	TriggerLerpAll: (lerpBy = 0.5) =>
		outputTriggered = {}

		for modif in *@CUSTOM_MODIFIERS
			if modif.iNameLerp
				for id = 1, #@[modif.iNameLerp]
					if @[modif.iNameLerp][id] ~= @[modif.iName][id]
						@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id])
						@[modif.iNameLerp][id] = @[modif.iName][id] if isnumber(@[modif.iNameLerp][id]) and @[modif.iNameLerp][id] ~= @[modif.iNameLerp][id]
						table.insert(outputTriggered, {modif.name, @[modif.iNameLerp][id]})

		for modif in *@@MODIFIERS
			if modif.iNameLerp
				for id = 1, #@[modif.iNameLerp]
					if @[modif.iNameLerp][id] ~= @[modif.iName][id]
						@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id])
						@[modif.iNameLerp][id] = @[modif.iName][id] if isnumber(@[modif.iNameLerp][id]) and @[modif.iNameLerp][id] ~= @[modif.iNameLerp][id]
						table.insert(outputTriggered, {modif.name, @[modif.iNameLerp][id]})

		return outputTriggered

	@ClearModifiers: =>
		for _, modif in ipairs @MODIFIERS
			@__base['SetModifier' .. modif.name] = nil
			@__base['Calculate' .. modif.name] = nil
		@MODIFIERS = {}

	ClearModifiers: =>
		for _, modif in ipairs @CUSTOM_MODIFIERS
			@[modif.iName] = nil
			@['SetModifier' .. modif.name] = nil
			@['Calculate' .. modif.name] = nil
		@CUSTOM_MODIFIERS = {}

	new: =>
		@CUSTOM_MODIFIERS = {}
		for _, modif in ipairs @@MODIFIERS
			@[modif.iName] = {}
			@[modif.iNameLerp] = {} if modif.iNameLerp
		@modifiersNames = {}
		@nextModifierID = 0

	GetModifierID: (name = '') =>
		return @modifiersNames[name] if @modifiersNames[name]
		@nextModifierID += 1
		id = @nextModifierID
		@modifiersNames[name] = id
		@[modif.iName][id] = modif.def() for _, modif in ipairs @@MODIFIERS
		@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @@MODIFIERS when modif.iNameLerp
		@[modif.iName][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS
		@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS when modif.iNameLerp
		return id

	ResetModifiers: (name = '', hard = false) =>
		return false if not @modifiersNames[name]
		id = @modifiersNames[name]
		@[modif.iName][id] = modif.def() for _, modif in ipairs @@MODIFIERS
		@[modif.iName][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS
		if hard
			@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @@MODIFIERS when modif.iNameLerp
			@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS when modif.iNameLerp
		return true

--
-- Copyright (C) 2017-2020 DBotThePony

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

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

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

-- uuuuuuuuugh
Lerp = (byVal, fromValue, intoValue) ->
	delta = intoValue - fromValue
	if delta < 0 and delta > -0.025
		return intoValue
	elseif delta > 0 and delta < 0.025
		return intoValue
	return fromValue + delta * byVal

class PPM2.ModifierBase
	@MODIFIERS = {}
	@SetupModifiers: =>
	@__inherited: (child) =>
		child.MODIFIERS = {}
		child\SetupModifiers()

	@RegisterModifier: (modifName = 'MyModifier', def = 0, calculateStart = 0) =>
		iName = modifName .. 'Modifiers'
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.def = def
				return

		targetTable = {
			name: modifName
			:def, :iName
			clamp: (val) -> val
			clampFinal: (val) -> val
			lerpFunc: Lerp
			:calculateStart
		}

		targetTable.def = (-> def) if type(def) ~= 'function'
		targetTable.calculateStart = (-> calculateStart) if type(calculateStart) ~= 'function'

		table.insert(@MODIFIERS, targetTable)

		@__base['SetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			return if not @[iName][modifID]
			@[iName][modifID] = targetTable.clamp(val)

		@__base['GetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			if targetTable.isLerped
				return @[targetTable.iNameLerp][modifID]
			else
				return @[iName][modifID]

		@__base['Calculate' .. modifName] = (inputAdd) =>
			calc = targetTable.calculateStart()
			calc += inputAdd if inputAdd
			if targetTable.isLerped and @[targetTable.iNameLerp]
				calc += modif for _, modif in ipairs @[targetTable.iNameLerp]
			elseif @[iName]
				calc += modif for _, modif in ipairs @[iName]
			return targetTable.clampFinal(calc)

	@SetModifierMinMax: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.mins = mins
				data.maxs = maxs
				if not mins and not maxs
					data.clamp = (val) -> val
				elseif not mins
					data.clamp = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clamp = (val) -> math.max(val, mins)
				else
					data.clamp = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	@SetModifierMinMaxFinal: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.minsFinal = mins
				data.maxsFinal = maxs
				if not mins and not maxs
					data.clampFinal = (val) -> val
				elseif not mins
					data.clampFinal = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clampFinal = (val) -> math.max(val, mins)
				else
					data.clampFinal = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	RegisterModifier: (modifName = 'MyModifier', def = 0, calculateStart = 0) =>
		iName = modifName .. 'Modifiers'
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.def = def
				return

		targetTable = {
			name: modifName
			:def, :iName
			clamp: (val) -> val
			clampFinal: (val) -> val
			lerpFunc: Lerp
			:calculateStart
		}

		targetTable.def = (-> def) if type(def) ~= 'function'
		targetTable.calculateStart = (-> calculateStart) if type(calculateStart) ~= 'function'

		table.insert(@CUSTOM_MODIFIERS, targetTable)
		@[iName] = {}

		@['SetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			return if not @[iName][modifID]
			@[iName][modifID] = targetTable.clamp(val)

		@['GetModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			if targetTable.isLerped
				return @[targetTable.iNameLerp][modifID]
			else
				return @[iName][modifID]

		@['GetRawModifier' .. modifName] = (modifID, val = 0) =>
			return if not modifID
			return @[iName][modifID]

		@['Calculate' .. modifName] = (inputAdd) =>
			calc = targetTable.calculateStart()
			calc += inputAdd if inputAdd
			if targetTable.isLerped and @[targetTable.iNameLerp]
				calc += modif for i, modif in ipairs @[targetTable.iNameLerp]
			elseif @[iName]
				calc += modif for i, modif in ipairs @[iName]
			return targetTable.clampFinal(calc)

	SetModifierMinMax: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.mins = mins
				data.maxs = maxs
				if not mins and not maxs
					data.clamp = (val) -> val
				elseif not mins
					data.clamp = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clamp = (val) -> math.max(val, mins)
				else
					data.clamp = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	SetModifierMinMaxFinal: (modifName = 'MyModifier', mins, maxs) =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.minsFinal = mins
				data.maxsFinal = maxs
				if not mins and not maxs
					data.clampFinal = (val) -> val
				elseif not mins
					data.clampFinal = (val) -> math.min(val, maxs)
				elseif not maxs
					data.clampFinal = (val) -> math.max(val, mins)
				else
					data.clamp = (val) -> math.Clamp(val, mins, maxs)
				return true
		return false

	@SetupLerpTables: (modifName = 'MyModifier') =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.isLerped = true
				data.iNameLerp = data.iName .. 'Lerp'
				return true
		return false

	SetupLerpTables: (modifName = 'MyModifier') =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.isLerped = true
				data.lerpTable = {k, v for k, v in pairs @[data.iName]}
				data.iNameLerp = data.iName .. 'Lerp'
				@[data.iNameLerp] = data.lerpTable
				return true, data.lerpTable
		return false

	@SetLerpFunc: (modifName = 'MyModifier', func = Lerp) =>
		for _, data in ipairs @MODIFIERS
			if data.name == modifName
				data.lerpFunc = func
				return true
		return false

	SetLerpFunc: (modifName = 'MyModifier', func = Lerp) =>
		for _, data in ipairs @CUSTOM_MODIFIERS
			if data.name == modifName
				data.lerpFunc = func
				return true
		return false

	TriggerLerp: (modifName = 'MyModifier', lerpBy = 0.5) =>
		for _, modif in ipairs @CUSTOM_MODIFIERS
			if modif.name == modifName
				@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id]) for id = 1, #@[modif.iNameLerp] when @[modif.iNameLerp][id] ~= @[modif.iName][id]
				return true
		for _, modif in ipairs @@MODIFIERS
			if modif.name == modifName
				@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id]) for id = 1, #@[modif.iNameLerp] when @[modif.iNameLerp][id] ~= @[modif.iName][id]
				return true
		return false

	TriggerLerpAll: (lerpBy = 0.5) =>
		outputTriggered = {}
		for _, modif in ipairs @CUSTOM_MODIFIERS
			if modif.iNameLerp
				for id = 1, #@[modif.iNameLerp]
					if @[modif.iNameLerp][id] ~= @[modif.iName][id]
						@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id])
						table.insert(outputTriggered, {modif.name, @[modif.iNameLerp][id]})
		for _, modif in ipairs @@MODIFIERS
			if modif.iNameLerp
				for id = 1, #@[modif.iNameLerp]
					if @[modif.iNameLerp][id] ~= @[modif.iName][id]
						@[modif.iNameLerp][id] = modif.lerpFunc(lerpBy, @[modif.iNameLerp][id], @[modif.iName][id])
						table.insert(outputTriggered, {modif.name, @[modif.iNameLerp][id]})
		return outputTriggered

	@ClearModifiers: =>
		for _, modif in ipairs @MODIFIERS
			@__base['SetModifier' .. modif.name] = nil
			@__base['Calculate' .. modif.name] = nil
		@MODIFIERS = {}

	ClearModifiers: =>
		for _, modif in ipairs @CUSTOM_MODIFIERS
			@[modif.iName] = nil
			@['SetModifier' .. modif.name] = nil
			@['Calculate' .. modif.name] = nil
		@CUSTOM_MODIFIERS = {}

	new: =>
		@CUSTOM_MODIFIERS = {}
		for _, modif in ipairs @@MODIFIERS
			@[modif.iName] = {}
			@[modif.iNameLerp] = {} if modif.iNameLerp
		@modifiersNames = {}
		@nextModifierID = 0

	GetModifierID: (name = '') =>
		return @modifiersNames[name] if @modifiersNames[name]
		@nextModifierID += 1
		id = @nextModifierID
		@modifiersNames[name] = id
		@[modif.iName][id] = modif.def() for _, modif in ipairs @@MODIFIERS
		@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @@MODIFIERS when modif.iNameLerp
		@[modif.iName][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS
		@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS when modif.iNameLerp
		return id

	ResetModifiers: (name = '', hard = false) =>
		return false if not @modifiersNames[name]
		id = @modifiersNames[name]
		@[modif.iName][id] = modif.def() for _, modif in ipairs @@MODIFIERS
		@[modif.iName][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS
		if hard
			@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @@MODIFIERS when modif.iNameLerp
			@[modif.iNameLerp][id] = modif.def() for _, modif in ipairs @CUSTOM_MODIFIERS when modif.iNameLerp
		return true