Aegisub example: add DependencyControl record

-- This library is unlicensed under CC0

DependencyControl = require "l0.DependencyControl"

versionRecord = DependencyControl{
    name: "ASSInspector",
    version: "0.4.0",
    description: "Provides low level inspection and analysis of subtitles post-rasterization.",
    author: "torque",
    url: "https://github.com/TypesettingCartel/ASSInspector",
    moduleName: "ASSInspector.Inspector",
    feed: "https://raw.githubusercontent.com/TypesettingCartel/ASSInspector/master/DependencyControl.json",
    {"ffi"}
}

ffi = versionRecord\requireModules!

ASSIVersionCompat = 0x000400
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;
	uint32_t hash;
	uint8_t solid;
} 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. Must be v%d.x.x")\format ASSIVersionCompat_major
	elseif ASSIVersion_major < ASSIVersionCompat_major or ASSIVersion_minor < ASSIVersionCompat_minor
		return nil, ("libASSInspector library is too old. Must be v%d.%d.x compatible.")\format ASSIVersionCompat_major, ASSIVersionCompat_minor

	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 )
		hash: tonumber( rect.hash )
		solid: (rect.solid == 1)
	}

	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 Inspectorversion_major
	@version = versionRecord

	new: ( subtitles, fontconfigConfig = aegisub.decode_path( libraryPath .. "/fonts.conf" ), fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		assert subtitles, "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 )
		assert success, message

		success, message = @updateHeader( subtitles )
		assert success, message

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

		return true

	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 versionRecord\register Inspector

-- This library is unlicensed under CC0

ASSIVersionCompat = 0x000400
InspectorVersion  = 0x000400

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

InspectorVersion_major = bit.rshift( ASSIVersionCompat, 16 )
InspectorVersion_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
InspectorVersion_patch = bit.band( ASSIVersionCompat, 0xFF )

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;
	uint32_t hash;
	uint8_t solid;
} 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. Must be v%d.x.x")\format ASSIVersionCompat_major
	elseif ASSIVersion_major < ASSIVersionCompat_major or ASSIVersion_minor < ASSIVersionCompat_minor
		return nil, ("libASSInspector library is too old. Must be v%d.%d.x compatible.")\format ASSIVersionCompat_major, ASSIVersionCompat_minor

	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 )
		hash: tonumber( rect.hash )
		solid: (rect.solid == 1)
	}

	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 = InspectorVersion_major
	@version_minor = InspectorVersion_minor
	@version_patch = InspectorVersion_patch

	new: ( subtitles, fontconfigConfig = aegisub.decode_path( libraryPath .. "/fonts.conf" ), fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		assert subtitles, "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 )
		assert success, message

		success, message = @updateHeader( subtitles )
		assert success, message

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

		return true

	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


Decrease sound level of other player's hoofsteps

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

DISABLE_HOOFSTEP_SOUND_CLIENT = CreateConVar('ppm2_cl_no_hoofsound', '0', {FCVAR_ARCHIVE, FCVAR_USERINFO}, 'Disable hoofstep sound play time') if CLIENT
DISABLE_HOOFSTEP_SOUND = CreateConVar('ppm2_no_hoofsound', '0', {FCVAR_REPLICATED}, 'Disable hoofstep sound play time')

hook.Remove('PlayerStepSoundTime', 'PPM2.Hooks')
hook.Add 'PlayerStepSoundTime', 'PPM2.Hoofstep', (stepType = STEPSOUNDTIME_NORMAL, isWalking = false) =>
	return if not @IsPonyCached() or CLIENT and DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool() or DISABLE_HOOFSTEP_SOUND\GetBool()
	rate = @GetPlaybackRate() * .5
	if @Crouching()
		switch stepType
			when STEPSOUNDTIME_NORMAL
				return not isWalking and (300 / rate) or (600 / rate)
			when STEPSOUNDTIME_ON_LADDER
				return 500 / rate
			when STEPSOUNDTIME_WATER_KNEE
				return not isWalking and (400 / rate) or (800 / rate)
			when STEPSOUNDTIME_WATER_FOOT
				return not isWalking and (350 / rate) or (700 / rate)
	else
		switch stepType
			when STEPSOUNDTIME_NORMAL
				return not isWalking and (150 / rate) or (300 / rate)
			when STEPSOUNDTIME_ON_LADDER
				return 500 / rate
			when STEPSOUNDTIME_WATER_KNEE
				return not isWalking and (250 / rate) or (500 / rate)
			when STEPSOUNDTIME_WATER_FOOT
				return not isWalking and (175 / rate) or (350 / rate)

net.pool('ppm2_workaround_emitsound') if SERVER

SOUND_STRINGS_POOL = {}
SOUND_STRINGS_POOL_INV = {}
AddSoundString = (sound) ->
	nextid = #SOUND_STRINGS_POOL_INV + 1
	SOUND_STRINGS_POOL[sound] = nextid
	SOUND_STRINGS_POOL_INV[nextid] = sound

class PPM2.MaterialSoundEntry
	@REGISTRIES = {}

	@Ask = (matType = MAT_DEFAULT) =>
		for reg in *@REGISTRIES
			if reg.material == matType
				return reg

		return false

	new: (name, material, variantsWalk = 0, variantsRun = 0, variantsWander = 0) =>
		table.insert(@@REGISTRIES, @)
		@name = name
		@material = material
		@variantsWalk = variantsWalk
		@variantsRun = variantsRun
		@variantsWander = variantsWander
		@variantsLand = 0
		@playHoofclap = true
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_walk' .. i .. '.ogg') for i = 1, @variantsWalk
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_run' .. i .. '.ogg') for i = 1, @variantsRun
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_wander' .. i .. '.ogg') for i = 1, @variantsWander

	ShouldPlayHoofclap: => @playHoofclap

	DisableHoofclap: =>
		@playHoofclap = false
		return @

	GetWalkSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_walk' .. math.random(1, @variantsWalk) .. '.ogg' if @variantsWalk ~= 0
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_run' .. math.random(1, @variantsRun) .. '.ogg' if @variantsRun ~= 0

	GetRunSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_run' .. math.random(1, @variantsRun) .. '.ogg' if @variantsRun ~= 0
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_walk' .. math.random(1, @variantsWalk) .. '.ogg' if @variantsWalk ~= 0

	GetWanderSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_wander' .. math.random(1, @variantsWander) .. '.ogg' if @variantsWander ~= 0

	GetLandSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_land' .. math.random(1, @variantsLand) .. '.ogg' if @variantsLand ~= 0

	AddLandSounds: (variants) =>
		@variantsLand = variants
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_land' .. i .. '.ogg') for i = 1, variants
		return @

AddSoundString('player/ppm2/hooves' .. i .. '.ogg') for i = 1, 3
AddSoundString('player/ppm2/falldown.ogg')
AddSoundString('player/ppm2/jump.ogg')

LEmitSound = (ply, name, level = 75, volume = 1, levelIfOnServer = level) ->
	if CLIENT
		ply\EmitSound(name, level, 100, volume) if not game.SinglePlayer() -- Some mods fix this globally (PAC3 for example)
		-- so lets try to avoid problems
		return

	if game.SinglePlayer()
		ply\EmitSound(name, level, 100, volume)
		return

	error('Tried to play unpooled sound: ' .. name) if not SOUND_STRINGS_POOL[name]

	filter = RecipientFilter()
	filter\AddPAS(ply\GetPos())
	filter\RemovePlayer(ply)

	for ply2 in *filter\GetPlayers()
		if ply2\GetInfoBool('ppm2_cl_no_hoofsound', false)
			filter\RemovePlayer(ply2)

	return if filter\GetCount() == 0

	net.Start('ppm2_workaround_emitsound')
	net.WritePlayer(ply)
	net.WriteUInt8(SOUND_STRINGS_POOL[name])
	net.WriteUInt8(levelIfOnServer)
	net.WriteUInt8((volume * 100)\floor())
	net.Send(filter)

class PPM2.PlayerFootstepsListener
	new: (ply) =>
		ply.__ppm2_walkc = @
		@ply = ply
		@walkSpeed = ply\GetWalkSpeed()
		@runSpeed = ply\GetRunSpeed()
		@playedWanderSound = false
		@running = false
		@lastVelocity = @ply\GetVelocity()
		@initialVel = @ply\GetVelocity()
		@lastTrace = @TraceNow()
		@onGround = @ply\OnGround()
		@Validate() if @onGround
		hook.Add 'PlayerFootstep', @, @PlayerFootstep, 8
		hook.Add 'Think', @, @Think

		@lambdaEmitWander = ->
			return if not @ply\IsValid()
			return if not @onGround
			return if not @lastEntry
			sound = @lastEntry\GetWanderSound()
			return if not sound
			@EmitSound(sound, 50, 1, 70)

		@lambdaEmitWalk = ->
			return if not @ply\IsValid()
			return if not @onGround

			if not @lastEntry
				@EmitSound(@@RandHoof(), 50, 0.8, 65)
				return

			@EmitSound(@@RandHoof(), 50, 0.8, 65) if @lastEntry\ShouldPlayHoofclap()
			sound = @lastEntry\GetWalkSound()
			return if not sound
			@EmitSound(sound, 40, 0.8, 55)
			return true

		@lambdaEmitRun = ->
			return if not @ply\IsValid()
			return if not @onGround

			if not @lastEntry
				@EmitSound(@@RandHoof(), 60, 1, 70)
				return

			@EmitSound(@@RandHoof(), 60, 1, 70) if @lastEntry\ShouldPlayHoofclap()
			sound = @lastEntry\GetRunSound()
			return if not sound
			@EmitSound(sound, 40, 0.7, 60)
			return true

	IsValid: => @ply\IsValid() and not @playedWanderSound

	Validate: =>
		newMatType = @lastTrace.MatType == 0 and MAT_DEFAULT or @lastTrace.MatType
		return if @lastMatType == newMatType
		@lastMatType = newMatType
		@lastEntry = PPM2.MaterialSoundEntry\Ask(@lastMatType)

	@RandHoof: => 'player/ppm2/hooves' .. math.random(1, 3) .. '.ogg'

	EmitSound: (name, level = 75, volume = 1, levelIfOnServer = level) => LEmitSound(@ply, name, level, volume, levelIfOnServer)

	PlayWalk: =>
		timer.Simple 0.13, @lambdaEmitWalk
		timer.Simple 0.21, @lambdaEmitWalk
		--timer.Simple 0.19, @lambdaEmitWalk
		return @lambdaEmitWalk()

	PlayWander: =>
		@playedWanderSound = true
		timer.Simple 0.13, @lambdaEmitWander
		timer.Simple 0.17, @lambdaEmitWander
		--timer.Simple 0.27, @lambdaEmitWander
		@lambdaEmitWander()

	PlayRun: =>
		timer.Simple 0.18, @lambdaEmitRun
		--timer.Simple 0.13, @lambdaEmitRun
		--timer.Simple 0.17, @lambdaEmitRun
		return @lambdaEmitRun()

	TraceNow: => @@TraceNow(@ply)

	@TraceNow: (ply, dropToGround) =>
		mins, maxs = ply\GetHull()

		trData = {
			start: ply\GetPos()
			endpos: ply\GetPos() - Vector(0, 0, not dropToGround and 5 or 15)
			:mins, :maxs
			filter: ply
		}

		return util.TraceHull(trData)

	PlayerFootstep: (ply) =>
		return if ply ~= @ply
		return true if CLIENT and @ply ~= LocalPlayer()
		@lastTrace = @TraceNow()
		@Validate()

		if @running
			return @PlayRun()
		else
			return @PlayWalk()

	Think: =>
		@lastVelocity = @ply\GetVelocity()
		vlen = @lastVelocity\Length()
		@onGround = @ply\OnGround()
		@running = vlen >= @runSpeed

		if vlen < @walkSpeed * 0.2
			@ply.__ppm2_walkc = nil
			@PlayWander()

PPM2.MaterialSoundEntry('concrete', MAT_CONCRETE, 11, 11, 5)
PPM2.MaterialSoundEntry('dirt', MAT_DIRT, 11, 11, 5)\AddLandSounds(4)\DisableHoofclap()
PPM2.MaterialSoundEntry('grass', MAT_GRASS, 10, 4, 6)\DisableHoofclap()
PPM2.MaterialSoundEntry('gravel', MAT_DEFAULT, 11, 11, 3)\DisableHoofclap() -- e
PPM2.MaterialSoundEntry('metalbar', MAT_METAL, 11, 11, 6)
PPM2.MaterialSoundEntry('metalbox', MAT_VENT, 10, 9, 4)
PPM2.MaterialSoundEntry('mud', MAT_SLOSH, 10, 9, 4)\DisableHoofclap()
PPM2.MaterialSoundEntry('sand', MAT_SAND, 11, 11, 0)\DisableHoofclap()
PPM2.MaterialSoundEntry('snow', MAT_SNOW, 11, 11, 5)\DisableHoofclap()
PPM2.MaterialSoundEntry('squeakywood', MAT_WOOD, 11, 0, 7)

if CLIENT
	net.receive 'ppm2_workaround_emitsound', ->
		return if DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool()
		ply, sound, level, volume = net.ReadPlayer(), SOUND_STRINGS_POOL_INV[net.ReadUInt8()], net.ReadUInt8(), net.ReadUInt8() / 100
		return if not IsValid(ply)
		ply\EmitSound(sound, level, 100, volume)

hook.Add 'PlayerFootstep', 'PPM2.Hoofstep', (pos, foot, sound, volume, filter) =>
	return if CLIENT and game.SinglePlayer()
	return if not @IsPonyCached() or CLIENT and DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool() or DISABLE_HOOFSTEP_SOUND\GetBool()
	return if @__ppm2_walkc
	return PPM2.PlayerFootstepsListener(@)\PlayerFootstep(@)

ProcessFalldownEvents = (cmd) =>
	return if not @IsPonyCached()
	return if CLIENT and DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool() or DISABLE_HOOFSTEP_SOUND\GetBool()

	if @GetMoveType() ~= MOVETYPE_WALK
		@__ppm2_jump = false
		return

	self2 = @GetTable()
	ground = @OnGround()
	jump = cmd\KeyDown(IN_JUMP)

	if @__ppm2_jump and ground
		@__ppm2_jump = false

		tr = PPM2.PlayerFootstepsListener\TraceNow(@)
		entry = PPM2.MaterialSoundEntry\Ask(tr.MatType == 0 and MAT_DEFAULT or tr.MatType)

		if entry
			if sound = entry\GetLandSound()
				LEmitSound(@, sound, 60, 1, 75)
			elseif not @__ppm2_walkc
				if sound = entry\GetWalkSound()
					LEmitSound(@, sound, 45, 0.2, 55)
					timer.Simple 0.04, -> LEmitSound(@, entry\GetWalkSound(), 45, 0.2, 55)
					timer.Simple 0.07, -> LEmitSound(@, entry\GetWalkSound(), 45, 0.2, 55)
					timer.Simple 0.1, -> LEmitSound(@, entry\GetWalkSound(), 45, 0.2, 55)

			if not entry\ShouldPlayHoofclap()
				return

		LEmitSound(@, 'player/ppm2/falldown.ogg', 60, 1, 75)
	elseif jump and not ground and not @__ppm2_jump
		@__ppm2_jump = true
		LEmitSound(@, 'player/ppm2/jump.ogg', 50, 1, 65)

		tr = PPM2.PlayerFootstepsListener\TraceNow(@, true)
		entry = PPM2.MaterialSoundEntry\Ask(tr.MatType == 0 and MAT_DEFAULT or tr.MatType)

		if not entry or entry\ShouldPlayHoofclap()
			LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 65)
			timer.Simple 0.04, -> LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 65)
			timer.Simple 0.07, -> LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 65)
			timer.Simple 0.1, -> LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 65)

