Aegisub example: check that line.raw exists in checkBounds.

Previously, checkBounds would silently insert nothing when line.raw was
nil.

This tries to be somewhat compatible with recent versions of the
a-mo.Line library by calling the createRaw method on the line if
line.raw is nil. Otherwise, it returns an error.

-- This library is unlicensed under CC0

ASSIVersionCompat = 0x000200
InspectorVersion  = 0x000201

ffi = require( 'ffi' )
bit = require( 'bit' )

ASSIVersionCompat_major = bit.rshift( ASSIVersionCompat, 16 )
ASSIVersionCompat_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
ASSIVersionCompat_patch = bit.band( ASSIVersionCompat, 0xFF )

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
} ASSI_Rect;

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

-- Figure out a better way to load this?
extension = ('OSX' == ffi.os and '.dylib' or 'Windows' == ffi.os and '.dll' or '.so')
libraryPath = "?user/automation/include/ASSInspector"
success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
if not success
	libraryPath = "?data/automation/include/ASSInspector"
	success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
	if not success
		error( "Could not load required libASSInspector library." )

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	ASSIVersion_major = bit.rshift( ASSIVersion, 16 )
	ASSIVersion_minor = bit.band( bit.rshift( ASSIVersion, 8 ), 0xFF )
	ASSIVersion_patch = bit.band( ASSIVersion, 0xFF )

	if ASSIVersion_major > ASSIVersionCompat_major
		return nil, "Inspector.moon library is too old."
	elseif ASSIVersion_major < ASSIVersionCompat_major or ASSIVersion_minor < ASSIVersionCompat_minor
		return nil, "libASSInspector library is too old."

	return true

-- Inspector is not actually a property of the class but a property of
-- the script. Since we don't need to worry about concurrency or
-- anything silly like that, we can initialize ASSInspector once and
-- keep that reference alive until Aegisub is closed or scripts are all
-- reloaded.
state = {
	inspector:        nil
	fontconfigConfig: nil
	fontDir:          nil
	resX:             nil
	resY:             nil
}

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert( scriptHeader, .raw )

				if "PlayResX" == .key
					resX = tonumber( .value )
				elseif "PlayResY" == .key
					resY = tonumber( .value )

			elseif "style" == .class
				unless seenStyles
					table.insert( scriptHeader, "[V4+ Styles]\n" )
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size( )
	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat( scriptHeader, '\n' )
	@styles = styles

initializeInspector = ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) ->
	if nil == state.inspector
		success, message = looseVersionCompare( ASSInspector.assi_getVersion! )
		unless success
			return nil, message

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

		if nil == state.inspector
			return nil, "ASSInspector library initialization failed."

		state.resX = 1
		state.resY = 1
		state.fontDir = fontDirectory
		state.fontconfigConfig = fontconfigConfig

	return true

