types

types

import Model, enum from require "lapis.db.model"
import create_table, types, add_column, drop_column from require "lapis.db.schema"
import band, bor from require "bit"
import list from require "lapis.db"
import to_json, from_json from require "lapis.util"
Users = require "lazuli.modules.user_management.models.users"
Timeline_Entries = require "models.timeline_entries"
import OrderedPaginator from require "lapis.db.pagination"
config = (require "lapis.config").get!

class Timelines extends Model
  @relations: {
    {"user",               belongs_to: "Users"}
    {"acl",                belongs_to: "ACLs"}
  }

  fetchEntries: do
    local all_users
    (per_page=nil)=>
      cuname = "users-"..@acl_id
      users = ngx.shared.timeline_cache\get cuname
      if not users
        acl = @get_acl!
        all_users or= Users\select!
        users=list [u.id for u in *all_users when acl\matchUser u]
        ngx.shared.timeline_cache\set cuname, users, 30
      ctname = "types-"..@id
      types = ngx.shared.timeline_cache\get ctname
      if not types
        types=list [k for k,v in ipairs Timeline_Entries.types when @["flag_include_"..v.."s"]]
        ngx.shared.timeline_cache\set ctname, types, 30
      OrderedPaginator Timeline_Entries "id", [[where "user_id" in ? and "type" in ?]], users, types, {
        per_page: per_page
        order: "desc"
      }

  @get_relation_model: (name)=> switch name
    when "Users"
      require "lazuli.modules.user_management.models.users"
    when "ACLs"
      require "models.acls"

  @migrations: {
    ->
      create_table "timelines", {
        {"id", types.serial}
        {"user_id", types.integer}
        {"acl_id", types.integer}
        --{"flag_include_posts_text", types.boolean}
        --{"flag_include_posts_image", types.boolean}
        --{"flag_include_posts_video", types.boolean}
        --{"flag_include_comments", types.boolean}
        --{"flag_include_likes", types.boolean}
        --{"flag_include_profiles", types.boolean}
        "PRIMARY KEY (id)"
      }

  }

import Model, enum from require "lapis.db.model"
import create_table, types, add_column, drop_column from require "lapis.db.schema"
import band, bor from require "bit"
import list from require "lapis.db"
import to_json, from_json from require "lapis.util"
Users = require "lazuli.modules.user_management.models.users"
Timeline_Entries = require "models.timeline_entries"
import OrderedPaginator from require "lapis.db.pagination"
config = (require "lapis.config").get!

class Timelines extends Model
  @relations: {
    {"user",               belongs_to: "Users"}
    {"acl",                belongs_to: "ACLs"}
  }

  fetchEntries: do
    local all_users, acl
    (per_page=nil)=>
      cuname = "users-"..@acl_id
      users = ngx.shared.timeline_cache\get cuname
      if users
        users=from_json users
      if not users
        acl or= @get_acl!
        all_users or= Users\select!
        users=[u.id for u in *all_users when acl\matchUser u]
        ngx.shared.timeline_cache\set cuname, to_json(users), 30
      OrderedPaginator Timeline_Entries "id", "where user_id in ?", list(users), per_page:per_page, order:"desc"

  @get_relation_model: (name)=> switch name
    when "Users"
      require "lazuli.modules.user_management.models.users"
    when "ACLs"
      require "models.acls"

  @migrations: {
    ->
      create_table "timelines", {
        {"id", types.serial}
        {"user_id", types.integer}
        {"acl_id", types.integer}
        --{"flag_include_posts_text", types.boolean}
        --{"flag_include_posts_image", types.boolean}
        --{"flag_include_posts_video", types.boolean}
        --{"flag_include_comments", types.boolean}
        --{"flag_include_likes", types.boolean}
        --{"flag_include_profiles", types.boolean}
        "PRIMARY KEY (id)"
      }

  }


Added tweens.motion.Motion

Added tweens.motion.Motion

require "LunoPunk.Tween"

export ^

class Motion extends Tween
	-- @param	duration	Duration of the Tween.
	-- @param	complete	Optional completion callback.
	-- @param	type		Tween type.
	-- @param	ease		Optional easer function.
	new: (duration, type = TweenType.Persist, complete = nil, ease = nil) =>
		@x, @y = 0, 0
		super duration, type, complete, ease


add links to kr1, kr2

add links to kr1, kr2

import Widget from require "lapis.html"

quizs = require('quiz.all')
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)")
        text ' '
        a href: @url_for("prep-kr", {name: 'hello'}), ->
          text @_("kr1")
        text ' '
        url = @url_for("prep-kr", {
          name: 'if_list_for_range_short'})
        a href: url, ->
          text @_("kr2")
      p @_("Start new quiz:")
      element 'table', -> element 'tr', ->
        for name, _ in pairs quizs
          element 'td', ->
            @new_test_button name
      my_quizs = (text, state) ->
        qq = model.my_quizs(@, state)
        if #qq > 0
          p text
        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
                b " | #{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')
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)")
      p @_("Start new quiz:")
      element 'table', -> element 'tr', ->
        for name, _ in pairs quizs
          element 'td', ->
            @new_test_button name
      my_quizs = (text, state) ->
        qq = model.my_quizs(@, state)
        if #qq > 0
          p text
        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
                b " | #{r} / #{a}"
      my_quizs @_("Your active quizes:"), model.ACTIVE
      my_quizs @_("Your finished quizes:"), model.FINISHED


plugins/data: add a space

plugins/data: add a space

import IRCClient from require 'irc'

serve_self = (new_table)-> setmetatable(new_table, {__call: => pairs(@)})
unpack = unpack or table.unpack

caps = {'extended-join', 'multi-prefix', 'away-notify', 'account-notify',
	'chghost', 'server-time', 'echo-message', 'invite-notify'}

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

IRCClient\add_hook 'ACK_CAP', =>
	@data.set_caps -= 1
	if @data.set_caps <= 0
		@send_raw 'CAP END'

IRCClient\add_hook 'CONNECT', =>
	-- Welcome
	@data = {} if not @data
	@data.last_connect = os.time()
	@data.set_caps = 0
	@send_raw 'CAP LS 302'
	for i=1, #caps
		@data.set_caps += 1

IRCClient\add_handler 'BATCH', (prefix, args, trail, tags)=>
	tag_type, tag = args[1]\match '(.)(.+)'
	if tag_type == '+'
		@server.batches[tag] = {unpack(args, 2)}
	elseif tag_type == '-'
		@server.batches[tag] = nil

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, trail)=>
	nick = prefix\match '^(.-)!'
	@users[nick].away = trail

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

