trigger: fix pattern, add list, fix deletion, add show

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      name UNIQUE ON CONFLICT REPLACE,
      pattern UNIQUE ON CONFLICT REPLACE,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
   @Msg 'privmsg', destination, source, 'Usage: !trigger add <name>|<lua pattern>|<lua code>. Ex: !trigger add help|^!help (%w+)|say "%s, you need help. Delete: !trigger del <name>"'

-- Construct a safe environ for trigger with two functions; say and reply
--- Preferrably you should be able to cross-call modules here to make aliasing possible
sandbox = (func) ->
  (source, destination, ...) =>
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Register the command
regCommand = (source, destination, name, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (name, pattern, funcstr) VALUES(?, ?, ?)"
  code = ins\bind_values name, pattern, funcstr
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand 'trigger', pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, name) =>
  db = openDb!
  -- Find pattern which is used for deletion of handler
  stmt = db\prepare "SELECT pattern FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  code = stmt\step!
  if code == sqlite3.DONE
    -- Invalid name
    db\close!
    @Msg 'privmsg', destination, source, 'Invalid name'
    return
  pattern = stmt\get_values()[1]

  -- Delete trigger from db
  ins = db\prepare "DELETE FROM trigger WHERE name = ?"
  code = ins\bind_values name
  code = ins\step!
  code = ins\finalize!
  db\close!

  @UnregisterCommand 'triggers', pattern
  @Msg 'privmsg', destination, source, "Trigger #{pattern} deleted"

listTriggers = (source, destination) =>
  db = openDb!
  stmt = db\prepare "SELECT name FROM trigger"
  out = {}
  for row in stmt\nrows! do
    table.insert out, row.name

  db\close!

  if #out > 0
    @Msg 'privmsg', destination, source, "Triggers: "..table.concat(out, ', ')

showTrigger = (source, destination, name) =>
  db = openDb!
  stmt = db\prepare "SELECT * FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  for row in stmt\nrows! do
    @Msg 'privmsg', destination, source, "Trigger: \002#{row.name}\002, pattern: #{row.pattern}, code: #{row.funcstr}"
  db\close!

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr FROM trigger'
  ivar2\RegisterCommand 'triggers', row.pattern, triggerHandler(ivar2, nil, nil, row.funcstr)
db\close!

PRIVMSG:
  '^%ptrigger$': triggerHelp
  '^%ptrigger add (.+)|(.+)|(.+)$': regCommand
  '^%ptrigger del (.+)$': delCommand
  '^%ptrigger list$': listTriggers
  '^%ptrigger show (.+)$': showTrigger

sql = require'lsqlite3'

openDb = ->
  db = sql.open 'cache/trigger.sql'

  db\exec [[
    CREATE TABLE IF NOT EXISTS trigger (
      name UNIQUE ON CONFLICT REPLACE,
      pattern UNIQUE ON CONFLICT REPLACE,
      funcstr
    );
    ]]
  return db

triggerHelp  = (source, destination, argument) =>
   @Msg 'privmsg', destination, source, 'Usage: !trigger add <name>|<lua pattern>|<lua code>. Ex: !trigger add help|^!help (%w+)|say "%s, you need help"'

-- Construct a safe environ for trigger with two functions; say and reply
--- Preferrably you should be able to cross-call modules here to make aliasing possible
sandbox = (func) ->
  (source, destination, ...) =>
    -- Store args so we can use them in env
    arg = {...}
    env =
      say: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      reply: (str) ->
        @Msg 'privmsg', destination, source, source.nick..': '..str, unpack(arg),
      print: (str) ->
        @Msg 'privmsg', destination, source, str, unpack(arg),
      simplehttp:require'simplehttp'
      json:require'json'
      :string
      :type
      :tostring
      :tonumber
      :ipars
      :pairs
      :table
      :next

    --setfenv func, setmetatable(env, {__index: _G })
    setfenv func, env
    success, err = pcall func, arg
    if err
      @Msg 'privmsg', destination, source, err
    else
      success

-- Construct a safe function to run and return a handler
triggerHandler = (source, destination, funcstr) =>
  func, err = loadstring funcstr
  unless func
    @Msg 'privmsg', destination, source, 'Trigger error: %s', err
  else
    sandbox(func)

-- Register the command
regCommand = (source, destination, name, pattern, funcstr) =>
  db = openDb!
  ins = db\prepare "INSERT INTO trigger (name, pattern, funcstr) VALUES(?, ?, ?)"
  code = ins\bind_values name, pattern, funcstr
  code = ins\step!
  code = ins\finalize!
  db\close!

  @RegisterCommand 'trigger', pattern, triggerHandler(@, source, destination, funcstr)