hook.Add 'StartCommand', 'PPM2.Hoofsteps', ProcessFalldownEvents

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

DISABLE_HOOFSTEP_SOUND_CLIENT = CreateConVar('ppm2_cl_no_hoofsound', '0', {FCVAR_ARCHIVE, FCVAR_USERINFO}, 'Disable hoofstep sound play time') if CLIENT
DISABLE_HOOFSTEP_SOUND = CreateConVar('ppm2_no_hoofsound', '0', {FCVAR_REPLICATED}, 'Disable hoofstep sound play time')

hook.Remove('PlayerStepSoundTime', 'PPM2.Hooks')
hook.Add 'PlayerStepSoundTime', 'PPM2.Hoofstep', (stepType = STEPSOUNDTIME_NORMAL, isWalking = false) =>
	return if not @IsPonyCached() or CLIENT and DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool() or DISABLE_HOOFSTEP_SOUND\GetBool()
	rate = @GetPlaybackRate() * .5
	if @Crouching()
		switch stepType
			when STEPSOUNDTIME_NORMAL
				return not isWalking and (300 / rate) or (600 / rate)
			when STEPSOUNDTIME_ON_LADDER
				return 500 / rate
			when STEPSOUNDTIME_WATER_KNEE
				return not isWalking and (400 / rate) or (800 / rate)
			when STEPSOUNDTIME_WATER_FOOT
				return not isWalking and (350 / rate) or (700 / rate)
	else
		switch stepType
			when STEPSOUNDTIME_NORMAL
				return not isWalking and (150 / rate) or (300 / rate)
			when STEPSOUNDTIME_ON_LADDER
				return 500 / rate
			when STEPSOUNDTIME_WATER_KNEE
				return not isWalking and (250 / rate) or (500 / rate)
			when STEPSOUNDTIME_WATER_FOOT
				return not isWalking and (175 / rate) or (350 / rate)