IRCClient\add_handler 'JOIN', (prefix, args, trail, 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] or trail
	nick, username, host = prefix\match '^(.-)!(.-)@(.-)$'
	if prefix\match '^.-!.-@.-$'
		if not @users[nick] then
			@users[nick] = {
				account: account
				channels: {
					[channel]: {
						status: ""
					}
				},
				:username,
				: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.ircv3_caps['userhost-in-names']
			@send_raw ('NAMES %s')\format channel
		else
			@send_raw ('WHO %s')\format channel
		@channels[channel] = {
			users: {
				[nick]: @users[nick]
			}
		}
	else
		@channels[channel].users[nick] = @users[nick]

IRCClient\add_handler 'NICK', (prefix, args, trail)=>
	old = prefix\match('^(.-)!') or prefix
	new = args[1] or trail
	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 %s')\format args[1]

IRCClient\add_handler '353', (prefix, args, trail)=>
	-- Result of NAMES
	channel = args[3]
	statuses = @server.caps.PREFIX and @server.caps.PREFIX\match '%(.-%)(.+)' or "+@"
	statuses = "[" .. statuses\gsub("%p", "%%%1") .. "]"
	for text in trail\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\match '^(.-)!'
	@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 #@users[nick].channels == 0
		@users[nick] = nil

IRCClient\add_handler 'PART', (prefix, args)=>
	-- User or bot parted channel, clear from lists
	channel = args[1]
	nick = prefix\match '^(.-)!'
	@users[nick].channels[channel] = nil
	if #@users[nick].channels == 0
		@users[nick] = nil -- User left network, garbagecollect

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

IRCClient\add_handler 'CAP', (prefix, args, trailing)=>
	to_process = {} if args[2] == 'LS' or args[2] == 'ACK' or args[2] == 'NAK'
	if args[2] == 'LS' or args[2] == 'ACK' or args[2] == 'NEW' or args[2] == 'DEL'
		for item in trailing\gmatch '%S+'
			for cap in *caps
				if item == cap
					to_process[#to_process + 1] = cap
	if args[2] == 'LS'
		if #to_process > 0
			@send_raw ('CAP REQ :%s')\format table.concat(to_process, ' ')
		for i=#to_process + 1, #caps
			@fire_hook 'ACK_CAP'
	elseif args[2] == 'NEW'
		to_send = {}
		for item in trailing\gmatch '%S+'
			for cap in *caps
				if item == cap
					to_send[#to_send + 1] = item
		@send_raw ('CAP REQ :%s')\format table.concat(to_send, ' ')
	elseif args[2] == 'DEL'
		for item in trailing\gmatch '%S+'
			@ircv3_caps[item] = nil
	elseif args[2] == 'ACK'
		for cap in *to_process
			key, value = cap\match '^(.-)=(.+)'
			if value
				@server.ircv3_caps[key] = value
			else
				@server.ircv3_caps[cap] = true
			@fire_hook 'ACK_CAP'

import IRCClient from require 'irc'

serve_self = (new_table)-> setmetatable(new_table, {__call: =>pairs(@)})
unpack = unpack or table.unpack

caps = {'extended-join', 'multi-prefix', 'away-notify', 'account-notify',
	'chghost', 'server-time', 'echo-message', 'invite-notify'}

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

IRCClient\add_hook 'ACK_CAP', =>
	@data.set_caps -= 1
	if @data.set_caps <= 0
		@send_raw 'CAP END'

IRCClient\add_hook 'CONNECT', =>
	-- Welcome
	@data = {} if not @data
	@data.last_connect = os.time()
	@data.set_caps = 0
	@send_raw 'CAP LS 302'
	for i=1, #caps
		@data.set_caps += 1

IRCClient\add_handler 'BATCH', (prefix, args, trail, tags)=>
	tag_type, tag = args[1]\match '(.)(.+)'
	if tag_type == '+'
		@server.batches[tag] = {unpack(args, 2)}
	elseif tag_type == '-'
		@server.batches[tag] = nil

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, trail)=>
	nick = prefix\match '^(.-)!'
	@users[nick].away = trail

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

IRCClient\add_handler 'JOIN', (prefix, args, trail, 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] or trail
	nick, username, host = prefix\match '^(.-)!(.-)@(.-)$'
	if prefix\match '^.-!.-@.-$'
		if not @users[nick] then
			@users[nick] = {
				account: account
				channels: {
					[channel]: {
						status: ""
					}
				},
				:username,
				: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.ircv3_caps['userhost-in-names']
			@send_raw ('NAMES %s')\format channel
		else
			@send_raw ('WHO %s')\format channel
		@channels[channel] = {
			users: {
				[nick]: @users[nick]
			}
		}
	else
		@channels[channel].users[nick] = @users[nick]

IRCClient\add_handler 'NICK', (prefix, args, trail)=>
	old = prefix\match('^(.-)!') or prefix
	new = args[1] or trail
	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 %s')\format args[1]

IRCClient\add_handler '353', (prefix, args, trail)=>
	-- Result of NAMES
	channel = args[3]
	statuses = @server.caps.PREFIX and @server.caps.PREFIX\match '%(.-%)(.+)' or "+@"
	statuses = "[" .. statuses\gsub("%p", "%%%1") .. "]"
	for text in trail\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\match '^(.-)!'
	@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 #@users[nick].channels == 0
		@users[nick] = nil

IRCClient\add_handler 'PART', (prefix, args)=>
	-- User or bot parted channel, clear from lists
	channel = args[1]
	nick = prefix\match '^(.-)!'
	@users[nick].channels[channel] = nil
	if #@users[nick].channels == 0
		@users[nick] = nil -- User left network, garbagecollect

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

IRCClient\add_handler 'CAP', (prefix, args, trailing)=>
	to_process = {} if args[2] == 'LS' or args[2] == 'ACK' or args[2] == 'NAK'
	if args[2] == 'LS' or args[2] == 'ACK' or args[2] == 'NEW' or args[2] == 'DEL'
		for item in trailing\gmatch '%S+'
			for cap in *caps
				if item == cap
					to_process[#to_process + 1] = cap
	if args[2] == 'LS'
		if #to_process > 0
			@send_raw ('CAP REQ :%s')\format table.concat(to_process, ' ')
		for i=#to_process + 1, #caps
			@fire_hook 'ACK_CAP'
	elseif args[2] == 'NEW'
		to_send = {}
		for item in trailing\gmatch '%S+'
			for cap in *caps
				if item == cap
					to_send[#to_send + 1] = item
		@send_raw ('CAP REQ :%s')\format table.concat(to_send, ' ')
	elseif args[2] == 'DEL'
		for item in trailing\gmatch '%S+'
			@ircv3_caps[item] = nil
	elseif args[2] == 'ACK'
		for cap in *to_process
			key, value = cap\match '^(.-)=(.+)'
			if value
				@server.ircv3_caps[key] = value
			else
				@server.ircv3_caps[cap] = true
			@fire_hook 'ACK_CAP'


Updated RS3 model heuristic.

Updated RS3 model heuristic.

- Fixed index extraction logic. It skipped the last index.

TextureAtlasFilter = require "Heuristics/TextureAtlasFilter"

class RS3Model
	new: (model, texture) =>
		@model = model
		@texture = texture

class RS3ModelGroup
	new: (id) =>
		@id = id
		@models = {}

	add_model: (model, texture) =>
		table.insert(@models, RS3Model(model, texture))

	save_node: (directory) =>
		for i = 1, #@models
			filename = string.format("%s/%010d.%03d", directory, @id, i)
			@models[i].model\save_dae(filename .. ".dae")

			if @models[i].texture != nil
				@models[i].texture\save_png(filename .. ".png")

			glurg.common.Log\info('medium', "saved model #{@id}.#{i}")

class ExtractRS3Model extends glurg.heuristics.GL3StaticModelHeuristic
	new: (data) =>
		super(data)

		@texture_atlas_filter = TextureAtlasFilter!
		@attributes.atlas_binding = data["atlas"]
		@attributes.color_binding = data["color"]
		@atlas_sampler = data["atlas-sampler"]
		@atlas_meta = data["atlas-meta"]

	get_filters: =>
		return @texture_atlas_filter, super\get_filters!

	is_compatible: =>
		program = @program_filter\get_current_program!
		atlas = program\has_attribute(@attributes.atlas_binding)
		color = program\has_attribute(@attributes.color_binding)

		return super\is_compatible! and atlas

	examine_meshes: =>
		atlas_mesh = @\make_mesh(@attributes.atlas_binding)

		meshes = {}
		p = atlas_mesh\get_value(1) -- previous
		p_start = 0
		index = 1
		while index < atlas_mesh.num_values
			c = atlas_mesh\get_value(index + 1) -- current
			d = 1
			dx = math.abs(p.x - c.x) > d
			dy = math.abs(p.y - c.y) > d
			dz = math.abs(p.z - c.z) > d
			dw = math.abs(p.w - c.w) > d
			if dx or dy or dz or dw
				table.insert(meshes, { start: p_start, stop: index - 1, meta: p })
				p = c
				p_start = index
			index += 1
		table.insert(meshes, { start: p_start, stop: index - 1, meta: p})

		return meshes

	calculate_byte_offset_size: (attribute_index, offset_start, offset_end, call) =>
		-- See glurg.resources.MeshBlobBuilder.extract_attrib_call for specifics
		-- about how vertex descriptino is extracted
		vertex_array = @vertex_array_filter\get_current_vertex_array!
		description = vertex_array.descriptions[attribute_index]

		size = call\get_argument_by_name("size")\query!
		local num_components
		if size.value_name == 'GL_BGRA'
			num_components = 4
		else
			size = size.value\query!

		format = call\get_argument_by_name("type")\query!
		local component_size
		switch format.value_name
			when 'GL_FLOAT' then component_size = 4
			when 'GL_BYTE', 'GL_UNSIGNED_BYTE' then component_size = 1
			when 'GL_SHORT', 'GL_UNSIGNED_SHORT' then component_size = 2
			when 'GL_INT', 'GL_UNSIGNED_INT' then component_size = 4
			else error "unknown or unsupported data format"

		stride = call\get_argument_by_name("stride")\query!
		pointer = call\get_argument_by_name("pointer")\query!
		element_offset = 0
		local element_size
		if stride == 0
			element_size = num_components * component_size
			if type(pointer) == "number"
				element_offset = pointer
		else
			element_size = stride
			element_offset = 0

		byte_count = (offset_end - offset_start) * element_size
		byte_offset = offset_start * element_size + element_offset

		return byte_offset, byte_count

	make_sliced_mesh: (attribute_name, offset_start, offset_end) =>
		program = @program_filter\get_current_program!
		attribute_index = program\get_attribute(attribute_name)

		vertex_array = @vertex_array_filter\get_current_vertex_array!
		description = vertex_array.descriptions[attribute_index]

		buffer = @array_buffer_filter\get_buffer(description.array_buffer)
		buffer_offset, buffer_size = @\calculate_byte_offset_size(
			attribute_index, offset_start, offset_end, description.call)

		builder = glurg.resources.MeshBlobBuilder!
		extracted_description = builder\extract_attrib_call(description.call)
		extracted_buffer = builder\set_vertex_data(buffer.data\make_slice(buffer_offset, buffer_size))
		if extracted_description and extracted_buffer
			return glurg.resources.Mesh(builder\build!)

	make_sliced_model: (positions_mesh, normals_mesh, texture_coordinates_mesh, colors_mesh, offset_start, offset_end, call) =>
		model = glurg.resources.Model(positions_mesh)
		model.normals = normals_mesh
		model.texture_coordinates[1] = texture_coordinates_mesh
		model.colors[1] = colors_mesh

		index_format, index_size = switch call\get_argument_by_name("type")\query!.value_name
			when 'GL_UNSIGNED_BYTE' then 'unsigned_byte', 1
			when 'GL_UNSIGNED_SHORT' then 'unsigned_short', 2
			when 'GL_UNSIGNED_INT' then 'unsigned_integer', 4
			else error "unknown index type"

		vertex_array = @vertex_array_filter\get_current_vertex_array!
		element_array_buffer = @element_array_buffer_filter\get_buffer(vertex_array.element_array_buffer)

		index_data = element_array_buffer.data
		index_data_start = nil
		index_data_end = nil
		do
			offset = 0
			while offset < index_data.length
				value = switch index_format
					when 'unsigned_byte' then index_data\read_unsigned_byte(offset)
					when 'unsigned_short' then index_data\read_unsigned_short(offset)
					when 'unsigned_integer' then index_data\read_unsigned_integer(offset)
				if index_data_start == nil
					if value >= offset_start
						index_data_start = offset
				else
					if value <= offset_end
						index_data_end = offset + index_size
					else
						break

				offset += index_size

		glurg.common.Log\info('low',
			"index sub-region start: %d, index sub-region end: %d",
			index_data_start, index_data_end)

		index_data = index_data\get_mutable!
		do
			offset = index_data_start
			while offset < index_data_end
				switch index_format
					when 'unsigned_byte'
						value = index_data\read_unsigned_byte(offset) - offset_start
						index_data\write_unsigned_byte(offset, value)
					when 'unsigned_short'
						value = index_data\read_unsigned_short(offset) - offset_start
						index_data\write_unsigned_short(offset, value)
					when 'unsigned_integer'
						value = index_data\read_unsigned_integer(offset) - offset_start
						index_data\write_unsigned_integer(offset, value)

				offset += index_size

		index_length = (index_data_end - index_data_start)
		model\set_index_data(index_format, index_data\make_slice(index_data_start, index_length))

		return model

	get_texture_coords: (texture_meta, atlas_meta) =>
		atlas_slot = math.floor(texture_meta.x + 0.5)
		pixel_size = texture_meta.y
		atlas_slot_size = atlas_meta.x
		atlas_slot_pixel_size = atlas_meta.y
		atlas_pixel_size = atlas_meta.z

		y_slot = math.floor(atlas_slot / atlas_slot_size)
		x_slot = atlas_slot - atlas_slot_size * y_slot

		x = x_slot * 16
		y = y_slot * 16

		return x, y

	get_mesh_texture: (texture_meta) =>
		program = @program_filter\get_current_program!
		unit = program\get_uniform(@atlas_sampler)
		if unit
			atlas = @texture_atlas_filter\get_current_texture(unit)
			atlas_meta = program\get_uniform(@atlas_meta)

			if atlas and atlas_meta then
				glurg.common.Log\info('low', "getting texture from atlas %d...", atlas.name)
				x, y = @\get_texture_coords(texture_meta, atlas_meta)
				glurg.common.Log\info('low', "texture coordinates: (%f, %f)", x, y)
				sub = atlas\get_sub(x, y)
				if not sub then
					sub = atlas\get_sub(x - 32, y - 32)
				if sub then
					glurg.common.Log\info('low', "got texture")
				else
					glurg.common.Log\info('low', "didn't get texture")
				return sub

		return nil

	save_model: (call) =>
		if @\is_compatible!
			meshes = @\examine_meshes!
			glurg.common.Log\info('low', "examined meshes, got %d total", #meshes)
			group = RS3ModelGroup(call.index)
			for i = 1, #meshes
				local positions_mesh, normals_mesh, texture_coordinates_mesh
				do
					p = @attributes.position_binding
					n = @attributes.normal_binding
					t = @attributes.texture_coordinate_binding
					c = @attributes.color_binding
					start = meshes[i].start
					stop = meshes[i].stop
					glurg.common.Log\info('low', "mesh #{i} start: #{start}")
					glurg.common.Log\info('low', "mesh #{i} stop: #{stop}")
					glurg.common.Log\info('low', "aTextureMeta: (%f, %f, %f, %f)",
						meshes[i].meta.x, meshes[i].meta.y, meshes[i].meta.z, meshes[i].meta.w)
					positions_mesh = @\make_sliced_mesh(p, start, stop)
					normals_mesh = @\make_sliced_mesh(n, start, stop)
					texture_coordinates_mesh = @\make_sliced_mesh(t, start, stop)
					colors_mesh = @\make_sliced_mesh(c, start, stop)

				if positions_mesh and normals_mesh and texture_coordinates_mesh
					model = @\make_sliced_model(
						positions_mesh, normals_mesh, texture_coordinates_mesh, colors_mesh,
						meshes[i].start, meshes[i].stop, call)
					texture = @\get_mesh_texture(meshes[i].meta)
					glurg.common.Log\info('low', "extracted model")

					group\add_model(model, (texture or {}).texture)

			group\save_node(@output_directory)
		super\save_model(call)

return ExtractRS3Model

TextureAtlasFilter = require "heuristics.TextureAtlasFilter"

class RS3Model
	new: (model, texture) =>
		@model = model
		@texture = texture

class RS3ModelGroup
	new: (id) =>
		@id = id
		@models = {}

	add_model: (model, texture) =>
		table.insert(@models, RS3Model(model, texture))

	save_node: (directory) =>
		for i = 1, #@models
			filename = string.format("%s/%010d.%03d", directory, @id, i)
			@models[i].model\save_dae(filename .. ".dae")

			if @models[i].texture != nil
				@models[i].texture\save_png(filename .. ".png")

			glurg.common.Log\info('medium', "saved model #{@id}.#{i}")

class ExtractRS3Model extends glurg.heuristics.GL3StaticModelHeuristic
	new: (data) =>
		super(data)

		@texture_atlas_filter = TextureAtlasFilter!
		@attributes.atlas_binding = data["atlas"]
		@atlas_sampler = data["atlas-sampler"]
		@atlas_meta = data["atlas-meta"]

	get_filters: =>
		return @texture_atlas_filter, super\get_filters!

	is_compatible: =>
		program = @program_filter\get_current_program!
		atlas = program\has_attribute(@attributes.atlas_binding)

		return super\is_compatible! and atlas

	examine_meshes: =>
		atlas_mesh = @\make_mesh(@attributes.atlas_binding)

		meshes = {}
		p = atlas_mesh\get_value(1) -- previous
		p_start = 0
		index = 1
		while index < atlas_mesh.num_values
			c = atlas_mesh\get_value(index + 1) -- current
			d = 0.001
			dx = math.abs(p.x - c.x) > d
			dy = math.abs(p.y - c.y) > d
			dz = math.abs(p.z - c.z) > d
			dw = math.abs(p.w - c.w) > d
			if dx or dy or dz or dw
				table.insert(meshes, { start: p_start, stop: index, meta: p })
				p = c
				p_start = index
			index += 1
		table.insert(meshes, { start: p_start, stop: index - 1, meta: p})

		return meshes

	calculate_byte_offset_size: (attribute_index, offset_start, offset_end, call) =>
		-- See glurg.resources.MeshBlobBuilder.extract_attrib_call for specifics
		-- about how vertex descriptino is extracted
		vertex_array = @vertex_array_filter\get_current_vertex_array!
		description = vertex_array.descriptions[attribute_index]

		size = call\get_argument_by_name("size")\query!
		local num_components
		if size.value_name == 'GL_BGRA'
			num_components = 4
		else
			size = size.value\query!

		format = call\get_argument_by_name("type")\query!
		local component_size
		switch format.value_name
			when 'GL_FLOAT' then component_size = 4
			when 'GL_BYTE', 'GL_UNSIGNED_BYTE' then component_size = 1
			when 'GL_SHORT', 'GL_UNSIGNED_SHORT' then component_size = 2
			when 'GL_INT', 'GL_UNSIGNED_INT' then component_size = 4
			else error "unknown or unsupported data format"

		stride = call\get_argument_by_name("stride")\query!
		pointer = call\get_argument_by_name("pointer")\query!
		element_offset = 0
		local element_size
		if stride == 0
			element_size = num_components * component_size
			if type(pointer) == "number"
				element_offset = pointer
		else
			element_size = stride
			element_offset = 0

		byte_count = (offset_end - offset_start) * element_size
		byte_offset = offset_start * element_size + element_offset

		return byte_offset, byte_count

	make_sliced_mesh: (attribute_name, offset_start, offset_end) =>
		program = @program_filter\get_current_program!
		attribute_index = program\get_attribute(attribute_name)

		vertex_array = @vertex_array_filter\get_current_vertex_array!
		description = vertex_array.descriptions[attribute_index]

		buffer = @array_buffer_filter\get_buffer(description.array_buffer)
		buffer_offset, buffer_size = @\calculate_byte_offset_size(
			attribute_index, offset_start, offset_end, description.call)

		builder = glurg.resources.MeshBlobBuilder!
		extracted_description = builder\extract_attrib_call(description.call)
		extracted_buffer = builder\extract_buffer_call_slice(
			buffer.data, buffer_offset, buffer_size)
		if extracted_description and extracted_buffer
			return glurg.resources.Mesh(builder\build!)

	make_sliced_model: (positions_mesh, normals_mesh, texture_coordinates_mesh, offset_start, offset_end, call) =>
		model = glurg.resources.Model(positions_mesh)
		model.normals = normals_mesh
		model.texture_coordinates[1] = texture_coordinates_mesh

		index_format, index_size = switch call\get_argument_by_name("type")\query!.value_name
			when 'GL_UNSIGNED_BYTE' then 'unsigned_byte', 1
			when 'GL_UNSIGNED_SHORT' then 'unsigned_short', 2
			when 'GL_UNSIGNED_INT' then 'unsigned_integer', 4
			else error "unknown index type"

		vertex_array = @vertex_array_filter\get_current_vertex_array!
		element_array_buffer = @element_array_buffer_filter\get_buffer(vertex_array.element_array_buffer)

		index_data = element_array_buffer.data\get_argument_by_name("data")\query!
		index_data_start = nil
		index_data_end = nil
		do
			offset = 0
			while offset < index_data.length
				value = switch index_format
					when 'unsigned_byte' then index_data\read_unsigned_byte(offset)
					when 'unsigned_short' then index_data\read_unsigned_short(offset)
					when 'unsigned_integer' then index_data\read_unsigned_integer(offset)
				if index_data_start == nil
					if value >= offset_start
						index_data_start = offset
				else
					if value <= offset_end
						index_data_end = offset
					else
						break

				offset += index_size

		glurg.common.Log\info('low', "index start: %d, index end: %d", index_data_start, index_data_end)

		index_data = index_data\get_mutable!
		do
			offset = index_data_start
			while offset <= index_data_end
				switch index_format
					when 'unsigned_byte'
						value = index_data\read_unsigned_byte(offset) - offset_start
						index_data\write_unsigned_byte(offset, value)
					when 'unsigned_short'
						value = index_data\read_unsigned_short(offset) - offset_start
						index_data\write_unsigned_short(offset, value)
					when 'unsigned_integer'
						value = index_data\read_unsigned_integer(offset) - offset_start
						index_data\write_unsigned_integer(offset, value)

				offset += index_size

		index_count = (index_data_end - index_data_start)
		model\set_index_data(index_format, index_data\raw_slice(index_data_start), index_count)

		return model

	get_texture_coords: (texture_meta, atlas_meta) =>
		atlas_slot = math.floor(texture_meta.x + 0.5)
		pixel_size = texture_meta.y
		atlas_slot_size = atlas_meta.x
		atlas_slot_pixel_size = atlas_meta.y
		atlas_pixel_size = atlas_meta.z

		y_slot = math.floor(atlas_slot / atlas_slot_size)
		x_slot = atlas_slot - atlas_slot_size * y_slot

		x = x_slot * 16
		y = y_slot * 16

		return x, y

	get_mesh_texture: (texture_meta) =>
		program = @program_filter\get_current_program!
		unit = program\get_uniform(@atlas_sampler)
		if unit
			atlas = @texture_atlas_filter\get_current_texture(unit)
			atlas_meta = program\get_uniform(@atlas_meta)

			if atlas and atlas_meta then
				glurg.common.Log\info('low', "getting texture from atlas %d...", atlas.name)
				x, y = @\get_texture_coords(texture_meta, atlas_meta)
				glurg.common.Log\info('low', "texture coordinates: (%f, %f)", x, y)
				sub = atlas\get_sub(x, y)
				if not sub then
					sub = atlas\get_sub(x - 32, y - 32)
				if sub then
					glurg.common.Log\info('low', "got texture")
				else
					glurg.common.Log\info('low', "didn't get texture")
				return sub

		return nil

	save_model: (call) =>
		if @\is_compatible!
			meshes = @\examine_meshes!
			glurg.common.Log\info('low', "examined meshes, got %d total", #meshes)
			group = RS3ModelGroup(call.index)
			for i = 1, #meshes
				local positions_mesh, normals_mesh, texture_coordinates_mesh
				do
					p = @attributes.position_binding
					n = @attributes.normal_binding
					t = @attributes.texture_coordinate_binding
					start = meshes[i].start
					stop = meshes[i].stop
					glurg.common.Log\info('low', "mesh #{i} start: #{start}")
					glurg.common.Log\info('low', "mesh #{i} stop: #{stop}")
					glurg.common.Log\info('low', "aTextureMeta: (%f, %f, %f, %f)",
						meshes[i].meta.x, meshes[i].meta.y, meshes[i].meta.z, meshes[i].meta.w)
					positions_mesh = @\make_sliced_mesh(p, start, stop)
					normals_mesh = @\make_sliced_mesh(n, start, stop)
					texture_coordinates_mesh = @\make_sliced_mesh(t, start, stop)

				if positions_mesh and normals_mesh and texture_coordinates_mesh
					model = @\make_sliced_model(
						positions_mesh, normals_mesh, texture_coordinates_mesh,
						meshes[i].start, meshes[i].stop, call)
					texture = @\get_mesh_texture(meshes[i].meta)
					glurg.common.Log\info('low', "extracted model")

					group\add_model(model, (texture or {}).texture)

			group\save_node(@output_directory)

return ExtractRS3Model


GetMutatedPhrase return nil on no phrase available

[=[
Class: Lang
A static class used for dealing with translated messages.

Revisions:
	4.0.0 - Initial.
]=]
class ulx.Lang
	@CurrentLanguage: nil
	@BackupLanguage: nil
	@Phrases: {}
	@Mutators: {}

	parseAndVerifyLangTxt = (txt using nil) ->
		phrases = util.JSONToTable txt
		if not phrases
			return false, "Unable to parse JSON"

		types = tableshape.types
		dictionary = types.map_of types.string, types.string
		valid, err = dictionary\check_value phrases

		if not valid
			return false, err

		return phrases

	loadPhrasesForLang = (lang using nil) ->
		langFiles = ulx.Directory.GetFiles("locale", lang .. ".json", true)
		phraseAccum = {}

		if #langFiles == 0
			return false, "Language files do not exist"

		for langFile in *langFiles
			txt = ulx.File.ReadAllText langFile
			phrases, err = parseAndVerifyLangTxt txt
			if not phrases
				print err
				--log.warn "invalid..." -- TODO
				continue
			ulx.TableX.UnionByKey phraseAccum, phrases, true
		return phraseAccum

	init = () ->
		configLang = ulx.CoreConfig.Language
		@.SetLanguage configLang
		return
	ulx.RegisterInitCallback init

	[=[
	Function: AddMutator
	TODO
	]=]
	@AddMutator: (name, func using nil) ->
		@Mutators[name] = func

	[=[
	Function: SetLanguage
	TODO
	]=]
	@SetLanguage: (language, backupLanguage="english" using nil) ->
		ulx.UtilX.CheckArg "Language.SetLanguage", 1, "string", language
		ulx.UtilX.CheckArg "Language.SetLanguage", 2, "string", backupLanguage

		@CurrentLanguage = language\lower!
		@BackupLanguage = backupLanguage\lower!

		primaryPhrases, errPrimary = loadPhrasesForLang @CurrentLanguage

		if @BackupLanguage ~= @CurrentLanguage
			secondaryPhrases, errSecondary = loadPhrasesForLang @BackupLanguage
			if secondaryPhrases
				primaryPhrases = ulx.TableX.UnionByKey secondaryPhrases, primaryPhrases or {}, true

		if not primaryPhrases
			return false, errPrimary

		@Phrases = primaryPhrases
		return true

	[=[
	Function: GetPhrase
	TODO
	]=]
	@GetPhrase: (phraseName using nil) ->
		@Phrases[phraseName]

	[=[
	Function: GetMutatedPhrase
	TODO
	]=]
	@GetMutatedPhrase: (phraseName, data using nil) ->
		phrase = @.GetPhrase phraseName
		if not phrase
			return nil

		mutatedPhrase = phrase\gsub "{.-}", (placeholder using nil) ->
			processPlaceholder(placeholder, data)
		ulx.UtilX.Trim mutatedPhrase

	-- Receives everything between and including the brackets "{DAMAGE|NonZero:for %i damage}"
	processPlaceholder = (placeholder, data using nil) ->
		curPos = placeholder\find("|", 3, true) or #placeholder -- next pipe or the end bracket
		placeholderName = placeholder\sub(2, curPos-1)

		mutatorFnsAndArgs = {}
		while placeholder\sub(curPos, curPos) == "|"
			nextPos = placeholder\find("|", curPos+2, true) or #placeholder -- next pipe or the end bracket
			functionBlock = placeholder\sub(curPos+1, nextPos-1)
			fn, args = getFnAndArgs functionBlock, mutated
			table.insert mutatorFnsAndArgs, {fn, args}
			curPos = nextPos

		replacement = data[placeholderName]
		mutated = pipeMutators replacement, mutatorFnsAndArgs
		if type(mutated) == "table"
			mutated = listPipeMutators replacement, mutatorFnsAndArgs

		return mutated

	-- Receives a mutator function and all its arguments "NonZero:for %i damage"
	getFnAndArgs = (functionBlock, txt) ->
		curPos = functionBlock\find(":", 2, true) or #functionBlock+1 -- next colon of end of string
		functionName = functionBlock\sub(1, curPos-1)
		args = {}

		while functionBlock\sub(curPos, curPos) == ":"
			nextPos = functionBlock\find(":", curPos+1, true) or #functionBlock+1 -- next colon of end of string
			arg = functionBlock\sub(curPos+1, nextPos-1)
			table.insert args, arg
			curPos = nextPos

		return @Mutators[functionName], args

	-- Make a string from a list while piping each item
	listPipeMutators = (list, ... using nil) ->
		str = ""
		for i=1, #list-2
			str ..= pipeMutators(list[i], ...) .. ", "

		if #list > 1
			mutated = pipeMutators(list[#list-1], ...)
			conjuction = " " .. @.GetPhrase("AND") .. " "
			str ..= mutated .. conjuction

		str ..= pipeMutators(list[#list], ...)
		return str

	-- Send a string through each mutator function with the specified arguments
	pipeMutators = (replacement, mutatorFnsAndArgs using nil) ->
		for mutatorFnAndArgs in *mutatorFnsAndArgs
			fn = mutatorFnAndArgs[1]
			args = mutatorFnAndArgs[2]
			replacement = fn(replacement, unpack(args))
		return replacement

[=[
Class: Lang
A static class used for dealing with translated messages.

Revisions:
	4.0.0 - Initial.
]=]
class ulx.Lang
	@CurrentLanguage: nil
	@BackupLanguage: nil
	@Phrases: {}
	@Mutators: {}

	parseAndVerifyLangTxt = (txt using nil) ->
		phrases = util.JSONToTable txt
		if not phrases
			return false, "Unable to parse JSON"

		types = tableshape.types
		dictionary = types.map_of types.string, types.string
		valid, err = dictionary\check_value phrases

		if not valid
			return false, err

		return phrases

	loadPhrasesForLang = (lang using nil) ->
		langFiles = ulx.Directory.GetFiles("locale", lang .. ".json", true)
		phraseAccum = {}

		if #langFiles == 0
			return false, "Language files do not exist"

		for langFile in *langFiles
			txt = ulx.File.ReadAllText langFile
			phrases, err = parseAndVerifyLangTxt txt
			if not phrases
				print err
				--log.warn "invalid..." -- TODO
				continue
			ulx.TableX.UnionByKey phraseAccum, phrases, true
		return phraseAccum

	init = () ->
		configLang = ulx.CoreConfig.Language
		@.SetLanguage configLang
		return
	ulx.RegisterInitCallback init

	[=[
	Function: AddMutator
	TODO
	]=]
	@AddMutator: (name, func using nil) ->
		@Mutators[name] = func

	[=[
	Function: SetLanguage
	TODO
	]=]
	@SetLanguage: (language, backupLanguage="english" using nil) ->
		ulx.UtilX.CheckArg "Language.SetLanguage", 1, "string", language
		ulx.UtilX.CheckArg "Language.SetLanguage", 2, "string", backupLanguage

		@CurrentLanguage = language\lower!
		@BackupLanguage = backupLanguage\lower!

		primaryPhrases, errPrimary = loadPhrasesForLang @CurrentLanguage

		if @BackupLanguage ~= @CurrentLanguage
			secondaryPhrases, errSecondary = loadPhrasesForLang @BackupLanguage
			if secondaryPhrases
				primaryPhrases = ulx.TableX.UnionByKey secondaryPhrases, primaryPhrases or {}, true

		if not primaryPhrases
			return false, errPrimary

		@Phrases = primaryPhrases
		return true

	[=[
	Function: GetPhrase
	TODO
	]=]
	@GetPhrase: (phraseName using nil) ->
		@Phrases[phraseName]

	[=[
	Function: GetMutatedPhrase
	TODO
	]=]
	@GetMutatedPhrase: (phraseName, data using nil) ->
		phrase = @.GetPhrase phraseName
		mutatedPhrase = phrase\gsub "{.-}", (placeholder using nil) ->
			processPlaceholder(placeholder, data)
		ulx.UtilX.Trim mutatedPhrase

	-- Receives everything between and including the brackets "{DAMAGE|NonZero:for %i damage}"
	processPlaceholder = (placeholder, data using nil) ->
		curPos = placeholder\find("|", 3, true) or #placeholder -- next pipe or the end bracket
		placeholderName = placeholder\sub(2, curPos-1)

		mutatorFnsAndArgs = {}
		while placeholder\sub(curPos, curPos) == "|"
			nextPos = placeholder\find("|", curPos+2, true) or #placeholder -- next pipe or the end bracket
			functionBlock = placeholder\sub(curPos+1, nextPos-1)
			fn, args = getFnAndArgs functionBlock, mutated
			table.insert mutatorFnsAndArgs, {fn, args}
			curPos = nextPos

		replacement = data[placeholderName]
		mutated = pipeMutators replacement, mutatorFnsAndArgs
		if type(mutated) == "table"
			mutated = listPipeMutators replacement, mutatorFnsAndArgs

		return mutated

	-- Receives a mutator function and all its arguments "NonZero:for %i damage"
	getFnAndArgs = (functionBlock, txt) ->
		curPos = functionBlock\find(":", 2, true) or #functionBlock+1 -- next colon of end of string
		functionName = functionBlock\sub(1, curPos-1)
		args = {}

		while functionBlock\sub(curPos, curPos) == ":"
			nextPos = functionBlock\find(":", curPos+1, true) or #functionBlock+1 -- next colon of end of string
			arg = functionBlock\sub(curPos+1, nextPos-1)
			table.insert args, arg
			curPos = nextPos

		return @Mutators[functionName], args

	-- Make a string from a list while piping each item
	listPipeMutators = (list, ... using nil) ->
		str = ""
		for i=1, #list-2
			str ..= pipeMutators(list[i], ...) .. ", "

		if #list > 1
			mutated = pipeMutators(list[#list-1], ...)
			conjuction = " " .. @.GetPhrase("AND") .. " "
			str ..= mutated .. conjuction

		str ..= pipeMutators(list[#list], ...)
		return str

	-- Send a string through each mutator function with the specified arguments
	pipeMutators = (replacement, mutatorFnsAndArgs using nil) ->
		for mutatorFnAndArgs in *mutatorFnsAndArgs
			fn = mutatorFnAndArgs[1]
			args = mutatorFnAndArgs[2]
			replacement = fn(replacement, unpack(args))
		return replacement


Useless check

Useless check

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

PPM2.BODYGROUP_SKELETON = 0
PPM2.BODYGROUP_GENDER = 1
PPM2.BODYGROUP_HORN = 2
PPM2.BODYGROUP_WINGS = 3
PPM2.BODYGROUP_MANE_UPPER = 4
PPM2.BODYGROUP_MANE_LOWER = 5
PPM2.BODYGROUP_TAIL = 6
PPM2.BODYGROUP_CMARK = 7
PPM2.BODYGROUP_EYELASH = 8

class DefaultBodygroupController
    @AVALIABLE_CONTROLLERS = {}
    @MODELS = {'models/ppm/player_default_base.mdl', 'models/ppm/player_default_base_nj.mdl'}
    @__inherited: (child) =>
        child.MODELS_HASH = {mod, true for mod in *child.MODELS}
        @AVALIABLE_CONTROLLERS[mod] = child for mod in *child.MODELS
    @__inherited(@)

    @BODYGROUP_SKELETON = 0
    @BODYGROUP_GENDER = 1
    @BODYGROUP_HORN = 2
    @BODYGROUP_WINGS = 3
    @BODYGROUP_MANE_UPPER = 4
    @BODYGROUP_MANE_LOWER = 5
    @BODYGROUP_TAIL = 6
    @BODYGROUP_CMARK = 7
    @BODYGROUP_EYELASH = 8

    @NEXT_OBJ_ID = 0

    @COOLDOWN_TIME = 5
    @COOLDOWN_MAX_COUNT = 4

    new: (controller) =>
        @isValid = true
        @ent = controller.ent
        @entID = controller.entID
        @controller = controller
        @objID = @@NEXT_OBJ_ID
        @@NEXT_OBJ_ID += 1
        @SocksModelUpdateCooldown = 0
        @SocksModelUpdateCount = 0

    UpdateCooldowns: =>
        if CLIENT
            @SocksModelUpdateCooldown = 0
            @SocksModelUpdateCount = 0
        rTime = RealTime()
        if @SocksModelUpdateCooldown < rTime
            @SocksModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @SocksModelUpdateCount = 0

    __tostring: => "[#{@@__name}:#{@objID}|#{@ent}]"
    IsValid: => @isValid
    GetData: => @controller
    GetEntity: => @ent
    GetEntityID: => @entID
    GetDataID: => @entID

    @ATTACHMENT_EYES = 4
    @ATTACHMENT_EYES_NAME = 'eyes'

    CreateSocksModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @socksModel if IsValid(@socksModel)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isSocks and ent.manePlayer == @ent
                @socksModel = ent
                @GetData()\SetSocksModel(@socksModel)
                return ent

        @UpdateCooldowns()
        return NULL if @SocksModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @SocksModelUpdateCount += 1

        model = 'models/props_pony/ppm/cosmetics/ppm_socks.mdl'

        @socksModel = ents.Create('prop_dynamic') if SERVER
        @socksModel = ClientsideModel(model) if CLIENT
        with @socksModel
            .isPonyPropModel = true
            .isSocks = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@socksModel)
                @GetData()\SetSocksModel(@socksModel)
                @ent\SetNWEntity('PPM2.SocksModel', @socksModel) if IsValid(@ent)
        else
            @GetData()\SetSocksModel(@socksModel)

        return @socksModel
    CreateSocksModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateSocksModel() if not IsValid(@socksModel)
        @socksModel\SetParent(@ent) if IsValid(@ent)
        @socksModel\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        @GetData()\SetSocksModel(@socksModel)
        return @socksModel

    MergeModels: (targetEnt = NULL) =>
        return unless @isValid
        return unless IsValid(targetEnt)
        socks = @CreateSocksModelIfNotExists() if @GetData()\GetSocksAsModel()
        if IsValid(socks)
            socks\SetParent(targetEnt)
            socks\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER

    GetSocks: => @socksModel or NULL

    ApplyRace: =>
        return unless @isValid
        switch @GetData()\GetRace()
            when PPM2.RACE_EARTH
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_PEGASUS
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 0)
            when PPM2.RACE_UNICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 0)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_ALICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 0)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 0)

    ResetBodygroups: =>
        return unless @isValid
        for grp in *@ent\GetBodyGroups()
            @ent\SetBodygroup(grp.id, 0)
    SlowUpdate: =>
        @ent\SetBodygroup(@@BODYGROUP_MANE_UPPER, @GetData()\GetManeType())
        @ent\SetBodygroup(@@BODYGROUP_MANE_LOWER, @GetData()\GetManeTypeLower())
        @ent\SetBodygroup(@@BODYGROUP_TAIL, @GetData()\GetTailType())
        @ent\SetBodygroup(@@BODYGROUP_EYELASH, @GetData()\GetEyelashType())
        @ent\SetBodygroup(@@BODYGROUP_GENDER, @GetData()\GetGender())
        @ApplyRace()
        @CreateSocksModelIfNotExists() if @GetData()\GetSocksAsModel()
    ApplyBodygroups: =>
        return unless @isValid
        @ResetBodygroups()
        @SlowUpdate()

    Remove: =>
        @socksModel\Remove() if IsValid(@socksModel)
        @ResetBodygroups()
        @isValid = false

    @TAIL_BONE1 = 38
    @TAIL_BONE2 = 39
    @TAIL_BONE3 = 40
    DataChanges: (state) =>
        return unless @isValid
        switch state\GetKey()
            when 'ManeType'
                @ent\SetBodygroup(@@BODYGROUP_MANE_UPPER, @GetData()\GetManeType())
            when 'ManeTypeLower'
                @ent\SetBodygroup(@@BODYGROUP_MANE_LOWER, @GetData()\GetManeTypeLower())
            when 'TailType'
                @ent\SetBodygroup(@@BODYGROUP_TAIL, @GetData()\GetTailType())
            when 'TailSize'
                size = state\GetValue()
                vec = Vector(size, size, size)
                @ent\ManipulateBoneScale(@@TAIL_BONE1, vec)
                @ent\ManipulateBoneScale(@@TAIL_BONE2, vec)
                @ent\ManipulateBoneScale(@@TAIL_BONE3, vec)
            when 'EyelashType'
                @ent\SetBodygroup(@@BODYGROUP_EYELASH, @GetData()\GetEyelashType())
            when 'Gender'
                @ent\SetBodygroup(@@BODYGROUP_GENDER, @GetData()\GetGender())
            when 'SocksAsModel'
                if state\GetValue()
                    @CreateSocksModelIfNotExists()
                else
                    @socksModel\Remove() if IsValid(@socksModel)
            when 'Race'
                @ApplyRace()

class CPPMBodygroupController extends DefaultBodygroupController
    @MODELS = {'models/cppm/player_default_base.mdl', 'models/cppm/player_default_base_nj.mdl'}

    new: (...) => super(...)

    ApplyRace: =>
        switch @GetData()\GetRace()
            when PPM2.RACE_EARTH
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_PEGASUS
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 0)
            when PPM2.RACE_UNICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 0)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_ALICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 2)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 3)

class NewBodygroupController extends DefaultBodygroupController
    @MODELS = {'models/ppm/player_default_base_new.mdl'}

    @BODYGROUP_SKELETON = 0
    @BODYGROUP_GENDER = -1
    @BODYGROUP_HORN = 1
    @BODYGROUP_WINGS = 2

    __tostring: => "[#{@@__name}:#{@objID}|#{@GetData()}]"

    new: (...) =>
        super(...)
        @UpperManeModelUpdateCooldown = 0
        @UpperManeModelUpdateCount = 0
        @LowerManeModelUpdateCooldown = 0
        @LowerManeModelUpdateCount = 0
        @TailModelUpdateCooldown = 0
        @TailModelUpdateCount = 0

    UpdateCooldowns: =>
        super()
        if CLIENT
            @UpperManeModelUpdateCooldown = 0
            @UpperManeModelUpdateCount = 0
            @LowerManeModelUpdateCooldown = 0
            @LowerManeModelUpdateCount = 0
            @TailModelUpdateCooldown = 0
            @TailModelUpdateCount = 0
        rTime = RealTime()
        if @UpperManeModelUpdateCooldown < rTime
            @UpperManeModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @UpperManeModelUpdateCount = 0
        if @LowerManeModelUpdateCooldown < rTime
            @LowerManeModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @LowerManeModelUpdateCount = 0
        if @TailModelUpdateCooldown < rTime
            @TailModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @TailModelUpdateCount = 0

    CreateUpperManeModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @maneModelUP if IsValid(@maneModelUP)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.upperMane and ent.manePlayer == @ent
                @maneModelUP = ent
                @GetData()\SetUpperManeModel(@maneModelUP)
                return ent

        @UpdateCooldowns()
        return NULL if @UpperManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @UpperManeModelUpdateCount += 1

        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetupper#{modelID}.mdl"

        @maneModelUP = ents.Create('prop_dynamic') if SERVER
        @maneModelUP = ClientsideModel(model) if CLIENT
        with @maneModelUP
            .isPonyPropModel = true
            .upperMane = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetBodygroup(1, bodygroupID)
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@maneModelUP)
                @GetData()\SetUpperManeModel(@maneModelUP)
        else
            @GetData()\SetUpperManeModel(@maneModelUP)

        return @maneModelUP
    CreateLowerManeModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @maneModelLower if IsValid(@maneModelLower)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.lowerMane and ent.manePlayer == @ent
                @maneModelLower = ent
                @GetData()\SetLowerManeModel(@maneModelLower)
                return ent

        @UpdateCooldowns()
        return NULL if @LowerManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @LowerManeModelUpdateCount += 1

        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeLowerNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetlower#{modelID}.mdl"

        @maneModelLower = ents.Create('prop_dynamic') if SERVER
        @maneModelLower = ClientsideModel(model) if CLIENT
        with @maneModelLower
            .isPonyPropModel = true
            .lowerMane = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetBodygroup(1, bodygroupID)
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@maneModelLower)
                @GetData()\SetLowerManeModel(@maneModelLower)
        else
            @GetData()\SetLowerManeModel(@maneModelLower)

        return @maneModelLower
    CreateTailModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @tailModel if IsValid(@tailModel)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isTail and ent.manePlayer == @ent
                @tailModel = ent
                @GetData()\SetTailModel(@tailModel)
                return ent

        @UpdateCooldowns()
        return NULL if @TailModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @TailModelUpdateCount += 1

        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetTailTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_tailset#{modelID}.mdl"

        @tailModel = ents.Create('prop_dynamic') if SERVER
        @tailModel = ClientsideModel(model) if CLIENT
        with @tailModel
            .isPonyPropModel = true
            .isTail = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetBodygroup(1, bodygroupID)
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@tailModel)
                @GetData()\SetTailModel(@tailModel)
                @ent\SetNWEntity('PPM2.TailModel', @tailModel) if IsValid(@ent)
        else
            @GetData()\SetTailModel(@tailModel)

        return @tailModel

    CreateUpperManeModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateUpperManeModel() if not IsValid(@maneModelUP)
        @GetData()\SetUpperManeModel(@maneModelUP) if IsValid(@maneModelUP)
        return @maneModelUP
    CreateLowerManeModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateLowerManeModel() if not IsValid(@maneModelLower)
        @GetData()\SetLowerManeModel(@maneModelLower) if IsValid(@maneModelLower)
        return @maneModelLower
    CreateTailModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateTailModel() if not IsValid(@tailModel)
        @GetData()\SetTailModel(@tailModel) if IsValid(@tailModel)
        return @tailModel

    GetUpperMane: => @maneModelUP or NULL
    GetLowerMane: => @maneModelLower or NULL
    GetTail: => @tailModel or NULL

    MergeModels: (targetEnt = NULL) =>
        return unless @isValid
        super(targetEnt)
        return unless IsValid(targetEnt)
        maneUpper = @CreateUpperManeModelIfNotExists()
        maneLower = @CreateLowerManeModelIfNotExists()
        tail = @CreateTailModelIfNotExists()
        if IsValid(maneUpper)
            maneUpper\SetParent(targetEnt)
            maneUpper\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        if IsValid(maneLower)
            maneLower\SetParent(targetEnt)
            maneLower\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        if IsValid(tail)
            tail\SetParent(targetEnt)
            tail\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER

    UpdateUpperMane: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateUpperManeModelIfNotExists()
        return NULL if not IsValid(@maneModelUP)
        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetupper#{modelID}.mdl"
        @maneModelUP\SetModel(model) if model ~= @maneModelUP\GetModel()
        @maneModelUP\SetBodygroup(1, bodygroupID) if @maneModelUP\GetBodygroup(1) ~= bodygroupID
        @maneModelUP\SetParent(@ent) if @maneModelUP\GetParent() ~= @ent and IsValid(@ent)
        @GetData()\SetUpperManeModel(@maneModelUP)
        return @maneModelUP
    UpdateLowerMane: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateLowerManeModelIfNotExists()
        return NULL if not IsValid(@maneModelLower)
        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeLowerNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetlower#{modelID}.mdl"
        @maneModelLower\SetModel(model) if model ~= @maneModelLower\GetModel()
        @maneModelLower\SetBodygroup(1, bodygroupID) if @maneModelLower\GetBodygroup(1) ~= bodygroupID
        @maneModelLower\SetParent(@ent) if IsValid(@ent)
        @GetData()\SetLowerManeModel(@maneModelLower)
        return @maneModelLower
    UpdateTailModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateTailModelIfNotExists()
        return NULL if not IsValid(@tailModel)
        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetTailTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_tailset#{modelID}.mdl"
        @tailModel\SetModel(model) if model ~= @tailModel\GetModel()
        @tailModel\SetBodygroup(1, bodygroupID) if @tailModel\GetBodygroup(1) ~= bodygroupID
        @tailModel\SetModelScale(@GetData()\GetTailSize())
        @tailModel\SetParent(@ent) if IsValid(@ent)
        @GetData()\SetTailModel(@tailModel)
        return @tailModel

    @FLEX_ID_EYELASHES = 16
    @FLEX_ID_MALE = 25
    @FLEX_ID_BAT_PONY_EARS = 28
    @FLEX_ID_FANGS = 31
    @FLEX_ID_CLAW_TEETH = 30

    ResetBodygroups: =>
        return unless IsValid(@ent)
        @ent\SetFlexWeight(@@FLEX_ID_EYELASHES, 0)
        @ent\SetFlexWeight(@@FLEX_ID_MALE, 0)
        @ent\SetFlexWeight(@@FLEX_ID_BAT_PONY_EARS, 0)
        @ent\SetFlexWeight(@@FLEX_ID_FANGS, 0)
        @ent\SetFlexWeight(@@FLEX_ID_CLAW_TEETH, 0)
        super()

    SlowUpdate: =>
        @ent\SetFlexWeight(@@FLEX_ID_EYELASHES,     @GetData()\GetEyelashType() == PPM2.EYELASHES_NONE and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_MALE,          @GetData()\GetGender() == PPM2.GENDER_MALE and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_BAT_PONY_EARS, @GetData()\GetBatPonyEars() and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_FANGS,         @GetData()\GetFangs() and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_CLAW_TEETH,    @GetData()\GetClawTeeth() and 1 or 0)
        @ApplyRace()
        @UpdateUpperMane()
        @UpdateLowerMane()
        @UpdateTailModel()
        @CreateSocksModelIfNotExists() if @GetData()\GetSocksAsModel()
    ApplyBodygroups: =>
        return unless @isValid
        @ResetBodygroups()
        @SlowUpdate()

    DataChanges: (state) =>
        return unless @isValid
        switch state\GetKey()
            when 'EyelashType'
                @ent\SetFlexWeight(@@FLEX_ID_EYELASHES, @GetData()\GetEyelashType() == PPM2.EYELASHES_NONE and 1 or 0)
            when 'Gender'
                @ent\SetFlexWeight(@@FLEX_ID_MALE, @GetData()\GetGender() == PPM2.GENDER_MALE and 1 or 0)
            when 'BatPonyEars'
                @ent\SetFlexWeight(@@FLEX_ID_BAT_PONY_EARS, @GetData()\GetBatPonyEars() and 1 or 0)
            when 'Fangs'
                @ent\SetFlexWeight(@@FLEX_ID_FANGS, @GetData()\GetFangs() and 1 or 0)
            when 'ClawTeeth'
                @ent\SetFlexWeight(@@FLEX_ID_CLAW_TEETH, @GetData()\GetClawTeeth() and 1 or 0)
            when 'ManeTypeNew'
                @UpdateUpperMane()
            when 'ManeTypeLowerNew'
                @UpdateLowerMane()
            when 'TailTypeNew'
                @UpdateTailModel()
            when 'TailSize'
                @UpdateTailModel()
            when 'Race'
                @ApplyRace()
            when 'SocksAsModel'
                if state\GetValue()
                    @CreateSocksModelIfNotExists()
                else
                    @socksModel\Remove() if IsValid(@socksModel)
    Remove: =>
        @maneModelUP\Remove() if IsValid(@maneModelUP)
        @maneModelLower\Remove() if IsValid(@maneModelLower)
        @tailModel\Remove() if IsValid(@tailModel)
        super()

PPM2.CPPMBodygroupController = CPPMBodygroupController
PPM2.DefaultBodygroupController = DefaultBodygroupController

PPM2.GetBodygroupController = (model = 'models/ppm/player_default_base.mdl') -> DefaultBodygroupController.AVALIABLE_CONTROLLERS[model\lower()] or DefaultBodygroupController

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

PPM2.BODYGROUP_SKELETON = 0
PPM2.BODYGROUP_GENDER = 1
PPM2.BODYGROUP_HORN = 2
PPM2.BODYGROUP_WINGS = 3
PPM2.BODYGROUP_MANE_UPPER = 4
PPM2.BODYGROUP_MANE_LOWER = 5
PPM2.BODYGROUP_TAIL = 6
PPM2.BODYGROUP_CMARK = 7
PPM2.BODYGROUP_EYELASH = 8

class DefaultBodygroupController
    @AVALIABLE_CONTROLLERS = {}
    @MODELS = {'models/ppm/player_default_base.mdl', 'models/ppm/player_default_base_nj.mdl'}
    @__inherited: (child) =>
        child.MODELS_HASH = {mod, true for mod in *child.MODELS}
        @AVALIABLE_CONTROLLERS[mod] = child for mod in *child.MODELS
    @__inherited(@)

    @BODYGROUP_SKELETON = 0
    @BODYGROUP_GENDER = 1
    @BODYGROUP_HORN = 2
    @BODYGROUP_WINGS = 3
    @BODYGROUP_MANE_UPPER = 4
    @BODYGROUP_MANE_LOWER = 5
    @BODYGROUP_TAIL = 6
    @BODYGROUP_CMARK = 7
    @BODYGROUP_EYELASH = 8

    @NEXT_OBJ_ID = 0

    @COOLDOWN_TIME = 5
    @COOLDOWN_MAX_COUNT = 4

    new: (controller) =>
        @isValid = true
        @ent = controller.ent
        @entID = controller.entID
        @controller = controller
        @objID = @@NEXT_OBJ_ID
        @@NEXT_OBJ_ID += 1
        @SocksModelUpdateCooldown = 0
        @SocksModelUpdateCount = 0

    UpdateCooldowns: =>
        if CLIENT
            @SocksModelUpdateCooldown = 0
            @SocksModelUpdateCount = 0
        rTime = RealTime()
        if @SocksModelUpdateCooldown < rTime
            @SocksModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @SocksModelUpdateCount = 0

    __tostring: => "[#{@@__name}:#{@objID}|#{@ent}]"
    IsValid: => @isValid
    GetData: => @controller
    GetEntity: => @ent
    GetEntityID: => @entID
    GetDataID: => @entID

    @ATTACHMENT_EYES = 4
    @ATTACHMENT_EYES_NAME = 'eyes'

    CreateSocksModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @socksModel if IsValid(@socksModel)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isSocks and ent.manePlayer == @ent
                @socksModel = ent
                @GetData()\SetSocksModel(@socksModel)
                return ent

        @UpdateCooldowns()
        return NULL if @SocksModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @SocksModelUpdateCount += 1

        model = 'models/props_pony/ppm/cosmetics/ppm_socks.mdl'

        return if CLIENT and @GetData()\IsGoingToNetwork()
        @socksModel = ents.Create('prop_dynamic') if SERVER
        @socksModel = ClientsideModel(model) if CLIENT
        with @socksModel
            .isPonyPropModel = true
            .isSocks = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@socksModel)
                @GetData()\SetSocksModel(@socksModel)
                @ent\SetNWEntity('PPM2.SocksModel', @socksModel) if IsValid(@ent)
        else
            @GetData()\SetSocksModel(@socksModel)

        return @socksModel
    CreateSocksModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateSocksModel() if not IsValid(@socksModel)
        @socksModel\SetParent(@ent) if IsValid(@ent)
        @socksModel\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        @GetData()\SetSocksModel(@socksModel)
        return @socksModel

    MergeModels: (targetEnt = NULL) =>
        return unless @isValid
        return unless IsValid(targetEnt)
        socks = @CreateSocksModelIfNotExists() if @GetData()\GetSocksAsModel()
        if IsValid(socks)
            socks\SetParent(targetEnt)
            socks\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER

    GetSocks: => @socksModel or NULL

    ApplyRace: =>
        return unless @isValid
        switch @GetData()\GetRace()
            when PPM2.RACE_EARTH
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_PEGASUS
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 0)
            when PPM2.RACE_UNICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 0)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_ALICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 0)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 0)

    ResetBodygroups: =>
        return unless @isValid
        for grp in *@ent\GetBodyGroups()
            @ent\SetBodygroup(grp.id, 0)
    SlowUpdate: =>
        @ent\SetBodygroup(@@BODYGROUP_MANE_UPPER, @GetData()\GetManeType())
        @ent\SetBodygroup(@@BODYGROUP_MANE_LOWER, @GetData()\GetManeTypeLower())
        @ent\SetBodygroup(@@BODYGROUP_TAIL, @GetData()\GetTailType())
        @ent\SetBodygroup(@@BODYGROUP_EYELASH, @GetData()\GetEyelashType())
        @ent\SetBodygroup(@@BODYGROUP_GENDER, @GetData()\GetGender())
        @ApplyRace()
        @CreateSocksModelIfNotExists() if @GetData()\GetSocksAsModel()
    ApplyBodygroups: =>
        return unless @isValid
        @ResetBodygroups()
        @SlowUpdate()

    Remove: =>
        @socksModel\Remove() if IsValid(@socksModel)
        @ResetBodygroups()
        @isValid = false

    @TAIL_BONE1 = 38
    @TAIL_BONE2 = 39
    @TAIL_BONE3 = 40
    DataChanges: (state) =>
        return unless @isValid
        switch state\GetKey()
            when 'ManeType'
                @ent\SetBodygroup(@@BODYGROUP_MANE_UPPER, @GetData()\GetManeType())
            when 'ManeTypeLower'
                @ent\SetBodygroup(@@BODYGROUP_MANE_LOWER, @GetData()\GetManeTypeLower())
            when 'TailType'
                @ent\SetBodygroup(@@BODYGROUP_TAIL, @GetData()\GetTailType())
            when 'TailSize'
                size = state\GetValue()
                vec = Vector(size, size, size)
                @ent\ManipulateBoneScale(@@TAIL_BONE1, vec)
                @ent\ManipulateBoneScale(@@TAIL_BONE2, vec)
                @ent\ManipulateBoneScale(@@TAIL_BONE3, vec)
            when 'EyelashType'
                @ent\SetBodygroup(@@BODYGROUP_EYELASH, @GetData()\GetEyelashType())
            when 'Gender'
                @ent\SetBodygroup(@@BODYGROUP_GENDER, @GetData()\GetGender())
            when 'SocksAsModel'
                if state\GetValue()
                    @CreateSocksModelIfNotExists()
                else
                    @socksModel\Remove() if IsValid(@socksModel)
            when 'Race'
                @ApplyRace()