delCommand = (source, destination, name) =>
  db = openDb!
  -- Find pattern which is used for deletion of handler
  stmt = db\prepare "SELECT pattern FROM trigger WHERE name = ?"
  code = stmt\bind_values name
  code = stmt\step!
  if code == sqlite3.DONE
    -- Invalid name
    db\close!
    return
  pattern = stmt\get_values()[1]

  -- Delete trigger from db
  ins = db\prepare "DELETE FROM trigger WHERE name = ?"
  code = ins\bind_values pattern
  code = ins\step!
  code = ins\finalize!
  db\close!

  @UnregisterCommand 'triggers', pattern

-- Register commands on startup
db = openDb!
for row in db\nrows 'SELECT pattern, funcstr FROM trigger'
  ivar2\RegisterCommand 'triggers', row.pattern, triggerHandler(ivar2, nil, nil, row.funcstr)
db\close!

PRIVMSG:
  '%ptrigger$': triggerHelp
  '%ptrigger add (.+)|(.+)|(.+)$': regCommand
  '%ptrigger del (.+)$': delCommand


Fixed up fetching uniforms.

Fixed up fetching uniforms.

- Also fixed _extract_source in Shader object.

-- 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")\query!
		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, :name, :location }
		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_uniforms: =>
		return [item for key, item in pairs(@uniforms) when type(key) == "number"]

	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[1], '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]\query!,
							y: value[i * 2 + 1]\query!
						}
					if count == 1
						return result[1], 'vec2'
					else
						return result, '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 - 1) * 3 + 1]\query!,
							y: value[(i - 1) * 3 + 2]\query!,
							z: value[(i - 1) * 3 + 3]\query!
						}
					if count == 1
						return result[1], 'vec3'
					else
						return result, '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 - 1) * 4 + 1]\query!,
							y: value[(i - 1) * 4 + 2]\query!,
							z: value[(i - 1) * 4 + 3]\query!,
							w: value[(i - 1) * 4 + 4]\query!
						}
					if count == 1
						return result[1], 'vec4'
					else
						return result, '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 - 1) * 16 + (j - 1) * 4 + 1]\query!
								result[i][2][j] = value[(i - 1) * 16 + (j - 1) * 4 + 2]\query!
								result[i][3][j] = value[(i - 1) * 16 + (j - 1) * 4 + 3]\query!
								result[i][4][j] = value[(i - 1) * 16 + (j - 1) * 4 + 4]\query!
						else
							for j = 1, 4 do
								result[i][j] = {
									value[(i - 1) * 16 + (j - 1) * 4 + 1]\query!,
									value[(i - 1) * 16 + (j - 1) * 4 + 2]\query!,
									value[(i - 1) * 16 + (j - 1) * 4 + 3]\query!,
									value[(i - 1) * 16 + (j - 1) * 4 + 4]\query!
								}

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

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


views/all-tests: use quiz.groups

views/all-tests: use quiz.groups

Group and order quizes

import Widget from require "lapis.html"

quizs = require('quiz.groups')
kr = require('quiz.kr')

model = require('model')
msk_time = require "msk_time"

Helpers = require('views.helpers')

class AllTests extends Widget
  @include Helpers

  content: =>
    div class: "body", ->
      if @prep
        a href: @url_for("prep-quizs"), ->
          text @_("finished quizes")
        text ' | '
        a href: @url_for("prep-quizs-today"), ->
          text @_("today quizes")
        text ' | '
        a href: @url_for("quiz-state"), ->
          text @_("enable/disable quiz")
        for i, name in ipairs kr
          text ' | '
          a href: @url_for("prep-kr", {name: name}), ->
            text @_("kr") .. i
      p @_("Start new quiz:")
      for _, group in ipairs(quizs)
        group_name = group[1]
        group_quizs = group[2]
        element 'table', -> tr ->
          td ->
            text group_name
          for _, quiz in ipairs(group_quizs)
            quiz_name = quiz[1]
            if model.Quiz.can_create(quiz_name) or @prep
              td ->
                @new_test_button quiz_name
      my_quizs = (t, state) ->
        qq = model.my_quizs(@, state)
        if #qq > 0
          p t
        element 'ul', ->
          for quiz in *qq
            element 'li', ->
              time = msk_time(quiz.created_at)
              raw quiz\anchor(@) .. ', ' .. time
              if quiz.state == model.FINISHED
                r = quiz.right_answers
                a = quiz.tasks
                text " | "
                b style: 'color: ' .. quiz\color!, ->
                  text "#{r} / #{a}"
      my_quizs @_("Your active quizes:"), model.ACTIVE
      my_quizs @_("Your finished quizes:"), model.FINISHED

