fix voltmeter

fix voltmeter

-- see http://www.instructables.com/id/ESP8266-with-Multiple-Analog-Sensors/
-- for details to connect multiple sensors
Switch = require("sw").Switch

class Voltmeter
  @v: 3.3
  @r1: 100000
  @r2: 10000
  @ratio: 1 + (@r1 / @r2)

  new: (enablePin) =>
    @enable = Switch(enablePin)
    adc.force_init_mode adc.INIT_ADC

  value: =>
    @enable\on()
    ret = adc.read(0)
    @enable\off()
    return @@ratio * ret * @@v / 1024

return Voltmeter

-- see http://www.instructables.com/id/ESP8266-with-Multiple-Analog-Sensors/
-- for details to connect multiple sensors
Switch = require("sw").Switch

class Voltmeter
  @v: 3.3

  new: (enablePin) =>
    @enable = Switch(enablePin)
    adc.force_init_mode adc.INIT_ADC

  value: =>
    @enable\on()
    ret = adc.read(0)
    @enable\off()
    return ret * @@v / 1024

return Voltmeter


Some LP.scene changes

Some LP.scene changes

* Give LP.scene a sane default.
* Add a private __scene to check against for when scene is changed.
* Change gotoIsNull to sceneIsNull for above check.
* Implemented swapScene (which more accurately updates __scene)

require "LovePunk.Scene"
require "LovePunk.Tweener"

export ^

class lp
	-- Private
	BASELAYER = 10
	DEG = nil
	INT_MAX_VALUE = nil
	NUMBER_MAX_VALUE = nil
	RAD = nil
	VERSION = "0.0.1"
	__scene = nil

	_gameTime: nil
	_renderTime: nil
	_systemTime: nil
	_updateTime: nil
	assignedFrameRate: nil
	blackColor: 0x00000000
	bounds: nil
	buffer: nil
	camera: nil
	console: nil
	defaultFont: "default"
	elapsed: 0
	engine: nil
	entity: nil
	fixed: false
	focused: nil
	frameRate: 0
	fullscreen: nil
	halfHeight: nil
	halfWidth: nil
	height: nil
	log: nil
	matrix: nil
	orientations: nil
	pan: nil
	point: nil
	point2: nil
	random: -> math.random!
	randomSeed: nil
	rate: 0
	rect: nil
	renderMode: nil
	scene: Scene!
	screen: nil
	sprite: nil
	stage: nil
	tweener: Tweener!
	volume: nil
	watch: nil
	width: nil
	windowHeight: nil
	windowWidth: nil
	world: nil
	zero: nil

	alarm: (delay, complete, typeweenType, tweener) ->
	anchorTo: (object, anchor, distance) ->
	angle: (x1, y1, x2, y2) ->
	angleDifference: (angle1, angle2) ->
	angleXY: (object, angle, length, x, y) ->
	approach: (value, target, amount) ->
	choose: (objs) ->
	clamp: (value, min, max) ->
	clampInRect: (object, x, y, width, height, padding) ->
	clear: (array) ->
	colorLerp: (fromColor, toColor, t) ->
	consoleEnabled: ->
	convertColor: (color) ->
	createBitmap: (width, height, transparent, color) ->
	distance: (x1, y1, x2, y2) ->
	istanceRectPoint: (px, py, rx, ry, rw, rh) ->
	distanceRects: (x1, y1, w1, h1, x2, y2, w2, h2) ->
	distanceSquared: (x1, y1, xprint2, y2) ->
	frames: (from_frame, to_frame, skip) ->
	getBitmap: (source) ->
	getBlue: (color) ->
	getColorHSV: (h, s, v) ->
	getColorHue: (color) ->
	getColorRGB: (R, G, B) ->
	getColorSaturation: (color) ->
	getColorValue: (color) ->
	getGreen: (color) ->
	getRed: (color) ->

	-- read-only
	DEG: ->
		DEG

	-- read-only
	NUMBER_MAX_VALUE: ->
		NUMBER_MAX_VALUE

	-- read-only
	RAD: ->
		RAD

	-- read-only
	VERSION: ->
		VERSION

	sceneIsNull: => @scene != @__scene or @__scene == nil

	lerp: (a, b, t) ->
	next: (current, options, loop) ->
	prev: (current, options, loop) ->

	rand: (amount) ->
		math.random(amount)

	randomizeSeed: ->
		math.randomseed os.time()

	resetCamera: ->
	resize: (width, height) ->
	rotateAround: (object, anchor, angle, relative) ->
	round: (num, precision) ->
	scale: (value, min, max, min2, max2) ->
	scaleClamp: (value, min, max, min2, max2) ->
	setCamera: (x, y) ->
	shuffle: (a) ->
	sign: (value) ->
	stepTowards: (object, x, y, distance) ->
	swap: (current, a, b) ->
	swapScene: => @__scene = @scene
	timeFlag: ->
	tween: (object, values, duration, options) ->

LP = lp!

require "LovePunk.Tweener"

export ^

class lp
	-- Private
	BASELAYER = 10
	DEG = nil
	INT_MAX_VALUE = nil
	NUMBER_MAX_VALUE = nil
	RAD = nil
	VERSION = "0.0.1"

	_gameTime: nil
	_renderTime: nil
	_systemTime: nil
	_updateTime: nil
	assignedFrameRate: nil
	blackColor: 0x00000000
	bounds: nil
	buffer: nil
	camera: nil
	console: nil
	defaultFont: "default"
	elapsed: 0
	engine: nil
	entity: nil
	fixed: false
	focused: nil
	frameRate: 0
	fullscreen: nil
	halfHeight: nil
	halfWidth: nil
	height: nil
	log: nil
	matrix: nil
	orientations: nil
	pan: nil
	point: nil
	point2: nil
	random: -> math.random!
	randomSeed: nil
	rate: 0
	rect: nil
	renderMode: nil
	scene: nil
	screen: nil
	sprite: nil
	stage: nil
	tweener: Tweener!
	volume: nil
	watch: nil
	width: nil
	windowHeight: nil
	windowWidth: nil
	world: nil
	zero: nil

	alarm: (delay, complete, typeweenType, tweener) ->
	anchorTo: (object, anchor, distance) ->
	angle: (x1, y1, x2, y2) ->
	angleDifference: (angle1, angle2) ->
	angleXY: (object, angle, length, x, y) ->
	approach: (value, target, amount) ->
	choose: (objs) ->
	clamp: (value, min, max) ->
	clampInRect: (object, x, y, width, height, padding) ->
	clear: (array) ->
	colorLerp: (fromColor, toColor, t) ->
	consoleEnabled: ->
	convertColor: (color) ->
	createBitmap: (width, height, transparent, color) ->
	distance: (x1, y1, x2, y2) ->
	istanceRectPoint: (px, py, rx, ry, rw, rh) ->
	distanceRects: (x1, y1, w1, h1, x2, y2, w2, h2) ->
	distanceSquared: (x1, y1, xprint2, y2) ->
	frames: (from_frame, to_frame, skip) ->
	getBitmap: (source) ->
	getBlue: (color) ->
	getColorHSV: (h, s, v) ->
	getColorHue: (color) ->
	getColorRGB: (R, G, B) ->
	getColorSaturation: (color) ->
	getColorValue: (color) ->
	getGreen: (color) ->
	getRed: (color) ->

	-- read-only
	DEG: ->
		DEG

	-- read-only
	NUMBER_MAX_VALUE: ->
		NUMBER_MAX_VALUE

	-- read-only
	RAD: ->
		RAD

	-- read-only
	VERSION: ->
		VERSION

	gotoIsNull: ->
		-- TODO STUB
		false

	lerp: (a, b, t) ->
	next: (current, options, loop) ->
	prev: (current, options, loop) ->

	rand: (amount) ->
		math.random(amount)

	randomizeSeed: ->
		math.randomseed os.time()

	resetCamera: ->
	resize: (width, height) ->
	rotateAround: (object, anchor, angle, relative) ->
	round: (num, precision) ->
	scale: (value, min, max, min2, max2) ->
	scaleClamp: (value, min, max, min2, max2) ->
	setCamera: (x, y) ->
	shuffle: (a) ->
	sign: (value) ->
	stepTowards: (object, x, y, distance) ->
	swap: (current, a, b) ->
	swapScene: ->
	timeFlag: ->
	tween: (object, values, duration, options) ->

LP = lp!


Separate LP.sign function into multiple lines

require "LunoPunk.geometry.Point"
require "LunoPunk.geometry.Rectangle"
require "LunoPunk.geometry.Matrix"
require "LunoPunk.Scene"
require "LunoPunk.Tweener"

export ^

