Example: fix bad error handling.

Example: fix bad error handling.

-- This example is unlicensed under CC0

export script_name        = "ASSInspector Example"
export script_description = "Calculates bounding rectangles on the start and end times of selected events."
export script_author      = "torque"
export script_version     = 0x000003

ffi  = require( 'ffi' )
util = require( 'aegisub.util' )
log  = require( 'a-mo.log' )

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

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, uint32_t, const char* );
void        assi_setFontDir( void*, const char* );
int         assi_setScript( void*, const char*, uint32_t, const char *, const uint32_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." )

-- Script-wide local variables are stored until the script is reloaded,
-- at which point they are garbage collected. We can abuse this system
-- to create a persistent ASSInspector state that only gets deallocated
-- when the automation script is reloaded or Aegisub is quit.

-- NOTE: this can be somewhat risky. For example, loading a new subtitle
-- file in Aegisub won't reset the automation scripts, and thus the old
-- header information will continue being used. It would be better to
-- make some uniqueness check on the loaded script. The full path to the
-- script would be an option, except that doesn't work for scripts that
-- have yet to be saved to disk. Appending some sort of UUID to the
-- script header and checking that each run would probably be more
-- foolproof. This implementation detail is left as an exercise to the
-- user.
inspector = nil
initializeInspector = ( resX, resY, minimalHeader ) ->
	if nil == inspector
		inspector = ffi.gc( ASSInspector.assi_init( resX, resY, minimalHeader, #minimalHeader, aegisub.decode_path( libraryPath .. "/fonts.conf" ) ), ASSInspector.assi_cleanup )
		if nil == inspector
			log.windowError( "ASSInspector library init failed." )

		-- Tell ASSInspector to tell fontconfig to search the fonts directory
		-- in the script directory for fonts to load.
		if '?script' != aegisub.decode_path( '?script' )
			ASSInspector.assi_setFontDir( inspector, aegisub.decode_path( '?script/fonts' ) )

dumpRect = ( event, rect ) ->
	bounds = {
		x:  tonumber( rect.x )
		y:  tonumber( rect.y )
		x2: tonumber( rect.w + rect.x )
		y2: tonumber( rect.h + rect.y )
	}

	newEvent = util.deep_copy( event )
	with bounds
		newEvent.text = ([[{\an7\pos(0,0)\bord0\shad0\c&H0000FF&\1a&H00&\p1}m %d %d l %d %d %d %d %d %d]])\format( .x, .y, .x2, .y, .x2, .y2, .x, .y2 )

	event.layer += 1
	return newEvent

mainFunction = ( subtitle, selectedLines, activeLine ) ->
	if ASSInspector.assi_getVersion( ) < script_version
		log.windowError( "Installed ASSInspector library is outdated. Please update it." )
		return selectedLines
	elseif ASSInspector.assi_getVersion( ) > script_version
		log.windowError( "Installed OptimASS is outdated. Please update it." )
		return selectedLines

	scriptHeader = {
		"[Script Info]"
	}

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

	styles = { }

	-- If there is no video (or, at minimum, timecodes) loaded, this will
	-- return nil.
	hasTimecodes = aegisub.frame_from_ms( 0 )

	-- Copy these functions into local variables to shorten them.
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	subtitleLen = #subtitle
	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, subtitleLen
		with line = subtitle[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. Not convinced this is a great idea because people may
	-- use smaller workraws or something like that. Well, hopefully they
	-- don't do that for typesetting.
	vidResX, vidResY = aegisub.video_size( )
	resX = vidResX or resX
	resY = vidResY or resY

	minimalHeader = table.concat( scriptHeader, '\n' )

	initializeInspector( resX, resY, minimalHeader )

	for eventIndex in *selectedLines
		with event = subtitle[eventIndex]
			-- This does not account for \r[name] being in the line.
			styles = (styles[.style] or "") .. "\n[Events]\n"
			events = .raw

			-- It occurs to me that perhaps having the user just pass in one
			-- string that contains both styles and events may be easier.
			if 0 < ASSInspector.assi_setScript( inspector, styles, #styles, events, #events )
				log.warn( ASSInspector.assi_getErrorString( inspector ) )
				ASSInspector.assi_cleanup( inspector )
				aegisub.cancel( )

			-- We're going to render a line twice, once at its start and once
			-- at its end.
			renderCount = 2

			renderTimes = { }

			-- Cheat a bit when generating the times to render. This obviously
			-- will not scale with a renderCount > 2. Use 0-index arrays to
			-- match up with C indices in a later loop.
			if hasTimecodes
				-- Using the aegisub.frame_from_ms function pointers we copied
				-- earlier.
				startFrame = ffms( .start_time )
				endFrame   = ffms( .end_time ) - 1
				renderTimes[0] = math.floor( 0.5*( msff( startFrame ) + msff( startFrame + 1 ) ) )
				renderTimes[1] = math.floor( 0.5*( msff( endFrame ) + msff( endFrame + 1 ) ) )
			else
				renderTimes[0] = .start_time
				-- Trying to render exactly at end_time will result in a
				-- completely blank frame.
				renderTimes[1] = .end_time - 1

			-- Because we're rendering the line twice, we need two rects.
			rects = ffi.new( "ASSI_Rect[?]", renderCount )

			-- Allocate the array of times for rendering.
			times = ffi.new( "int32_t[?]", renderCount )

			-- Set up the render times. There's no real reason to do this in a
			-- loop if you know beforehand you're only going to render the
			-- line twice, but loops are more generic.
			for render = 0, renderCount - 1
				times[render] = renderTimes[render]

			-- Send the data in for rendering and bounds calculation.
			if 0 < ASSInspector.assi_calculateBounds( inspector, rects, times, renderCount )
				log.warn( ASSInspector.assi_getErrorString( inspector ) )
				aegisub.cancel( )

			-- Check out the calculated bounding rects
			for render = 0, renderCount - 1
				newEvent = dumpRect( event, rects[render] )
				-- log.dump newEvent
				subtitle.insert( eventIndex + render, newEvent )

			subtitle[eventIndex + renderCount] = event

aegisub.register_macro( script_name, script_description, mainFunction )

-- This example is unlicensed under CC0

export script_name        = "ASSInspector Example"
export script_description = "Calculates bounding rectangles on the start and end times of selected events."
export script_author      = "torque"
export script_version     = 0x000003

ffi  = require( 'ffi' )
util = require( 'aegisub.util' )
log  = require( 'a-mo.log' )

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

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, uint32_t, const char* );
void        assi_setFontDir( void*, const char* );
int         assi_setScript( void*, const char*, uint32_t, const char *, const uint32_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." )

-- Script-wide local variables are stored until the script is reloaded,
-- at which point they are garbage collected. We can abuse this system
-- to create a persistent ASSInspector state that only gets deallocated
-- when the automation script is reloaded or Aegisub is quit.

-- NOTE: this can be somewhat risky. For example, loading a new subtitle
-- file in Aegisub won't reset the automation scripts, and thus the old
-- header information will continue being used. It would be better to
-- make some uniqueness check on the loaded script. The full path to the
-- script would be an option, except that doesn't work for scripts that
-- have yet to be saved to disk. Appending some sort of UUID to the
-- script header and checking that each run would probably be more
-- foolproof. This implementation detail is left as an exercise to the
-- user.
inspector = nil
initializeInspector = ( resX, resY, minimalHeader ) ->
	if nil == inspector
		inspector = ffi.gc( ASSInspector.assi_init( resX, resY, minimalHeader, #minimalHeader, aegisub.decode_path( libraryPath .. "/fonts.conf" ) ), ASSInspector.assi_cleanup )
		if nil == inspector
			log.windowError( "ASSInspector library init failed." )

		-- Tell ASSInspector to tell fontconfig to search the fonts directory
		-- in the script directory for fonts to load.
		if '?script' != aegisub.decode_path( '?script' )
			ASSInspector.assi_setFontDir( inspector, aegisub.decode_path( '?script/fonts' ) )

dumpRect = ( event, rect ) ->
	bounds = {
		x:  tonumber( rect.x )
		y:  tonumber( rect.y )
		x2: tonumber( rect.w + rect.x )
		y2: tonumber( rect.h + rect.y )
	}

	newEvent = util.deep_copy( event )
	with bounds
		newEvent.text = ([[{\an7\pos(0,0)\bord0\shad0\c&H0000FF&\1a&H00&\p1}m %d %d l %d %d %d %d %d %d]])\format( .x, .y, .x2, .y, .x2, .y2, .x, .y2 )

	event.layer += 1
	return newEvent

mainFunction = ( subtitle, selectedLines, activeLine ) ->
	if ASSInspector.assi_getVersion( ) < script_version
		log.windowError( "Installed ASSInspector library is outdated. Please update it." )
		return selectedLines
	elseif ASSInspector.assi_getVersion( ) > script_version
		log.windowError( "Installed OptimASS is outdated. Please update it." )
		return selectedLines

	scriptHeader = {
		"[Script Info]"
	}

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

	styles = { }

	-- If there is no video (or, at minimum, timecodes) loaded, this will
	-- return nil.
	hasTimecodes = aegisub.frame_from_ms( 0 )

	-- Copy these functions into local variables to shorten them.
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	subtitleLen = #subtitle
	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, subtitleLen
		with line = subtitle[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. Not convinced this is a great idea because people may
	-- use smaller workraws or something like that. Well, hopefully they
	-- don't do that for typesetting.
	vidResX, vidResY = aegisub.video_size( )
	resX = vidResX or resX
	resY = vidResY or resY

	minimalHeader = table.concat( scriptHeader, '\n' )

	initializeInspector( resX, resY, minimalHeader )

	for eventIndex in *selectedLines
		with event = subtitle[eventIndex]
			-- This does not account for \r[name] being in the line.
			styles = (styles[.style] or "") .. "\n[Events]\n"
			events = .raw

			-- It occurs to me that perhaps having the user just pass in one
			-- string that contains both styles and events may be easier.
			if 0 < ASSInspector.assi_setScript( inspector, styles, #styles, events, #events )
				log.warn( assi_getErrorString( inspector ) )
				ASSInspector.assi_cleanup( inspector )
				aegisub.cancel( )

			-- We're going to render a line twice, once at its start and once
			-- at its end.
			renderCount = 2

			renderTimes = { }

			-- Cheat a bit when generating the times to render. This obviously
			-- will not scale with a renderCount > 2. Use 0-index arrays to
			-- match up with C indices in a later loop.
			if hasTimecodes
				-- Using the aegisub.frame_from_ms function pointers we copied
				-- earlier.
				startFrame = ffms( .start_time )
				endFrame   = ffms( .end_time ) - 1
				renderTimes[0] = math.floor( 0.5*( msff( startFrame ) + msff( startFrame + 1 ) ) )
				renderTimes[1] = math.floor( 0.5*( msff( endFrame ) + msff( endFrame + 1 ) ) )
			else
				renderTimes[0] = .start_time
				-- Trying to render exactly at end_time will result in a
				-- completely blank frame.
				renderTimes[1] = .end_time - 1

			-- Because we're rendering the line twice, we need two rects.
			rects = ffi.new( "ASSI_Rect[?]", renderCount )

			-- Allocate the array of times for rendering.
			times = ffi.new( "int32_t[?]", renderCount )

			-- Set up the render times. There's no real reason to do this in a
			-- loop if you know beforehand you're only going to render the
			-- line twice, but loops are more generic.
			for render = 0, renderCount - 1
				times[render] = renderTimes[render]

			-- Send the data in for rendering and bounds calculation.
			if 0 < ASSInspector.assi_calculateBounds( inspector, rects, times, renderCount )
				log.warn( inspector.error )
				aegisub.cancel( )

			-- Check out the calculated bounding rects
			for render = 0, renderCount - 1
				newEvent = dumpRect( event, rects[render] )
				-- log.dump newEvent
				subtitle.insert( eventIndex + render, newEvent )

			subtitle[eventIndex + renderCount] = event

aegisub.register_macro( script_name, script_description, mainFunction )


Fix typos in locale strings introduced by find replace

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

NAME, T = ...

with T.localization
    -- General
    .loaderror = 'LOAD ORDER ERROR: %s loaded before %s'
    .leftclick = 'Left Click'
    .enabled = 'enabled'
    .disabled = 'disabled'
    .name = 'name'
    .unknown = 'unknown'
    .invalid_command = 'Invalid command.'
    .debug_status = 'Debugging %s!'

    -- Broker
    .b_text_empty = "#{NAME}: No data"
    .b_announce_smart = 'Announce XP (Smart)'
    .b_header_xp = 'XP data for party'
    .b_header_empty = 'No data at the moment'
    .b_minimap_status = 'Minimap icon is now %s.'

    -- Chat messages
    .chat_xp_report = "[#{NAME}] Level %d, %s/%s EXP (%d%%)"

    -- Comm module
    .comm_message_length = 'comm_manager\\.send = (%s) message length > %d'
    .comm_prefix_registered = 'Registered addon message prefix: %s'

    -- Database
    .db_safety_notice = 'For safety reasons, db command is only operable in debug mode.'
    .db_usage = 'Usage: <key> [value]'
    .db_current = 'Value of %s is %s (default: %s)'
    .db_reset = 'Value of %s has been reset to %s'
    .db_invalid = 'Invalid value given, Lua error: %s'
    .db_set = 'Value of %s set to %s'
    .db_enqueue_loaded = 'Database.enqueue called when db loaded'
    .db_enqueue_invalid = 'Database.enqueue called with invalid method arg: %s'
    .db_get_load = 'get called before load, returning approximate value for %s'
    .db_get_default_load = 'get_default called before load, returning nil for %s'
    .db_set_load = 'set called before load, queueing set action on %s'
    .db_reset_load = 'reset called before load, queueing reset action on %s'

    -- Debug
    .debug_exported = "Private AddOn table exported to _G['#{NAME}']"

    -- Log
    .log_status = 'Logging is %s.'
    .log_status_changed = 'Logging %s.'
    .log_color_changed = '%s color changed to %s.'
    .log_coloring_changed = 'Log coloring %s.'
    .log_level_changed = 'Logging level changed to %s.'
    .log_current_level = 'Current log level is %s (%d)'
    .log_current_color = 'Current %s color is %s'
    .log_color_usage = 'Usage: enable/disable / name/<level> <color>'
    .log_invalid_prefix = 'Invalid prefix'

    -- Options panel
    .opt_minimap = 'Minimap icon'
    .opt_minimap_desc = "Shows the #{NAME} icon around your minimap"
    .opt_logging = 'Enable logging'
    .opt_logging_desc = "Note that turning off logging will disable ALL output from #{NAME}"
    .opt_debug = 'Enable debug mode'
    .opt_debug_desc = 'Enables AddOn debug mode, can produce a lot of spam'
    .opt_loglevel = 'Log level: %s'
    .opt_request = 'Request XP'
    .opt_request_desc = 'Requests an XP update from party members'
    .opt_send = 'Send XP'
    .opt_send_desc = 'Sends your XP data to party members'

    -- XP
    .xp_invalid_data = 'Invalid data received from %s'
    .xp_updated = 'XP updated for %s'
    .xp_sent_update = 'Sent XP update to party'
    .xp_sent_request = 'Sent XP request to party'
    .xp_removing = 'Removing %s from group, no longer present'
    .xp_received_update = 'Received XP update from %s:%s'
    .xp_received_request = 'Received XP request from %s:%s'

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

NAME, T = ...

with T.localization
    -- General
    .loaderror = 'LOAD ORDER ERROR: %s loaded before %s'
    .leftclick = 'Left Click'
    .enabled = 'enabled'
    .disabled = 'disabled'
    .name = 'name'
    .unknown = 'unknown'
    .invalid_command = 'Invalid command.'
    .debug_status = 'Debugging %s!'

    -- Broker
    .b_text_empty = "#{NAME}: No data"
    .b_announce_smart = 'Announce XP (Smart)'
    .b_header_xp = 'XP data for party'
    .b_header_empty = 'No data at the moment'
    .b_minimap_status = 'Minimap icon is now %s.'

    -- Chat messages
    .chat_xp_report = "[#{NAME}] Level %d, %s/%s EXP (%d%%)"

    -- Comm module
    .comm_message_length = 'comm_manager\\.send = (%s) message length > %d'
    .comm_prefix_registered = 'Registered addon message .prefix = %s'

    -- Database
    .db_safety_notice = 'For safety reasons, db command is only operable in debug mode.'
    .db_usage = 'U.sage = <key> [value]'
    .db_current = 'Value of %s is %s (.default = %s)'
    .db_reset = 'Value of %s has been reset to %s'
    .db_invalid = 'Invalid value given, Lua .error = %s'
    .db_set = 'Value of %s set to %s'
    .db_enqueue_loaded = 'Database.enqueue called when db loaded'
    .db_enqueue_invalid = 'Database.enqueue called with invalid method .arg = %s'
    .db_get_load = 'get called before load, returning approximate value for %s'
    .db_get_default_load = 'get_default called before load, returning nil for %s'
    .db_set_load = 'set called before load, queueing set action on %s'
    .db_reset_load = 'reset called before load, queueing reset action on %s'

    -- Debug
    .debug_exported = "Private AddOn table exported to _G['#{NAME}']"

    -- Log
    .log_status = 'Logging is %s.'
    .log_status_changed = 'Logging %s.'
    .log_color_changed = '%s color changed to %s.'
    .log_coloring_changed = 'Log coloring %s.'
    .log_level_changed = 'Logging level changed to %s.'
    .log_current_level = 'Current log level is %s (%d)'
    .log_current_color = 'Current %s color is %s'
    .log_color_usage = 'U.sage = enable/disable / name/<level> <color>'
    .log_invalid_prefix = 'Invalid prefix'

    -- Options panel
    .opt_minimap = 'Minimap icon'
    .opt_minimap_desc = "Shows the #{NAME} icon around your minimap"
    .opt_logging = 'Enable logging'
    .opt_logging_desc = "Note that turning off logging will disable ALL output from #{NAME}"
    .opt_debug = 'Enable debug mode'
    .opt_debug_desc = 'Enables AddOn debug mode, can produce a lot of spam'
    .opt_loglevel = 'Log .level = %s'
    .opt_request = 'Request XP'
    .opt_request_desc = 'Requests an XP update from party members'
    .opt_send = 'Send XP'
    .opt_send_desc = 'Sends your XP data to party members'

    -- XP
    .xp_invalid_data = 'Invalid data received from %s'
    .xp_updated = 'XP updated for %s'
    .xp_sent_update = 'Sent XP update to party'
    .xp_sent_request = 'Sent XP request to party'
    .xp_removing = 'Removing %s from group, no longer present'
    .xp_received_update = 'Received XP update from %.s =%s'
    .xp_received_request = 'Received XP request from %.s =%s'


Added support for uniforms to glurg.filters.ProgramFilter

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

Log = require "glurg.common.Log"
Promise = require "glurg.common.Promise"
Filter = require "glurg.filters.Filter"
Call = require "glurg.trace.Call"
Trace = require "glurg.trace.Trace"

class Shader
	new: (name, shader_type) =>
		@name = name
		@type = shader_type
		@source = ""
		@is_deleted = false

	_extract_source: (call) =>
		Promise.keep("@name == shader",
			@name == call\get_argument_by_name("shader")\query!)

		s = {}
		argument = call\get_argument_by_name("string")
		for i = 1, #argument
			table.insert(s, argument[i]\query!)
		@source = table.concat(s, "\n")

class Program
	new: (name) =>
		@name = name
		@attributes = {}
		@shaders = {}
		@uniforms = {}
		@is_deleted = false

	_set_attribute: (name, index) =>
		Promise.keep("name", Promise.IsString(name))
		Promise.keep("index", Promise.IsNumber(index))

		@attributes[name] = index
		@attributes[index] = name

	get_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key]
			else error "invalid key type, expected string or number"

	has_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key] != nil
			else error "invalid key type, expected string or number"

	_add_uniform: (name, location) =>
		if location == -1
			return false

		uniform = { call: false }
		if @uniforms[name] == nil
			Promise.keep("@uniforms[location] == nil",
				@uniforms[location] == nil)

			@uniforms[name] = uniform
			@uniforms[location] = uniform

		return true

	_set_uniform: (key, call, trace) =>
		Promise.keep_any("key", Promise.IsNumber(key), Promise.IsString(key))
		Promise.keep("call", Promise.IsClass(call, Call))
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("@uniforms[key] != nil", @uniforms[key] != nil)

		if @uniforms[key].call then
			trace\delete_call(@uniforms[key].call.index)
		@uniforms[key].call = call

	get_uniform: (key) =>
		Promise.keep_any("key", Promise.IsNumber(key), Promise.IsString(key))

		if @uniforms[key] and @uniforms[key].call then
			call = @uniforms[key].call
			switch call.signature.name
				when "glUniform1f", "glUniform1i", "glUniform1ui"
					v0 = call\get_argument_by_name("v0")\query!
					return v0, 'scalar'
				when "glUniform2f", "glUniform2i", "glUniform2ui"
					v0 = call\get_argument_by_name("v0")\query!
					v1 = call\get_argument_by_name("v1")\query!
					return { x: v0, y: v1 }, 'vec2'
				when "glUniform3f", "glUniform3i", "glUniform3ui"
					v0 = call\get_argument_by_name("v0")\query!
					v1 = call\get_argument_by_name("v1")\query!
					v2 = call\get_argument_by_name("v2")\query!
					return { x: v0, y: v1, z: v2 }, 'vec3'
				when "glUniform4f", "glUniform4i", "glUniform4ui"
					v0 = call\get_argument_by_name("v0")\query!
					v1 = call\get_argument_by_name("v1")\query!
					v2 = call\get_argument_by_name("v2")\query!
					v4 = call\get_argument_by_name("v4")\query!
					return { x: v0, y: v1, z: v2, w: v4 }, 'vec4'
				when "glUniform1fv", "glUniform1iv", "glUniform1uiv"
					count = call\get_argument_by_name("count")\query!
					value = call\get_argument_by_name("value")\query!
					if count == 1
						return value, 'scalar'
					else
						return value, 'scalar_array'
				when "glUniform2fv", "glUniform2iv", "glUniform2uiv"
					count = call\get_argument_by_name("count")\query!
					value = call\get_argument_by_name("value")\query!
					result = {}
					for i = 1, count
						result[i] = {
							x: value[i * 2 + 0],
							y: value[i * 2 + 1]
						}
					if count == 1
						return value[1], 'vec2'
					else
						return value, 'vec2_array'
				when "glUniform3fv", "glUniform3iv", "glUniform3uiv"
					count = call\get_argument_by_name("count")\query!
					value = call\get_argument_by_name("value")\query!
					result = {}
					for i = 1, count
						result[i] = {
							x: value[i * 3 + 0],
							y: value[i * 3 + 1],
							z: value[i * 3 + 2]
						}
					if count == 1
						return value[1], 'vec3'
					else
						return value, 'vec3_array'
				when "glUniform4fv", "glUniform4iv", "glUniform4uiv"
					count = call\get_argument_by_name("count")\query!
					value = call\get_argument_by_name("value")\query!
					result = {}
					for i = 1, count
						result[i] = {
							x: value[i * 4 + 0],
							y: value[i * 4 + 1],
							z: value[i * 4 + 2],
							w: value[i * 4 + 3]
						}
					if count == 1
						return value[1], 'vec4'
					else
						return value, 'vec4_array'
				when "glUniformMatrix2fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				when "glUniformMatrix3fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				when "glUniformMatrix4fv"
					count = call\get_argument_by_name("count")\query!
					transpose = call\get_argument_by_name("transpose")\query!
					value = call\get_argument_by_name("value")\query!
					result = {}
					for i = 1, count
						result[i] = {}
						if transpose.value_name == "GL_TRUE"
							for j = 1, 4 do
								result[i][j] = {}
							for j = 1, 4 do
								result[i][1][j] = value[i * 16 + j * 4 + 0]
								result[i][2][j] = value[i * 16 + j * 4 + 1]
								result[i][3][j] = value[i * 16 + j * 4 + 2]
								result[i][4][j] = value[i * 16 + j * 4 + 3]
						else
							for j = 1, 4 do
								result[i][j] = {
									value[i * 16 + j * 4 + 0],
									value[i * 16 + j * 4 + 1],
									value[i * 16 + j * 4 + 2],
									value[i * 16 + j * 4 + 3]
								}

					if count == 1
						return result[1], 'mat4'
					else
						return result, 'mat4_array'
				when "glUniformMatrix2x3fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				when "glUniformMatrix2x4fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				when "glUniformMatrix3x2fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				when "glUniformMatrix3x4fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				when "glUniformMatrix4x2fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				when "glUniformMatrix4x3fv"
					Log\warn('high', "cannot get uniform; type not yet implemented")
					return nil
				else
					Log\error("unknown glUniform call")
					return nil
		else
			return nil

	_attach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type] == nil
			@shaders[shader.type] = {}

		table.insert(@shaders[shader.type], shader)

	_detach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type]
			for i = 1, #@shaders[shader.type]
				if @shaders[shader.type][i] == shader then
					table.remove(@shaders[shader.type], i)
					break

	get_shader_source: (shader_type) =>
		source = [s for i, s in ipairs(@shaders[shader_type] or {})]
		return table.concat(source, "\n")

class ProgramFilter extends Filter
	new: =>
		@_shaders = {}
		@_programs = {}
		@current_program = 0

	_get_shader: (name) =>
		Promise.keep("@_shaders[name] != nil", @_shaders[name] != nil)

		return @_shaders[name]

	_add_shader: (shader) =>
		Promise.keep("@_shaders[shader.name] == nil",
			@_shaders[shader.name] == nil)

		@_shaders[shader.name] = shader

	_remove_shader: (shader) =>
		@_shaders[shader] = nil

	_get_program: (name) =>
		Promise.keep("@_programs[name] != nil", @_programs[name] != nil)

		return @_programs[name]

	_add_program: (program) =>
		Promise.keep("@_programs[program.name] == nil",
			@_programs[program.name] == nil)

		@_programs[program.name] = program

	_remove_program: (program) =>
		@_programs[program] = nil

	get_current_program: =>
		return @\_get_program(@current_program)

	glCreateShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader_type = call\get_argument_by_name("type")\query!.value_name
		name = call.result\query!

		@\_add_shader(Shader(name, shader_type))

		return false

	glShaderSource: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call\get_argument_by_name("shader")\query!
		@\_get_shader(name)\_extract_source(call)

		return false

	glCreateProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call.result\query!
		@\_add_program(Program(name))

		return false

	glUseProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		@current_program = call\get_argument_by_name("program")\query!

		return false

	glAttachShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_attach_shader(@\_get_shader(shader))

		return false

	glDetachShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_detach_shader(@\_get_shader(shader))

		return false

	glDeleteShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader = call\get_argument_by_name("shader")\query!
		@\_get_shader(shader).is_deleted = true
		@\_remove_shader(shader)

		return false

	glDeleteProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		@\_get_program(program).is_deleted = true
		@\_remove_program(program)

		return false

	glBindAttribLocation: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		name = call\get_argument_by_name("name")\query!
		index = call\get_argument_by_name("index")\query!

		@\_get_program(program)\_set_attribute(name, index)

		return false

	glGetUniformLocation: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		name = call\get_argument_by_name("name")\query!
		location = call.result\query!
		@\_get_program(program)\_add_uniform(name, location)

	_set_uniform: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		location = call\get_argument_by_name("location")\query!
		@\get_current_program!\_set_uniform(location, call, trace)

		return true

	glUniform1f: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform2f: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform3f: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform4f: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform1i: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform2i: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform3i: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform4i: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform1ui: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform2ui: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform3ui: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform4ui: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform1fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform2fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform3fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform4fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform1iv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform2iv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform3iv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform4iv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform1uiv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform2uiv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform3uiv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniform4uiv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix2fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix3fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix4fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix2x3fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix2x4fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix3x2fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix3x4fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix4x2fv: (trace, call) =>
		return @\_set_uniform(trace, call)

	glUniformMatrix4x3fv: (trace, call) =>
		return @\_set_uniform(trace, call)

return ProgramFilter

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

Promise = require "glurg.common.Promise"
Filter = require "glurg.filters.Filter"
Call = require "glurg.trace.Call"
Trace = require "glurg.trace.Trace"

class Shader
	new: (name, shader_type) =>
		@name = name
		@type = shader_type
		@source = ""
		@is_deleted = false

	_extract_source: (call) =>
		Promise.keep("@name == shader",
			@name == call\get_argument_by_name("shader")\query!)

		s = {}
		argument = call\get_argument_by_name("string")
		for i = 1, #argument
			table.insert(s, argument[i]\query!)
		@source = table.concat(s, "\n")

class Program
	new: (name) =>
		@name = name
		@attributes = {}
		@shaders = {}
		@is_deleted = false

	_set_attribute: (name, index) =>
		Promise.keep("name", Promise.IsString(name))
		Promise.keep("index", Promise.IsNumber(index))

		@attributes[name] = index
		@attributes[index] = name

	get_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key]
			else error "invalid key type, expected string or number"

	has_attribute: (key) =>
		return switch type(key)
			when 'number', 'string' then @attributes[key] != nil
			else error "invalid key type, expected string or number"

	_attach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type] == nil
			@shaders[shader.type] = {}

		table.insert(@shaders[shader.type], shader)

	_detach_shader: (shader) =>
		Promise.keep("shader", Promise.IsClass(shader, Shader))

		if @shaders[shader.type]
			for i = 1, #@shaders[shader.type]
				if @shaders[shader.type][i] == shader then
					table.remove(@shaders[shader.type], i)
					break

	get_shader_source: (shader_type) =>
		source = [s for i, s in ipairs(@shaders[shader_type] or {})]
		return table.concat(source, "\n")

class ProgramFilter extends Filter
	new: =>
		@_shaders = {}
		@_programs = {}
		@current_program = 0

	_get_shader: (name) =>
		Promise.keep("@_shaders[name] != nil", @_shaders[name] != nil)

		return @_shaders[name]

	_add_shader: (shader) =>
		Promise.keep("@_shaders[shader.name] == nil",
			@_shaders[shader.name] == nil)

		@_shaders[shader.name] = shader

	_remove_shader: (shader) =>
		@_shaders[shader] = nil

	_get_program: (name) =>
		Promise.keep("@_programs[name] != nil", @_programs[name] != nil)

		return @_programs[name]

	_add_program: (program) =>
		Promise.keep("@_programs[program.name] == nil",
			@_programs[program.name] == nil)

		@_programs[program.name] = program

	_remove_program: (program) =>
		@_programs[program] = nil

	get_current_program: =>
		return @\_get_program(@current_program)

	glCreateShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader_type = call\get_argument_by_name("type")\query!.value_name
		name = call.result\query!

		@\_add_shader(Shader(name, shader_type))

		return false

	glShaderSource: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call\get_argument_by_name("shader")\query!
		@\_get_shader(name)\_extract_source(call)

		return false

	glCreateProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		name = call.result\query!
		@\_add_program(Program(name))

		return false

	glUseProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		@current_program = call\get_argument_by_name("program")\query!

		return false

	glAttachShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_attach_shader(@\_get_shader(shader))

		return false

	glDetachShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		shader = call\get_argument_by_name("shader")\query!

		@\_get_program(program)\_detach_shader(@\_get_shader(shader))

		return false

	glDeleteShader: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		shader = call\get_argument_by_name("shader")\query!
		@\_get_shader(shader).is_deleted = true
		@\_remove_shader(shader)

		return false

	glDeleteProgram: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		@\_get_program(program).is_deleted = true
		@\_remove_program(program)

		return false

	glBindAttribLocation: (trace, call) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))
		Promise.keep("call", Promise.IsClass(call, Call))

		program = call\get_argument_by_name("program")\query!
		name = call\get_argument_by_name("name")\query!
		index = call\get_argument_by_name("index")\query!

		@\_get_program(program)\_set_attribute(name, index)

		return false

return ProgramFilter


Fix paranthese issue in comm manager

Fix paranthese issue in comm manager

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

NAME, T = ...

DELIMITER = '\t'
DELIMITER_MATCH = '[^' .. DELIMITER .. ']+'

MAX_MESSAGE_LENGTH = 250

local comm, log, misc

{
    :SendAddonMessage
    :wipe
    :UnitName
    :RegisterAddonMessagePrefix
} = _G

{localization: L} = T

T.comm_manager = do
    class CommManager extends T.CallbackManager
        new: =>
            super!
            @prefix = NAME\upper!

        handle: (message, channel, sender) =>
            return unless message
            message = message\trim!
            return if message == ''
            di = message\find DELIMITER
            kind = di and message\sub(1, di - 1) or message
            kind = kind\lower!

            return unless @handlers[kind]

            args = {}
            if di
                arg = message\sub di + 1
                for token in arg\gmatch DELIMITER_MATCH
                    args[#args + 1] = token

            @fire kind, channel, sender, unpack args

        send: (channel, target, kind, ...) =>
            if channel != 'WHISPER' and channel != 'CHANNEL' and target
                return @send channel, nil, target, kind, ...

            message = kind
            for v in *{...}
                message ..= DELIMITER .. v

            if message\len! > MAX_MESSAGE_LENGTH
                log\warn L.comm_message_length, kind, MAX_MESSAGE_LENGTH

            SendAddonMessage @prefix, message, channel, target

        register_callback: (kind, handler) =>
            kind = kind\lower!
            super kind, handler

        unregister_callback: (kind, handler) =>
            kind = kind\lower!
            super kind, handler

        unregister_callbacks: (kind) =>
            kind = kind\lower!
            super kind

        unregister_all_callbacks: (kind) =>
            kind = kind\lower!
            super kind

    CommManager!

{comm_manager: comm, :log, :misc} = T

T.CHAT_MSG_ADDON = (event, prefix, message, channel, sender) ->
    return if prefix != comm.prefix or channel != 'PARTY'
    fixed_sender = misc.fix_name sender
    player = misc.fix_name UnitName 'player'
    return if fixed_sender == player
    comm\handle message, channel, sender

T.SHAREXP_LOADED = ->
    RegisterAddonMessagePrefix comm.prefix
    log\debug L.comm_prefix_registered, comm.prefix

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

NAME, T = ...

DELIMITER = '\t'
DELIMITER_MATCH = '[^' .. DELIMITER .. ']+'

MAX_MESSAGE_LENGTH = 250

local comm, log, misc

{
    :SendAddonMessage
    :wipe
    :UnitName
    :RegisterAddonMessagePrefix
} = _G

{localization: L} = T

T.comm_manager = do
    class CommManager extends T.CallbackManager
        new: =>
            super!
            @prefix = NAME\upper!

        handle: (message, channel, sender) =>
            return unless message
            message = message\trim!
            return if message == ''
            di = message\find DELIMITER
            kind = di and message\sub 1, di - 1 or message
            kind = kind\lower!

            return unless @handlers[kind]

            args = {}
            if di
                arg = message\sub di + 1
                for token in arg\gmatch DELIMITER_MATCH
                    args[#args + 1] = token

            @fire kind, channel, sender, unpack args

        send: (channel, target, kind, ...) =>
            if channel != 'WHISPER' and channel != 'CHANNEL' and target
                return @send channel, nil, target, kind, ...

            message = kind
            for v in *{...}
                message ..= DELIMITER .. v

            if message\len! > MAX_MESSAGE_LENGTH
                log\warn L.comm_message_length, kind, MAX_MESSAGE_LENGTH

            SendAddonMessage @prefix, message, channel, target

        register_callback: (kind, handler) =>
            kind = kind\lower!
            super kind, handler

        unregister_callback: (kind, handler) =>
            kind = kind\lower!
            super kind, handler

        unregister_callbacks: (kind) =>
            kind = kind\lower!
            super kind

        unregister_all_callbacks: (kind) =>
            kind = kind\lower!
            super kind

    CommManager!

{comm_manager: comm, :log, :misc} = T

T.CHAT_MSG_ADDON = (event, prefix, message, channel, sender) ->
    return if prefix != comm.prefix or channel != 'PARTY'
    fixed_sender = misc.fix_name sender
    player = misc.fix_name UnitName 'player'
    return if fixed_sender == player
    comm\handle message, channel, sender

T.SHAREXP_LOADED = ->
    RegisterAddonMessagePrefix comm.prefix
    log\debug L.comm_prefix_registered, comm.prefix


Fix printing

Fix printing

import java, Font, Image, Quad from sauce3

Constants = require "sauce3/wrappers"
Color     = java.require "com.badlogic.gdx.graphics.Color"
Gdx       = java.require "com.badlogic.gdx.Gdx"
GL20      = java.require "com.badlogic.gdx.graphics.GL20"
Matrix4   = java.require "com.badlogic.gdx.math.Matrix4"
Sauce3VM   = java.require "sauce3.Sauce3VM"

OrthographicCamera = java.require "com.badlogic.gdx.graphics.OrthographicCamera"
ShapeRender        = java.require "com.badlogic.gdx.graphics.glutils.ShapeRenderer"
SpriteBatch        = java.require "com.badlogic.gdx.graphics.g2d.SpriteBatch"

shader = SpriteBatch\createDefaultShader!

batch  = java.new SpriteBatch, 1000, shader
shapes = java.new ShapeRender

font = Font!

shapes\setAutoShapeType true

matrix = java.new Matrix4
color  = java.new Color, 1, 1, 1, 1

background = java.new Color, 0, 0, 0, 1
blending   = "alpha"

batch\setColor color
shapes\setColor color
font.font\setColor color

camera = java.new OrthographicCamera
camera\setToOrtho true

batch\setProjectionMatrix camera.combined
shapes\setProjectionMatrix camera.combined

matrix_dirty = false

check = (texture_based) ->
  if texture_based
    if matrix_dirty
      batch\setTransformMatrix matrix
      matrix_dirty = false

    if shapes\isDrawing!
      Sauce3VM.util\endShapes shapes
    unless batch\isDrawing!
      batch\begin!
  else
    if matrix_dirty
      shapes\setTransformMatrix matrix
      matrix_dirty = false

    if batch\isDrawing!
      Sauce3VM.util\endBatch batch
    unless shapes\isDrawing!
      shapes\begin!

arc = (mode, x, y, radius, a1, a2) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\arc x, y, radius, (math.deg a1), math.deg a2

circle = (mode, x, y, radius) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\circle x, y, radius

clear = ->
  Gdx.gl\glClearColor background.r, background.g, background.b
  Gdx.gl\glClear GL20.GL_COLOR_BUFFER_BIT

draw = (image, x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  w = image\getWidth!
  h = image\getHeight!

  src_x = 0
  src_y = 0

  src_w = w
  src_h = h

  x -= ox
  y -= oy

  batch\draw image.texture, x, y, ox, oy, w, h, sx, sy, r, srx_x, srx_y, src_w, src_h, false, true

draw_q = (image, quad, x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  w = quad.width
  h = quad.height

  src_x = quad.x
  src_y = quad.y

  src_w = quad.sw
  src_h = quad.sh

  x -= ox
  y -= oy

  batch\draw image.texture, x, y, ox, oy, w, h, sx, sy, r, src_x, src_y, src_w, src_h, false, true

ellipse = (mode, x, y, width, height) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\ellipse x, y, width, height

line = (x1, y1, x2, y2) ->
  check false
  shapes\line x1, y1, x2, y2

origin = ->
  matrix\idt!
  matrix_dirty = true

point = (x, y) ->
  check false
  shapes\point x, y, 0

polygon = (mode, ...) ->
  check false
  shapes\set Constants.shapes[mode]

  args = table.pack ...

  if type args[1] == "table"
    shapes\polygon args[1]
  else
    shapes\polygon args

present = ->
  if shapes\isDrawing!
    Sauce3VM.util\endShapes shapes
  if batch\isDrawing!
    Sauce3VM.util\endBatch batch

print = (text, x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  local tmp

  unless r == 0
    tmp = batch\getTransformMatrix!
    translate x, y
    rotate r
    translate -x, -y

    batch\setTransformMatrix matrix
    matrix_dirty = false

  font.font\getData!\setScale sx, -sy
  font.font\draw batch, text, x - ox * sx, y - oy * sy

  unless r == 0
    translate x, y
    rotate -r
    translate -x, -y

    batch\setTransformMatrix tmp
    matrix_dirty = false

print_f = (text, width = 0, align = "left", x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  tmp = nil

  unless r == 0
    tmp = batch\getTransformMatrix!
    translate x, y
    rotate r
    translate -x, -y

    batch\setTransformMatrix matrix
    matrix_dirty = false

  font.font\getData!\setScale sx, -sy
  font.font\draw batch, text, x - ox * sx, y - oy * sy, width, Constants.aligns[align], true

  unless r == 0
    translate x, y
    rotate -r
    translate -x, -y

    batch\setTransformMatrix tmp
    matrix_dirty = false

rectangle = (mode, x, y, width, height) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\rect x, y, width, height

reset = ->
  shader = SpriteBatch\createDefaultShader!
  batch\setShader shader

  background\set 0.4, 0.3, 0.4, 1
  color\set 1, 1, 1, 1

  batch\setColor color
  shapes\setColor color
  font.font\setColor color

  matrix\idt!
  matrix_dirty = true

translate = (x, y) ->
  matrix\translate x, y
  matrix_dirty = true

rotate = (radians) ->
  matrix\rotate 0, 0, 1, math.deg radians
  matrix_dirty = true

scale = (sx, sy) ->
  matrix\scale sx, sy, 1
  matrix_dirty = true

get_background_color = ->
  background.r * 255, background.g * 255, background.b * 255

get_blend_mode = ->
  blending

get_color = ->
  color.r * 255, color.g * 255, color.b * 255, color.a * 255

get_font = ->
  font

set_background_color = (r, g, b, a = 255) ->
  background\set r / 255, g / 255, b / 255, a / 255

set_blend_mode = (mode) ->
  blending = mode

  blend_mode = Constants[blending]
  batch\setBlendFunction blend_mode[1], blend_mode[2]

set_font = (v) ->
  font = v

set_new_font = (path, size, f_type) ->
  font = new_font path, size, f_type

new_font = (path, size, f_type) ->
  (Font path, size, f_type)

new_image = (path, format, f_type) ->
  (Image path, format, f_type)

new_quad = (x, y, width, height, sx, sy) ->
  (Quad x, y, width, height, sx, sy)

{
  :arc
  :circle
  :clear
  :draw
  :draw_q
  :ellipse
  :get_background_color
  :get_blendMode
  :get_color
  :get_font
  :line
  :origin
  :point
  :polygon
  :present
  :print
  :print_f
  :rectangle
  :reset
  :translate
  :rotate
  :scale
  :set_background_color
  :set_blend_mode
  :set_color
  :set_font
  :set_new_font
  :new_font
  :new_image
  :new_quad
}

import java, Font, Image, Quad from sauce3

Constants = require "sauce3/wrappers"
Color     = java.require "com.badlogic.gdx.graphics.Color"
Gdx       = java.require "com.badlogic.gdx.Gdx"
GL20      = java.require "com.badlogic.gdx.graphics.GL20"
Matrix4   = java.require "com.badlogic.gdx.math.Matrix4"
Sauce3VM   = java.require "sauce3.Sauce3VM"

OrthographicCamera = java.require "com.badlogic.gdx.graphics.OrthographicCamera"
ShapeRender        = java.require "com.badlogic.gdx.graphics.glutils.ShapeRenderer"
SpriteBatch        = java.require "com.badlogic.gdx.graphics.g2d.SpriteBatch"

shader = SpriteBatch\createDefaultShader!

batch  = java.new SpriteBatch, 1000, shader
shapes = java.new ShapeRender

font = Font!

shapes\setAutoShapeType true

matrix = java.new Matrix4
color  = java.new Color, 1, 1, 1, 1

background = java.new Color, 0, 0, 0, 1
blending   = "alpha"

batch\setColor color
shapes\setColor color
font.font\setColor color

camera = java.new OrthographicCamera
camera\setToOrtho true

batch\setProjectionMatrix camera.combined
shapes\setProjectionMatrix camera.combined

matrix_dirty = false

check = (texture_based) ->
  if texture_based
    if matrix_dirty
      batch\setTransformMatrix matrix
      matrix_dirty = false

    if shapes\isDrawing!
      Sauce3VM.util\endShapes shapes
    unless batch\isDrawing!
      batch\begin!
  else
    if matrix_dirty
      shapes\setTransformMatrix matrix
      matrix_dirty = false

    if batch\isDrawing!
      Sauce3VM.util\endBatch batch
    unless shapes\isDrawing!
      shapes\begin!

arc = (mode, x, y, radius, a1, a2) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\arc x, y, radius, (math.deg a1), math.deg a2

circle = (mode, x, y, radius) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\circle x, y, radius

clear = ->
  Gdx.gl\glClearColor background.r, background.g, background.b
  Gdx.gl\glClear GL20.GL_COLOR_BUFFER_BIT

draw = (image, x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  w = image\getWidth!
  h = image\getHeight!

  src_x = 0
  src_y = 0

  src_w = w
  src_h = h

  x -= ox
  y -= oy

  batch\draw image.texture, x, y, ox, oy, w, h, sx, sy, r, srx_x, srx_y, src_w, src_h, false, true

draw_q = (image, quad, x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  w = quad.width
  h = quad.height

  src_x = quad.x
  src_y = quad.y

  src_w = quad.sw
  src_h = quad.sh

  x -= ox
  y -= oy

  batch\draw image.texture, x, y, ox, oy, w, h, sx, sy, r, src_x, src_y, src_w, src_h, false, true

ellipse = (mode, x, y, width, height) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\ellipse x, y, width, height

line = (x1, y1, x2, y2) ->
  check false
  shapes\line x1, y1, x2, y2

origin = ->
  matrix\idt!
  matrix_dirty = true

point = (x, y) ->
  check false
  shapes\point x, y, 0

polygon = (mode, ...) ->
  check false
  shapes\set Constants.shapes[mode]

  args = table.pack ...

  if type args[1] == "table"
    shapes\polygon args[1]
  else
    shapes\polygon args

present = ->
  if shapes\isDrawing!
    Sauce3VM.util\endShapes shapes
  if batch\isDrawing!
    Sauce3VM.util\endBatch batch

print = (text, x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  local tmp

  unless r == 0
    tmp = batch\getTransformMatrix!
    translate x, y
    rotate r
    translate -x, -y

    batch\setTransformMatrix matrix
    matrix_dirty = false

  font.font\getData!\setScale sx, sy
  font.font\draw batch, text, x - ox * sx, y - oy * sy

  unless r == 0
    translate x, y
    rotate -r
    translate -x, -y

    batch\setTransformMatrix tmp
    matrix_dirty = false

prin_f = (text, width = 0, align = "left", x = 0, y = 0, r = 0, sx = 1, sy = 1, ox = 0, oy = 0) ->
  check true

  tmp = nil

  unless r == 0
    tmp = batch\getTransformMatrix!
    translate x, y
    rotate r
    translate -x, -y

    batch\setTransformMatrix matrix
    matrix_dirty = false

  font.font\getData!\setScale sx, -sy
  font.font\draw batch, text, x - ox * sx, y - oy * sy, width, Constants.aligns[align], true

  unless r == 0
    translate x, y
    rotate -r
    translate -x, -y

    batch\setTransformMatrix tmp
    matrix_dirty = false

rectangle = (mode, x, y, width, height) ->
  check false

  shapes\set Constants.shapes[mode]
  shapes\rect x, y, width, height

reset = ->
  shader = SpriteBatch\createDefaultShader!
  batch\setShader shader

  background\set 0.4, 0.3, 0.4, 1
  color\set 1, 1, 1, 1

  batch\setColor color
  shapes\setColor color
  font.font\setColor color

  matrix\idt!
  matrix_dirty = true

translate = (x, y) ->
  matrix\translate x, y
  matrix_dirty = true

rotate = (radians) ->
  matrix\rotate 0, 0, 1, math.deg radians
  matrix_dirty = true

scale = (sx, sy) ->
  matrix\scale sx, sy, 1
  matrix_dirty = true

get_background_color = ->
  background.r * 255, background.g * 255, background.b * 255

get_blend_mode = ->
  blending

get_color = ->
  color.r * 255, color.g * 255, color.b * 255, color.a * 255

get_font = ->
  font

set_background_color = (r, g, b, a = 255) ->
  background\set r / 255, g / 255, b / 255, a / 255

set_blend_mode = (mode) ->
  blending = mode

  blend_mode = Constants[blending]
  batch\setBlendFunction blend_mode[1], blend_mode[2]

set_font = (v) ->
  font = v

set_new_font = (path, size, f_type) ->
  font = new_font path, size, f_type

new_font = (path, size, f_type) ->
  (Font path, size, f_type)

new_image = (path, format, f_type) ->
  (Image path, format, f_type)

new_quad = (x, y, width, height, sx, sy) ->
  (Quad x, y, width, height, sx, sy)

{
  :arc
  :circle
  :clear
  :draw
  :draw_q
  :ellipse
  :get_background_color
  :get_blendMode
  :get_color
  :get_font
  :line
  :origin
  :point
  :polygon
  :present
  :print
  :print_f
  :rectangle
  :reset
  :translate
  :rotate
  :scale
  :set_background_color
  :set_blend_mode
  :set_color
  :set_font
  :set_new_font
  :new_font
  :new_image
  :new_quad
}


Update example to match new API.

Update example to match new API.

-- This example is unlicensed under CC0

export script_name        = "ASSInspector Example"
export script_description = "Calculates bounding rectangles on the start and end times of selected events."
export script_author      = "torque"
export script_version     = 0x000003

ffi  = require( 'ffi' )
util = require( 'aegisub.util' )
log  = require( 'a-mo.log' )

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* );
int         assi_setHeader( void*, const char* );
int         assi_setScript( void*, const char* );
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." )

-- Script-wide local variables are stored until the script is reloaded,
-- at which point they are garbage collected. We can abuse this system
-- to create a persistent ASSInspector state that only gets deallocated
-- when the automation script is reloaded or Aegisub is quit.

-- NOTE: this can be somewhat risky. For example, loading a new subtitle
-- file in Aegisub won't reset the automation scripts, and thus the old
-- header information will continue being used. It would be better to
-- make some uniqueness check on the loaded script. The full path to the
-- script would be an option, except that doesn't work for scripts that
-- have yet to be saved to disk. Appending some sort of UUID to the
-- script header and checking that each run would probably be more
-- foolproof. This implementation detail is left as an exercise to the
-- user.
inspector = nil
initializeInspector = ( resX, resY, minimalHeader ) ->
	if nil == inspector
		if '?script' != aegisub.decode_path( '?script' )
			-- Tell ASSInspector to tell fontconfig to search the fonts
			-- directory in the script directory for fonts to load.
			inspector = ffi.gc( ASSInspector.assi_init( resX, resY, aegisub.decode_path( libraryPath .. "/fonts.conf" ), aegisub.decode_path( '?script/fonts' ) ), ASSInspector.assi_cleanup )
		else
			inspector = ffi.gc( ASSInspector.assi_init( resX, resY, aegisub.decode_path( libraryPath .. "/fonts.conf" ), nil ), ASSInspector.assi_cleanup )
		if nil == inspector
			log.windowError( "ASSInspector library init failed." )

		if 0 < ASSInspector.assi_setHeader( inspector, minimalHeader )
			log.warn "Failed to set header."
			log.warn( ffi.string( ASSInspector.assi_getErrorString( inspector ) ) )

dumpRect = ( event, rect ) ->
	bounds = {
		x:  tonumber( rect.x )
		y:  tonumber( rect.y )
		x2: tonumber( rect.w + rect.x )
		y2: tonumber( rect.h + rect.y )
	}

	log.dump { x: bounds.x, y: bounds.y, w: bounds.x2-bounds.x, h: bounds.y2-bounds.y }

	newEvent = util.deep_copy( event )
	with bounds
		newEvent.text = ([[{\an7\pos(0,0)\bord0\shad0\c&H0000FF&\1a&H00&\p1}m %d %d l %d %d %d %d %d %d]])\format( .x, .y, .x2, .y, .x2, .y2, .x, .y2 )

	event.layer += 1
	return newEvent

mainFunction = ( subtitle, selectedLines, activeLine ) ->
	if ASSInspector.assi_getVersion( ) < script_version
		log.windowError( "Installed ASSInspector library is outdated. Please update it." )
		return selectedLines
	elseif ASSInspector.assi_getVersion( ) > script_version
		log.windowError( "Installed OptimASS is outdated. Please update it." )
		return selectedLines

	scriptHeader = {
		"[Script Info]"
	}

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

	styles = { }

	-- If there is no video (or, at minimum, timecodes) loaded, this will
	-- return nil.
	hasTimecodes = aegisub.frame_from_ms( 0 )

	-- Copy these functions into local variables to shorten them.
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	subtitleLen = #subtitle
	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, subtitleLen
		with line = subtitle[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. Not convinced this is a great idea because people may
	-- use smaller workraws or something like that. Well, hopefully they
	-- don't do that for typesetting.
	vidResX, vidResY = aegisub.video_size( )
	resX = vidResX or resX
	resY = vidResY or resY

	minimalHeader = table.concat( scriptHeader, '\n' )

	initializeInspector( resX, resY, minimalHeader )

	insertIndices = { }
	insertEvents  = { }

	for eventIndex in *selectedLines
		with event = subtitle[eventIndex]
			-- This does not account for \r[name] being in the line.
			scriptText = (styles[.style] or "") .. "\n[Events]\n" .. .raw

			-- It occurs to me that perhaps having the user just pass in one
			-- string that contains both styles and events may be easier.
			if 0 < ASSInspector.assi_setScript( inspector, scriptText )
				log.warn( ffi.string( ASSInspector.assi_getErrorString( inspector ) ) )
				ASSInspector.assi_cleanup( inspector )
				aegisub.cancel( )

			-- We're going to render a line twice, once at its start and once
			-- at its end.
			renderCount = 1

			renderTimes = { }

			-- Cheat a bit when generating the times to render. This obviously
			-- will not scale with a renderCount > 2. Use 0-index arrays to
			-- match up with C indices in a later loop.
			if hasTimecodes
				-- Using the aegisub.frame_from_ms function pointers we copied
				-- earlier.
				startFrame = ffms( .start_time )
				endFrame   = ffms( .end_time ) - 1
				renderTimes[0] = math.floor( 0.5*( msff( startFrame ) + msff( startFrame + 1 ) ) )
				renderTimes[1] = math.floor( 0.5*( msff( endFrame ) + msff( endFrame + 1 ) ) )
			else
				renderTimes[0] = .start_time
				-- Trying to render exactly at end_time will result in a
				-- completely blank frame.
				renderTimes[1] = .end_time - 1

			-- Because we're rendering the line twice, we need two rects.
			rects = ffi.new( "ASSI_Rect[?]", renderCount )

			-- Allocate the array of times for rendering.
			times = ffi.new( "int32_t[?]", renderCount )

			-- Set up the render times. There's no real reason to do this in a
			-- loop if you know beforehand you're only going to render the
			-- line twice, but loops are more generic.
			for render = 0, renderCount - 1
				times[render] = renderTimes[render]

			-- Send the data in for rendering and bounds calculation.
			if 0 < ASSInspector.assi_calculateBounds( inspector, rects, times, renderCount )
				log.warn( ASSInspector.assi_getErrorString( inspector ) )
				aegisub.cancel( )

			-- Check out the calculated bounding rects
			for render = 0, renderCount - 1
				if rects[render].w > 0 and rects[render].h > 0
					newEvent = dumpRect( event, rects[render] )
					table.insert( insertIndices, eventIndex )
					table.insert( insertEvents, newEvent)

			subtitle[eventIndex] = event

	for index = #insertIndices, 1, -1
		subtitle.insert( insertIndices[index], insertEvents[index] )

aegisub.register_macro( script_name, script_description, mainFunction )

-- This example is unlicensed under CC0

export script_name        = "ASSInspector Example"
export script_description = "Calculates bounding rectangles on the start and end times of selected events."
export script_author      = "torque"
export script_version     = 0x000003

ffi  = require( 'ffi' )
util = require( 'aegisub.util' )
log  = require( 'a-mo.log' )

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*, uint32_t, const char*, const char* );
int         assi_setScript( void*, const char*, uint32_t, const char *, const uint32_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." )

-- Script-wide local variables are stored until the script is reloaded,
-- at which point they are garbage collected. We can abuse this system
-- to create a persistent ASSInspector state that only gets deallocated
-- when the automation script is reloaded or Aegisub is quit.

-- NOTE: this can be somewhat risky. For example, loading a new subtitle
-- file in Aegisub won't reset the automation scripts, and thus the old
-- header information will continue being used. It would be better to
-- make some uniqueness check on the loaded script. The full path to the
-- script would be an option, except that doesn't work for scripts that
-- have yet to be saved to disk. Appending some sort of UUID to the
-- script header and checking that each run would probably be more
-- foolproof. This implementation detail is left as an exercise to the
-- user.
inspector = nil
initializeInspector = ( resX, resY, minimalHeader ) ->
	if nil == inspector
		if '?script' != aegisub.decode_path( '?script' )
			-- Tell ASSInspector to tell fontconfig to search the fonts
			-- directory in the script directory for fonts to load.
			inspector = ffi.gc( ASSInspector.assi_init( resX, resY, minimalHeader, #minimalHeader, aegisub.decode_path( libraryPath .. "/fonts.conf" ), aegisub.decode_path( '?script/fonts' ) ), ASSInspector.assi_cleanup )
		else
			inspector = ffi.gc( ASSInspector.assi_init( resX, resY, minimalHeader, #minimalHeader, aegisub.decode_path( libraryPath .. "/fonts.conf" ), nil ), ASSInspector.assi_cleanup )
		if nil == inspector
			log.windowError( "ASSInspector library init failed." )

dumpRect = ( event, rect ) ->
	bounds = {
		x:  tonumber( rect.x )
		y:  tonumber( rect.y )
		x2: tonumber( rect.w + rect.x )
		y2: tonumber( rect.h + rect.y )
	}

	newEvent = util.deep_copy( event )
	with bounds
		newEvent.text = ([[{\an7\pos(0,0)\bord0\shad0\c&H0000FF&\1a&H00&\p1}m %d %d l %d %d %d %d %d %d]])\format( .x, .y, .x2, .y, .x2, .y2, .x, .y2 )

	event.layer += 1
	return newEvent

mainFunction = ( subtitle, selectedLines, activeLine ) ->
	if ASSInspector.assi_getVersion( ) < script_version
		log.windowError( "Installed ASSInspector library is outdated. Please update it." )
		return selectedLines
	elseif ASSInspector.assi_getVersion( ) > script_version
		log.windowError( "Installed OptimASS is outdated. Please update it." )
		return selectedLines

	scriptHeader = {
		"[Script Info]"
	}

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

	styles = { }

	-- If there is no video (or, at minimum, timecodes) loaded, this will
	-- return nil.
	hasTimecodes = aegisub.frame_from_ms( 0 )

	-- Copy these functions into local variables to shorten them.
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	subtitleLen = #subtitle
	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, subtitleLen
		with line = subtitle[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. Not convinced this is a great idea because people may
	-- use smaller workraws or something like that. Well, hopefully they
	-- don't do that for typesetting.
	vidResX, vidResY = aegisub.video_size( )
	resX = vidResX or resX
	resY = vidResY or resY

	minimalHeader = table.concat( scriptHeader, '\n' )

	initializeInspector( resX, resY, minimalHeader )

	insertIndices = { }
	insertEvents  = { }

	for eventIndex in *selectedLines
		with event = subtitle[eventIndex]
			-- This does not account for \r[name] being in the line.
			styleText = (styles[.style] or "") .. "\n[Events]\n"
			eventText = .raw

			-- It occurs to me that perhaps having the user just pass in one
			-- string that contains both styles and events may be easier.
			if 0 < ASSInspector.assi_setScript( inspector, styleText, #styleText, eventText, #eventText )
				log.warn( ASSInspector.assi_getErrorString( inspector ) )
				ASSInspector.assi_cleanup( inspector )
				aegisub.cancel( )

			-- We're going to render a line twice, once at its start and once
			-- at its end.
			renderCount = 2

			renderTimes = { }

			-- Cheat a bit when generating the times to render. This obviously
			-- will not scale with a renderCount > 2. Use 0-index arrays to
			-- match up with C indices in a later loop.
			if hasTimecodes
				-- Using the aegisub.frame_from_ms function pointers we copied
				-- earlier.
				startFrame = ffms( .start_time )
				endFrame   = ffms( .end_time ) - 1
				renderTimes[0] = math.floor( 0.5*( msff( startFrame ) + msff( startFrame + 1 ) ) )
				renderTimes[1] = math.floor( 0.5*( msff( endFrame ) + msff( endFrame + 1 ) ) )
			else
				renderTimes[0] = .start_time
				-- Trying to render exactly at end_time will result in a
				-- completely blank frame.
				renderTimes[1] = .end_time - 1

			-- Because we're rendering the line twice, we need two rects.
			rects = ffi.new( "ASSI_Rect[?]", renderCount )

			-- Allocate the array of times for rendering.
			times = ffi.new( "int32_t[?]", renderCount )

			-- Set up the render times. There's no real reason to do this in a
			-- loop if you know beforehand you're only going to render the
			-- line twice, but loops are more generic.
			for render = 0, renderCount - 1
				times[render] = renderTimes[render]

			-- Send the data in for rendering and bounds calculation.
			if 0 < ASSInspector.assi_calculateBounds( inspector, rects, times, renderCount )
				log.warn( ASSInspector.assi_getErrorString( inspector ) )
				aegisub.cancel( )

			-- Check out the calculated bounding rects
			for render = 0, renderCount - 1
				if rects[render].w > 0 and rects[render].h > 0
					newEvent = dumpRect( event, rects[render] )
					table.insert( insertIndices, eventIndex )
					table.insert( insertEvents, newEvent)

			subtitle[eventIndex] = event

	for index = #insertIndices, 1, -1
		subtitle.insert( insertIndices[index], insertEvents[index] )

aegisub.register_macro( script_name, script_description, mainFunction )


quiz-results: show dates in MSK

quiz-results: show dates in MSK

import Widget from require "lapis.html"

Helpers = require('views.helpers')

quizs = require('quiz.all')
model = require('model')
msk_time = require('msk_time')

class QuizResults extends Widget
  @include Helpers

  content: =>
    h2 @_("Quiz ") .. @quiz.name
    h2 @_("Student ") .. @quiz.user
    if @prep
      url = @url_for("prep-quizs-of", {user: @quiz.user})
      a href: url, ->
        text @_("Other quizes by ") .. @quiz.user
    h2 style: 'color: ' .. @quiz\color!, ->
      text @quiz.right_answers .. ' / ' .. @quiz.tasks
    -- push
    @push_form @url_for("push-quiz", {id: @quiz.id})
    created_at = msk_time(@quiz.created_at)
    updated_at = msk_time(@quiz.updated_at)
    h2 created_at .. ' - ' .. updated_at
    if quizs[@quiz.name] and model.Quiz.can_create(@quiz.name)
      element 'table', ->
        element 'tr', ->
          element 'td', ->
            text @_('Go through this test again:')
          element 'td', ->
            @new_test_button @quiz.name
    raw '
'
    for task in *(@quiz\all_tasks!)
      @task_table task
      raw '
'

import Widget from require "lapis.html"

Helpers = require('views.helpers')

quizs = require('quiz.all')
model = require('model')

class QuizResults extends Widget
  @include Helpers

  content: =>
    h2 @_("Quiz ") .. @quiz.name
    h2 @_("Student ") .. @quiz.user
    if @prep
      url = @url_for("prep-quizs-of", {user: @quiz.user})
      a href: url, ->
        text @_("Other quizes by ") .. @quiz.user
    h2 style: 'color: ' .. @quiz\color!, ->
      text @quiz.right_answers .. ' / ' .. @quiz.tasks
    -- push
    @push_form @url_for("push-quiz", {id: @quiz.id})
    h2 @quiz.created_at .. ' - ' .. @quiz.updated_at
    if quizs[@quiz.name] and model.Quiz.can_create(@quiz.name)
      element 'table', ->
        element 'tr', ->
          element 'td', ->
            text @_('Go through this test again:')
          element 'td', ->
            @new_test_button @quiz.name
    raw '
'
    for task in *(@quiz\all_tasks!)
      @task_table task
      raw '
'


fuck priorities

fuck priorities

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

class BonesSequence extends DLib.SequenceBase
	new: (controller, data) =>
		super(controller, data)

		{
			'bones': @bonesNames
			'numid': @numid
		} = data

		@modifierID = controller\GetModifierID(@name .. '_sequence')
		@bonesFuncsPos = ['SetModifier' .. boneName .. 'Position' for boneName in *@bonesNames]
		@bonesFuncsScale = ['SetModifier' .. boneName .. 'Scale' for boneName in *@bonesNames]
		@bonesFuncsAngles = ['SetModifier' .. boneName .. 'Angles' for boneName in *@bonesNames]
		@ent = controller.ent
		@controller = controller
		@Launch()

	GetController: => @controller
	GetEntity: => @ent

	Think: (delta = 0) =>
		@ent = @controller.ent
		return false if not IsValid(@ent)
		super(delta)

	Stop: =>
		super()
		@controller\ResetModifiers(@name .. '_sequence')

	SetBonePosition: (id = 1, val = Vector(0, 0, 0)) => @controller[@bonesFuncsPos[id]] and @controller[@bonesFuncsPos[id]](@controller, @modifierID, val)
	SetBoneScale: (id = 1, val = 0) => @controller[@bonesFuncsScale[id]] and @controller[@bonesFuncsScale[id]](@controller, @modifierID, val)
	SetBoneAngles: (id = 1, val = Angle(0, 0, 0)) => @controller[@bonesFuncsAngles[id]] and @controller[@bonesFuncsAngles[id]](@controller, @modifierID, val)

PPM2.BonesSequence = BonesSequence

-- 0	LrigPelvis
-- 1	Lrig_LEG_BL_Femur
-- 2	Lrig_LEG_BL_Tibia
-- 3	Lrig_LEG_BL_LargeCannon
-- 4	Lrig_LEG_BL_PhalanxPrima
-- 5	Lrig_LEG_BL_RearHoof
-- 6	Lrig_LEG_BR_Femur
-- 7	Lrig_LEG_BR_Tibia
-- 8	Lrig_LEG_BR_LargeCannon
-- 9	Lrig_LEG_BR_PhalanxPrima
-- 10	Lrig_LEG_BR_RearHoof
-- 11	LrigSpine1
-- 12	LrigSpine2
-- 13	LrigRibcage
-- 14	Lrig_LEG_FL_Scapula
-- 15	Lrig_LEG_FL_Humerus
-- 16	Lrig_LEG_FL_Radius
-- 17	Lrig_LEG_FL_Metacarpus
-- 18	Lrig_LEG_FL_PhalangesManus
-- 19	Lrig_LEG_FL_FrontHoof
-- 20	Lrig_LEG_FR_Scapula
-- 21	Lrig_LEG_FR_Humerus
-- 22	Lrig_LEG_FR_Radius
-- 23	Lrig_LEG_FR_Metacarpus
-- 24	Lrig_LEG_FR_PhalangesManus
-- 25	Lrig_LEG_FR_FrontHoof
-- 26	LrigNeck1
-- 27	LrigNeck2
-- 28	LrigNeck3
-- 29	LrigScull
-- 30	Jaw
-- 31	Ear_L
-- 32	Ear_R
-- 33	Mane02
-- 34	Mane03
-- 35	Mane03_tip
-- 36	Mane04
-- 37	Mane05
-- 38	Mane06
-- 39	Mane07
-- 40	Mane01
-- 41	Lrigweaponbone
-- 42	Tail01
-- 43	Tail02
-- 44	Tail03

RESET_BONE_POS = Vector(0, 0, 0)
RESET_BONE_ANGLES = Angle(0, 0, 0)
RESET_BONE_SCALE = Vector(1, 1, 1)
resetBones = (ent) ->
	for i = 0, ent\GetBoneCount() - 1
		ent\ManipulateBonePosition(i, RESET_BONE_POS)
		ent\ManipulateBoneScale(i, RESET_BONE_SCALE)
		ent\ManipulateBoneAngles(i, RESET_BONE_ANGLES)

for ent in *ents.GetAll()
	ent.__ppmBonesModifiers = nil

class PPM2.EntityBonesModifier extends DLib.SequenceHolder
	@OBJECTS = {}
	@resetBones = resetBones

	@SEQUENCES = {
		{
			'name': 'floppy_ears'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'Ear_L', 'Ear_R'}
			'reset': =>
				@SetBoneAngles(1, Angle(0, -84, -40))
				@SetBoneAngles(2, Angle(0, 84, -40))
			'func': (delta, timeOfAnim) =>
		}

		{
			'name': 'floppy_ears_weak'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'Ear_L', 'Ear_R'}
			'reset': =>
				@SetBoneAngles(1, Angle(0, -20, -20))
				@SetBoneAngles(2, Angle(0, 20, -20))
			'func': (delta, timeOfAnim) =>
		}

		{
			'name': 'forward_ears'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'Ear_L', 'Ear_R'}
			'reset': =>
				@SetBoneAngles(1, Angle(0, -15, -27))
				@SetBoneAngles(2, Angle(0, 15, -27))
			'func': (delta, timeOfAnim) =>
		}

		{
			'name': 'neck_flopping_backward'
			'autostart': false
			'repeat': false
			'time': 3
			'bones': {'LrigNeck3'}
			'reset': =>
			'func': (delta, timeOfAnim) =>
				@SetBoneAngles(1, Angle(0, -12 * timeOfAnim, math.sin(RealTime() * 4) * 20))
		}

		{
			'name': 'neck_backward'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'reset': => @SetBoneAngles(1, Angle(0, -12, 0))
		}

		{
			'name': 'neck_twitch'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'func': (delta, timeOfAnim) =>
				@SetBoneAngles(1, Angle(0, math.cos(RealTime() * 4) * 20, 0))
		}

		{
			'name': 'neck_twitch_fast'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'func': (delta, timeOfAnim) =>
				@SetBoneAngles(1, Angle(0, math.cos(RealTime() * 8) * 20, 0))
		}

		{
			'name': 'neck_left'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'reset': => @SetBoneAngles(1, Angle(14, 0, 12))
		}

		{
			'name': 'neck_right'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'reset': => @SetBoneAngles(1, Angle(-14, 0, -12))
		}

		{
			'name': 'forward_left'
			'autostart': false
			'repeat': false
			'time': 3
			'bones': {'LrigNeck3'}
			'reset': =>
				@SetBoneAngles(1, Angle(10, 12, -9))
		}

		{
			'name': 'forward_right'
			'autostart': false
			'repeat': false
			'time': 3
			'bones': {'LrigNeck3'}
			'reset': =>
				@SetBoneAngles(1, Angle(-10, 12, 9))
		}
	}

	@SequenceObject = BonesSequence

	PreDrawOpaqueRenderables = (a, b) ->
		--return if a or b
		frame = FrameNumber()
		rtime = RealTime()
		for obj in *@OBJECTS
			if not obj\IsValid()
				oldObjects = @OBJECTS
				@OBJECTS = {}
				for obj2 in *oldObjects
					if obj2\IsValid()
						table.insert(@OBJECTS, obj2)
					else
						lent = obj2.ent
						if IsValid(lent)
							lent.__ppmBonesModifiers = nil
				return

			if obj.callFrame ~= frame and (not obj.pac3Last or obj.pac3Last < rtime) and not obj.ent\IsDormant() and not obj.ent\GetNoDraw()
				resetBones(obj.ent)
				data = obj.ent\GetPonyData()
				hook.Call('PPM2.SetupBones', nil, StrongEntity(obj.ent), data) if data
				obj\Think()

	hook.Add 'PreDrawOpaqueRenderables', 'PPM2.EntityBonesModifier', PreDrawOpaqueRenderables, -2

	new: (ent = NULL) =>
		super()
		@ent = ent
		@bonesMappingID = {}
		@bonesMappingName = {}
		@bonesMapping = {}
		@bonesMappingForName = {}
		@bonesMappingForID = {}
		@bonesIterable = {}
		@boneCount = 0
		@isValid = false
		table.insert(@@OBJECTS, @)
		@lastCall = RealTime()
		@Setup() if IsValid(ent)

	Setup: (ent = @ent) =>
		return false if not IsValid(ent)
		@lastModel = ent\GetModel()
		@ClearModifiers()
		@isValid = true
		@ent = ent
		@bonesMappingID = {}
		@bonesMappingName = {}
		@boneCount = ent\GetBoneCount()
		@bonesIterable = for i = 0, @boneCount - 1
			name = ent\GetBoneName(i)
			@bonesMappingID[i] = name
			@bonesMapping[i] = name
			@bonesMappingName[name] = i
			@bonesMapping[name] = i
			@bonesMappingForName[name] = name
			@bonesMappingForName[i] = name
			@bonesMappingForID[name] = i
			@bonesMappingForID[i] = i
			@RegisterModifier(name .. 'Position', (-> Vector(0, 0, 0)), (-> Vector(0, 0, 0)))
			@RegisterModifier(name .. 'Scale', (-> Vector(0, 0, 0)), (-> Vector(0, 0, 0)))
			@RegisterModifier(name .. 'Angles', (-> Angle(0, 0, 0)), (-> Angle(0, 0, 0)))
			@SetupLerpTables(name .. 'Position')
			@SetupLerpTables(name .. 'Scale')
			@SetupLerpTables(name .. 'Angles')
			@SetLerpFunc(name .. 'Position', LerpVector)
			@SetLerpFunc(name .. 'Scale', LerpVector)
			@SetLerpFunc(name .. 'Angles', LerpAngle)
			--@RegisterModifier(name .. 'Jiggle', 0)
			{i, name, 'Calculate' .. name .. 'Position', 'Calculate' .. name .. 'Scale', 'Calculate' .. name .. 'Angles'}

	Think: (force = false) =>
		return if not super() or not force and @callFrame == FrameNumber()
		@callFrame = FrameNumber()
		for data in *@bonesIterable
			id = data[1]
			name = data[2]
			calc = data[3]
			calcScale = data[4]
			calcAngles = data[5]
			with @ent
				\ManipulateBonePosition(id, \GetManipulateBonePosition(id) + @[calc](@))
				\ManipulateBoneScale(id, \GetManipulateBoneScale(id) + @[calcScale](@))
				\ManipulateBoneAngles(id, \GetManipulateBoneAngles(id) + @[calcAngles](@))

	ResetBones: =>
		return if @pac3Last and @pac3Last > RealTime()
		resetBones(@ent)

	IsValid: => @isValid and @ent\IsValid()
	GetEntity: => @ent

with FindMetaTable('Entity')
	.PPMBonesModifier = =>
		with t = .GetTable(@)
			return if not t
			return .__ppmBonesModifiers if IsValid(.__ppmBonesModifiers)
			.__ppmBonesModifiers = PPM2.EntityBonesModifier(@)
			.__ppmBonesModifiers.ent = @
			.__ppmBonesModifiers\Setup(@) if .__ppmBonesModifiers.lastModel ~= @GetModel()
			return .__ppmBonesModifiers

hook.Add 'PAC3ResetBones', 'PPM2.EntityBonesModifier', =>
	data = @GetPonyData()
	hook.Call('PPM2.SetupBones', nil, StrongEntity(@), data) if data
	if @__ppmBonesModifiers
		@__ppmBonesModifiers\Think()
		@__ppmBonesModifiers.pac3Last = RealTime() + 0.2

ent.__ppmBonesModifiers = nil for ent in *ents.GetAll()

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

class BonesSequence extends DLib.SequenceBase
	new: (controller, data) =>
		super(controller, data)

		{
			'bones': @bonesNames
			'numid': @numid
		} = data

		@modifierID = controller\GetModifierID(@name .. '_sequence')
		@bonesFuncsPos = ['SetModifier' .. boneName .. 'Position' for boneName in *@bonesNames]
		@bonesFuncsScale = ['SetModifier' .. boneName .. 'Scale' for boneName in *@bonesNames]
		@bonesFuncsAngles = ['SetModifier' .. boneName .. 'Angles' for boneName in *@bonesNames]
		@ent = controller.ent
		@controller = controller
		@Launch()

	GetController: => @controller
	GetEntity: => @ent

	Think: (delta = 0) =>
		@ent = @controller.ent
		return false if not IsValid(@ent)
		super(delta)

	Stop: =>
		super()
		@controller\ResetModifiers(@name .. '_sequence')

	SetBonePosition: (id = 1, val = Vector(0, 0, 0)) => @controller[@bonesFuncsPos[id]] and @controller[@bonesFuncsPos[id]](@controller, @modifierID, val)
	SetBoneScale: (id = 1, val = 0) => @controller[@bonesFuncsScale[id]] and @controller[@bonesFuncsScale[id]](@controller, @modifierID, val)
	SetBoneAngles: (id = 1, val = Angle(0, 0, 0)) => @controller[@bonesFuncsAngles[id]] and @controller[@bonesFuncsAngles[id]](@controller, @modifierID, val)

PPM2.BonesSequence = BonesSequence

-- 0	LrigPelvis
-- 1	Lrig_LEG_BL_Femur
-- 2	Lrig_LEG_BL_Tibia
-- 3	Lrig_LEG_BL_LargeCannon
-- 4	Lrig_LEG_BL_PhalanxPrima
-- 5	Lrig_LEG_BL_RearHoof
-- 6	Lrig_LEG_BR_Femur
-- 7	Lrig_LEG_BR_Tibia
-- 8	Lrig_LEG_BR_LargeCannon
-- 9	Lrig_LEG_BR_PhalanxPrima
-- 10	Lrig_LEG_BR_RearHoof
-- 11	LrigSpine1
-- 12	LrigSpine2
-- 13	LrigRibcage
-- 14	Lrig_LEG_FL_Scapula
-- 15	Lrig_LEG_FL_Humerus
-- 16	Lrig_LEG_FL_Radius
-- 17	Lrig_LEG_FL_Metacarpus
-- 18	Lrig_LEG_FL_PhalangesManus
-- 19	Lrig_LEG_FL_FrontHoof
-- 20	Lrig_LEG_FR_Scapula
-- 21	Lrig_LEG_FR_Humerus
-- 22	Lrig_LEG_FR_Radius
-- 23	Lrig_LEG_FR_Metacarpus
-- 24	Lrig_LEG_FR_PhalangesManus
-- 25	Lrig_LEG_FR_FrontHoof
-- 26	LrigNeck1
-- 27	LrigNeck2
-- 28	LrigNeck3
-- 29	LrigScull
-- 30	Jaw
-- 31	Ear_L
-- 32	Ear_R
-- 33	Mane02
-- 34	Mane03
-- 35	Mane03_tip
-- 36	Mane04
-- 37	Mane05
-- 38	Mane06
-- 39	Mane07
-- 40	Mane01
-- 41	Lrigweaponbone
-- 42	Tail01
-- 43	Tail02
-- 44	Tail03

RESET_BONE_POS = Vector(0, 0, 0)
RESET_BONE_ANGLES = Angle(0, 0, 0)
RESET_BONE_SCALE = Vector(1, 1, 1)
resetBones = (ent) ->
	for i = 0, ent\GetBoneCount() - 1
		ent\ManipulateBonePosition(i, RESET_BONE_POS)
		ent\ManipulateBoneScale(i, RESET_BONE_SCALE)
		ent\ManipulateBoneAngles(i, RESET_BONE_ANGLES)

class PPM2.EntityBonesModifier extends DLib.SequenceHolder
	@OBJECTS = {}
	@resetBones = resetBones

	@SEQUENCES = {
		{
			'name': 'floppy_ears'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'Ear_L', 'Ear_R'}
			'reset': =>
				@SetBoneAngles(1, Angle(0, -84, -40))
				@SetBoneAngles(2, Angle(0, 84, -40))
			'func': (delta, timeOfAnim) =>
		}

		{
			'name': 'floppy_ears_weak'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'Ear_L', 'Ear_R'}
			'reset': =>
				@SetBoneAngles(1, Angle(0, -20, -20))
				@SetBoneAngles(2, Angle(0, 20, -20))
			'func': (delta, timeOfAnim) =>
		}

		{
			'name': 'forward_ears'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'Ear_L', 'Ear_R'}
			'reset': =>
				@SetBoneAngles(1, Angle(0, -15, -27))
				@SetBoneAngles(2, Angle(0, 15, -27))
			'func': (delta, timeOfAnim) =>
		}

		{
			'name': 'neck_flopping_backward'
			'autostart': false
			'repeat': false
			'time': 3
			'bones': {'LrigNeck3'}
			'reset': =>
			'func': (delta, timeOfAnim) =>
				@SetBoneAngles(1, Angle(0, -12 * timeOfAnim, math.sin(RealTime() * 4) * 20))
		}

		{
			'name': 'neck_backward'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'reset': => @SetBoneAngles(1, Angle(0, -12, 0))
		}

		{
			'name': 'neck_twitch'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'func': (delta, timeOfAnim) =>
				@SetBoneAngles(1, Angle(0, math.cos(RealTime() * 4) * 20, 0))
		}

		{
			'name': 'neck_twitch_fast'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'func': (delta, timeOfAnim) =>
				@SetBoneAngles(1, Angle(0, math.cos(RealTime() * 8) * 20, 0))
		}

		{
			'name': 'neck_left'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'reset': => @SetBoneAngles(1, Angle(14, 0, 12))
		}

		{
			'name': 'neck_right'
			'autostart': false
			'repeat': false
			'time': 5
			'bones': {'LrigNeck3'}
			'reset': => @SetBoneAngles(1, Angle(-14, 0, -12))
		}

		{
			'name': 'forward_left'
			'autostart': false
			'repeat': false
			'time': 3
			'bones': {'LrigNeck3'}
			'reset': =>
				@SetBoneAngles(1, Angle(10, 12, -9))
		}

		{
			'name': 'forward_right'
			'autostart': false
			'repeat': false
			'time': 3
			'bones': {'LrigNeck3'}
			'reset': =>
				@SetBoneAngles(1, Angle(-10, 12, 9))
		}
	}

	@SequenceObject = BonesSequence

	PreDrawOpaqueRenderables = (a, b) ->
		return if a or b
		frame = FrameNumber()
		rtime = RealTime()
		for obj in *@OBJECTS
			if not obj\IsValid()
				oldObjects = @OBJECTS
				@OBJECTS = {}
				for obj2 in *oldObjects
					if obj2\IsValid()
						table.insert(@OBJECTS, obj2)
					else
						lent = obj2.ent
						if IsValid(lent)
							lent.__ppmBonesModifiers = nil
				return

			if obj.callFrame ~= frame and (not obj.pac3Last or obj.pac3Last < rtime) and not obj.ent\IsDormant() and not obj.ent\GetNoDraw()
				resetBones(obj.ent)
				data = obj.ent\GetPonyData()
				hook.Call('PPM2.SetupBones', nil, StrongEntity(obj.ent), data) if data
				obj\Think()

	hook.Add 'PreDrawOpaqueRenderables', 'PPM2.EntityBonesModifier', PreDrawOpaqueRenderables, -2

	new: (ent = NULL) =>
		super()
		@ent = ent
		@bonesMappingID = {}
		@bonesMappingName = {}
		@bonesMapping = {}
		@bonesMappingForName = {}
		@bonesMappingForID = {}
		@bonesIterable = {}
		@boneCount = 0
		@isValid = false
		table.insert(@@OBJECTS, @)
		@lastCall = RealTime()
		@Setup() if IsValid(ent)

	Setup: (ent = @ent) =>
		return false if not IsValid(ent)
		@lastModel = ent\GetModel()
		@ClearModifiers()
		@isValid = true
		@ent = ent
		@bonesMappingID = {}
		@bonesMappingName = {}
		@boneCount = ent\GetBoneCount()
		@bonesIterable = for i = 0, @boneCount - 1
			name = ent\GetBoneName(i)
			@bonesMappingID[i] = name
			@bonesMapping[i] = name
			@bonesMappingName[name] = i
			@bonesMapping[name] = i
			@bonesMappingForName[name] = name
			@bonesMappingForName[i] = name
			@bonesMappingForID[name] = i
			@bonesMappingForID[i] = i
			@RegisterModifier(name .. 'Position', (-> Vector(0, 0, 0)), (-> Vector(0, 0, 0)))
			@RegisterModifier(name .. 'Scale', (-> Vector(0, 0, 0)), (-> Vector(0, 0, 0)))
			@RegisterModifier(name .. 'Angles', (-> Angle(0, 0, 0)), (-> Angle(0, 0, 0)))
			@SetupLerpTables(name .. 'Position')
			@SetupLerpTables(name .. 'Scale')
			@SetupLerpTables(name .. 'Angles')
			@SetLerpFunc(name .. 'Position', LerpVector)
			@SetLerpFunc(name .. 'Scale', LerpVector)
			@SetLerpFunc(name .. 'Angles', LerpAngle)
			--@RegisterModifier(name .. 'Jiggle', 0)
			{i, name, 'Calculate' .. name .. 'Position', 'Calculate' .. name .. 'Scale', 'Calculate' .. name .. 'Angles'}

	Think: (force = false) =>
		return if not super() or not force and @callFrame == FrameNumber()
		@callFrame = FrameNumber()
		for data in *@bonesIterable
			id = data[1]
			name = data[2]
			calc = data[3]
			calcScale = data[4]
			calcAngles = data[5]
			with @ent
				\ManipulateBonePosition(id, \GetManipulateBonePosition(id) + @[calc](@))
				\ManipulateBoneScale(id, \GetManipulateBoneScale(id) + @[calcScale](@))
				\ManipulateBoneAngles(id, \GetManipulateBoneAngles(id) + @[calcAngles](@))

	ResetBones: =>
		return if @pac3Last and @pac3Last > RealTime()
		resetBones(@ent)

	IsValid: => @isValid and @ent\IsValid()
	GetEntity: => @ent

with FindMetaTable('Entity')
	.PPMBonesModifier = =>
		return @__ppmBonesModifiers if IsValid(@__ppmBonesModifiers)
		@__ppmBonesModifiers = PPM2.EntityBonesModifier(@)
		@__ppmBonesModifiers.ent = @
		@__ppmBonesModifiers\Setup(@) if @__ppmBonesModifiers.lastModel ~= @GetModel()
		return @__ppmBonesModifiers

hook.Add 'PAC3ResetBones', 'PPM2.EntityBonesModifier', =>
	data = @GetPonyData()
	hook.Call('PPM2.SetupBones', nil, StrongEntity(@), data) if data
	if @__ppmBonesModifiers
		@__ppmBonesModifiers\Think()
		@__ppmBonesModifiers.pac3Last = RealTime() + 0.2

ent.__ppmBonesModifiers = nil for ent in *ents.GetAll()