import Widget from require "lapis.html"

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

model = require('model')
msk_time = require "msk_time"

Helpers = require('views.helpers')

class AllTests extends Widget
  @include Helpers

  content: =>
    div class: "body", ->
      if @prep
        a href: @url_for("prep-quizs"), ->
          text @_("finished quizes")
        text ' | '
        a href: @url_for("prep-quizs-today"), ->
          text @_("today quizes")
        text ' | '
        a href: @url_for("quiz-state"), ->
          text @_("enable/disable quiz")
        for i, name in ipairs kr
          text ' | '
          a href: @url_for("prep-kr", {name: name}), ->
            text @_("kr") .. i
      p @_("Start new quiz:")
      names = {}
      for name, _ in pairs(quizs)
        if model.Quiz.can_create(name) or @prep
          table.insert(names, name)
      table.sort(names)
      for name in *names
        element 'table', style: 'display:inline-table', ->
          tr ->
            td ->
              @new_test_button name
      my_quizs = (t, state) ->
        qq = model.my_quizs(@, state)
        if #qq > 0
          p t
        element 'ul', ->
          for quiz in *qq
            element 'li', ->
              time = msk_time(quiz.created_at)
              raw quiz\anchor(@) .. ', ' .. time
              if quiz.state == model.FINISHED
                r = quiz.right_answers
                a = quiz.tasks
                text " | "
                b style: 'color: ' .. quiz\color!, ->
                  text "#{r} / #{a}"
      my_quizs @_("Your active quizes:"), model.ACTIVE
      my_quizs @_("Your finished quizes:"), model.FINISHED


Aegisub example: bump version.

Aegisub example: bump version.

-- This library is unlicensed under CC0

DependencyControl = require "l0.DependencyControl"

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

SIVersionCompat = DependencyControl( {
	moduleName: "SubInspector.Compat",
	version: "0.5.1",
	virtual: true
} )

ffi = versionRecord\requireModules!

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
	uint32_t hash;
	uint8_t solid;
} SI_Rect;

uint32_t    si_getVersion( void );
const char* si_getErrorString( void* );
void*       si_init( int, int, const char*, const char* );
void        si_changeResolution( void*, int, int );
void        si_reloadFonts( void*, const char*, const char* );
int         si_setHeader( void*, const char*, size_t );
int         si_setScript( void*, const char*, size_t );
int         si_calculateBounds( void*, SI_Rect*, const int32_t*, const uint32_t );
void        si_cleanup( void* );
]] )

packagePaths = ( namespace ) ->
	paths = { }
	-- fixedLibraryName = namespace .. "/" .. "#{(ffi.os != 'Windows') and 'lib' or ''}#{libraryName}.#{(OSX: 'dylib', Windows: 'dll')[ffi.os] or 'so'}"
	package.path\gsub "([^;]+)", ( path ) ->
		-- the init.lua paths are just dupes of other paths.
		if path\match "/%?/init%.lua$"
			return

		path = path\gsub "//?%?%.lua$", "/"
		table.insert paths, path .. namespace

	return paths

__name = "SubInspector"
libraryPaths = packagePaths "#{__name}/Inspector/"
libraryName = "#{(ffi.os != 'Windows') and 'lib' or ''}#{__name}.#{(OSX: 'dylib', Windows: 'dll')[ffi.os] or 'so'}"
local SubInspector, libraryPath
for path in *libraryPaths
	success, SubInspector = pcall ffi.load, path .. libraryName
	if success
		libraryPath = path
		break

assert libraryPath, "Could not load #{__name} C library."

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	siVer = DependencyControl moduleName: "SubInspector.Lib", version: ASSIVersion, virtual: true
	unless SIVersionCompat\checkVersion siVer, "major"
		return nil, ("Inspector.moon library is too old. Must be v%s.")\format siVer\getVersionString nil, "major"
	unless siVer\checkVersion SIVersionCompat, "minor"
		return nil, ("libSubInspector library is too old. Must be v%s compatible.")\format SIVersionCompat\getVersionString nil, "minor"

	return true

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

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

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

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

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

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

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

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

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

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

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

		state.inspector = ffi.gc( SubInspector.si_init( 1, 1, fontconfigConfig, fontDirectory ), SubInspector.si_cleanup )

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

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

	return true

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

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

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

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

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

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

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

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

class Inspector
	@version = versionRecord

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

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

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

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

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			SubInspector.si_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < SubInspector.si_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( SubInspector.si_getErrorString( state.inspector ) )

		return true

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		SubInspector.si_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

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

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

		@updateHeader( subtitles )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		if 0 < SubInspector.si_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( SubInspector.si_getErrorString( state.inspector ) )

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

		return rects, times