net.pool('ppm2_workaround_emitsound') if SERVER

SOUND_STRINGS_POOL = {}
SOUND_STRINGS_POOL_INV = {}
AddSoundString = (sound) ->
	nextid = #SOUND_STRINGS_POOL_INV + 1
	SOUND_STRINGS_POOL[sound] = nextid
	SOUND_STRINGS_POOL_INV[nextid] = sound

class PPM2.MaterialSoundEntry
	@REGISTRIES = {}

	@Ask = (matType = MAT_DEFAULT) =>
		for reg in *@REGISTRIES
			if reg.material == matType
				return reg

		return false

	new: (name, material, variantsWalk = 0, variantsRun = 0, variantsWander = 0) =>
		table.insert(@@REGISTRIES, @)
		@name = name
		@material = material
		@variantsWalk = variantsWalk
		@variantsRun = variantsRun
		@variantsWander = variantsWander
		@variantsLand = 0
		@playHoofclap = true
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_walk' .. i .. '.ogg') for i = 1, @variantsWalk
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_run' .. i .. '.ogg') for i = 1, @variantsRun
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_wander' .. i .. '.ogg') for i = 1, @variantsWander

	ShouldPlayHoofclap: => @playHoofclap

	DisableHoofclap: =>
		@playHoofclap = false
		return @

	GetWalkSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_walk' .. math.random(1, @variantsWalk) .. '.ogg' if @variantsWalk ~= 0
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_run' .. math.random(1, @variantsRun) .. '.ogg' if @variantsRun ~= 0

	GetRunSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_run' .. math.random(1, @variantsRun) .. '.ogg' if @variantsRun ~= 0
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_walk' .. math.random(1, @variantsWalk) .. '.ogg' if @variantsWalk ~= 0

	GetWanderSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_wander' .. math.random(1, @variantsWander) .. '.ogg' if @variantsWander ~= 0

	GetLandSound: =>
		return 'player/ppm2/' .. @name .. '/' .. @name .. '_land' .. math.random(1, @variantsLand) .. '.ogg' if @variantsLand ~= 0

	AddLandSounds: (variants) =>
		@variantsLand = variants
		AddSoundString('player/ppm2/' .. @name .. '/' .. @name .. '_land' .. i .. '.ogg') for i = 1, variants
		return @

AddSoundString('player/ppm2/hooves' .. i .. '.ogg') for i = 1, 3
AddSoundString('player/ppm2/falldown.ogg')
AddSoundString('player/ppm2/jump.ogg')

LEmitSound = (ply, name, level = 75, volume = 1, levelIfOnServer = level) ->
	if CLIENT
		ply\EmitSound(name, level, 100, volume) if not game.SinglePlayer() -- Some mods fix this globally (PAC3 for example)
		-- so lets try to avoid problems
		return

	if game.SinglePlayer()
		ply\EmitSound(name, level, 100, volume)
		return

	error('Tried to play unpooled sound: ' .. name) if not SOUND_STRINGS_POOL[name]

	filter = RecipientFilter()
	filter\AddPAS(ply\GetPos())
	filter\RemovePlayer(ply)

	for ply2 in *filter\GetPlayers()
		if ply2\GetInfoBool('ppm2_cl_no_hoofsound', false)
			filter\RemovePlayer(ply2)

	return if filter\GetCount() == 0

	net.Start('ppm2_workaround_emitsound')
	net.WritePlayer(ply)
	net.WriteUInt8(SOUND_STRINGS_POOL[name])
	net.WriteUInt8(levelIfOnServer)
	net.WriteUInt8((volume * 100)\floor())
	net.Send(filter)

class PPM2.PlayerFootstepsListener
	new: (ply) =>
		ply.__ppm2_walkc = @
		@ply = ply
		@walkSpeed = ply\GetWalkSpeed()
		@runSpeed = ply\GetRunSpeed()
		@playedWanderSound = false
		@running = false
		@lastVelocity = @ply\GetVelocity()
		@initialVel = @ply\GetVelocity()
		@lastTrace = @TraceNow()
		@onGround = @ply\OnGround()
		@Validate() if @onGround
		hook.Add 'PlayerFootstep', @, @PlayerFootstep, 8
		hook.Add 'Think', @, @Think

		@lambdaEmitWander = ->
			return if not @ply\IsValid()
			return if not @onGround
			return if not @lastEntry
			sound = @lastEntry\GetWanderSound()
			return if not sound
			@EmitSound(sound, 50, 1, 70)

		@lambdaEmitWalk = ->
			return if not @ply\IsValid()
			return if not @onGround

			if not @lastEntry
				@EmitSound(@@RandHoof(), 50, 0.8, 75)
				return

			@EmitSound(@@RandHoof(), 50, 0.8, 75) if @lastEntry\ShouldPlayHoofclap()
			sound = @lastEntry\GetWalkSound()
			return if not sound
			@EmitSound(sound, 40, 0.8, 75)
			return true

		@lambdaEmitRun = ->
			return if not @ply\IsValid()
			return if not @onGround

			if not @lastEntry
				@EmitSound(@@RandHoof(), 60, 1, 95)
				return

			@EmitSound(@@RandHoof(), 60, 1, 95) if @lastEntry\ShouldPlayHoofclap()
			sound = @lastEntry\GetRunSound()
			return if not sound
			@EmitSound(sound, 40, 0.7, 95)
			return true

	IsValid: => @ply\IsValid() and not @playedWanderSound

	Validate: =>
		newMatType = @lastTrace.MatType == 0 and MAT_DEFAULT or @lastTrace.MatType
		return if @lastMatType == newMatType
		@lastMatType = newMatType
		@lastEntry = PPM2.MaterialSoundEntry\Ask(@lastMatType)

	@RandHoof: => 'player/ppm2/hooves' .. math.random(1, 3) .. '.ogg'

	EmitSound: (name, level = 75, volume = 1, levelIfOnServer = level) => LEmitSound(@ply, name, level, volume, levelIfOnServer)

	PlayWalk: =>
		timer.Simple 0.13, @lambdaEmitWalk
		timer.Simple 0.21, @lambdaEmitWalk
		--timer.Simple 0.19, @lambdaEmitWalk
		return @lambdaEmitWalk()

	PlayWander: =>
		@playedWanderSound = true
		timer.Simple 0.13, @lambdaEmitWander
		timer.Simple 0.17, @lambdaEmitWander
		--timer.Simple 0.27, @lambdaEmitWander
		@lambdaEmitWander()

	PlayRun: =>
		timer.Simple 0.18, @lambdaEmitRun
		--timer.Simple 0.13, @lambdaEmitRun
		--timer.Simple 0.17, @lambdaEmitRun
		return @lambdaEmitRun()

	TraceNow: => @@TraceNow(@ply)

	@TraceNow: (ply, dropToGround) =>
		mins, maxs = ply\GetHull()

		trData = {
			start: ply\GetPos()
			endpos: ply\GetPos() - Vector(0, 0, not dropToGround and 5 or 15)
			:mins, :maxs
			filter: ply
		}

		return util.TraceHull(trData)

	PlayerFootstep: (ply) =>
		return if ply ~= @ply
		return true if CLIENT and @ply ~= LocalPlayer()
		@lastTrace = @TraceNow()
		@Validate()

		if @running
			return @PlayRun()
		else
			return @PlayWalk()

	Think: =>
		@lastVelocity = @ply\GetVelocity()
		vlen = @lastVelocity\Length()
		@onGround = @ply\OnGround()
		@running = vlen >= @runSpeed

		if vlen < @walkSpeed * 0.2
			@ply.__ppm2_walkc = nil
			@PlayWander()

