Some optimizations

Some optimizations

--
-- 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)
        @UpdateCooldowns()
        return NULL if @SocksModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @SocksModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isSocks and ent.manePlayer == @ent
                @socksModel = ent
                @GetData()\SetSocksModel(@socksModel)
                return ent

        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\GetPos())
            \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)
        @UpdateCooldowns()
        return NULL if @UpperManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @UpperManeModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.upperMane and ent.manePlayer == @ent
                @maneModelUP = ent
                @GetData()\SetUpperManeModel(@maneModelUP)
                return ent

        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\GetPos())
            \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)
        @UpdateCooldowns()
        return NULL if @LowerManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @LowerManeModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.lowerMane and ent.manePlayer == @ent
                @maneModelLower = ent
                @GetData()\SetLowerManeModel(@maneModelLower)
                @ent\SetNWEntity('PPM2.LowerManeModel', @maneModelLower) if IsValid(@ent) and SERVER
                return ent

        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\GetPos())
            \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)
        @UpdateCooldowns()
        return NULL if @TailModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @TailModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isTail and ent.manePlayer == @ent
                @tailModel = ent
                @GetData()\SetTailModel(@tailModel)
                @ent\SetNWEntity('PPM2.TailModel', @tailModel) if IsValid(@ent) and SERVER
                return ent

        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\GetPos())
            \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)
        @UpdateCooldowns()
        return NULL if @SocksModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @SocksModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isSocks and ent.manePlayer == @ent
                @socksModel = ent
                @GetData()\SetSocksModel(@socksModel)
                @ent\SetNWEntity('PPM2.SocksModel', @socksModel) if IsValid(@ent) and SERVER
                return ent

        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\GetPos())
            \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)
        @ent\SetNWEntity('PPM2.SocksModel', @socksModel) if IsValid(@ent) and SERVER
        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)
        @UpdateCooldowns()
        return NULL if @UpperManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @UpperManeModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.upperMane and ent.manePlayer == @ent
                @maneModelUP = ent
                @GetData()\SetUpperManeModel(@maneModelUP)
                @ent\SetNWEntity('PPM2.UpperManeModel', @maneModelUP) if IsValid(@ent) and SERVER
                return ent

        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\GetPos())
            \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)
                @ent\SetNWEntity('PPM2.UpperManeModel', @maneModelUP) if IsValid(@ent)
        else
            @GetData()\SetUpperManeModel(@maneModelUP)

        return @maneModelUP
    CreateLowerManeModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @maneModelLower if IsValid(@maneModelLower)
        @UpdateCooldowns()
        return NULL if @LowerManeModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @LowerManeModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.lowerMane and ent.manePlayer == @ent
                @maneModelLower = ent
                @GetData()\SetLowerManeModel(@maneModelLower)
                @ent\SetNWEntity('PPM2.LowerManeModel', @maneModelLower) if IsValid(@ent) and SERVER
                return ent

        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\GetPos())
            \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)
                @ent\SetNWEntity('PPM2.LowerManeModel', @maneModelLower) if IsValid(@ent)
        else
            @GetData()\SetLowerManeModel(@maneModelLower)

        return @maneModelLower
    CreateTailModel: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        return @tailModel if IsValid(@tailModel)
        @UpdateCooldowns()
        return NULL if @TailModelUpdateCount > @@COOLDOWN_MAX_COUNT
        @TailModelUpdateCount += 1

        for ent in *ents.GetAll()
            if ent.isPonyPropModel and ent.isTail and ent.manePlayer == @ent
                @tailModel = ent
                @GetData()\SetTailModel(@tailModel)
                @ent\SetNWEntity('PPM2.TailModel', @tailModel) if IsValid(@ent) and SERVER
                return ent

        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\GetPos())
            \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)
        if IsValid(@maneModelUP)
            @GetData()\SetUpperManeModel(@maneModelUP)
            @ent\SetNWEntity('PPM2.UpperManeModel', @maneModelUP) if IsValid(@ent) and SERVER
        return @maneModelUP
    CreateLowerManeModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateLowerManeModel() if not IsValid(@maneModelLower)
        if IsValid(@maneModelLower)
            @GetData()\SetLowerManeModel(@maneModelLower)
            @ent\SetNWEntity('PPM2.LowerManeModel', @maneModelLower) if IsValid(@ent) and SERVER
        return @maneModelLower
    CreateTailModelIfNotExists: =>
        return NULL unless @isValid
        return NULL if CLIENT and @GetData()\IsGoingToNetwork()
        @CreateTailModel() if not IsValid(@tailModel)
        if IsValid(@tailModel)
            @GetData()\SetTailModel(@tailModel)
            @ent\SetNWEntity('PPM2.TailModel', @tailModel) if IsValid(@ent) and SERVER
        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)
        @maneModelUP\SetBodygroup(1, bodygroupID)
        @maneModelUP\SetParent(@ent) if IsValid(@ent)
        @maneModelUP\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        @GetData()\SetUpperManeModel(@maneModelUP)
        @ent\SetNWEntity('PPM2.UpperManeModel', @maneModelUP) if IsValid(@ent) and SERVER
        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)
        @maneModelLower\SetBodygroup(1, bodygroupID)
        @maneModelLower\SetParent(@ent) if IsValid(@ent)
        @maneModelLower\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        @GetData()\SetLowerManeModel(@maneModelLower)
        @ent\SetNWEntity('PPM2.LowerManeModel', @maneModelLower) if IsValid(@ent) and SERVER
        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)
        @tailModel\SetBodygroup(1, bodygroupID)
        @tailModel\SetModelScale(@GetData()\GetTailSize())
        @tailModel\SetParent(@ent) if IsValid(@ent)
        @tailModel\Fire('SetParentAttachment', @@ATTACHMENT_EYES_NAME) if SERVER
        @GetData()\SetTailModel(@tailModel)
        @ent\SetNWEntity('PPM2.TailModel', @tailModel) if IsValid(@ent) and SERVER
        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


show me your moves

show me your moves

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

ENABLE_FULLBRIGHT = CreateConVar('ppm2_editor_fullbright', '1', {FCVAR_ARCHIVE}, 'Disable lighting in editor')

local EDIT_TREE

inRadius = (val, min, max) -> val >= min and val <= max
inBox = (pointX, pointY, x, y, w, h) -> inRadius(pointX, x - w, x + w) and inRadius(pointY, y - h, y + h)

-- loal
surface.CreateFont('PPM2BackButton', {
	font: 'Roboto'
	size: ScreenScale(24)\floor()
	weight: 600
})

import HUDCommons from DLib
drawCrosshair = (x, y, radius = ScreenScale(10), arcColor = Color(255, 255, 255), boxesColor = Color(200, 200, 200)) ->
	x -= radius / 2
	y -= radius / 2

	HUDCommons.DrawCircleHollow(x, y, radius, radius * 2, radius * 0.2, arcColor)
	h = radius * 0.1
	w = radius * 0.6
	surface.SetDrawColor(boxesColor)
	surface.DrawRect(x - w / 2, y + radius / 2 - h / 2, w, h)
	surface.DrawRect(x + radius / 2 + w / 2, y + radius / 2 - h / 2, w, h)

	surface.DrawRect(x + radius / 2 - h / 2, y - w / 2, h, w)
	surface.DrawRect(x + radius / 2 - h / 2, y + radius / 2 + w / 3, h, w)