return versionRecord\register Inspector

-- This library is unlicensed under CC0

DependencyControl = require "l0.DependencyControl"

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

SIVersionCompat = DependencyControl( {
	moduleName: "SubInspector.Compat",
	version: "0.5.0",
	virtual: true
} )

ffi = versionRecord\requireModules!

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
	uint32_t hash;
	uint8_t solid;
} SI_Rect;

uint32_t    si_getVersion( void );
const char* si_getErrorString( void* );
void*       si_init( int, int, const char*, const char* );
void        si_changeResolution( void*, int, int );
void        si_reloadFonts( void*, const char*, const char* );
int         si_setHeader( void*, const char*, size_t );
int         si_setScript( void*, const char*, size_t );
int         si_calculateBounds( void*, SI_Rect*, const int32_t*, const uint32_t );
void        si_cleanup( void* );
]] )

packagePaths = ( namespace ) ->
	paths = { }
	-- fixedLibraryName = namespace .. "/" .. "#{(ffi.os != 'Windows') and 'lib' or ''}#{libraryName}.#{(OSX: 'dylib', Windows: 'dll')[ffi.os] or 'so'}"
	package.path\gsub "([^;]+)", ( path ) ->
		-- the init.lua paths are just dupes of other paths.
		if path\match "/%?/init%.lua$"
			return

		path = path\gsub "//?%?%.lua$", "/"
		table.insert paths, path .. namespace

	return paths

__name = "SubInspector"
libraryPaths = packagePaths "#{__name}/Inspector/"
libraryName = "#{(ffi.os != 'Windows') and 'lib' or ''}#{__name}.#{(OSX: 'dylib', Windows: 'dll')[ffi.os] or 'so'}"
local SubInspector, libraryPath
for path in *libraryPaths
	success, SubInspector = pcall ffi.load, path .. libraryName
	if success
		libraryPath = path
		break

assert libraryPath, "Could not load #{__name} C library."

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	siVer = DependencyControl moduleName: "SubInspector.Lib", version: ASSIVersion, virtual: true
	unless SIVersionCompat\checkVersion siVer, "major"
		return nil, ("Inspector.moon library is too old. Must be v%s.")\format siVer\getVersionString nil, "major"
	unless siVer\checkVersion SIVersionCompat, "minor"
		return nil, ("libSubInspector library is too old. Must be v%s compatible.")\format SIVersionCompat\getVersionString nil, "minor"

	return true

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

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

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

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

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

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

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

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

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

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

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

		state.inspector = ffi.gc( SubInspector.si_init( 1, 1, fontconfigConfig, fontDirectory ), SubInspector.si_cleanup )

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

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

	return true

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

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

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

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

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

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

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

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

class Inspector
	@version = versionRecord

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

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

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

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

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			SubInspector.si_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < SubInspector.si_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( SubInspector.si_getErrorString( state.inspector ) )

		return true

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		SubInspector.si_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

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

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

		@updateHeader( subtitles )

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		if 0 < SubInspector.si_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( SubInspector.si_getErrorString( state.inspector ) )

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

		return rects, times

return versionRecord\register Inspector


Fix setting the title from within config

* Forgot to recheck that this worked after moving things around.

-- Try a few different json libraries and return one that loads
require_json = ->
	json = nil
	rj = (s) -> pcall -> json = require s
	if not (rj("dkjson") or rj("lua2json") or rj("cjson"))
		error "Cannot find a json library to use"

	json

-- Extract the Löve version information for later use
extract_love_version = (version) ->
	major, minor, patch = string.match version, "^(%d+)%.(%d+)%.(%d+)$"
	release = string.match version, '^%d+%.%d+'
	export LunoPunk = {love_version: {:release, :major, :minor, :patch}}

-- Sets the window title of the engine
set_love_title = (title, release = false) ->
	development = " (development)" if not release

	if title == nil
		title = string.format "%s%s", "LunoPunk", development
	else
		title = string.format "%s%s", title, development

	if LunoPunk.love_version.release == "0.9" and love.window
		love.window.setTitle title
	elseif love.graphics
		love.graphics.setCaption title

	title