class lp
	-- Private
	BASELAYER = 10
	DEG = -180 / math.pi
	INT_MAX_VALUE = nil
	NUMBER_MAX_VALUE = nil
	RAD = math.pi / -180
	VERSION = "0.0.1"
	__scene = nil

	_gameTime: nil
	_renderTime: nil
	_systemTime: nil
	_updateTime: nil
	assignedFrameRate: nil
	blackColor: 0x00000000
	bounds: nil
	buffer: nil
	camera: Point!
	console: nil
	defaultFont: "default"
	elapsed: 0
	engine: nil
	entity: nil
	fixed: false
	focused: nil
	frameRate: 0
	log: nil
	matrix: Matrix!
	orientations: nil
	pan: nil
	point: Point!
	point2: Point!
	random: -> math.random!
	randomSeed: nil
	rate: 0
	rect: Rectangle!
	renderMode = (value) ->
		return __renderMode if value == nil or __renderMode == value
		__renderMode = value

		-- recreate screen for buffer rendering
		if LP.screen == nil
			LP.screen = Screen!
		else
			LP.screen.init!

		value

	scene: Scene!
	screen: nil
	sprite: nil
	tweener: Tweener!
	volume: 1
	watch: nil
	world: nil
	zero: Point!

	-- Window/resolution related fields
	fullscreen: false
	halfHeight: -> LP.height/2
	halfWidth: -> LP.width/2
	height: 0
	width: 0
	windowHeight: 0
	windowWidth: 0

	alarm: (delay, complete, typeweenType, tweener) ->
	anchorTo: (object, anchor, distance) ->
	angle: (x1, y1, x2, y2) ->
	angleDifference: (angle1, angle2) ->
	angleXY: (object, angle, length, x, y) ->

	-- Approaches the value towards the target, by the specified amount, without overshooting the target.
	-- @param	value	The starting value.
	-- @param	target	The target that you want value to approach.
	-- @param	amount	How much you want the value to approach target by.
	-- @return	The new value.
	approach: (value, target, amount) ->
		if value < target - amount
			value + amount
		else if value > target + amount
			value - amount
		else
			target

	choose: (objs) ->

	-- Clamps the value within the minimum and maximum values.
	-- @param	value		The Float to evaluate.
	-- @param	min			The minimum range.
	-- @param	max			The maximum range.
	-- @return	The clamped value.
	clamp: (value, min, max) -> if value < min
			min
		else if value > max
			max
		else
			value

	clampInRect: (object, x, y, width, height, padding) ->
	clear: (t) -> while #t > 0 do table.remove t
	consoleEnabled: ->
	createBitmap: (width, height, transparent, color) ->
	distance: (x1, y1, x2, y2) ->
	istanceRectPoint: (px, py, rx, ry, rw, rh) ->
	distanceRects: (x1, y1, w1, h1, x2, y2, w2, h2) ->
	distanceSquared: (x1, y1, xprint2, y2) ->
	frames: (from_frame, to_frame, skip) ->
	getImage: (source) -> love.graphics.newImage source
	getColorHSV: (h, s, v) ->
	getColorHue: (color) ->

	getColorSaturation: (color) ->
	getColorValue: (color) ->

	-- Take a 12 bit RGB integer and turn it into a table
	convertColor: (color) -> { lp.getRed(color), lp.getGreen(color), lp.getBlue(color) }

	-- Make a 12 bit RGB integer
	getColorRGB: (r, g, b) -> r * 0x10000 + g * 0x100 + b

	-- Get the red value from an RGB 12 bit integer
	getRed: (color) -> math.floor color / 0x10000

	-- Get the green value from an RGB 12 bit integer
	getGreen: (color) -> math.floor color / 0x100 % 0x100

	-- Get the blue value from an RGB 12 bit integer
	getBlue: (color) -> math.floor color % 0x100

	-- Finds the index of the specified item within a table
	-- Assumes the table is an array indexed table
	indexOf: (tbl, item) -> for i, v in ipairs tbl do return i if item == v

	-- read-only
	DEG: -> DEG

	-- read-only
	NUMBER_MAX_VALUE: -> NUMBER_MAX_VALUE

	-- read-only
	RAD: -> RAD

	-- read-only
	VERSION: -> VERSION

	-- read-only
	BASELAYER: -> BASELAYER

	-- Returns true if the scene doesn't match the internal scene or if the
	-- internal scene is null
	sceneIsNull: => @scene != @__scene or @__scene == nil

	-- Linear interpolation between two values.
	-- @param	a		First value.
	-- @param	b		Second value.
	-- @param	t		Interpolation factor.
	-- @return	When t=0, returns a. When t=1, returns b. When t=0.5, will return halfway between a and b. Etc.
	lerp: (a, b, t = 1) -> a + (b - a) * t

	-- Linear interpolation between two colors.
	-- @param	fromColor		First color.
	-- @param	toColor			Second color.
	-- @param	t				Interpolation value. Clamped to the range [0, 1].
	-- return	RGB component-interpolated color value.
	colorLerp: (fromColor, toColor, t = 1) ->
		if t <= 0
			fromColor
		else if t >= 1
			toColor
		else
			f = LP.convertColor fromColor
			d = LP.convertColor toColor
			d[1] -= f[1]
			d[2] -= f[2]
			d[3] -= f[3]
			f[1] = d[1] * t
			f[2] = d[2] * t
			f[3] = d[3] * t
			LP.getColorRGB unpack f

	next: (current, options, loop) ->
	prev: (current, options, loop) ->

	rand: (amount) -> math.random(amount)
	randomizeSeed: -> math.randomseed os.time()

	resetCamera: -> camera.x, camera.y = 0, 0

	resize: (width, height) ->
		-- resize scene to scale
		width /= LP.screen\fullScaleX!
		height = height / LP.screen\fullScaleY!
		LP.width, LP.height = width, height
		LP.bounds.width, LP.bounds.height = width, height
		LP.screen\resize!

	rotateAround: (object, anchor, angle, relative) ->
	round: (num, precision) ->
	scale: (value, min, max, min2, max2) ->
	scaleClamp: (value, min, max, min2, max2) ->

	-- Sets the camera's x and y
	setCamera: (x, y) -> LP.camera.x, LP.camera.y = x, y
	shuffle: (a) ->

	-- Finds the sign of the provided value.
	-- @param	value		The Float to evaluate.
	-- @return	1 if value > 0, -1 if value < 0, and 0 when value == 0.
	sign: (value) -> if value < 0
			-1
		elseif value == 0
			0
		else
			1

	-- Steps the object towards a point.
	-- @param	object		Object to move (must have an x and y property).
	-- @param	x			X position to step towards.
	-- @param	y			Y position to step towards.
	-- @param	distance	The distance to step (will not overshoot target).
	stepTowards: (object, x, y, distance) ->
		assert object.x != nil and object.y != nil, "Object must have x and y components"
		object.x = LP.approach object.x, x, distance
		object.y = LP.approach object.y, y, distance

	-- Swaps the current item between a and b. Useful for quick state/string/value swapping.
	-- @param	current		The currently selected item.
	-- @param	a			Item a.
	-- @param	b			Item b.
	-- @return	Returns a if current is b, and b if current is a.
	swap: (current, a, b) -> if current == a then b else a

	swapScene: => @__scene = @scene
	timeFlag: ->
	tween: (object, values, duration, options) ->

	-- Used to determine the Löve version that's being used
	-- @param	version		The string containing the major.minor Löve version
	-- @return	true if version matches the Löve version
	__love: (version) -> version == string.format "%d.%d", love._version_major, love._version_minor

LP = lp!

-- Don't allow unknown Löve versions
if not (LP.__love("0.8") or LP.__love("0.9"))
	assert false, "Unsupported Löve version"

require "LunoPunk.geometry.Point"
require "LunoPunk.geometry.Rectangle"
require "LunoPunk.geometry.Matrix"
require "LunoPunk.Scene"
require "LunoPunk.Tweener"

export ^

