Fixed lighting

Fixed lighting

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

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

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

    EDITOR_SEQUENCES: {
        -- idle
        {
            time: 5
            func: (dist, ang, delta) -> dist, ang
        }

        -- Slow move to left
        {
            time: 5
            func: (dist, ang, delta) ->
                ang.y += delta * 10
                return dist, ang
        }

        -- Eyes
        {
            time: 5
            func: (dist, ang, delta) ->
                return 40, Angle(0, 0, 0)
        }

        -- Left view
        {
            time: 3
            func: (dist, ang, delta) ->
                return 80, Angle(0, 90, 0)
        }

        -- Move to right
        {
            time: 7
            func: (dist, ang, delta) ->
                ang.y -= 10 * delta
                return 80, ang
        }

        -- Eyes (bottom)
        {
            time: 5
            func: (dist, ang, delta) ->
                return 20, Angle(40, 0, 0)
        }

        -- Look at top mane
        {
            time: 5
            func: (dist, ang, delta) ->
                return 50, Angle(-20, 180, 0)
        }
    }

    Init: =>
        @animRate = 1
        @seq = @SEQUENCE_STAND
        @targetAngle = Angle(0, 0, 0)
        @angle = Angle(0, 0, 0)
        @distToPony = 100
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @hold = false
        @holdLast = 0
        @mouseX, @mouseY = 0, 0
        @SetMouseInputEnabled(true)
        @editorSeq = 1
        @nextSeq = @EDITOR_SEQUENCES[@editorSeq].time + RealTime()
        @playing = true
        @lastTick = RealTime()
        @SetCursor('none')

        @animButton = vgui.Create('DButton', @)
        with @animButton
            \SetSize(120, 20)
            \SetText('Playing animation')
            .LastStatus = true
            .Think = ->
                if .LastStatus ~= @playing
                    .LastStatus = @playing
                    \SetText('Playing Animation') if @playing
                    \SetText('Play Animation') if not @playing
            .DoClick = ->
                @playing = not @playing
                if @playing
                    @editorSeq = 1
                    @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                    @ResetPosition()

        @seqButton = vgui.Create('DComboBox', @)
        with @seqButton
            \SetSize(120, 20)
            \SetValue('Standing')
            SEQUENCES
            \AddChoice(choice, num) for choice, num in pairs @SEQUENCES
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                @SetSequence(data)
    ResetPosition: =>
        @targetAngle = Angle(0, 0, 0)
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)

    PerformLayout: (w = 0, h = 0) =>
        @animButton\SetPos(w - 130, 10)
        @seqButton\SetPos(10, 10)

    OnMousePressed: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = true
        @SetCursor('sizeall')
        @holdLast = RealTime() + .1
        @oldPlaying = @playing
        @playing = false
        @mouseX, @mouseY = gui.MousePos()
    OnMouseReleased: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = false
        @SetCursor('none')
        if @holdLast > RealTime()
            @playing = true
            if not @oldPlaying
                @editorSeq = 1
                @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                @ResetPosition()

    SetController: (val) => @controller = val

    OnMouseWheeled: (wheelDelta = 0) =>
        @playing = false
        @editorSeq = 1
        @targetDistToPony = math.Clamp(@targetDistToPony - wheelDelta * 10, 20, 150)
    GetModel: => @model
    GetSequence: => @seq
    GetSeq: => @seq
    GetAnimRate: => @animRate
    SetAnimRate: (val = 1) => @animRate = val
    SetSeq: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    SetSequence: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    ResetSequence: => @SetSequence(@SEQUENCE_STAND)
    ResetSeq: => @SetSequence(@SEQUENCE_STAND)

    ResetModel: (ponydata, model = 'models/ppm/player_default_base.mdl') =>
        @model\Remove() if IsValid(@model)
        @model = ClientsideModel(model)
        with @model
            \SetNoDraw(true)
            .__PPM2_PonyData = ponydata
        @model\SetSequence(@seq)
        @model\FrameAdvance(0)
        return @model
    Think: =>
        rtime = RealTime()
        delta = rtime - @lastTick
        @lastTick = rtime
        if IsValid(@model)
            @model\FrameAdvance(delta * @animRate)
            @model\SetPlaybackRate(1)
            @model\SetPoseParameter('move_x', 1)

        @hold = @IsHovered() if @hold

        if @playing
            cseq = @EDITOR_SEQUENCES[@editorSeq]
            if @nextSeq < rtime
                @editorSeq += 1
                @editorSeq = 1 if not @EDITOR_SEQUENCES[@editorSeq]
                cseq = @EDITOR_SEQUENCES[@editorSeq]
                @nextSeq = rtime + cseq.time

            {:p, :y, :r} = @targetAngle
            @targetDistToPony, @targetAngle = cseq.func(@targetDistToPony, Angle(p, y, r), delta)
            @targetDistToPony = math.Clamp(@targetDistToPony, 20, 150)
            @targetAngle.p = math.Clamp(@targetAngle.p, -40, 10)
        else
            if @hold
                x, y = gui.MousePos()
                deltaX, deltaY = x - @mouseX, y - @mouseY
                @mouseX, @mouseY = x, y
                {:pitch, :yaw, :roll} = @targetAngle
                yaw -= deltaX * .5
                pitch = math.Clamp(pitch - deltaY * .5, -40, 10)
                @targetAngle = Angle(pitch, yaw, roll)

        @angle = LerpAngle(delta * 4, @angle, @targetAngle)
        @distToPony = Lerp(delta * 4, @distToPony, @targetDistToPony)
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @vectorPos\Rotate(@angle)
        @drawAngle = (Vector(0, 0, @PONY_VEC_Z * .7) - @vectorPos)\Angle()

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

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

    WALL_COLOR: Color(98, 189, 176)
    FLOOR_COLOR: Color(53, 150, 84)

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

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

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

        @controller\GetRenderController()\PreDraw(@model) if @controller
        render.SuppressEngineLighting(true)
        render.ResetModelLighting(1, 1, 1)
        render.SetColorModulation(1, 1, 1)
        @model\DrawModel()
        render.SuppressEngineLighting(false)
        @controller\GetRenderController()\PostDraw(@model) if @controller

        cam.End3D()
    OnRemove: =>
        @model\Remove() if IsValid(@model)
}

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

PANEL_SETTINGS_BASE = {
    Init: =>
        @shouldSaveData = false
        @SetMouseInputEnabled(true)
        @SetKeyboardInputEnabled(true)
        @DockPadding(5, 5, 5, 5)
        @unsavedChanges = false
        @updateFuncs = {}
    ValueChanges: (valID, newVal, pnl) =>
        @unsavedChanges = true
        @frame.unsavedChanges = true
        @frame\SetTitle("#{@GetTargetData() and @GetTargetData()\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor; *Unsaved changes*")
    GetShouldSaveData: => @shouldSaveData
    ShouldSaveData: => @shouldSaveData
    SetShouldSaveData: (val = false) => @shouldSaveData = val
    GetTargetData: => @data
    TargetData: => @data
    SetTargetData: (val) => @data = val
    DoUpdate: => func() for func in *@updateFuncs
    NumSlider: (name = 'Slider', option = '', decimals = 0) =>
		with vgui.Create('DNumSlider', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 0, 2, 0)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetText(name)
			\SetMin(0)
			\SetMax(1)
			\SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
			\SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
			\SetDecimals(decimals)
            \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
			.TextArea\SetTextColor(color_white)
			.Label\SetTextColor(color_white)
            .OnValueChanged = (pnl, newVal = 1) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
                \SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
                \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    Label: (text = '') =>
        with vgui.Create('DLabel', @scroll or @)
            \SetText(text)
            \Dock(TOP)
            \SetTextColor(color_white)
            \SizeToContents()
            w, h = \GetSize()
            \SetSize(w, h + 5)
            @scroll\AddItem(_with_0) if IsValid(@scroll)
	CheckBox: (name = 'Label', option = '') =>
		with vgui.Create('DCheckBoxLabel', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 2, 2, 2)
			\SetText(name)
			\SetTextColor(color_white)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .OnChange = (pnl, newVal = false) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal and 1 or 0, @GetShouldSaveData())
                @ValueChanges(option, newVal and 1 or 0, pnl)
            table.insert @updateFuncs, ->
                \SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    ColorBox: (name = 'Colorful Box', option = '') =>
        collapse = vgui.Create('DCollapsibleCategory', @scroll or @)
        box = vgui.Create('DColorMixer', collapse)
        collapse.box = box
        with box
            \SetSize(250, 250)
			\SetTooltip("#{name}\nData value: #{option}")
            \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .ValueChanged = (pnl, newVal = Color(0, 0, 0)) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        with collapse
            \SetContents(box)
            \Dock(TOP)
            \DockMargin(2, 2, 2, 2)
            \SetSize(250, 250)
            \SetLabel(name)
            \SetExpanded(false)
        @scroll\AddItem(collapse) if IsValid(@scroll)
        return box, collapse
    ComboBox: (name = 'Combo Box', option = '', choices) =>
        label = vgui.Create('DLabel', @scroll or @)
        with label
            \SetText(name)
            \SetTextColor(color_white)
            \Dock(TOP)
            \SetSize(0, 20)
            \DockMargin(5, 0, 5, 0)
            \SetMouseInputEnabled(true)
        @scroll\AddItem(label) if IsValid(@scroll)
        box = vgui.Create('DComboBox', label)
        with box
            \Dock(RIGHT)
            \SetSize(170, 0)
            \DockMargin(0, 0, 5, 0)
            \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
            if choices
                \AddChoice(choice) for choice in *choices
            else
                \AddChoice(choice) for choice in *@GetTargetData()["Get#{option}Types"](@GetTargetData()) if @GetTargetData() and @GetTargetData()["Get#{option}Types"]
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                index -= 1
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, index, @GetShouldSaveData())
                @ValueChanges(option, index, pnl)
            table.insert @updateFuncs, ->
                \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
        return box, label
    URLInput: (option = '') =>
        wrapper = vgui.Create('EditablePanel', @scroll or @)
        with wrapper
            \Dock(TOP)
            \DockMargin(5, 10, 5, 10)
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            \SetSize(0, 30)
        textInput = vgui.Create('DTextEntry', wrapper)
        @scroll\AddItem(wrapper) if IsValid(@scroll)
        with textInput
            \Dock(FILL)
            \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            .OnEnter = ->
                text = \GetValue()
                if text\find('^https?://')
                    @GetTargetData()["Set#{option}"](@GetTargetData(), text)
                    @ValueChanges(option, text, textInput)
                else
                    @GetTargetData()["Set#{option}"](@GetTargetData(), '')
                    @ValueChanges(option, '', textInput)
            .OnKeyCodeTyped = (pnl, key = KEY_FIRST) ->
                switch key
                    when KEY_FIRST
                        return true
                    when KEY_NONE
                        return true
                    when KEY_TAB
                        return true
                    when KEY_ENTER
                        .OnEnter()
                        \KillFocus()
                        return true
                timer.Create "PPM2.EditorCodeChange.#{option}", 1, 1, ->
                    return if not IsValid(textInput)
                    .OnEnter()
            table.insert @updateFuncs, -> \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        return textInput
    ScrollPanel: =>
        return @scroll if IsValid(@scroll)
        @scroll = vgui.Create('DScrollPanel', @)
        @scroll\Dock(FILL)
        return @scroll
    Paint: (w = 0, h = 0) =>
		surface.SetDrawColor(130, 130, 130)
		surface.DrawRect(0, 0, w, h)
}