-- This should be called from conf.lua from the main directory of your project
-- The filename is actualy very important. Your entire project and LunoPunk
-- must be compiled with moonc before this will work. Love actually reads
-- conf.lua before all others. Using require won't work unless you include the
-- moonscript loader first.
--
-- @param options A table containing options for configuring the engine
-- @param cfg A configuration filename, json is currently accepted. If a pure Lua YAML library is created, that could be an option as well.
-- @param width The window width
-- @param height The window height
-- @param fullscreen If the engine should switch to fullscreen mode
-- @param title The application title
-- @param author Likely you or your team's name
-- @param release Setting this to true so the engine doesn't display back traces to the user
config = (options) ->
	if options and type(options.cfg) == "string"
		json = require_json!
		if json == nil
			print "Error loading a json decoding library"
		else
			f, err = io.open options.cfg
			if f == nil
				print "I/O Error:", err
			else
				tmp_options, err = json.decode f\read "*a"
				f\close!
				if tmp_options == nil
					error string.format "Parse error in %s: %s", options.cfg, err
				else
					options[k] = v for k,v in pairs tmp_options

	-- This is the actual callback Löve will call upon loading conf.lua
	love.conf = (t) ->
		extract_love_version t.version

		if options != nil
			window = nil
			if t.version == "0.9.0"
				window = t.window
			else
				window = t.screen

			window.width = options.width if options.width != nil
			window.height = options.height if options.height != nil
			window.fullscreen = options.fullscreen if options.fullscreen != nil
			t.title = set_love_title options.title, options.release
		else
			t.title = set_love_title!

{:config, :extract_love_version, :set_love_title}

-- Try a few different json libraries and return one that loads
require_json = ->
	json = nil
	rj = (s) -> pcall -> json = require s
	if not (rj("dkjson") or rj("lua2json") or rj("cjson"))
		error "Cannot find a json library to use"

	json

-- Extract the Löve version information for later use
extract_love_version = (version) ->
	major, minor, patch = string.match version, "^(%d+)%.(%d+)%.(%d+)$"
	release = string.match version, '^%d+%.%d+'
	export LunoPunk = {love_version: {:release, :major, :minor, :patch}}

-- Sets the window title of the engine
set_love_title = (title, release = false) ->
	development = " (development)" if not release

	if title == nil
		title = string.format "%s%s", "LunoPunk", development
	else
		title = string.format "%s%s", title, development

	if LunoPunk.love_version.release == "0.9"
		love.window.setTitle title
	else
		love.graphics.setCaption title

-- This should be called from conf.lua from the main directory of your project
-- The filename is actualy very important. Your entire project and LunoPunk
-- must be compiled with moonc before this will work. Love actually reads
-- conf.lua before all others. Using require won't work unless you include the
-- moonscript loader first.
--
-- @param options A table containing options for configuring the engine
-- @param cfg A configuration filename, json is currently accepted. If a pure Lua YAML library is created, that could be an option as well.
-- @param width The window width
-- @param height The window height
-- @param fullscreen If the engine should switch to fullscreen mode
-- @param title The application title
-- @param author Likely you or your team's name
-- @param release Setting this to true so the engine doesn't display back traces to the user
config = (options) ->
	if options and type(options.cfg) == "string"
		json = require_json!
		if json == nil
			print "Error loading a json decoding library"
		else
			f, err = io.open options.cfg
			if f == nil
				print "I/O Error:", err
			else
				tmp_options, err = json.decode f\read "*a"
				f\close!
				if tmp_options == nil
					error string.format "Parse error in %s: %s", options.cfg, err
				else
					options[k] = v for k,v in pairs tmp_options

	-- This is the actual callback Löve will call upon loading conf.lua
	love.conf = (t) ->
		extract_love_version t.version

		if options != nil
			window = nil
			if t.version == "0.9.0"
				window = t.window
			else
				window = t.screen

			window.width = options.width if options.width != nil
			window.height = options.height if options.height != nil
			window.fullscreen = options.fullscreen if options.fullscreen != nil
			set_love_title options.title, options.release
		else
			set_love_title!

{:config, :extract_love_version, :set_love_title}


Fixed invalid call priority

Fixed invalid call priority

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

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

    for ply in *player.GetAll()
        ply.__cachedIsPony = ply\IsPony()
        ponydata = ply\GetPonyData()

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

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

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

lastDataSend = 0
lastDataReceived = 0
net.Receive 'PPM2.RequestPonyData', ->
    return if lastDataReceived > RealTime()
    lastDataReceived = RealTime() + 10
    RunConsoleCommand('ppm2_reload')

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

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

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

hook.Add 'KeyPress', 'PPM2.RequireData', ->
    hook.Remove 'KeyPress', 'PPM2.RequireData'
    RunConsoleCommand('ppm2_reload')
    timer.Simple 3, -> RunConsoleCommand('ppm2_require')

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