class lp
	-- Private
	BASELAYER = 10
	DEG = -180 / math.pi
	INT_MAX_VALUE = nil
	NUMBER_MAX_VALUE = nil
	RAD = math.pi / -180
	VERSION = "0.0.1"
	__scene = nil

	_gameTime: nil
	_renderTime: nil
	_systemTime: nil
	_updateTime: nil
	assignedFrameRate: nil
	blackColor: 0x00000000
	bounds: nil
	buffer: nil
	camera: Point!
	console: nil
	defaultFont: "default"
	elapsed: 0
	engine: nil
	entity: nil
	fixed: false
	focused: nil
	frameRate: 0
	log: nil
	matrix: Matrix!
	orientations: nil
	pan: nil
	point: Point!
	point2: Point!
	random: -> math.random!
	randomSeed: nil
	rate: 0
	rect: Rectangle!
	renderMode = (value) ->
		return __renderMode if value == nil or __renderMode == value
		__renderMode = value

		-- recreate screen for buffer rendering
		if LP.screen == nil
			LP.screen = Screen!
		else
			LP.screen.init!

		value

	scene: Scene!
	screen: nil
	sprite: nil
	tweener: Tweener!
	volume: 1
	watch: nil
	world: nil
	zero: Point!

	-- Window/resolution related fields
	fullscreen: false
	halfHeight: -> LP.height/2
	halfWidth: -> LP.width/2
	height: 0
	width: 0
	windowHeight: 0
	windowWidth: 0

	alarm: (delay, complete, typeweenType, tweener) ->
	anchorTo: (object, anchor, distance) ->
	angle: (x1, y1, x2, y2) ->
	angleDifference: (angle1, angle2) ->
	angleXY: (object, angle, length, x, y) ->

	-- Approaches the value towards the target, by the specified amount, without overshooting the target.
	-- @param	value	The starting value.
	-- @param	target	The target that you want value to approach.
	-- @param	amount	How much you want the value to approach target by.
	-- @return	The new value.
	approach: (value, target, amount) ->
		if value < target - amount
			value + amount
		else if value > target + amount
			value - amount
		else
			target

	choose: (objs) ->

	-- Clamps the value within the minimum and maximum values.
	-- @param	value		The Float to evaluate.
	-- @param	min			The minimum range.
	-- @param	max			The maximum range.
	-- @return	The clamped value.
	clamp: (value, min, max) -> if value < min
			min
		else if value > max
			max
		else
			value

	clampInRect: (object, x, y, width, height, padding) ->
	clear: (t) -> while #t > 0 do table.remove t
	consoleEnabled: ->
	createBitmap: (width, height, transparent, color) ->
	distance: (x1, y1, x2, y2) ->
	istanceRectPoint: (px, py, rx, ry, rw, rh) ->
	distanceRects: (x1, y1, w1, h1, x2, y2, w2, h2) ->
	distanceSquared: (x1, y1, xprint2, y2) ->
	frames: (from_frame, to_frame, skip) ->
	getImage: (source) -> love.graphics.newImage source
	getColorHSV: (h, s, v) ->
	getColorHue: (color) ->

	getColorSaturation: (color) ->
	getColorValue: (color) ->

	-- Take a 12 bit RGB integer and turn it into a table
	convertColor: (color) -> { lp.getRed(color), lp.getGreen(color), lp.getBlue(color) }

	-- Make a 12 bit RGB integer
	getColorRGB: (r, g, b) -> r * 0x10000 + g * 0x100 + b

	-- Get the red value from an RGB 12 bit integer
	getRed: (color) -> math.floor color / 0x10000

	-- Get the green value from an RGB 12 bit integer
	getGreen: (color) -> math.floor color / 0x100 % 0x100

	-- Get the blue value from an RGB 12 bit integer
	getBlue: (color) -> math.floor color % 0x100

	-- Finds the index of the specified item within a table
	-- Assumes the table is an array indexed table
	indexOf: (tbl, item) -> for i, v in ipairs tbl do return i if item == v

	-- read-only
	DEG: -> DEG

	-- read-only
	NUMBER_MAX_VALUE: -> NUMBER_MAX_VALUE

	-- read-only
	RAD: -> RAD

	-- read-only
	VERSION: -> VERSION

	-- read-only
	BASELAYER: -> BASELAYER

	-- Returns true if the scene doesn't match the internal scene or if the
	-- internal scene is null
	sceneIsNull: => @scene != @__scene or @__scene == nil

	-- Linear interpolation between two values.
	-- @param	a		First value.
	-- @param	b		Second value.
	-- @param	t		Interpolation factor.
	-- @return	When t=0, returns a. When t=1, returns b. When t=0.5, will return halfway between a and b. Etc.
	lerp: (a, b, t = 1) -> a + (b - a) * t

	-- Linear interpolation between two colors.
	-- @param	fromColor		First color.
	-- @param	toColor			Second color.
	-- @param	t				Interpolation value. Clamped to the range [0, 1].
	-- return	RGB component-interpolated color value.
	colorLerp: (fromColor, toColor, t = 1) ->
		if t <= 0
			fromColor
		else if t >= 1
			toColor
		else
			f = LP.convertColor fromColor
			d = LP.convertColor toColor
			d[1] -= f[1]
			d[2] -= f[2]
			d[3] -= f[3]
			f[1] = d[1] * t
			f[2] = d[2] * t
			f[3] = d[3] * t
			LP.getColorRGB unpack f

	next: (current, options, loop) ->
	prev: (current, options, loop) ->

	rand: (amount) -> math.random(amount)
	randomizeSeed: -> math.randomseed os.time()

	resetCamera: -> camera.x, camera.y = 0, 0

	resize: (width, height) ->
		-- resize scene to scale
		width /= LP.screen\fullScaleX!
		height = height / LP.screen\fullScaleY!
		LP.width, LP.height = width, height
		LP.bounds.width, LP.bounds.height = width, height
		LP.screen\resize!

	rotateAround: (object, anchor, angle, relative) ->
	round: (num, precision) ->
	scale: (value, min, max, min2, max2) ->
	scaleClamp: (value, min, max, min2, max2) ->

	-- Sets the camera's x and y
	setCamera: (x, y) -> LP.camera.x, LP.camera.y = x, y
	shuffle: (a) ->

	-- Finds the sign of the provided value.
	-- @param	value		The Float to evaluate.
	-- @return	1 if value > 0, -1 if value < 0, and 0 when value == 0.
	sign: (value) -> if value < 0 then -1 elseif value == 0 then 0 else 1

	-- Steps the object towards a point.
	-- @param	object		Object to move (must have an x and y property).
	-- @param	x			X position to step towards.
	-- @param	y			Y position to step towards.
	-- @param	distance	The distance to step (will not overshoot target).
	stepTowards: (object, x, y, distance) ->
		assert object.x != nil and object.y != nil, "Object must have x and y components"
		object.x = LP.approach object.x, x, distance
		object.y = LP.approach object.y, y, distance

	-- Swaps the current item between a and b. Useful for quick state/string/value swapping.
	-- @param	current		The currently selected item.
	-- @param	a			Item a.
	-- @param	b			Item b.
	-- @return	Returns a if current is b, and b if current is a.
	swap: (current, a, b) -> if current == a then b else a

	swapScene: => @__scene = @scene
	timeFlag: ->
	tween: (object, values, duration, options) ->

	-- Used to determine the Löve version that's being used
	-- @param	version		The string containing the major.minor Löve version
	-- @return	true if version matches the Löve version
	__love: (version) -> version == string.format "%d.%d", love._version_major, love._version_minor

LP = lp!

-- Don't allow unknown Löve versions
if not (LP.__love("0.8") or LP.__love("0.9"))
	assert false, "Unsupported Löve version"


Implement utils.Input.check (key)

Implement utils.Input.check (key)


Fix missing pairs in commands.moon

Fix missing pairs in commands.moon

-- Copyright (c) 2014 by Adam Hellberg <sharparam@sharparam.com>.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

NAME, T = ...

import Logger, log from T
import decorate, deserialize from T.misc

{database: db, localization: L} = T

commands = {}

T.command_manager =
    handle: (message) =>
        name, arg = message\match '^([^%s]+)(.*)'
        return @help! unless name
        name = name\lower!
        local command

        for _, cmd in pairs commands
            for _, alias in pairs cmd[1]
                if name\match '^' .. alias
                    command = cmd
                    break

        return log\error L.invalid_command unless command

        args = {}

        if arg
            arg = arg\trim!
            if arg == ""
                arg = nil
            else
                for token in arg\gmatch '[^%s]+'
                    table.insert args, token

        command[2] arg, unpack args

    help: =>
        log\info 'TODO: Add help'

    register: (names, handler) =>
        names = {names} unless type(names) == 'table'
        entry = {names, handler}
        table.insert commands, entry

cm = T.command_manager
register = cm\register

register 'de', (arg) ->
    T\set_debug arg\match('^[ye]') and true or false if arg else T\toggle_debug!
    log\notice L.debug_status, T\is_debug_enabled! and L.enabled or L.disabled

register 'db$', (arg, key, value) ->
    if not T\is_debug_enabled!
        return log\error L.db_safety_notice
    if not arg
        return log\error L.db_usage

    if not value
        log\info L.db_current, key, tostring(db key), tostring db\get_default key
    elseif value == '_RESET'
        db\reset key
        log\info L.db_reset, key, tostring db key
    else
        value = arg\match("^#{key} (.*)")
        -- This makes it easier to set strings in the db
        value = tostring value unless value\match '^%d+$' or value\match '^\{.*\}$'
        valid, result = deserialize value
        if not valid
            return log\error L.db_invalid, result
        db\set key, result
        log\info L.db_set, key, tostring db key

register 'l', (arg, section, s_arg, ss_arg) ->
    if not arg
        return log\notice L.log_status, db('log') and L.enabled or L.disabled
    elseif arg\match '^[ts]' -- Log [t]oggle/[s]witch
        return db\set 'log', (not db 'log', true)
    elseif arg\match('^[ye]') or arg\match '^on' -- Log [y]es, [e]nable, [on]
        return db\set 'log', true
    elseif arg\match('^[nd]') or arg\match '^of' -- Log [n]o, [d]isable, [of]f
        return db\set 'log', false

    return unless section

    -- If user log level threshold is set high enough (notice)
    -- some log statements may not appear

    if section\match '^l' -- Log [l]evel
        if not s_arg
            level = db 'log.level', Logger.levels.INFO
            prefix = Logger\level_to_prefix level
            return log\notice L.log_current_level, prefix, level
        level = s_arg\match '^%d+'
        level = tonumber level
        if not level -- Attempt to parse a text value
            prefix = s_arg\match('^.')\upper!
            for pre, lvl in pairs Logger.levels
                if prefix == pre\match '^%w'
                    level = lvl
                    break
        if type(level) == 'number'
            db\set 'log.level', level
    elseif section\match '^c' -- Log [c]olor
        if not s_arg
            name_color = Logger.colors.name!
            log\notice decorate(L.log_current_color, name_color), L.name, name_color
            for level, func in pairs Logger.colors.levels
                color = func!
                prefix = Logger\level_to_prefix level
                log\notice decorate(L.log_current_color, color), prefix, color
        elseif s_arg\match('^[ye]') or s_arg\match '^on'
            db\set 'log.color', true
        elseif s_arg\match('^di') or s_arg\match('^of') or s_arg\match '^not'
            db\set 'log.color', false
        elseif not ss_arg
            log\error L.log_color_usage
        elseif s_arg\match '^na' -- [na]me
            db\set 'log.color.name', ss_arg
        else
            prefix_letter = s_arg\match '^.'\upper!
            local level, prefix
            for lvl, pre in pairs Logger.prefixes
                if prefix_letter == pre\match '^%w'
                    level = lvl
                    prefix = pre\lower!
                    break
            return log\error L.log_invalid_prefix unless prefix
            db\set 'log.color.level.' .. prefix, ss_arg

register 'minimap', ->
    T.broker\toggle_minimap!
    log\notice L.b_minimap_status, T.broker\is_minimap_enabled! and L.enabled or L.disabled

for i, v in ipairs {'sharexp', 'sxp'}
    _G['SLASH_' .. NAME\upper! .. i] = '/' .. v

SlashCmdList[NAME\upper!] = (msg, editBox) ->
    cm\handle msg

-- Copyright (c) 2014 by Adam Hellberg <sharparam@sharparam.com>.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.

