Force hook

Force hook

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

ALLOW_FLIGHT = CreateConVar('ppm2_sv_flight', '1', {FCVAR_ARCHIVE, FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow flight for pegasus and alicorns. It obeys PlayerNoClip hook.')
FORCE_ALLOW_FLIGHT = CreateConVar('ppm2_sv_flight_force', '0', {FCVAR_ARCHIVE, FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Ignore PlayerNoClip hook')
FLIGHT_DAMAGE = CreateConVar('ppm2_sv_flightdmg', '1', {FCVAR_ARCHIVE, FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Damage players in flight')

class PonyflyController
    new: (data) =>
        @controller = data
        @ent = data.ent
        @speedMult = 0.5
        @speedMultDirections = 1.25
        @dragMult = 0.95
        @obbCenter = Vector(0, 0, 0)
        @obbMins = Vector(0, 0, 0)
        @obbMaxs = Vector(0, 0, 0)
        @speedMultDiv = 2
        @speedMultLift = 2
        @pitch = 0
        @yaw = 0
        @roll = 0
        @angleLerp = 1
        @lastVelocity = Vector(0, 0, 0)
        @lastState = false

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

    Switch: (status = false) =>
        @ent = @controller.ent
        return if not IsValid(@ent)
        return if @lastState == status
        @lastState = status
        if not status
            {:p, :y, :r} = @ent\EyeAngles()
            newAng = Angle(p, y, 0)
            @ent\SetEyeAngles(newAng)
            @ent\SetMoveType(MOVETYPE_WALK)
            @roll = 0
            @pitch = 0
            @yaw = 0
            @ent\SetVelocity(@lastVelocity * 50)
            @lastVelocity = Vector(0, 0, 0)
        else
            @lastVelocity = Vector(0, 0, 0)
            @ent\SetVelocity(-@ent\GetVelocity() * .97)
            @ent\SetMoveType(MOVETYPE_CUSTOM)
            @obbCenter = @ent\OBBCenter()
            @obbMins = @ent\OBBMins()
            @obbMaxs = @ent\OBBMaxs()
            @roll = 0
            @pitch = 0
            @yaw = 0
    Think: (movedata) =>
        pos     = movedata\GetOrigin()
        ang     = movedata\GetAngles()
        fwd     = ang\Forward()
        bcwd    = -ang\Forward()
        right   = ang\Right()
        left    = -ang\Right()
        up      = ang\Up()
        down    = -ang\Up()
        W       = movedata\KeyDown(IN_FORWARD)
        S       = movedata\KeyDown(IN_BACK)
        D       = movedata\KeyDown(IN_MOVERIGHT)
        A       = movedata\KeyDown(IN_MOVELEFT)
        CTRL    = movedata\KeyDown(IN_DUCK)
        MULT    = FrameTime() * 66

        velocity = movedata\GetVelocity()
        cSpeed = velocity\Length()
        cSpeed = 1 if cSpeed < 1
        dragSqrt = math.min(math.sqrt(cSpeed) / cSpeed * 2, 0.99)
        cSpeedMult = @speedMultDiv / cSpeed
        cSpeedMult = @speedMultDiv if cSpeedMult ~= cSpeedMult
        cSpeedLiftMult = @speedMultLift / cSpeed
        cSpeedLiftMult = @speedMultLift if cSpeedLiftMult ~= cSpeedLiftMult

        dragCalc = math.Clamp(@dragMult / dragSqrt, 0, 0.99)
        pitch = 0
        yaw = 0
        roll = 0

        hit = false
        hitLift = false

        if W
            velocity += fwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            pitch += 20

        if S
            velocity += bcwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            pitch -= 20

        if A
            velocity += left * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            roll -= 20

        if D
            velocity += right * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            roll += 20

        if CTRL
            velocity += Vector(0, 0, -MULT * @speedMult * cSpeedLiftMult)
            hitLift = true

        if @isLiftingUp
            velocity += Vector(0, 0, MULT * @speedMult * cSpeedLiftMult)
            hitLift = true

        if CLIENT
            lerpMult = FrameTime() * @angleLerp
            {:p, :y, :r} = ang
            p -= @pitch
            y -= @yaw
            @pitch = Lerp(lerpMult, @pitch, pitch)
            @yaw = Lerp(lerpMult, @yaw, yaw)
            @roll = Lerp(lerpMult, @roll, roll)
            p += @pitch
            y += @yaw
            r = @roll + math.sin(RealTime()) * 2
            newAng = Angle(p, y, r)
            @ent\SetEyeAngles(newAng)

        if not hit
            velocity.x *= dragCalc
            velocity.y *= dragCalc

        if not hitLift
            velocity.z *= dragCalc
            velocity.z += math.sin(RealTime() * 2) * .01

        pos += velocity

        movedata\SetVelocity(velocity)
        movedata\SetOrigin(pos)
        return true

    SetupMove: (movedata, cmd) =>
        @isLiftingUp = movedata\KeyDown(IN_JUMP)
        if @isLiftingUp
            movedata\SetButtons(movedata\GetButtons() - IN_JUMP)
        cmd\SetButtons(cmd\GetButtons() - IN_JUMP) if cmd\KeyDown(IN_JUMP)

    FinishMove: (movedata) =>
        mvPos = movedata\GetOrigin()
        pos = @ent\GetPos()
        rpos = pos + @obbCenter
        tryMove = util.TraceHull({
            filter: (ent) ->
                return false if @ent == ent
                return true if not IsValid(ent)
                collision = ent\GetCollisionGroup()
                return false if collision == COLLISION_GROUP_WORLD
                return false if collision == COLLISION_GROUP_DEBRIS
                return false if collision == COLLISION_GROUP_DEBRIS_TRIGGER
                return false if collision == COLLISION_GROUP_WEAPON
                return false if collision == COLLISION_GROUP_PASSABLE_DOOR
                return true
            mins: @obbMins
            maxs: @obbMaxs
            start: rpos
            endpos: mvPos
        })

        velocity = movedata\GetVelocity()
        newVelocity = velocity
        length = velocity\Length()

        if not tryMove.Hit
            @ent\SetPos(mvPos)
        else
            if IsValid(tryMove.Entity)
                newVelocity = Vector(0, 0, 0)
                movedata\SetVelocity(newVelocity)
                newPos = tryMove.HitPos + tryMove.HitNormal
                @ent\SetPos(newPos)
                movedata\SetOrigin(newPos)
            else
                newVelocity = velocity - tryMove.HitNormal * velocity\Dot(tryMove.HitNormal * 1.1)
                movedata\SetVelocity(newVelocity)
                newPos = tryMove.HitPos + tryMove.HitNormal
                @ent\SetPos(newPos)
                movedata\SetOrigin(newPos)
            if length > 7 and SERVER and FLIGHT_DAMAGE\GetBool()
                dmgInfo = DamageInfo()
                dmgInfo\SetAttacker(@ent)
                dmgInfo\SetInflictor(@ent)
                dmgInfo\SetDamageType(DMG_CRUSH)
                calcDamage = math.Clamp((length / 4) ^ 2, 1, 100)
                if calcDamage >= 100
                    @ent\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
                elseif calcDamage > 70
                    @ent\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
                elseif calcDamage > 50
                    @ent\EmitSound("physics/body/body_medium_break#{math.random(2, 4)}.wav", 75)
                elseif calcDamage > 25
                    @ent\EmitSound("physics/body/body_medium_impact_hard#{math.random(1, 6)}.wav", 75)
                elseif calcDamage > 10
                    @ent\EmitSound("physics/body/body_medium_impact_soft#{math.random(1, 7)}.wav", 75)
                else
                    @ent\EmitSound("physics/flesh/flesh_impact_bullet#{math.random(1, 5)}.wav", 75)
                dmgInfo\SetDamage(calcDamage)
                @ent\TakeDamageInfo(dmgInfo)
        @lastVelocity = newVelocity

PPM2.PonyflyController = PonyflyController

hook.Add 'SetupMove', 'PPM2.Ponyfly', (movedata, cmd) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    flight = data\GetFlightController()
    return if not flight
    return flight\SetupMove(movedata, cmd)

hook.Add 'Move', 'PPM2.Ponyfly', (movedata) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    flight = data\GetFlightController()
    return if not flight
    return flight\Think(movedata)

hook.Add 'FinishMove', 'PPM2.Ponyfly', (movedata) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    flight = data\GetFlightController()
    return if not flight
    return flight\FinishMove(movedata)

hook.Add 'CalcMainActivity', 'PPM2.Ponyfly', (movedata) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    return ACT_SWIM, 370

if SERVER
    concommand.Add 'ppm2_fly', =>
        return if not ALLOW_FLIGHT\GetBool()
        return if not IsValid(@)
        return if not @IsPonyCached()
        data = @GetPonyData()
        return if not data
        if data\GetFly()
            return data\SetFly(false) if FORCE_ALLOW_FLIGHT\GetBool()
            can = hook.Run('PlayerNoClip', @, false)
            data\SetFly(false) if can ~= false
        else
            return if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
            return data\SetFly(true) if FORCE_ALLOW_FLIGHT\GetBool()
            can = hook.Run('PlayerNoClip', @, true)
            data\SetFly(true) if can ~= false
else
    lastDouble = 0
    hook.Add 'PlayerBindPress', 'PPM2.Ponyfly', (bind = '', pressed = false) =>
        return if not ALLOW_FLIGHT\GetBool()
        return if not pressed
        return if bind ~= '+jump' and bind ~= 'jump'
        if lastDouble > RealTime()
            return if not @IsPonyCached()
            data = @GetPonyData()
            return if not data
            if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
                PPM2.ChatPrint('You need to be a Pegasus or an Alicorn to fly!')
                return
            RunConsoleCommand('ppm2_fly')
            lastDouble = 0
            return
        lastDouble = RealTime() + 0.2

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

ALLOW_FLIGHT = CreateConVar('ppm2_sv_flight', '1', {FCVAR_ARCHIVE, FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow flight for pegasus and alicorns. It obeys PlayerNoClip hook.')
FLIGHT_DAMAGE = CreateConVar('ppm2_sv_flightdmg', '1', {FCVAR_ARCHIVE, FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Damage players in flight')

class PonyflyController
    new: (data) =>
        @controller = data
        @ent = data.ent
        @speedMult = 0.5
        @speedMultDirections = 1.25
        @dragMult = 0.95
        @obbCenter = Vector(0, 0, 0)
        @obbMins = Vector(0, 0, 0)
        @obbMaxs = Vector(0, 0, 0)
        @speedMultDiv = 2
        @speedMultLift = 2
        @pitch = 0
        @yaw = 0
        @roll = 0
        @angleLerp = 1
        @lastVelocity = Vector(0, 0, 0)
        @lastState = false

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

    Switch: (status = false) =>
        @ent = @controller.ent
        return if not IsValid(@ent)
        return if @lastState == status
        @lastState = status
        if not status
            {:p, :y, :r} = @ent\EyeAngles()
            newAng = Angle(p, y, 0)
            @ent\SetEyeAngles(newAng)
            @ent\SetMoveType(MOVETYPE_WALK)
            @roll = 0
            @pitch = 0
            @yaw = 0
            @ent\SetVelocity(@lastVelocity * 50)
            @lastVelocity = Vector(0, 0, 0)
        else
            @lastVelocity = Vector(0, 0, 0)
            @ent\SetVelocity(-@ent\GetVelocity() * .97)
            @ent\SetMoveType(MOVETYPE_CUSTOM)
            @obbCenter = @ent\OBBCenter()
            @obbMins = @ent\OBBMins()
            @obbMaxs = @ent\OBBMaxs()
            @roll = 0
            @pitch = 0
            @yaw = 0
    Think: (movedata) =>
        pos     = movedata\GetOrigin()
        ang     = movedata\GetAngles()
        fwd     = ang\Forward()
        bcwd    = -ang\Forward()
        right   = ang\Right()
        left    = -ang\Right()
        up      = ang\Up()
        down    = -ang\Up()
        W       = movedata\KeyDown(IN_FORWARD)
        S       = movedata\KeyDown(IN_BACK)
        D       = movedata\KeyDown(IN_MOVERIGHT)
        A       = movedata\KeyDown(IN_MOVELEFT)
        CTRL    = movedata\KeyDown(IN_DUCK)
        MULT    = FrameTime() * 66

        velocity = movedata\GetVelocity()
        cSpeed = velocity\Length()
        cSpeed = 1 if cSpeed < 1
        dragSqrt = math.min(math.sqrt(cSpeed) / cSpeed * 2, 0.99)
        cSpeedMult = @speedMultDiv / cSpeed
        cSpeedMult = @speedMultDiv if cSpeedMult ~= cSpeedMult
        cSpeedLiftMult = @speedMultLift / cSpeed
        cSpeedLiftMult = @speedMultLift if cSpeedLiftMult ~= cSpeedLiftMult

        dragCalc = math.Clamp(@dragMult / dragSqrt, 0, 0.99)
        pitch = 0
        yaw = 0
        roll = 0

        hit = false
        hitLift = false

        if W
            velocity += fwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            pitch += 20

        if S
            velocity += bcwd * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            pitch -= 20

        if A
            velocity += left * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            roll -= 20

        if D
            velocity += right * MULT * @speedMult * cSpeedMult * @speedMultDirections
            hit = true
            roll += 20

        if CTRL
            velocity += Vector(0, 0, -MULT * @speedMult * cSpeedLiftMult)
            hitLift = true

        if @isLiftingUp
            velocity += Vector(0, 0, MULT * @speedMult * cSpeedLiftMult)
            hitLift = true

        if CLIENT
            lerpMult = FrameTime() * @angleLerp
            {:p, :y, :r} = ang
            p -= @pitch
            y -= @yaw
            @pitch = Lerp(lerpMult, @pitch, pitch)
            @yaw = Lerp(lerpMult, @yaw, yaw)
            @roll = Lerp(lerpMult, @roll, roll)
            p += @pitch
            y += @yaw
            r = @roll + math.sin(RealTime()) * 2
            newAng = Angle(p, y, r)
            @ent\SetEyeAngles(newAng)

        if not hit
            velocity.x *= dragCalc
            velocity.y *= dragCalc

        if not hitLift
            velocity.z *= dragCalc
            velocity.z += math.sin(RealTime() * 2) * .01

        pos += velocity

        movedata\SetVelocity(velocity)
        movedata\SetOrigin(pos)
        return true

    SetupMove: (movedata, cmd) =>
        @isLiftingUp = movedata\KeyDown(IN_JUMP)
        if @isLiftingUp
            movedata\SetButtons(movedata\GetButtons() - IN_JUMP)
        cmd\SetButtons(cmd\GetButtons() - IN_JUMP) if cmd\KeyDown(IN_JUMP)

    FinishMove: (movedata) =>
        mvPos = movedata\GetOrigin()
        pos = @ent\GetPos()
        rpos = pos + @obbCenter
        tryMove = util.TraceHull({
            filter: (ent) ->
                return false if @ent == ent
                return true if not IsValid(ent)
                collision = ent\GetCollisionGroup()
                return false if collision == COLLISION_GROUP_WORLD
                return false if collision == COLLISION_GROUP_DEBRIS
                return false if collision == COLLISION_GROUP_DEBRIS_TRIGGER
                return false if collision == COLLISION_GROUP_WEAPON
                return false if collision == COLLISION_GROUP_PASSABLE_DOOR
                return true
            mins: @obbMins
            maxs: @obbMaxs
            start: rpos
            endpos: mvPos
        })

        velocity = movedata\GetVelocity()
        newVelocity = velocity
        length = velocity\Length()

        if not tryMove.Hit
            @ent\SetPos(mvPos)
        else
            if IsValid(tryMove.Entity)
                newVelocity = Vector(0, 0, 0)
                movedata\SetVelocity(newVelocity)
                newPos = tryMove.HitPos + tryMove.HitNormal
                @ent\SetPos(newPos)
                movedata\SetOrigin(newPos)
            else
                newVelocity = velocity - tryMove.HitNormal * velocity\Dot(tryMove.HitNormal * 1.1)
                movedata\SetVelocity(newVelocity)
                newPos = tryMove.HitPos + tryMove.HitNormal
                @ent\SetPos(newPos)
                movedata\SetOrigin(newPos)
            if length > 7 and SERVER and FLIGHT_DAMAGE\GetBool()
                dmgInfo = DamageInfo()
                dmgInfo\SetAttacker(@ent)
                dmgInfo\SetInflictor(@ent)
                dmgInfo\SetDamageType(DMG_CRUSH)
                calcDamage = math.Clamp((length / 4) ^ 2, 1, 100)
                if calcDamage >= 100
                    @ent\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
                elseif calcDamage > 70
                    @ent\EmitSound('physics/flesh/flesh_bloody_break.wav', 100)
                elseif calcDamage > 50
                    @ent\EmitSound("physics/body/body_medium_break#{math.random(2, 4)}.wav", 75)
                elseif calcDamage > 25
                    @ent\EmitSound("physics/body/body_medium_impact_hard#{math.random(1, 6)}.wav", 75)
                elseif calcDamage > 10
                    @ent\EmitSound("physics/body/body_medium_impact_soft#{math.random(1, 7)}.wav", 75)
                else
                    @ent\EmitSound("physics/flesh/flesh_impact_bullet#{math.random(1, 5)}.wav", 75)
                dmgInfo\SetDamage(calcDamage)
                @ent\TakeDamageInfo(dmgInfo)
        @lastVelocity = newVelocity

PPM2.PonyflyController = PonyflyController

hook.Add 'SetupMove', 'PPM2.Ponyfly', (movedata, cmd) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    flight = data\GetFlightController()
    return if not flight
    return flight\SetupMove(movedata, cmd)

hook.Add 'Move', 'PPM2.Ponyfly', (movedata) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    flight = data\GetFlightController()
    return if not flight
    return flight\Think(movedata)

hook.Add 'FinishMove', 'PPM2.Ponyfly', (movedata) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    flight = data\GetFlightController()
    return if not flight
    return flight\FinishMove(movedata)

hook.Add 'CalcMainActivity', 'PPM2.Ponyfly', (movedata) =>
    return if not @IsPonyCached()
    data = @GetPonyData()
    return if not data
    return if not data\GetFly()
    return ACT_SWIM, 370

if SERVER
    concommand.Add 'ppm2_fly', =>
        return if not ALLOW_FLIGHT\GetBool()
        return if not IsValid(@)
        return if not @IsPonyCached()
        data = @GetPonyData()
        return if not data
        if data\GetFly()
            can = hook.Run('PlayerNoClip', @, false)
            data\SetFly(false) if can ~= false
        else
            return if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
            can = hook.Run('PlayerNoClip', @, true)
            data\SetFly(true) if can ~= false
else
    lastDouble = 0
    hook.Add 'PlayerBindPress', 'PPM2.Ponyfly', (bind = '', pressed = false) =>
        return if not ALLOW_FLIGHT\GetBool()
        return if not pressed
        return if bind ~= '+jump' and bind ~= 'jump'
        if lastDouble > RealTime()
            return if not @IsPonyCached()
            data = @GetPonyData()
            return if not data
            if data\GetRace() ~= PPM2.RACE_PEGASUS and data\GetRace() ~= PPM2.RACE_ALICORN
                PPM2.ChatPrint('You need to be a Pegasus or an Alicorn to fly!')
                return
            RunConsoleCommand('ppm2_fly')
            lastDouble = 0
            return
        lastDouble = RealTime() + 0.2


Added softened horn

Added softened horn

--
-- Copyright (C) 2017-2019 DBot

-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:

-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

PPM2.ALLOW_TO_MODIFY_SCALE = CreateConVar('ppm2_sv_allow_resize', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Allow to resize ponies. Disables resizing completely (visual; mechanical)')

player_manager.AddValidModel('pony', 'models/ppm/player_default_base_new.mdl')
list.Set('PlayerOptionsModel', 'pony', 'models/ppm/player_default_base_new.mdl')

player_manager.AddValidModel('ponynj', 'models/ppm/player_default_base_new_nj.mdl')
list.Set('PlayerOptionsModel', 'ponynj', 'models/ppm/player_default_base_new_nj.mdl')

player_manager.AddValidModel('ponynj_old', 'models/ppm/player_default_base_nj.mdl')
list.Set('PlayerOptionsModel', 'ponynj_old', 'models/ppm/player_default_base_nj.mdl')

player_manager.AddValidModel('pony_old', 'models/ppm/player_default_base.mdl')
list.Set('PlayerOptionsModel', 'pony_old', 'models/ppm/player_default_base.mdl')

player_manager.AddValidModel('pony_cppm', 'models/cppm/player_default_base.mdl')
list.Set('PlayerOptionsModel', 'pony_cppm', 'models/cppm/player_default_base.mdl')

player_manager.AddValidModel('ponynj_cppm', 'models/cppm/player_default_base_nj.mdl')
list.Set('PlayerOptionsModel', 'ponynj_cppm', 'models/cppm/player_default_base_nj.mdl')

player_manager.AddValidHands(model, 'models/ppm/c_arms_pony.mdl', 0, '') for model in *{'pony', 'pony_cppm', 'ponynj', 'ponynj_cppm', 'pony_old'}

PPM2.MIN_WEIGHT = 0.7
PPM2.MAX_WEIGHT = 1.5

PPM2.MIN_SCALE = 0.5
PPM2.MAX_SCALE = 1.3

PPM2.PONY_HEIGHT_MODIFIER = 0.64
PPM2.PONY_HEIGHT_MODIFIER_DUCK = 1.12
PPM2.PONY_HEIGHT_MODIFIER_DUCK_HULL = 1

PPM2.MIN_NECK = 0.6
PPM2.MAX_NECK = 1.4

PPM2.MIN_LEGS = 0.6
PPM2.MAX_LEGS = 1.75

PPM2.MIN_SPINE = 0.8
PPM2.MAX_SPINE = 2

PPM2.PONY_JUMP_MODIFIER = 1.4

PPM2.PLAYER_VOFFSET = 64 * PPM2.PONY_HEIGHT_MODIFIER
PPM2.PLAYER_VOFFSET_DUCK = 28 * PPM2.PONY_HEIGHT_MODIFIER_DUCK

PPM2.PLAYER_VIEW_OFFSET = Vector(0, 0, PPM2.PLAYER_VOFFSET)
PPM2.PLAYER_VIEW_OFFSET_DUCK = Vector(0, 0, PPM2.PLAYER_VOFFSET_DUCK)

PPM2.PLAYER_VIEW_OFFSET_ORIGINAL = Vector(0, 0, 64)
PPM2.PLAYER_VIEW_OFFSET_DUCK_ORIGINAL = Vector(0, 0, 28)

PPM2.MIN_TAIL_SIZE = 0.6
PPM2.MAX_TAIL_SIZE = 1.7 -- i luv big tails

PPM2.MIN_IRIS = 0.4
PPM2.MAX_IRIS = 1.3

PPM2.MIN_HOLE = 0.1
PPM2.MAX_HOLE = .95

PPM2.MIN_HOLE_SHIFT = -0.5
PPM2.MAX_HOLE_SHIFT = 0.5

PPM2.MIN_PUPIL_SIZE = 0.2
PPM2.MAX_PUPIL_SIZE = 1

PPM2.MIN_EYE_ROTATION = -180
PPM2.MAX_EYE_ROTATION = 180

PPM2.HAND_BODYGROUP_ID = 0
PPM2.HAND_BODYGROUP_MAGIC = 0
PPM2.HAND_BODYGROUP_HOOVES = 1

PPM2.AvaliableTails = {
	'MAILCALL'
	'FLOOFEH'
	'ADVENTUROUS'
	'SHOWBOAT'
	'ASSERTIVE'
	'BOLD'
	'STUMPY'
	'SPEEDSTER'
	'EDGY'
	'RADICAL'
	'BOOKWORM'
	'BUMPKIN'
	'POOFEH'
	'CURLY'
	'NONE'
}

PPM2.AvailableClothesHead = {
	'EMPTY', 'APPLEJACK_HAT', 'BRAEBURN_HAT', 'TRIXIE_HAT', 'HEADPHONES'
}

PPM2.AvailableClothesNeck = {
	'EMPTY', 'SCARF', 'TRIXIE_CAPE', 'TIE', 'BOWTIE'
}

PPM2.AvailableClothesBody = {
	'EMPTY', 'VEST', 'SHIRT', 'HOODIE', 'WONDERBOLTS_BADGE'
}

PPM2.AvailableClothesEye = {
	'EMPTY', 'GOGGLES_ROUND_FEMALE', 'GOGGLES_ROUND_MALE', 'SHADES_FEMALE', 'SHADES_MALE'
	'MONOCLE_FEMALE', 'MONOCLE_MALE', 'EYEPATH_FEMALE', 'EYEPATH_MALE'
}

PPM2.MAX_CLOTHES_COLORS = 6

PPM2.AvailableHorns = {
	'EMPTY', 'CUSTOM', 'CLASSIC_SHARP', 'CLASSIC', 'BROKEN', 'LONG'
	'LONG_CURLED', 'POISON_JOKE', 'CHANGELING'
	'CHANGELING_QUEEN', 'KIRIN'
}

PPM2.AvaliableUpperManes = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT', 'ASSERTIVE'
	'BOLD', 'STUMPY', 'SPEEDSTER', 'RADICAL', 'SPIKED'
	'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY', 'INSTRUCTOR', 'NONE'
}

PPM2.AvaliableLowerManes = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
	'ASSERTIVE', 'BOLD', 'STUMPY', 'HIPPIE', 'SPEEDSTER'
	'BOOKWORM', 'BUMPKIN', 'CURLY', 'NONE'
}

PPM2.EyelashTypes = {
	'Default', 'Double', 'Coy', 'Full', 'Mess', 'None'
}

PPM2.BodyDetails = {
	'None', 'Leg gradient', 'Lines', 'Stripes', 'Head stripes'
	'Freckles', 'Hooves big', 'Hooves small', 'Head layer'
	'Hooves big rnd', 'Hooves small rnd', 'Spots 1', 'Robotic'
	'DASH-E', 'Eye Scar', 'Eye Wound', 'Scars', 'MGS Socks'
	'Sharp Hooves', 'Sharp Hooves 2', 'Muzzle', 'Eye Scar Left'
	'Eye Scar Right'

	'Albedo Printed Plate Skin'
	'Paintable Printed Plate Skin'
	'Albedo Printed Plate Strip'
	'Paintable Printed Plate Strip'
	'Cow Details'
	'Deer Details'
	'Extended Deer Details'
	'Demonic'
	'Ear Inner Detail'
	'Zebra Details'
}

PPM2.BodyDetailsEnum = {
	'NONE', 'GRADIENT', 'LINES', 'STRIPES', 'HSTRIPES'
	'FRECKLES', 'HOOF_BIG', 'HOOF_SMALL', 'LAYER'
	'HOOF_BIG_ROUND', 'HOOF_SMALL_ROUND', 'SPOTS', 'ROBOTIC'
	'DASH_E', 'EYE_SCAR', 'EYE_WOUND', 'SCARS', 'MGS_SOCKS'
	'SHARP_HOOVES', 'SHARP_HOOVES_2', 'MUZZLE', 'EYE_SCAR_LEFT'
	'EYE_SCAR_RIGHT'

	'ALBEDO_ANDROID'
	'PAINT_ANDROID'
	'ALBEDO_ANDROID_STRIP'
	'PAINT_ANDROID_STRIP'
	'COW'
	'DEER'
	'DEER_EXTENDED'
	'DEMONIC'
	'EAR_INNER'
	'ZEBRA_DETAILS'
}

PPM2.SocksTypes = {
	'DEFAULT'
	'GEOMETRIC1'
	'GEOMETRIC2'
	'GEOMETRIC3'
	'GEOMETRIC4'
	'GEOMETRIC5'
	'GEOMETRIC6'
	'GEOMETRIC7'
	'GEOMETRIC8'
	'DARK1'
	'FLOWERS10'
	'FLOWERS11'
	'FLOWERS12'
	'FLOWERS13'
	'FLOWERS14'
	'FLOWERS15'
	'FLOWERS16'
	'FLOWERS17'
	'FLOWERS18'
	'FLOWERS19'
	'FLOWERS2'
	'FLOWERS20'
	'FLOWERS3'
	'FLOWERS4'
	'FLOWERS5'
	'FLOWERS6'
	'FLOWERS7'
	'FLOWERS8'
	'FLOWERS9'
	'GREY1'
	'GREY2'
	'GREY3'
	'HEARTS1'
	'HEARTS2'
	'SNOW1'
	'WALLPAPER1'
	'WALLPAPER2'
	'WALLPAPER3'
}

PPM2.AvaliableLightwarps = {
	'SFM_PONY'
	'AIRBRUSH'
	'HARD_LIGHT'
	'PURPLE_SKY'
	'SPAWN'
	'TF2'
	'TF2_CINEMATIC'
	'TF2_CLASSIC'
	'WELL_OILED'
}

PPM2.MAX_LIGHTWARP = #PPM2.AvaliableLightwarps - 1

PPM2.AvaliableLightwarpsPaths = ['models/ppm2/lightwarps/' .. mat\lower() for _, mat in ipairs PPM2.AvaliableLightwarps]

PPM2.DefaultCutiemarks = {
	'8ball', 'dice', 'magichat',
	'magichat02', 'record', 'microphone',
	'bits', 'checkered', 'lumps',
	'mirror', 'camera', 'magnifier',
	'padlock', 'binaryfile', 'floppydisk',
	'cube', 'bulb', 'battery',
	'deskfan', 'flames', 'alarm',
	'myon', 'beer', 'berryglass',
	'roadsign', 'greentree', 'seasons',
	'palette', 'palette02', 'palette03',
	'lightningstone', 'partiallycloudy', 'thunderstorm',
	'storm', 'stoppedwatch', 'twistedclock',
	'surfboard', 'surfboard02', 'star',
	'ussr', 'vault', 'anarchy',
	'suit', 'deathscythe', 'shoop',
	'smiley', 'dawsome', 'weegee'
	'applej', 'applem', 'bon_bon', 'carrots', 'celestia', 'cloudy', 'custom01', 'custom02', 'derpy', 'firezap',
	'fluttershy', 'fruits', 'island', 'lyra', 'mine', 'note', 'octavia', 'pankk', 'pinkie_pie', 'rainbow_dash',
	'rarity', 'rosen', 'sflowers', 'storm', 'time', 'time2', 'trixie', 'twilight', 'waters', 'weer', 'zecora'
}

PPM2.AvaliableUpperManesNew = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT', 'ASSERTIVE'
	'BOLD', 'STUMPY', 'SPEEDSTER', 'RADICAL', 'SPIKED'
	'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY', 'INSTRUCTOR'
	'TIMID', 'FILLY', 'MECHANIC', 'MOON', 'CLOUD'
	'DRUNK', 'EMO'
	'NONE'
}

PPM2.AvaliableLowerManesNew = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
	'ASSERTIVE', 'BOLD', 'STUMPY', 'HIPPIE', 'SPEEDSTER'
	'BOOKWORM', 'BUMPKIN', 'CURLY'
	'TIMID', 'MOON', 'BUN', 'CLOUD', 'EMO'
	'NONE'
}

PPM2.AvaliableTailsNew = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
	'ASSERTIVE', 'BOLD', 'STUMPY', 'SPEEDSTER', 'EDGY'
	'RADICAL', 'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY'
	'NONE'
}

PPM2.AvaliableEyeTypes = {
	'DEFAULT', 'APERTURE'
}

PPM2.AvaliableEyeReflections = {
	'DEFAULT', 'CRYSTAL_FOAL', 'CRYSTAL'
	'FOAL', 'MALE'
}

PPM2.AvaliablePonyWings = {'DEFAULT', 'BATPONY'}

PPM2.AvaliablePonySuits = {'NONE', 'ROYAL_GUARD', 'SHADOWBOLTS_FULL', 'SHADOWBOLTS_LIGHT', 'WONDERBOLTS_FULL', 'WONDERBOLTS_LIGHT', 'SPIDERMANE_LIGHT', 'SPIDERMANE_FULL'}

do
	i = -1
	for _, mark in ipairs PPM2.DefaultCutiemarks
		i += 1
		PPM2["CMARK_#{mark\upper()}"] = i

PPM2.MIN_EYELASHES = 0
PPM2.MAX_EYELASHES = #PPM2.EyelashTypes - 1
PPM2.EYELASHES_NONE = #PPM2.EyelashTypes - 1

PPM2.MIN_TAILS = 0
PPM2.MAX_TAILS = #PPM2.AvaliableTails - 1

PPM2.MIN_TAILS_NEW = 0
PPM2.MAX_TAILS_NEW = #PPM2.AvaliableTailsNew - 1

PPM2.MIN_UPPER_MANES = 0
PPM2.MAX_UPPER_MANES = #PPM2.AvaliableUpperManes - 1

PPM2.MIN_LOWER_MANES = 0
PPM2.MAX_LOWER_MANES = #PPM2.AvaliableLowerManes - 1

PPM2.MIN_UPPER_MANES_NEW = 0
PPM2.MAX_UPPER_MANES_NEW = #PPM2.AvaliableUpperManesNew - 1

PPM2.MIN_LOWER_MANES_NEW = 0
PPM2.MAX_LOWER_MANES_NEW = #PPM2.AvaliableLowerManesNew - 1

PPM2.MIN_EYE_TYPE = 0
PPM2.MAX_EYE_TYPE = #PPM2.AvaliableEyeTypes - 1

PPM2.MIN_DETAIL = 0
PPM2.MAX_DETAIL = #PPM2.BodyDetails - 1

PPM2.MIN_CMARK = 0
PPM2.MAX_CMARK = #PPM2.DefaultCutiemarks - 1

PPM2.MIN_SUIT = 0
PPM2.MAX_SUIT = #PPM2.AvaliablePonySuits - 1

PPM2.MIN_WINGS = 0
PPM2.MAX_WINGS = #PPM2.AvaliablePonyWings - 1

PPM2.MIN_SOCKS = 0
PPM2.MAX_SOCKS = #PPM2.SocksTypes - 1

PPM2.GENDER_FEMALE = 0
PPM2.GENDER_MALE = 1

PPM2.MAX_BODY_DETAILS = 8

PPM2.RACE_EARTH = 0
PPM2.RACE_PEGASUS = 1
PPM2.RACE_UNICORN = 2
PPM2.RACE_ALICORN = 3
PPM2.RACE_ENUMS = {'EARTH', 'PEGASUS', 'UNICORN', 'ALICORN'}

PPM2.RACE_HAS_HORN = 0x1
PPM2.RACE_HAS_WINGS = 0x2

PPM2.AGE_FILLY = 0
PPM2.AGE_ADULT = 1
PPM2.AGE_MATURE = 2
PPM2.AGE_ENUMS = {'FILLY', 'ADULT', 'MATURE'}

PPM2.MIN_DERP_STRENGTH = 0.1
PPM2.MAX_DERP_STRENGTH = 1.3

PPM2.MIN_MALE_BUFF = 0
PPM2.DEFAULT_MALE_BUFF = 1
PPM2.MAX_MALE_BUFF = 2

PPM2.MAX_TATTOOS = 25

PPM2.TATTOOS_REGISTRY = {
	'NONE', 'ARROW', 'BLADES', 'CROSS', 'DIAMONDINNER', 'DIAMONDOUTER'
	'DRACO', 'EVILHEART', 'HEARTWAVE', 'JUNCTION', 'NOTE', 'NOTE2'
	'TATTOO1', 'TATTOO2', 'TATTOO3', 'TATTOO4', 'TATTOO5', 'TATTOO6', 'TATTOO7'
	'WING', 'HEART'
}

PPM2.MIN_TATTOOS = 0
PPM2.MAX_TATTOOS = #PPM2.TATTOOS_REGISTRY - 1

PPM2.MIN_WING = 0.1
PPM2.MIN_WINGX = -10
PPM2.MIN_WINGY = -10
PPM2.MIN_WINGZ = -10
PPM2.MAX_WING = 2
PPM2.MAX_WINGX = 10
PPM2.MAX_WINGY = 10
PPM2.MAX_WINGZ = 10

--
-- Copyright (C) 2017-2019 DBot

-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do so,
-- subject to the following conditions:

-- The above copyright notice and this permission notice shall be included in all copies
-- or substantial portions of the Software.

-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
-- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
-- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
-- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
-- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-- DEALINGS IN THE SOFTWARE.

PPM2.ALLOW_TO_MODIFY_SCALE = CreateConVar('ppm2_sv_allow_resize', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Allow to resize ponies. Disables resizing completely (visual; mechanical)')

player_manager.AddValidModel('pony', 'models/ppm/player_default_base_new.mdl')
list.Set('PlayerOptionsModel', 'pony', 'models/ppm/player_default_base_new.mdl')

player_manager.AddValidModel('ponynj', 'models/ppm/player_default_base_new_nj.mdl')
list.Set('PlayerOptionsModel', 'ponynj', 'models/ppm/player_default_base_new_nj.mdl')

player_manager.AddValidModel('ponynj_old', 'models/ppm/player_default_base_nj.mdl')
list.Set('PlayerOptionsModel', 'ponynj_old', 'models/ppm/player_default_base_nj.mdl')

player_manager.AddValidModel('pony_old', 'models/ppm/player_default_base.mdl')
list.Set('PlayerOptionsModel', 'pony_old', 'models/ppm/player_default_base.mdl')

player_manager.AddValidModel('pony_cppm', 'models/cppm/player_default_base.mdl')
list.Set('PlayerOptionsModel', 'pony_cppm', 'models/cppm/player_default_base.mdl')

player_manager.AddValidModel('ponynj_cppm', 'models/cppm/player_default_base_nj.mdl')
list.Set('PlayerOptionsModel', 'ponynj_cppm', 'models/cppm/player_default_base_nj.mdl')

player_manager.AddValidHands(model, 'models/ppm/c_arms_pony.mdl', 0, '') for model in *{'pony', 'pony_cppm', 'ponynj', 'ponynj_cppm', 'pony_old'}

PPM2.MIN_WEIGHT = 0.7
PPM2.MAX_WEIGHT = 1.5

PPM2.MIN_SCALE = 0.5
PPM2.MAX_SCALE = 1.3

PPM2.PONY_HEIGHT_MODIFIER = 0.64
PPM2.PONY_HEIGHT_MODIFIER_DUCK = 1.12
PPM2.PONY_HEIGHT_MODIFIER_DUCK_HULL = 1

PPM2.MIN_NECK = 0.6
PPM2.MAX_NECK = 1.4

PPM2.MIN_LEGS = 0.6
PPM2.MAX_LEGS = 1.75

PPM2.MIN_SPINE = 0.8
PPM2.MAX_SPINE = 2

PPM2.PONY_JUMP_MODIFIER = 1.4

PPM2.PLAYER_VOFFSET = 64 * PPM2.PONY_HEIGHT_MODIFIER
PPM2.PLAYER_VOFFSET_DUCK = 28 * PPM2.PONY_HEIGHT_MODIFIER_DUCK

PPM2.PLAYER_VIEW_OFFSET = Vector(0, 0, PPM2.PLAYER_VOFFSET)
PPM2.PLAYER_VIEW_OFFSET_DUCK = Vector(0, 0, PPM2.PLAYER_VOFFSET_DUCK)

PPM2.PLAYER_VIEW_OFFSET_ORIGINAL = Vector(0, 0, 64)
PPM2.PLAYER_VIEW_OFFSET_DUCK_ORIGINAL = Vector(0, 0, 28)

PPM2.MIN_TAIL_SIZE = 0.6
PPM2.MAX_TAIL_SIZE = 1.7 -- i luv big tails

PPM2.MIN_IRIS = 0.4
PPM2.MAX_IRIS = 1.3

PPM2.MIN_HOLE = 0.1
PPM2.MAX_HOLE = .95

PPM2.MIN_HOLE_SHIFT = -0.5
PPM2.MAX_HOLE_SHIFT = 0.5

PPM2.MIN_PUPIL_SIZE = 0.2
PPM2.MAX_PUPIL_SIZE = 1

PPM2.MIN_EYE_ROTATION = -180
PPM2.MAX_EYE_ROTATION = 180

PPM2.HAND_BODYGROUP_ID = 0
PPM2.HAND_BODYGROUP_MAGIC = 0
PPM2.HAND_BODYGROUP_HOOVES = 1

PPM2.AvaliableTails = {
	'MAILCALL'
	'FLOOFEH'
	'ADVENTUROUS'
	'SHOWBOAT'
	'ASSERTIVE'
	'BOLD'
	'STUMPY'
	'SPEEDSTER'
	'EDGY'
	'RADICAL'
	'BOOKWORM'
	'BUMPKIN'
	'POOFEH'
	'CURLY'
	'NONE'
}

PPM2.AvailableClothesHead = {
	'EMPTY', 'APPLEJACK_HAT', 'BRAEBURN_HAT', 'TRIXIE_HAT', 'HEADPHONES'
}

PPM2.AvailableClothesNeck = {
	'EMPTY', 'SCARF', 'TRIXIE_CAPE', 'TIE', 'BOWTIE'
}

PPM2.AvailableClothesBody = {
	'EMPTY', 'VEST', 'SHIRT', 'HOODIE', 'WONDERBOLTS_BADGE'
}

PPM2.AvailableClothesEye = {
	'EMPTY', 'GOGGLES_ROUND_FEMALE', 'GOGGLES_ROUND_MALE', 'SHADES_FEMALE', 'SHADES_MALE'
	'MONOCLE_FEMALE', 'MONOCLE_MALE', 'EYEPATH_FEMALE', 'EYEPATH_MALE'
}

PPM2.MAX_CLOTHES_COLORS = 6

PPM2.AvailableHorns = {
	'EMPTY', 'CUSTOM', 'CLASSIC', 'BROKEN', 'LONG',
	'LONG_CURLED', 'POISON_JOKE', 'CHANGELING'
	'CHANGELING_QUEEN', 'KIRIN'
}

PPM2.AvaliableUpperManes = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT', 'ASSERTIVE'
	'BOLD', 'STUMPY', 'SPEEDSTER', 'RADICAL', 'SPIKED'
	'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY', 'INSTRUCTOR', 'NONE'
}

PPM2.AvaliableLowerManes = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
	'ASSERTIVE', 'BOLD', 'STUMPY', 'HIPPIE', 'SPEEDSTER'
	'BOOKWORM', 'BUMPKIN', 'CURLY', 'NONE'
}

PPM2.EyelashTypes = {
	'Default', 'Double', 'Coy', 'Full', 'Mess', 'None'
}

PPM2.BodyDetails = {
	'None', 'Leg gradient', 'Lines', 'Stripes', 'Head stripes'
	'Freckles', 'Hooves big', 'Hooves small', 'Head layer'
	'Hooves big rnd', 'Hooves small rnd', 'Spots 1', 'Robotic'
	'DASH-E', 'Eye Scar', 'Eye Wound', 'Scars', 'MGS Socks'
	'Sharp Hooves', 'Sharp Hooves 2', 'Muzzle', 'Eye Scar Left'
	'Eye Scar Right'

	'Albedo Printed Plate Skin'
	'Paintable Printed Plate Skin'
	'Albedo Printed Plate Strip'
	'Paintable Printed Plate Strip'
	'Cow Details'
	'Deer Details'
	'Extended Deer Details'
	'Demonic'
	'Ear Inner Detail'
	'Zebra Details'
}

PPM2.BodyDetailsEnum = {
	'NONE', 'GRADIENT', 'LINES', 'STRIPES', 'HSTRIPES'
	'FRECKLES', 'HOOF_BIG', 'HOOF_SMALL', 'LAYER'
	'HOOF_BIG_ROUND', 'HOOF_SMALL_ROUND', 'SPOTS', 'ROBOTIC'
	'DASH_E', 'EYE_SCAR', 'EYE_WOUND', 'SCARS', 'MGS_SOCKS'
	'SHARP_HOOVES', 'SHARP_HOOVES_2', 'MUZZLE', 'EYE_SCAR_LEFT'
	'EYE_SCAR_RIGHT'

	'ALBEDO_ANDROID'
	'PAINT_ANDROID'
	'ALBEDO_ANDROID_STRIP'
	'PAINT_ANDROID_STRIP'
	'COW'
	'DEER'
	'DEER_EXTENDED'
	'DEMONIC'
	'EAR_INNER'
	'ZEBRA_DETAILS'
}

PPM2.SocksTypes = {
	'DEFAULT'
	'GEOMETRIC1'
	'GEOMETRIC2'
	'GEOMETRIC3'
	'GEOMETRIC4'
	'GEOMETRIC5'
	'GEOMETRIC6'
	'GEOMETRIC7'
	'GEOMETRIC8'
	'DARK1'
	'FLOWERS10'
	'FLOWERS11'
	'FLOWERS12'
	'FLOWERS13'
	'FLOWERS14'
	'FLOWERS15'
	'FLOWERS16'
	'FLOWERS17'
	'FLOWERS18'
	'FLOWERS19'
	'FLOWERS2'
	'FLOWERS20'
	'FLOWERS3'
	'FLOWERS4'
	'FLOWERS5'
	'FLOWERS6'
	'FLOWERS7'
	'FLOWERS8'
	'FLOWERS9'
	'GREY1'
	'GREY2'
	'GREY3'
	'HEARTS1'
	'HEARTS2'
	'SNOW1'
	'WALLPAPER1'
	'WALLPAPER2'
	'WALLPAPER3'
}

PPM2.AvaliableLightwarps = {
	'SFM_PONY'
	'AIRBRUSH'
	'HARD_LIGHT'
	'PURPLE_SKY'
	'SPAWN'
	'TF2'
	'TF2_CINEMATIC'
	'TF2_CLASSIC'
	'WELL_OILED'
}

PPM2.MAX_LIGHTWARP = #PPM2.AvaliableLightwarps - 1

PPM2.AvaliableLightwarpsPaths = ['models/ppm2/lightwarps/' .. mat\lower() for _, mat in ipairs PPM2.AvaliableLightwarps]

PPM2.DefaultCutiemarks = {
	'8ball', 'dice', 'magichat',
	'magichat02', 'record', 'microphone',
	'bits', 'checkered', 'lumps',
	'mirror', 'camera', 'magnifier',
	'padlock', 'binaryfile', 'floppydisk',
	'cube', 'bulb', 'battery',
	'deskfan', 'flames', 'alarm',
	'myon', 'beer', 'berryglass',
	'roadsign', 'greentree', 'seasons',
	'palette', 'palette02', 'palette03',
	'lightningstone', 'partiallycloudy', 'thunderstorm',
	'storm', 'stoppedwatch', 'twistedclock',
	'surfboard', 'surfboard02', 'star',
	'ussr', 'vault', 'anarchy',
	'suit', 'deathscythe', 'shoop',
	'smiley', 'dawsome', 'weegee'
	'applej', 'applem', 'bon_bon', 'carrots', 'celestia', 'cloudy', 'custom01', 'custom02', 'derpy', 'firezap',
	'fluttershy', 'fruits', 'island', 'lyra', 'mine', 'note', 'octavia', 'pankk', 'pinkie_pie', 'rainbow_dash',
	'rarity', 'rosen', 'sflowers', 'storm', 'time', 'time2', 'trixie', 'twilight', 'waters', 'weer', 'zecora'
}

PPM2.AvaliableUpperManesNew = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT', 'ASSERTIVE'
	'BOLD', 'STUMPY', 'SPEEDSTER', 'RADICAL', 'SPIKED'
	'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY', 'INSTRUCTOR'
	'TIMID', 'FILLY', 'MECHANIC', 'MOON', 'CLOUD'
	'DRUNK', 'EMO'
	'NONE'
}

PPM2.AvaliableLowerManesNew = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
	'ASSERTIVE', 'BOLD', 'STUMPY', 'HIPPIE', 'SPEEDSTER'
	'BOOKWORM', 'BUMPKIN', 'CURLY'
	'TIMID', 'MOON', 'BUN', 'CLOUD', 'EMO'
	'NONE'
}

PPM2.AvaliableTailsNew = {
	'MAILCALL', 'FLOOFEH', 'ADVENTUROUS', 'SHOWBOAT'
	'ASSERTIVE', 'BOLD', 'STUMPY', 'SPEEDSTER', 'EDGY'
	'RADICAL', 'BOOKWORM', 'BUMPKIN', 'POOFEH', 'CURLY'
	'NONE'
}

PPM2.AvaliableEyeTypes = {
	'DEFAULT', 'APERTURE'
}

PPM2.AvaliableEyeReflections = {
	'DEFAULT', 'CRYSTAL_FOAL', 'CRYSTAL'
	'FOAL', 'MALE'
}

PPM2.AvaliablePonyWings = {'DEFAULT', 'BATPONY'}

PPM2.AvaliablePonySuits = {'NONE', 'ROYAL_GUARD', 'SHADOWBOLTS_FULL', 'SHADOWBOLTS_LIGHT', 'WONDERBOLTS_FULL', 'WONDERBOLTS_LIGHT', 'SPIDERMANE_LIGHT', 'SPIDERMANE_FULL'}

do
	i = -1
	for _, mark in ipairs PPM2.DefaultCutiemarks
		i += 1
		PPM2["CMARK_#{mark\upper()}"] = i

PPM2.MIN_EYELASHES = 0
PPM2.MAX_EYELASHES = #PPM2.EyelashTypes - 1
PPM2.EYELASHES_NONE = #PPM2.EyelashTypes - 1

PPM2.MIN_TAILS = 0
PPM2.MAX_TAILS = #PPM2.AvaliableTails - 1

PPM2.MIN_TAILS_NEW = 0
PPM2.MAX_TAILS_NEW = #PPM2.AvaliableTailsNew - 1

PPM2.MIN_UPPER_MANES = 0
PPM2.MAX_UPPER_MANES = #PPM2.AvaliableUpperManes - 1

PPM2.MIN_LOWER_MANES = 0
PPM2.MAX_LOWER_MANES = #PPM2.AvaliableLowerManes - 1

PPM2.MIN_UPPER_MANES_NEW = 0
PPM2.MAX_UPPER_MANES_NEW = #PPM2.AvaliableUpperManesNew - 1

PPM2.MIN_LOWER_MANES_NEW = 0
PPM2.MAX_LOWER_MANES_NEW = #PPM2.AvaliableLowerManesNew - 1

PPM2.MIN_EYE_TYPE = 0
PPM2.MAX_EYE_TYPE = #PPM2.AvaliableEyeTypes - 1

PPM2.MIN_DETAIL = 0
PPM2.MAX_DETAIL = #PPM2.BodyDetails - 1

PPM2.MIN_CMARK = 0
PPM2.MAX_CMARK = #PPM2.DefaultCutiemarks - 1

PPM2.MIN_SUIT = 0
PPM2.MAX_SUIT = #PPM2.AvaliablePonySuits - 1

PPM2.MIN_WINGS = 0
PPM2.MAX_WINGS = #PPM2.AvaliablePonyWings - 1

PPM2.MIN_SOCKS = 0
PPM2.MAX_SOCKS = #PPM2.SocksTypes - 1

PPM2.GENDER_FEMALE = 0
PPM2.GENDER_MALE = 1

PPM2.MAX_BODY_DETAILS = 8

PPM2.RACE_EARTH = 0
PPM2.RACE_PEGASUS = 1
PPM2.RACE_UNICORN = 2
PPM2.RACE_ALICORN = 3
PPM2.RACE_ENUMS = {'EARTH', 'PEGASUS', 'UNICORN', 'ALICORN'}

PPM2.RACE_HAS_HORN = 0x1
PPM2.RACE_HAS_WINGS = 0x2

PPM2.AGE_FILLY = 0
PPM2.AGE_ADULT = 1
PPM2.AGE_MATURE = 2
PPM2.AGE_ENUMS = {'FILLY', 'ADULT', 'MATURE'}

PPM2.MIN_DERP_STRENGTH = 0.1
PPM2.MAX_DERP_STRENGTH = 1.3

PPM2.MIN_MALE_BUFF = 0
PPM2.DEFAULT_MALE_BUFF = 1
PPM2.MAX_MALE_BUFF = 2

PPM2.MAX_TATTOOS = 25

PPM2.TATTOOS_REGISTRY = {
	'NONE', 'ARROW', 'BLADES', 'CROSS', 'DIAMONDINNER', 'DIAMONDOUTER'
	'DRACO', 'EVILHEART', 'HEARTWAVE', 'JUNCTION', 'NOTE', 'NOTE2'
	'TATTOO1', 'TATTOO2', 'TATTOO3', 'TATTOO4', 'TATTOO5', 'TATTOO6', 'TATTOO7'
	'WING', 'HEART'
}

PPM2.MIN_TATTOOS = 0
PPM2.MAX_TATTOOS = #PPM2.TATTOOS_REGISTRY - 1

PPM2.MIN_WING = 0.1
PPM2.MIN_WINGX = -10
PPM2.MIN_WINGY = -10
PPM2.MIN_WINGZ = -10
PPM2.MAX_WING = 2
PPM2.MAX_WINGX = 10
PPM2.MAX_WINGY = 10
PPM2.MAX_WINGZ = 10


Раскраска слов сделана локальной для каждого открытого редактора.

F=far.Flags
color = far.AdvControl(F.ACTL_GETCOLOR, far.Colors.COL_EDITORTEXT)
color.ForegroundColor, color.BackgroundColor = color.BackgroundColor, color.ForegroundColor
colorguid=win.Uuid "507CFA2A-3BA3-4f2b-8A80-318F5A831235"
words={}

Macro
  area:"Editor"
  key:"F5"
  description:"Color Word Under Cursor"
  action:->
    ei=editor.GetInfo!
    id=ei.EditorID
    if words[id] then words[id]=nil
    else
      pos=ei.CurPos
      line=editor.GetString!.StringText
      if pos<=line\len()+1
        slab=pos>1 and line\sub(1,pos-1)\match('[%w_]+$') or ""
        tail=line\sub(pos)\match('^[%w_]+') or ""
        if slab~="" or tail~="" then words[id]=slab..tail

Event
  group:"EditorEvent"
  action:(id,event,param)->
    if event==F.EE_REDRAW
      if words[id]
        ei=editor.GetInfo!
        start,finish=ei.TopScreenLine,math.min ei.TopScreenLine+ei.WindowSizeY,ei.TotalLines
        for ii=start,finish
          line,pos=editor.GetString(-1,ii).StringText,1
          while true
            jj,kk,curr=line\cfind("([%w_]+)",pos)
            if not jj then break
            if curr==words[id] then editor.AddColor ei.EditorID,ii,jj,kk,F.ECF_AUTODELETE,color,100,colorguid
            pos=kk+1
    elseif event==F.EE_CLOSE then words[id]=nil

F=far.Flags
color = far.AdvControl(F.ACTL_GETCOLOR, far.Colors.COL_EDITORTEXT)
color.ForegroundColor, color.BackgroundColor  = color.BackgroundColor, color.ForegroundColor
colorguid=win.Uuid "507CFA2A-3BA3-4f2b-8A80-318F5A831235"
word=false

Macro
  area:"Editor"
  key:"F5"
  description:"Color Word Under Cursor"
  action:->
    if word then word=false
    else
      ei=editor.GetInfo!
      pos,row=ei.CurPos,ei.CurLine
      line=editor.GetString!.StringText
      if pos<=line\len()+1
        slab=pos>1 and line\sub(1,pos-1)\match('[%w_]+$') or ""
        tail=line\sub(pos)\match('^[%w_]+') or ""
        word=slab..tail
      if word\len()<=0 then word=false

Event
  group:"EditorEvent"
  action:(id,event,param)->
    if event==F.EE_REDRAW
      if word
        ei=editor.GetInfo!
        start,finish=ei.TopScreenLine,math.min ei.TopScreenLine+ei.WindowSizeY,ei.TotalLines
        for ii=start,finish
          line,pos=editor.GetString(-1,ii).StringText,1
          while true
            jj,kk,curr=line\cfind("([%w_]+)",pos)
            if not jj then break
            if curr==word then editor.AddColor ei.EditorID,ii,jj,kk,F.ECF_AUTODELETE,color,100,colorguid
            pos=kk+1


Cleanup unused models

Cleanup unused models

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

WATCH_WEAPONS = {}

timer.Create 'PPM2.ModelChecks', 1, 0, ->
    for ply in *player.GetAll()
        ply.__cachedIsPony = ply\IsPony()
        if ply.__ppm2_weapon_hit and not ply.__cachedIsPony
            wep\SetNoDraw(false) for wep in *ply\GetWeapons()
            ply.__ppm2_weapon_hit = false

    for i, wep in pairs WATCH_WEAPONS
        if not IsValid(wep)
            WATCH_WEAPONS[wep] = nil
            continue
        if not IsValid(wep\GetOwner())
            WATCH_WEAPONS[wep] = nil
            wep\SetNoDraw(false)

PPM2.PostDrawOpaqueRenderables = (a, b) ->
    return if a or b
    lply = LocalPlayer()

    for ply in *player.GetAll()
        if ply.__cachedIsPony
            wep = ply\GetActiveWeapon()
            if IsValid(wep)
                ply.__ppm2_weapon_hit = true
                wep\SetNoDraw(ply ~= lply or lply\ShouldDrawLocalPlayer())
                WATCH_WEAPONS[wep\EntIndex()] = wep

            if ply\GetPonyData() and not ply\Alive()
                data = ply\GetPonyData()
                rag = ply\GetRagdollEntity()
                if IsValid(rag)
                    rag\SetNoDraw(true)
                    renderController = data\GetRenderController()
                    data\DoRagdollMerge()
                    if renderController
                        renderController\PreDraw(rag)
                        rag\DrawModel()
                        renderController\PostDraw(rag)

PPM2.PrePlayerDraw = =>
    return unless @GetPonyData()
    return unless @__cachedIsPony
    return if @__ppm2_last_draw == FrameNumber()
    @__ppm2_last_draw = FrameNumber()
    return if @IsDormant()
    data = @GetPonyData()
    renderController = data\GetRenderController()
    status = renderController\PreDraw() if renderController
    return status if status ~= nil
PPM2.PostPlayerDraw = =>
    return unless @GetPonyData()
    return unless @__cachedIsPony
    data = @GetPonyData()
    renderController = data\GetRenderController()
    renderController\PostDraw() if renderController

hook.Add 'PrePlayerDraw', 'PPM2.PlayerDraw', PPM2.PrePlayerDraw, -2
hook.Add 'PostPlayerDraw', 'PPM2.PostPlayerDraw', PPM2.PostPlayerDraw, -2
hook.Add 'PostDrawOpaqueRenderables', 'PPM2.PostDrawOpaqueRenderables', PPM2.PostDrawOpaqueRenderables, 2
hook.Add 'RenderScreenspaceEffects', 'PPM2.RenderScreenspaceEffects', ->
    self = LocalPlayer()

    if @__cachedIsPony and @GetPonyData() and @Alive()
        @GetPonyData()\GetRenderController()\DrawLegs()

SHOULD_DRAW_VIEWMODEL = CreateConVar('cl_ppm2_draw_hands', '1', {FCVAR_ARCHIVE}, 'Should draw hooves as viewmodel')

hook.Add 'PreDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
    return true unless SHOULD_DRAW_VIEWMODEL\GetBool()
    return unless IsValid(arms)
    return unless ply.__cachedIsPony
    return unless ply\Alive()
    data = ply\GetPonyData()
    return unless data
    status = data\GetRenderController()\PreDrawArms(arms)
    return status if status ~= nil
    arms.__ppm2_draw = true

hook.Add 'PostDrawPlayerHands', 'PPM2.ViewModel', (arms = NULL, viewmodel = NULL, ply = LocalPlayer(), weapon = NULL) ->
    return unless IsValid(arms)
    return unless arms.__ppm2_draw
    data = ply\GetPonyData()
    return unless data
    data\GetRenderController()\PostDrawArms(arms)
    arms.__ppm2_draw = false

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

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

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

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

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

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

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

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

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

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

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


weatherunderground: change language, remove dummy

Former-commit-id: 07a015369a3b6f5a3aa704d40c34cce4c8c6ce71 [formerly 480cd3d06a39fe11a73b5a8dc98084d7322ab6b0]
Former-commit-id: 46edab10e6ad041eb4a359f0f82ec99ef3263a93

-- Weather Underground API module, requires a key set in configuration
-- WeatherUndergroundKey

util = require'util'
os = require'os'
iconv = require'iconv'

iso2utf = iconv.new('latin1', 'utf-8')

key = ivar2.config.WeatherUndergroundKey
unless key
  return {}

geoApiBase = 'http://api.geonames.org/searchJSON?name=%s&featureClass=P&username=haste'
language = 'EN'

tempColor = (v) ->
  v = tonumber(v)
  if v > 0
    color = util.red
    return color(v)
  else
    color = util.lightblue
    return color(v)

windSpeedToName = (ws) ->
  windtext = ''
  unless ws
    return windtext
  if ws > 32
    windtext = 'orkan!'
  elseif ws > 28.5
    windtext = 'sterk storm'
  elseif ws > 24.5
    windtext = 'full storm'
  elseif ws > 20.8
    windtext = 'liten storm'
  elseif ws > 17.2
    windtext = 'sterk kuling'
  elseif ws > 13.9
    windtext = 'stiv kuling'
  elseif ws > 10.8
    windtext = 'liten kuling'
  elseif ws > 8
    windtext = 'frisk bris'
  elseif ws > 5.5
    windtext = 'laber bris'
  elseif ws > 3.4
    windtext = 'lett bris'
  elseif ws > 1.5
    windtext = 'svak vind'
  elseif ws > 0.3
    windtext = 'flau vind'
  else
    windtext = 'vindstille'
  return windtext

findLocation = (source, destination, name, cb) =>
  gurl = geoApiBase\format(util.urlEncode(name))
  util.simplehttp gurl, (data) ->
    json = util.json.decode(data)
    if json.totalResultsCount == 0
      return @Msg('privmsg', destination, source, "Does that place even exist?")
    city = json.geonames[1]
    lat = city.lat
    lon = city.lng
    cb(lon, lat)

lookupConditions = (source, destination, input, pws) =>
  findLocation @, source, destination, input, (lon, lat) ->
    pws = pws or false
    if pws
      pws = 'pws:1/'
    else
      pws = 'pws:0/'
    url = "http://api.wunderground.com/api/#{key}/lang:#{language}/#{pws}conditions/q/#{lat},#{lon}.json"
    @Log 'debug', "Fetching WU URL: %s", url
    util.simplehttp url, (data) ->
      json = util.json.decode(data)
      if json.error ~= nil and json.error.description ~=nil
        return say json.error.description
      current = json.current_observation

      location = current.observation_location
      city = iso2utf\iconv location.city

      weather = current.weather
      temp = current.temp_c
      feelsLike = current.feelslike_c

      windSpeedname = windSpeedToName(current.wind_kph/3.6)

      windDirection = current.wind_dir

      rain = current.precip_today_metric

      time = os.date('%H:%M', tonumber(current.observation_epoch))

      @Msg 'privmsg', destination, source, '%s, %s °C (feels like %s °C), %s %s, %s mm, (%s, %s)', weather, tempColor(temp), tempColor(feelsLike), windDirection, windSpeedname, rain, city, time

lookupConditionsPWS = (source, destination, input) =>
  lookupConditions(@, source, destination, input, true)

PRIVMSG:
  '^%pwu (.+)$': lookupConditions
  '^%ppws (.+)$': lookupConditionsPWS

-- Weather Underground API module, requires a key set in configuration
-- WeatherUndergroundKey

util = require'util'
os = require'os'
iconv = require'iconv'

iso2utf = iconv.new('latin1', 'utf-8')

key = ivar2.config.WeatherUndergroundKey
unless key
  return {}

geoApiBase = 'http://api.geonames.org/searchJSON?name=%s&featureClass=P&username=haste'
wugeolookupurl = 'http://api.wunderground.com/api/%s/geolookup/q/59.788,5.7218.json'
language = 'NO'

tempColor = (v) ->
  v = tonumber(v)
  if v > 0
    color = util.red
    return color(v)
  else
    color = util.lightblue
    return color(v)

windSpeedToName = (ws) ->
  windtext = ''
  unless ws
    return windtext
  if ws > 32
    windtext = 'orkan!'
  elseif ws > 28.5
    windtext = 'sterk storm'
  elseif ws > 24.5
    windtext = 'full storm'
  elseif ws > 20.8
    windtext = 'liten storm'
  elseif ws > 17.2
    windtext = 'sterk kuling'
  elseif ws > 13.9
    windtext = 'stiv kuling'
  elseif ws > 10.8
    windtext = 'liten kuling'
  elseif ws > 8
    windtext = 'frisk bris'
  elseif ws > 5.5
    windtext = 'laber bris'
  elseif ws > 3.4
    windtext = 'lett bris'
  elseif ws > 1.5
    windtext = 'svak vind'
  elseif ws > 0.3
    windtext = 'flau vind'
  else
    windtext = 'vindstille'
  return windtext

findLocation = (source, destination, name, cb) =>
  gurl = geoApiBase\format(util.urlEncode(name))
  util.simplehttp gurl, (data) ->
    json = util.json.decode(data)
    if json.totalResultsCount == 0
      return @Msg('privmsg', destination, source, "Does that place even exist?")
    city = json.geonames[1]
    lat = city.lat
    lon = city.lng
    cb(lon, lat)

lookupConditions = (source, destination, input, pws) =>
  findLocation @, source, destination, input, (lon, lat) ->
    pws = pws or false
    if pws
      pws = 'pws:1/'
    else
      pws = 'pws:0/'
    url = "http://api.wunderground.com/api/#{key}/lang:#{language}/#{pws}conditions/q/#{lat},#{lon}.json"
    @Log 'debug', "Fetching WU URL: %s", url
    util.simplehttp url, (data) ->
      json = util.json.decode(data)
      if json.error ~= nil and json.error.description ~=nil
        return say json.error.description
      current = json.current_observation

      location = current.observation_location
      city = iso2utf\iconv location.city

      weather = current.weather
      temp = current.temp_c
      feelsLike = current.feelslike_c

      windSpeedname = windSpeedToName(current.wind_kph/3.6)

      windDirection = current.wind_dir

      rain = current.precip_today_metric

      time = os.date('%H:%M', tonumber(current.observation_epoch))

      @Msg 'privmsg', destination, source, '%s, %s °C (følt %s °C), %s %s, %s mm, (%s, %s)', weather, tempColor(temp), tempColor(feelsLike), windDirection, windSpeedname, rain, city, time

lookupConditionsPWS = (source, destination, input) =>
  lookupConditions(@, source, destination, input, true)

PRIVMSG:
  '^%pwu (.+)$': lookupConditions
  '^%ppws (.+)$': lookupConditionsPWS


rss: use abort to avoid autoincrement

rss: use abort to avoid autoincrement

Former-commit-id: 41d720b78bdaaafbf4eb14924c466aab539f5598 [formerly 468f6a9bd5093b98f23f7296ed8da0dfde2837ef]
Former-commit-id: 8b096502f90f7a5d8ec73c21ade0f053ae5a2831

-- Module for subscribing to RSS and announce to channels
feedparser = require 'feedparser' -- http://feedparser.luaforge.net/
{:simplehttp, :json, :urlEncode, :bold} = require'util'
html2unicode = require 'html'
sql = require'lsqlite3'

moduleName = 'rss'

get_db = ->
  db = sql.open 'cache/rss.sql'

  code = db\exec [[
    CREATE TABLE IF NOT EXISTS feed (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT,
      rssurl VARCHAR(1024) NOT NULL,
      url VARCHAR(1024),
      title VARCHAR(1024),
      author VARCHAR(1024),
      etag TEXT,
      lastmodified TEXT
    );

    CREATE TABLE IF NOT EXISTS item (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      feed_id INTEGER NOT NULL,
      guid VARCHAR(64),
      title VARCHAR(1024),
      author VARCHAR(1024),
      url VARCHAR(1024),
      feedurl VARCHAR(1024),
      pubDate TEXT,
      summary TEXT,
      content TEXT,
      FOREIGN KEY(feed_id) REFERENCES feed(id),
      UNIQUE (guid, feed_id)
    );

    CREATE TABLE IF NOT EXISTS subscription (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      destination varchar(100),
      feed_id INTEGER,
      last INTEGER DEFAULT 0,
      created_at timestamp DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY(feed_id) REFERENCES feed(id)
    );
    ]]

  if code != sql.OK
    ivar2\Log 'error', "Error during opendb: #{db\errmsg!}"
    return nil

  return db

prepare = (db, stmt, ...) ->
  code = db\prepare stmt
  unless code
    ivar2\Log 'error', "Error with statement prepare: #{db\errmsg!}"
    return nil
  code\bind_values ...
  return code

-- Function that add functionality to extract specific information for specific sites
feedSpecific = (feedurl, link, title, summary, content) ->
  if feedurl\match 'https://comics.io/my/'

    title = summary\match '
(.-)
' or ''
    img, alt = summary\match '
'
    return "#{html2unicode title} #{img} #{html2unicode alt}"

  return "#{title} - #{link}"

announce = (id) ->
  db = get_db!
  stmt = prepare db, "SELECT DISTINCT feed.id, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id WHERE feed.id = ?", id
  for row in stmt\rows!
    destination = row[2]

    highest = 0
    -- Check last value
    stmt = prepare db, "SELECT last FROM subscription WHERE feed_id = ? and destination = ?", id, destination
    stmt\step!
    value = stmt\get_values()[1]
    if value and value ~= '' and tonumber(value) and tonumber(value) > 0
      highest = value

    ivar2\Log 'debug', 'Got highest %s, %s, %s', id, destination, highest

    stmt = prepare db, "SELECT feed.id AS feed_id, item.id AS item_id, rssurl, pubDate, name, etag, item.url AS link, item.title, content, summary FROM feed JOIN item on item.feed_id = feed.id WHERE feed.id = ? AND item.id > ? ORDER BY item.id DESC LIMIT 100", id, highest
    out = {}
    for item in stmt\nrows!
      with item
        if .item_id > highest
          highest = .item_id
          table.insert out, "[#{bold .name}]: #{feedSpecific(.rssurl, .link, .title, .summary, .content)}"
    if #out > 0
      ivar2\Msg 'privmsg', destination, ivar2.nick, "RSS "..table.concat(out, ', ')
    -- Update last red
    ins = prepare db, 'UPDATE subscription SET last=? WHERE feed_id = ? and destination = ?', highest, id, destination
    code, err = ins\step!
    code, err = ins\finalize!
  db\close!

poll = ->
  db = get_db!
  channels = {}
  for c,_ in pairs(ivar2.channels)
    channels[c] = true
  for row in db\rows "SELECT DISTINCT feed.id, rssurl, lastmodified, name, etag, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id"
  -- Ignore feeds this particular instance of the bot does not subscribe to
    if not channels[row[6]]
      continue
    id = row[1]
    rssurl = row[2]
    lm = row[3]
    name = row[4]
    etag = row[5]
    headers = {}
    if lm and lm != '' then
      headers['If-Modified-Since'] = lm
    if etag and etag != '' then
      headers['If-None-Match'] = etag

    simplehttp {url:rssurl,headers:headers}, (data, url, response) ->
      -- Reopenn DB because of async
      sdb = get_db!

      lastModified = response.headers['Last-Modified']
      if not lastModified
        lastModified = response.headers['Date']

      -- Unmodified content
      if response.status_code == 304
        -- return from the simplehttp callback
        return

      feed, err = feedparser.parse data
      if err
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{err}> data for feed: <#{name}> with URL <#{url}>"
      else
        title = feed.title
        author = feed.author
        url = feed.link

        -- Update feed with new values
        ins = sdb\prepare 'UPDATE feed SET title=?, author=?, url=?, lastmodified=? WHERE id=?'
        code, err = ins\bind_values title, author, url, lastModified, id
        code, err = ins\step!
        code, err = ins\finalize!

        out = {}
        for i, e in ipairs(feed.entries)
          -- Attempt to get a unique entry ID
          guid = e.guid
          if not guid then guid = e.id
          if not guid then guid = e.link
          if not guid
            ivar2\Log 'error', "#{moduleName}: No GUID when parsing entry: <#{e}> data for feed: <#{name}> with URL <#{url}>"
            break

          --if guid == last
          --  break
          ins = sdb\prepare [[
            INSERT OR ABORT INTO
              item(feed_id, guid, title, author, url, pubDate, content, summary)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)
          ]]
          if not ins then
            ivar2\Log 'error', "#{moduleName}: Error during inserting entry: <#{err}> feed: <#{name}> with URL <#{url}> and position: #{i}"
          else
            code = ins\bind_values id, guid, e.title, e.author, e.link, e.updated, e.content, e.summary
            code = ins\step!
            code = ins\finalize!
        announce(id)
      sdb\close!
  db\close!

subscribe = (source, destination, name, url) =>
  db = get_db!
  ins = db\prepare "INSERT INTO feed (name, rssurl) VALUES(?, ?)"
  code = ins\bind_values name, url
  code = ins\step!
  code = ins\finalize!
  ins = db\prepare "INSERT INTO subscription (feed_id, destination) VALUES(?, ?)"
  code = ins\bind_values db\last_insert_rowid!, destination
  code = ins\step!
  code = ins\finalize!
  db\close!
  reply "Ok. Subscribed to #{bold name}"
  poll!

unsubscribe = (source, destination, name) =>
  db = get_db!
  ins = db\prepare "DELETE FROM subscription WHERE feed_id IN (select id from feed where name = ?) and destination = ?"
  code = ins\bind_values name, destination
  code = ins\step!
  code = ins\finalize!
  total_changes = db\total_changes!
  db\close!
  if total_changes < 1
    reply "Wasn't subscribed. But, sure."
  else
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  out = {}
  db = get_db!
  stmt = db\prepare("SELECT name FROM feed JOIN subscription ON feed.id = subscription.feed_id WHERE destination = ?")
  stmt\bind_values destination
  for row in stmt\rows!
    out[#out+1] = row[1]
  if #out > 0 then
    reply "Subscribed to: #{table.concat(out, ', ')}"
  else
    reply "Not subscribed to any feeds."

  db\close!

-- Start the subscribe poller
ivar2\Timer(moduleName, 60*5, 60*5, poll)

PRIVMSG:
  '^%prss latest (.*)': getLatest
  '^%prss subscribe (.-) (.*)': subscribe
  '^%prss unsubscribe (.*)': unsubscribe
  '^%prss list': list
  -- for debugging
  '^%prss poll': poll

-- Module for subscribing to RSS and announce to channels
feedparser = require 'feedparser' -- http://feedparser.luaforge.net/
{:simplehttp, :json, :urlEncode, :bold} = require'util'
html2unicode = require 'html'
sql = require'lsqlite3'

moduleName = 'rss'

get_db = ->
  db = sql.open 'cache/rss.sql'

  code = db\exec [[
    CREATE TABLE IF NOT EXISTS feed (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT,
      rssurl VARCHAR(1024) NOT NULL,
      url VARCHAR(1024),
      title VARCHAR(1024),
      author VARCHAR(1024),
      etag TEXT,
      lastmodified TEXT
    );

    CREATE TABLE IF NOT EXISTS item (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      feed_id INTEGER NOT NULL,
      guid VARCHAR(64),
      title VARCHAR(1024),
      author VARCHAR(1024),
      url VARCHAR(1024),
      feedurl VARCHAR(1024),
      pubDate TEXT,
      summary TEXT,
      content TEXT,
      FOREIGN KEY(feed_id) REFERENCES feed(id),
      UNIQUE (guid, feed_id)
    );

    CREATE TABLE IF NOT EXISTS subscription (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      destination varchar(100),
      feed_id INTEGER,
      last INTEGER DEFAULT 0,
      created_at timestamp DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY(feed_id) REFERENCES feed(id)
    );
    ]]

  if code != sql.OK
    ivar2\Log 'error', "Error during opendb: #{db\errmsg!}"
    return nil

  return db

prepare = (db, stmt, ...) ->
  code = db\prepare stmt
  unless code
    ivar2\Log 'error', "Error with statement prepare: #{db\errmsg!}"
    return nil
  code\bind_values ...
  return code

-- Function that add functionality to extract specific information for specific sites
feedSpecific = (feedurl, link, title, summary, content) ->
  if feedurl\match 'https://comics.io/my/'

    title = summary\match '
(.-)
' or ''
    img, alt = summary\match '
'
    return "#{html2unicode title} #{img} #{html2unicode alt}"

  return "#{title} - #{link}"

announce = (id) ->
  db = get_db!
  stmt = prepare db, "SELECT DISTINCT feed.id, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id WHERE feed.id = ?", id
  for row in stmt\rows!
    destination = row[2]

    highest = 0
    -- Check last value
    stmt = prepare db, "SELECT last FROM subscription WHERE feed_id = ? and destination = ?", id, destination
    stmt\step!
    value = stmt\get_values()[1]
    if value and value ~= '' and tonumber(value) and tonumber(value) > 0
      highest = value

    ivar2\Log 'debug', 'Got highest %s, %s, %s', id, destination, highest

    stmt = prepare db, "SELECT feed.id AS feed_id, item.id AS item_id, rssurl, pubDate, name, etag, item.url AS link, item.title, content, summary FROM feed JOIN item on item.feed_id = feed.id WHERE feed.id = ? AND item.id > ? ORDER BY item.id DESC LIMIT 100", id, highest
    out = {}
    for item in stmt\nrows!
      with item
        if .item_id > highest
          highest = .item_id
          table.insert out, "[#{bold .name}]: #{feedSpecific(.rssurl, .link, .title, .summary, .content)}"
    if #out > 0
      ivar2\Msg 'privmsg', destination, ivar2.nick, "RSS "..table.concat(out, ', ')
    -- Update last red
    ins = prepare db, 'UPDATE subscription SET last=? WHERE feed_id = ? and destination = ?', highest, id, destination
    code, err = ins\step!
    code, err = ins\finalize!
  db\close!

poll = ->
  db = get_db!
  channels = {}
  for c,_ in pairs(ivar2.channels)
    channels[c] = true
  for row in db\rows "SELECT DISTINCT feed.id, rssurl, lastmodified, name, etag, destination FROM feed JOIN subscription ON subscription.feed_id = feed.id"
  -- Ignore feeds this particular instance of the bot does not subscribe to
    if not channels[row[6]]
      continue
    id = row[1]
    rssurl = row[2]
    lm = row[3]
    name = row[4]
    etag = row[5]
    headers = {}
    if lm and lm != '' then
      headers['If-Modified-Since'] = lm
    if etag and etag != '' then
      headers['If-None-Match'] = etag

    simplehttp {url:rssurl,headers:headers}, (data, url, response) ->
      -- Reopenn DB because of async
      sdb = get_db!

      lastModified = response.headers['Last-Modified']
      if not lastModified
        lastModified = response.headers['Date']

      -- Unmodified content
      if response.status_code == 304
        -- return from the simplehttp callback
        return

      feed, err = feedparser.parse data
      if err
        ivar2\Log 'error', "#{moduleName}: Error during parsing: <#{err}> data for feed: <#{name}> with URL <#{url}>"
      else
        title = feed.title
        author = feed.author
        url = feed.link

        -- Update feed with new values
        ins = sdb\prepare 'UPDATE feed SET title=?, author=?, url=?, lastmodified=? WHERE id=?'
        code, err = ins\bind_values title, author, url, lastModified, id
        code, err = ins\step!
        code, err = ins\finalize!

        out = {}
        for i, e in ipairs(feed.entries)
          -- Attempt to get a unique entry ID
          guid = e.guid
          if not guid then guid = e.id
          if not guid then guid = e.link
          if not guid
            ivar2\Log 'error', "#{moduleName}: No GUID when parsing entry: <#{e}> data for feed: <#{name}> with URL <#{url}>"
            break

          --if guid == last
          --  break
          ins = sdb\prepare [[
            INSERT OR IGNORE INTO
              item(feed_id, guid, title, author, url, pubDate, content, summary)
              VALUES (?, ?, ?, ?, ?, ?, ?, ?)
          ]]
          if not ins then
            ivar2\Log 'error', "#{moduleName}: Error during inserting entry: <#{err}> feed: <#{name}> with URL <#{url}> and position: #{i}"
          else
            code = ins\bind_values id, guid, e.title, e.author, e.link, e.updated, e.content, e.summary
            code = ins\step!
            code = ins\finalize!
        announce(id)
      sdb\close!
  db\close!

subscribe = (source, destination, name, url) =>
  db = get_db!
  ins = db\prepare "INSERT INTO feed (name, rssurl) VALUES(?, ?)"
  code = ins\bind_values name, url
  code = ins\step!
  code = ins\finalize!
  ins = db\prepare "INSERT INTO subscription (feed_id, destination) VALUES(?, ?)"
  code = ins\bind_values db\last_insert_rowid!, destination
  code = ins\step!
  code = ins\finalize!
  db\close!
  reply "Ok. Subscribed to #{bold name}"
  poll!

unsubscribe = (source, destination, name) =>
  db = get_db!
  ins = db\prepare "DELETE FROM subscription WHERE feed_id IN (select id from feed where name = ?) and destination = ?"
  code = ins\bind_values name, destination
  code = ins\step!
  code = ins\finalize!
  total_changes = db\total_changes!
  db\close!
  if total_changes < 1
    reply "Wasn't subscribed. But, sure."
  else
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  out = {}
  db = get_db!
  stmt = db\prepare("SELECT name FROM feed JOIN subscription ON feed.id = subscription.feed_id WHERE destination = ?")
  stmt\bind_values destination
  for row in stmt\rows!
    out[#out+1] = row[1]
  if #out > 0 then
    reply "Subscribed to: #{table.concat(out, ', ')}"
  else
    reply "Not subscribed to any feeds."

  db\close!

-- Start the subscribe poller
ivar2\Timer(moduleName, 60*5, 60*5, poll)

PRIVMSG:
  '^%prss latest (.*)': getLatest
  '^%prss subscribe (.-) (.*)': subscribe
  '^%prss unsubscribe (.*)': unsubscribe
  '^%prss list': list
  -- for debugging
  '^%prss poll': poll


Added `parents` to classes.

Added `parents` to classes.

class Class
	@name: 'Class'
	@parents: { @ }

	@__inherited: (child) =>
		child.name = child.__name
		child.parents = _.union(@@parents, { @@ })

	new: () =>
		@class = @@
		@className = @@name
		@type = _.lowerFirst(@@name)

		@isClass = true

class Class
	@__inherited: (child) =>
		child.name = child.__name

	new: () =>
		@class = @@
		@className = @@__name
		@type = _.lowerFirst(@@__name)


Aegisub example: fix syntax error.

Aegisub example: fix syntax error.

-- This library is unlicensed under CC0

version = 0x000101

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

version_major = bit.rshift( version, 16 )
version_minor = bit.band( bit.rshift( version, 8 ), 0xFF )
version_patch = bit.band( version, 0xFF )

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

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

-- Figure out a better way to load this?
extension = ('OSX' == ffi.os and '.dylib' or 'Windows' == ffi.os and '.dll' or '.so')
libraryPath = "?user/automation/include/ASSInspector"
success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
if not success
	libraryPath = "?data/automation/include/ASSInspector"
	success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
	if not success
		error( "Could not load required libASSInspector library." )

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	ASSIVersion_major = bit.rshift( ASSIVersion, 16 )
	ASSIVersion_minor = bit.band( bit.rshift( ASSIVersion, 8 ), 0xFF )
	ASSIVersion_patch = bit.band( ASSIVersion, 0xFF )

	if ASSIVersion_major > version_major
		return nil, "Inspector.moon library is too old."
	elseif ASSIVersion_major < version_major or ASSIVersion_minor < version_minor
		return nil, "libASSInspector library is too old."

	return true

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

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

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

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

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

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

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

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

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

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

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

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

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

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

	return true

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

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

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

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

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

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

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

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

class Inspector

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

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		aegisub.progress.title = "Updating fontconfig cache."
		aegisub.progress.task  = "This may take a long time and cannot be cancelled."
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

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

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		aegisub.progress.title = "Updating fontconfig cache."
		aegisub.progress.task  = "This may take a long time and cannot be cancelled."
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

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

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

		@updateHeader( subtitles )

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

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

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

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

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

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

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

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

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

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

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

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

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

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

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

		return rects, times

return Inspector

-- This library is unlicensed under CC0

version = 0x000101

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

version_major = bit.rshift( version, 16 )
version_minor = bit.band( bit.rshift( version, 8 ), 0xFF )
version_patch = bit.band( version, 0xFF )

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

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

-- Figure out a better way to load this?
extension = ('OSX' == ffi.os and '.dylib' or 'Windows' == ffi.os and '.dll' or '.so')
libraryPath = "?user/automation/include/ASSInspector"
success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
if not success
	libraryPath = "?data/automation/include/ASSInspector"
	success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
	if not success
		error( "Could not load required libASSInspector library." )

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	ASSIVersion_major = bit.rshift( ASSIVersion, 16 )
	ASSIVersion_minor = bit.band( bit.rshift( ASSIVersion, 8 ), 0xFF )
	ASSIVersion_patch = bit.band( ASSIVersion, 0xFF )

	if ASSIVersion_major > version_major
		return nil, "Inspector.moon library is too old."
	elseif ASSIVersion_major < version_major or ASSIVersion_minor < version_minor
		return nil, "libASSInspector library is too old."

	return true

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

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

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

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

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

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

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

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

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

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

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

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

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

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

	return true

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

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

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

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

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

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

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

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

class Inspector

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

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		aegisub.progress.title = "Updating fontconfig cache."
		aegisub.progress.task  = "This may take a long time and cannot be cancelled."
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

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

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		aegisub.progress.title = "Updating fontconfig cache."
		aegisub.progress.task  = "This may take a long time and cannot be cancelled."
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

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

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

		@updateHeader( subtitles )

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

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

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

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

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

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

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

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

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

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

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

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

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

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

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

		return rects, times

return Inspector


Add comments to Mask

Add comments to Mask

require "LovePunk.masks.Masklist"

export ^

class Mask
	new: =>
		@parent = nil
		@list = Masklist!

		@__check = {}
		@__check[@@] = @\collideMask
		@__check[Masklist.__class] = @\collideMasklist

	-- Checks for collision with another Mask.
	-- @param	mask	The other Mask to check against.
	-- @return	If the Masks overlap.
	collide: (mask) =>
		assert @parent != nil, "Mask must be attached to a parent Entity"
		cb = @__check[mask.__class]
		return cb if cb != nil

		cb = mask.__check[@@]
		return cb if cb != nil
		false

	-- @private Collide against an Entity.
	collideMask: (other) =>
		l1 = @parent\x! - @parent.originX + @parent.width > other.parent\x! - other.parent.originX
		l2 = @parent\y! - @parent.originY + @parent.height > other.parent\y! - other.parent.originY
		l3 = @parent\x! - @parent.originX < other.parent\x! - other.parent.originX + other.parent.width
		l4 = @parent\y! - @parent.originY < other.parent\y! - other.parent.originY + other.parent.height
		l1 and l2 and l3 and l4

	-- Checks for collision with another Mask.
	-- @param	mask	The other Mask to check against.
	-- @return	If the Masks overlap.
	collideMasklist: (other) =>
		other\collide @

	-- @private Assigns the mask to the parent.
	assignTo: (parent) =>
		@parent = parent
		@update! if parent != nil

	-- Override this
	debugDraw: (graphics, scaleX, scaleY) =>

	-- Updates the parent's bounds for this mask.
	update: =>

	project: (axis, projection) =>
		max = -9999999999.0
		min = 9999999999.0

		cur = -@parent.originX * axis.x - @parent.originY * axit.y
		min = cur if cur < min
		max = cur if cur > max

		cur = (-@parent.originX + @parent.width) * axis.x - @parent.originY * axis.y
		min = cur if cur < min
		max = cur if cur > max

		cur = -@parent.originX * axis.x + (-@parent.originY + @parent.height) * axis.y
		min = cur if cur < min
		max = cur if cur > max

		cur = (-@parent.originX + @parent.width) * axis.x + (-@parent.originY + @parent.height) * axis.y
		min = cur if cur < min
		max = cur if cur > max

		projection.min = min
		projection.max = max

require "LovePunk.masks.Masklist"

export ^

class Mask
	new: =>
		@parent = nil
		@list = Masklist!

		@__check = {}
		@__check[@@] = @\collideMask
		@__check[Masklist.__class] = @\collideMasklist

	collide: (mask) =>
		assert @parent != nil, "Mask must be attached to a parent Entity"
		cb = @__check[mask.__class]
		return cb if cb != nil

		cb = mask.__check[@@]
		return cb if cb != nil
		false

	collideMask: (other) =>
		l1 = @parent\x! - @parent.originX + @parent.width > other.parent\x! - other.parent.originX
		l2 = @parent\y! - @parent.originY + @parent.height > other.parent\y! - other.parent.originY
		l3 = @parent\x! - @parent.originX < other.parent\x! - other.parent.originX + other.parent.width
		l4 = @parent\y! - @parent.originY < other.parent\y! - other.parent.originY + other.parent.height
		l1 and l2 and l3 and l4

	collideMasklist: (other) =>
		other\collide @

	assignTo: (parent) =>
		@parent = parent
		@update! if parent != nil

	update: =>

	project: (axis, projection) =>
		max = -9999999999.0
		min = 9999999999.0

		cur = -@parent.originX * axis.x - @parent.originY * axit.y
		min = cur if cur < min
		max = cur if cur > max

		cur = (-@parent.originX + @parent.width) * axis.x - @parent.originY * axis.y
		min = cur if cur < min
		max = cur if cur > max

		cur = -@parent.originX * axis.x + (-@parent.originY + @parent.height) * axis.y
		min = cur if cur < min
		max = cur if cur > max

		cur = (-@parent.originX + @parent.width) * axis.x + (-@parent.originY + @parent.height) * axis.y
		min = cur if cur < min
		max = cur if cur > max

		projection.min = min
		projection.max = max