vgui.Register('PPM2SettingsBase', PANEL_SETTINGS_BASE, 'EditablePanel')

EditorPages = {
    {
        'name': 'Main'
        'internal': 'main'
        'func': (sheet) =>
            @CheckBox('Gender', 'Gender')
            @ComboBox('Race', 'Race')
            @NumSlider('Weight', 'Weight', 2)
            @CheckBox('Socks (simple texture)', 'Socks')
            @ComboBox('Eyelashes', 'EyelashType')
            @ComboBox('Bodysuit', 'Bodysuit')
            @ColorBox('Body color', 'BodyColor')
            @CheckBox('Socks (as model)', 'SocksAsModel')
            @ColorBox('Socks model color', 'SocksColor')
    }

    {
        'name': 'Eyes'
        'internal': 'eyes'
        'func': (sheet) =>
            @ScrollPanel()
            @CheckBox('Use separated settings for eyes', 'SeparateEyes')
            for publicName in *{'', 'Left', 'Right'}
                @Label("'#{publicName}' Eye settings")
                @CheckBox("#{publicName} Eye lines", "EyeLines#{publicName}")
                @CheckBox("#{publicName} Derp eye", "DerpEyes#{publicName}")
                @NumSlider("#{publicName} Derp eye strength", "DerpEyesStrength#{publicName}", 2)
                @NumSlider("#{publicName} Eye size", "IrisSize#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole width", "HoleWidth#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole size", "HoleSize#{publicName}", 2)

                @ColorBox("#{publicName} Eye background", "EyeBackground#{publicName}")
                @ColorBox("#{publicName} Eye hole", "EyeHole#{publicName}")
                @ColorBox("#{publicName} Top eye iris", "EyeIrisTop#{publicName}")
                @ColorBox("#{publicName} Bottom eye iris", "EyeIrisBottom#{publicName}")
                @ColorBox("#{publicName} Eye line 1", "EyeIrisLine1#{publicName}")
                @ColorBox("#{publicName} Eye line 2", "EyeIrisLine2#{publicName}")
                @ColorBox("#{publicName} Eye line 2", "EyeIrisLine2#{publicName}")
    }

    {
        'name': 'Mane and tail'
        'internal': 'manetail'
        'func': (sheet) =>
            @ScrollPanel()
            @Label('"New" types affect only new model')
            @ComboBox('Tail type', 'TailType')
            @ComboBox('New Tail type', 'TailTypeNew')

            @ComboBox('Mane type', 'ManeType')
            @ComboBox('New Mane type', 'ManeTypeNew')

            @ComboBox('Lower Mane type', 'ManeTypeLower')
            @ComboBox('New Lower Mane type', 'ManeTypeLowerNew')

            for i = 1, 2
                @ColorBox("Mane color #{i}", "ManeColor#{i}")
                @ColorBox("Tail color #{i}", "TailColor#{i}")

            for i = 1, 6
                @ColorBox("Mane detail color #{i}", "ManeColor#{i}")
                @ColorBox("Tail detail color #{i}", "TailDetailColor#{i}")

            @CheckBox('Separate upper and lower mane colors', 'SeparateMane')
            @Label('These options have effect only on new model')

            for i = 1, 2
                @ColorBox("Upper Mane color #{i}", "UpperManeColor#{i}")
                @ColorBox("Lower Mane color #{i}", "LowerManeColor#{i}")

            for i = 1, 6
                @ColorBox("Upper Mane detail color #{i}", "UpperManeDetailColor#{i}")
                @ColorBox("Lower Tail detail color #{i}", "LowerManeDetailColor#{i}")
    }

    {
        'name': 'Body details'
        'internal': 'bodydetail'
        'func': (sheet) =>
            @ScrollPanel()
            for i = 1, PPM2.MAX_BODY_DETAILS
                @ComboBox("Detail #{i}", "BodyDetail#{i}")
                @ColorBox("Detail color #{i}", "BodyDetailColor#{i}")

            @Label('Body detail URL image input fields\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')
            for i = 1, PPM2.MAX_BODY_DETAILS
                @Label("Body detail #{i}")
                @URLInput("BodyDetailURL#{i}")
                @ColorBox("URL Detail color #{i}", "BodyDetailURLColor#{i}")
    }

    {
        'name': 'Cutiemark'
        'internal': 'cmark'
        'func': (sheet) =>
            @CheckBox('Display cutiemark', 'CMark')
            @ComboBox('Cutiemark type', 'CMarkType')
            @markDisplay = vgui.Create('EditablePanel', @)
            with @markDisplay
                \Dock(TOP)
                \SetSize(320, 320)
                \DockMargin(20, 20, 20, 20)
                .Paint = (pnl, w = 0, h = 0) ->
                    data = @GetTargetData()
                    return if not data
                    controller = data\GetController()
                    return if not controller
                    rcontroller = controller\GetRenderController()
                    return if not rcontroller
                    tcontroller = rcontroller\GetTextureController()
                    return if not tcontroller
                    mat = tcontroller\GetCMarkGUI()
                    return if not mat
                    surface.SetDrawColor(0, 0, 0)
                    surface.DrawRect(0, 0, w, h)
                    surface.SetDrawColor(255, 255, 255)
                    surface.SetMaterial(mat)
                    surface.DrawTexturedRect(0, 0, w, h)

            @Label('Cutiemark URL image input field\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')\DockMargin(5, 10, 5, 10)
            @URLInput('CMarkURL')
    }

    {
        'name': 'Files'
        'internal': 'saves'
        'func': (sheet) =>
            @Label('Open file by double click')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    @frame.data\SetFilename(fil)
                    @frame.data\ReadFromDisk(true)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = false
                    @frame.unsavedChanges = false
                    @frame\SetTitle("#{fil} - PPM2 Pony Editor")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm2/*', 'DATA')
                for fil in *files
                    matchBak = '.bak.txt'
                    continue if fil\sub(-#matchBak) == matchBak
                    list\AddLine(fil\sub(1, #fil - 4))
            @rebuildFileList()
    }

    {
        'name': 'Old Files'
        'internal': 'oldsaves'
        'func': (sheet) =>
            @Label('!!! It may or may not work. You will be squished.')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    newData = PPM2.ReadFromOldData(fil)
                    if not newData
                        Derma_Message('Failed to import.', 'Onoh!', 'Okai ;w;')
                        return
                    @frame.data\SetFilename(newData\GetFilename())
                    newData\ApplyDataToObject(@frame.data, false)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = true
                    @frame.unsavedChanges = true
                    @frame\SetTitle("#{newData\GetFilename()} - PPM2 Pony Editor; *Unsaved changes*")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm/*', 'DATA')
                list\AddLine(fil\sub(1, #fil - 4)) for fil in *files
            @rebuildFileList()
    }
}

EditorModels = {
    'DEFAULT': 'models/ppm/player_default_base.mdl'
    'CPPM': 'models/cppm/player_default_base.mdl'
    'NEW': 'models/ppm/player_default_base_new.mdl'
}

USE_MODEL = CreateConVar('ppm2_editor_model', 'new', {FCVAR_ARCHIVE}, 'What model to use in editor. Valids are "default", "cppm", "new"')

if IsValid(PPM2.EditorFrame)
    PPM2.EditorFrame\Remove()
    net.Start('PPM2.EditorStatus')
    net.WriteBool(false)
    net.SendToServer()
PPM2.OpenEditor = ->
    if IsValid(PPM2.EditorFrame)
        PPM2.EditorFrame\SetVisible(true)
        PPM2.EditorFrame\Center()
        PPM2.EditorFrame\MakePopup()
        net.Start('PPM2.EditorStatus')
        net.WriteBool(true)
        net.SendToServer()
        return

    frame = vgui.Create('DFrame')
    self = frame
    W, H = ScrW() - 25, ScrH() - 25
    @SetSize(W, H)
    @Center()
    @MakePopup()
    @SetTitle('PPM2 Pony Editor')
    @SetDeleteOnClose(false)
    PPM2.EditorFrame = @

    @OnClose = ->
        net.Start('PPM2.EditorStatus')
        net.WriteBool(false)
        net.SendToServer()

    @menus = vgui.Create('DPropertySheet', @)
    @menus\Dock(LEFT)
    @menus\SetSize(370, 0)

    @model = vgui.Create('PPM2ModelPanel', @)
    @model\Dock(FILL)

    copy = PPM2.GetMainData()\Copy()
    ply = LocalPlayer()
    editorModelSelect = USE_MODEL\GetString()\upper()
    editorModelSelect = EditorModels[editorModelSelect] and editorModelSelect or 'DEFAULT'
    ent = @model\ResetModel(nil, EditorModels[editorModelSelect])
    controller = copy\CreateCustomController(ent)
    copy\SetController(controller)
    frame.controller = controller
    frame.data = copy
    frame.DoUpdate = -> pnl\DoUpdate() for i, pnl in pairs @panels

    saveAs = (callback = (->)) ->
        confirm = (txt = '') ->
            txt = txt\Trim()
            return if txt == ''
            copy\SetFilename(txt)
            copy\Save()
            @unsavedChanges = false
            @model.unsavedChanges = false
            @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")
            @panels.saves.rebuildFileList()
            callback(txt)
        Derma_StringRequest('Save as', 'Enter file name without ppm2/ and .txt', copy\GetFilename(), confirm)

    @saveButton = vgui.Create('DButton', @)
    with @saveButton
        \SetText('Save')
        \SetPos(W - 205, 5)
        \SetSize(90, 20)
        .DoClick = -> saveAs()

    @wearButton = vgui.Create('DButton', @)
    with @wearButton
        \SetText('Apply changes (wear)')
        \SetPos(W - 350, 5)
        \SetSize(140, 20)
        lastWear = 0
        .DoClick = ->
            return if RealTime() < lastWear
            lastWear = RealTime() + 5
            mainData = PPM2.GetMainData()
            copy\ApplyDataToObject(mainData, false) -- no save on apply
    @selectModelBox = vgui.Create('DComboBox', @)
    with @selectModelBox
        \SetSize(120, 20)
        \SetPos(W - 475, 5)
        \SetValue(editorModelSelect)
        \AddChoice(choice) for choice in *{'default', 'cppm', 'new'}
        .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
            @SetDeleteOnClose(true)
            RunConsoleCommand('ppm2_editor_model', value)

            confirm = ->
                @Close()
                timer.Simple 0.1, PPM2.OpenEditor
            Derma_Query(
                'You should restart editor for applying change.\nRestart now?\nUnsaved data can be lost!',
                'Editor restart required',
                'Yas!',
                confirm,
                'Noh!'
            )

    @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")

    @model\SetController(controller)
    controller\SetupEntity(ent)

    @panels = {}

    for {:name, :func, :internal} in *EditorPages
        pnl = vgui.Create('PPM2SettingsBase', @menus)
        @menus\AddSheet(name, pnl)
        pnl\SetTargetData(copy)
        pnl\Dock(FILL)
        pnl.frame = @
        func(pnl, @menus)
        @panels[internal] = pnl

concommand.Add 'ppm2_editor', PPM2.OpenEditor

IconData =
	title: 'PPM V2.0',
	icon: 'gui/pped_icon.png',
	width: 960,
	height: 700,
	onewindow: true,
	init: (icon, window) ->
		window\Remove()
		RunConsoleCommand('ppm2_editor')

list.Set('DesktopWindows', 'PPM2', IconData)
CreateContextMenu() if IsValid(g_ContextMenu)

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

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

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

    EDITOR_SEQUENCES: {
        -- idle
        {
            time: 5
            func: (dist, ang, delta) -> dist, ang
        }

        -- Slow move to left
        {
            time: 5
            func: (dist, ang, delta) ->
                ang.y += delta * 10
                return dist, ang
        }

        -- Eyes
        {
            time: 5
            func: (dist, ang, delta) ->
                return 40, Angle(0, 0, 0)
        }

        -- Left view
        {
            time: 3
            func: (dist, ang, delta) ->
                return 80, Angle(0, 90, 0)
        }

        -- Move to right
        {
            time: 7
            func: (dist, ang, delta) ->
                ang.y -= 10 * delta
                return 80, ang
        }

        -- Eyes (bottom)
        {
            time: 5
            func: (dist, ang, delta) ->
                return 20, Angle(40, 0, 0)
        }

        -- Look at top mane
        {
            time: 5
            func: (dist, ang, delta) ->
                return 50, Angle(-20, 180, 0)
        }
    }

    Init: =>
        @animRate = 1
        @seq = @SEQUENCE_STAND
        @targetAngle = Angle(0, 0, 0)
        @angle = Angle(0, 0, 0)
        @distToPony = 100
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @hold = false
        @holdLast = 0
        @mouseX, @mouseY = 0, 0
        @SetMouseInputEnabled(true)
        @editorSeq = 1
        @nextSeq = @EDITOR_SEQUENCES[@editorSeq].time + RealTime()
        @playing = true
        @lastTick = RealTime()
        @SetCursor('none')

        @animButton = vgui.Create('DButton', @)
        with @animButton
            \SetSize(120, 20)
            \SetText('Playing animation')
            .LastStatus = true
            .Think = ->
                if .LastStatus ~= @playing
                    .LastStatus = @playing
                    \SetText('Playing Animation') if @playing
                    \SetText('Play Animation') if not @playing
            .DoClick = ->
                @playing = not @playing
                if @playing
                    @editorSeq = 1
                    @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                    @ResetPosition()

        @seqButton = vgui.Create('DComboBox', @)
        with @seqButton
            \SetSize(120, 20)
            \SetValue('Standing')
            SEQUENCES
            \AddChoice(choice, num) for choice, num in pairs @SEQUENCES
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                @SetSequence(data)
    ResetPosition: =>
        @targetAngle = Angle(0, 0, 0)
        @targetDistToPony = 100
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)

    PerformLayout: (w = 0, h = 0) =>
        @animButton\SetPos(w - 130, 10)
        @seqButton\SetPos(10, 10)

    OnMousePressed: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = true
        @SetCursor('sizeall')
        @holdLast = RealTime() + .1
        @oldPlaying = @playing
        @playing = false
        @mouseX, @mouseY = gui.MousePos()
    OnMouseReleased: (code = MOUSE_LEFT) =>
        return if code ~= MOUSE_LEFT
        @hold = false
        @SetCursor('none')
        if @holdLast > RealTime()
            @playing = true
            if not @oldPlaying
                @editorSeq = 1
                @nextSeq = RealTime() + @EDITOR_SEQUENCES[@editorSeq].time
                @ResetPosition()

    SetController: (val) => @controller = val

    OnMouseWheeled: (wheelDelta = 0) =>
        @playing = false
        @editorSeq = 1
        @targetDistToPony = math.Clamp(@targetDistToPony - wheelDelta * 10, 20, 150)
    GetModel: => @model
    GetSequence: => @seq
    GetSeq: => @seq
    GetAnimRate: => @animRate
    SetAnimRate: (val = 1) => @animRate = val
    SetSeq: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    SetSequence: (val = @SEQUENCE_STAND) =>
        @seq = val
        @model\SetSequence(@seq) if IsValid(@model)
    ResetSequence: => @SetSequence(@SEQUENCE_STAND)
    ResetSeq: => @SetSequence(@SEQUENCE_STAND)

    ResetModel: (ponydata, model = 'models/ppm/player_default_base.mdl') =>
        @model\Remove() if IsValid(@model)
        @model = ClientsideModel(model)
        with @model
            \SetNoDraw(true)
            .__PPM2_PonyData = ponydata
        @model\SetSequence(@seq)
        @model\FrameAdvance(0)
        return @model
    Think: =>
        rtime = RealTime()
        delta = rtime - @lastTick
        @lastTick = rtime
        if IsValid(@model)
            @model\FrameAdvance(delta * @animRate)
            @model\SetPlaybackRate(1)
            @model\SetPoseParameter('move_x', 1)

        @hold = @IsHovered() if @hold

        if @playing
            cseq = @EDITOR_SEQUENCES[@editorSeq]
            if @nextSeq < rtime
                @editorSeq += 1
                @editorSeq = 1 if not @EDITOR_SEQUENCES[@editorSeq]
                cseq = @EDITOR_SEQUENCES[@editorSeq]
                @nextSeq = rtime + cseq.time

            {:p, :y, :r} = @targetAngle
            @targetDistToPony, @targetAngle = cseq.func(@targetDistToPony, Angle(p, y, r), delta)
            @targetDistToPony = math.Clamp(@targetDistToPony, 20, 150)
            @targetAngle.p = math.Clamp(@targetAngle.p, -40, 10)
        else
            if @hold
                x, y = gui.MousePos()
                deltaX, deltaY = x - @mouseX, y - @mouseY
                @mouseX, @mouseY = x, y
                {:pitch, :yaw, :roll} = @targetAngle
                yaw -= deltaX * .5
                pitch = math.Clamp(pitch - deltaY * .5, -40, 10)
                @targetAngle = Angle(pitch, yaw, roll)

        @angle = LerpAngle(delta * 4, @angle, @targetAngle)
        @distToPony = Lerp(delta * 4, @distToPony, @targetDistToPony)
        @vectorPos = Vector(@distToPony, 0, @PONY_VEC_Z)
        @vectorPos\Rotate(@angle)
        @drawAngle = (Vector(0, 0, @PONY_VEC_Z * .7) - @vectorPos)\Angle()

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

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

    WALL_COLOR: Color(98, 189, 176)
    FLOOR_COLOR: Color(53, 150, 84)

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

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

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

        @controller\GetRenderController()\PreDraw(@model) if @controller
        @model\DrawModel()
        @controller\GetRenderController()\PostDraw(@model) if @controller

        cam.End3D()
    OnRemove: =>
        @model\Remove() if IsValid(@model)
}

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

PANEL_SETTINGS_BASE = {
    Init: =>
        @shouldSaveData = false
        @SetMouseInputEnabled(true)
        @SetKeyboardInputEnabled(true)
        @DockPadding(5, 5, 5, 5)
        @unsavedChanges = false
        @updateFuncs = {}
    ValueChanges: (valID, newVal, pnl) =>
        @unsavedChanges = true
        @frame.unsavedChanges = true
        @frame\SetTitle("#{@GetTargetData() and @GetTargetData()\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor; *Unsaved changes*")
    GetShouldSaveData: => @shouldSaveData
    ShouldSaveData: => @shouldSaveData
    SetShouldSaveData: (val = false) => @shouldSaveData = val
    GetTargetData: => @data
    TargetData: => @data
    SetTargetData: (val) => @data = val
    DoUpdate: => func() for func in *@updateFuncs
    NumSlider: (name = 'Slider', option = '', decimals = 0) =>
		with vgui.Create('DNumSlider', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 0, 2, 0)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetText(name)
			\SetMin(0)
			\SetMax(1)
			\SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
			\SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
			\SetDecimals(decimals)
            \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
			.TextArea\SetTextColor(color_white)
			.Label\SetTextColor(color_white)
            .OnValueChanged = (pnl, newVal = 1) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetMin(@GetTargetData()["GetMin#{option}"](@GetTargetData())) if @GetTargetData()
                \SetMax(@GetTargetData()["GetMax#{option}"](@GetTargetData())) if @GetTargetData()
                \SetValue(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    Label: (text = '') =>
        with vgui.Create('DLabel', @scroll or @)
            \SetText(text)
            \Dock(TOP)
            \SetTextColor(color_white)
            \SizeToContents()
            w, h = \GetSize()
            \SetSize(w, h + 5)
            @scroll\AddItem(_with_0) if IsValid(@scroll)
	CheckBox: (name = 'Label', option = '') =>
		with vgui.Create('DCheckBoxLabel', @scroll or @)
			\Dock(TOP)
			\DockMargin(2, 2, 2, 2)
			\SetText(name)
			\SetTextColor(color_white)
			\SetTooltip("#{name}\nData value: #{option}")
			\SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .OnChange = (pnl, newVal = false) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal and 1 or 0, @GetShouldSaveData())
                @ValueChanges(option, newVal and 1 or 0, pnl)
            table.insert @updateFuncs, ->
                \SetChecked(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            @scroll\AddItem(_with_0) if IsValid(@scroll)
    ColorBox: (name = 'Colorful Box', option = '') =>
        collapse = vgui.Create('DCollapsibleCategory', @scroll or @)
        box = vgui.Create('DColorMixer', collapse)
        collapse.box = box
        with box
            \SetSize(250, 250)
			\SetTooltip("#{name}\nData value: #{option}")
            \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            .ValueChanged = (pnl, newVal = Color(0, 0, 0)) ->
                return if option == ''
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, newVal, @GetShouldSaveData())
                @ValueChanges(option, newVal, pnl)
            table.insert @updateFuncs, ->
                \SetColor(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        with collapse
            \SetContents(box)
            \Dock(TOP)
            \DockMargin(2, 2, 2, 2)
            \SetSize(250, 250)
            \SetLabel(name)
            \SetExpanded(false)
        @scroll\AddItem(collapse) if IsValid(@scroll)
        return box, collapse
    ComboBox: (name = 'Combo Box', option = '', choices) =>
        label = vgui.Create('DLabel', @scroll or @)
        with label
            \SetText(name)
            \SetTextColor(color_white)
            \Dock(TOP)
            \SetSize(0, 20)
            \DockMargin(5, 0, 5, 0)
            \SetMouseInputEnabled(true)
        @scroll\AddItem(label) if IsValid(@scroll)
        box = vgui.Create('DComboBox', label)
        with box
            \Dock(RIGHT)
            \SetSize(170, 0)
            \DockMargin(0, 0, 5, 0)
            \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
            if choices
                \AddChoice(choice) for choice in *choices
            else
                \AddChoice(choice) for choice in *@GetTargetData()["Get#{option}Types"](@GetTargetData()) if @GetTargetData() and @GetTargetData()["Get#{option}Types"]
            .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
                index -= 1
                data = @GetTargetData()
                return if not data
                data["Set#{option}"](data, index, @GetShouldSaveData())
                @ValueChanges(option, index, pnl)
            table.insert @updateFuncs, ->
                \SetValue(@GetTargetData()["Get#{option}Enum"](@GetTargetData())) if @GetTargetData()
        return box, label
    URLInput: (option = '') =>
        wrapper = vgui.Create('EditablePanel', @scroll or @)
        with wrapper
            \Dock(TOP)
            \DockMargin(5, 10, 5, 10)
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            \SetSize(0, 30)
        textInput = vgui.Create('DTextEntry', wrapper)
        @scroll\AddItem(wrapper) if IsValid(@scroll)
        with textInput
            \Dock(FILL)
            \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
            \SetKeyboardInputEnabled(true)
            \SetMouseInputEnabled(true)
            .OnEnter = ->
                text = \GetValue()
                if text\find('^https?://')
                    @GetTargetData()["Set#{option}"](@GetTargetData(), text)
                    @ValueChanges(option, text, textInput)
                else
                    @GetTargetData()["Set#{option}"](@GetTargetData(), '')
                    @ValueChanges(option, '', textInput)
            .OnKeyCodeTyped = (pnl, key = KEY_FIRST) ->
                switch key
                    when KEY_FIRST
                        return true
                    when KEY_NONE
                        return true
                    when KEY_TAB
                        return true
                    when KEY_ENTER
                        .OnEnter()
                        \KillFocus()
                        return true
                timer.Create "PPM2.EditorCodeChange.#{option}", 1, 1, ->
                    return if not IsValid(textInput)
                    .OnEnter()
            table.insert @updateFuncs, -> \SetText(@GetTargetData()["Get#{option}"](@GetTargetData())) if @GetTargetData()
        return textInput
    ScrollPanel: =>
        return @scroll if IsValid(@scroll)
        @scroll = vgui.Create('DScrollPanel', @)
        @scroll\Dock(FILL)
        return @scroll
    Paint: (w = 0, h = 0) =>
		surface.SetDrawColor(130, 130, 130)
		surface.DrawRect(0, 0, w, h)
}

vgui.Register('PPM2SettingsBase', PANEL_SETTINGS_BASE, 'EditablePanel')

EditorPages = {
    {
        'name': 'Main'
        'internal': 'main'
        'func': (sheet) =>
            @CheckBox('Gender', 'Gender')
            @ComboBox('Race', 'Race')
            @NumSlider('Weight', 'Weight', 2)
            @CheckBox('Socks (simple texture)', 'Socks')
            @ComboBox('Eyelashes', 'EyelashType')
            @ComboBox('Bodysuit', 'Bodysuit')
            @ColorBox('Body color', 'BodyColor')
            @CheckBox('Socks (as model)', 'SocksAsModel')
            @ColorBox('Socks model color', 'SocksColor')
    }

    {
        'name': 'Eyes'
        'internal': 'eyes'
        'func': (sheet) =>
            @ScrollPanel()
            @CheckBox('Use separated settings for eyes', 'SeparateEyes')
            for publicName in *{'', 'Left', 'Right'}
                @Label("'#{publicName}' Eye settings")
                @CheckBox("#{publicName} Eye lines", "EyeLines#{publicName}")
                @CheckBox("#{publicName} Derp eye", "DerpEyes#{publicName}")
                @NumSlider("#{publicName} Derp eye strength", "DerpEyesStrength#{publicName}", 2)
                @NumSlider("#{publicName} Eye size", "IrisSize#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole width", "HoleWidth#{publicName}", 2)
                @NumSlider("#{publicName} Eye hole size", "HoleSize#{publicName}", 2)

                @ColorBox("#{publicName} Eye background", "EyeBackground#{publicName}")
                @ColorBox("#{publicName} Eye hole", "EyeHole#{publicName}")
                @ColorBox("#{publicName} Top eye iris", "EyeIrisTop#{publicName}")
                @ColorBox("#{publicName} Bottom eye iris", "EyeIrisBottom#{publicName}")
                @ColorBox("#{publicName} Eye line 1", "EyeIrisLine1#{publicName}")
                @ColorBox("#{publicName} Eye line 2", "EyeIrisLine2#{publicName}")
                @ColorBox("#{publicName} Eye line 2", "EyeIrisLine2#{publicName}")
    }

    {
        'name': 'Mane and tail'
        'internal': 'manetail'
        'func': (sheet) =>
            @ScrollPanel()
            @Label('"New" types affect only new model')
            @ComboBox('Tail type', 'TailType')
            @ComboBox('New Tail type', 'TailTypeNew')

            @ComboBox('Mane type', 'ManeType')
            @ComboBox('New Mane type', 'ManeTypeNew')

            @ComboBox('Lower Mane type', 'ManeTypeLower')
            @ComboBox('New Lower Mane type', 'ManeTypeLowerNew')

            for i = 1, 2
                @ColorBox("Mane color #{i}", "ManeColor#{i}")
                @ColorBox("Tail color #{i}", "TailColor#{i}")

            for i = 1, 6
                @ColorBox("Mane detail color #{i}", "ManeColor#{i}")
                @ColorBox("Tail detail color #{i}", "TailDetailColor#{i}")

            @CheckBox('Separate upper and lower mane colors', 'SeparateMane')
            @Label('These options have effect only on new model')

            for i = 1, 2
                @ColorBox("Upper Mane color #{i}", "UpperManeColor#{i}")
                @ColorBox("Lower Mane color #{i}", "LowerManeColor#{i}")

            for i = 1, 6
                @ColorBox("Upper Mane detail color #{i}", "UpperManeDetailColor#{i}")
                @ColorBox("Lower Tail detail color #{i}", "LowerManeDetailColor#{i}")
    }

    {
        'name': 'Body details'
        'internal': 'bodydetail'
        'func': (sheet) =>
            @ScrollPanel()
            for i = 1, PPM2.MAX_BODY_DETAILS
                @ComboBox("Detail #{i}", "BodyDetail#{i}")
                @ColorBox("Detail color #{i}", "BodyDetailColor#{i}")

            @Label('Body detail URL image input fields\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')
            for i = 1, PPM2.MAX_BODY_DETAILS
                @Label("Body detail #{i}")
                @URLInput("BodyDetailURL#{i}")
                @ColorBox("URL Detail color #{i}", "BodyDetailURLColor#{i}")
    }

    {
        'name': 'Cutiemark'
        'internal': 'cmark'
        'func': (sheet) =>
            @CheckBox('Display cutiemark', 'CMark')
            @ComboBox('Cutiemark type', 'CMarkType')
            @markDisplay = vgui.Create('EditablePanel', @)
            with @markDisplay
                \Dock(TOP)
                \SetSize(320, 320)
                \DockMargin(20, 20, 20, 20)
                .Paint = (pnl, w = 0, h = 0) ->
                    data = @GetTargetData()
                    return if not data
                    controller = data\GetController()
                    return if not controller
                    rcontroller = controller\GetRenderController()
                    return if not rcontroller
                    tcontroller = rcontroller\GetTextureController()
                    return if not tcontroller
                    mat = tcontroller\GetCMarkGUI()
                    return if not mat
                    surface.SetDrawColor(0, 0, 0)
                    surface.DrawRect(0, 0, w, h)
                    surface.SetDrawColor(255, 255, 255)
                    surface.SetMaterial(mat)
                    surface.DrawTexturedRect(0, 0, w, h)

            @Label('Cutiemark URL image input field\nShould be PNG or JPEG (works same as\nPAC3 URL texture)')\DockMargin(5, 10, 5, 10)
            @URLInput('CMarkURL')
    }

    {
        'name': 'Files'
        'internal': 'saves'
        'func': (sheet) =>
            @Label('Open file by double click')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    @frame.data\SetFilename(fil)
                    @frame.data\ReadFromDisk(true)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = false
                    @frame.unsavedChanges = false
                    @frame\SetTitle("#{fil} - PPM2 Pony Editor")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm2/*', 'DATA')
                for fil in *files
                    matchBak = '.bak.txt'
                    continue if fil\sub(-#matchBak) == matchBak
                    list\AddLine(fil\sub(1, #fil - 4))
            @rebuildFileList()
    }

    {
        'name': 'Old Files'
        'internal': 'oldsaves'
        'func': (sheet) =>
            @Label('!!! It may or may not work. You will be squished.')
            list = vgui.Create('DListView', @)
            list\Dock(FILL)
            list\SetMultiSelect(false)
            list.DoDoubleClick = (pnl, rowID, line) ->
                fil = line\GetColumnText(1)
                confirm = ->
                    newData = PPM2.ReadFromOldData(fil)
                    if not newData
                        Derma_Message('Failed to import.', 'Onoh!', 'Okai ;w;')
                        return
                    @frame.data\SetFilename(newData\GetFilename())
                    newData\ApplyDataToObject(@frame.data, false)
                    @frame.data\UpdateController()
                    @frame.DoUpdate()
                    @unsavedChanges = true
                    @frame.unsavedChanges = true
                    @frame\SetTitle("#{newData\GetFilename()} - PPM2 Pony Editor; *Unsaved changes*")
                if @unsavedChanges
                    Derma_Query(
                        "Currently, you did not stated your changes.\nDo you really want to open #{fil}?",
                        'Unsaved changes!',
                        'Yas!',
                        confirm,
                        'Noh!'
                    )
                else
                    confirm()
            list\AddColumn('Filename')
            @rebuildFileList = ->
                list\Clear()
                files, dirs = file.Find('ppm/*', 'DATA')
                list\AddLine(fil\sub(1, #fil - 4)) for fil in *files
            @rebuildFileList()
    }
}

EditorModels = {
    'DEFAULT': 'models/ppm/player_default_base.mdl'
    'CPPM': 'models/cppm/player_default_base.mdl'
    'NEW': 'models/ppm/player_default_base_new.mdl'
}

USE_MODEL = CreateConVar('ppm2_editor_model', 'new', {FCVAR_ARCHIVE}, 'What model to use in editor. Valids are "default", "cppm", "new"')

if IsValid(PPM2.EditorFrame)
    PPM2.EditorFrame\Remove()
    net.Start('PPM2.EditorStatus')
    net.WriteBool(false)
    net.SendToServer()
PPM2.OpenEditor = ->
    if IsValid(PPM2.EditorFrame)
        PPM2.EditorFrame\SetVisible(true)
        PPM2.EditorFrame\Center()
        PPM2.EditorFrame\MakePopup()
        net.Start('PPM2.EditorStatus')
        net.WriteBool(true)
        net.SendToServer()
        return

    frame = vgui.Create('DFrame')
    self = frame
    W, H = ScrW() - 25, ScrH() - 25
    @SetSize(W, H)
    @Center()
    @MakePopup()
    @SetTitle('PPM2 Pony Editor')
    @SetDeleteOnClose(false)
    PPM2.EditorFrame = @

    @OnClose = ->
        net.Start('PPM2.EditorStatus')
        net.WriteBool(false)
        net.SendToServer()

    @menus = vgui.Create('DPropertySheet', @)
    @menus\Dock(LEFT)
    @menus\SetSize(370, 0)

    @model = vgui.Create('PPM2ModelPanel', @)
    @model\Dock(FILL)

    copy = PPM2.GetMainData()\Copy()
    ply = LocalPlayer()
    editorModelSelect = USE_MODEL\GetString()\upper()
    editorModelSelect = EditorModels[editorModelSelect] and editorModelSelect or 'DEFAULT'
    ent = @model\ResetModel(nil, EditorModels[editorModelSelect])
    controller = copy\CreateCustomController(ent)
    copy\SetController(controller)
    frame.controller = controller
    frame.data = copy
    frame.DoUpdate = -> pnl\DoUpdate() for i, pnl in pairs @panels

    saveAs = (callback = (->)) ->
        confirm = (txt = '') ->
            txt = txt\Trim()
            return if txt == ''
            copy\SetFilename(txt)
            copy\Save()
            @unsavedChanges = false
            @model.unsavedChanges = false
            @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")
            @panels.saves.rebuildFileList()
            callback(txt)
        Derma_StringRequest('Save as', 'Enter file name without ppm2/ and .txt', copy\GetFilename(), confirm)

    @saveButton = vgui.Create('DButton', @)
    with @saveButton
        \SetText('Save')
        \SetPos(W - 205, 5)
        \SetSize(90, 20)
        .DoClick = -> saveAs()

    @wearButton = vgui.Create('DButton', @)
    with @wearButton
        \SetText('Apply changes (wear)')
        \SetPos(W - 350, 5)
        \SetSize(140, 20)
        lastWear = 0
        .DoClick = ->
            return if RealTime() < lastWear
            lastWear = RealTime() + 5
            mainData = PPM2.GetMainData()
            copy\ApplyDataToObject(mainData, false) -- no save on apply
    @selectModelBox = vgui.Create('DComboBox', @)
    with @selectModelBox
        \SetSize(120, 20)
        \SetPos(W - 475, 5)
        \SetValue(editorModelSelect)
        \AddChoice(choice) for choice in *{'default', 'cppm', 'new'}
        .OnSelect = (pnl = box, index = 1, value = '', data = value) ->
            @SetDeleteOnClose(true)
            RunConsoleCommand('ppm2_editor_model', value)

            confirm = ->
                @Close()
                timer.Simple 0.1, PPM2.OpenEditor
            Derma_Query(
                'You should restart editor for applying change.\nRestart now?\nUnsaved data can be lost!',
                'Editor restart required',
                'Yas!',
                confirm,
                'Noh!'
            )

    @SetTitle("#{copy\GetFilename() or '%ERRNAME%'} - PPM2 Pony Editor")

    @model\SetController(controller)
    controller\SetupEntity(ent)

    @panels = {}

    for {:name, :func, :internal} in *EditorPages
        pnl = vgui.Create('PPM2SettingsBase', @menus)
        @menus\AddSheet(name, pnl)
        pnl\SetTargetData(copy)
        pnl\Dock(FILL)
        pnl.frame = @
        func(pnl, @menus)
        @panels[internal] = pnl

concommand.Add 'ppm2_editor', PPM2.OpenEditor

IconData =
	title: 'PPM V2.0',
	icon: 'gui/pped_icon.png',
	width: 960,
	height: 700,
	onewindow: true,
	init: (icon, window) ->
		window\Remove()
		RunConsoleCommand('ppm2_editor')

list.Set('DesktopWindows', 'PPM2', IconData)
CreateContextMenu() if IsValid(g_ContextMenu)


Update theme color

Update theme color

html = require "lapis.html"
import to_json from require "lapis.util"

class DefaultLayout extends html.Widget
    content: =>
        js = (require 'views.javascript')!.js
        html_5 ->
            head ->
                meta charset: "UTF-8"
                meta property:"og:image", content:"/static/images/ni.png"

                -- This enables web-scale mode, right?
                meta name: "viewport", content:"initial-scale=1"

                link href: "/static/style.css", rel: "stylesheet", type: "text/css"
                link rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png?v=6"
                link rel: "icon", type: "image/png", href: "/favicon-32x32.png?v=6", sizes: "32x32"
                link rel: "icon", type: "image/png", href: "/favicon-16x16.png?v=6", sizes: "16x16"
                link rel: "manifest", href: "/manifest.json?v=3"
                link rel: "mask-icon", color: "e37100", href: "/safari-pinned-tab.svg?v=6"
                link rel: "shortcut icon", href: "/favicon.ico?v=6"
                meta name: "theme-color", content: "#e37100"

                title (@m and @m.title) or "MG CS Week"

            body id: @page_id, ->
                @content_for "header"

                div id: "skrollr-body"
                main id: "content", ->
                    @content_for "inner"

                @content_for "footer"
                for scrpt in *js
                    with scrpt
                        contents = .contents
                        .contents = nil
                        script scrpt, ->
                            raw contents

    upload: =>

html = require "lapis.html"
import to_json from require "lapis.util"

class DefaultLayout extends html.Widget
    content: =>
        js = (require 'views.javascript')!.js
        html_5 ->
            head ->
                meta charset: "UTF-8"
                meta property:"og:image", content:"/static/images/ni.png"

                -- This enables web-scale mode, right?
                meta name: "viewport", content:"initial-scale=1"

                link href: "/static/style.css", rel: "stylesheet", type: "text/css"
                link rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png?v=6"
                link rel: "icon", type: "image/png", href: "/favicon-32x32.png?v=6", sizes: "32x32"
                link rel: "icon", type: "image/png", href: "/favicon-16x16.png?v=6", sizes: "16x16"
                link rel: "manifest", href: "/manifest.json?v=3"
                link rel: "mask-icon", color: "890ca0", href: "/safari-pinned-tab.svg?v=6"
                link rel: "shortcut icon", href: "/favicon.ico?v=6"
                meta name: "theme-color", content: "#890ca0"

                title (@m and @m.title) or "MG CS Week"

            body id: @page_id, ->
                @content_for "header"

                div id: "skrollr-body"
                main id: "content", ->
                    @content_for "inner"

                @content_for "footer"
                for scrpt in *js
                    with scrpt
                        contents = .contents
                        .contents = nil
                        script scrpt, ->
                            raw contents

    upload: =>


More test code

More test code

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

class NetworkChangeState
	new: (key = '', keyValid = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@keyValid = keyValid
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTime()
		@rtime = RealTime()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
	GetPlayer: => @ply
	ChangedByClient: => IsValid(@ply)
	ChangedByPlayer: => IsValid(@ply)
	GetKey: => @keyValid
	GetVariable: => @keyValid
	GetVar: => @keyValid
	GetKeyInternal: => @key
	GetVariableInternal: => @key
	GetVarInternal: => @key
	GetNewValue: => @newValue
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTime: => @rtime
	GetRealTime: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len

	Revert: => @obj[@key] = @oldValue
	Apply: => @obj[@key] = @newValue

class NetworkedObject
	@Setup = =>
		@NW_Vars = {}
		@NW_VarsTable = {}
		@NW_Objects = {}
		@NW_Waiting = {}
		@NW_WaitID = -1
		@NW_Setup = true
		@NW_NextVarID = -1
		@NW_Create = "PPM2.NW.Created.#{@__name}"
		@NW_Modify = "PPM2.NW.Modified.#{@__name}"
		@NW_Remove = "PPM2.NW.Removed.#{@__name}"
		@NW_ReceiveID = "PPM2.NW.ReceiveID.#{@__name}"
		@NW_NextObjectID = -1

		if SERVER
			util.AddNetworkString(@NW_Create)
			util.AddNetworkString(@NW_Modify)
			util.AddNetworkString(@NW_Remove)
			util.AddNetworkString(@NW_ReceiveID)

		net.Receive @NW_Create, (len = 0, ply = NULL) -> @OnNetworkedCreated(ply, len)
		net.Receive @NW_Modify, (len = 0, ply = NULL) -> @OnNetworkedModify(ply, len)
		net.Receive @NW_Remove, (len = 0, ply = NULL) -> @OnNetworkedDelete(ply, len)
		net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
			return if SERVER
			waitID = net.ReadUInt(16)
			netID = net.ReadUInt(16)
			obj = @NW_Waiting[netID]
			@NW_Waiting[netID] = nil
			return unless obj
			obj.NETWORKED = true
			obj.netID = netID
	@__inherited = (child) => child.Setup(child)
	@Setup()

	@AddNetworkVar = (getName = 'Var', readFunc = (->), writeFunc = (->), defValue) =>
		strName = "_NW_#{getName}"
		@NW_NextVarID += 1
		id = @NW_NextVarID
		tab = {:strName, :readFunc, :getName, :writeFunc, :defValue, :id}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[id] = tab
		@__base[strName] = defValue
		@__base["Get#{getName}"] = => @[strName]
		@__base["Set#{getName}"] = (val = defValue, networkNow = true) =>
			oldVal = @[strName]
			@[strName] = val
			@SetLocalChange(getName, oldVal, val)
			if networkNow and @NETWORKED and (CLIENT and @@NW_ClientsideCreation or SERVER)
				net.Start(@@NW_Modify)
				net.WriteUInt(@GetNetworkID(), 16)
				net.WriteUInt(id, 8)
				writeFunc(@[strName])
				if CLIENT
					net.SendToServer()
				else
					net.Broadcast()
	@NetworkVar = (...) => @AddNetworkVar(...)

	@NW_ClientsideCreation = false
	@NW_RemoveOnPlayerLeave = true
	@OnNetworkedCreated = (ply = NULL, len = 0) =>
		return if SERVER and not @NW_ClientsideCreation
		if CLIENT
			netID = net.ReadUInt(16)
			obj = @NW_Objects[netID] or @(netID)
			obj.NETWORKED = true
			obj\ReadNetworkData()
			@OnNetworkedCreatedCallback(obj, ply, len)
		else
			waitID = net.ReadUInt(16)
			obj = @()
			obj.NW_Player = ply
			obj.NETWORKED = ply
			obj\ReadNetworkData()
			obj\Create()
			net.Start(@NW_ReceiveID)
			net.WriteUInt(waitID, 16)
			net.WriteUInt(obj.netID)
			net.Send(ply)
			@OnNetworkedCreatedCallback(obj, ply, len)
	@OnNetworkedCreatedCallback = (obj, ply = NULL, len = 0) => -- Override

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		return unless obj
		return if IsValid(ply) and obj.NW_Player ~= ply
		varID = net.ReadUInt(8)
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :getName, :readFunc, :writeFunc} = varData
		newVal = readFunc()
		state = NetworkChangeState(strName, getName, newVal, obj, len, ply)
		state\Apply()
		obj\NetworkDataChanges(state)
		if SERVER
			net.Start(@NW_Modify)
			net.WriteUInt(id, 16)
			net.WriteUInt(varID, 8)
			writeFunc(newVal)
			net.SendOmit(ply)
		@OnNetworkedModifyCallback(state)
	@OnNetworkedModifyCallback = (state) => -- Override

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove()
		@OnNetworkedDeleteCallback(obj, ply, len)
	@OnNetworkedDeleteCallback = (obj, ply = NULL, len = 0) => -- Override

	@ReadNetworkData = =>
		output = {strName, readFunc() for {:strName, :readFunc} in *@NW_Vars}
		return output

	new: (netID = @@NW_NextObjectID, localObject = false) =>
		@valid = true
		@NETWORKED = false

		if SERVER
			@netID = @@NW_NextObjectID
			@@NW_NextObjectID += 1
		else
			@netID = netID

		@@NW_Objects[@netID] = @
		@NW_Player = NULL
		@isLocal = localObject
		@NW_Player = LocalPlayer() if localObject

	IsValid: => @valid
	IsNetworked: => @NETWORKED
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID
	Remove: =>
		@@NW_Objects[@netID] = nil
		@valid = false
		if CLIENT and @isLocal and @NETWORKED and @@NW_ClientsideCreation
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID)
			net.SendToServer()
		elseif SERVER and @NETWORKED
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID)
			if not IsValid(@NW_Player)
				net.Broadcast()
			else
				net.SendOmit(@NW_Player)

	NetworkDataChanges: (state) => -- Override
	SetLocalChange: (key, oldVal, newVal) => -- Override
	ReadNetworkData: (len = 24, ply = NULL, silent = false) =>
		data = @@ReadNetworkData()
		states = [NetworkChangeState(key, newVal, @, len, ply) for key, newVal in pairs data]
		for state in *states
			state\Apply()
			@NetworkDataChanges(state) unless silent

	WriteNetworkData: => writeFunc(@[strName]) for {:strName, :writeFunc} in *@NW_Vars

	SendVar: (Var = '') =>
		return if @[Var] == nil

	Create: =>
		return if @NETWORKED
		return if CLIENT and not @@NW_ClientsideCreation
		@NETWORKED = true
		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt(@netID)
			@WriteNetworkData()
			if not IsValid(@NW_Player)
				net.Broadcast()
			else
				net.SendOmit(@NW_Player)
		else
			@NW_WaitID += 1
			net.Start(@@NW_Create)
			net.WriteUInt(@NW_WaitID, 16)
			@WriteNetworkData()
			net.SendToServer()
			@@NW_Waiting[@NW_WaitID] = @

PPM2.NetworkedObject = NetworkedObject

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

class NetworkChangeState
	new: (key = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTime()
		@rtime = RealTime()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
	GetPlayer: => @ply
	ChangedByClient: => IsValid(@ply)
	ChangedByPlayer: => IsValid(@ply)
	GetKey: => @key
	GetVariable: => @key
	GetVar: => @key
	GetNewValue: => @newValue
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTime: => @rtime
	GetRealTime: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len

	Revert: => @obj[@key] = @oldValue
	Apply: => @obj[@key] = @newValue

class NetworkedObject
	@Setup = =>
		@NW_Vars = {}
		@NW_VarsTable = {}
		@NW_Objects = {}
		@NW_Waiting = {}
		@NW_WaitID = -1
		@NW_Setup = true
		@NW_NextVarID = -1
		@NW_Create = "PPM2.NW.Created.#{@__name}"
		@NW_Modify = "PPM2.NW.Modified.#{@__name}"
		@NW_Remove = "PPM2.NW.Removed.#{@__name}"
		@NW_ReceiveID = "PPM2.NW.ReceiveID.#{@__name}"
		@NW_NextObjectID = -1

		if SERVER
			util.AddNetworkString(@NW_Create)
			util.AddNetworkString(@NW_Modify)
			util.AddNetworkString(@NW_Remove)
			util.AddNetworkString(@NW_ReceiveID)

		net.Receive @NW_Create, (len = 0, ply = NULL) -> @OnNetworkedCreated(ply, len)
		net.Receive @NW_Modify, (len = 0, ply = NULL) -> @OnNetworkedModify(ply, len)
		net.Receive @NW_Remove, (len = 0, ply = NULL) -> @OnNetworkedDelete(ply, len)
		net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
			return if SERVER
			waitID = net.ReadUInt(16)
			netID = net.ReadUInt(16)
			obj = @NW_Waiting[netID]
			@NW_Waiting[netID] = nil
			return unless obj
			obj.NETWORKED = true
			obj.netID = netID
	@__inherited = (child) => child.Setup(child)
	@Setup()

	@AddNetworkVar = (strName = 'var', readFunc = (->), writeFunc = (->)) =>
		@NW_NextVarID += 1
		tab = {:strName, :readFunc, :writeFunc, id: @NW_NextVarID}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[@NW_NextVarID] = tab

	@NW_ClientsideCreation = false
	@NW_RemoveOnPlayerLeave = true
	@OnNetworkedCreated = (ply = NULL, len = 0) =>
		return if SERVER and not @NW_ClientsideCreation
		if CLIENT
			netID = net.ReadUInt(16)
			obj = @NW_Objects[netID] or @(netID)
			obj.NETWORKED = true
			obj\ReadNetworkData()
			@OnNetworkedCreatedCallback(obj, ply, len)
		else
			waitID = net.ReadUInt(16)
			obj = @()
			obj.NW_Player = ply
			obj.NETWORKED = ply
			obj\ReadNetworkData()
			obj\Create()
			net.Start(@NW_ReceiveID)
			net.WriteUInt(waitID, 16)
			net.WriteUInt(obj.netID)
			net.Send(ply)
			@OnNetworkedCreatedCallback(obj, ply, len)
	@OnNetworkedCreatedCallback = (obj, ply = NULL, len = 0) => -- Override

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		return unless obj
		return if IsValid(ply) and obj.NW_Player ~= ply
		varID = net.ReadUInt(8)
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :readFunc} = varData
		newVal = readFunc()
		state = NetworkChangeState(strName, newVal, obj, len, ply)
		state\Apply()
		obj\NetworkDataChanges(state)
		@OnNetworkedModifyCallback(state)
	@OnNetworkedModifyCallback = (state) => -- Override

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		return if not @NW_ClientsideCreation and IsValid(ply)
		id = net.ReadUInt(16)
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove()
		@OnNetworkedDeleteCallback(obj, ply, len)
	@OnNetworkedDeleteCallback = (obj, ply = NULL, len = 0) => -- Override

	@ReadNetworkData = =>
		output = {strName, readFunc() for {:strName, :readFunc} in *@NW_Vars}
		return output

	new: (netID = @@NW_NextObjectID, localObject = false) =>
		@valid = true
		@NETWORKED = false

		if SERVER
			@netID = @@NW_NextObjectID
			@@NW_NextObjectID += 1
		else
			@netID = netID

		@@NW_Objects[@netID] = @
		@NW_Player = NULL
		@isLocal = localObject
		@NW_Player = LocalPlayer() if localObject

	IsValid: => @valid
	IsNetworked: => @NETWORKED
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID
	Remove: =>
		@@NW_Objects[@netID] = nil
		@valid = false
		if CLIENT and @isLocal and @NETWORKED and @@NW_ClientsideCreation
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID)
			net.SendToServer()
		elseif SERVER and @NETWORKED
			net.Start(@@NW_Remove)
			net.WriteUInt(@netID)
			if not IsValid(@NW_Player)
				net.Broadcast()
			else
				net.SendOmit(@NW_Player)

	NetworkDataChanges: (state) => -- Override
	ReadNetworkData: (len = 24, ply = NULL, silent = false) =>
		data = @@ReadNetworkData()
		states = [NetworkChangeState(key, newVal, @, len, ply) for key, newVal in pairs data]
		for state in *states
			state\Apply()
			@NetworkDataChanges(state) unless silent

	WriteNetworkData: => writeFunc(@[strName]) for {:strName, :writeFunc} in *@NW_Vars

	Create: =>
		return if @NETWORKED
		return if CLIENT and not @@NW_ClientsideCreation
		@NETWORKED = true
		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt(@netID)
			@WriteNetworkData()
			if not IsValid(@NW_Player)
				net.Broadcast()
			else
				net.SendOmit(@NW_Player)
		else
			@NW_WaitID += 1
			net.Start(@@NW_Create)
			net.WriteUInt(@NW_WaitID, 16)
			@WriteNetworkData()
			net.SendToServer()
			@@NW_Waiting[@NW_WaitID] = @

PPM2.NetworkedObject = NetworkedObject


netutils: support checking wildcard certs

cqueues = require 'cqueues'
dns = require'cqueues.dns'
packet = require'cqueues.dns.packet'
record = require'cqueues.dns.record'
errno = require'cqueues.errno'
socket = require'cqueues.socket'
context = require'openssl.ssl.context'
http_tls = require'http.tls'
pkey = require'openssl.pkey'
auxlib = require"openssl.auxlib"
util = require'util'
{:simplehttp, :bold, :split, :green, :red, :simplehttp, :json, :urlEncode} = require'util'

cache = {}

HasValue = (table, value) ->
  if(type(table) ~= 'table') then return
  for _, v in next, table
    if(v == value) then return true

Good = ->
  "#{green '🔒'} TLS #{green 'ok'}"

Bad = ->
  "#{red '🔓 '} #{red 'Bad'} TLS"

Query = (host, types='AAAA,A', timeout=15) ->
  out = {}

  for type in *split(types, ',')
    answer = dns.query(host, type, 'IN', timeout)

    for rec in answer\grep{section:packet.section.ANSWER}
      out[#out+1] = tostring(rec)

  return out

{
  PRIVMSG: {
    '^%pttls (.*)$': (source, destination, arg) =>
      host = arg\match'^%s*(.-)[:%s].*$' or arg
      port = arg\match'^.*[: ](.*)%s*$' or 443
      port = tonumber(port) or 0
      if port <= 0 or port > 65535
        return reply "Think you're clever?"
      hosts = Query(host)
      if #hosts == 0
        -- assume IP
        hosts = {host}
      found = false
      for ip in *hosts
        sock = socket.connect{host:ip, :port, verify:false, tls_sendname:host}
        ok, sock, err = pcall -> sock\connect 5
        if not ok
          return say "Error connecting to #{ip}:#{port} : #{sock}"
        if ip\match':'
          ip = '[' .. ip .. ']'
        if not sock
          reply "Error #{err} connecting to #{ip}:#{port} : #{errno.strerror err}"
        else
          ctx = http_tls.new_client_context()
          --ctx = context.new('SSL3', false)
          --ctx\setOptions(context.OP_NO_COMPRESSION+contextopenssl_ctx.OP_SINGLE_ECDH_USE)
          --ctx\setEphemeralKey(pkey.new{ type = "EC", curve = "prime256v1" }
          ok, tls, err = pcall -> sock\starttls 5
          if not ok
            return say "TLS negotiation error : #{tls}"
          if tls
            ssl = tls\checktls!
            if sock
              sock\close!
            cipher = ssl\getCipherInfo!
            if http_tls.banned_ciphers[cipher.name]
              return say "Banned cipher: #{cipher.name} when negotiating TLS #{ip}:#{port}"
            cert = ssl\getPeerCertificate!
            before, after = cert\getLifetime!
            cn = tostring cert\getSubject!
            san = cert\getSubjectAlt!
            validhosts = {cn\match'CN=(.*)'}
            for type, sanhost in auxlib.pairs(san)
              table.insert(validhosts, sanhost)
            -- cheat and parse text
            text = cert\text!
            @Log 'debug', text
            expires = text\match('Not After : (.-)%s*Sub')
            verdict = Good!
            reason = ''
            -- Check for wildcard
            for _, host in ipairs(validhosts)
              if host\match '^*.'
                -- replace the wildcard with a valid lua pattern
                host = host\gsub '^%*', '%w'
            foundValidHost = false
            for validhost in *validhosts
              -- use match to match wildcards
              if host\match(validhost)
                foundValidHost = true
                break
            if not foundValidHost
              verdict = Bad!
              reason = "Hostname #{host} not in list #{table.concat validhosts, ', '}"
            if after < os.time!
              verdict = Bad!
              reason ..= " #{red 'Expired'} cert!"

            say "#{verdict} #{reason} #{ip}:#{port}. Expires #{expires} Cipher: #{cipher.bits} bits #{cipher.name}."
          else
            reply "Error #{err} when negotiating TLS #{ip}:#{port} : #{err}"
          found = true
        if found
          break
    '^%pconnect (.*)[:%s]([0-9]+)$': (source, destination, host, port) =>
      port = tonumber port
      if port < 0 or port > 65535
        return reply "Think you're clever?"
      hosts = Query(host)
      if #hosts == 0
        -- assume IP
        hosts = {host}
      for ip in *hosts
        sock = socket.connect(ip, port)
        ok, sock, err = pcall -> sock\connect 5
        if not ok
          return say "Error connecting to #{ip}:#{port} : #{sock}"
        if ok and sock
          sock\close!
        if ip\match':'
          ip = '[' .. ip .. ']'
        if not sock
          reply "Error #{err} connecting to #{ip}:#{port} : #{errno.strerror err}"
        else
          say "Yup, could connect to #{ip}:#{port}"
          break
    '^%pdns (.*)$': (source, destination, arg) =>
      args = split arg, ' '
      argc = #args
      host = args[1]
      types = 'AAAA,A'
      if argc == 2
        types = args[1]
        host = args[2]

      out = Query(host, types)

      if #out == 0
        say "No answers"
      say table.concat(out, ' ')

    '^%pipv6 stats$': (source, destination) =>
      @Send "WHO #{destination}"
    '^%pipv6 stats (.*)$': (source, destination, dest) =>
      @Send "WHO #{dest}"
    '^%phttp (.*)$': (source, destination, arg) =>
      args = split arg, ' '
      argc = #args
      url = args[1]
      field = ''
      if argc == 2
        field = args[1]
        url = args[2]
      unless url\match '^http'
        url = 'http://' .. url
      data, uri, response = simplehttp(url)
      unless data
        reply 'Error: '..tostring(uri)
      unless response
        reply data..uri
      status = response.status_code
      headers = {}
      if argc == 1
        for k, v in pairs response.headers
          headers[#headers+1] = "#{k}:#{v}"
        table.sort(headers)
        headers = table.concat(headers, ', ')
        say "HTTP v#{response.version} #{headers}"
      elseif argc == 2
        field = field\lower!
        value = response.headers[field]
        if field == 'version'
          value = response.version

        say "#{field} : #{value or 'Header not found'}"
    '^%pwhereis (.*)$': (source, destination, arg) =>
      -- Download from http://dev.maxmind.com/geoip/geoip2/geolite2/
      geodb = (require "mmdb").open("GeoLite2-City.mmdb")
      -- if space, use first
      if arg\match'(.-) .*'
        arg = arg\match'(.-) .*'
      p = (obj) ->
        out = {}
        if obj.city and obj.city.names and obj.city.names.en
          out[#out+1] = obj.city.names.en
        if obj.subdivisions and obj.subdivisions[1] and obj.subdivisions[1].names and obj.subdivisions[1].names.en
          out[#out+1] = obj.subdivisions[1].names.en
        if obj.country and obj.country.names and obj.country.names.en
          out[#out+1] = obj.country.names.en
        say table.concat(out, ', ')
      if arg\match '%d+%.%d+%.%d+%.%d+'
        obj = geodb\search_ipv4 arg
        p obj
      elseif arg\match ':'
        obj = geodb\search_ipv6 arg
        p obj
      else
        reply 'Needs valid IPv4'

  }
  ['352']: {
    (source, destination, input) =>
      --:efnet.port80.se 352 xt #jensaskaret xt 2a02:cc41:100f::1 efnet.port80.se xt H :0 Tor H.
      unless cache[destination]
        cache[destination] = {}

      nick = input.nick
      host = input.host
      --if @channels[destination]
      cache[destination][nick] = host

  }
  ['315']: {
    (source, destination, input) =>
      -- nondeterministic order.. hey ho here we go
      -- TODO use condition?
      cqueues.sleep(2)
      ipv4 = 0
      ipv6 = 0
      cloak = 0
      for nick, host in pairs(cache[destination])
        if host\match ':'
          ipv6 += 1
        elseif host\match '/' -- freenode cloak
          cloak += 1
        elseif host\match '%d+%.%d+%.%d+%.%d+'
          ipv4 += 1
        else
          timeout = 360
          ok, packet, err = pcall(dns.query, host, 'AAAA', 'IN', timeout)
          if not ok
            print 'pack', ok, packet, err
            print(debug.traceback())
          elseif not packet
            print 'no packet for host', host
            ipv4 += 1
          elseif packet\count() >= 2 -- 1 for question and atleast 1 for answer
            ipv6 = ipv6 + 1
          else
            ipv4 = ipv4 + 1
      -- Empty cache
      cache[destination] = nil
      percent = math.floor(100*ipv6/(ipv4+ipv6+cloak))
      out = "IP stats: #{bold percent}% IPv6. #{ipv6} IPv6 clients. #{ipv4} IPv4 clients. "
      if cloak > 0
        out = out .. "#{cloak} cloaked hosts."
      say out
  }
}

cqueues = require 'cqueues'
dns = require'cqueues.dns'
packet = require'cqueues.dns.packet'
record = require'cqueues.dns.record'
errno = require'cqueues.errno'
socket = require'cqueues.socket'
context = require'openssl.ssl.context'
http_tls = require'http.tls'
pkey = require'openssl.pkey'
auxlib = require"openssl.auxlib"
util = require'util'
{:simplehttp, :bold, :split, :green, :red, :simplehttp, :json, :urlEncode} = require'util'

cache = {}

HasValue = (table, value) ->
  if(type(table) ~= 'table') then return
  for _, v in next, table
    if(v == value) then return true

Good = ->
  "#{green '🔒'} TLS #{green 'ok'}"

Bad = ->
  "#{red '🔓 '} #{red 'Bad'} TLS"

Query = (host, types='AAAA,A', timeout=15) ->
  out = {}

  for type in *split(types, ',')
    answer = dns.query(host, type, 'IN', timeout)

    for rec in answer\grep{section:packet.section.ANSWER}
      out[#out+1] = tostring(rec)

  return out

{
  PRIVMSG: {
    '^%pttls (.*)$': (source, destination, arg) =>
      host = arg\match'^%s*(.-)[:%s].*$' or arg
      port = arg\match'^.*[: ](.*)%s*$' or 443
      port = tonumber(port) or 0
      if port <= 0 or port > 65535
        return reply "Think you're clever?"
      hosts = Query(host)
      if #hosts == 0
        -- assume IP
        hosts = {host}
      found = false
      for ip in *hosts
        sock = socket.connect{host:ip, :port, verify:false, tls_sendname:host}
        ok, sock, err = pcall -> sock\connect 5
        if not ok
          return say "Error connecting to #{ip}:#{port} : #{sock}"
        if ip\match':'
          ip = '[' .. ip .. ']'
        if not sock
          reply "Error #{err} connecting to #{ip}:#{port} : #{errno.strerror err}"
        else
          ctx = http_tls.new_client_context()
          --ctx = context.new('SSL3', false)
          --ctx\setOptions(context.OP_NO_COMPRESSION+contextopenssl_ctx.OP_SINGLE_ECDH_USE)
          --ctx\setEphemeralKey(pkey.new{ type = "EC", curve = "prime256v1" }
          ok, tls, err = pcall -> sock\starttls 5
          if not ok
            return say "TLS negotiation error : #{tls}"
          if tls
            ssl = tls\checktls!
            if sock
              sock\close!
            cipher = ssl\getCipherInfo!
            if http_tls.banned_ciphers[cipher.name]
              return say "Banned cipher: #{cipher.name} when negotiating TLS #{ip}:#{port}"
            cert = ssl\getPeerCertificate!
            before, after = cert\getLifetime!
            cn = tostring cert\getSubject!
            san = cert\getSubjectAlt!
            validhosts = {cn\match'CN=(.*)'}
            for type, sanhost in auxlib.pairs(san)
              table.insert(validhosts, sanhost)
            -- cheat and parse text
            text = cert\text!
            @Log 'debug', text
            expires = text\match('Not After : (.-)%s*Sub')
            verdict = Good!
            reason = ''
            unless HasValue(validhosts, host)
              verdict = Bad!
              reason = "Hostname #{host} not in list #{table.concat validhosts, ', '}"
            if after < os.time!
              verdict = Bad!
              reason ..= " #{red 'Expired'} cert!"

            say "#{verdict} #{reason} #{ip}:#{port}. Expires #{expires} Cipher: #{cipher.bits} bits #{cipher.name}."
          else
            reply "Error #{err} when negotiating TLS #{ip}:#{port} : #{err}"
          found = true
        if found
          break
    '^%pconnect (.*)[:%s]([0-9]+)$': (source, destination, host, port) =>
      port = tonumber port
      if port < 0 or port > 65535
        return reply "Think you're clever?"
      hosts = Query(host)
      if #hosts == 0
        -- assume IP
        hosts = {host}
      for ip in *hosts
        sock = socket.connect(ip, port)
        ok, sock, err = pcall -> sock\connect 5
        if not ok
          return say "Error connecting to #{ip}:#{port} : #{sock}"
        if ok and sock
          sock\close!
        if ip\match':'
          ip = '[' .. ip .. ']'
        if not sock
          reply "Error #{err} connecting to #{ip}:#{port} : #{errno.strerror err}"
        else
          say "Yup, could connect to #{ip}:#{port}"
          break
    '^%pdns (.*)$': (source, destination, arg) =>
      args = split arg, ' '
      argc = #args
      host = args[1]
      types = 'AAAA,A'
      if argc == 2
        types = args[1]
        host = args[2]

      out = Query(host, types)

      if #out == 0
        say "No answers"
      say table.concat(out, ' ')

    '^%pipv6 stats$': (source, destination) =>
      @Send "WHO #{destination}"
    '^%pipv6 stats (.*)$': (source, destination, dest) =>
      @Send "WHO #{dest}"
    '^%phttp (.*)$': (source, destination, arg) =>
      args = split arg, ' '
      argc = #args
      url = args[1]
      field = ''
      if argc == 2
        field = args[1]
        url = args[2]
      unless url\match '^http'
        url = 'http://' .. url
      data, uri, response = simplehttp(url)
      unless data
        reply 'Error: '..tostring(uri)
      unless response
        reply data..uri
      status = response.status_code
      headers = {}
      if argc == 1
        for k, v in pairs response.headers
          headers[#headers+1] = "#{k}:#{v}"
        table.sort(headers)
        headers = table.concat(headers, ', ')
        say "HTTP v#{response.version} #{headers}"
      elseif argc == 2
        field = field\lower!
        value = response.headers[field]
        if field == 'version'
          value = response.version

        say "#{field} : #{value or 'Header not found'}"
    '^%pwhereis (.*)$': (source, destination, arg) =>
      -- Download from http://dev.maxmind.com/geoip/geoip2/geolite2/
      geodb = (require "mmdb").open("GeoLite2-City.mmdb")
      -- if space, use first
      if arg\match'(.-) .*'
        arg = arg\match'(.-) .*'
      p = (obj) ->
        out = {}
        if obj.city and obj.city.names and obj.city.names.en
          out[#out+1] = obj.city.names.en
        if obj.subdivisions and obj.subdivisions[1] and obj.subdivisions[1].names and obj.subdivisions[1].names.en
          out[#out+1] = obj.subdivisions[1].names.en
        if obj.country and obj.country.names and obj.country.names.en
          out[#out+1] = obj.country.names.en
        say table.concat(out, ', ')
      if arg\match '%d+%.%d+%.%d+%.%d+'
        obj = geodb\search_ipv4 arg
        p obj
      elseif arg\match ':'
        obj = geodb\search_ipv6 arg
        p obj
      else
        reply 'Needs valid IPv4'

  }
  ['352']: {
    (source, destination, input) =>
      --:efnet.port80.se 352 xt #jensaskaret xt 2a02:cc41:100f::1 efnet.port80.se xt H :0 Tor H.
      unless cache[destination]
        cache[destination] = {}

      nick = input.nick
      host = input.host
      --if @channels[destination]
      cache[destination][nick] = host

  }
  ['315']: {
    (source, destination, input) =>
      -- nondeterministic order.. hey ho here we go
      -- TODO use condition?
      cqueues.sleep(2)
      ipv4 = 0
      ipv6 = 0
      cloak = 0
      for nick, host in pairs(cache[destination])
        if host\match ':'
          ipv6 += 1
        elseif host\match '/' -- freenode cloak
          cloak += 1
        elseif host\match '%d+%.%d+%.%d+%.%d+'
          ipv4 += 1
        else
          timeout = 360
          ok, packet, err = pcall(dns.query, host, 'AAAA', 'IN', timeout)
          if not ok
            print 'pack', ok, packet, err
            print(debug.traceback())
          elseif not packet
            print 'no packet for host', host
            ipv4 += 1
          elseif packet\count() >= 2 -- 1 for question and atleast 1 for answer
            ipv6 = ipv6 + 1
          else
            ipv4 = ipv4 + 1
      -- Empty cache
      cache[destination] = nil
      percent = math.floor(100*ipv6/(ipv4+ipv6+cloak))
      out = "IP stats: #{bold percent}% IPv6. #{ipv6} IPv6 clients. #{ipv4} IPv4 clients. "
      if cloak > 0
        out = out .. "#{cloak} cloaked hosts."
      say out
  }
}


benis: Fix trailing whitespace.

benis: Fix trailing whitespace.

Former-commit-id: 07258610fee5ab002cf89b8425769b277c54b100 [formerly 9bb9bc38dc753944cb02359dccaf4b368cfaf085]
Former-commit-id: 90fb7f86e5054fca805cfb832d4d73a085613550

-- code from q66's module from luabot ported from rfs benis module
reduce = (fun, list, def) ->
  for k, v in ipairs(list)
    def = fun(def, v)
  return def

rand = math.random
tconc = table.concat

benisify = (s) ->
  reduce (acc, f) ->
    f(acc),
    {
      (s) -> s\lower()
      (s) -> s\gsub('x', 'cks')
      (s) -> s\gsub('ing', 'in')
      (s) -> s\gsub('you', 'u')
      (s) -> s\gsub 'oo', ->
        ('u')\rep(rand(1, 5))
      (s) ->
        s\gsub '[%w_]%z', (x) ->
          x\sub(1, 1)\rep(rand(1, 2))
      (s) ->
        s\gsub 'ck', ->
          'g'\rep(rand(1, 5))
      (s) ->
        s\gsub '(t+)%f[aeiouys ]', (x) ->
          ('d')\rep(#x)
      (s) ->
        s\gsub '(t+)$', (x) ->
          ('d')\rep(#x)
      (s) -> s\gsub('p', 'b'),
      (s) -> s\gsub('%f[%w_]the%f[^%w_]', 'da'),
      (s) -> s\gsub('%f[%w_]c', 'g'),
      (s) -> s\gsub('%f[%w_]is%f[^%w_]', 'are'),
      (s) -> s\gsub '(c+)(.)', (x, y) ->
        (y == 'e' or y == 'i' or y == 'y') and (x .. y) or ('g')\rep(rand(1, 5)) ..  (y == 'c' and 'g' or y)
      (s) ->
        s\gsub '([qk]+)%f[aeiouy ]', ->
          ('g')\rep(rand(1, 5))
      (s) ->
        s\gsub '([qk]+)$', ->
          ('g')\rep(rand(1, 5))
      (s) ->
          endswith = s\find '[?!.]$'
          repf = (x) ->
              ret = x\rep(rand(2, 5)) .. ' '
              for i = 1, rand(2, 5)
                  ret = ret .. (':')\rep(rand(1, 2)) .. ('D')\rep(rand(1, 4))
              return ret
          unless endswith then return s\gsub('$', repf)
          s\gsub('[?!.]+', repf)
    }, s

PRIVMSG:
  '^%pbenis (.*)$':(source, destination, arg) =>
    say(benisify(arg))


pure upgrade

pure upgrade

class FaviconMixin
  userManagementMixinMenu: (classes={})=>
    cli=(name)->
      @req.parsed_url.path==@url_for("lazuli_modules_usermanagement_"..name) and classes.active_li or ""
    stateclass=@lazuli_modules_usermanagement_currentuser and "logged_in " or "logged_out "
    ul class: "lazuli module user_management mixin_menu "..stateclass..(classes.ul or ""), ->
      if @lazuli_modules_usermanagement_currentuser
        li class: "logout "..(classes.li or "")..cli"logout", ->
          a href:@url_for("lazuli_modules_usermanagement_logout"),class:(classes.a or ""), "Logout"
      else
        li class: "login "..(classes.li or "")..cli"login", ->
          a href:@url_for("lazuli_modules_usermanagement_login"),class:(classes.a or ""), "Login"
        li class: "register "..(classes.li or "")..cli"register", ->
          a href:@url_for("lazuli_modules_usermanagement_register"),class:(classes.a or ""), "Register"

class FaviconMixin
  userManagementMixinMenu: (classes={})=>
    stateclass=@lazuli_modules_usermanagement_currentuser and "logged_in " or "logged_out "
    ul class: "lazuli module user_management mixin_menu "..stateclass..(classes.ul or ""), ->
      if @lazuli_modules_usermanagement_currentuser
        li class: "logout "..(classes.li or ""), ->
          a href:@url_for("lazuli_modules_usermanagement_logout"),class:(classes.a or ""), "Logout"
      else
        li class: "login "..(classes.li or ""), ->
          a href:@url_for("lazuli_modules_usermanagement_login"),class:(classes.a or ""), "Login"
        li class: "register "..(classes.li or ""), ->
          a href:@url_for("lazuli_modules_usermanagement_register"),class:(classes.a or ""), "Register"