MODEL_BOX_PANEL = {
	SEQUENCE_STAND: 22
	PONY_VEC_Z: 64 * .7

	SEQUENCES: {
		'Standing':    22
		'Moving':      316
		'Walking':     232
		'Sit':         202
		'Swim':        370
		'Run':         328
		'Crouch walk': 286
		'Crouch':      76
		'Jump':        160
	}

	Init: =>
		@animRate = 1
		@seq = @SEQUENCE_STAND
		@hold = false
		@holdOnPoint = false
		@holdRightClick = false
		@canHold = true
		@lastTick = RealTimeL()

		@holdLast = 0
		@mouseX, @mouseY = 0, 0

		@crosshairCircleInactive = Color(150, 150, 150)
		@crosshairBoxInactive = Color(100, 100, 100)

		@crosshairCircleHovered = Color(137, 195, 196)
		@crosshairBoxHovered = Color(200, 200, 200)

		@crosshairCircleSelected = Color(0, 0, 0, 0)
		@crosshairBoxSelected = Color(211, 255, 192)

		@angle = Angle(0, 0, 0)
		@distToPony = 90
		@ldistToPony = 90
		@trackBone = -1
		@trackBoneName = 'LrigSpine2'
		@trackAttach = -1
		@trackAttachName = ''
		@shouldAutoTrack = true
		@autoTrackPos = Vector(0, 0, 0)
		@fixedDistanceToPony = 100
		@lfixedDistanceToPony = 100

		@vectorPos = Vector(@fixedDistanceToPony, 0, 0)
		@lvectorPos = Vector(@fixedDistanceToPony, 0, 0)
		@targetPos = Vector(0, 0, @PONY_VEC_Z * .7)
		@ldrawAngle = Angle()

		@SetCursor('none')
		@SetMouseInputEnabled(true)

		@buildingModel = ClientsideModel('models/ppm/ppm2_stage.mdl', RENDERGROUP_OTHER)
		@buildingModel\SetNoDraw(true)
		@buildingModel\SetModelScale(0.9)

		with @seqButton = vgui.Create('DComboBox', @)
			\SetSize(120, 20)
			\SetValue('Standing')
			\AddChoice(choice, num) for choice, num in pairs @SEQUENCES
			.OnSelect = (pnl = box, index = 1, value = '', data = value) -> @SetSequence(data)

		with @emotesPanel = PPM2.CreateEmotesPanel(@, @model, false)
			\SetPos(10, 40)
			\SetMouseInputEnabled(true)
			\SetVisible(true)

	UpdateAttachsIDs: =>
		@trackBone = @model\LookupBone(@trackBoneName) or -1 if @trackBoneName ~= ''
		@trackAttach = @model\LookupAttachment(@trackAttachName) or -1 if @trackAttachName ~= ''
	GetTrackedPosition: =>
		return @autoTrackPos if @shouldAutoTrack
		return @targetPos

	PerformLayout: (w = 0, h = 0) =>
		@seqButton\SetPos(10, 10)
		@emotesPanel\SetPos(10, 40)

	OnMousePressed: (code = MOUSE_LEFT) =>
		if code == MOUSE_LEFT and @canHold
			if not @selectPoint
				@hold = true
				@SetCursor('sizeall')
				@holdLast = RealTimeL() + .1
				@mouseX, @mouseY = gui.MousePos()
			else
				@holdOnPoint = true
				@SetCursor('crosshair')
		elseif code == MOUSE_RIGHT
			@holdRightClick = true

	OnMouseReleased: (code = MOUSE_LEFT) =>
		if code == MOUSE_LEFT and @canHold
			@holdOnPoint = false
			@SetCursor('none')

			if not @selectPoint
				@hold = false
			else
				@PushMenu(@selectPoint.linkTable)

		elseif code == MOUSE_RIGHT
			@holdRightClick = false

	SetController: (val) => @controller = val

	OnMouseWheeled: (wheelDelta = 0) =>
		if @canHold
			@distToPony = math.clamp(@distToPony - wheelDelta * 10, 20, 150)

	GetModel: => @model
	GetSequence: => @seq
	GetAnimRate: => @animRate
	SetAnimRate: (val = 1) => @animRate = val
	SetSequence: (val = @SEQUENCE_STAND) =>
		@seq = val
		@model\SetSequence(@seq) if IsValid(@model)

	ResetSequence: => @SetSequence(@SEQUENCE_STAND)
	ResetSeq: => @SetSequence(@SEQUENCE_STAND)

	GetParentTarget: => @parentTarget
	SetParentTarget: (val) => @parentTarget = val

	PushMenu: (menu) =>
		table.insert(@stack, menu)
		if not IsValid(@backButton)
			with @backButton = vgui.Create('DButton', @)
				x, y = @GetPos()
				w, h = @GetSize()
				\SetText('↩')
				\SetFont('PPM2BackButton')
				\SizeToContents()
				W, H = \GetSize()
				W += ScreenScale(8)
				\SetSize(W, H)
				\SetPos(w - ScreenScale(6) - W, ScreenScale(4))
				.DoClick = -> @PopMenu()
		if @InMenu()
			x, y = @GetPos()
			frame = @GetParentTarget() or @GetParent() or @
			W, H = @GetSize()
			width = ScreenScale(120)
			with @settingsPanel = vgui.Create('PPM2SettingsBase', frame)
				\SetPos(x, y)
				\SetSize(width, H)
				menu.populate(@settingsPanel)
		return @

	PopMenu: =>
		assert(#@stack > 1, 'invalid stack size to pop from')
		@settingsPanel\Remove() if @InMenu() and IsValid(@settingsPanel)
		table.remove(@stack)
		@backButton\Remove() if #@stack == 1 and IsValid(@backButton)
		return @

	CurrentMenu: => @stack[#@stack]
	InRoot: => #@stack == 1
	InSelection: => @CurrentMenu().type == 'level'
	InMenu: => @CurrentMenu().type == 'menu'

	ResetModel: (ponydata, model = 'models/ppm/player_default_base_new.mdl') =>
		@model\Remove() if IsValid(@model)

		with @model = ClientsideModel(model)
			\SetNoDraw(true)
			.__PPM2_PonyData = ponydata
			\SetSequence(@seq)
			\FrameAdvance(0)
			\SetPos(Vector())
			\InvalidateBoneCache()

		@UpdateAttachsIDs()
		return @model

	Think: =>
		rtime = RealTimeL()
		delta = rtime - @lastTick
		@lastTick = rtime

		if IsValid(@model)
			@model\FrameAdvance(delta * @animRate)
			@model\SetPlaybackRate(1)
			@model\SetPoseParameter('move_x', 1)

			if @shouldAutoTrack
				if @trackAttach ~= -1
					{:Ang, :Pos} = @model\GetAttachment(@trackAttach)
					@autoTrackPos = Pos or Vector()
				elseif @trackBone ~= -1
					@autoTrackPos = @model\GetBonePosition(@trackBone) or Vector()
				else
					@shouldAutoTrack = false

		@hold = @IsHovered() if @hold

		if @hold
			x, y = gui.MousePos()
			deltaX, deltaY = x - @mouseX, y - @mouseY
			@mouseX, @mouseY = x, y
			{:pitch, :yaw, :roll} = @angle
			yaw -= deltaX * .5
			pitch = math.clamp(pitch - deltaY * .5, -45, 45)
			@angle = Angle(pitch, yaw % 360, roll)

		@vectorPos = Vector(@fixedDistanceToPony, 0, 0)
		@vectorPos\Rotate(@angle)
		@lvectorPos = LerpVector(RealFrameTime() * 15, @lvectorPos, @vectorPos)
		@drawAngle = Angle(-@angle.p, @angle.y - 180)
		@ldrawAngle = LerpAngle(RealFrameTime() * 15, @ldrawAngle, @drawAngle)

	FLOOR_VECTOR: Vector(0, 0, -30)
	FLOOR_ANGLE: Vector(0, 0, 1)

	DRAW_WALLS: {
		{Vector(-4000, 0, 900), Vector(1, 0, 0), 8000, 2000}
		{Vector(4000, 0, 900), Vector(-1, 0, 0), 8000, 2000}
		{Vector(0, -4000, 900), Vector(0, 1, 0), 8000, 2000}
		{Vector(0, 4000, 900), Vector(0, -1, 0), 8000, 2000}
		{Vector(0, 0, 900), Vector(0, 0, -1), 8000, 8000}
	}

	WALL_COLOR: Color() - 255
	FLOOR_COLOR: Color() - 255
	EMPTY_VECTOR: Vector()

	Paint: (w = 0, h = 0) =>
		surface.SetDrawColor(0, 0, 0)
		surface.DrawRect(0, 0, w, h)
		return if not IsValid(@model)
		x, y = @LocalToScreen(0, 0)
		drawpos = @lvectorPos + @GetTrackedPosition()
		cam.Start3D(drawpos, @ldrawAngle, @distToPony, x, y, w, h)

		if @holdRightClick
			@model\SetEyeTarget(drawpos)
			turnpitch, turnyaw = DLib.combat.turnAngle(@EMPTY_VECTOR, drawpos, Angle())

			if not inRadius(turnyaw, -20, 20)
				if turnyaw < 0
					@model\SetPoseParameter('head_yaw', turnyaw + 20)
				else
					@model\SetPoseParameter('head_yaw', turnyaw - 20)
			else
				@model\SetPoseParameter('head_yaw', 0)

			turnpitch = turnpitch + 20
			if not inRadius(turnpitch, -10, 0)
				if turnpitch < 0
					@model\SetPoseParameter('head_pitch', turnpitch + 10)
				else
					@model\SetPoseParameter('head_pitch', turnpitch)
			else
				@model\SetPoseParameter('head_pitch', 0)
		else
			@model\SetEyeTarget(@EMPTY_VECTOR)
			@model\SetPoseParameter('head_yaw', 0)
			@model\SetPoseParameter('head_pitch', 0)

		render.DrawQuadEasy(@FLOOR_VECTOR, @FLOOR_ANGLE, 7000, 7000, @FLOOR_COLOR)

		for {pos, ang, w, h} in *@DRAW_WALLS
			render.DrawQuadEasy(pos, ang, w, h, @WALL_COLOR)

		if ENABLE_FULLBRIGHT\GetBool()
			render.SuppressEngineLighting(true)
			render.ResetModelLighting(1, 1, 1)
			render.SetColorModulation(1, 1, 1)

		@buildingModel\DrawModel()
		ctrl = @controller\GetRenderController()

		if bg = @controller\GetBodygroupController()
			bg\ApplyBodygroups()

		with @model\PPMBonesModifier()
			\ResetBones()
			hook.Call('PPM2.SetupBones', nil, @model, @controller)
			\Think(true)

		with ctrl
			\DrawModels()
			\HideModels(true)
			\PreDraw(@model)

		@model\DrawModel()
		ctrl\PostDraw(@model)

		render.SuppressEngineLighting(false) if ENABLE_FULLBRIGHT\GetBool()

		menu = @CurrentMenu()
		@drawPoints = false
		lx, ly = x, y

		if type(menu.points) == 'table'
			@drawPoints = true
			@pointsData = for point in *menu.points
				vecpos = point.getpos(@model)
				position = vecpos\ToScreen()
				{position, point, vecpos\Distance(drawpos)}
		elseif @InMenu() and menu.getpos
			{:x, :y} = menu.getpos(@model)\ToScreen(drawpos)
			x, y = x - lx, y - ly

		cam.End3D()

		if @drawPoints
			mx, my = gui.MousePos()
			mx, my = mx - lx, my - ly
			radius = ScreenScale(20)
			local drawnSelected
			min = 9999

			for pointdata in *@pointsData
				{:x, :y} = pointdata[1]
				x, y = x - lx, y - ly
				pointdata[1].x, pointdata[1].y = x, y

				if inBox(mx, my, x, y, radius, radius)
					if min > pointdata[3]
						drawnSelected = pointdata
						min = pointdata[3]

			@selectPoint = drawnSelected and drawnSelected[2] or false

			for pointdata in *@pointsData
				{:x, :y} = pointdata[1]

				if pointdata == drawnSelected
					drawCrosshair(x, y, radius, @crosshairCircleHovered, @crosshairBoxHovered)
				else
					drawCrosshair(x, y, radius, @crosshairCircleInactive, @crosshairBoxInactive)

			if not @hold and not @holdOnPoint
				if drawnSelected
					@SetCursor('hand')
				else
					@SetCursor('none')
		else
			@selectPoint = false
			if @InMenu() and menu.getpos
				radius = ScreenScale(20)
				drawCrosshair(x, y, radius, @crosshairCircleSelected, @crosshairBoxSelected)

	OnRemove: =>
		@model\Remove() if IsValid(@model)
		@buildingModel\Remove() if IsValid(@buildingModel)
}

vgui.Register('PPM2Model2Panel', MODEL_BOX_PANEL, 'EditablePanel')

EDIT_TREE = {
	type: 'level'
	name: 'Pony overview'
	dist: 100
	defpos: Vector(100, 0, 0)

	points: {
		{
			type: 'bone'
			target: 'LrigScull'
			link: 'head_submenu'
		}

		{
			type: 'bone'
			target: 'LrigSpine1'
			link: 'spine_length'
		}

		{
			type: 'bone'
			target: 'Tail01'
			link: 'tail'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_FL_Metacarpus'
			link: 'legs_submenu'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_FR_Metacarpus'
			link: 'legs_submenu'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_BR_LargeCannon'
			link: 'legs_submenu'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_BL_LargeCannon'
			link: 'legs_submenu'
		}
	}

	children: {
		head_submenu: {
			type: 'level'
			name: 'Head anatomy'

			points: {
				{
					type: 'attach'
					target: 'eyes'
					link: 'eyes'
				}
			}

			children: {
				eyes: {
					type: 'menu'
					populate: =>
				}
			}
		}

		legs_submenu: {
			type: 'level'
			name: 'Hooves anatomy'

			points: {

			}

			children: {

			}
		}
	}
}

patchSubtree = (node) ->
	if type(node.children) == 'table'
		for childID, child in pairs node.children
			child.id = childID
			child.defpos = child.defpos or Vector(node.defpos)
			child.dist = child.dist or node.dist
			patchSubtree(child)

	if type(node.points) == 'table'
		for point in *node.points
			point.addvector = point.addvector or Vector()

			switch point.type
				when 'point'
					point.getpos = => Vector(point.target)
				when 'bone'
					point.getpos = =>
						if not point.targetID or point.targetID == -1
							point.targetID = @LookupBone(point.target) or -1

						if point.targetID == -1
							return Vector(point.addvector)
						else
							return @GetBonePosition(point.targetID) + point.addvector
				when 'attach'
					point.getpos = =>
						if not point.targetID or point.targetID == -1
							point.targetID = @LookupAttachment(point.target) or -1

						if point.targetID == -1
							return Vector(point.addvector)
						else
							{:Pos, :Ang} = @GetAttachment(point.targetID)
							return Pos and (Pos + point.addvector) or Vector(point.addvector)

			if type(node.children) == 'table'
				point.linkTable = node.children[point.link]
				if type(point.linkTable) == 'table' and point.linkTable.type == 'menu'
					point.linkTable.getpos = point.getpos

EDIT_TREE.id = 'root'
patchSubtree(EDIT_TREE)

ppm2_editor3 = ->
	if IsValid(PPM2.EDITOR3)
		PPM2.EDITOR3\SetVisible(true)
		PPM2.EDITOR3\MakePopup()
		return PPM2.EDITOR3
	PPM2.EDITOR3 = vgui.Create('DLib_Window')
	self = PPM2.EDITOR3
	@SetSize(ScrWL(), ScrHL())
	@SetPos(0, 0)
	@MakePopup()
	@SetTitle('PPM/2 Editor/3')
	@SetDraggable(false)
	@RemoveResize()

	with @modelPanel = vgui.Create('PPM2Model2Panel', @)
		\Dock(FILL)
		\DockMargin(3, 3, 3, 3)

	copy = PPM2.GetMainData()\Copy()
	ply = LocalPlayer()
	ent = @modelPanel\ResetModel()

	controller = copy\CreateCustomController(ent)
	controller\SetFlexLerpMultiplier(1.3)
	copy\SetController(controller)

	@modelPanel\SetController(controller)
	controller\SetupEntity(ent)
	controller\SetDisableTask(true)

	@modelPanel.stack = {EDIT_TREE}
	@modelPanel\SetParentTarget(@)

concommand.Add 'ppm2_editor3', ppm2_editor3

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

ENABLE_FULLBRIGHT = CreateConVar('ppm2_editor_fullbright', '1', {FCVAR_ARCHIVE}, 'Disable lighting in editor')

local EDIT_TREE

inRadius = (val, min, max) -> val >= min and val <= max
inBox = (pointX, pointY, x, y, w, h) -> inRadius(pointX, x - w, x + w) and inRadius(pointY, y - h, y + h)

-- loal
surface.CreateFont('PPM2BackButton', {
	font: 'Roboto'
	size: ScreenScale(24)\floor()
	weight: 600
})

import HUDCommons from DLib
drawCrosshair = (x, y, radius = ScreenScale(10), arcColor = Color(255, 255, 255), boxesColor = Color(200, 200, 200)) ->
	x -= radius / 2
	y -= radius / 2

	HUDCommons.DrawCircleHollow(x, y, radius, radius * 2, radius * 0.2, arcColor)
	h = radius * 0.1
	w = radius * 0.6
	surface.SetDrawColor(boxesColor)
	surface.DrawRect(x - w / 2, y + radius / 2 - h / 2, w, h)
	surface.DrawRect(x + radius / 2 + w / 2, y + radius / 2 - h / 2, w, h)

	surface.DrawRect(x + radius / 2 - h / 2, y - w / 2, h, w)
	surface.DrawRect(x + radius / 2 - h / 2, y + radius / 2 + w / 3, h, w)

MODEL_BOX_PANEL = {
	SEQUENCE_STAND: 22
	PONY_VEC_Z: 64 * .7

	SEQUENCES: {
		'Standing':    22
		'Moving':      316
		'Walking':     232
		'Sit':         202
		'Swim':        370
		'Run':         328
		'Crouch walk': 286
		'Crouch':      76
		'Jump':        160
	}

	Init: =>
		@animRate = 1
		@seq = @SEQUENCE_STAND
		@hold = false
		@holdOnPoint = false
		@holdRightClick = false
		@canHold = true
		@lastTick = RealTimeL()

		@holdLast = 0
		@mouseX, @mouseY = 0, 0

		@crosshairCircleInactive = Color(150, 150, 150)
		@crosshairBoxInactive = Color(100, 100, 100)

		@crosshairCircleHovered = Color(137, 195, 196)
		@crosshairBoxHovered = Color(200, 200, 200)

		@crosshairCircleSelected = Color(0, 0, 0, 0)
		@crosshairBoxSelected = Color(211, 255, 192)

		@angle = Angle(0, 0, 0)
		@distToPony = 90
		@trackBone = -1
		@trackBoneName = 'LrigSpine2'
		@trackAttach = -1
		@trackAttachName = ''
		@shouldAutoTrack = true
		@autoTrackPos = Vector(0, 0, 0)
		@fixedDistanceToPony = 100

		@vectorPos = Vector(@fixedDistanceToPony, 0, 0)
		@targetPos = Vector(0, 0, @PONY_VEC_Z * .7)

		@SetCursor('none')
		@SetMouseInputEnabled(true)

		@buildingModel = ClientsideModel('models/ppm/ppm2_stage.mdl', RENDERGROUP_OTHER)
		@buildingModel\SetNoDraw(true)
		@buildingModel\SetModelScale(0.9)

		with @seqButton = vgui.Create('DComboBox', @)
			\SetSize(120, 20)
			\SetValue('Standing')
			\AddChoice(choice, num) for choice, num in pairs @SEQUENCES
			.OnSelect = (pnl = box, index = 1, value = '', data = value) -> @SetSequence(data)

		with @emotesPanel = PPM2.CreateEmotesPanel(@, @model, false)
			\SetPos(10, 40)
			\SetMouseInputEnabled(true)
			\SetVisible(true)

	UpdateAttachsIDs: =>
		@trackBone = @model\LookupBone(@trackBoneName) or -1 if @trackBoneName ~= ''
		@trackAttach = @model\LookupAttachment(@trackAttachName) or -1 if @trackAttachName ~= ''
	GetTrackedPosition: =>
		return @autoTrackPos if @shouldAutoTrack
		return @targetPos

	PerformLayout: (w = 0, h = 0) =>
		@seqButton\SetPos(10, 10)
		@emotesPanel\SetPos(10, 40)

	OnMousePressed: (code = MOUSE_LEFT) =>
		if code == MOUSE_LEFT and @canHold
			if not @selectPoint
				@hold = true
				@SetCursor('sizeall')
				@holdLast = RealTimeL() + .1
				@mouseX, @mouseY = gui.MousePos()
			else
				@holdOnPoint = true
				@SetCursor('crosshair')
		elseif code == MOUSE_RIGHT
			@holdRightClick = true

	OnMouseReleased: (code = MOUSE_LEFT) =>
		if code == MOUSE_LEFT and @canHold
			@holdOnPoint = false
			@SetCursor('none')

			if not @selectPoint
				@hold = false
			else
				@PushMenu(@selectPoint.linkTable)

		elseif code == MOUSE_RIGHT
			@holdRightClick = false

	SetController: (val) => @controller = val

	OnMouseWheeled: (wheelDelta = 0) =>
		if @canHold
			@distToPony = math.clamp(@distToPony - wheelDelta * 10, 20, 150)

	GetModel: => @model
	GetSequence: => @seq
	GetAnimRate: => @animRate
	SetAnimRate: (val = 1) => @animRate = val
	SetSequence: (val = @SEQUENCE_STAND) =>
		@seq = val
		@model\SetSequence(@seq) if IsValid(@model)

	ResetSequence: => @SetSequence(@SEQUENCE_STAND)
	ResetSeq: => @SetSequence(@SEQUENCE_STAND)

	GetParentTarget: => @parentTarget
	SetParentTarget: (val) => @parentTarget = val

	PushMenu: (menu) =>
		table.insert(@stack, menu)
		if not IsValid(@backButton)
			with @backButton = vgui.Create('DButton', @)
				x, y = @GetPos()
				w, h = @GetSize()
				\SetText('↩')
				\SetFont('PPM2BackButton')
				\SizeToContents()
				W, H = \GetSize()
				W += ScreenScale(8)
				\SetSize(W, H)
				\SetPos(w - ScreenScale(6) - W, ScreenScale(4))
				.DoClick = -> @PopMenu()
		if @InMenu()
			x, y = @GetPos()
			frame = @GetParentTarget() or @GetParent() or @
			W, H = @GetSize()
			width = ScreenScale(120)
			with @settingsPanel = vgui.Create('PPM2SettingsBase', frame)
				\SetPos(x, y)
				\SetSize(width, H)
				menu.populate(@settingsPanel)
		return @

	PopMenu: =>
		assert(#@stack > 1, 'invalid stack size to pop from')
		@settingsPanel\Remove() if @InMenu() and IsValid(@settingsPanel)
		table.remove(@stack)
		@backButton\Remove() if #@stack == 1 and IsValid(@backButton)
		return @

	CurrentMenu: => @stack[#@stack]
	InRoot: => #@stack == 1
	InSelection: => @CurrentMenu().type == 'level'
	InMenu: => @CurrentMenu().type == 'menu'

	ResetModel: (ponydata, model = 'models/ppm/player_default_base_new.mdl') =>
		@model\Remove() if IsValid(@model)

		with @model = ClientsideModel(model)
			\SetNoDraw(true)
			.__PPM2_PonyData = ponydata
			\SetSequence(@seq)
			\FrameAdvance(0)
			\SetPos(Vector())
			\InvalidateBoneCache()

		@UpdateAttachsIDs()
		return @model

	Think: =>
		rtime = RealTimeL()
		delta = rtime - @lastTick
		@lastTick = rtime

		if IsValid(@model)
			@model\FrameAdvance(delta * @animRate)
			@model\SetPlaybackRate(1)
			@model\SetPoseParameter('move_x', 1)

			if @shouldAutoTrack
				if @trackAttach ~= -1
					{:Ang, :Pos} = @model\GetAttachment(@trackAttach)
					@autoTrackPos = Pos or Vector()
				elseif @trackBone ~= -1
					@autoTrackPos = @model\GetBonePosition(@trackBone) or Vector()
				else
					@shouldAutoTrack = false

		@hold = @IsHovered() if @hold

		if @hold
			x, y = gui.MousePos()
			deltaX, deltaY = x - @mouseX, y - @mouseY
			@mouseX, @mouseY = x, y
			{:pitch, :yaw, :roll} = @angle
			yaw -= deltaX * .5
			pitch = math.clamp(pitch - deltaY * .5, -45, 45)
			@angle = Angle(pitch, yaw % 360, roll)

		@vectorPos = Vector(@fixedDistanceToPony, 0, 0)
		@vectorPos\Rotate(@angle)
		@drawAngle = Angle(-@angle.p, @angle.y - 180)

	FLOOR_VECTOR: Vector(0, 0, -30)
	FLOOR_ANGLE: Vector(0, 0, 1)

	DRAW_WALLS: {
		{Vector(-4000, 0, 900), Vector(1, 0, 0), 8000, 2000}
		{Vector(4000, 0, 900), Vector(-1, 0, 0), 8000, 2000}
		{Vector(0, -4000, 900), Vector(0, 1, 0), 8000, 2000}
		{Vector(0, 4000, 900), Vector(0, -1, 0), 8000, 2000}
		{Vector(0, 0, 900), Vector(0, 0, -1), 8000, 8000}
	}

	WALL_COLOR: Color() - 255
	FLOOR_COLOR: Color() - 255
	EMPTY_VECTOR: Vector()

	Paint: (w = 0, h = 0) =>
		surface.SetDrawColor(0, 0, 0)
		surface.DrawRect(0, 0, w, h)
		return if not IsValid(@model)
		x, y = @LocalToScreen(0, 0)
		drawpos = @vectorPos + @GetTrackedPosition()
		cam.Start3D(drawpos, @drawAngle, @distToPony, x, y, w, h)

		if @holdRightClick
			@model\SetEyeTarget(drawpos)
			turnpitch, turnyaw = DLib.combat.turnAngle(@EMPTY_VECTOR, drawpos, Angle())

			if not inRadius(turnyaw, -20, 20)
				if turnyaw < 0
					@model\SetPoseParameter('head_yaw', turnyaw + 20)
				else
					@model\SetPoseParameter('head_yaw', turnyaw - 20)
			else
				@model\SetPoseParameter('head_yaw', 0)

			turnpitch = turnpitch + 20
			if not inRadius(turnpitch, -10, 0)
				if turnpitch < 0
					@model\SetPoseParameter('head_pitch', turnpitch + 10)
				else
					@model\SetPoseParameter('head_pitch', turnpitch)
			else
				@model\SetPoseParameter('head_pitch', 0)
		else
			@model\SetEyeTarget(@EMPTY_VECTOR)
			@model\SetPoseParameter('head_yaw', 0)
			@model\SetPoseParameter('head_pitch', 0)

		render.DrawQuadEasy(@FLOOR_VECTOR, @FLOOR_ANGLE, 7000, 7000, @FLOOR_COLOR)

		for {pos, ang, w, h} in *@DRAW_WALLS
			render.DrawQuadEasy(pos, ang, w, h, @WALL_COLOR)

		if ENABLE_FULLBRIGHT\GetBool()
			render.SuppressEngineLighting(true)
			render.ResetModelLighting(1, 1, 1)
			render.SetColorModulation(1, 1, 1)

		@buildingModel\DrawModel()
		ctrl = @controller\GetRenderController()

		if bg = @controller\GetBodygroupController()
			bg\ApplyBodygroups()

		with @model\PPMBonesModifier()
			\ResetBones()
			hook.Call('PPM2.SetupBones', nil, @model, @controller)
			\Think(true)

		with ctrl
			\DrawModels()
			\HideModels(true)
			\PreDraw(@model)

		@model\DrawModel()
		ctrl\PostDraw(@model)

		render.SuppressEngineLighting(false) if ENABLE_FULLBRIGHT\GetBool()

		menu = @CurrentMenu()
		@drawPoints = false
		lx, ly = x, y

		if type(menu.points) == 'table'
			@drawPoints = true
			@pointsData = for point in *menu.points
				vecpos = point.getpos(@model)
				position = vecpos\ToScreen()
				{position, point, vecpos\Distance(drawpos)}
		elseif @InMenu() and menu.getpos
			{:x, :y} = menu.getpos(@model)\ToScreen(drawpos)
			x, y = x - lx, y - ly

		cam.End3D()

		if @drawPoints
			mx, my = gui.MousePos()
			mx, my = mx - lx, my - ly
			radius = ScreenScale(20)
			local drawnSelected
			min = 9999

			for pointdata in *@pointsData
				{:x, :y} = pointdata[1]
				x, y = x - lx, y - ly
				pointdata[1].x, pointdata[1].y = x, y

				if inBox(mx, my, x, y, radius, radius)
					if min > pointdata[3]
						drawnSelected = pointdata
						min = pointdata[3]

			@selectPoint = drawnSelected and drawnSelected[2] or false

			for pointdata in *@pointsData
				{:x, :y} = pointdata[1]

				if pointdata == drawnSelected
					drawCrosshair(x, y, radius, @crosshairCircleHovered, @crosshairBoxHovered)
				else
					drawCrosshair(x, y, radius, @crosshairCircleInactive, @crosshairBoxInactive)

			if not @hold and not @holdOnPoint
				if drawnSelected
					@SetCursor('hand')
				else
					@SetCursor('none')
		else
			@selectPoint = false
			if @InMenu() and menu.getpos
				radius = ScreenScale(20)
				drawCrosshair(x, y, radius, @crosshairCircleSelected, @crosshairBoxSelected)

	OnRemove: =>
		@model\Remove() if IsValid(@model)
		@buildingModel\Remove() if IsValid(@buildingModel)
}

vgui.Register('PPM2Model2Panel', MODEL_BOX_PANEL, 'EditablePanel')

EDIT_TREE = {
	type: 'level'
	name: 'Pony overview'

	points: {
		{
			type: 'bone'
			target: 'LrigScull'
			link: 'head_submenu'
		}

		{
			type: 'bone'
			target: 'LrigSpine1'
			link: 'spine_length'
		}

		{
			type: 'bone'
			target: 'Tail01'
			link: 'tail'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_FL_Metacarpus'
			link: 'legs_submenu'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_FR_Metacarpus'
			link: 'legs_submenu'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_BR_LargeCannon'
			link: 'legs_submenu'
		}

		{
			type: 'bone'
			target: 'Lrig_LEG_BL_LargeCannon'
			link: 'legs_submenu'
		}
	}

	children: {
		head_submenu: {
			type: 'level'
			name: 'Head anatomy'

			points: {
				{
					type: 'attach'
					target: 'eyes'
					link: 'eyes'
				}
			}

			children: {
				eyes: {
					type: 'menu'
					populate: =>
				}
			}
		}

		legs_submenu: {
			type: 'level'
			name: 'Hooves anatomy'

			points: {

			}

			children: {

			}
		}
	}
}

patchSubtree = (node) ->
	if type(node.children) == 'table'
		for childID, child in pairs node.children
			child.id = childID
			patchSubtree(child)

	if type(node.points) == 'table'
		for point in *node.points
			point.addvector = point.addvector or Vector()

			switch point.type
				when 'point'
					point.getpos = => Vector(point.target)
				when 'bone'
					point.getpos = =>
						if not point.targetID or point.targetID == -1
							point.targetID = @LookupBone(point.target) or -1

						if point.targetID == -1
							return Vector(point.addvector)
						else
							return @GetBonePosition(point.targetID) + point.addvector
				when 'attach'
					point.getpos = =>
						if not point.targetID or point.targetID == -1
							point.targetID = @LookupAttachment(point.target) or -1

						if point.targetID == -1
							return Vector(point.addvector)
						else
							{:Pos, :Ang} = @GetAttachment(point.targetID)
							return Pos and (Pos + point.addvector) or Vector(point.addvector)

			if type(node.children) == 'table'
				point.linkTable = node.children[point.link]
				if type(point.linkTable) == 'table' and point.linkTable.type == 'menu'
					point.linkTable.getpos = point.getpos

EDIT_TREE.id = 'root'
patchSubtree(EDIT_TREE)

ppm2_editor3 = ->
	if IsValid(PPM2.EDITOR3)
		PPM2.EDITOR3\SetVisible(true)
		PPM2.EDITOR3\MakePopup()
		return PPM2.EDITOR3
	PPM2.EDITOR3 = vgui.Create('DLib_Window')
	self = PPM2.EDITOR3
	@SetSize(ScrWL(), ScrHL())
	@SetPos(0, 0)
	@MakePopup()
	@SetTitle('PPM/2 Editor/3')
	@SetDraggable(false)
	@RemoveResize()

	with @modelPanel = vgui.Create('PPM2Model2Panel', @)
		\Dock(FILL)
		\DockMargin(3, 3, 3, 3)

	copy = PPM2.GetMainData()\Copy()
	ply = LocalPlayer()
	ent = @modelPanel\ResetModel()

	controller = copy\CreateCustomController(ent)
	controller\SetFlexLerpMultiplier(1.3)
	copy\SetController(controller)

	@modelPanel\SetController(controller)
	controller\SetupEntity(ent)
	controller\SetDisableTask(true)

	@modelPanel.stack = {EDIT_TREE}
	@modelPanel\SetParentTarget(@)

concommand.Add 'ppm2_editor3', ppm2_editor3


Made index format enumeration assignment more succinct.

-- 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"
BufferFilter = require "glurg.filters.BufferFilter"
ProgramFilter = require "glurg.filters.ProgramFilter"
VertexArrayFilter = require "glurg.filters.VertexArrayFilter"
Model = require "glurg.resources.Model"
Mesh = require "glurg.resources.Mesh"
MeshBlobBuilder = require "glurg.resources.MeshBlobBuilder"
Call = require "glurg.trace.Call"
Trace = require "glurg.trace.Trace"

class GL3StaticModelHeuristic
	new: (data) =>
		@array_buffer_filter = BufferFilter("GL_ARRAY_BUFFER")
		@element_array_buffer_filter = BufferFilter("GL_ELEMENT_ARRAY_BUFFER")
		@program_filter = ProgramFilter!
		@vertex_array_filter = VertexArrayFilter!
		@attributes =
			position_binding: data["position"]
			normal_binding: data["normal"]
			texture_coordinate_binding: data["texture"]
		@output_directory = data["output-dir"] or "."

	get_filters: =>
		return @array_buffer_filter, @element_array_buffer_filter, @program_filter, @vertex_array_filter

	make_mesh: (attribute_name) =>
		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)

		builder = MeshBlobBuilder!
		extracted_description = builder\extract_attrib_call(description.call)
		extracted_buffer = builder\extract_buffer_call(buffer.data)
		if extracted_description and extracted_buffer
			return Mesh(builder\build!)

	make_model: (positions_mesh, normals_mesh, texture_coordinates_mesh, call) =>
		model = Model(positions_mesh)
		model.normals = normals_mesh
		model.texture_coordinates[1] = texture_coordinates_mesh

		index_format = switch call\get_argument_by_name("type")\query!.value_name
			when 'GL_UNSIGNED_BYTE' then 'unsigned_byte'
			when 'GL_UNSIGNED_SHORT' then 'unsigned_short'
			when 'GL_UNSIGNED_INT' then 'unsigned_integer'
			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)

		model\extract_index_data(index_format, element_array_buffer.data)

		return model

	is_compatible: =>
		program = @program_filter\get_current_program!

		vertices = program\has_attribute(@attributes.position_binding)
		normals = program\has_attribute(@attributes.normal_binding)
		textures = program\has_attribute(@attributes.texture_coordinate_binding)

		return vertices and normals and textures

	save_model: (call) =>
		if @\is_compatible!
			positions_mesh = @\make_mesh(@attributes.position_binding)
			normals_mesh = @\make_mesh(@attributes.normal_binding)
			texture_coordinates_mesh = @\make_mesh(@attributes.texture_coordinate_binding)

			if positions_mesh and normals_mesh and texture_coordinates_mesh
				model = @\make_model(positions_mesh, normals_mesh, texture_coordinates_mesh, call)

				filename = string.format("%s/%010d.dae", @output_directory, call.index)
				model\save_dae(filename)

				Log\info('medium', "saved model: #{filename}")

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

		@\save_model(call)

		return false

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

		@\save_model(call)

		return false

	dispose: (trace) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))

		@array_buffer_filter\dispose(trace)
		@element_array_buffer_filter\dispose(trace)
		@program_filter\dispose(trace)
		@vertex_array_filter\dispose(trace)