hook.Add 'HUDPaint', 'PPM2.EditorStatus', ->
    lply = LocalPlayer()
    lpos = lply\EyePos()
    for ply in *player.GetAll()
        if ply ~= lply
            if ply\GetNWBool('PPM2.InEditor')
                pos = ply\EyePos()
                dist = pos\Distance(lpos)
                if dist < 250
                    pos.z += 10
                    alpha = math.Clamp(1.3 - dist / 250, 0.1, 1) * 255
                    {:x, :y} = pos\ToScreen()
                    draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)
                    draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)

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

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

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

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

    for ply in *player.GetAll()
        ply.__cachedIsPony = ply\IsPony()
        ponydata = ply\GetPonyData()

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

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

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

lastDataSend = 0
lastDataReceived = 0
net.Receive 'PPM2.RequestPonyData', ->
    return if lastDataReceived > RealTime()
    lastDataReceived = RealTime() + 10
    RunConsoleCommand('ppm2_reload')

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

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

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

hook.Add 'KeyPress', 'PPM2.RequireData', ->
    hook.Remove 'KeyPress', 'PPM2.RequireData'
    RunConsoleCommand('ppm2_reload')
    timer.Simple 3, -> RunConsoleCommand('ppm2_require')

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

hook.Add 'HUDPaint', 'PPM2.EditorStatus', ->
    lply = LocalPlayer()
    lpos = lply\EyePos()
    for ply in *player.GetAll()
        if ply ~= lply
            if ply\GetNWBool('PPM2.InEditor')
                pos = ply\EyePos()
                dist = pos\Distance(lpos)
                if dist < 250
                    pos.z += 10
                    alpha = math.Clamp(1.3 - dist / 250, 0.1, 1) * 255
                    {:x, :y} = pos\ToScreen()
                    draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x + 1, y + 1, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER)
                    draw.DrawText('In PPM/2 Editor', 'HudHintTextLarge', x, y, Color(0, 0, 0, alpha), TEXT_ALIGN_CENTER)

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

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


plugins/data: Add storage for vars that should only persist with the current connection

import IRCClient, priority from require 'irc'

unpack = unpack or table.unpack

IRCClient\add_hook 'CONNECT', =>
	@channels = {}
	@users    = {}
	@server   = {
		caps:       {}
		ircv3_caps: {}
		batches:    {}
	}
	@vars = {}

IRCClient\add_hook 'CONNECT', =>
	-- Welcome
	@data = {} if not @data
	@data.last_connect = os.time()
	@send_raw 'CAP', 'LS', '302'

IRCClient\add_handler '005', (prefix, args)=>
	-- Capabilities
	isupport_caps = {select 2, unpack args}
	for cap in *isupport_caps
		if cap\sub(1, 1) == "-"
			-- remove support
			if cap\find "=" then
				@server.caps[cap\match "^%-(.+)="] = nil
			else
				@server.caps[cap\sub 2] = nil
		elseif cap\find "="
			key, value = cap\match '^(.-)=(.+)'
			@server.caps[key] = value
		else
			@server.caps[cap] = true