NAME, T = ...

import Logger, log from T
import decorate, deserialize from T.misc

{database: db, localization: L} = T

commands = {}

T.command_manager =
    handle: (message) =>
        name, arg = message\match '^([^%s]+)(.*)'
        return @help! unless name
        name = name\lower!
        local command

        for _, cmd in pairs commands
            for _, alias in pairs cmd[1]
                if name\match '^' .. alias
                    command = cmd
                    break

        return log\error L.invalid_command unless command

        args = {}

        if arg
            arg = arg\trim!
            if arg == ""
                arg = nil
            else
                for token in arg\gmatch '[^%s]+'
                    table.insert args, token

        command[2] arg, unpack args

    help: =>
        log\info 'TODO: Add help'

    register: (names, handler) =>
        names = {names} unless type(names) == 'table'
        entry = {names, handler}
        table.insert commands, entry

cm = T.command_manager
register = cm\register

register 'de', (arg) ->
    T\set_debug arg\match('^[ye]') and true or false if arg else T\toggle_debug!
    log\notice L.debug_status, T\is_debug_enabled! and L.enabled or L.disabled

register 'db$', (arg, key, value) ->
    if not T\is_debug_enabled!
        return log\error L.db_safety_notice
    if not arg
        return log\error L.db_usage

    if not value
        log\info L.db_current, key, tostring(db key), tostring db\get_default key
    elseif value == '_RESET'
        db\reset key
        log\info L.db_reset, key, tostring db key
    else
        value = arg\match("^#{key} (.*)")
        -- This makes it easier to set strings in the db
        value = tostring value unless value\match '^%d+$' or value\match '^\{.*\}$'
        valid, result = deserialize value
        if not valid
            return log\error L.db_invalid, result
        db\set key, result
        log\info L.db_set, key, tostring db key

register 'l', (arg, section, s_arg, ss_arg) ->
    if not arg
        return log\notice L.log_status, db('log') and L.enabled or L.disabled
    elseif arg\match '^[ts]' -- Log [t]oggle/[s]witch
        return db\set 'log', (not db 'log', true)
    elseif arg\match('^[ye]') or arg\match '^on' -- Log [y]es, [e]nable, [on]
        return db\set 'log', true
    elseif arg\match('^[nd]') or arg\match '^of' -- Log [n]o, [d]isable, [of]f
        return db\set 'log', false

    return unless section

    -- If user log level threshold is set high enough (notice)
    -- some log statements may not appear

    if section\match '^l' -- Log [l]evel
        if not s_arg
            level = db 'log.level', Logger.levels.INFO
            prefix = Logger\level_to_prefix level
            return log\notice L.log_current_level, prefix, level
        level = s_arg\match '^%d+'
        level = tonumber level
        if not level -- Attempt to parse a text value
            prefix = s_arg\match('^.')\upper!
            for pre, lvl in Logger.levels
                if prefix == pre\match '^%w'
                    level = lvl
                    break
        if type(level) == 'number'
            db\set 'log.level', level
    elseif section\match '^c' -- Log [c]olor
        if not s_arg
            name_color = Logger.colors.name!
            log\notice decorate(L.log_current_color, name_color), L.name, name_color
            for level, func in pairs Logger.colors.levels
                color = func!
                prefix = Logger\level_to_prefix level
                log\notice decorate(L.log_current_color, color), prefix, color
        elseif s_arg\match('^[ye]') or s_arg\match '^on'
            db\set 'log.color', true
        elseif s_arg\match('^di') or s_arg\match('^of') or s_arg\match '^not'
            db\set 'log.color', false
        elseif not ss_arg
            log\error L.log_color_usage
        elseif s_arg\match '^na' -- [na]me
            db\set 'log.color.name', ss_arg
        else
            prefix_letter = s_arg\match '^.'\upper!
            local level, prefix
            for lvl, pre in pairs Logger.prefixes
                if prefix_letter == pre\match '^%w'
                    level = lvl
                    prefix = pre\lower!
                    break
            return log\error L.log_invalid_prefix unless prefix
            db\set 'log.color.level.' .. prefix, ss_arg

register 'minimap', ->
    T.broker\toggle_minimap!
    log\notice L.b_minimap_status, T.broker\is_minimap_enabled! and L.enabled or L.disabled

for i, v in ipairs {'sharexp', 'sxp'}
    _G['SLASH_' .. NAME\upper! .. i] = '/' .. v

SlashCmdList[NAME\upper!] = (msg, editBox) ->
    cm\handle msg


ok [PPM/2 - Customizable Pony Player Models] lua/ppm2/common/ponydata.lua:345: Tried to use a NULL entity!   1. SetNW2Bool - [C]:-1    2. ModelChanges - lua/ppm2/common/ponydata.lua:345     3. SetupEntity - lua/ppm2/common/ponydata.lua:304      4. PreDrawModel - lua/ppm2/client/player_menu.lua:78       5. DrawModel - lua/vgui/dmodelpanel.lua:100        6. unknown - lua/vgui/dmodelpanel.lua:146

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

doPatch = =>
	return if not @IsValid()
	local target

	for _, child in ipairs @GetChildren()
		if child\GetName() == 'DIconLayout'
			target = child
			break

	return if not target
	local buttonTarget

	for _, button in ipairs target\GetChildren()
		buttonChilds = button\GetChildren()
		cond1 = buttonChilds[1] and buttonChilds[1]\GetName() == 'DLabel'
		cond2 = buttonChilds[2] and buttonChilds[2]\GetName() == 'DLabel'
		if cond1 and buttonChilds[1]\GetText() == 'Player Model'
			buttonTarget = button
			break
		elseif cond2 and buttonChilds[2]\GetText() == 'Player Model'
			buttonTarget = button
			break

	return if not buttonTarget
	{:title, :init, :icon, :width, :height, :onewindow} = list.Get('DesktopWindows').PlayerEditor
	buttonTarget.DoClick = ->
		return buttonTarget.Window\Center() if onewindow and IsValid(buttonTarget.Window)

		buttonTarget.Window = @Add('DFrame')
		with buttonTarget.Window
			\SetSize(width, height)
			\SetTitle(title)
			\Center()

		init(buttonTarget, buttonTarget.Window)
		local targetModel

		for _, child in ipairs buttonTarget.Window\GetChildren()
			if child\GetName() == 'DModelPanel'
				targetModel = child
				break

		return if not targetModel
		targetModel.oldSetModel = targetModel.SetModel
		targetModel.SetModel = (model) =>
			oldModel = @Entity\GetModel()
			oldPonyData = @Entity\GetPonyData()
			@oldSetModel(model)
			if IsValid(@Entity) and oldPonyData
				oldPonyData\SetupEntity(@Entity)
				oldPonyData\ModelChanges(oldModel, model)

		targetModel.PreDrawModel = (ent) =>
			return if not IsValid(ent)
			controller = @ponyController
			return if not controller
			return if not ent\IsPony()
			controller\SetupEntity(ent) if controller.ent ~= ent
			controller\GetRenderController()\DrawModels()
			controller\GetRenderController()\PreDraw(ent)
			controller\GetRenderController()\HideModels(true)
			bg = controller\GetBodygroupController()
			bg\ApplyBodygroups() if bg

		copy = PPM2.GetMainData()\Copy()
		controller = copy\CreateCustomController(targetModel.Entity)
		copy\SetNetworkObject(controller)
		controller\SetDisableTask(true)
		targetModel.ponyController = controller

		hook.Run 'BuildPlayerModelMenu', buttonTarget, buttonTarget.Window

hook.Add 'ContextMenuCreated', 'PPM2.PatchPlayerModelMenu', => timer.Simple 0, -> doPatch(@)

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

doPatch = =>
	return if not @IsValid()
	local target

	for _, child in ipairs @GetChildren()
		if child\GetName() == 'DIconLayout'
			target = child
			break

	return if not target
	local buttonTarget

	for _, button in ipairs target\GetChildren()
		buttonChilds = button\GetChildren()
		cond1 = buttonChilds[1] and buttonChilds[1]\GetName() == 'DLabel'
		cond2 = buttonChilds[2] and buttonChilds[2]\GetName() == 'DLabel'
		if cond1 and buttonChilds[1]\GetText() == 'Player Model'
			buttonTarget = button
			break
		elseif cond2 and buttonChilds[2]\GetText() == 'Player Model'
			buttonTarget = button
			break

	return if not buttonTarget
	{:title, :init, :icon, :width, :height, :onewindow} = list.Get('DesktopWindows').PlayerEditor
	buttonTarget.DoClick = ->
		return buttonTarget.Window\Center() if onewindow and IsValid(buttonTarget.Window)

		buttonTarget.Window = @Add('DFrame')
		with buttonTarget.Window
			\SetSize(width, height)
			\SetTitle(title)
			\Center()

		init(buttonTarget, buttonTarget.Window)
		local targetModel

		for _, child in ipairs buttonTarget.Window\GetChildren()
			if child\GetName() == 'DModelPanel'
				targetModel = child
				break

		return if not targetModel
		targetModel.oldSetModel = targetModel.SetModel
		targetModel.SetModel = (model) =>
			oldModel = @Entity\GetModel()
			oldPonyData = @Entity\GetPonyData()
			@oldSetModel(model)
			if IsValid(@Entity) and oldPonyData
				oldPonyData\SetupEntity(@Entity)
				oldPonyData\ModelChanges(oldModel, model)
		targetModel.PreDrawModel = (ent) =>
			controller = @ponyController
			return if not controller
			return if not ent\IsPony()
			controller\SetupEntity(ent) if controller.ent ~= ent
			controller\GetRenderController()\DrawModels()
			controller\GetRenderController()\PreDraw(ent)
			controller\GetRenderController()\HideModels(true)
			bg = controller\GetBodygroupController()
			bg\ApplyBodygroups() if bg

		copy = PPM2.GetMainData()\Copy()
		controller = copy\CreateCustomController(targetModel.Entity)
		copy\SetNetworkObject(controller)
		controller\SetDisableTask(true)
		targetModel.ponyController = controller

		hook.Run 'BuildPlayerModelMenu', buttonTarget, buttonTarget.Window