PPM2.MaterialSoundEntry('concrete', MAT_CONCRETE, 11, 11, 5)
PPM2.MaterialSoundEntry('dirt', MAT_DIRT, 11, 11, 5)\AddLandSounds(4)\DisableHoofclap()
PPM2.MaterialSoundEntry('grass', MAT_GRASS, 10, 4, 6)\DisableHoofclap()
PPM2.MaterialSoundEntry('gravel', MAT_DEFAULT, 11, 11, 3)\DisableHoofclap() -- e
PPM2.MaterialSoundEntry('metalbar', MAT_METAL, 11, 11, 6)
PPM2.MaterialSoundEntry('metalbox', MAT_VENT, 10, 9, 4)
PPM2.MaterialSoundEntry('mud', MAT_SLOSH, 10, 9, 4)\DisableHoofclap()
PPM2.MaterialSoundEntry('sand', MAT_SAND, 11, 11, 0)\DisableHoofclap()
PPM2.MaterialSoundEntry('snow', MAT_SNOW, 11, 11, 5)\DisableHoofclap()
PPM2.MaterialSoundEntry('squeakywood', MAT_WOOD, 11, 0, 7)

if CLIENT
	net.receive 'ppm2_workaround_emitsound', ->
		return if DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool()
		ply, sound, level, volume = net.ReadPlayer(), SOUND_STRINGS_POOL_INV[net.ReadUInt8()], net.ReadUInt8(), net.ReadUInt8() / 100
		return if not IsValid(ply)
		ply\EmitSound(sound, level, 100, volume)

hook.Add 'PlayerFootstep', 'PPM2.Hoofstep', (pos, foot, sound, volume, filter) =>
	return if CLIENT and game.SinglePlayer()
	return if not @IsPonyCached() or CLIENT and DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool() or DISABLE_HOOFSTEP_SOUND\GetBool()
	return if @__ppm2_walkc
	return PPM2.PlayerFootstepsListener(@)\PlayerFootstep(@)

ProcessFalldownEvents = (cmd) =>
	return if not @IsPonyCached()
	return if CLIENT and DISABLE_HOOFSTEP_SOUND_CLIENT\GetBool() or DISABLE_HOOFSTEP_SOUND\GetBool()

	if @GetMoveType() ~= MOVETYPE_WALK
		@__ppm2_jump = false
		return

	self2 = @GetTable()
	ground = @OnGround()
	jump = cmd\KeyDown(IN_JUMP)

	if @__ppm2_jump and ground
		@__ppm2_jump = false

		tr = PPM2.PlayerFootstepsListener\TraceNow(@)
		entry = PPM2.MaterialSoundEntry\Ask(tr.MatType == 0 and MAT_DEFAULT or tr.MatType)

		if entry
			if sound = entry\GetLandSound()
				LEmitSound(@, sound, 85, 1, 105)
			elseif not @__ppm2_walkc
				if sound = entry\GetWalkSound()
					LEmitSound(@, sound, 45, 0.2, 55)
					timer.Simple 0.04, -> LEmitSound(@, entry\GetWalkSound(), 45, 0.2, 55)
					timer.Simple 0.07, -> LEmitSound(@, entry\GetWalkSound(), 45, 0.2, 55)
					timer.Simple 0.1, -> LEmitSound(@, entry\GetWalkSound(), 45, 0.2, 55)

			if not entry\ShouldPlayHoofclap()
				return

		LEmitSound(@, 'player/ppm2/falldown.ogg', 85, 1, 105)
	elseif jump and not ground and not @__ppm2_jump
		@__ppm2_jump = true
		LEmitSound(@, 'player/ppm2/jump.ogg', 75, 1, 85)

		tr = PPM2.PlayerFootstepsListener\TraceNow(@, true)
		entry = PPM2.MaterialSoundEntry\Ask(tr.MatType == 0 and MAT_DEFAULT or tr.MatType)

		if not entry or entry\ShouldPlayHoofclap()
			LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 75)
			timer.Simple 0.04, -> LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 75)
			timer.Simple 0.07, -> LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 75)
			timer.Simple 0.1, -> LEmitSound(@, PPM2.PlayerFootstepsListener.RandHoof(), 55, 0.4, 75)

hook.Add 'StartCommand', 'PPM2.Hoofsteps', ProcessFalldownEvents


Change stupid campos logic

Change stupid campos logic

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

DLib.RegisterAddonName('PPM/2')

mat_dxlevel = GetConVar('mat_dxlevel')

timer.Create 'PPM2.Unsupported', 600, 4, ->
	if mat_dxlevel\GetInt() >= 90
		timer.Remove 'PPM2.Unsupported'
		return

	Derma_Message('gui.ppm2.dxlevel.not_supported', 'gui.ppm2.dxlevel.toolow')

timer.Create 'PPM2.ModelChecks', 1, 0, ->
	for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
		ent = task.ent
		if IsValid(ent)
			ent.__cachedIsPony = ent\IsPony()

	for _, ply in ipairs player.GetAll()
		if not ply\IsDormant()
			ply.__cachedIsPony = ply\IsPony()
			ponydata = ply\GetPonyData()

			if ply.__cachedIsPony
				if (not ponydata or ponydata\GetHideWeapons()) and not hook.Run('SuppressPonyWeaponsHide', ply) and not ply.RenderOverride
					for _, wep in ipairs ply\GetWeapons()
						if wep
							if not hook.Run('ShouldDrawPonyWeapon', ply, wep) and (not wep.ShouldPonyDraw or not wep\ShouldPonyDraw(ply))
								wep\SetNoDraw(true)
								wep.__ppm2_weapon_hit = true
							elseif wep.__ppm2_weapon_hit
								wep\SetNoDraw(false)
								ply.__ppm2_weapon_hit = false
				else
					for _, wep in ipairs ply\GetWeapons()
						if wep and wep.__ppm2_weapon_hit
							wep\SetNoDraw(false)
							ply.__ppm2_weapon_hit = false

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

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

lastDataSend = 0
lastDataReceived = 0

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

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

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

if not IsValid(LocalPlayer())
	times = 0
	hook.Add 'Think', 'PPM2.RequireData', ->
		ply = LocalPlayer()
		return if not IsValid(ply)
		times += 1 if ply\GetVelocity()\Length() > 5

		return if times < 200
		hook.Remove 'Think', 'PPM2.RequireData'
		hook.Add 'KeyPress', 'PPM2.RequireData', ->
			hook.Remove 'KeyPress', 'PPM2.RequireData'
			RunConsoleCommand('ppm2_reload')
			timer.Simple 3, -> RunConsoleCommand('ppm2_require')