IRCClient\add_handler 'AWAY', (prefix, args)=>
	{:nick} = prefix
	@users[nick].away = args[#args]

IRCClient\add_handler 'ACCOUNT', (prefix, args)=>
	@users[nick].account = args[1] != "*" and args[1] or nil

IRCClient\add_handler 'RENAME', (prefix, args)=>
	{old, new} = args
	for _, user in pairs @channels[old].users
		user.channels[new] = user.channels[old]
		user.channels[old] = nil
	@channels[new] = @channels[old]
	@channels[old] = nil

IRCClient\add_handler 'JOIN', (prefix, args, tags={})=>
	-- user JOINs a channel
	local channel
	local account
	if @server.ircv3_caps['extended-join']
		account = args[2] if args[2] != '*'
		channel = args[1]
	elseif @server.ircv3_caps['account-tag'] and tags.account
		account = tags.account
		channel = args[1]
	else
		channel = args[1]
	{:nick, :user, :host} = prefix
	if host
		if not @users[nick] then
			@users[nick] = {
				account: account
				channels: {
					[channel]: {
						status: ""
					}
				},
				:user,
				:host
			}
		else
			if not @users[nick].channels
				@users[nick].channels = {
					[channel]: {
						status: ""
					}
				}
			else
				@users[nick].channels[channel] = status: ""
		@users[nick].account = account if account
	if not @channels[channel]
		if @server.caps['WHOX']
			@send_raw 'WHO', channel, '%nat,001'
		elseif @server.ircv3_caps['userhost-in-names']
			@send_raw 'NAMES', channel
		else
			@send_raw 'WHO', channel

		@channels[channel] = {
			users: {
				[nick]: @users[nick]
			}
		}
	else
		@channels[channel].users[nick] = @users[nick]

IRCClient\add_hook 'WHOX_001', (nick, account)=>
	@users[nick].account = account if @users[nick] and account ~= '0'

IRCClient\add_handler 'NICK', (prefix, args)=>
	old = prefix.nick
	new = args[1]
	for channel_name in pairs @users[old].channels
		@channels[channel_name].users[new] = @channels[channel_name].users[old]
		@channels[channel_name].users[old] = nil
	@users[new] = @users[old]
	@users[old] = nil

IRCClient\add_handler 'MODE', (prefix, args)=>
	-- User or bot called /mode
	if args[1] and args[1]\sub(1,1) == "#"
		@send_raw 'NAMES', args[1]

IRCClient\add_handler '353', (prefix, args)=>
	-- Result of NAMES
	channel = args[3]
	statuses = @server.caps.PREFIX and @server.caps.PREFIX\match '%(.-%)(.+)' or "+@"
	statuses = "[" .. statuses\gsub("%p", "%%%1") .. "]"
	for text in args[#args]\gmatch '%S+'
		local status, pre, nick, user, host
		if text\match statuses
			status, pre = text\match ('^(%s+)(.+)')\format statuses
		else
			status, pre = '', text
		if @server.ircv3_caps['userhost-in-names']
			nick, user, host = pre\match '^(.-)!(.-)@(.-)$'
		else
			nick = pre
		if not @users[nick]
			@users[nick] = {channels: {}}
		@users[nick].user = user if user
		@users[nick].host = host if host
		if @channels[channel].users[nick]
			if @users[nick].channels[channel]
				@users[nick].channels[channel].status = status
			else
				@users[nick].channels[channel] = :status
		else
			@channels[channel].users[nick] = @users[nick]
			@users[nick].channels[channel] = :status

IRCClient\add_handler '352', (prefix, args)=>
	_, user, host, _, nick, away = unpack args
	@users[nick] = {channels: {}} if not @users[nick]
	@users[nick].user = user
	@users[nick].host = host
	@users[nick].away = away\sub(1, 1) == "G"

IRCClient\add_handler 'CHGHOST', (prefix, args)=>
	{:nick} = prefix
	@users[nick].user = args[1]
	@users[nick].host = args[2]

IRCClient\add_handler 'KICK', (prefix, args)=>
	channel = args[1]
	nick = args[2]
	@users[nick].channels[channel] = nil
	if not next @users[nick].channels
		@users[nick] = nil

IRCClient\add_handler 'PART', (prefix, args)=>
	-- User or bot parted channel, clear from lists
	channel = args[1]
	{:nick} = prefix
	@users[nick].channels[channel] = nil
	if not next @users[nick].channels
		@users[nick] = nil -- User left network, garbagecollect

IRCClient\add_handler 'QUIT', (prefix, args)=>
	-- User or bot parted network, nuke from lists
	{:nick} = prefix
	for channel in pairs @users[nick].channels
		@channels[channel].users[nick] = nil
	@users[nick] = nil

IRCClient\add_handler 'PRIVMSG', priority: priority.HIGH, (prefix, args, tags={})=>
	account_tag = @get_tag tags, key: "account"
	if account_tag and prefix.nick and @users[prefix.nick]
		@users[prefix.nick].account = account_tag.value

import IRCClient, priority from require 'irc'

unpack = unpack or table.unpack

IRCClient\add_hook 'CONNECT', =>
	@channels = {}
	@users    = {}
	@server   = {
		caps:       {}
		ircv3_caps: {}
		batches:    {}
	}

IRCClient\add_hook 'CONNECT', =>
	-- Welcome
	@data = {} if not @data
	@data.last_connect = os.time()
	@send_raw 'CAP', 'LS', '302'

IRCClient\add_handler '005', (prefix, args)=>
	-- Capabilities
	isupport_caps = {select 2, unpack args}
	for cap in *isupport_caps
		if cap\sub(1, 1) == "-"
			-- remove support
			if cap\find "=" then
				@server.caps[cap\match "^%-(.+)="] = nil
			else
				@server.caps[cap\sub 2] = nil
		elseif cap\find "="
			key, value = cap\match '^(.-)=(.+)'
			@server.caps[key] = value
		else
			@server.caps[cap] = true

IRCClient\add_handler 'AWAY', (prefix, args)=>
	{:nick} = prefix
	@users[nick].away = args[#args]

IRCClient\add_handler 'ACCOUNT', (prefix, args)=>
	@users[nick].account = args[1] != "*" and args[1] or nil

IRCClient\add_handler 'RENAME', (prefix, args)=>
	{old, new} = args
	for _, user in pairs @channels[old].users
		user.channels[new] = user.channels[old]
		user.channels[old] = nil
	@channels[new] = @channels[old]
	@channels[old] = nil

IRCClient\add_handler 'JOIN', (prefix, args, tags={})=>
	-- user JOINs a channel
	local channel
	local account
	if @server.ircv3_caps['extended-join']
		account = args[2] if args[2] != '*'
		channel = args[1]
	elseif @server.ircv3_caps['account-tag'] and tags.account
		account = tags.account
		channel = args[1]
	else
		channel = args[1]
	{:nick, :user, :host} = prefix
	if host
		if not @users[nick] then
			@users[nick] = {
				account: account
				channels: {
					[channel]: {
						status: ""
					}
				},
				:user,
				:host
			}
		else
			if not @users[nick].channels
				@users[nick].channels = {
					[channel]: {
						status: ""
					}
				}
			else
				@users[nick].channels[channel] = status: ""
		@users[nick].account = account if account
	if not @channels[channel]
		if @server.caps['WHOX']
			@send_raw 'WHO', channel, '%nat,001'
		elseif @server.ircv3_caps['userhost-in-names']
			@send_raw 'NAMES', channel
		else
			@send_raw 'WHO', channel

		@channels[channel] = {
			users: {
				[nick]: @users[nick]
			}
		}
	else
		@channels[channel].users[nick] = @users[nick]

IRCClient\add_hook 'WHOX_001', (nick, account)=>
	@users[nick].account = account if @users[nick] and account ~= '0'

IRCClient\add_handler 'NICK', (prefix, args)=>
	old = prefix.nick
	new = args[1]
	for channel_name in pairs @users[old].channels
		@channels[channel_name].users[new] = @channels[channel_name].users[old]
		@channels[channel_name].users[old] = nil
	@users[new] = @users[old]
	@users[old] = nil

IRCClient\add_handler 'MODE', (prefix, args)=>
	-- User or bot called /mode
	if args[1] and args[1]\sub(1,1) == "#"
		@send_raw 'NAMES', args[1]

IRCClient\add_handler '353', (prefix, args)=>
	-- Result of NAMES
	channel = args[3]
	statuses = @server.caps.PREFIX and @server.caps.PREFIX\match '%(.-%)(.+)' or "+@"
	statuses = "[" .. statuses\gsub("%p", "%%%1") .. "]"
	for text in args[#args]\gmatch '%S+'
		local status, pre, nick, user, host
		if text\match statuses
			status, pre = text\match ('^(%s+)(.+)')\format statuses
		else
			status, pre = '', text
		if @server.ircv3_caps['userhost-in-names']
			nick, user, host = pre\match '^(.-)!(.-)@(.-)$'
		else
			nick = pre
		if not @users[nick]
			@users[nick] = {channels: {}}
		@users[nick].user = user if user
		@users[nick].host = host if host
		if @channels[channel].users[nick]
			if @users[nick].channels[channel]
				@users[nick].channels[channel].status = status
			else
				@users[nick].channels[channel] = :status
		else
			@channels[channel].users[nick] = @users[nick]
			@users[nick].channels[channel] = :status

IRCClient\add_handler '352', (prefix, args)=>
	_, user, host, _, nick, away = unpack args
	@users[nick] = {channels: {}} if not @users[nick]
	@users[nick].user = user
	@users[nick].host = host
	@users[nick].away = away\sub(1, 1) == "G"

IRCClient\add_handler 'CHGHOST', (prefix, args)=>
	{:nick} = prefix
	@users[nick].user = args[1]
	@users[nick].host = args[2]

IRCClient\add_handler 'KICK', (prefix, args)=>
	channel = args[1]
	nick = args[2]
	@users[nick].channels[channel] = nil
	if not next @users[nick].channels
		@users[nick] = nil

IRCClient\add_handler 'PART', (prefix, args)=>
	-- User or bot parted channel, clear from lists
	channel = args[1]
	{:nick} = prefix
	@users[nick].channels[channel] = nil
	if not next @users[nick].channels
		@users[nick] = nil -- User left network, garbagecollect

IRCClient\add_handler 'QUIT', (prefix, args)=>
	-- User or bot parted network, nuke from lists
	{:nick} = prefix
	for channel in pairs @users[nick].channels
		@channels[channel].users[nick] = nil
	@users[nick] = nil

IRCClient\add_handler 'PRIVMSG', priority: priority.HIGH, (prefix, args, tags={})=>
	account_tag = @get_tag tags, key: "account"
	if account_tag and prefix.nick and @users[prefix.nick]
		@users[prefix.nick].account = account_tag.value