hook.Add 'ContextMenuCreated', 'PPM2.PatchPlayerModelMenu', => timer.Simple 0, -> doPatch(@)


weatherunderground: change language, remove dummy

-- Weather Underground API module, requires a key set in configuration
-- WeatherUndergroundKey

util = require'util'
os = require'os'
iconv = require'iconv'

iso2utf = iconv.new('latin1', 'utf-8')

key = ivar2.config.WeatherUndergroundKey
unless key
  return {}

geoApiBase = 'http://api.geonames.org/searchJSON?name=%s&featureClass=P&username=haste'
language = 'EN'

tempColor = (v) ->
  v = tonumber(v)
  if v > 0
    color = util.red
    return color(v)
  else
    color = util.lightblue
    return color(v)

windSpeedToName = (ws) ->
  windtext = ''
  unless ws
    return windtext
  if ws > 32
    windtext = 'orkan!'
  elseif ws > 28.5
    windtext = 'sterk storm'
  elseif ws > 24.5
    windtext = 'full storm'
  elseif ws > 20.8
    windtext = 'liten storm'
  elseif ws > 17.2
    windtext = 'sterk kuling'
  elseif ws > 13.9
    windtext = 'stiv kuling'
  elseif ws > 10.8
    windtext = 'liten kuling'
  elseif ws > 8
    windtext = 'frisk bris'
  elseif ws > 5.5
    windtext = 'laber bris'
  elseif ws > 3.4
    windtext = 'lett bris'
  elseif ws > 1.5
    windtext = 'svak vind'
  elseif ws > 0.3
    windtext = 'flau vind'
  else
    windtext = 'vindstille'
  return windtext

findLocation = (source, destination, name, cb) =>
  gurl = geoApiBase\format(util.urlEncode(name))
  util.simplehttp gurl, (data) ->
    json = util.json.decode(data)
    if json.totalResultsCount == 0
      return @Msg('privmsg', destination, source, "Does that place even exist?")
    city = json.geonames[1]
    lat = city.lat
    lon = city.lng
    cb(lon, lat)

lookupConditions = (source, destination, input, pws) =>
  findLocation @, source, destination, input, (lon, lat) ->
    pws = pws or false
    if pws
      pws = 'pws:1/'
    else
      pws = 'pws:0/'
    url = "http://api.wunderground.com/api/#{key}/lang:#{language}/#{pws}conditions/q/#{lat},#{lon}.json"
    @Log 'debug', "Fetching WU URL: %s", url
    util.simplehttp url, (data) ->
      json = util.json.decode(data)
      if json.error ~= nil and json.error.description ~=nil
        return say json.error.description
      current = json.current_observation

      location = current.observation_location
      city = iso2utf\iconv location.city

      weather = current.weather
      temp = current.temp_c
      feelsLike = current.feelslike_c

      windSpeedname = windSpeedToName(current.wind_kph/3.6)

      windDirection = current.wind_dir

      rain = current.precip_today_metric

      time = os.date('%H:%M', tonumber(current.observation_epoch))

      @Msg 'privmsg', destination, source, '%s, %s °C (feels like %s °C), %s %s, %s mm, (%s, %s)', weather, tempColor(temp), tempColor(feelsLike), windDirection, windSpeedname, rain, city, time

lookupConditionsPWS = (source, destination, input) =>
  lookupConditions(@, source, destination, input, true)

PRIVMSG:
  '^%pwu (.+)$': lookupConditions
  '^%ppws (.+)$': lookupConditionsPWS

-- Weather Underground API module, requires a key set in configuration
-- WeatherUndergroundKey

util = require'util'
os = require'os'
iconv = require'iconv'

iso2utf = iconv.new('latin1', 'utf-8')

key = ivar2.config.WeatherUndergroundKey
unless key
  return {}

geoApiBase = 'http://api.geonames.org/searchJSON?name=%s&featureClass=P&username=haste'
wugeolookupurl = 'http://api.wunderground.com/api/%s/geolookup/q/59.788,5.7218.json'
language = 'NO'

tempColor = (v) ->
  v = tonumber(v)
  if v > 0
    color = util.red
    return color(v)
  else
    color = util.lightblue
    return color(v)

windSpeedToName = (ws) ->
  windtext = ''
  unless ws
    return windtext
  if ws > 32
    windtext = 'orkan!'
  elseif ws > 28.5
    windtext = 'sterk storm'
  elseif ws > 24.5
    windtext = 'full storm'
  elseif ws > 20.8
    windtext = 'liten storm'
  elseif ws > 17.2
    windtext = 'sterk kuling'
  elseif ws > 13.9
    windtext = 'stiv kuling'
  elseif ws > 10.8
    windtext = 'liten kuling'
  elseif ws > 8
    windtext = 'frisk bris'
  elseif ws > 5.5
    windtext = 'laber bris'
  elseif ws > 3.4
    windtext = 'lett bris'
  elseif ws > 1.5
    windtext = 'svak vind'
  elseif ws > 0.3
    windtext = 'flau vind'
  else
    windtext = 'vindstille'
  return windtext

findLocation = (source, destination, name, cb) =>
  gurl = geoApiBase\format(util.urlEncode(name))
  util.simplehttp gurl, (data) ->
    json = util.json.decode(data)
    if json.totalResultsCount == 0
      return @Msg('privmsg', destination, source, "Does that place even exist?")
    city = json.geonames[1]
    lat = city.lat
    lon = city.lng
    cb(lon, lat)

lookupConditions = (source, destination, input, pws) =>
  findLocation @, source, destination, input, (lon, lat) ->
    pws = pws or false
    if pws
      pws = 'pws:1/'
    else
      pws = 'pws:0/'
    url = "http://api.wunderground.com/api/#{key}/lang:#{language}/#{pws}conditions/q/#{lat},#{lon}.json"
    @Log 'debug', "Fetching WU URL: %s", url
    util.simplehttp url, (data) ->
      json = util.json.decode(data)
      if json.error ~= nil and json.error.description ~=nil
        return say json.error.description
      current = json.current_observation

      location = current.observation_location
      city = iso2utf\iconv location.city

      weather = current.weather
      temp = current.temp_c
      feelsLike = current.feelslike_c

      windSpeedname = windSpeedToName(current.wind_kph/3.6)

      windDirection = current.wind_dir

      rain = current.precip_today_metric

      time = os.date('%H:%M', tonumber(current.observation_epoch))

      @Msg 'privmsg', destination, source, '%s, %s °C (følt %s °C), %s %s, %s mm, (%s, %s)', weather, tempColor(temp), tempColor(feelsLike), windDirection, windSpeedname, rain, city, time

lookupConditionsPWS = (source, destination, input) =>
  lookupConditions(@, source, destination, input, true)

PRIVMSG:
  '^%pwu (.+)$': lookupConditions
  '^%ppws (.+)$': lookupConditionsPWS


Update LP spec to work again

Update LP spec to work again

require "busted"
require "tests.mock_love"

import LP from require "LunoPunk.LP"

-- We only need to setup some Coveralls options once
require "coveralls.coveralls"
Coveralls.dirname = './LunoPunk'

describe "LP", ->
	it "sign", ->
		-- Negative
		assert.are.equal -1, LP.sign -1
		assert.are.equal -1, LP.sign -15
		-- bounds
		assert.are_not.equal -1, LP.sign 0
		assert.are_not.equal 0, LP.sign -1
		assert.are.equal 0, LP.sign 0
		assert.are_not.equal 0, LP.sign 1
		-- Positive
		assert.are.equal 1, LP.sign 1
		assert.are.equal 1, LP.sign 15

	it "clamp", ->
		-- contained
		assert.are.equal 1.5, LP.clamp 1.5, 0, 2
		assert.are.equal 1.9999, LP.clamp 1.9999, 0, 2
		-- Max
		assert.are.equal 2, LP.clamp 2, 0, 2
		assert.are.equal 2, LP.clamp 3, 0, 2
		-- Min
		assert.are.equal 0, LP.clamp 0, 0, 1
		assert.are.equal 0, LP.clamp -1, 0, 1

	it "convertColor", ->
		rgb = LP.convertColor 0x1f2f3f
		assert.are.equal rgb[1], 0x1f
		assert.are.equal rgb[2], 0x2f
		assert.are.equal rgb[3], 0x3f

	it "getColorRGB", ->
		assert.are.equal 0x1f2f3f, LP.getColorRGB 0x1f, 0x2f, 0x3f

	it "lerp", ->
		-- t = 0, return the first value
		assert.are.equal 1, LP.lerp 1, 2, 0
		-- t = 1, return the second value
		assert.are.equal 2, LP.lerp 1, 2, 1
		-- t = .5, return the midpoint between the values
		assert.are.equal 1.5, LP.lerp 1, 2, .5

	it "colorLerp", ->
		color1 = 0x000000
		color2 = 0x101010
		-- t = 0, return the first color
		assert.are.equal 0x000000, LP.colorLerp color1, color2, 0
		-- t = 1, return the second color
		assert.are.equal 0x101010, LP.colorLerp color1, color2, 1
		-- t = .5, return the color half way between the two colors
		assert.are.equal 0x080808, LP.colorLerp color1, color2, .5

	it "approach", ->
		-- No approach
		assert.are.equal 1, LP.approach 1, 2, 0
		-- Contained approach
		assert.are.equal 1, LP.approach 0, 2, 1
		-- <= approach
		assert.are.equal 2, LP.approach 0, 2, 2
		-- approach, but do not go over
		assert.are.equal 2, LP.approach 0, 2, 3

	it "stepTowards", ->
		class xy
			new: => @__x, @__y = 0, 0
			x: (v) => if v == nil then @__x else @__x = v
			y: (v) => if v == nil then @__y else @__y = v

		-- No step
		c = xy!
		LP.stepTowards c, 1, 1, 0
		assert.are.equal 0, c\x!
		assert.are.equal 0, c\y!

		-- Contained step
		LP.stepTowards c, 2, 2, 1
		assert.are.equal 1, c\x!
		assert.are.equal 1, c\y!

		-- Bounds step
		LP.stepTowards c, 2, 2, 1
		assert.are.equal 2, c\x!
		assert.are.equal 2, c\y!

		-- Over step
		LP.stepTowards c, 2, 2, 1
		assert.are.equal 2, c\x!
		assert.are.equal 2, c\y!

		-- Different x and y
		LP.stepTowards c, 2, 3, 1
		assert.are.equal 2, c\x!
		assert.are.equal 3, c\y!

		-- Under step (changed bounds)
		LP.stepTowards c, 1, 2, 1
		assert.are.equal 1, c\x!
		assert.are.equal 2, c\y!

	it "swap", ->
		a, b = 1, 2
		-- Not within
		assert.are.same b, LP.swap a, b, b

		-- standard swap
		assert.are.same b, LP.swap a, a, b
		assert.are.same b, LP.swap a, b, a