else
	timer.Simple 0, ->
		RunConsoleCommand('ppm2_reload')
		timer.Simple 3, -> RunConsoleCommand('ppm2_require')

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

net.receive 'PPM2.EditorCamPos', ->
	ply = net.ReadPlayer()
	return if not ply\IsValid()
	pVector, pAngle = net.ReadVector(), net.ReadAngle()

	if not IsValid(ply.__ppm2_cam)
		ply.__ppm2_cam = ClientsideModel('models/tools/camera/camera.mdl', RENDERGROUP_BOTH)
		ply.__ppm2_cam\SetModelScale(0.4)
		ply.__ppm2_cam.RenderOverride = =>
			return if not ply.__ppm2_campos_lerp
			render.DrawLine(ply.__ppm2_campos_lerp, ply\EyePos(), color_white, true)
			@DrawModel()

		hook.Add 'Think', ply.__ppm2_cam, =>
			return @Remove() if not IsValid(ply) or not ply\GetNWBool('PPM2.InEditor')
			findPos, findAng = LocalToWorld(pVector, pAngle, ply\GetPos(), ply\EyeAngles())
			ply.__ppm2_campos_lerp = Lerp(RealFrameTime() * 22, ply.__ppm2_campos_lerp or findPos, findPos)
			@SetPos(ply.__ppm2_campos_lerp)
			@SetAngles(findAng)

hook.Add 'HUDPaint', 'PPM2.EditorStatus', ->
	lply = LocalPlayer()
	lpos = lply\EyePos()

	editortext = DLib.i18n.localize('tip.ppm2.in_editor')

	for ply in *player.GetAll()
		if ply ~= lply and ply\GetNWBool('PPM2.InEditor')
			pos = ply\EyePos()
			dist = pos\Distance(lpos)

			if dist < 250
				pos.z += 10
				alpha = (1 - dist\progression(0, 250))\max(0.1) * 255
				{:x, :y} = pos\ToScreen()
				draw.DrawText(editortext, 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)
				draw.DrawText(editortext, 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)

			if ply.__ppm2_campos
				pos = Vector(ply.__ppm2_campos)
				dist = pos\Distance(lpos)

				if dist < 250
					pos.z += 9
					alpha = (1 - dist\progression(0, 250))\max(0.1) * 255
					{:x, :y} = pos\ToScreen()
					text = DLib.i18n.localize('tip.ppm2.camera', ply\Nick())
					draw.DrawText(text, 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)
					draw.DrawText(text, 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)

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

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