return GL3StaticModelHeuristic

-- 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"
BufferFilter = require "glurg.filters.BufferFilter"
ProgramFilter = require "glurg.filters.ProgramFilter"
VertexArrayFilter = require "glurg.filters.VertexArrayFilter"
Model = require "glurg.resources.Model"
Mesh = require "glurg.resources.Mesh"
MeshBlobBuilder = require "glurg.resources.MeshBlobBuilder"
Call = require "glurg.trace.Call"
Trace = require "glurg.trace.Trace"

class GL3StaticModelHeuristic
	new: (data) =>
		@array_buffer_filter = BufferFilter("GL_ARRAY_BUFFER")
		@element_array_buffer_filter = BufferFilter("GL_ELEMENT_ARRAY_BUFFER")
		@program_filter = ProgramFilter!
		@vertex_array_filter = VertexArrayFilter!
		@attributes =
			position_binding: data["position"]
			normal_binding: data["normal"]
			texture_coordinate_binding: data["texture"]
		@output_directory = data["output-dir"] or "."

	get_filters: =>
		return @array_buffer_filter, @element_array_buffer_filter, @program_filter, @vertex_array_filter

	make_mesh: (attribute_name) =>
		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)

		builder = MeshBlobBuilder!
		extracted_description = builder\extract_attrib_call(description.call)
		extracted_buffer = builder\extract_buffer_call(buffer.data)
		if extracted_description and extracted_buffer
			return Mesh(builder\build!)

	make_model: (positions_mesh, normals_mesh, texture_coordinates_mesh, call) =>
		model = Model(positions_mesh)
		model.normals = normals_mesh
		model.texture_coordinates[1] = texture_coordinates_mesh

		index_format = switch call\get_argument_by_name("type")\query!.value_name
			when 'GL_UNSIGNED_BYTE' then index_format = 'unsigned_byte'
			when 'GL_UNSIGNED_SHORT' then index_format = 'unsigned_short'
			when 'GL_UNSIGNED_INT' then index_format = 'unsigned_integer'
			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)

		model\extract_index_data(index_format, element_array_buffer.data)

		return model

	is_compatible: =>
		program = @program_filter\get_current_program!

		vertices = program\has_attribute(@attributes.position_binding)
		normals = program\has_attribute(@attributes.normal_binding)
		textures = program\has_attribute(@attributes.texture_coordinate_binding)

		return vertices and normals and textures

	save_model: (call) =>
		if @\is_compatible!
			positions_mesh = @\make_mesh(@attributes.position_binding)
			normals_mesh = @\make_mesh(@attributes.normal_binding)
			texture_coordinates_mesh = @\make_mesh(@attributes.texture_coordinate_binding)

			if positions_mesh and normals_mesh and texture_coordinates_mesh
				model = @\make_model(positions_mesh, normals_mesh, texture_coordinates_mesh, call)

				filename = string.format("%s/%010d.dae", @output_directory, call.index)
				model\save_dae(filename)

				Log\info('medium', "saved model: #{filename}")

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

		@\save_model(call)

		return false

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

		@\save_model(call)

		return false

	dispose: (trace) =>
		Promise.keep("trace", Promise.IsClass(trace, Trace))

		@array_buffer_filter\dispose(trace)
		@element_array_buffer_filter\dispose(trace)
		@program_filter\dispose(trace)
		@vertex_array_filter\dispose(trace)