require "busted"
require "tests.mock_love"

import LP from require "LunoPunk.LP"

-- We only need to setup some Coveralls options once
require "coveralls.coveralls"
Coveralls.dirname = './LunoPunk'

describe "LP", ->
	it "sign", ->
		-- Negative
		assert.are.equal -1, LP.sign -1
		assert.are.equal -1, LP.sign -15
		-- bounds
		assert.are_not.equal -1, LP.sign 0
		assert.are_not.equal 0, LP.sign -1
		assert.are.equal 0, LP.sign 0
		assert.are_not.equal 0, LP.sign 1
		-- Positive
		assert.are.equal 1, LP.sign 1
		assert.are.equal 1, LP.sign 15

	it "clamp", ->
		-- contained
		assert.are.equal 1.5, LP.clamp 1.5, 0, 2
		assert.are.equal 1.9999, LP.clamp 1.9999, 0, 2
		-- Max
		assert.are.equal 2, LP.clamp 2, 0, 2
		assert.are.equal 2, LP.clamp 3, 0, 2
		-- Min
		assert.are.equal 0, LP.clamp 0, 0, 1
		assert.are.equal 0, LP.clamp -1, 0, 1

	it "convertColor", ->
		rgb = LP.convertColor 0x1f2f3f
		assert.are.equal rgb[1], 0x1f
		assert.are.equal rgb[2], 0x2f
		assert.are.equal rgb[3], 0x3f

	it "getColorRGB", ->
		assert.are.equal 0x1f2f3f, LP.getColorRGB 0x1f, 0x2f, 0x3f

	it "lerp", ->
		-- t = 0, return the first value
		assert.are.equal 1, LP.lerp 1, 2, 0
		-- t = 1, return the second value
		assert.are.equal 2, LP.lerp 1, 2, 1
		-- t = .5, return the midpoint between the values
		assert.are.equal 1.5, LP.lerp 1, 2, .5

	it "colorLerp", ->
		color1 = 0x000000
		color2 = 0x101010
		-- t = 0, return the first color
		assert.are.equal 0x000000, LP.colorLerp color1, color2, 0
		-- t = 1, return the second color
		assert.are.equal 0x101010, LP.colorLerp color1, color2, 1
		-- t = .5, return the color half way between the two colors
		assert.are.equal 0x080808, LP.colorLerp color1, color2, .5

	it "approach", ->
		-- No approach
		assert.are.equal 1, LP.approach 1, 2, 0
		-- Contained approach
		assert.are.equal 1, LP.approach 0, 2, 1
		-- <= approach
		assert.are.equal 2, LP.approach 0, 2, 2
		-- approach, but do not go over
		assert.are.equal 2, LP.approach 0, 2, 3

	it "stepTowards", ->
		class xy
			new: => @x, @y = 0, 0

		-- No step
		c = xy!
		LP.stepTowards c, 1, 1, 0
		assert.are.equal 0, c.x
		assert.are.equal 0, c.y

		-- Contained step
		LP.stepTowards c, 2, 2, 1
		assert.are.equal 1, c.x
		assert.are.equal 1, c.y

		-- Bounds step
		LP.stepTowards c, 2, 2, 1
		assert.are.equal 2, c.x
		assert.are.equal 2, c.y

		-- Over step
		LP.stepTowards c, 2, 2, 1
		assert.are.equal 2, c.x
		assert.are.equal 2, c.y

		-- Different x and y
		LP.stepTowards c, 2, 3, 1
		assert.are.equal 2, c.x
		assert.are.equal 3, c.y

		-- Under step (changed bounds)
		LP.stepTowards c, 1, 2, 1
		assert.are.equal 1, c.x
		assert.are.equal 2, c.y

	it "swap", ->
		a, b = 1, 2
		-- Not within
		assert.are.same b, LP.swap a, b, b

		-- standard swap
		assert.are.same b, LP.swap a, a, b
		assert.are.same b, LP.swap a, b, a


irc.moon: Improve matching over tags

irc.moon: Improve matching over tags

--- IRC client class
-- @classmod IRCClient

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

moonscript = {
	errors: require 'moonscript.errors'
}

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

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	-- ::TODO:: allow for multiple *and* index
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple
			output
		elseif index
			index\get name, :multiple, :uses_priority

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]+} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {%S+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client ~= is_client
			if key ~= nil
				continue if tag.vendor ~= nil and key ~= "#{tag.vendor}/#{tag.key}"
				continue if tag.vendor == nil and key ~= tag.key
			return tag

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, line, ...)=>
					@send 'TAGMSG', target, "+draft/typing": "active"
					@pcall command, prefix, target, line, ...
					@send 'TAGMSG', target, "+draft/typing": "done"

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port

		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname

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

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @param ... List of arguments to be passed to sender
	-- @see IRCClient\add_sender
	send: (name, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

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

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for _, hook in pairs IRCClient.hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)
		for _, hook in pairs @hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		Logger.debug Logger.level.error .. "*** Hook not found for #{command}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *IRCClient.handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		for handler in *@handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line in @socket\lines! do
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.debug "Arguments:"
			for arg in *{...}
				Logger.debug tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.debug "Arguments:"
			Logger.debug tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.print "#{Logger.level.error} *** #{err}"
		Logger.debug moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.debug "#{Logger.level.error} ---"
		return err

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			Logger.print '\00311(\003' .. (@server.caps and
				@server.caps['NETWORK'] or @config.server) ..
				"\00311)\003 #{line}"

return {:IRCClient, :priority}

--- IRC client class
-- @classmod IRCClient

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

moonscript = {
	errors: require 'moonscript.errors'
}

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

local IRCClient, ContextTable, priority

priority = {
	HIGH: 1,
	DEFAULT: 2,
	LOW: 3,
}

class ContextTable
	-- ::TODO:: allow for multiple *and* index
	get: (name, opts = {})=>
		:multiple, :index, :uses_priority = opts
		multiple = true if multiple == nil
		uses_priority = true if uses_priority == nil

		local output
		output = {} if multiple
		for context, items in pairs(self)
			-- context
			if multiple
				-- add all that exist
				if uses_priority
					for p=priority.HIGH, priority.LOW
						if items[p][name]
							for element in *items[p][name]
								table.insert output, element
				else
					if items[name]
						for element in *items[name]
							table.insert output, element
			else
				-- return the singular if it exists
				if uses_priority
					for p=priority.HIGH, priority.LOW
						return items[p][name] if items[p][name] ~= nil
				else
					return items[name] if items[name] ~= nil

		if multiple
			output
		elseif index
			index\get name, :multiple, :uses_priority

	remove: (name)=>
		for context_name, tbl in pairs(self)
			tbl[name] = nil