validateRect = ( rect ) ->
	bounds = {
		x: tonumber( rect.x )
		y: tonumber( rect.y )
		w: tonumber( rect.w )
		h: tonumber( rect.h )
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms( 0 )

	if hasFrames
		for line in *lines
			with line
				for frame = ffms( .start_time ), true == .assi_exhaustive and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor( 0.5*( msff( frame ) + msff( frame + 1 ) ) )
					unless seenTimes[frameTime]
						table.insert( times, frameTime )
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert( times, .start_time )
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert( scriptText, @styles[line.style] )
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert( scriptText, @styles[styleName] )
				seenStyles[styleName] = true

class Inspector
	@version = InspectorVersion
	@version_major = bit.rshift( ASSIVersionCompat, 16 )
	@version_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
	@version_patch = bit.band( ASSIVersionCompat, 0xFF )

	new: ( subtitles, fontconfigConfig = aegisub.decode_path( libraryPath .. "/fonts.conf" ), fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

	forceLibraryReload: ( subtitles, fontconfigConfig, fontDirectory ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		state.inspector = nil
		-- let initializeInspector handle the default arguments.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.assi_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes( lines ) ) =>
		unless times
			return nil, "The render times table was empty."

		for i = 1,#lines
			line = lines[i]
			if nil == line.raw
				if line.createRaw
					line\createRaw!
				else
					return nil, "line.raw is missing from line #{i}."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert( scriptText, @styles.Default )

		for line in *lines
			addStyles( @, line, scriptText, seenStyles )

		table.insert( scriptText, '[Events]' )

		for line in *lines
			table.insert( scriptText, line.raw )

		scriptString = table.concat( scriptText, '\n' )
		if 0 < ASSInspector.assi_setScript( state.inspector, scriptString, #scriptString )
			return nil, "Could not set script" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		renderCount = #times
		cTimes = ffi.new( 'int32_t[?]', renderCount )
		cRects = ffi.new( 'ASSI_Rect[?]', renderCount )

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		rects = { }
		for i = 0, renderCount - 1
			table.insert( rects, validateRect( cRects[i] ) )

		return rects, times

return Inspector

-- This library is unlicensed under CC0

ASSIVersionCompat = 0x000200
InspectorVersion  = 0x000201

ffi = require( 'ffi' )
bit = require( 'bit' )

ASSIVersionCompat_major = bit.rshift( ASSIVersionCompat, 16 )
ASSIVersionCompat_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
ASSIVersionCompat_patch = bit.band( ASSIVersionCompat, 0xFF )

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
} ASSI_Rect;

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

-- Figure out a better way to load this?
extension = ('OSX' == ffi.os and '.dylib' or 'Windows' == ffi.os and '.dll' or '.so')
libraryPath = "?user/automation/include/ASSInspector"
success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
if not success
	libraryPath = "?data/automation/include/ASSInspector"
	success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
	if not success
		error( "Could not load required libASSInspector library." )

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	ASSIVersion_major = bit.rshift( ASSIVersion, 16 )
	ASSIVersion_minor = bit.band( bit.rshift( ASSIVersion, 8 ), 0xFF )
	ASSIVersion_patch = bit.band( ASSIVersion, 0xFF )

	if ASSIVersion_major > ASSIVersionCompat_major
		return nil, "Inspector.moon library is too old."
	elseif ASSIVersion_major < ASSIVersionCompat_major or ASSIVersion_minor < ASSIVersionCompat_minor
		return nil, "libASSInspector library is too old."

	return true

-- Inspector is not actually a property of the class but a property of
-- the script. Since we don't need to worry about concurrency or
-- anything silly like that, we can initialize ASSInspector once and
-- keep that reference alive until Aegisub is closed or scripts are all
-- reloaded.
state = {
	inspector:        nil
	fontconfigConfig: nil
	fontDir:          nil
	resX:             nil
	resY:             nil
}

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert( scriptHeader, .raw )

				if "PlayResX" == .key
					resX = tonumber( .value )
				elseif "PlayResY" == .key
					resY = tonumber( .value )

			elseif "style" == .class
				unless seenStyles
					table.insert( scriptHeader, "[V4+ Styles]\n" )
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size( )
	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat( scriptHeader, '\n' )
	@styles = styles

initializeInspector = ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) ->
	if nil == state.inspector
		success, message = looseVersionCompare( ASSInspector.assi_getVersion! )
		unless success
			return nil, message

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

		if nil == state.inspector
			return nil, "ASSInspector library initialization failed."

		state.resX = 1
		state.resY = 1
		state.fontDir = fontDirectory
		state.fontconfigConfig = fontconfigConfig

	return true

validateRect = ( rect ) ->
	bounds = {
		x: tonumber( rect.x )
		y: tonumber( rect.y )
		w: tonumber( rect.w )
		h: tonumber( rect.h )
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms( 0 )

	if hasFrames
		for line in *lines
			with line
				for frame = ffms( .start_time ), true == .assi_exhaustive and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor( 0.5*( msff( frame ) + msff( frame + 1 ) ) )
					unless seenTimes[frameTime]
						table.insert( times, frameTime )
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert( times, .start_time )
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert( scriptText, @styles[line.style] )
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert( scriptText, @styles[styleName] )
				seenStyles[styleName] = true

class Inspector
	@version = InspectorVersion
	@version_major = bit.rshift( ASSIVersionCompat, 16 )
	@version_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
	@version_patch = bit.band( ASSIVersionCompat, 0xFF )

	new: ( subtitles, fontconfigConfig = aegisub.decode_path( libraryPath .. "/fonts.conf" ), fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

	forceLibraryReload: ( subtitles, fontconfigConfig, fontDirectory ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		state.inspector = nil
		-- let initializeInspector handle the default arguments.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.assi_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes( lines ) ) =>
		unless times
			return nil, "The render times table was empty."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert( scriptText, @styles.Default )

		for line in *lines
			addStyles( @, line, scriptText, seenStyles )

		table.insert( scriptText, '[Events]' )

		for line in *lines
			table.insert( scriptText, line.raw )

		scriptString = table.concat( scriptText, '\n' )
		if 0 < ASSInspector.assi_setScript( state.inspector, scriptString, #scriptString )
			return nil, "Could not set script" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		renderCount = #times
		cTimes = ffi.new( 'int32_t[?]', renderCount )
		cRects = ffi.new( 'ASSI_Rect[?]', renderCount )

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		rects = { }
		for i = 0, renderCount - 1
			table.insert( rects, validateRect( cRects[i] ) )

		return rects, times

return Inspector


Reduced minimal

Reduced minimal

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

PPM2.MIN_WEIGHT = 0.7
PPM2.MAX_WEIGHT = 1.3

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

PPM2.MIN_IRIS = 0.4
PPM2.MAX_IRIS = 1.3

PPM2.MIN_HOLE = 0.1
PPM2.MAX_HOLE = .95

PPM2.MIN_PUPIL_SIZE = 0.2
PPM2.MAX_PUPIL_SIZE = 1

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

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

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

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

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

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

PPM2.DefaultCutiemarks = {
    '8ball', 'dice', 'magichat',
    'magichat02', 'record', 'microphone',
    'bits', 'checkered', 'lumps',
    'mirror', 'camera', 'magnifier',
    'padlock', 'binaryfile', 'floppydisk',
    'cube', 'bulb', 'battery',
    'deskfan', 'flames', 'alarm',
    'myon', 'beer', 'berryglass',
    'roadsign', 'greentree', 'seasons',
    'palette', 'palette02', 'palette03',
    'lightningstone', 'partiallycloudy', 'thunderstorm',
    'storm', 'stoppedwatch', 'twistedclock',
    'surfboard', 'surfboard02', 'star',
    'ussr', 'vault', 'anarchy',
    'suit', 'deathscythe', 'shoop',
    'smiley', 'dawsome', 'weegee'
    'applej', 'applem', 'bon_bon', 'carrots', 'celestia', 'cloudy', 'custom01', 'custom02', 'derpy', 'firezap',
    'fluttershy', 'fruits', 'island', 'lyra', 'mine', 'note', 'octavia', 'pankk', 'pinkie_pie', 'rainbow_dash',
    'rarity', 'rosen', 'sflowers', 'storm', 'time', 'time2', 'trixie', 'twilight', 'waters', 'weer', 'zecora'
}

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

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

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

PPM2.AvaliableEyeTypes = {
    'DEFAULT', 'APERTURE'
}

PPM2.AvaliablePonyWings = {'DEFAULT', 'BATPONY'}

PPM2.AvaliablePonySuits = {'NONE', 'ROYAL_GUARD', 'SHADOWBOLTS_FULL', 'SHADOWBOLTS_LIGHT', 'WONDERBOLTS_FULL', 'WONDERBOLTS_LIGHT'}

do
    i = -1
    for mark in *PPM2.DefaultCutiemarks
        i += 1
        PPM2["CMARK_#{mark\upper()}"] = i

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

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

PPM2.MIN_TAILS_NEW = 0
PPM2.MAX_TAILS_NEW = #PPM2.AvaliableTailsNew - 1

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

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

PPM2.MIN_UPPER_MANES_NEW = 0
PPM2.MAX_UPPER_MANES_NEW = #PPM2.AvaliableUpperManesNew - 1

PPM2.MIN_LOWER_MANES_NEW = 0
PPM2.MAX_LOWER_MANES_NEW = #PPM2.AvaliableLowerManesNew - 1

PPM2.MIN_EYE_TYPE = 0
PPM2.MAX_EYE_TYPE = #PPM2.AvaliableEyeTypes - 1

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

PPM2.MIN_CMARK = 0
PPM2.MAX_CMARK = #PPM2.DefaultCutiemarks - 1

PPM2.MIN_SUIT = 0
PPM2.MAX_SUIT = #PPM2.AvaliablePonySuits - 1

PPM2.MIN_WINGS = 0
PPM2.MAX_WINGS = #PPM2.AvaliablePonyWings - 1

PPM2.GENDER_FEMALE = 0
PPM2.GENDER_MALE = 1

PPM2.MAX_BODY_DETAILS = 8

PPM2.RACE_EARTH = 0
PPM2.RACE_PEGASUS = 1
PPM2.RACE_UNICORN = 2
PPM2.RACE_ALICORN = 3
PPM2.RACE_ENUMS = {'EARTH', 'PEGASUS', 'UNICORN', 'ALICORN'}

PPM2.AGE_FILLY = 0
PPM2.AGE_ADULT = 1
PPM2.AGE_MATURE = 2
PPM2.AGE_ENUMS = {'FILLY', 'ADULT', 'MATURE'}

PPM2.MIN_DERP_STRENGTH = 0.1
PPM2.MAX_DERP_STRENGTH = 1.3

PPM2.MIN_MALE_BUFF = 0
PPM2.DEFAULT_MALE_BUFF = 1
PPM2.MAX_MALE_BUFF = 2

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

PPM2.MIN_WEIGHT = 0.7
PPM2.MAX_WEIGHT = 1.3

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

PPM2.MIN_IRIS = 0.7
PPM2.MAX_IRIS = 1.3

PPM2.MIN_HOLE = 0.1
PPM2.MAX_HOLE = .95

PPM2.MIN_PUPIL_SIZE = 0.2
PPM2.MAX_PUPIL_SIZE = 1

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

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

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

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

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

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

PPM2.DefaultCutiemarks = {
    '8ball', 'dice', 'magichat',
    'magichat02', 'record', 'microphone',
    'bits', 'checkered', 'lumps',
    'mirror', 'camera', 'magnifier',
    'padlock', 'binaryfile', 'floppydisk',
    'cube', 'bulb', 'battery',
    'deskfan', 'flames', 'alarm',
    'myon', 'beer', 'berryglass',
    'roadsign', 'greentree', 'seasons',
    'palette', 'palette02', 'palette03',
    'lightningstone', 'partiallycloudy', 'thunderstorm',
    'storm', 'stoppedwatch', 'twistedclock',
    'surfboard', 'surfboard02', 'star',
    'ussr', 'vault', 'anarchy',
    'suit', 'deathscythe', 'shoop',
    'smiley', 'dawsome', 'weegee'
    'applej', 'applem', 'bon_bon', 'carrots', 'celestia', 'cloudy', 'custom01', 'custom02', 'derpy', 'firezap',
    'fluttershy', 'fruits', 'island', 'lyra', 'mine', 'note', 'octavia', 'pankk', 'pinkie_pie', 'rainbow_dash',
    'rarity', 'rosen', 'sflowers', 'storm', 'time', 'time2', 'trixie', 'twilight', 'waters', 'weer', 'zecora'
}

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

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

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

PPM2.AvaliableEyeTypes = {
    'DEFAULT', 'APERTURE'
}

PPM2.AvaliablePonyWings = {'DEFAULT', 'BATPONY'}

PPM2.AvaliablePonySuits = {'NONE', 'ROYAL_GUARD', 'SHADOWBOLTS_FULL', 'SHADOWBOLTS_LIGHT', 'WONDERBOLTS_FULL', 'WONDERBOLTS_LIGHT'}

do
    i = -1
    for mark in *PPM2.DefaultCutiemarks
        i += 1
        PPM2["CMARK_#{mark\upper()}"] = i

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

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

PPM2.MIN_TAILS_NEW = 0
PPM2.MAX_TAILS_NEW = #PPM2.AvaliableTailsNew - 1

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

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

PPM2.MIN_UPPER_MANES_NEW = 0
PPM2.MAX_UPPER_MANES_NEW = #PPM2.AvaliableUpperManesNew - 1

PPM2.MIN_LOWER_MANES_NEW = 0
PPM2.MAX_LOWER_MANES_NEW = #PPM2.AvaliableLowerManesNew - 1

PPM2.MIN_EYE_TYPE = 0
PPM2.MAX_EYE_TYPE = #PPM2.AvaliableEyeTypes - 1

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

PPM2.MIN_CMARK = 0
PPM2.MAX_CMARK = #PPM2.DefaultCutiemarks - 1

PPM2.MIN_SUIT = 0
PPM2.MAX_SUIT = #PPM2.AvaliablePonySuits - 1

PPM2.MIN_WINGS = 0
PPM2.MAX_WINGS = #PPM2.AvaliablePonyWings - 1

PPM2.GENDER_FEMALE = 0
PPM2.GENDER_MALE = 1

PPM2.MAX_BODY_DETAILS = 8

PPM2.RACE_EARTH = 0
PPM2.RACE_PEGASUS = 1
PPM2.RACE_UNICORN = 2
PPM2.RACE_ALICORN = 3
PPM2.RACE_ENUMS = {'EARTH', 'PEGASUS', 'UNICORN', 'ALICORN'}

PPM2.AGE_FILLY = 0
PPM2.AGE_ADULT = 1
PPM2.AGE_MATURE = 2
PPM2.AGE_ENUMS = {'FILLY', 'ADULT', 'MATURE'}

PPM2.MIN_DERP_STRENGTH = 0.1
PPM2.MAX_DERP_STRENGTH = 1.3

PPM2.MIN_MALE_BUFF = 0
PPM2.DEFAULT_MALE_BUFF = 1
PPM2.MAX_MALE_BUFF = 2


qr fix

qr fix

import Widget from require "lapis.html"
import escape from require "lapis.util"
class extends Widget
  content: =>
    if @invkey
      h1 "Invite key created"
      img style: "display: block; margin: 10px auto;width: 300px", src: "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chld=M|3&choe=UTF-8&chl="..escape("http://kinky-eureka.com/users/register?key="..@invkey)
      text "Give the following key to the person who wants to register as "
      b style: "background: rgba(0,0,0,0.8); padding: 2px;", @params.username
      text ":"
      pre style: "background: rgba(0,0,0,0.8); padding: 1em; margin: 1em; text-align: center;",->
        code ->
          text @invkey
      h2 "Create another"
    else
      h1 "Create invite key"
    form action: @url_for("make_invite_key_form"), method: "post", class: "pure-form pure-form-aligned", ->
      fieldset ->
        div class: "pure-control-group", ->
          label for: "username", "Nick name:"
          input id: "username",name: "username", value: @params.username or ""
          text " they'll have to register using this name."
        div class: "pure-controls", ->
          input type: "submit", class: "pure-button pure-button-primary"

import Widget from require "lapis.html"

class extends Widget
  content: =>
    if @invkey
      h1 "Invite key created"
      img style: "display: block; margin: 10px auto;width: 300px", src: "https://chart.googleapis.com/chart?chs=300x300&cht=qr&chld=M|3&chl=Hello%20world&choe=UTF-8"
      text "Give the following key to the person who wants to register as "
      b style: "background: rgba(0,0,0,0.8); padding: 2px;", @params.username
      text ":"
      pre style: "background: rgba(0,0,0,0.8); padding: 1em; margin: 1em; text-align: center;",->
        code ->
          text @invkey
      h2 "Create another"
    else
      h1 "Create invite key"
    form action: @url_for("make_invite_key_form"), method: "post", class: "pure-form pure-form-aligned", ->
      fieldset ->
        div class: "pure-control-group", ->
          label for: "username", "Nick name:"
          input id: "username",name: "username", value: @params.username or ""
          text " they'll have to register using this name."
        div class: "pure-controls", ->
          input type: "submit", class: "pure-button pure-button-primary"


rss: fix indent, more error handling, general improv

-- Module for subscribing to RSS and announce to channels
feedparser = require 'feedparser' -- http://feedparser.luaforge.net/
{:simplehttp, :json, :urlEncode, :bold} = require'util'
html2unicode = require 'html'
sql = require'lsqlite3'

moduleName = 'rss'

get_db = ->
  db = sql.open 'cache/rss.sql'

  code = db\exec [[
    CREATE TABLE IF NOT EXISTS feed (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT,
      rssurl VARCHAR(1024) NOT NULL,
      url VARCHAR(1024),
      title VARCHAR(1024),
      author VARCHAR(1024),
      etag TEXT,
      lastmodified TEXT
    );

    CREATE TABLE IF NOT EXISTS item (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      feed_id INTEGER NOT NULL,
      guid VARCHAR(64),
      title VARCHAR(1024),
      author VARCHAR(1024),
      url VARCHAR(1024),
      feedurl VARCHAR(1024),
      pubDate TEXT,
      summary TEXT,
      content TEXT,
      FOREIGN KEY(feed_id) REFERENCES feed(id),
      UNIQUE (guid, feed_id)
    );

    CREATE TABLE IF NOT EXISTS subscription (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      destination varchar(100),
      feed_id INTEGER,
      last INTEGER DEFAULT 0,
      created_at timestamp DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY(feed_id) REFERENCES feed(id)
    );
    ]]

  if code != sql.OK
    ivar2\Log 'error', "Error during opendb: #{db\errmsg!}"
    return nil

  return db

prepare = (db, stmt, ...) ->
  code = db\prepare stmt
  unless code
    ivar2\Log 'error', "Error with statement prepare: #{db\errmsg!}"
    return nil
  code\bind_values ...
  return code

-- Function that add functionality to extract specific information for specific sites
feedSpecific = (feedurl, link, title, summary, content) ->
  if feedurl\match 'https://comics.io/my/'

    title = summary\match '
(.-)
' or ''
    img, alt = summary\match '
'
    return "#{html2unicode title} #{img} #{html2unicode alt}"

  return "#{title} - #{link}"

announce = (id) ->
  db = get_db!
  stmt = prepare db, "SELECT DISTINCT feed.id, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id WHERE feed.id = ?", id
  for row in stmt\rows!
    destination = row[2]

    highest = 0
    -- Check last value
    stmt = prepare db, "SELECT last FROM subscription WHERE feed_id = ? and destination = ?", id, destination
    stmt\step!
    value = stmt\get_values()[1]
    if value and value ~= '' and tonumber(value) and tonumber(value) > 0
      highest = value

    ivar2\Log 'debug', 'Got highest %s, %s, %s', id, destination, highest

    stmt = prepare db, "SELECT feed.id AS feed_id, item.id AS item_id, rssurl, pubDate, name, etag, item.url AS link, item.title, content, summary FROM feed JOIN item on item.feed_id = feed.id WHERE feed.id = ? AND item.id > ? ORDER BY item.id DESC LIMIT 100", id, highest
    out = {}
    for item in stmt\nrows!
      with item
        if .item_id > highest
          highest = .item_id
        table.insert out, "[#{bold .name}]: #{feedSpecific(.rssurl, .link, .title, .summary, .content)}"
    if #out > 0
      ivar2\Msg 'privmsg', destination, ivar2.nick, "RSS "..table.concat(out, ', ')
    -- Update last red
    ins = prepare db, 'UPDATE subscription SET last=? WHERE feed_id = ? and destination = ?', highest, id, destination
    code, err = ins\step!
    code, err = ins\finalize!
  db\close!

poll = ->
  db = get_db!
  channels = {}
  for c,_ in pairs(ivar2.channels)
    channels[c] = true
  for row in db\rows "SELECT DISTINCT feed.id, rssurl, lastmodified, name, etag, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id"
  -- Ignore feeds this particular instance of the bot does not subscribe to
    if not channels[row[6]]
      continue
    id = row[1]
    rssurl = row[2]
    lm = row[3]
    name = row[4]
    etag = row[5]
    headers = {}
    if lm and lm != '' then
      headers['If-Modified-Since'] = lm
    if etag and etag != '' then
      headers['If-None-Match'] = etag

    simplehttp {url:rssurl,headers:headers}, (data, url, response) ->
      -- Reopen DB because of async
      sdb = get_db!

      lastModified = response.headers['Last-Modified']
      if not lastModified
        lastModified = response.headers['Date']

      -- Unmodified content
      if response.status_code == 304 or not data
        -- return from the simplehttp callback
        return

      ok, feed, err = pcall -> feedparser.parse data
      if not ok
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{feed}> data for feed: <#{name}> with URL <#{url}>"
        return
      if err
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{err}> data for feed: <#{name}> with URL <#{url}>"
        return
      else
        title = feed.title
        author = feed.author
        url = feed.link

        -- Update feed with new values
        ins = sdb\prepare 'UPDATE feed SET title=?, author=?, url=?, lastmodified=? WHERE id=?'
        code, err = ins\bind_values title, author, url, lastModified, id
        code, err = ins\step!
        code, err = ins\finalize!

        out = {}
        for i, e in ipairs(feed.entries)
          -- Attempt to get a unique entry ID
          guid = e.guid
          if not guid then guid = e.id
          if not guid then guid = e.link
          if not guid
            ivar2\Log 'error', "#{moduleName}: No GUID when parsing entry: <#{e}> data for feed: <#{name}> with URL <#{url}>"
            break

          ins = sdb\prepare [[
            INSERT OR ABORT INTO
              item(feed_id, guid, title, author, url, pubDate, content, summary)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)
          ]]
          if not ins then
            ivar2\Log 'error', "#{moduleName}: Error during inserting entry: <#{err}> feed: <#{name}> with URL <#{url}> and position: #{i}"
          else
            ivar2\Log 'info', "#{moduleName}: Inserting entry: #{e.link}, #{e.title}"
            code = ins\bind_values id, guid, e.title, e.author, e.link, e.updated, e.content, e.summary
            code = ins\step!
            code = ins\finalize!
            if code == sql.CONSTRAINT -- duplicate value
              ivar2\Log 'info', "Reached duplicate link, breaking"
              break
      sdb\close!
      announce(id)
  db\close!

subscribe = (source, destination, name, url) =>
  db = get_db!
  ins = db\prepare "INSERT INTO feed (name, rssurl) VALUES(?, ?)"
  code = ins\bind_values name, url
  code = ins\step!
  code = ins\finalize!
  ins = db\prepare "INSERT INTO subscription (feed_id, destination) VALUES(?, ?)"
  code = ins\bind_values db\last_insert_rowid!, destination
  code = ins\step!
  code = ins\finalize!
  db\close!
  reply "Ok. Subscribed to #{bold name}"
  poll!

unsubscribe = (source, destination, name) =>
  db = get_db!
  ins = db\prepare "DELETE FROM subscription WHERE feed_id IN (select id from feed where name = ?) and destination = ?"
  code = ins\bind_values name, destination
  code = ins\step!
  code = ins\finalize!
  total_changes = db\total_changes!
  db\close!
  if total_changes < 1
    reply "Wasn't subscribed. But, sure."
  else
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  out = {}
  db = get_db!
  stmt = db\prepare("SELECT name FROM feed JOIN subscription ON feed.id = subscription.feed_id WHERE destination = ?")
  stmt\bind_values destination
  for row in stmt\rows!
    out[#out+1] = row[1]
  if #out > 0 then
    reply "Subscribed to: #{table.concat(out, ', ')}"
  else
    reply "Not subscribed to any feeds."

  db\close!

-- Start the subscribe poller
ivar2\Timer(moduleName, 60*5, 60*5, poll)

PRIVMSG:
  '^%prss latest (.*)': getLatest
  '^%prss subscribe (.-) (.*)': subscribe
  '^%prss unsubscribe (.*)': unsubscribe
  '^%prss list': list
  -- for debugging
  '^%prss poll': poll

-- Module for subscribing to RSS and announce to channels
feedparser = require 'feedparser' -- http://feedparser.luaforge.net/
{:simplehttp, :json, :urlEncode, :bold} = require'util'
html2unicode = require 'html'
sql = require'lsqlite3'

moduleName = 'rss'

get_db = ->
  db = sql.open 'cache/rss.sql'

  code = db\exec [[
    CREATE TABLE IF NOT EXISTS feed (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT,
      rssurl VARCHAR(1024) NOT NULL,
      url VARCHAR(1024),
      title VARCHAR(1024),
      author VARCHAR(1024),
      etag TEXT,
      lastmodified TEXT
    );

    CREATE TABLE IF NOT EXISTS item (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      feed_id INTEGER NOT NULL,
      guid VARCHAR(64),
      title VARCHAR(1024),
      author VARCHAR(1024),
      url VARCHAR(1024),
      feedurl VARCHAR(1024),
      pubDate TEXT,
      summary TEXT,
      content TEXT,
      FOREIGN KEY(feed_id) REFERENCES feed(id),
      UNIQUE (guid, feed_id)
    );

    CREATE TABLE IF NOT EXISTS subscription (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      destination varchar(100),
      feed_id INTEGER,
      last INTEGER DEFAULT 0,
      created_at timestamp DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY(feed_id) REFERENCES feed(id)
    );
    ]]

  if code != sql.OK
    ivar2\Log 'error', "Error during opendb: #{db\errmsg!}"
    return nil

  return db

prepare = (db, stmt, ...) ->
  code = db\prepare stmt
  unless code
    ivar2\Log 'error', "Error with statement prepare: #{db\errmsg!}"
    return nil
  code\bind_values ...
  return code

-- Function that add functionality to extract specific information for specific sites
feedSpecific = (feedurl, link, title, summary, content) ->
  if feedurl\match 'https://comics.io/my/'

    title = summary\match '
(.-)
' or ''
    img, alt = summary\match '
'
    return "#{html2unicode title} #{img} #{html2unicode alt}"

  return "#{title} - #{link}"

announce = (id) ->
  db = get_db!
  stmt = prepare db, "SELECT DISTINCT feed.id, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id WHERE feed.id = ?", id
  for row in stmt\rows!
    destination = row[2]

    highest = 0
    -- Check last value
    stmt = prepare db, "SELECT last FROM subscription WHERE feed_id = ? and destination = ?", id, destination
    stmt\step!
    value = stmt\get_values()[1]
    if value and value ~= '' and tonumber(value) and tonumber(value) > 0
      highest = value

    ivar2\Log 'debug', 'Got highest %s, %s, %s', id, destination, highest

    stmt = prepare db, "SELECT feed.id AS feed_id, item.id AS item_id, rssurl, pubDate, name, etag, item.url AS link, item.title, content, summary FROM feed JOIN item on item.feed_id = feed.id WHERE feed.id = ? AND item.id > ? ORDER BY item.id DESC LIMIT 100", id, highest
    out = {}
    for item in stmt\nrows!
      with item
        if .item_id > highest
          highest = .item_id
          table.insert out, "[#{bold .name}]: #{feedSpecific(.rssurl, .link, .title, .summary, .content)}"
    if #out > 0
      ivar2\Msg 'privmsg', destination, ivar2.nick, "RSS "..table.concat(out, ', ')
    -- Update last red
    ins = prepare db, 'UPDATE subscription SET last=? WHERE feed_id = ? and destination = ?', highest, id, destination
    code, err = ins\step!
    code, err = ins\finalize!
  db\close!

poll = ->
  db = get_db!
  channels = {}
  for c,_ in pairs(ivar2.channels)
    channels[c] = true
  for row in db\rows "SELECT DISTINCT feed.id, rssurl, lastmodified, name, etag, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id"
  -- Ignore feeds this particular instance of the bot does not subscribe to
    if not channels[row[6]]
      continue
    id = row[1]
    rssurl = row[2]
    lm = row[3]
    name = row[4]
    etag = row[5]
    headers = {}
    if lm and lm != '' then
      headers['If-Modified-Since'] = lm
    if etag and etag != '' then
      headers['If-None-Match'] = etag

    simplehttp {url:rssurl,headers:headers}, (data, url, response) ->
      -- Reopenn DB because of async
      sdb = get_db!

      lastModified = response.headers['Last-Modified']
      if not lastModified
        lastModified = response.headers['Date']

      -- Unmodified content
      if response.status_code == 304
        -- return from the simplehttp callback
        return

      feed, err = feedparser.parse data
      if err
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{err}> data for feed: <#{name}> with URL <#{url}>"
      else
        title = feed.title
        author = feed.author
        url = feed.link

        -- Update feed with new values
        ins = sdb\prepare 'UPDATE feed SET title=?, author=?, url=?, lastmodified=? WHERE id=?'
        code, err = ins\bind_values title, author, url, lastModified, id
        code, err = ins\step!
        code, err = ins\finalize!

        out = {}
        for i, e in ipairs(feed.entries)
          -- Attempt to get a unique entry ID
          guid = e.guid
          if not guid then guid = e.id
          if not guid then guid = e.link
          if not guid
            ivar2\Log 'error', "#{moduleName}: No GUID when parsing entry: <#{e}> data for feed: <#{name}> with URL <#{url}>"
            break

          --if guid == last
          --  break
          ins = sdb\prepare [[
            INSERT OR ABORT INTO
              item(feed_id, guid, title, author, url, pubDate, content, summary)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)
          ]]
          if not ins then
            ivar2\Log 'error', "#{moduleName}: Error during inserting entry: <#{err}> feed: <#{name}> with URL <#{url}> and position: #{i}"
          else
            code = ins\bind_values id, guid, e.title, e.author, e.link, e.updated, e.content, e.summary
            code = ins\step!
            code = ins\finalize!
        announce(id)
      sdb\close!
  db\close!

subscribe = (source, destination, name, url) =>
  db = get_db!
  ins = db\prepare "INSERT INTO feed (name, rssurl) VALUES(?, ?)"
  code = ins\bind_values name, url
  code = ins\step!
  code = ins\finalize!
  ins = db\prepare "INSERT INTO subscription (feed_id, destination) VALUES(?, ?)"
  code = ins\bind_values db\last_insert_rowid!, destination
  code = ins\step!
  code = ins\finalize!
  db\close!
  reply "Ok. Subscribed to #{bold name}"
  poll!

unsubscribe = (source, destination, name) =>
  db = get_db!
  ins = db\prepare "DELETE FROM subscription WHERE feed_id IN (select id from feed where name = ?) and destination = ?"
  code = ins\bind_values name, destination
  code = ins\step!
  code = ins\finalize!
  total_changes = db\total_changes!
  db\close!
  if total_changes < 1
    reply "Wasn't subscribed. But, sure."
  else
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  out = {}
  db = get_db!
  stmt = db\prepare("SELECT name FROM feed JOIN subscription ON feed.id = subscription.feed_id WHERE destination = ?")
  stmt\bind_values destination
  for row in stmt\rows!
    out[#out+1] = row[1]
  if #out > 0 then
    reply "Subscribed to: #{table.concat(out, ', ')}"
  else
    reply "Not subscribed to any feeds."

  db\close!

-- Start the subscribe poller
ivar2\Timer(moduleName, 60*5, 60*5, poll)

PRIVMSG:
  '^%prss latest (.*)': getLatest
  '^%prss subscribe (.-) (.*)': subscribe
  '^%prss unsubscribe (.*)': unsubscribe
  '^%prss list': list
  -- for debugging
  '^%prss poll': poll


Added method to save Blob.

Added method to save Blob.

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

glurg = require "glurg"
Promise = require "glurg.common.Promise"

class Blob
	new: (blob, size) =>
		if type(blob) == "userdata"
			Promise.keep("blob", Promise.IsUserdata(blob))

			if size != nil
				Promise.keep("size", Promise.IsNumber(size))
				Promise.keep("size >= 0", size >= 0)

				@_blob = false
				@data = blob
				@length = size
				@is_mutable = false
			else
				@_blob = blob
				@data = blob.data
				@length = blob.length
				@is_mutable = false
		elseif type(blob) == "number"
			Promise.keep("blob >= 1", blob >= 1)

			@_blob = false
			@data = glurg.common.create_data(blob)
			@length = blob
			@is_mutable = true
		else
			@_blob = false
			@data = nil
			@length = 0
			@is_mutable = false

	get_mutable: =>
		blob = Blob(nil)
		blob.data = glurg.common.duplicate_data(@data, @length)
		blob.length = @length
		blob.is_mutable = true

		return blob

	cast: =>
		return glurg.common.cast_data(@data)

	raw_slice: (offset) =>
		Promise.keep("offset <= @length", offset <= @length)

		return glurg.common.slice_data(@data, offset)

	make_slice: (offset) =>
		Promise.keep("offset <= @length", offset <= @length)

		slice = Blob(glurg.common.slice_data(@data, offset), @length - offset)
		slice.is_mutable = @is_mutable

		return slice

	copy: (other, num_bytes, offset, other_offset) =>
		offset = offset or 0
		other_offset = other_offset or 0

		Promise.keep("other", Promise.IsClass(other, Blob))
		Promise.keep("num_bytes", Promise.IsNumber(num_bytes))
		Promise.keep("num_bytes >= 0", num_bytes >= 0)
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("offset + num_bytes <= @length",
			offset + num_bytes <= @length)
		Promise.keep("other_offset >= 0", other_offset >= 0)
		Promise.keep("other_offset + num_bytes <= other.length",
			other_offset + num_bytes <= other.length)

		glurg.common.copy_data(
			@\raw_slice(offset), other\raw_slice(other_offset), num_bytes)

	save: (filename) =>
		Promise.keep("filename", Promise.IsString(filename))

		fileStream = glurg.common.SimpleFileStream.new!
		fileStream\open(filename, glurg.common.file_mode_write)
		fileStream\write(@data, @length)
		fileStream\close!

	calculate_hash: (offset, size) =>
		offset = offset or 0
		size = size or @length

		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("size", Promise.IsNumber(size))
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("size >= 1", size >= 1)
		Promise.keep("offset + size <= @length", offset + size <= @length)

		return glurg.common.hash_data(@\raw_slice(offset), size)\to_string!

	write_byte: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("value", Promise.IsNumber(value))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_byte(@data, offset, value)

	write_unsigned_byte: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_unsigned_byte(@data, offset, value)

	write_short: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_short(@data, offset, value)

	write_unsigned_short: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_unsigned_short(@data, offset, value)

	write_integer: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_integer(@data, offset, value)

	write_unsigned_integer: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_unsigned_integer(@data, offset, value)

	write_float: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_float(@data, offset, value)

	read_byte: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_byte(@data, offset)

	read_unsigned_byte: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_unsigned_byte(@data, offset)

	read_short: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_short(@data, offset)

	read_unsigned_short: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_unsigned_short(@data, offset)

	read_integer: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_integer(@data, offset)

	read_unsigned_integer: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_unsigned_integer(@data, offset)

	read_float: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_float(@data, offset)

return Blob

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

glurg = require "glurg"
Promise = require "glurg.common.Promise"

class Blob
	new: (blob, size) =>
		if type(blob) == "userdata"
			Promise.keep("blob", Promise.IsUserdata(blob))

			if size != nil
				Promise.keep("size", Promise.IsNumber(size))
				Promise.keep("size >= 0", size >= 0)

				@_blob = false
				@data = blob
				@length = size
				@is_mutable = false
			else
				@_blob = blob
				@data = blob.data
				@length = blob.length
				@is_mutable = false
		elseif type(blob) == "number"
			Promise.keep("blob >= 1", blob >= 1)

			@_blob = false
			@data = glurg.common.create_data(blob)
			@length = blob
			@is_mutable = true
		else
			@_blob = false
			@data = nil
			@length = 0
			@is_mutable = false

	get_mutable: =>
		blob = Blob(nil)
		blob.data = glurg.common.duplicate_data(@data, @length)
		blob.length = @length
		blob.is_mutable = true

		return blob

	cast: =>
		return glurg.common.cast_data(@data)

	raw_slice: (offset) =>
		Promise.keep("offset <= @length", offset <= @length)

		return glurg.common.slice_data(@data, offset)

	make_slice: (offset) =>
		Promise.keep("offset <= @length", offset <= @length)

		slice = Blob(glurg.common.slice_data(@data, offset), @length - offset)
		slice.is_mutable = @is_mutable

		return slice

	copy: (other, num_bytes, offset, other_offset) =>
		offset = offset or 0
		other_offset = other_offset or 0

		Promise.keep("other", Promise.IsClass(other, Blob))
		Promise.keep("num_bytes", Promise.IsNumber(num_bytes))
		Promise.keep("num_bytes >= 0", num_bytes >= 0)
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("offset + num_bytes <= @length",
			offset + num_bytes <= @length)
		Promise.keep("other_offset >= 0", other_offset >= 0)
		Promise.keep("other_offset + num_bytes <= other.length",
			other_offset + num_bytes <= other.length)

		glurg.common.copy_data(
			@\raw_slice(offset), other\raw_slice(other_offset), num_bytes)

	calculate_hash: (offset, size) =>
		offset = offset or 0
		size = size or @length

		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("size", Promise.IsNumber(size))
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("size >= 1", size >= 1)
		Promise.keep("offset + size <= @length", offset + size <= @length)

		return glurg.common.hash_data(@\raw_slice(offset), size)\to_string!

	write_byte: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("value", Promise.IsNumber(value))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_byte(@data, offset, value)

	write_unsigned_byte: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_unsigned_byte(@data, offset, value)

	write_short: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_short(@data, offset, value)

	write_unsigned_short: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_unsigned_short(@data, offset, value)

	write_integer: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_integer(@data, offset, value)

	write_unsigned_integer: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_unsigned_integer(@data, offset, value)

	write_float: (offset, value) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)
		Promise.keep("@is_mutable == true", @is_mutable)

		return glurg.common.data.write_float(@data, offset, value)

	read_byte: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_byte(@data, offset)

	read_unsigned_byte: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 1 <= @length", offset + 1 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_unsigned_byte(@data, offset)

	read_short: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_short(@data, offset)

	read_unsigned_short: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 2 <= @length", offset + 2 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_unsigned_short(@data, offset)

	read_integer: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_integer(@data, offset)

	read_unsigned_integer: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_unsigned_integer(@data, offset)

	read_float: (offset) =>
		Promise.keep("offset", Promise.IsNumber(offset))
		Promise.keep("offset + 4 <= @length", offset + 4 <= @length)
		Promise.keep("offset >= 0", offset >= 0)

		return glurg.common.data.read_float(@data, offset)

return Blob


Remove old task render mode again

Remove old task render mode again

--
-- Copyright (C) 2017-2018 DBot

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

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

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

import FrameNumberL, RealTimeL, PPM2 from _G
import GetPonyData, IsDormant, PPMBonesModifier, IsPony from FindMetaTable('Entity')

RENDER_HORN_GLOW = CreateConVar('ppm2_horn_glow', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Visual horn glow when player uses physgun')
HORN_PARTICLES = CreateConVar('ppm2_horn_particles', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Visual horn particles when player uses physgun')
HORN_FP = CreateConVar('ppm2_horn_firstperson', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Visual horn effetcs in first person')
HORN_HIDE_BEAM = CreateConVar('ppm2_horn_nobeam', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Hide physgun beam')
DRAW_LEGS_DEPTH = CreateConVar('ppm2_render_legsdepth', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Render legs in depth pass. Useful with Boken DoF enabled')
LEGS_RENDER_TYPE = CreateConVar('ppm2_render_legstype', '0', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'When render legs. 0 - Before Opaque renderables; 1 - after Translucent renderables')
ENABLE_NEW_RAGDOLLS = CreateConVar('ppm2_sv_new_ragdolls', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Enable new ragdolls')
SHOULD_DRAW_VIEWMODEL = CreateConVar('ppm2_cl_draw_hands', '1', {FCVAR_ARCHIVE}, 'Should draw hooves as viewmodel')
SV_SHOULD_DRAW_VIEWMODEL = CreateConVar('ppm2_sv_draw_hands', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Should draw hooves as viewmodel')

hook.Add 'PreDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
	return if PPM2.__RENDERING_REFLECTIONS
	return unless IsValid(arms)
	return unless ply.__cachedIsPony
	return true unless SV_SHOULD_DRAW_VIEWMODEL\GetBool()
	return true unless SHOULD_DRAW_VIEWMODEL\GetBool()
	return unless ply\Alive()
	arms\SetPos(ply\EyePos() + Vector(0, 0, 100))
	wep = ply\GetActiveWeapon()
	if IsValid(wep) and wep.UseHands == false
		return true -- Dafuck?
	return if arms\GetModel() ~= 'models/cppm/pony_arms.mdl'
	data = ply\GetPonyData()
	return unless data
	status = data\GetRenderController()\PreDrawArms(arms)
	return status if status ~= nil
	arms.__ppm2_draw = true

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

mat_dxlevel = GetConVar('mat_dxlevel')

timer.Create 'PPM2.CheckDXLevel', 180, 0, ->
	if mat_dxlevel\GetInt() > 90
		timer.Remove 'PPM2.CheckDXLevel'
		return

	PPM2.Message('Direct3D Level is LESS THAN 9.1! This will not work!')

IN_DRAW = false
MARKED_FOR_DRAW = {}

PPM2.PreDrawOpaqueRenderables = (bDrawingDepth, bDrawingSkybox) ->
	return if IN_DRAW or PPM2.__RENDERING_REFLECTIONS

	MARKED_FOR_DRAW = {}

	for _, ply in ipairs player.GetAll()
		if not IsDormant(ply)
			p = IsPony(ply)
			ply.__cachedIsPony = p
			if p
				data = GetPonyData(ply)
				if data
					renderController = data\GetRenderController()
					if renderController
						renderController\PreDraw()
						table.insert(MARKED_FOR_DRAW, renderController)

	if bDrawingDepth and DRAW_LEGS_DEPTH\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegsDepth()
					IN_DRAW = false

	return if bDrawingDepth or bDrawingSkybox

	if not LEGS_RENDER_TYPE\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegs()
					IN_DRAW = false

PPM2.PostDrawTranslucentRenderables = (bDrawingDepth, bDrawingSkybox) ->
	if not bDrawingDepth and not bDrawingSkybox
		for _, draw in ipairs MARKED_FOR_DRAW
			draw\PostDraw()

PPM2.PostDrawOpaqueRenderables = (bDrawingDepth, bDrawingSkybox) ->
	return if IN_DRAW or PPM2.__RENDERING_REFLECTIONS

	if bDrawingDepth and DRAW_LEGS_DEPTH\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegsDepth()
					IN_DRAW = false

	return if bDrawingDepth or bDrawingSkybox

	if not ENABLE_NEW_RAGDOLLS\GetBool()
		for _, ply in ipairs player.GetAll()
			alive = ply\Alive()
			ply.__ppm2_last_dead = RealTimeL() + 2 if not alive
			if ply.__cachedIsPony
				if ply\GetPonyData() and not alive
					data = ply\GetPonyData()
					rag = ply\GetRagdollEntity()
					if IsValid(rag)
						renderController = data\GetRenderController()
						data\DoRagdollMerge()
						if renderController
							renderController\PreDraw(rag)
							IN_DRAW = true
							rag\DrawModel()
							IN_DRAW = false
							renderController\PostDraw(rag)

	if LEGS_RENDER_TYPE\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegs()
					IN_DRAW = false

Think = ->
	for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
		ent = task.ent
		if IsValid(ent) and ent.__cachedIsPony
			if ent.__ppm2_task_hit
				ent.__ppm2_task_hit = false
				ent\SetNoDraw(false)

			if not ent.__ppm2RenderOverride
				ent = ent
				ent.__ppm2_oldRenderOverride = ent.RenderOverride
				ent.__ppm2RenderOverride = ->
					renderController = task\GetRenderController()
					renderController\PreDraw(ent, true)
					ent\DrawModel()
					renderController\PostDraw(ent, true)
					ent.__ppm2_oldRenderOverride(ent) if ent.__ppm2_oldRenderOverride
				ent.RenderOverride = ent.__ppm2RenderOverride

PPM2.PrePlayerDraw = =>
	return if PPM2.__RENDERING_REFLECTIONS

	with data = GetPonyData(@)
		return if not data
		@__cachedIsPony = IsPony(@)
		return if not @__cachedIsPony
		f = FrameNumberL()
		return if @__ppm2_last_draw == f
		@__ppm2_last_draw = f
		bones = PPMBonesModifier(@)
		if data and bones\CanThink()
			@ResetBoneManipCache()
			bones\ResetBones()
			hook.Call('PPM2.SetupBones', nil, @, data) if data
			bones\Think()
			@ApplyBoneManipulations()
			@_ppmBonesModified = true

PPM2.PostPlayerDraw = =>
	return if PPM2.__RENDERING_REFLECTIONS
	with data = GetPonyData(@)
		return if not data or not @__cachedIsPony
		renderController = data\GetRenderController()
		renderController\PostDraw() if renderController

do
	hornGlowStatus = {}
	smokeMaterial = 'ppm2/hornsmoke'
	fireMat = 'particle/fire'
	hornShift = Vector(1, 0.15, 14.5)

	hook.Add 'PreDrawHalos', 'PPM2.HornEffects', =>
		return if not HORN_HIDE_BEAM\GetBool()
		frame = FrameNumberL()
		cTime = (RealTimeL() % 20) * 4
		for ent, status in pairs hornGlowStatus
			if IsValid(ent) and status.frame == frame and IsValid(status.target)
				additional = math.sin(cTime / 2 + status.haloSeed * 3) * 40
				newCol = status.color + Color(additional, additional, additional)
				halo.Add({status.target}, newCol, math.sin(cTime + status.haloSeed) * 4 + 8, math.cos(cTime + status.haloSeed) * 4 + 8, 2)

	hook.Add 'Think', 'PPM2.HornEffects', =>
		frame = FrameNumberL()
		for ent, status in pairs hornGlowStatus
			if not IsValid(ent)
				status.emmiter\Finish() if IsValid(status.emmiter)
				status.emmiterProp\Finish() if IsValid(status.emmiterProp)
				hornGlowStatus[ent] = nil
			elseif status.frame ~= frame
				status.data\SetHornGlow(status.prevStatus)
				status.emmiter\Finish() if IsValid(status.emmiter)
				status.emmiterProp\Finish() if IsValid(status.emmiterProp)
				hornGlowStatus[ent] = nil
			else
				if not status.prevStatus and RENDER_HORN_GLOW\GetBool() and status.data\GetHornGlow() ~= status.isEnabled
					status.data\SetHornGlow(status.isEnabled)
				if status.attach and IsValid(status.target)
					grabHornPos = Vector(hornShift) * status.data\GetPonySize()
					{:Pos, :Ang} = ent\GetAttachment(status.attach)
					grabHornPos\Rotate(Ang)
					if status.isEnabled and IsValid(status.emmiter) and status.nextSmokeParticle < RealTimeL()
						status.nextSmokeParticle = RealTimeL() + math.Rand(0.1, 0.2)
						for i = 1, math.random(1, 4)
							vec = VectorRand()
							calcPos = Pos + grabHornPos + vec
							with particle = status.emmiter\Add(smokeMaterial, calcPos)
								\SetRollDelta(math.rad(math.random(0, 360)))
								\SetPos(calcPos)
								life = math.Rand(0.6, 0.9)
								\SetStartAlpha(math.random(80, 170))
								\SetDieTime(life)
								\SetColor(status.color.r, status.color.g, status.color.b)
								\SetEndAlpha(0)
								size = math.Rand(2, 3)
								\SetEndSize(math.Rand(2, size))
								\SetStartSize(size)
								\SetGravity(Vector())
								\SetAirResistance(10)
								vecRand = VectorRand()
								vecRand.z *= 2
								\SetVelocity(ent\GetVelocity() + vecRand * status.data\GetPonySize() * 2)
								\SetCollide(false)
					if status.isEnabled and IsValid(status.emmiterProp) and status.nextGrabParticle < RealTimeL() and status.mins and status.maxs
						status.nextGrabParticle = RealTimeL() + math.Rand(0.05, 0.3)
						status.emmiterProp\SetPos(status.tpos)
						for i = 1, math.random(2, 6)
							calcPos = Vector(math.Rand(status.mins.x, status.maxs.x), math.Rand(status.mins.y, status.maxs.y), math.Rand(status.mins.z, status.maxs.z))
							with particle = status.emmiterProp\Add(fireMat, calcPos)
								\SetRollDelta(math.rad(math.random(0, 360)))
								\SetPos(calcPos)
								life = math.Rand(0.5, 0.9)
								\SetStartAlpha(math.random(130, 230))
								\SetDieTime(life)
								\SetColor(status.color.r, status.color.g, status.color.b)
								\SetEndAlpha(0)
								\SetEndSize(0)
								\SetStartSize(math.Rand(2, 6))
								\SetGravity(Vector())
								\SetAirResistance(15)
								\SetVelocity(VectorRand() * 6)
								\SetCollide(false)

	hook.Add 'DrawPhysgunBeam', 'PPM2.HornEffects', (physgun = NULL, isEnabled = false, target = NULL, bone = 0, hitPos = Vector()) =>
		return if not @IsPony() or not HORN_FP\GetBool() and @ == LocalPlayer() and not @ShouldDrawLocalPlayer()
		data = @GetPonyData()
		return if not data
		return if data\GetRace() ~= PPM2.RACE_UNICORN and data\GetRace() ~= PPM2.RACE_ALICORN
		if not hornGlowStatus[@]
			hornGlowStatus[@] = {
				frame: FrameNumberL()
				prevStatus: data\GetHornGlow()
				:data, :isEnabled, :hitPos, :target, :bone
				tpos: @GetPos()
				attach: @LookupAttachment('eyes')
				nextSmokeParticle: 0
				nextGrabParticle: 0
			}

			with hornGlowStatus[@]
				if HORN_PARTICLES\GetBool()
					.emmiter = ParticleEmitter(EyePos())
					.emmiterProp = ParticleEmitter(EyePos())

				.color = data\GetHornMagicColor()
				.haloSeed = math.rad(math.random(-1000, 1000))

				if not data\GetSeparateMagicColor()
					if not data\GetSeparateEyes()
						.color = data\GetEyeIrisTop()\Lerp(0.5, data\GetEyeIrisBottom())
					else
						lerpLeft = data\GetEyeIrisTopLeft()\Lerp(0.5, data\GetEyeIrisBottomLeft())
						lerpRight = data\GetEyeIrisTopRight()\Lerp(0.5, data\GetEyeIrisBottomRight())
						.color = lerpLeft\Lerp(0.5, lerpRight)
		else
			with hornGlowStatus[@]
				.frame = FrameNumberL()
				.isEnabled = isEnabled
				.target = target
				.bone = bone
				.hitPos = hitPos
				if IsValid(target)
					.tpos = target\GetPos() + hitPos
					center = target\WorldSpaceCenter()
					.center = center
					mins, maxs = target\WorldSpaceAABB()
					.mins = center + (mins - center) * 1.2
					.maxs = center + (maxs - center) * 1.2
		return false if HORN_HIDE_BEAM\GetBool() and IsValid(target)

hook.Add 'PrePlayerDraw', 'PPM2.PlayerDraw', PPM2.PrePlayerDraw, -2
hook.Add 'PostPlayerDraw', 'PPM2.PostPlayerDraw', PPM2.PostPlayerDraw, -2
hook.Add 'PostDrawOpaqueRenderables', 'PPM2.PostDrawOpaqueRenderables', PPM2.PostDrawOpaqueRenderables, -2
hook.Add 'Think', 'PPM2.UpdateRenderTasks', Think, -2
hook.Add 'PreDrawOpaqueRenderables', 'PPM2.PreDrawOpaqueRenderables', PPM2.PreDrawOpaqueRenderables, -2
hook.Add 'PostDrawTranslucentRenderables', 'PPM2.PostDrawTranslucentRenderables', PPM2.PostDrawTranslucentRenderables, -2

--
-- Copyright (C) 2017-2018 DBot

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

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

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

import FrameNumberL, RealTimeL, PPM2 from _G
import GetPonyData, IsDormant, PPMBonesModifier, IsPony from FindMetaTable('Entity')

RENDER_HORN_GLOW = CreateConVar('ppm2_horn_glow', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Visual horn glow when player uses physgun')
HORN_PARTICLES = CreateConVar('ppm2_horn_particles', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Visual horn particles when player uses physgun')
HORN_FP = CreateConVar('ppm2_horn_firstperson', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Visual horn effetcs in first person')
HORN_HIDE_BEAM = CreateConVar('ppm2_horn_nobeam', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Hide physgun beam')
TASK_RENDER_TYPE = CreateConVar('ppm2_task_render_type', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Task rendering type (e.g. pony ragdolls and NPCs). 1 - better render; less conflicts; more FPS. 0 - "old-style" render; possible conflicts;')
DRAW_LEGS_DEPTH = CreateConVar('ppm2_render_legsdepth', '1', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'Render legs in depth pass. Useful with Boken DoF enabled')
LEGS_RENDER_TYPE = CreateConVar('ppm2_render_legstype', '0', {FCVAR_ARCHIVE, FCVAR_NOTIFY}, 'When render legs. 0 - Before Opaque renderables; 1 - after Translucent renderables')
ENABLE_NEW_RAGDOLLS = CreateConVar('ppm2_sv_new_ragdolls', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Enable new ragdolls')
SHOULD_DRAW_VIEWMODEL = CreateConVar('ppm2_cl_draw_hands', '1', {FCVAR_ARCHIVE}, 'Should draw hooves as viewmodel')
SV_SHOULD_DRAW_VIEWMODEL = CreateConVar('ppm2_sv_draw_hands', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Should draw hooves as viewmodel')

hook.Add 'PreDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
	return if PPM2.__RENDERING_REFLECTIONS
	return unless IsValid(arms)
	return unless ply.__cachedIsPony
	return true unless SV_SHOULD_DRAW_VIEWMODEL\GetBool()
	return true unless SHOULD_DRAW_VIEWMODEL\GetBool()
	return unless ply\Alive()
	arms\SetPos(ply\EyePos() + Vector(0, 0, 100))
	wep = ply\GetActiveWeapon()
	if IsValid(wep) and wep.UseHands == false
		return true -- Dafuck?
	return if arms\GetModel() ~= 'models/cppm/pony_arms.mdl'
	data = ply\GetPonyData()
	return unless data
	status = data\GetRenderController()\PreDrawArms(arms)
	return status if status ~= nil
	arms.__ppm2_draw = true

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

mat_dxlevel = GetConVar('mat_dxlevel')

timer.Create 'PPM2.CheckDXLevel', 180, 0, ->
	if mat_dxlevel\GetInt() > 90
		timer.Remove 'PPM2.CheckDXLevel'
		return

	PPM2.Message('Direct3D Level is LESS THAN 9.1! This will not work!')

IN_DRAW = false
MARKED_FOR_DRAW = {}

PPM2.PreDrawOpaqueRenderables = (bDrawingDepth, bDrawingSkybox) ->
	return if IN_DRAW or PPM2.__RENDERING_REFLECTIONS

	MARKED_FOR_DRAW = {}

	for _, ply in ipairs player.GetAll()
		if not IsDormant(ply)
			p = IsPony(ply)
			ply.__cachedIsPony = p
			if p
				data = GetPonyData(ply)
				if data
					renderController = data\GetRenderController()
					if renderController
						renderController\PreDraw()
						table.insert(MARKED_FOR_DRAW, renderController)

	if bDrawingDepth and DRAW_LEGS_DEPTH\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegsDepth()
					IN_DRAW = false

	return if bDrawingDepth or bDrawingSkybox

	if not LEGS_RENDER_TYPE\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegs()
					IN_DRAW = false

PPM2.PostDrawTranslucentRenderables = (bDrawingDepth, bDrawingSkybox) ->
	if not bDrawingDepth and not bDrawingSkybox
		for _, draw in ipairs MARKED_FOR_DRAW
			draw\PostDraw()

PPM2.PostDrawOpaqueRenderables = (bDrawingDepth, bDrawingSkybox) ->
	return if IN_DRAW or PPM2.__RENDERING_REFLECTIONS

	if bDrawingDepth and DRAW_LEGS_DEPTH\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegsDepth()
					IN_DRAW = false

	return if bDrawingDepth or bDrawingSkybox

	if not TASK_RENDER_TYPE\GetBool()
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ent = task.ent
			if IsValid(ent)
				if ent.__cachedIsPony
					ent\SetNoDraw(true)
					ent.__ppm2_task_hit = true
					renderController = task\GetRenderController()
					renderController\PreDraw(ent)
					IN_DRAW = true
					ent\DrawModel()
					IN_DRAW = false
					renderController\PostDraw(ent)
				else
					if ent.__ppm2_task_hit
						ent.__ppm2_task_hit = false
						ent\SetNoDraw(false)
						task\Reset()

	if not ENABLE_NEW_RAGDOLLS\GetBool()
		for _, ply in ipairs player.GetAll()
			alive = ply\Alive()
			ply.__ppm2_last_dead = RealTimeL() + 2 if not alive
			if ply.__cachedIsPony
				if ply\GetPonyData() and not alive
					data = ply\GetPonyData()
					rag = ply\GetRagdollEntity()
					if IsValid(rag)
						renderController = data\GetRenderController()
						data\DoRagdollMerge()
						if renderController
							renderController\PreDraw(rag)
							IN_DRAW = true
							rag\DrawModel()
							IN_DRAW = false
							renderController\PostDraw(rag)

	if LEGS_RENDER_TYPE\GetBool()
		with LocalPlayer()
			if .__cachedIsPony and \Alive()
				if data = \GetPonyData()
					IN_DRAW = true
					data\GetRenderController()\DrawLegs()
					IN_DRAW = false

Think = ->
	if TASK_RENDER_TYPE\GetBool()
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ent = task.ent
			if IsValid(ent) and ent.__cachedIsPony
				if ent.__ppm2_task_hit
					ent.__ppm2_task_hit = false
					ent\SetNoDraw(false)

				if not ent.__ppm2RenderOverride
					ent = ent
					ent.__ppm2_oldRenderOverride = ent.RenderOverride
					ent.__ppm2RenderOverride = ->
						renderController = task\GetRenderController()
						renderController\PreDraw(ent, true)
						ent\DrawModel()
						renderController\PostDraw(ent, true)
						ent.__ppm2_oldRenderOverride(ent) if ent.__ppm2_oldRenderOverride
					ent.RenderOverride = ent.__ppm2RenderOverride

PPM2.PrePlayerDraw = =>
	return if PPM2.__RENDERING_REFLECTIONS

	with data = GetPonyData(@)
		return if not data
		@__cachedIsPony = IsPony(@)
		return if not @__cachedIsPony
		f = FrameNumberL()
		return if @__ppm2_last_draw == f
		@__ppm2_last_draw = f
		bones = PPMBonesModifier(@)
		if data and bones\CanThink()
			@ResetBoneManipCache()
			bones\ResetBones()
			hook.Call('PPM2.SetupBones', nil, @, data) if data
			bones\Think()
			@ApplyBoneManipulations()
			@_ppmBonesModified = true

PPM2.PostPlayerDraw = =>
	return if PPM2.__RENDERING_REFLECTIONS
	with data = GetPonyData(@)
		return if not data or not @__cachedIsPony
		renderController = data\GetRenderController()
		renderController\PostDraw() if renderController

do
	hornGlowStatus = {}
	smokeMaterial = 'ppm2/hornsmoke'
	fireMat = 'particle/fire'
	hornShift = Vector(1, 0.15, 14.5)

	hook.Add 'PreDrawHalos', 'PPM2.HornEffects', =>
		return if not HORN_HIDE_BEAM\GetBool()
		frame = FrameNumberL()
		cTime = (RealTimeL() % 20) * 4
		for ent, status in pairs hornGlowStatus
			if IsValid(ent) and status.frame == frame and IsValid(status.target)
				additional = math.sin(cTime / 2 + status.haloSeed * 3) * 40
				newCol = status.color + Color(additional, additional, additional)
				halo.Add({status.target}, newCol, math.sin(cTime + status.haloSeed) * 4 + 8, math.cos(cTime + status.haloSeed) * 4 + 8, 2)

	hook.Add 'Think', 'PPM2.HornEffects', =>
		frame = FrameNumberL()
		for ent, status in pairs hornGlowStatus
			if not IsValid(ent)
				status.emmiter\Finish() if IsValid(status.emmiter)
				status.emmiterProp\Finish() if IsValid(status.emmiterProp)
				hornGlowStatus[ent] = nil
			elseif status.frame ~= frame
				status.data\SetHornGlow(status.prevStatus)
				status.emmiter\Finish() if IsValid(status.emmiter)
				status.emmiterProp\Finish() if IsValid(status.emmiterProp)
				hornGlowStatus[ent] = nil
			else
				if not status.prevStatus and RENDER_HORN_GLOW\GetBool() and status.data\GetHornGlow() ~= status.isEnabled
					status.data\SetHornGlow(status.isEnabled)
				if status.attach and IsValid(status.target)
					grabHornPos = Vector(hornShift) * status.data\GetPonySize()
					{:Pos, :Ang} = ent\GetAttachment(status.attach)
					grabHornPos\Rotate(Ang)
					if status.isEnabled and IsValid(status.emmiter) and status.nextSmokeParticle < RealTimeL()
						status.nextSmokeParticle = RealTimeL() + math.Rand(0.1, 0.2)
						for i = 1, math.random(1, 4)
							vec = VectorRand()
							calcPos = Pos + grabHornPos + vec
							with particle = status.emmiter\Add(smokeMaterial, calcPos)
								\SetRollDelta(math.rad(math.random(0, 360)))
								\SetPos(calcPos)
								life = math.Rand(0.6, 0.9)
								\SetStartAlpha(math.random(80, 170))
								\SetDieTime(life)
								\SetColor(status.color.r, status.color.g, status.color.b)
								\SetEndAlpha(0)
								size = math.Rand(2, 3)
								\SetEndSize(math.Rand(2, size))
								\SetStartSize(size)
								\SetGravity(Vector())
								\SetAirResistance(10)
								vecRand = VectorRand()
								vecRand.z *= 2
								\SetVelocity(ent\GetVelocity() + vecRand * status.data\GetPonySize() * 2)
								\SetCollide(false)
					if status.isEnabled and IsValid(status.emmiterProp) and status.nextGrabParticle < RealTimeL() and status.mins and status.maxs
						status.nextGrabParticle = RealTimeL() + math.Rand(0.05, 0.3)
						status.emmiterProp\SetPos(status.tpos)
						for i = 1, math.random(2, 6)
							calcPos = Vector(math.Rand(status.mins.x, status.maxs.x), math.Rand(status.mins.y, status.maxs.y), math.Rand(status.mins.z, status.maxs.z))
							with particle = status.emmiterProp\Add(fireMat, calcPos)
								\SetRollDelta(math.rad(math.random(0, 360)))
								\SetPos(calcPos)
								life = math.Rand(0.5, 0.9)
								\SetStartAlpha(math.random(130, 230))
								\SetDieTime(life)
								\SetColor(status.color.r, status.color.g, status.color.b)
								\SetEndAlpha(0)
								\SetEndSize(0)
								\SetStartSize(math.Rand(2, 6))
								\SetGravity(Vector())
								\SetAirResistance(15)
								\SetVelocity(VectorRand() * 6)
								\SetCollide(false)

	hook.Add 'DrawPhysgunBeam', 'PPM2.HornEffects', (physgun = NULL, isEnabled = false, target = NULL, bone = 0, hitPos = Vector()) =>
		return if not @IsPony() or not HORN_FP\GetBool() and @ == LocalPlayer() and not @ShouldDrawLocalPlayer()
		data = @GetPonyData()
		return if not data
		return if data\GetRace() ~= PPM2.RACE_UNICORN and data\GetRace() ~= PPM2.RACE_ALICORN
		if not hornGlowStatus[@]
			hornGlowStatus[@] = {
				frame: FrameNumberL()
				prevStatus: data\GetHornGlow()
				:data, :isEnabled, :hitPos, :target, :bone
				tpos: @GetPos()
				attach: @LookupAttachment('eyes')
				nextSmokeParticle: 0
				nextGrabParticle: 0
			}

			with hornGlowStatus[@]
				if HORN_PARTICLES\GetBool()
					.emmiter = ParticleEmitter(EyePos())
					.emmiterProp = ParticleEmitter(EyePos())

				.color = data\GetHornMagicColor()
				.haloSeed = math.rad(math.random(-1000, 1000))

				if not data\GetSeparateMagicColor()
					if not data\GetSeparateEyes()
						.color = data\GetEyeIrisTop()\Lerp(0.5, data\GetEyeIrisBottom())
					else
						lerpLeft = data\GetEyeIrisTopLeft()\Lerp(0.5, data\GetEyeIrisBottomLeft())
						lerpRight = data\GetEyeIrisTopRight()\Lerp(0.5, data\GetEyeIrisBottomRight())
						.color = lerpLeft\Lerp(0.5, lerpRight)
		else
			with hornGlowStatus[@]
				.frame = FrameNumberL()
				.isEnabled = isEnabled
				.target = target
				.bone = bone
				.hitPos = hitPos
				if IsValid(target)
					.tpos = target\GetPos() + hitPos
					center = target\WorldSpaceCenter()
					.center = center
					mins, maxs = target\WorldSpaceAABB()
					.mins = center + (mins - center) * 1.2
					.maxs = center + (maxs - center) * 1.2
		return false if HORN_HIDE_BEAM\GetBool() and IsValid(target)

hook.Add 'PrePlayerDraw', 'PPM2.PlayerDraw', PPM2.PrePlayerDraw, -2
hook.Add 'PostPlayerDraw', 'PPM2.PostPlayerDraw', PPM2.PostPlayerDraw, -2
hook.Add 'PostDrawOpaqueRenderables', 'PPM2.PostDrawOpaqueRenderables', PPM2.PostDrawOpaqueRenderables, -2
hook.Add 'Think', 'PPM2.UpdateRenderTasks', Think, -2
hook.Add 'PreDrawOpaqueRenderables', 'PPM2.PreDrawOpaqueRenderables', PPM2.PreDrawOpaqueRenderables, -2
hook.Add 'PostDrawTranslucentRenderables', 'PPM2.PostDrawTranslucentRenderables', PPM2.PostDrawTranslucentRenderables, -2


Allow printing multiple functions in 'print_trace_functions' utility.

After all, it is 'print_trace_functions' and not 'print_trace_function'.


Shortened it a bit.

Shortened it a bit.

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

import insert from table

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

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

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

map_listing = (structure, func) ->
	for k, v in pairs structure
		func(v, map_listing)

print_listing = (value, func) ->
	{file, mode, data} = value
	print file, mode
	func(data, print_listing) if data

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

dbg = (b) ->
	"[DBG: #{(b and 'Y') or 'N'}]"

perform_action = (old, new, action) ->
	print dbg(debug) .. switch action
		when "copy"
			"[cp]: Not yet implemented."
		when "move"
			"[mv]: Not yet implemented."
		when "symlink", "hardlink"
			lfs.link(old, new, action == "hardlink" or false) if not debug
			"[link]: #{old} to: #{new}"
		else
			"unknown action: #{action}"

process_files = (filedata, regex, parent) ->
	for k, {file, mode, data} in pairs filedata
		if rex.gsub(file, regex, '')
			if mode == 'directory'
				f = rex.gsub(file, regex, '')\match("^%s*(.-)%s*$")
				f = parent.out .. '/' .. f
				lfs.mkdir(f)
				process_files(data, regex, {in: parent.in .. '/' .. file, out: f})
			else
				ext = file_ext(file)
				newer_f = rex.gsub(file, regex, '')
				new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
				new_f = new_f\gsub('[ ]', '')
				new_f = rex.gsub(new_f, [[([^\d]*)(\d+)[^\d]*$]], [[%1_ep%2]], 1)
				old_f, new_f = parent.in .. '/' .. file, parent.out .. '/' .. new_f .. ext
				perform_action(old_f, new_f, args["a"])

		else
			print "ERR: ", file, mode

cli\set_name("regexulon.lua")
cli\add_argument("DIR", "directory to scan")
cli\add_argument("TARGET", "target directory")
cli\optarg("IGNORED", "ignored files/directories", "")
cli\add_option("-a, --action=ACTION", "action to take: cp, mv, symlink, hardlink", "hardlink")
cli\add_flag("-d, --debug", "script will simulate execution.")
cli\add_flag("-V, --version", "prints the program version")
cli\add_flag("-v, --verbose", "verbose output")
export args = cli\parse_args()
return if not args

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

inputdir = args["DIR"]
targetdir = args["TARGET"]
export debug = args["d"]

files = dir_list(args["DIR"])
regex = [==[\(.*?\)|\[.*?\]]==]

--map_listing(files, print_listing)
processed = process_files(files, regex, {in: inputdir, out: targetdir})

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

import insert from table

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

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

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

map_listing = (structure, func) ->
	for k, v in pairs structure
		func(v, map_listing)

print_listing = (value, func) ->
	{file, mode, data} = value
	print file, mode
	func(data, print_listing) if data

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

dbg = (b) ->
	"[DBG: #{(b and 'Y') or 'N'}]"

perform_action = (old, new, action) ->
	print dbg(debug) .. switch action
		when "copy"
			"[cp]: Not yet implemented."
		when "move"
			"[mv]: Not yet implemented."
		when "symlink", "hardlink"
			lfs.link(old, new, action == "hardlink" or false) if not debug
			"[link]: #{old} to: #{new}"
		else
			"unknown action: #{action}"

process_files = (filedata, regex, parent) ->
	for k, {file, mode, data} in pairs filedata
		if rex.gsub(file, regex, '')
			if mode == 'directory'
				f = rex.gsub(file, regex, '')\match("^%s*(.-)%s*$")
				f = parent.out .. '/' .. f
				lfs.mkdir(f)
				process_files(data, regex, {in: parent.in .. '/' .. file, out: f})
			else
				ext = file_ext(file)
				newer_f = rex.gsub(file, regex, '')
				new_f = rex.match(newer_f, [[(.*)(v\d)]]) or newer_f
				new_f = new_f\gsub('[ ]', '')
				new_f = rex.gsub(new_f, [[([^\d]*)(\d+)[^\d]*$]], [[%1_ep%2]], 1)
				old_f, new_f = parent.in .. '/' .. file, parent.out .. '/' .. new_f .. ext
				perform_action(old_f, new_f, args["a"])

		else
			print "ERR: ", file, mode

cli\set_name("regexulon.lua")
cli\add_argument("DIR", "directory to scan")
cli\add_argument("TARGET", "target directory")
cli\optarg("IGNORED", "ignored files/directories")
cli\add_option("-a, --action=ACTION", "action to take: cp, mv, symlink, hardlink", "hardlink")
cli\add_flag("-d, --debug", "script will simulate execution.")
cli\add_flag("-V, --version", "prints the program version")
cli\add_flag("-v, --verbose", "verbose output")
export args = cli\parse_args()
return if not args

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

inputdir = args["DIR"]
targetdir = args["TARGET"]
export debug = args["d"]

files = dir_list(args["DIR"])
regex = [==[\(.*?\)|\[.*?\]]==]

--map_listing(files, print_listing)
processed = process_files(files, regex, {in: inputdir, out: targetdir})