return GL3StaticModelHeuristic


Updated audio API to match with latest LOVE audi

-------------------------------------------------------------------------------
-- Handles creating, playing and manipulating sound and music instances.
-------------------------------------------------------------------------------
-- Sound effects are small audio samples, usually no longer than a few seconds,
-- that are played back on specific game events such as a character jumping or
-- shooting a gun.
-- For any sound that's longer than a few seconds it is preferable to stream it
-- from disk instead of fully loading it into RAM. NÖN provides a "stream"
-- source that lets you do that.
--
-- NÖN supports MP3, OGG and WAV files.
-- ***note1:*** iOS currently does not support OGG files.
-- ***note2:*** On Android, a static source cannot be over 1 MB
--
-- @module non.audio

Source = require "non.objects.Source"

sources = {}
globalvolume = 1

getSourceCount = ->
  #sources

getVolume = ->
  globalvolume

newSource = (filename, audiotype, filetype) ->
  source = Source filename, audiotype, filetype
  table.insert sources, source
  return source

pause = (source) ->
  source\pause!

play = (source) ->
  source\setVolume globalvolume
  source\play!

resume = (source) ->
  source\resume!

setVolume = (volume) ->
  globalvolume = volume

  for i, source in ipairs sources
    source\setVolume globalvolume