class IRCClient
	commands: ContextTable!
	hooks: ContextTable!
	handlers: ContextTable!
	senders: ContextTable!

	__tostring: => "IRCClient"

	default_config = {
		prefix: "!"
	}

	line_pattern = re.compile [[
		-- tags, command, args
		line <- {|
			{:tags: {| (tags sp)? |} :}
			{:prefix: {| (prefix sp)? |} :}
			{:command: (command / numeric) :}
			{:args: {| (sp arg)* |} :} |}
		tags <- '@' tag (';' tag)*
		tag <- {|
			{:is_client: {'+'} :}? -- check: if tag.is_client
			{:vendor: {[^/]+} '/' :}?
			{:key: {[^=; ]+} -> esc_tag :}
			{:value: ('=' {[^; ]+} -> esc_tag) :}?
		|}
		prefix <- ':' (
			{:nick: {[^ !]+} :} '!'
			{:user: {[^ @]+} :} '@'
			{:host: {[^ ]+} :} /
			{:nick: {[^ ]+} :})
		command <- [A-Za-z]+
		numeric <- %d^+3^-4 -- at most four digits, at least three
		arg <- ':' {.+} / {%S+}
		sp <- %s
	]], esc_tag: (tag)-> tag\gsub "\\(.)", setmetatable({
		[":"]: ";"
		s: " "
		r: "\r"
		n: "\n"
	}, __index: (t, k) -> k)

	--- Get a tag based on search parameters (is_client IS NOT matched by key)
	-- @tparam table tags Tag from IRC parser
	-- @tparam table opts Options to search for (is_client: bool, key: string)
	get_tag: (tags, opts)=>
		:is_client, :key = opts
		for tag in *tags
			continue if is_client ~= nil and tag.is_client == is_client
			continue if key ~= nil and key == "#{tag.vendor}/#{tag.key}"
			return tag

	--- Generate a new IRCClient
	-- @tparam string server IRC server name
	-- @tparam number port IRC port number
	-- @tparam table config Default configuration
	new: (server, port=6697, config)=>
		assert server
		@config = :server, :port, :config, ssl: port == 6697
		for k, v in pairs default_config
			@config[k] = v
		if config
			for k, v in pairs config
				@config[k] = v

		@commands = ContextTable!
		@hooks    = ContextTable!
		@handlers = ContextTable!
		@senders  = ContextTable!
		@server   = {}

	unpack = unpack or table.unpack

	get_priority = (options, fn)->
		return fn and options.priority or priority.DEFAULT

	handle_options: (options, fn)=>
		unless fn
			fn = options
			options = {}

		if options.async
			return (...)->
				args = {...}
				require("queue")\wrap ->
					@pcall_bare fn, unpack args

		if options.wrap_iter
			tmp_fn = coroutine.wrap fn
			tmp_fn!
			return tmp_fn

		fn

	assert_context: (context_table)=>
		assert @context, "Missing @with_context"
		return context_table[assert @context, "Missing @with_context"]

	--- Change the module context for the current command
	-- @tparam string context module context (typically, the name)
	-- @tparam function fn code to run under context
	with_context: (context, fn)=>
		assert @context == nil, "Already in context: #{@context}"
		@context = context

		self["senders"][context] = {} -- no priority for senders, only one
		for key in *{"hooks", "handlers", "commands"}
			self[key][context] = [{} for p=priority.HIGH, priority.LOW]

		fn self, context
		@context = nil

	--- Add an IRC bot command
	-- @tparam string name Bot command name
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function command Function for handling command
	add_command: (name, options, command)=>
		final_command = command
		commands = @assert_context @commands

		-- `options` is passed, which fills in `command`
		if command
			-- is async, send typing until completed
			if options.async
				-- async, send @+draft/typing
				final_command = (prefix, target, line, ...)=>
					@send 'TAGMSG', target, "+draft/typing": "active"
					@pcall command, prefix, target, line, ...
					@send 'TAGMSG', target, "+draft/typing": "done"

		p = get_priority options, final_command
		commands[p][name] = @handle_options options, final_command

	--- Add a client processing hook
	-- @tparam string name Name of event to hook into
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function hook Processor for hook event
	add_hook: (name, options, hook)=>
		hooks = @assert_context @hooks

		p = get_priority options, hook

		unless hooks[p][name]
			hooks[p][name] = {@handle_options options, hook}
		else
			table.insert hooks[p][name], @handle_options(options, hook)

	--- Add an IRC command handler
	-- @tparam string id IRC command ID (numerics MUST be strings)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function handler IRC command processor
	add_handler: (id, options, handler)=>
		handlers = @assert_context @handlers

		p = get_priority options, handler

		unless handlers[p][id]
			handlers[p][id] = {@handle_options options, handler}
		else
			table.insert hooks[p][name], @handle_options(options, handler)

	--- Add an IRC command sending handler, does NOT take a priority
	-- @tparam string id IRC command ID ("PRIVMSG", "JOIN", etc.)
	-- @tparam table options [Optional] async: bool, wraps in cqueues
	-- @tparam function sender Function to handle message to be sent
	add_sender: (id, options, sender)=>
		senders = @assert_context @senders
		senders[id] = @handle_options(options, sender)

	--- Reset all modules in the IRCClient
	clear_modules: ()=>
		@senders = {}
		@handlers = {}
		@hooks = {}
		@commands = {}

	--- Connect to the IRC server specified in the configuration
	connect: ()=>
		if @socket
			Logger.debug "Shutting down socket: #{tostring(@socket)}"
			@socket\shutdown!
		host = @config.server
		port = @config.port
		ssl  = @config.ssl
		debug_msg = ('Connecting... {host: "%s", port: "%s"}')\format host, port

		@config.nick = 'Moon-Moon' if not @config.nick
		@config.username = 'Mooooon' if not @config.username
		@config.realname = 'Moon Moon: MoonScript IRC Bot' if not @config.realname

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

	--- Disconnect from the current IRC server
	disconnect: ()=>
		@socket\shutdown! if @socket
		@fire_hook 'DISCONNECT'

	tag_escapes = {
		";": ":"
		" ": "s"
		"\r": "r"
		"\n": "n"
	}

	--- Serialize a value for being sent in a tag
	serialize_tag_string = (key, value)->
		return key if type(value) == "boolean"
		return key .. "=" .. value\gsub "([: \r\n])", (value)-> "\\#{tbl[value]}"

	--- Serialize client->client tags for sending to the server
	-- @param tags Table of tags to serialize
	serialize_tags = (tags)->
		output = [serialize_tag_string(k, v) for k, v in pairs tags]
		return "@#{table.concat output, ';'}"

	--- Send a raw line to the currently connected IRC server
	-- @param ... List of strings, concatenated using spaces
	send_raw: (...)=>
		output = {...}
		arg_count = select("#", ...)
		opts_table = select(arg_count, ...)
		if type(opts_table) == "table"
			-- we have tags, server accepts tags
			if opts_table.tags and next(opts_table.tags) and @server.ircv3_caps["message-tags"]
				table.insert output, 1, serialize_tags opts_table.tags
			-- remove opts_table regardless of caps
			table.remove output, #output
		arg_count = #output
		if output[arg_count]\find " "
			output[arg_count] = ":#{output[arg_count]}"
		@socket\write table.concat(output, ' ') .. '\n'
		Logger.debug '==> ' .. table.concat output, ' '

	--- Send a command using a builtin sender configured with @add_sender
	-- @tparam string name Name of sender to use
	-- @param ... List of arguments to be passed to sender
	-- @see IRCClient\add_sender
	send: (name, ...)=>
		sender = @senders\get name, multiple: false, index: IRCClient.senders, uses_priority: false
		input = sender self, ...
		@socket\write input if input

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

	--- Parse the time of an ISO 8601 compliant string
	-- @tparam string date ISO 8601 compliant YYYY-MM-DDThh:mm:ssZ date
	-- @treturn number Seconds from Epoch from configured date
	parse_time: (date)=>
		year, month, day, hour, min, sec, mil = date\match @date_pattern
		return os.time(:year, :month, :day, :hour, :min, :sec) + tonumber(mil) / 1000

	--- parse an IRC command using line_pattern
	parse: (line)=> line_pattern\match line

	--- Activate hooks configured for the event name
	-- @tparam string hook_name Name of event to "fire"
	-- @treturn boolean True if no errors were encountered, false otherwise
	-- @treturn table Table containing list of errors from hooks
	fire_hook: (hook_name, ...)=>
		Logger.debug "#{Logger.level.warn}--- Running hooks for #{hook_name}"
		has_run = false
		has_errors = false
		errors = {}

		for _, hook in pairs IRCClient.hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running global hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)
		for _, hook in pairs @hooks\get hook_name
			has_run = true unless has_run
			Logger.debug Logger.level.warn .. '--- Running hook: ' .. hook_name
			ok, err = @pcall hook, ...
			if not ok
				has_errors = true if not has_errors
				table.insert(errors, err)

		Logger.debug Logger.level.error .. "*** Hook not found for #{command}" unless has_run

		return has_errors, errors, has_run

	process_line: (line, opts={})=>
		{:tags, :prefix, :command, :args} = @parse line
		opts.line = line
		@process prefix, command, args, tags, opts

	--- Run handlers for an unparsed command
	-- @tparam table prefix Prefix argument from server (nick, user, host)
	-- @tparam table args Argument list from server, including trailing
	-- @tparam table tags Tags list from server
	-- @tparam table opts Optional arguments, in_batch: IRCv3 batching
	process: (prefix, command, args, tags, opts={})=>
		{:in_batch} = opts
		{:nick, :user, :host} = prefix
		if opts.line
			Logger.debug Logger.level.warn .. '--- | Line: ' .. opts.line
			Logger.debug Logger.level.okay .. '--- |\\ Running trigger: ' .. Logger.level.warn .. command
			if nick and user and host
				Logger.debug "#{Logger.level.okay}--- |\\ User: #{nick}!#{user}@#{host}"
			elseif nick
				Logger.debug "#{Logger.level.okay}--- |\\ Nick: #{nick}"
			if #args > 0
				Logger.debug "#{Logger.level.okay}--- |\\ Arguments: #{table.concat args, ' '}"

		has_run = false

		if not in_batch and tags.batch
			-- will return _, _, true if a hook run; this is necessary because
			-- the protocol might send batches even if we don't support them.
			return unless select(2, @fire_hook "BATCH.#{tags.batch}", prefix,
				args, tags, opts)

		for handler in *IRCClient.handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		for handler in *@handlers\get command
			has_run = true unless has_run
			@pcall handler, prefix, args, tags, opts

		Logger.debug Logger.level.error .. "*** Handler not found for #{command}" unless has_run

	--- Iterate over lines from a server and handle errors appropriately
	loop: ()=>
		for received_line in @socket\lines! do
			@pcall @process_line, received_line, in_batch: false

	--- Call a function and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, self, ...
		if not ok
			Logger.debug "Arguments:"
			for arg in *{...}
				Logger.debug tostring arg
		return ok, err

	--- Call a function without self and, when failed, print debug information
	-- @tparam function fn Function to be called
	-- @param ... vararg list to pass to function
	pcall_bare: (fn, ...)=>
		ok, err = xpcall fn, self\log_traceback, ...
		if not ok
			Logger.debug "Arguments:"
			Logger.debug tostring arg for arg in *{...}
		return ok, err

	--- Print a traceback using the internal logging mechanism
	-- @see IRCClient\log
	log_traceback: (err)=>
		err = tostring err
		Logger.print "#{Logger.level.error} *** #{err}"
		Logger.debug moonscript.errors.rewrite_traceback debug.traceback!, err
		Logger.debug "#{Logger.level.error} ---"
		return err

	--- Log message from IRC server (used in plugins)
	-- @tparam string input Line to print, IRC color formatted
	-- @see logger
	log: (input)=>
		for line in input\gmatch "[^\r\n]+"
			Logger.print '\00311(\003' .. (@server.caps and
				@server.caps['NETWORK'] or @config.server) ..
				"\00311)\003 #{line}"