cvars.AddChangeCallback('mat_picmip', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_generic', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_body', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

return

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

DLib.RegisterAddonName('PPM/2')

mat_dxlevel = GetConVar('mat_dxlevel')

timer.Create 'PPM2.Unsupported', 600, 4, ->
	if mat_dxlevel\GetInt() >= 90
		timer.Remove 'PPM2.Unsupported'
		return

	Derma_Message('gui.ppm2.dxlevel.not_supported', 'gui.ppm2.dxlevel.toolow')

timer.Create 'PPM2.ModelChecks', 1, 0, ->
	for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
		ent = task.ent
		if IsValid(ent)
			ent.__cachedIsPony = ent\IsPony()

	for _, ply in ipairs player.GetAll()
		if not ply\IsDormant()
			ply.__cachedIsPony = ply\IsPony()
			ponydata = ply\GetPonyData()

			if ply.__cachedIsPony
				if (not ponydata or ponydata\GetHideWeapons()) and not hook.Run('SuppressPonyWeaponsHide', ply) and not ply.RenderOverride
					for _, wep in ipairs ply\GetWeapons()
						if wep
							if not hook.Run('ShouldDrawPonyWeapon', ply, wep) and (not wep.ShouldPonyDraw or not wep\ShouldPonyDraw(ply))
								wep\SetNoDraw(true)
								wep.__ppm2_weapon_hit = true
							elseif wep.__ppm2_weapon_hit
								wep\SetNoDraw(false)
								ply.__ppm2_weapon_hit = false
				else
					for _, wep in ipairs ply\GetWeapons()
						if wep and wep.__ppm2_weapon_hit
							wep\SetNoDraw(false)
							ply.__ppm2_weapon_hit = false

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

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

lastDataSend = 0
lastDataReceived = 0

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

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

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

if not IsValid(LocalPlayer())
	times = 0
	hook.Add 'Think', 'PPM2.RequireData', ->
		ply = LocalPlayer()
		return if not IsValid(ply)
		times += 1 if ply\GetVelocity()\Length() > 5

		return if times < 200
		hook.Remove 'Think', 'PPM2.RequireData'
		hook.Add 'KeyPress', 'PPM2.RequireData', ->
			hook.Remove 'KeyPress', 'PPM2.RequireData'
			RunConsoleCommand('ppm2_reload')
			timer.Simple 3, -> RunConsoleCommand('ppm2_require')
else
	timer.Simple 0, ->
		RunConsoleCommand('ppm2_reload')
		timer.Simple 3, -> RunConsoleCommand('ppm2_require')

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

net.receive 'PPM2.EditorCamPos', ->
	ply = net.ReadPlayer()
	return if not ply\IsValid()
	ply.__ppm2_campos, ply.__ppm2_camang = LocalToWorld(net.ReadVector(), net.ReadAngle(), ply\GetPos(), ply\EyeAngles())

	if not IsValid(ply.__ppm2_cam)
		ply.__ppm2_cam = ClientsideModel('models/tools/camera/camera.mdl', RENDERGROUP_BOTH)
		ply.__ppm2_cam\SetModelScale(0.4)
		ply.__ppm2_cam.RenderOverride = =>
			render.DrawLine(ply.__ppm2_campos_lerp, ply\EyePos(), color_white, true)
			@DrawModel()

		hook.Add 'Think', ply.__ppm2_cam, =>
			return @Remove() if not IsValid(ply) or not ply\GetNWBool('PPM2.InEditor')
			ply.__ppm2_campos_lerp = Lerp(RealFrameTime() * 22, ply.__ppm2_campos_lerp or ply.__ppm2_campos, ply.__ppm2_campos)
			@SetPos(ply.__ppm2_campos_lerp)
			@SetAngles(ply.__ppm2_camang)

hook.Add 'HUDPaint', 'PPM2.EditorStatus', ->
	lply = LocalPlayer()
	lpos = lply\EyePos()

	editortext = DLib.i18n.localize('tip.ppm2.in_editor')

	for ply in *player.GetAll()
		if ply ~= lply and ply\GetNWBool('PPM2.InEditor')
			pos = ply\EyePos()
			dist = pos\Distance(lpos)

			if dist < 250
				pos.z += 10
				alpha = (1 - dist\progression(0, 250))\max(0.1) * 255
				{:x, :y} = pos\ToScreen()
				draw.DrawText(editortext, 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)
				draw.DrawText(editortext, 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)

			if ply.__ppm2_campos
				pos = Vector(ply.__ppm2_campos)
				dist = pos\Distance(lpos)

				if dist < 250
					pos.z += 9
					alpha = (1 - dist\progression(0, 250))\max(0.1) * 255
					{:x, :y} = pos\ToScreen()
					text = DLib.i18n.localize('tip.ppm2.camera', ply\Nick())
					draw.DrawText(text, 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)
					draw.DrawText(text, 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)

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

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

cvars.AddChangeCallback('mat_picmip', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_generic', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

cvars.AddChangeCallback('ppm2_cl_hires_body', (->
	timer.Simple 0, (->
		RunConsoleCommand('ppm2_require')
		RunConsoleCommand('ppm2_reload')
	)
), 'ppm2')

return


aunesand: update to new URL

aunesand: update to new URL

html2unicode = require'html'

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

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

PRIVMSG:
  '^%paunesand': (source, destination) =>
    rnd = math.random(1,3000)
    ivar2.util.simplehttp 'http://p3.no/uv/aune-sands-poesigenerator/index_new.php?cachebuster='..rnd, (html) ->
      match = html\match[[<a title="Del diktet på Facebook" href="(.*)"]]
      if match
        poem = match\match[[facebookdescription=(.-)-]]
        poem = poem\gsub '+', ' '
        poem = unescape(poem)
        if poem
          say html2unicode(poem)

html2unicode = require'html'

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

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

PRIVMSG:
  '^%paunesand': (source, destination) =>
    rnd = math.random(1,3000)
    ivar2.util.simplehttp 'http://p3.no/uv/aune-sands-poesigenerator/?cachebuster='..rnd, (html) ->
      match = html\match[[<a title="Del diktet på Facebook" href="(.*)"]]
      if match
        poem = match\match[[facebookdescription=(.-)-]]
        poem = poem\gsub '+', ' '
        poem = unescape(poem)
        if poem
          say html2unicode(poem)


fixed debug

fixed debug

import Widget from require "lapis.html"
import slugify, to_json from require "lapis.util"

class loginDo extends Widget
  content: =>
    if @errors
      h1 "Login failed"
      ul ->
        for e in *@errors
          li e
      a href: @url_for("lazuli_modules_usermanagement_login"), "Back"
    else
      h1 "Login successful"
      text "User id: "..@modules.user_management.currentuser.id
      raw "<!--"..to_json(@modules.user_management.currentuser.logged_in_providers_tried).."-->"
    div class: "providers", ->
      for k,v in pairs @providerHtml
        div class: "provider "..slugify(k), ->
          raw v

import Widget from require "lapis.html"
import slugify, to_json from require "lapis.util"

class loginDo extends Widget
  content: =>
    if @errors
      h1 "Login failed"
      ul ->
        for e in *@errors
          li e
      a href: @url_for("lazuli_modules_usermanagement_login"), "Back"
    else
      h1 "Login successful"
      text "User id: "..@modules.user_management.currentuser.id
    div class: "providers", ->
      for k,v in pairs @providerHtml
        div class: "provider "..slugify(k), ->
          raw v
    raw "<!--"..to_json(@modules.user_management.currentuser.logged_in_providers_tried).."-->"


benis: remove unused import

benis: remove unused import

Former-commit-id: f2f92c28882e2509ea2a5f15c0cec6a4112c3ac6 [formerly 82941ea891d90df0aa64715ee6e77c7a0862e46f]
Former-commit-id: 401bdd2dacd9413bdf61e5a83782549d5bb0e080

-- code from q66's module from luabot ported from rfs benis module
reduce = (fun, list, def) ->
  for k, v in ipairs(list)
    def = fun(def, v)
  return def

rand = math.random

benisify = (s) ->
  reduce (acc, f) ->
    f(acc),
    {
      (s) -> s\lower()
      (s) -> s\gsub('x', 'cks')
      (s) -> s\gsub('ing', 'in')
      (s) -> s\gsub('you', 'u')
      (s) -> s\gsub 'oo', ->
        ('u')\rep(rand(1, 5))
      (s) ->
        s\gsub '[%w_]%z', (x) ->
          x\sub(1, 1)\rep(rand(1, 2))
      (s) ->
        s\gsub 'ck', ->
          'g'\rep(rand(1, 5))
      (s) ->
        s\gsub '(t+)%f[aeiouys ]', (x) ->
          ('d')\rep(#x)
      (s) ->
        s\gsub '(t+)$', (x) ->
          ('d')\rep(#x)
      (s) -> s\gsub('p', 'b'),
      (s) -> s\gsub('%f[%w_]the%f[^%w_]', 'da'),
      (s) -> s\gsub('%f[%w_]c', 'g'),
      (s) -> s\gsub('%f[%w_]is%f[^%w_]', 'are'),
      (s) -> s\gsub '(c+)(.)', (x, y) ->
        (y == 'e' or y == 'i' or y == 'y') and (x .. y) or ('g')\rep(rand(1, 5)) ..  (y == 'c' and 'g' or y)
      (s) ->
        s\gsub '([qk]+)%f[aeiouy ]', ->
          ('g')\rep(rand(1, 5))
      (s) ->
        s\gsub '([qk]+)$', ->
          ('g')\rep(rand(1, 5))
      (s) ->
          endswith = s\find '[?!.]$'
          repf = (x) ->
              ret = x\rep(rand(2, 5)) .. ' '
              for i = 1, rand(2, 5)
                  ret = ret .. (':')\rep(rand(1, 2)) .. ('D')\rep(rand(1, 4))
              return ret
          unless endswith then return s\gsub('$', repf)
          s\gsub('[?!.]+', repf)
    }, s

PRIVMSG:
  '^%pbenis (.*)$':(source, destination, arg) =>
    say(benisify(arg))

-- code from q66's module from luabot ported from rfs benis module
reduce = (fun, list, def) ->
  for k, v in ipairs(list)
    def = fun(def, v)
  return def

rand = math.random
tconc = table.concat

benisify = (s) ->
  reduce (acc, f) ->
    f(acc),
    {
      (s) -> s\lower()
      (s) -> s\gsub('x', 'cks')
      (s) -> s\gsub('ing', 'in')
      (s) -> s\gsub('you', 'u')
      (s) -> s\gsub 'oo', ->
        ('u')\rep(rand(1, 5))
      (s) ->
        s\gsub '[%w_]%z', (x) ->
          x\sub(1, 1)\rep(rand(1, 2))
      (s) ->
        s\gsub 'ck', ->
          'g'\rep(rand(1, 5))
      (s) ->
        s\gsub '(t+)%f[aeiouys ]', (x) ->
          ('d')\rep(#x)
      (s) ->
        s\gsub '(t+)$', (x) ->
          ('d')\rep(#x)
      (s) -> s\gsub('p', 'b'),
      (s) -> s\gsub('%f[%w_]the%f[^%w_]', 'da'),
      (s) -> s\gsub('%f[%w_]c', 'g'),
      (s) -> s\gsub('%f[%w_]is%f[^%w_]', 'are'),
      (s) -> s\gsub '(c+)(.)', (x, y) ->
        (y == 'e' or y == 'i' or y == 'y') and (x .. y) or ('g')\rep(rand(1, 5)) ..  (y == 'c' and 'g' or y)
      (s) ->
        s\gsub '([qk]+)%f[aeiouy ]', ->
          ('g')\rep(rand(1, 5))
      (s) ->
        s\gsub '([qk]+)$', ->
          ('g')\rep(rand(1, 5))
      (s) ->
          endswith = s\find '[?!.]$'
          repf = (x) ->
              ret = x\rep(rand(2, 5)) .. ' '
              for i = 1, rand(2, 5)
                  ret = ret .. (':')\rep(rand(1, 2)) .. ('D')\rep(rand(1, 4))
              return ret
          unless endswith then return s\gsub('$', repf)
          s\gsub('[?!.]+', repf)
    }, s

PRIVMSG:
  '^%pbenis (.*)$':(source, destination, arg) =>
    say(benisify(arg))


emoji: em8ball

emoji: em8ball

Former-commit-id: 3848783deb20dc7071da06c2330b3db06ad4e1d2 [formerly edfbe083cd9961c0036ceedaa5b0c0e01c8b7afc]
Former-commit-id: 4ad4e33bf2517514bf6583bac320e7e614b2e5cb

util = require 'util'
emojis =
  emoticons:'😀😁😂😃😄😅😆😇😈😉😊😋😌😍😎😏
    😐😑😒😓😔😕😖😗😘😙😚😛😜😝😞😟
    😠😡😢😣😤😥😦😧😨😩😪😫😬😭😮😯
    😰😱😲😳😴😵😶😷😸😹😺😻😼😽😾😿
    🙀🙅🙆🙇🙈🙉🙊🙋🙌🙍🙎🙏',
  transport:'🚀🚁🚂🚃🚄🚅🚆🚇🚈🚉🚊🚋🚌🚍🚎🚏
    🚐🚑🚒🚓🚔🚕🚖🚗🚘🚙🚚🚛🚜🚝🚞🚟
    🚠🚡🚢🚣🚤🚥🚦🚧🚨🚩🚪🚫🚬🚭🚮🚯
    🚰🚱🚲🚳🚴🚵🚶🚷🚸🚹🚺🚻🚼🚽🚾🚿
    🛀🛁🛂🛃🛄🛅',
  symbol:'☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏
    ☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟
    ☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯
    ☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿
    ♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏
    ♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟
    ♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯
    ♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿
    ⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏
    ⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟
    ⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯
    ⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿
    ⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛎⛏
    ⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛟
    ⛠⛡⛢⛣⛤⛥⛦⛧⛨⛩⛪⛫⛬⛭⛮⛯
    ⛰⛱⛲⛳⛴⛵⛶⛷⛸⛹⛺⛻⛼⛽⛾⛿',
  misc:'🌀🌁🌂🌃🌄🌅🌆🌇🌈🌉🌊🌋🌌🌍🌎🌏
    🌐🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜🌝🌞🌟
    🌠
    🌰🌱🌲🌳🌴🌵🌷🌸🌹🌺🌻🌼🌽🌾🌿
    🍀🍁🍂🍃🍄🍅🍆🍇🍈🍉🍊🍋🍌🍍🍎🍏
    🍐🍑🍒🍓🍔🍕🍖🍗🍘🍙🍚🍛🍜🍝🍞🍟
    🍠🍡🍢🍣🍤🍥🍦🍧🍨🍩🍪🍫🍬🍭🍮🍯
    🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🍻🍼
    🎀🎁🎂🎃🎄🎅🎆🎇🎈🎉🎊🎋🎌🎍🎎🎏
    🎐🎑🎒🎓
    🎠🎡🎢🎣🎤🎥🎦🎧🎨🎩🎪🎫🎬🎭🎮🎯
    🎰🎱🎲🎳🎴🎵🎶🎷🎸🎹🎺🎻🎼🎽🎾🎿
    🏀🏁🏂🏃🏄🏆🏇🏈🏉🏊

    🏠🏡🏢🏣🏤🏥🏦🏧🏨🏩🏪🏫🏬🏭🏮🏯
    🏰
    🐀🐁🐂🐃🐄🐅🐆🐇🐈🐉🐊🐋🐌🐍🐎🐏
    🐐🐑🐒🐓🐔🐕🐖🐗🐘🐙🐚🐛🐜🐝🐞🐟
    🐠🐡🐢🐣🐤🐥🐦🐧🐨🐩🐪🐫🐬🐭🐮🐯
    🐰🐱🐲🐳🐴🐵🐶🐷🐸🐹🐺🐻🐼🐽🐾
    👀👂👃👄👅👆👇👈👉👊👋👌👍👎👏
    👐👑👒👓👔👕👖👗👘👙👚👛👜👝👞👟
    👠👡👢👣👤👥👦👧👨👩👪👫👬👭👮👯
    👰👱👲👳👴👵👶👷👸👹👺👻👼👽👾👿
    💀💁💂💃💄💅💆💇💈💉💊💋💌💍💎💏
    💐💑💒💓💔💕💖💗💘💙💚💛💜💝💞💟
    💠💡💢💣💤💥💦💧💨💩💪💫💬💭💮💯
    💰💱💲💳💴💵💶💷💸💹💺💻💼💽💾💿
    📀📁📂📃📄📅📆📇📈📉📊📋📌📍📎📏
    📐📑📒📓📔📕📖📗📘📙📚📛📜📝📞📟
    📠📡📢📣📤📥📦📧📨📩📪📫📬📭📮📯
    📰📱📲📳📴📵📶📷📹📺📻📼
    🔀🔁🔂🔃🔄🔅🔆🔇🔈🔉🔊🔋🔌🔍🔎🔏
    🔐🔑🔒🔓🔔🔕🔖🔗🔘🔙🔚🔛🔜🔝🔞🔟
    🔠🔡🔢🔣🔤🔥🔦🔧🔨🔩🔪🔫🔬🔭🔮🔯
    🔰🔱🔲🔳🔴🔵🔶🔷🔸🔹🔺🔻🔼🔽
    🕀🕁🕂🕃
    🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛🕜🕝🕞🕟
    🕠🕡🕢🕣🕤🕥🕦🕧
    🗻🗼🗽🗾🗿'

for cat, str in pairs emojis
  str, n = str\gsub('%s', '')
  emojis[cat] = str

PRIVMSG:
  '^%pemoji all$': (source, dest, arg) =>
    out = {}
    for cat, str in pairs emojis
      table.insert out, str
    say table.concat out
  '^%pemoji$': (source, dest, arg) =>
    out = {}
    for cat, str in pairs emojis
      table.insert out, cat
    say table.concat(out, ', ')
  '^%pemoji (.+)$': (source, dest, cat) =>
    say emojis[cat]
  '^%pem8ball(.+)$': (source, dest, arg) =>
    out = {}
    for cat, str in pairs emojis
      for uchar in util.utf8.chars(str)
        table.insert out, uchar
    choice = math.random(1, #out)
    say out[choice]


utf8: combining underline

utf8: combining underline

util = require'util'
html2unicode = require'html'

an = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.?!"'`()[]{}<>&_]]
charmaps = {
ci: 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl: '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'
ud: [[∀BƆDƎℲפHIſK˥WNOԀQRS┴∩ΛMX⅄Zɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz0ƖᄅƐㄣϛ9ㄥ86'˙¿¡,,,)(][}{><⅋‾]]
nc: [[🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿]]
sq: [[🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789,⊡]]
ns: [[🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉]]
ds: [[𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡]]
bo: [[𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗]]
bi: [[𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛0123456789]]
bs: [[𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃]]
pt: [[⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵0⑴⑵⑶⑷⑸⑹⑺⑻⑼]]
tl: [[ค๒ς๔єŦﻮђเןкl๓ภ๏קợгรtยשฬץאzค๒ς๔єŦﻮђเןкl๓ภ๏קợгรtยשฬץאz0123456789,.؟!"'`()[]{}«»&_]]
ru: [[ДЬCDЗFGHIJКLMИФPQЯSTЦVШЖУZДЪCDЭFGHIJКLMЙФPQЯSTЦVЩЖЧZ0123456789,.?!"'`()[]{}<>&_]]
}

codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

mimic = {
  " ": {" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "},
  ["!"]: {"！", "ǃ", "ⵑ", "︕", "﹗", },
  ["\""]: {"＂", },
  ["#"]: {"＃", "﹟", },
  ["$"]: {"＄", "﹩", },
  ["%"]: {"％", "٪", "⁒", "﹪", },
  ["&"]: {"＆", "﹠", },
  ["'"]: {"＇", "ʹ", },
  ["("]: {"（", "⟮", "﹙", },
  [")"]: {"）", "⟯", "﹚", },
  ["*"]: {"＊", "⋆", "﹡", },
  ["+"]: {"＋", "᛭", "﹢", },
  [","]: {"，", "ˏ", "ᛧ", "‚", },
  ["-"]: {"－", "˗", " ", "ᝍ", "᠆", "ᱼ", "−", "⎯", "⎼", "╴", "ⲻ", "ⲻ", "ー", "ㄧ", "﹣", },
  ["."]: {"．", "․", },
  ["/"]: {"／", "᜵", "⁄", "∕", "⧸", "Ⳇ", "〳", },
  ["0"]: {"᱐", },
  ["2"]: {"ᒿ", },
  ["3"]: {"Ʒ", "ᢃ", "ℨ", "Ⳅ", "Ⳍ", "ⳍ", },
  ["4"]: {"Ꮞ", },
  ["6"]: {"Ꮾ", },
  ["9"]: {"Ꮽ", },
  [":"]: {"：", "ː", "˸", "։", "፡", "᛬", "᠄", "ᱺ", "⁚", "∶", "⠆", "︓", "︰", "﹕", },
  [";"]: {"；", ";", "︔", "﹔", },
  ["<"]: {"＜", "˂", "‹", "≺", "❮", "ⵦ", "〱", "ㄑ", "﹤", },
  ["="]: {"＝", "═", "⚌", "゠", "﹦", },
  [">"]: {"＞", "˃", "›", "≻", "❯", "﹥", },
  ["?"]: {"？", "︖", "﹖", },
  ["@"]: {"＠", "﹫", },
  ["A"]: {"Α", "А", "Ꭺ", },
  ["B"]: {"Β", "В", "Ᏼ", "ᗷ", "Ⲃ", },
  ["C"]: {"Ϲ", "С", "Ꮯ", "Ⅽ", "Ⲥ", },
  ["D"]: {"Ꭰ", "ᗪ", "Ⅾ", },
  ["E"]: {"Ε", "Е", "Ꭼ", },
  ["F"]: {"ᖴ", },
  ["G"]: {"Ԍ", "Ꮐ", },
  ["H"]: {"Η", "Н", "ዘ", "Ꮋ", "ᕼ", "Ⲏ", },
  ["I"]: {"Ι", "І", "Ⅰ", },
  ["J"]: {"Ј", "Ꭻ", "ᒍ", },
  ["K"]: {"Κ", "Κ", "Ꮶ", "ᛕ", "K", "Ⲕ", },
  ["L"]: {"Ꮮ", "ᒪ", "Ⅼ", "Ⳑ", },
  ["M"]: {"Μ", "Ϻ", "М", "Ꮇ", "Ⅿ", },
  ["N"]: {"Ν", "Ⲛ", },
  ["O"]: {"Ο", "О", "ᱛ", "Ⲟ", },
  ["P"]: {"Ρ", "Р", "Ꮲ", "Ⲣ", },
  ["Q"]: {"Ԛ", "Ⴓ", "ⵕ", },
  ["R"]: {"Ꭱ", "Ꮢ", "ᖇ", },
  ["S"]: {"Ѕ", "Ⴝ", "Ꮪ", },
  ["T"]: {"Τ", "Т", "Ꭲ", },
  ["V"]: {"Ꮩ", "Ⅴ", },
  ["W"]: {"Ꮃ", "Ꮤ", },
  ["X"]: {"Χ", "Х", "Ⅹ", "Ⲭ", },
  ["Y"]: {"Υ", "Ⲩ", },
  ["Z"]: {"Ζ", "Ꮓ", },
  ["["]: {"［", },
  ["\\"]: {"＼", "∖", "⧵", "⧹", "〵", "﹨", },
  ["]"]: {"］", },
  ["^"]: {"＾", "˄", "ˆ", "ᶺ", "⌃", },
  ["_"]: {"＿", "ˍ", "⚊", "﹘", },
  ["`"]: {"｀", "ˋ", "`", "‵", },
  ["a"]: {"ɑ", "а", },
  ["c"]: {"ϲ", "с", "ⅽ", },
  ["d"]: {"ԁ", "ⅾ", },
  ["e"]: {"е", "ᥱ", },
  ["g"]: {"ɡ", },
  ["h"]: {"һ", },
  ["i"]: {"і", "ⅰ", },
  ["j"]: {"ϳ", "ј", },
  ["l"]: {"ⅼ", },
  ["m"]: {"ⅿ", },
  ["n"]: {"ᥒ", },
  ["o"]: {"ο", "о", "೦", "ഠ", "൦", "ᦞ", "᧐", "ⲟ", },
  ["p"]: {"р", "ⲣ", },
  ["s"]: {"ѕ", },
  ["u"]: {"ᥙ", "∪", },
  ["v"]: {"ᴠ", "ⅴ", "∨", "⋁", },
  ["w"]: {"ᴡ", },
  ["x"]: {"х", "ⅹ", "ⲭ", },
  ["y"]: {"у", "ỿ", },
  ["z"]: {"ᤁ", "ᴢ", },
  ["{"]: {"｛", "﹛", },
  ["|"]: {"｜", "ǀ", "ᛁ", "⎜", "⎟", "⎢", "⎥", "⎪", "⎮", "⼁", "〡", "丨", "︱", "︳", "￨", },
  ["}"]: {"｝", "﹜", },
  ["~"]: {"～", "˜", "⁓", "∼", "〜", },
}

-- Construct a table which can be used for lookup replacement later
-- Iterate over normal ascii and find counterparts in the weirdo strings as substrings
maps = {}
for charmap, chars in pairs charmaps
  i = 1
  maps[charmap] = {}
  for uchar in util.utf8.chars(chars)
    maps[charmap][an\sub(i,i)] = uchar
    i = i +1

unichr = (n) ->
  html2unicode('&#x%x;'\format(n))

wireplace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      -- Replace space width ideographic space for fullwidth offset
      if bc == 32 and offset == 0xFEE0
        t[#t + 1] = '\227\128\128'
      elseif bc == 32
        t[#t + 1] = ' '
      elseif bc < 0x80 then
        t[#t + 1] = html2unicode("&#" .. (offset + bc) .. ";")
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

remap = (map, s) ->
  table.concat [map[s\sub(i,i)] or s\sub(i,i) for i=1, #s], ''

zalgo = (text, intensity=50) ->
  -- Limit text to prevent DoS
  text = text\sub(1,512)
  zalgo_chars = {}
  for i=0x0300, 0x036f
    zalgo_chars[i-0x2ff] = unichr(i)

  zalgo_chars[#zalgo_chars + 1] = unichr(0x0488)
  zalgo_chars[#zalgo_chars + 0] = unichr(0x0489)

  zalgoized = {}
  for letter in codepoints(text)
    zalgoized[#zalgoized + 1] = letter
    zalgo_num = math.random(1, intensity)
    for i=1, zalgo_num
      zalgoized[#zalgoized + 1] = zalgo_chars[math.random(1, #zalgo_chars)]
  table.concat(zalgoized)

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    say wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    say remap(maps.bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    say remap(maps.ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    say zalgo(arg, 7)
  '^%pupsidedown (.+)$': (source, destination, arg) =>
    say remap(maps.ud, util.utf8.reverse(arg))
  '^%pflip (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pthrow (.+)$': (source, destination, arg) =>
    say "（╯°□°）╯︵ #{remap maps.ud, util.utf8.reverse(arg)}"
  '^%pparanthesized (.+)$': (source, destination, arg) =>
    say remap(maps.pt, arg)
  '^%pnegcircle (.+)$': (source, destination, arg) =>
    say remap(maps['nc'], arg)
  '^%psquare (.+)$': (source, destination, arg) =>
    say remap(maps.sq, arg)
  '^%pnegsquare (.+)$': (source, destination, arg) =>
    say remap(maps.ns, arg)
  '^%pdoublestruck (.+)$': (source, destination, arg) =>
    say remap(maps.ds, arg)
  '^%pubold (.+)$': (source, destination, arg) =>
    say remap(maps.bo, arg)
  '^%pbolditalic (.+)$': (source, destination, arg) =>
    say remap(maps.bi, arg)
  '^%pboldscript (.+)$': (source, destination, arg) =>
    say remap(maps.bs, arg)
  '^%pthai (.+)$': (source, destination, arg) =>
    say remap(maps.tl, arg)
  '^%prussian (.+)$': (source, destination, arg) =>
    say remap(maps.ru, arg)
  '^%putfuk (.+)$': (source, destination, arg) =>
    keys = [x for x,_ in pairs(maps)]
    say table.concat([remap(maps[keys[math.random(#keys)]], letter) for letter in codepoints(arg)])
  '^%pmimic (.+)$': (source, destination, arg) =>
      out = {}
      for uchar in util.utf8.chars(arg)
        mimictbl =  mimic[uchar]
        if mimictbl
          out[#out+1] = mimictbl[math.random(#mimictbl)]
        else
          out[#out+1] = uchar
      say table.concat(out)
  '^%pul (.+)$': (source, destination, arg) =>
    -- combining low line 0332 => CC B2
    say table.concat([letter .. string.char(0xCC) .. string.char(0xB2) for letter in codepoints(arg)])