stop = (source) ->
  source\stop!

stopAll = ->
  for i, source in ipairs sources
    source\stop!
    source\free!

{
  :getNumSources
  :getVolume
  :newSource
  :pause
  :play
  :resume
  :setVolume
  :stop
  :stopAll
}

-------------------------------------------------------------------------------
-- Handles creating, playing and manipulating sound and music instances.
-------------------------------------------------------------------------------
-- Sound effects are small audio samples, usually no longer than a few seconds,
-- that are played back on specific game events such as a character jumping or
-- shooting a gun.
-- For any sound that's longer than a few seconds it is preferable to stream it
-- from disk instead of fully loading it into RAM. NÖN provides a "stream"
-- source that lets you do that.
--
-- NÖN supports MP3, OGG and WAV files.
-- ***note1:*** iOS currently does not support OGG files.
-- ***note2:*** On Android, a static source cannot be over 1 MB
--
-- @module non.audio

Source = require "non.objects.Source"

sources = {}
globalvolume = 1

getNumSources = ->
  #sources

getVolume = ->
  globalvolume

newSource = (filename, audiotype, filetype) ->
  source = Source filename, audiotype, filetype
  table.insert sources, source
  return source

pause = (source) ->
  source\pause!

play = (source) ->
  source\setVolume globalvolume
  source\play!