return {:IRCClient, :priority}


plugins/command: Create proxy self to make sending messages easier

import IRCClient from require "irc"
import sleep from require "cqueues"

unpack = unpack or table.unpack

handle_error = (target, command, err)=>
	@send "COMMAND_ERR", target, command, "Error: #{err}"

IRCClient\add_sender 'COMMAND_OK', (target, command_name, message, msgid)=>
	if msgid
		@send "PRIVMSG", target, "[\00303#{command_name}\003] #{message}", "+draft/reply": msgid
	else
		@send "PRIVMSG", target, "[\00303#{command_name}\003] #{message}"

IRCClient\add_sender 'COMMAND_ERR', (target, command_name, message, msgid)=>
	if msgid
		@send "PRIVMSG", target, "[\00304!#{command_name}\003] #{message}", "+draft/reply": msgid
	else
		@send "PRIVMSG", target, "[\00304!#{command_name}\003] #{message}"

IRCClient\add_handler 'PRIVMSG', (prefix, args, tags)=>
	{target, message} = args

	cmd_prefix = @config.prefix
	return unless message\sub(1, #cmd_prefix) == cmd_prefix

	msgid = @get_tag(tags, key: 'msgid').value

	cmd_name = message\match "%S+", #cmd_prefix + 1
	command_args = {prefix, target, tags}
	command = @commands\get cmd_name, multiple: false, index: IRCClient.commands
	return @send "COMMAND_ERR", target, "core", "Command not found: #{cmd_name}", msgid unless command

	args_start = (message\find"%s" or #message) + 1
	args = [arg for arg in message\sub(args_start)\gmatch "%S+"]
	command_args[#command_args + 1] = table.concat args, " "
	command_args[#command_args + 1] = arg for arg in *args

	proxy_self = {
		-- need to fill in @send_ok and @send_err
		send_ok: (message)=>
			@send "COMMAND_OK", target, cmd_name, message, msgid

		send_err: (message)=>
			@send "COMMAND_ERR", target, cmd_name, message, msgid
	}

	setmetatable proxy_self, __index: self

	-- command proxy_self, unpack command_args
	ok, err = @pcall_bare command, proxy_self, unpack command_args
	unless ok
		handle_error self, target, cmd_name, err

IRCClient\add_command "test", async: true, (prefix, target, tags)=>
	{:nick} = prefix

	sleep 5

	if @users[nick] and @users[nick].account
		@send_ok "Account name: #{@users[nick].account}"
	else
		@send_err "Account not known for: #{nick}"

IRCClient\add_command "caps", (prefix, target)=>
	line = [k for k in pairs @server.ircv3_caps]
	@send_ok table.concat(line, " ")

IRCClient\add_command "test_err", (prefix, target)=>
	@send_err "Error result"

IRCClient\add_command "test_fail", (prefix, target)=>
	error "test"

import IRCClient from require "irc"
import sleep from require "cqueues"

unpack = unpack or table.unpack

handle_error = (target, command, err)=>
	@send "COMMAND_ERR", target, command, "Error: #{err}"

IRCClient\add_sender 'COMMAND_OK', (target, command_name, message, msgid)=>
	if msgid
		@send "PRIVMSG", target, "[\00303#{command_name}\003] #{message}", "+draft/reply": msgid
	else
		@send "PRIVMSG", target, "[\00303#{command_name}\003] #{message}"

IRCClient\add_sender 'COMMAND_ERR', (target, command_name, message, msgid)=>
	if msgid
		@send "PRIVMSG", target, "[\00304!#{command_name}\003] #{message}", "+draft/reply": msgid
	else
		@send "PRIVMSG", target, "[\00304!#{command_name}\003] #{message}"

IRCClient\add_handler 'PRIVMSG', (prefix, args, tags)=>
	{target, message} = args

	cmd_prefix = @config.prefix
	return unless message\sub(1, #cmd_prefix) == cmd_prefix

	msgid = @get_tag(tags, key: 'msgid').value

	cmd_name = message\match "%S+", #cmd_prefix + 1
	command_args = {prefix, target, tags}
	command = @commands\get cmd_name, multiple: false, index: IRCClient.commands
	return @send "COMMAND_ERR", target, "core", "Command not found: #{cmd_name}", msgid unless command

	args_start = (message\find"%s" or #message) + 1
	args = [arg for arg in message\sub(args_start)\gmatch "%S+"]
	command_args[#command_args + 1] = table.concat args, " "
	command_args[#command_args + 1] = arg for arg in *args

	command self, unpack command_args
	--ok, err = @pcall command, unpack command_args
	--unless ok
	--	handle_error self, target, cmd_name, err

IRCClient\add_command "test", async: true, (prefix, target, tags)=>
	{:nick} = prefix

	msgid = @get_tag(tags, key: 'msgid').value

	sleep 5

	if @users[nick] and @users[nick].account
		@send "COMMAND_OK", target, "test", "Account name: #{@users[nick].account}", msgid
	else
		@send "COMMAND_ERR", target, "test", "Account not found for: #{nick}", msgid

IRCClient\add_command "caps", (prefix, target)=>
	line = [k for k in pairs @server.ircv3_caps]
	@send "COMMAND_OK", target, "caps", table.concat(line, ", ")


Added needed require to Tween

Added needed require to Tween

require "LunoPunk.LP"
require "LunoPunk.utils.EventListener"
require "LunoPunk.tweens.TweenEvent"

export ^

TweenType = {
	Persist: "Persist"
	Looping: "Looping"
	OneShot: "OneShot"
}

class Tween
	-- Constructor. Specify basic information about the Tween.
	-- @param	duration		Duration of the tween (in seconds or frames).
	-- @param	type			Tween type, one of Tween.PERSIST (default), Tween.LOOPING, or Tween.ONESHOT.
	-- @param	complete		Optional callback for when the Tween completes.
	-- @param	ease			Optional easer function to apply to the Tweened value.
	new: (duration, type = TweenType.Persist, complete = nil, ease = nil) =>
		assert duration != nil, "Cannot create a Tween without a duration"
		@active = false
		@__scale = 0.0
		@__parent = nil
		@__finish = false

		@__target = duration
		@type = if type == nil then "Persist" else type
		@ease = ease
		@t = 0
		@__time = 0

		if complete != nil
			AddEventListener TweenEvent.FINISH!, complete

	-- Updates the Tween, called by World.
	update: =>
		@__time += if LP.fixed then 1 else LP.elapsed
		@t = @__time/@__target
		if @ease != nil and @t > 0 and @t < 1
			@t = @ease @t

		if @__time >= @__target
			@t = 1
			@__finish = true

		DispatchEvent TweenEvent TweenEvent.UPDATE!
		return

	-- Starts the Tween, or restarts it if it's currently running.
	start: =>
		@__time = 0
		if @__target == 0
			@active = false
			DispatchEvent TweenEvent TweenEvent.FINISH!
		else
			@active = true
			DispatchEvent TweenEvent TweenEvent.START!
		return

	-- @private Called when the Tween completes.
	finish = =>
		switch @type
			when "Persist"
				@__time = @__target
				@active = false
			when "Looping"
				@__time %= @__target
				@t = @__time/@__target
				if @ease != nil and @t > 0 and @t < 1
					@t = @ease @t
				@start!
			when "OneShot"
				@__time = @__target
				@active = false
				@parent\removeTween @
		@__finish = false
		DispatchEvent TweenEvent TweenEvent.FINISH!

	-- Immediately stops the Tween and removes it from its Tweener without calling the complete callback.
	cancel: =>
		@active = false
		@parent\removeTween @ if @parent != nil
		return

	percent: (value) =>
		return @__time/@__target if value == nil
		@__time = @__target * value
		@__time

	scale: =>
		@__scale


Emotes loading time

Emotes loading time


Added Fader Tween

Added Fader Tween

require "LunoPunk.Tween"

export ^

-- Global volume fader.
class Fader extends Tween
	-- Constructor.
	-- @param	complete	Optional completion callback.
	-- @param	type		Tween type.
	new: (complete, type)  =>
		-- Fader information.
		@__start, @__range = 0, 0
		super 0, type, complete

	-- Fades FP.volume to the target volume.
	-- @param	volume		The volume to fade to.
	-- @param	duration	Duration of the fade.
	-- @param	ease		Optional easer function.
	fadeTo: (volume, duration, ease = nil) =>
		volume = 0 if volume < 0
		@__start = LP.volume
		@__range = volume - @__start
		@__target = duration
		@__ease = ease
		@start!

	-- @private Updates the Tween.
	update: ()  =>
		super!
		LP.volume = @__start + @__range * @t