class CPPMBodygroupController extends DefaultBodygroupController
    @MODELS = {'models/cppm/player_default_base.mdl', 'models/cppm/player_default_base_nj.mdl'}

    new: (...) => super(...)

    ApplyRace: =>
        switch @GetData()\GetRace()
            when PPM2.RACE_EARTH
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_PEGASUS
                @ent\SetBodygroup(@@BODYGROUP_HORN, 1)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 0)
            when PPM2.RACE_UNICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 0)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 1)
            when PPM2.RACE_ALICORN
                @ent\SetBodygroup(@@BODYGROUP_HORN, 2)
                @ent\SetBodygroup(@@BODYGROUP_WINGS, 3)

class NewBodygroupController extends DefaultBodygroupController
    @MODELS = {'models/ppm/player_default_base_new.mdl'}

    @BODYGROUP_SKELETON = 0
    @BODYGROUP_GENDER = -1
    @BODYGROUP_HORN = 1
    @BODYGROUP_WINGS = 2

    __tostring: => "[#{@@__name}:#{@objID}|#{@GetData()}]"

    new: (...) =>
        super(...)
        @UpperManeModelUpdateCooldown = 0
        @UpperManeModelUpdateCount = 0
        @LowerManeModelUpdateCooldown = 0
        @LowerManeModelUpdateCount = 0
        @TailModelUpdateCooldown = 0
        @TailModelUpdateCount = 0

    UpdateCooldowns: =>
        super()
        if CLIENT
            @UpperManeModelUpdateCooldown = 0
            @UpperManeModelUpdateCount = 0
            @LowerManeModelUpdateCooldown = 0
            @LowerManeModelUpdateCount = 0
            @TailModelUpdateCooldown = 0
            @TailModelUpdateCount = 0
        rTime = RealTime()
        if @UpperManeModelUpdateCooldown < rTime
            @UpperManeModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @UpperManeModelUpdateCount = 0
        if @LowerManeModelUpdateCooldown < rTime
            @LowerManeModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @LowerManeModelUpdateCount = 0
        if @TailModelUpdateCooldown < rTime
            @TailModelUpdateCooldown = rTime + @@COOLDOWN_TIME
            @TailModelUpdateCount = 0

    CreateUpperManeModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @maneModelUP if IsValid(@maneModelUP)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.upperMane and ent.manePlayer == @ent
                @maneModelUP = ent
                @GetData()\SetUpperManeModel(@maneModelUP)
                return ent

        @UpdateCooldowns()
        return NULL if @UpperManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @UpperManeModelUpdateCount += 1

        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetupper#{modelID}.mdl"

        return if CLIENT and @GetData()\IsGoingToNetwork()
        @maneModelUP = ents.Create('prop_dynamic') if SERVER
        @maneModelUP = ClientsideModel(model) if CLIENT
        with @maneModelUP
            .isPonyPropModel = true
            .upperMane = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetBodygroup(1, bodygroupID)
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@maneModelUP)
                @GetData()\SetUpperManeModel(@maneModelUP)
        else
            @GetData()\SetUpperManeModel(@maneModelUP)

        return @maneModelUP
    CreateLowerManeModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @maneModelLower if IsValid(@maneModelLower)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.lowerMane and ent.manePlayer == @ent
                @maneModelLower = ent
                @GetData()\SetLowerManeModel(@maneModelLower)
                return ent

        @UpdateCooldowns()
        return NULL if @LowerManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @LowerManeModelUpdateCount += 1

        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeLowerNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetlower#{modelID}.mdl"

        return if CLIENT and @GetData()\IsGoingToNetwork()
        @maneModelLower = ents.Create('prop_dynamic') if SERVER
        @maneModelLower = ClientsideModel(model) if CLIENT
        with @maneModelLower
            .isPonyPropModel = true
            .lowerMane = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetBodygroup(1, bodygroupID)
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@maneModelLower)
                @GetData()\SetLowerManeModel(@maneModelLower)
        else
            @GetData()\SetLowerManeModel(@maneModelLower)

        return @maneModelLower
    CreateTailModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @tailModel if IsValid(@tailModel)
        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isTail and ent.manePlayer == @ent
                @tailModel = ent
                @GetData()\SetTailModel(@tailModel)
                return ent

        @UpdateCooldowns()
        return NULL if @TailModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @TailModelUpdateCount += 1

        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetTailTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_tailset#{modelID}.mdl"

        return if CLIENT and @GetData()\IsGoingToNetwork()
        @tailModel = ents.Create('prop_dynamic') if SERVER
        @tailModel = ClientsideModel(model) if CLIENT
        with @tailModel
            .isPonyPropModel = true
            .isTail = true
            .manePlayer = @ent
            \SetModel(model)
            \SetPos(@ent\EyePos())
            \Spawn()
            \Activate()
            \SetBodygroup(1, bodygroupID)
            \SetParent(@ent)
            \Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
            \AddEffects(EF_BONEMERGE)

        if SERVER
            timer.Simple .5, ->
                return unless @isValid
                return unless IsValid(@tailModel)
                @GetData()\SetTailModel(@tailModel)
                @ent\SetNWEntity('PPM2.TailModel', @tailModel) if IsValid(@ent)
        else
            @GetData()\SetTailModel(@tailModel)

        return @tailModel

    CreateUpperManeModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateUpperManeModel() if not IsValid(@maneModelUP)
        @GetData()\SetUpperManeModel(@maneModelUP) if IsValid(@maneModelUP)
        return @maneModelUP
    CreateLowerManeModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateLowerManeModel() if not IsValid(@maneModelLower)
        @GetData()\SetLowerManeModel(@maneModelLower) if IsValid(@maneModelLower)
        return @maneModelLower
    CreateTailModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateTailModel() if not IsValid(@tailModel)
        @GetData()\SetTailModel(@tailModel) if IsValid(@tailModel)
        return @tailModel

    GetUpperMane: => @maneModelUP or NULL
    GetLowerMane: => @maneModelLower or NULL
    GetTail: => @tailModel or NULL

    MergeModels: (targetEnt = NULL) =>
        return unless @isValid
        super(targetEnt)
        return unless IsValid(targetEnt)
        maneUpper = @CreateUpperManeModelIfNotExists()
        maneLower = @CreateLowerManeModelIfNotExists()
        tail = @CreateTailModelIfNotExists()
        if IsValid(maneUpper)
            maneUpper\SetParent(targetEnt)
            maneUpper\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        if IsValid(maneLower)
            maneLower\SetParent(targetEnt)
            maneLower\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        if IsValid(tail)
            tail\SetParent(targetEnt)
            tail\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER

    UpdateUpperMane: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateUpperManeModelIfNotExists()
        return NULL if not IsValid(@maneModelUP)
        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetupper#{modelID}.mdl"
        @maneModelUP\SetModel(model) if model ~= @maneModelUP\GetModel()
        @maneModelUP\SetBodygroup(1, bodygroupID) if @maneModelUP\GetBodygroup(1) ~= bodygroupID
        @maneModelUP\SetParent(@ent) if @maneModelUP\GetParent() ~= @ent and IsValid(@ent)
        @GetData()\SetUpperManeModel(@maneModelUP)
        return @maneModelUP
    UpdateLowerMane: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateLowerManeModelIfNotExists()
        return NULL if not IsValid(@maneModelLower)
        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetManeTypeLowerNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_manesetlower#{modelID}.mdl"
        @maneModelLower\SetModel(model) if model ~= @maneModelLower\GetModel()
        @maneModelLower\SetBodygroup(1, bodygroupID) if @maneModelLower\GetBodygroup(1) ~= bodygroupID
        @maneModelLower\SetParent(@ent) if IsValid(@ent)
        @GetData()\SetLowerManeModel(@maneModelLower)
        return @maneModelLower
    UpdateTailModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateTailModelIfNotExists()
        return NULL if not IsValid(@tailModel)
        modelID, bodygroupID = PPM2.TransformNewModelID(@GetData()\GetTailTypeNew())
        modelID = "0" .. modelID if modelID < 10
        model = "models/ppm/hair/ppm_tailset#{modelID}.mdl"
        @tailModel\SetModel(model) if model ~= @tailModel\GetModel()
        @tailModel\SetBodygroup(1, bodygroupID) if @tailModel\GetBodygroup(1) ~= bodygroupID
        @tailModel\SetModelScale(@GetData()\GetTailSize())
        @tailModel\SetParent(@ent) if IsValid(@ent)
        @GetData()\SetTailModel(@tailModel)
        return @tailModel

    @FLEX_ID_EYELASHES = 16
    @FLEX_ID_MALE = 25
    @FLEX_ID_BAT_PONY_EARS = 28
    @FLEX_ID_FANGS = 31
    @FLEX_ID_CLAW_TEETH = 30

    ResetBodygroups: =>
        return unless IsValid(@ent)
        @ent\SetFlexWeight(@@FLEX_ID_EYELASHES, 0)
        @ent\SetFlexWeight(@@FLEX_ID_MALE, 0)
        @ent\SetFlexWeight(@@FLEX_ID_BAT_PONY_EARS, 0)
        @ent\SetFlexWeight(@@FLEX_ID_FANGS, 0)
        @ent\SetFlexWeight(@@FLEX_ID_CLAW_TEETH, 0)
        super()

    SlowUpdate: =>
        @ent\SetFlexWeight(@@FLEX_ID_EYELASHES,     @GetData()\GetEyelashType() == PPM2.EYELASHES_NONE and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_MALE,          @GetData()\GetGender() == PPM2.GENDER_MALE and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_BAT_PONY_EARS, @GetData()\GetBatPonyEars() and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_FANGS,         @GetData()\GetFangs() and 1 or 0)
        @ent\SetFlexWeight(@@FLEX_ID_CLAW_TEETH,    @GetData()\GetClawTeeth() and 1 or 0)
        @ApplyRace()
        @UpdateUpperMane()
        @UpdateLowerMane()
        @UpdateTailModel()
        @CreateSocksModelIfNotExists() if @GetData()\GetSocksAsModel()
    ApplyBodygroups: =>
        return unless @isValid
        @ResetBodygroups()
        @SlowUpdate()

    DataChanges: (state) =>
        return unless @isValid
        switch state\GetKey()
            when 'EyelashType'
                @ent\SetFlexWeight(@@FLEX_ID_EYELASHES, @GetData()\GetEyelashType() == PPM2.EYELASHES_NONE and 1 or 0)
            when 'Gender'
                @ent\SetFlexWeight(@@FLEX_ID_MALE, @GetData()\GetGender() == PPM2.GENDER_MALE and 1 or 0)
            when 'BatPonyEars'
                @ent\SetFlexWeight(@@FLEX_ID_BAT_PONY_EARS, @GetData()\GetBatPonyEars() and 1 or 0)
            when 'Fangs'
                @ent\SetFlexWeight(@@FLEX_ID_FANGS, @GetData()\GetFangs() and 1 or 0)
            when 'ClawTeeth'
                @ent\SetFlexWeight(@@FLEX_ID_CLAW_TEETH, @GetData()\GetClawTeeth() and 1 or 0)
            when 'ManeTypeNew'
                @UpdateUpperMane()
            when 'ManeTypeLowerNew'
                @UpdateLowerMane()
            when 'TailTypeNew'
                @UpdateTailModel()
            when 'TailSize'
                @UpdateTailModel()
            when 'Race'
                @ApplyRace()
            when 'SocksAsModel'
                if state\GetValue()
                    @CreateSocksModelIfNotExists()
                else
                    @socksModel\Remove() if IsValid(@socksModel)
    Remove: =>
        @maneModelUP\Remove() if IsValid(@maneModelUP)
        @maneModelLower\Remove() if IsValid(@maneModelLower)
        @tailModel\Remove() if IsValid(@tailModel)
        super()

PPM2.CPPMBodygroupController = CPPMBodygroupController
PPM2.DefaultBodygroupController = DefaultBodygroupController

PPM2.GetBodygroupController = (model = 'models/ppm/player_default_base.mdl') -> DefaultBodygroupController.AVALIABLE_CONTROLLERS[model\lower()] or DefaultBodygroupController