resume = (source) ->
  source\resume!

setVolume = (volume) ->
  globalvolume = volume

  for i, source in ipairs sources
    source\setVolume globalvolume

stop = (source) ->
  source\stop!

stopAll = ->
  for i, source in ipairs sources
    source\stop!
    source\free!

{
  :getNumSources
  :getVolume
  :newSource
  :pause
  :play
  :resume
  :setVolume
  :stop
  :stopAll
}


Slight abuse of moonscripts s expressions.

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

import insert from table

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

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

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

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

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

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

dbg = (b) -> return "[DBG:" .. ((b and "Y") or "N") .. "]"

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

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

		else
			print "ERR: ", file, mode

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

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

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

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

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

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

import insert from table

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

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

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

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

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

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

dbg = (b) -> return "[DBG:" .. ((b and "Y") or "N") .. "]"

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

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

		else
			print "ERR: ", file, mode

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

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

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

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

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


Fix golang option

Fix golang option

#!/usr/bin/env moon

-- Copyright (C) 2014 Sam Dodrill <xena@yolo-swag.com> All rights reserved.
--
-- This software is provided 'as-is', without any express or implied
-- warranty. In no event will the authors be held liable for any damages
-- arising from the use of this software.
--
-- Permission is granted to anyone to use this software for any purpose,
-- including commercial applications, and to alter it and redistribute it
-- freely, subject to the following restrictions:
--
-- 1. The origin of this software must not be misrepresented; you must not
--    claim that you wrote the original software. If you use this software
--    in a product, an acknowledgment in the product documentation would be
--    appreciated but is not required.
--
-- 2. Altered source versions must be plainly marked as such, and must not be
--    misrepresented as being the original software.
--
-- 3. This notice may not be removed or altered from any source
--    distribution.

export yaml = require "yaml"

--- yaml2Table loads a yaml file as text and returns it as a file.
--  Returns a table representing the yaml document and an error string or nil.
yaml2Table = (fname) -> --> table, error
  fin = io.open fname, "r"
  if fin == nil
    return {}, "File read (#{fname}) failed."

  data = fin\read "*all"
  ret = yaml.load data

  fin\close!

  return ret, nil

--- doCommand returns the output and return status of a command.
doCommand = (command) -> --> string, number
  n = os.tmpname!
  code = os.execute command .. " > " .. n
  lines = {}

  for line in io.lines n
    table.insert lines, line

  os.remove n

  lines, code

--- pairsByKeys is an iterator for a table aplhabetically
--  adapted from here: http://www.lua.org/pil/19.3.html
pairsByKeys = (t) -> --> function
  a = {}
  for n in pairs t
    table.insert a, n

  table.sort a

  i = 0
  iter = ->
    i += 1
    if a[i] == nil
      return nil
    else
      return a[i], t[a[i]]
  iter

export commands = {
  up: {"Brings up a development container", ->
    dcommand = "docker run -idt --name #{data.projname}-dev --hostname dev:#{data.projname} "
    path = "/home/#{data.user}/dev/"
    localdir = os.getenv "PWD"

    -- Is this go?
    if data.golang
      path = "/home/#{data.user}/go/src/#{data.repopath}"

    -- Add source code directory to mount
    dcommand ..= "-v #{localdir}:#{path} "

    -- Add ssh keys if needed
    if data.ssh
      dcommand ..= "-v /home/#{os.getenv "USER"}/.ssh:/home/#{data.user}/.ssh "

    image = data.base
    if data.base == nil and data.overlay ~= nil
      image = "dev-#{data.projname}"

    -- Append image name
    dcommand ..= image

    print "Starting up container for #{data.projname}"

    -- Start up the docker container
    lines, status = doCommand dcommand
    if status ~= 0
      print "Launch failed. Does this container already exist?"
      for _,line in pairs lines
        print "docker: " .. line
      os.exit status

    ctid = lines[#lines]

    -- report to user
    print "#{data.projname}-dev (ctid: #{ctid\sub(1,12)}) running!"
    print "To use this container please attach to it with:"
    print "    $ docker attach #{data.projname}-dev"
  }

  down: { "Destroys a development container", ->
    lines, status = doCommand "docker rm -f #{data.projname}-dev"
    if status ~= 0
      os.exit status

    print "Container destroyed."
  }

  establish: { "Create a Docker image from the manifest", ->
    if data.overlay == nil
      print "Error: docker container overlay not made. Exiting."
      os.exit 1

    dir = os.tmpname!
    os.remove dir

    out, err = doCommand "mkdir #{dir}"
    if err ~= 0
      print "Could not make temporary directory #{dir}. Dying."
      os.exit err

    fout = io.open "#{dir}/Dockerfile", "w"
    fout\write data.overlay
    fout\close!

    proc = io.popen "docker build -t dev-#{data.projname} #{dir}"
    for line in proc\lines!
      print line

    proc\close!

    _, err = doCommand "rm -rf #{dir}"
    if err ~= 0
      os.exit err

    print "Docker image dev-#{data.projname} created."
  }
}

if #arg == 0
  print "dev version 0.2\n"

  print "Usage: dev [command] <manifest>\n"

  print "  if manifest is undefined the default value"
  print "  .dev.yaml will be used.\n"

  print "Available commands:"
  for name,cmd in pairsByKeys commands
    print "%12s   %s"\format(name, cmd[1])

  os.exit 1

command = arg[1]\lower!

if commands[command] ~= nil
  export fname = arg[2]
  if fname == nil
    fname = ".dev.yaml"

  export data, err = yaml2Table fname
  if err ~= nil
    print "Cannot open file #{fname}. Please make sure this file exists or is readable."
    os.exit 1

  commands[command][2]!

#!/usr/bin/env moon

-- Copyright (C) 2014 Sam Dodrill <xena@yolo-swag.com> All rights reserved.
--
-- This software is provided 'as-is', without any express or implied
-- warranty. In no event will the authors be held liable for any damages
-- arising from the use of this software.
--
-- Permission is granted to anyone to use this software for any purpose,
-- including commercial applications, and to alter it and redistribute it
-- freely, subject to the following restrictions:
--
-- 1. The origin of this software must not be misrepresented; you must not
--    claim that you wrote the original software. If you use this software
--    in a product, an acknowledgment in the product documentation would be
--    appreciated but is not required.
--
-- 2. Altered source versions must be plainly marked as such, and must not be
--    misrepresented as being the original software.
--
-- 3. This notice may not be removed or altered from any source
--    distribution.

export yaml = require "yaml"

--- yaml2Table loads a yaml file as text and returns it as a file.
--  Returns a table representing the yaml document and an error string or nil.
yaml2Table = (fname) -> --> table, error
  fin = io.open fname, "r"
  if fin == nil
    return {}, "File read (#{fname}) failed."

  data = fin\read "*all"
  ret = yaml.load data

  fin\close!

  return ret, nil

--- doCommand returns the output and return status of a command.
doCommand = (command) -> --> string, number
  n = os.tmpname!
  code = os.execute command .. " > " .. n
  lines = {}

  for line in io.lines n
    table.insert lines, line

  os.remove n

  lines, code

--- pairsByKeys is an iterator for a table aplhabetically
--  adapted from here: http://www.lua.org/pil/19.3.html
pairsByKeys = (t) -> --> function
  a = {}
  for n in pairs t
    table.insert a, n

  table.sort a

  i = 0
  iter = ->
    i += 1
    if a[i] == nil
      return nil
    else
      return a[i], t[a[i]]
  iter

export commands = {
  up: {"Brings up a development container", ->
    dcommand = "docker run -idt --name #{data.projname}-dev --hostname dev:#{data.projname} "
    path = "/home/#{data.user}/dev/"
    localdir = os.getenv "PWD"

    -- Is this go?
    if data.golang
      path = "/home/#{data.user}/go/src/#{repopath}"

    -- Add source code directory to mount
    dcommand ..= "-v #{localdir}:#{path} "

    -- Add ssh keys if needed
    if data.ssh
      dcommand ..= "-v /home/#{os.getenv "USER"}/.ssh:/home/#{data.user}/.ssh "

    image = data.base
    if data.base == nil and data.overlay ~= nil
      image = "dev-#{data.projname}"

    -- Append image name
    dcommand ..= image

    print "Starting up container for #{data.projname}"

    -- Start up the docker container
    lines, status = doCommand dcommand
    if status ~= 0
      print "Launch failed. Does this container already exist?"
      for _,line in pairs lines
        print "docker: " .. line
      os.exit status

    ctid = lines[#lines]

    -- report to user
    print "#{data.projname}-dev (ctid: #{ctid\sub(1,12)}) running!"
    print "To use this container please attach to it with:"
    print "    $ docker attach #{data.projname}-dev"
  }

  down: { "Destroys a development container", ->
    lines, status = doCommand "docker rm -f #{data.projname}-dev"
    if status ~= 0
      os.exit status

    print "Container destroyed."
  }

  establish: { "Create a Docker image from the manifest", ->
    if data.overlay == nil
      print "Error: docker container overlay not made. Exiting."
      os.exit 1

    dir = os.tmpname!
    os.remove dir

    out, err = doCommand "mkdir #{dir}"
    if err ~= 0
      print "Could not make temporary directory #{dir}. Dying."
      os.exit err

    fout = io.open "#{dir}/Dockerfile", "w"
    fout\write data.overlay
    fout\close!

    proc = io.popen "docker build -t dev-#{data.projname} #{dir}"
    for line in proc\lines!
      print line

    proc\close!

    _, err = doCommand "rm -rf #{dir}"
    if err ~= 0
      os.exit err

    print "Docker image dev-#{data.projname} created."
  }
}

if #arg == 0
  print "dev version 0.2\n"

  print "Usage: dev [command] <manifest>\n"

  print "  if manifest is undefined the default value"
  print "  .dev.yaml will be used.\n"

  print "Available commands:"
  for name,cmd in pairsByKeys commands
    print "%12s   %s"\format(name, cmd[1])

  os.exit 1

command = arg[1]\lower!

if commands[command] ~= nil
  export fname = arg[2]
  if fname == nil
    fname = ".dev.yaml"

  export data, err = yaml2Table fname
  if err ~= nil
    print "Cannot open file #{fname}. Please make sure this file exists or is readable."
    os.exit 1

  commands[command][2]!