Background Сегодня в 07:13 I know that people like slinging them about when people die sometimes

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

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

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

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

DLib.nw.PoolBoolean('PPM2.InEditor', false)

do
	import GetModel, IsDormant, GetPonyData, IsValid from FindMetaTable('Entity')
	callback = ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply)
				model = GetModel(ply)
				ply.__ppm2_lastmodel = ply.__ppm2_lastmodel or model
				if ply.__ppm2_lastmodel ~= model
					data = GetPonyData(ply)
					if data and data.ModelChanges
						oldModel = ply.__ppm2_lastmodel
						ply.__ppm2_lastmodel = model
						data\ModelChanges(oldModel, model)

		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ply = task.ent
			if IsValid(ply) and not IsDormant(ply)
				model = GetModel(ply)
				ply.__ppm2_lastmodel = ply.__ppm2_lastmodel or model
				if ply.__ppm2_lastmodel ~= model
					data = GetPonyData(ply)
					if data and data.ModelChanges
						oldModel = ply.__ppm2_lastmodel
						ply.__ppm2_lastmodel = model
						data\ModelChanges(oldModel, model)
	timer.Create 'PPM2.ModelWatchdog', 1, 0, ->
		status, err = pcall callback
		print('PPM2 Error: ' .. err) if not status

do
	import GetModel, IsDormant, GetPonyData, IsValid, IsPonyCached from FindMetaTable('Entity')

	hook.Add 'Think', 'PPM2.PonyDataThink', ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply) and IsPonyCached(ply)
				data = GetPonyData(ply)
				data\Think() if data and data.Think
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ply = task.ent
			if IsValid(ply) and not IsDormant(ply) and IsPonyCached(ply) and task.Think
				task\Think()

	hook.Add 'RenderScreenspaceEffects', 'PPM2.PonyDataRenderScreenspaceEffects', ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply) and IsPonyCached(ply)
				data = GetPonyData(ply)
				data\RenderScreenspaceEffects() if data and data.RenderScreenspaceEffects
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ply = task.ent
			if IsValid(ply) and not IsDormant(ply) and IsPonyCached(ply) and task.RenderScreenspaceEffects
				task\RenderScreenspaceEffects()

do
	catchError = (err) ->
		PPM2.Message 'Slow Update Error: ', err
		PPM2.Message debug.traceback()

	import Alive from FindMetaTable('Player')
	import IsPonyCached, IsDormant, GetPonyData from FindMetaTable('Entity')
	timer.Create 'PPM2.SlowUpdate', CLIENT and 0.5 or 5, 0, ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply) and Alive(ply) and IsPonyCached(ply) and GetPonyData(ply)
				data = GetPonyData(ply)
				xpcall(data.SlowUpdate, catchError, data, CLIENT) if data.SlowUpdate
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			if IsValid(task.ent) and task.ent\IsPony()
				xpcall(task.SlowUpdate, catchError, task, CLIENT) if task.SlowUpdate

ENABLE_TOOLGUN = CreateConVar('ppm2_sv_ragdoll_toolgun', '0', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow toolgun usage on player death ragdolls')
ENABLE_PHYSGUN = CreateConVar('ppm2_sv_ragdoll_physgun', '1', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow physgun usage on player death ragdolls')

hook.Add 'CanTool', 'PPM2.DeathRagdoll', (ply = NULL, tr = {Entity: NULL}, tool = '') -> false if IsValid(tr.Entity) and tr.Entity\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_TOOLGUN\GetBool() and tool ~= 'rope'
hook.Add 'PhysgunPickup', 'PPM2.DeathRagdoll', (ply = NULL, ent = NULL) -> false if IsValid(ent) and ent\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_PHYSGUN\GetBool()
hook.Add 'CanProperty', 'PPM2.DeathRagdoll', (ply = NULL, mode = '', ent = NULL) -> false if IsValid(ent) and ent\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_TOOLGUN\GetBool()
hook.Add 'CanDrive', 'PPM2.DeathRagdoll', (ply = NULL, ent = NULL) -> false if IsValid(ent) and ent\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_TOOLGUN\GetBool()

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

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

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

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

DLib.nw.PoolBoolean('PPM2.InEditor', false)

do
	import GetModel, IsDormant, GetPonyData, IsValid from FindMetaTable('Entity')
	callback = ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply)
				model = GetModel(ply)
				ply.__ppm2_lastmodel = ply.__ppm2_lastmodel or model
				if ply.__ppm2_lastmodel ~= model
					data = GetPonyData(ply)
					if data and data.ModelChanges
						oldModel = ply.__ppm2_lastmodel
						ply.__ppm2_lastmodel = model
						data\ModelChanges(oldModel, model)

		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ply = task.ent
			if IsValid(ply) and not IsDormant(ply)
				model = GetModel(ply)
				ply.__ppm2_lastmodel = ply.__ppm2_lastmodel or model
				if ply.__ppm2_lastmodel ~= model
					data = GetPonyData(ply)
					if data and data.ModelChanges
						oldModel = ply.__ppm2_lastmodel
						ply.__ppm2_lastmodel = model
						data\ModelChanges(oldModel, model)
	timer.Create 'PPM2.ModelWatchdog', 1, 0, ->
		status, err = pcall callback
		print('PPM2 Error: ' .. err) if not status

do
	import GetModel, IsDormant, GetPonyData, IsValid, IsPonyCached from FindMetaTable('Entity')

	hook.Add 'Think', 'PPM2.PonyDataThink', ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply) and IsPonyCached(ply)
				data = GetPonyData(ply)
				data\Think() if data and data.Think
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ply = task.ent
			if IsValid(ply) and not IsDormant(ply) and IsPonyCached(ply) and task.Think
				task\Think()

	hook.Add 'RenderScreenspaceEffects', 'PPM2.PonyDataRenderScreenspaceEffects', ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply) and IsPonyCached(ply)
				data = GetPonyData(ply)
				data\RenderScreenspaceEffects() if data and data.RenderScreenspaceEffects
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			ply = task.ent
			if IsValid(ply) and not IsDormant(ply) and IsPonyCached(ply) and task.RenderScreenspaceEffects
				task\RenderScreenspaceEffects()

do
	catchError = (err) ->
		PPM2.Message 'Slow Update Error: ', err
		PPM2.Message debug.traceback()

	import Alive from FindMetaTable('Player')
	import IsPonyCached, IsDormant, GetPonyData from FindMetaTable('Entity')
	timer.Create 'PPM2.SlowUpdate', CLIENT and 0.5 or 5, 0, ->
		for _, ply in ipairs player.GetAll()
			if not IsDormant(ply) and Alive(ply) and IsPonyCached(ply) and GetPonyData(ply)
				data = GetPonyData(ply)
				xpcall(data.SlowUpdate, catchError, data, CLIENT) if data.SlowUpdate
		for _, task in ipairs PPM2.NetworkedPonyData.RenderTasks
			if IsValid(task.ent) and task.ent\IsPony()
				xpcall(task.SlowUpdate, catchError, task, CLIENT) if task.SlowUpdate

ENABLE_TOOLGUN = CreateConVar('ppm2_sv_ragdoll_toolgun', '0', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow toolgun usage on player death ragdolls')
ENABLE_PHYSGUN = CreateConVar('ppm2_sv_ragdoll_physgun', '1', {FCVAR_REPLICATED, FCVAR_NOTIFY}, 'Allow physgun usage on player death ragdolls')

hook.Add 'CanTool', 'PPM2.DeathRagdoll', (ply = NULL, tr = {Entity: NULL}, tool = '') -> false if IsValid(tr.Entity) and tr.Entity\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_TOOLGUN\GetBool()
hook.Add 'PhysgunPickup', 'PPM2.DeathRagdoll', (ply = NULL, ent = NULL) -> false if IsValid(ent) and ent\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_PHYSGUN\GetBool()
hook.Add 'CanProperty', 'PPM2.DeathRagdoll', (ply = NULL, mode = '', ent = NULL) -> false if IsValid(ent) and ent\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_TOOLGUN\GetBool()
hook.Add 'CanDrive', 'PPM2.DeathRagdoll', (ply = NULL, ent = NULL) -> false if IsValid(ent) and ent\GetNWBool('PPM2.IsDeathRagdoll') and not ENABLE_TOOLGUN\GetBool()


Improved old data import

Improved old data import

--
-- 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 PonyDataInstance
    @DATA_DIR = "ppm2/"
    @DATA_DIR_BACKUP = "ppm2/backups/"

    @FindFiles = =>
        output = [str\sub(1, #str - 4) for str in *file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.txt')]
        return output

    @FindInstances = =>
        output = [@(str\sub(1, #str - 4)) for str in *file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.txt')]
        return output

    @PONY_DATA = PPM2.PonyDataRegistry

    @PONY_DATA_MAPPING = {getFunc\lower(), key for key, {:getFunc} in pairs @PONY_DATA}
    @PONY_DATA_MAPPING[key] = key for key, value in pairs @PONY_DATA

    for key, data in pairs @PONY_DATA
        continue unless data.enum
        data.enum = [arg\upper() for arg in *data.enum]
        data.enumMapping = {}
        data.enumMappingBackward = {}
        i = -1
        for enumVal in *data.enum
            i += 1
            data.enumMapping[i] = enumVal
            data.enumMappingBackward[enumVal] = i
    for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max} in pairs @PONY_DATA
        @__base["Get#{getFunc}"] = => @dataTable[key]
        @__base["GetMin#{getFunc}"] = => min if min
        @__base["GetMax#{getFunc}"] = => max if max
        @__base["Enum#{getFunc}"] = => enum if enum
        @__base["Get#{getFunc}Types"] = => enum if enum

        @["GetMin#{getFunc}"] = => min if min
        @["GetMax#{getFunc}"] = => max if max
        @["GetEnum#{getFunc}"] = => enum if enum
        @["Enum#{getFunc}"] = => enum if enum

        if enumMapping
            @__base["Get#{getFunc}Enum"] = => enumMapping[@dataTable[key]] or enumMapping[0] or @dataTable[key]
            @__base["GetEnum#{getFunc}"] = @__base["Get#{getFunc}Enum"]
		@__base["Set#{getFunc}"] = (val = defValue, ...) =>
            if type(val) == 'string' and enumMappingBackward
                newVal = enumMappingBackward[val\upper()]
                val = newVal if newVal
            newVal = fix(val)
			oldVal = @dataTable[key]
			@dataTable[key] = newVal
            @ValueChanges(key, oldVal, newVal, ...)

    WriteNetworkData: =>
        for {:strName, :writeFunc, :getName, :defValue} in *PPM2.NetworkedPonyData.NW_Vars
            if @["Get#{getName}"]
                writeFunc(@["Get#{getName}"](@))
            else
                writeFunc(defValue)

    Copy: (fileName = @filename) =>
        copyOfData = {}
        for key, val in pairs @dataTable
            switch type(val)
                when 'number'
                    copyOfData[key] = val
                when 'string'
                    copyOfData[key] = val
                when 'boolean'
                    copyOfData[key] = val
                when 'table'
                    {:r, :g, :b} = val
                    if r and g and b
                        copyOfData[key] = Color(r, g, b)
        newData = @@(fileName, copyOfData, false)
        return newData
    CreateCustomNetworkObject: (ply = LocalPlayer(), ...) =>
        newData = PPM2.NetworkedPonyData(nil, ply)
        newData\SetEntity(ply)
        @ApplyDataToObject(newData, ...)
        return newData
    CreateNetworkObject: (gointToNetwork = true, ...) =>
        newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
        newData\SetIsGoingToNetwork(gointToNetwork)
        newData\SetEntity(LocalPlayer())
        @ApplyDataToObject(newData, ...)
        return newData
    ApplyDataToObject: (target, ...) =>
        for key, value in pairs @GetAsNetworked()
            error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
            target["Set#{key}"](target, value, ...)
    UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
    CreateController: (...) => @CreateNetworkObject(false, ...)
    CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

    new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
        @SetFilename(filename)
        @updateNWObject = true
        @networkNWObject = true
        @valid = @isOpen
        @rawData = data
        @dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
        @saveOnChange = true
        if data
            @SetupData(data, true)
        elseif @exists and readIfExists
            @ReadFromDisk(force, doBackup)

    @ERR_MISSING_PARAMETER = 4
    @ERR_MISSING_CONTENT = 5

    GetSaveOnChange: => @saveOnChange
    SaveOnChange: => @saveOnChange
    SetSaveOnChange: (val = true) => @saveOnChange = val
    SetupData: (data, force = false, doBackup = false) =>
        if doBackup or not force
            makeBackup = false
            for key, value in pairs data
                key = key\lower()
                map = @@PONY_DATA_MAPPING[key]
                if not map
                    return @@ERR_MISSING_PARAMETER if not force
                    makeBackup = true
                    break
                mapData = @@PONY_DATA[map]
                if mapData.enum
                    if type(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or type(value) == 'number' and not mapData.enumMapping[value]
                        return @@ERR_MISSING_CONTENT if not force
                        makeBackup = true
                        break
            if doBackup and makeBackup and @exists
                bkName = "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.txt"
                fRead = file.Read(@fpath, 'DATA')
                file.Write(bkName, fRead)

        for key, value in pairs data
            key = key\lower()
            map = @@PONY_DATA_MAPPING[key]
            continue unless map
            mapData = @@PONY_DATA[map]
            if mapData.enum and type(value) == 'string'
                @dataTable[key] = mapData.fix(mapData.enumMappingBackward[value\upper()])
            else
                @dataTable[key] = mapData.fix(value)
    ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
        if @nwObj and @updateNWObject
            {:getFunc} = @@PONY_DATA[key]
            @nwObj["Set#{getFunc}"](@nwObj, newVal, @networkNWObject)
        @Save() if saveNow
    SetFilename: (filename) =>
        @filename = filename
        @filenameFull = "#{filename}.txt"
        @fpath = "#{@@DATA_DIR}#{filename}.txt"
        @fpathBackup = "#{@@DATA_DIR}#{filename}.bak.txt"
        @fpathFull = "data/#{@@DATA_DIR}#{filename}.txt"
        @isOpen = @filename ~= nil
        @exists = file.Exists(@fpath, 'DATA')
        return @exists
    SetNetworkData: (nwObj) => @nwObj = nwObj
    SetPonyData: (nwObj) => @nwObj = nwObj
    SetPonyDataController: (nwObj) => @nwObj = nwObj
    SetPonyController: (nwObj) => @nwObj = nwObj
    SetController: (nwObj) => @nwObj = nwObj
    SetDataController: (nwObj) => @nwObj = nwObj

    SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
    SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal

    GetNetworkOnChange: => @networkNWObject
    GetUpdateOnChange: => @updateNWObject

    GetNetworkData: => @nwObj
    GetPonyData: => @nwObj
    GetPonyDataController: => @nwObj
    GetPonyController: => @nwObj
    GetController: => @nwObj
    GetDataController: => @nwObj

    IsValid: => @valid
    Exists: => @exists
    FileExists: => @exists
    IsExists: => @exists
    GetFileName: => @filename
    GetFilename: => @filename
    GetFileNameFull: => @filenameFull
    GetFilenameFull: => @filenameFull
    GetFilePath: => @fpath
    GetFullFilePath: => @fpathFull
    SerealizeValue: (valID = '') =>
        map = @@PONY_DATA[valID]
        return unless map
        val = @dataTable[valID]
        if map.enum
            return map.enumMapping[val] or map.enumMapping[map.default()]
        elseif map.serealize
            return map.serealize(val)
        else
            return val
    Serealize: (prettyPrint = true) =>
        serTab = {key, @SerealizeValue(key) for key, val in pairs @dataTable}
        util.TableToJSON(serTab, prettyPrint)
    GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

    @READ_SUCCESS = 0
    @ERR_FILE_NOT_EXISTS = 1
    @ERR_FILE_EMPTY = 2
    @ERR_FILE_CORRUPT = 3
    ReadFromDisk: (force = false, doBackup = true) =>
        return @@ERR_FILE_NOT_EXISTS unless @exists
        fRead = file.Read(@fpath, 'DATA')
        return @@ERR_FILE_EMPTY if not fRead or fRead == ''
        readTable = util.JSONToTable(fRead)
        return @@ERR_FILE_CORRUPT unless readTable
        return @SetupData(readTable, force, doBackup) or @@READ_SUCCESS
    SaveAs: (path = @fpath) =>
        fCreate = @Serealize()
        file.Write(path, fCreate)
    Save: (doBackup = true) =>
        if doBackup and @exists
            fRead = file.Read(@fpath, 'DATA')
            file.Write(@fpathBackup, fRead)
        @SaveAs(@fpath)
        @exists = true

do
    PARSE_VECTOR = (str = '1.0 1.0 1.0', X = 1, Y = 1, Z = 1) ->
        return Vector(X, Y, Z) if str == ''
        x, y, z = str\match('([0-9.]+) ([0-9.]+) ([0-9.]+)')
        return Vector(tonumber(x) or X, tonumber(y) or Y, tonumber(z) or Z)

    PARSE_COLOR = (str = '1.0 1.0 1.0', r = 255, g = 255, b = 255) ->
        return Color(r, g, b) if str == ''
        {x, y, z} = PARSE_VECTOR(str, r / 255, g / 255, b / 255)
        return Color(x * 255, y * 255, z * 255)

    IMPORT_TABLE = {
        'gender': {
            name: 'Gender'
            func: (arg = 0) ->
                num = tonumber(arg)
                return num == 0 and 'MALE' or 'FEMALE'
        }

        'coatcolor': {
            name: 'BodyColor'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_bg': {
            name: 'EyeBackground'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_grad': {
            name: 'EyeIrisBottom'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_iris': {
            name: 'EyeIrisTop'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line1': {
            name: 'EyeIrisLine1'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line2': {
            name: 'EyeIrisLine2'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'haircolor1': {
            name: {'ManeColor1', 'TailColor1', 'ManeColor2', 'TailColor2'}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyejholerssize': {
            name: 'HoleWidth'
            func: (arg = '1') -> tonumber(arg) or 1
        }

        'eyeirissize': {
            name: 'IrisSize'
            func: (arg = '1') -> (tonumber(arg) or 1) * 1.2
        }

        'eyeholesize': {
            name: 'HoleSize'
            func: (arg = '0.8') -> tonumber(arg) or 0.8
        }

        'bodyweight': {
            name: 'Weight'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'mane': {
            name: {'ManeType', 'ManeTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'manel': {
            name: {'ManeTypeLower', 'ManeTypeLowerNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tail': {
            name: {'TailType', 'TailTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tailsize': {
            name: 'TailSize'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'cmark': {
            name: 'CMarkType'
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        'cmark_enabled': {
            name: 'CMark'
            func: (arg = '1') -> arg == '1' or arg == '2'
        }
    }

    for i = 1, 8
        IMPORT_TABLE["bodydetail#{i}"] = {
            name: "BodyDetail#{i}"
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        IMPORT_TABLE["bodydetail#{i}_c"] = {
            name: "BodyDetailColor#{i}"
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    for i = 2, 6
        IMPORT_TABLE["haircolor#{i}"] = {
            name: {"ManeDetailColor#{i - 1}", "TailDetailColor#{i - 1}"}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    PPM2.ReadFromOldData = (filename = '_current') ->
        read = file.Read("ppm/#{filename}.txt", 'DATA')
        return false if read == ''
        split = [str\Trim() for str in *string.Explode('\n', read\Replace('\r', ''))]
        outputData = {}

        for line in *split
            varID = line\match('([a-zA-Z0-9_]+)')
            continue if not varID or varID == ''
            continue if not IMPORT_TABLE[varID]
            dt = IMPORT_TABLE[varID]
            value = line\sub(#varID + 2)
            if type(dt.name) ~= 'table'
                outputData[dt.name] = dt.func(value)
            else
                get = dt.func(value)
                outputData[name] = get for name in *dt.name

        data = PonyDataInstance("#{filename}_imported", nil, false)
        for key, value in pairs outputData
            data["Set#{key}"](data, value, false)
        return data, outputData

PPM2.PonyDataInstance = PonyDataInstance

PPM2.MainDataInstance = nil
PPM2.GetMainData = ->
    if not PPM2.MainDataInstance
        PPM2.MainDataInstance = PonyDataInstance('_current', nil, true, true)
        if not PPM2.MainDataInstance\FileExists()
            PPM2.MainDataInstance\Save()
    return PPM2.MainDataInstance

--
-- 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 PonyDataInstance
    @DATA_DIR = "ppm2/"
    @DATA_DIR_BACKUP = "ppm2/backups/"

    @FindFiles = =>
        output = [str\sub(1, #str - 4) for str in *file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.txt')]
        return output

    @FindInstances = =>
        output = [@(str\sub(1, #str - 4)) for str in *file.Find(@DATA_DIR .. '*', 'DATA') when not str\find('.bak.txt')]
        return output

    @PONY_DATA = PPM2.PonyDataRegistry

    @PONY_DATA_MAPPING = {getFunc\lower(), key for key, {:getFunc} in pairs @PONY_DATA}
    @PONY_DATA_MAPPING[key] = key for key, value in pairs @PONY_DATA

    for key, data in pairs @PONY_DATA
        continue unless data.enum
        data.enum = [arg\upper() for arg in *data.enum]
        data.enumMapping = {}
        data.enumMappingBackward = {}
        i = -1
        for enumVal in *data.enum
            i += 1
            data.enumMapping[i] = enumVal
            data.enumMappingBackward[enumVal] = i
    for key, {:getFunc, :fix, :enumMappingBackward, :enumMapping, :enum, :min, :max} in pairs @PONY_DATA
        @__base["Get#{getFunc}"] = => @dataTable[key]
        @__base["GetMin#{getFunc}"] = => min if min
        @__base["GetMax#{getFunc}"] = => max if max
        @__base["Enum#{getFunc}"] = => enum if enum
        @__base["Get#{getFunc}Types"] = => enum if enum

        @["GetMin#{getFunc}"] = => min if min
        @["GetMax#{getFunc}"] = => max if max
        @["GetEnum#{getFunc}"] = => enum if enum
        @["Enum#{getFunc}"] = => enum if enum

        if enumMapping
            @__base["Get#{getFunc}Enum"] = => enumMapping[@dataTable[key]] or enumMapping[0] or @dataTable[key]
            @__base["GetEnum#{getFunc}"] = @__base["Get#{getFunc}Enum"]
		@__base["Set#{getFunc}"] = (val = defValue, ...) =>
            if type(val) == 'string' and enumMappingBackward
                newVal = enumMappingBackward[val\upper()]
                val = newVal if newVal
            newVal = fix(val)
			oldVal = @dataTable[key]
			@dataTable[key] = newVal
            @ValueChanges(key, oldVal, newVal, ...)

    WriteNetworkData: =>
        for {:strName, :writeFunc, :getName, :defValue} in *PPM2.NetworkedPonyData.NW_Vars
            if @["Get#{getName}"]
                writeFunc(@["Get#{getName}"](@))
            else
                writeFunc(defValue)

    Copy: (fileName = @filename) =>
        copyOfData = {}
        for key, val in pairs @dataTable
            switch type(val)
                when 'number'
                    copyOfData[key] = val
                when 'string'
                    copyOfData[key] = val
                when 'boolean'
                    copyOfData[key] = val
                when 'table'
                    {:r, :g, :b} = val
                    if r and g and b
                        copyOfData[key] = Color(r, g, b)
        newData = @@(fileName, copyOfData, false)
        return newData
    CreateCustomNetworkObject: (ply = LocalPlayer(), ...) =>
        newData = PPM2.NetworkedPonyData(nil, ply)
        newData\SetEntity(ply)
        @ApplyDataToObject(newData, ...)
        return newData
    CreateNetworkObject: (gointToNetwork = true, ...) =>
        newData = PPM2.NetworkedPonyData(nil, LocalPlayer())
        newData\SetIsGoingToNetwork(gointToNetwork)
        newData\SetEntity(LocalPlayer())
        @ApplyDataToObject(newData, ...)
        return newData
    ApplyDataToObject: (target, ...) =>
        for key, value in pairs @GetAsNetworked()
            error("Attempt to apply data to object #{target} at unknown index #{key}!") if not target["Set#{key}"]
            target["Set#{key}"](target, value, ...)
    UpdateController: (...) => @ApplyDataToObject(@nwObj, ...)
    CreateController: (...) => @CreateNetworkObject(false, ...)
    CreateCustomController: (...) => @CreateCustomNetworkObject(false, ...)

    new: (filename, data, readIfExists = true, force = false, doBackup = true) =>
        @SetFilename(filename)
        @updateNWObject = true
        @networkNWObject = true
        @valid = @isOpen
        @rawData = data
        @dataTable = {k, default() for k, {:default} in pairs @@PONY_DATA}
        @saveOnChange = true
        if data
            @SetupData(data, true)
        elseif @exists and readIfExists
            @ReadFromDisk(force, doBackup)

    @ERR_MISSING_PARAMETER = 4
    @ERR_MISSING_CONTENT = 5

    GetSaveOnChange: => @saveOnChange
    SaveOnChange: => @saveOnChange
    SetSaveOnChange: (val = true) => @saveOnChange = val
    SetupData: (data, force = false, doBackup = false) =>
        if doBackup or not force
            makeBackup = false
            for key, value in pairs data
                key = key\lower()
                map = @@PONY_DATA_MAPPING[key]
                if not map
                    return @@ERR_MISSING_PARAMETER if not force
                    makeBackup = true
                    break
                mapData = @@PONY_DATA[map]
                if mapData.enum
                    if type(value) == 'string' and not mapData.enumMappingBackward[value\upper()] or type(value) == 'number' and not mapData.enumMapping[value]
                        return @@ERR_MISSING_CONTENT if not force
                        makeBackup = true
                        break
            if doBackup and makeBackup and @exists
                bkName = "#{@@DATA_DIR_BACKUP}#{@filename}_bak_#{os.date('%S_%M_%H-%d_%m_%Y', os.time())}.txt"
                fRead = file.Read(@fpath, 'DATA')
                file.Write(bkName, fRead)

        for key, value in pairs data
            key = key\lower()
            map = @@PONY_DATA_MAPPING[key]
            continue unless map
            mapData = @@PONY_DATA[map]
            if mapData.enum and type(value) == 'string'
                @dataTable[key] = mapData.fix(mapData.enumMappingBackward[value\upper()])
            else
                @dataTable[key] = mapData.fix(value)
    ValueChanges: (key, oldVal, newVal, saveNow = @exists and @saveOnChange) =>
        if @nwObj and @updateNWObject
            {:getFunc} = @@PONY_DATA[key]
            @nwObj["Set#{getFunc}"](@nwObj, newVal, @networkNWObject)
        @Save() if saveNow
    SetFilename: (filename) =>
        @filename = filename
        @filenameFull = "#{filename}.txt"
        @fpath = "#{@@DATA_DIR}#{filename}.txt"
        @fpathBackup = "#{@@DATA_DIR}#{filename}.bak.txt"
        @fpathFull = "data/#{@@DATA_DIR}#{filename}.txt"
        @isOpen = @filename ~= nil
        @exists = file.Exists(@fpath, 'DATA')
        return @exists
    SetNetworkData: (nwObj) => @nwObj = nwObj
    SetPonyData: (nwObj) => @nwObj = nwObj
    SetPonyDataController: (nwObj) => @nwObj = nwObj
    SetPonyController: (nwObj) => @nwObj = nwObj
    SetController: (nwObj) => @nwObj = nwObj
    SetDataController: (nwObj) => @nwObj = nwObj

    SetNetworkOnChange: (newVal = true) => @networkNWObject = newVal
    SetUpdateOnChange: (newVal = true) => @updateNWObject = newVal

    GetNetworkOnChange: => @networkNWObject
    GetUpdateOnChange: => @updateNWObject

    GetNetworkData: => @nwObj
    GetPonyData: => @nwObj
    GetPonyDataController: => @nwObj
    GetPonyController: => @nwObj
    GetController: => @nwObj
    GetDataController: => @nwObj

    IsValid: => @valid
    Exists: => @exists
    FileExists: => @exists
    IsExists: => @exists
    GetFileName: => @filename
    GetFilename: => @filename
    GetFileNameFull: => @filenameFull
    GetFilenameFull: => @filenameFull
    GetFilePath: => @fpath
    GetFullFilePath: => @fpathFull
    SerealizeValue: (valID = '') =>
        map = @@PONY_DATA[valID]
        return unless map
        val = @dataTable[valID]
        if map.enum
            return map.enumMapping[val] or map.enumMapping[map.default()]
        elseif map.serealize
            return map.serealize(val)
        else
            return val
    Serealize: (prettyPrint = true) =>
        serTab = {key, @SerealizeValue(key) for key, val in pairs @dataTable}
        util.TableToJSON(serTab, prettyPrint)
    GetAsNetworked: => {getFunc, @dataTable[k] for k, {:getFunc} in pairs @@PONY_DATA}

    @READ_SUCCESS = 0
    @ERR_FILE_NOT_EXISTS = 1
    @ERR_FILE_EMPTY = 2
    @ERR_FILE_CORRUPT = 3
    ReadFromDisk: (force = false, doBackup = true) =>
        return @@ERR_FILE_NOT_EXISTS unless @exists
        fRead = file.Read(@fpath, 'DATA')
        return @@ERR_FILE_EMPTY if not fRead or fRead == ''
        readTable = util.JSONToTable(fRead)
        return @@ERR_FILE_CORRUPT unless readTable
        return @SetupData(readTable, force, doBackup) or @@READ_SUCCESS
    SaveAs: (path = @fpath) =>
        fCreate = @Serealize()
        file.Write(path, fCreate)
    Save: (doBackup = true) =>
        if doBackup and @exists
            fRead = file.Read(@fpath, 'DATA')
            file.Write(@fpathBackup, fRead)
        @SaveAs(@fpath)
        @exists = true

do
    PARSE_VECTOR = (str = '1.0 1.0 1.0', X = 1, Y = 1, Z = 1) ->
        return Vector(X, Y, Z) if str == ''
        x, y, z = str\match('([0-9.]+) ([0-9.]+) ([0-9.]+)')
        return Vector(tonumber(x) or X, tonumber(y) or Y, tonumber(z) or Z)

    PARSE_COLOR = (str = '1.0 1.0 1.0', r = 255, g = 255, b = 255) ->
        return Color(r, g, b) if str == ''
        {x, y, z} = PARSE_VECTOR(str, r / 255, g / 255, b / 255)
        return Color(x * 255, y * 255, z * 255)

    IMPORT_TABLE = {
        'gender': {
            name: 'Gender'
            func: (arg = 0) ->
                num = tonumber(arg)
                return num == 0 and 'MALE' or 'FEMALE'
        }

        'coatcolor': {
            name: 'BodyColor'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_bg': {
            name: 'EyeBackground'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_grad': {
            name: 'EyeIrisBottom'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_iris': {
            name: 'EyeIrisTop'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line1': {
            name: 'EyeIrisLine1'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyecolor_line2': {
            name: 'EyeIrisLine2'
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'haircolor1': {
            name: {'ManeColor1', 'TailColor1'}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'haircolor2': {
            name: {'ManeColor2', 'TailColor2'}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

        'eyejholerssize': {
            name: 'HoleWidth'
            func: (arg = '1') -> tonumber(arg) or 1
        }

        'eyeirissize': {
            name: 'IrisSize'
            func: (arg = '1') -> tonumber(arg) or 1
        }

        'eyeholesize': {
            name: 'HoleSize'
            func: (arg = '0.8') -> tonumber(arg) or 0.8
        }

        'bodyweight': {
            name: 'Weight'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'mane': {
            name: {'ManeType', 'ManeTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'manel': {
            name: {'ManeTypeLower', 'ManeTypeLowerNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tail': {
            name: {'TailType', 'TailTypeNew'}
            func: (arg = 0) -> (tonumber(arg) or 0) - 1
        }

        'tailsize': {
            name: 'TailSize'
            func: (arg = 1) -> tonumber(arg) or 1
        }

        'cmark': {
            name: 'CMarkType'
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        'cmark_enabled': {
            name: 'CMark'
            func: (arg = '1') -> arg == '1' or arg == '2'
        }
    }

    for i = 1, 8
        IMPORT_TABLE["bodydetail#{i}"] = {
            name: "BodyDetail#{i}"
            func: (arg = 1) -> (tonumber(arg) or 1) - 1
        }

        IMPORT_TABLE["bodydetail#{i}_c"] = {
            name: "BodyDetailColor#{i}"
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    for i = 3, 6
        IMPORT_TABLE["haircolor#{i}"] = {
            name: {"ManeDetailColor#{i - 2}", "TailDetailColor#{i - 2}"}
            func: (arg = '1.0 1.0 1.0') -> PARSE_COLOR(arg)
        }

    PPM2.ReadFromOldData = (filename = '_current') ->
        read = file.Read("ppm/#{filename}.txt", 'DATA')
        return false if read == ''
        split = [str\Trim() for str in *string.Explode('\n', read\Replace('\r', ''))]
        outputData = {}

        for line in *split
            varID = line\match('([a-zA-Z0-9_]+)')
            continue if not varID or varID == ''
            continue if not IMPORT_TABLE[varID]
            dt = IMPORT_TABLE[varID]
            value = line\sub(#varID + 2)
            if type(dt.name) ~= 'table'
                outputData[dt.name] = dt.func(value)
            else
                get = dt.func(value)
                outputData[name] = get for name in *dt.name

        data = PonyDataInstance("#{filename}_imported", nil, false)
        for key, value in pairs outputData
            data["Set#{key}"](data, value, false)
        return data, outputData

PPM2.PonyDataInstance = PonyDataInstance

PPM2.MainDataInstance = nil
PPM2.GetMainData = ->
    if not PPM2.MainDataInstance
        PPM2.MainDataInstance = PonyDataInstance('_current', nil, true, true)
        if not PPM2.MainDataInstance\FileExists()
            PPM2.MainDataInstance\Save()
    return PPM2.MainDataInstance


Always write bools

Always write bools

--
-- Copyright (C) 2017-2020 DBotThePony

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

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

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

net = DLib.net

ALLOW_ONLY_RAGDOLLS = CreateConVar('ppm2_sv_edit_ragdolls_only', '0', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Allow to edit only ragdolls')
DISALLOW_PLAYERS = CreateConVar('ppm2_sv_edit_no_players', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'When unrestricted edit allowed, do not allow to edit players.')

genericUsageCheck = (ply, ent) ->
	return false if not IsValid(ply)
	return false if not IsValid(ent)
	return false if ALLOW_ONLY_RAGDOLLS\GetBool() and ent\GetClass() ~= 'prop_ragdoll'
	return false if DISALLOW_PLAYERS\GetBool() and ent\IsPlayer()
	return false if not ent\IsPony()
	return false if not hook.Run('CanTool', ply, {Entity: ent, HitPos: ent\GetPos(), HitNormal: Vector()}, 'ponydata')
	return false if not hook.Run('CanProperty', ply, 'ponydata', ent)
	return true

net.ReceiveAntispam('PPM2.RagdollEdit')
net.Receive 'PPM2.RagdollEdit', (len = 0, ply = NULL) ->
	ent = net.ReadEntity()
	useLocal = net.ReadBool()
	return if not genericUsageCheck(ply, ent)

	if useLocal
		return if not ply\GetPonyData()
		if not ent\GetPonyData()
			data = PPM2.NetworkedPonyData(nil, ent)

		data = ent\GetPonyData()
		plydata = ply\GetPonyData()
		plydata\ApplyDataToObject(data)

		data\Create() if not data\IsNetworked()
	else
		if not ent\GetPonyData()
			data = PPM2.NetworkedPonyData(nil, ent)

		data = ent\GetPonyData()
		data\ReadNetworkData(len, ply, false, false)
		data\ReBroadcast()
		data\Create() if not data\IsNetworked()

	duplicator.StoreEntityModifier(ent, 'ppm2_ragdolledit', ent\GetPonyData()\NetworkedIterable(false))

net.ReceiveAntispam('PPM2.RagdollEditFlex')
net.Receive 'PPM2.RagdollEditFlex', (len = 0, ply = NULL) ->
	ent = net.ReadEntity()
	status = net.ReadBool()
	return if not genericUsageCheck(ply, ent)

	if not ent\GetPonyData()
		data = PPM2.NetworkedPonyData(nil, ent)

	data = ent\GetPonyData()
	data\SetNoFlex(status)
	data\Create() if not data\IsNetworked()

net.ReceiveAntispam('PPM2.RagdollEditEmote')
net.Receive 'PPM2.RagdollEditEmote', (len = 0, ply = NULL) ->
	ent = net.ReadEntity()
	return if not genericUsageCheck(ply, ent)
	self = ply
	emoteID = net.ReadUInt(8)
	return if not PPM2.AVALIABLE_EMOTES[emoteID]
	@__ppm2_last_played_emote = @__ppm2_last_played_emote or 0
	return if @__ppm2_last_played_emote > RealTimeL()
	@__ppm2_last_played_emote = RealTimeL() + 1
	net.Start('PPM2.PlayEmote')
	net.WriteUInt(emoteID, 8)
	net.WriteEntity(ent)
	net.WriteBool(false)
	net.WriteBool(false)
	net.SendOmit(ply)

duplicator.RegisterEntityModifier 'ppm2_ragdolledit', (ply = NULL, ent = NULL, storeddata = {}) ->
	return if not IsValid(ent)
	if not ent\GetPonyData()
		data = PPM2.NetworkedPonyData(nil, ent)

	data = ent\GetPonyData()
	data["Set#{key}"](data, value, false) for _, {key, value} in ipairs storeddata when data["Set#{key}"]
	data\ReBroadcast()
	timer.Simple 0.5, -> data\Create() if not data\IsNetworked()

--
-- Copyright (C) 2017-2020 DBotThePony

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

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

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

net = DLib.net

ALLOW_ONLY_RAGDOLLS = CreateConVar('ppm2_sv_edit_ragdolls_only', '0', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'Allow to edit only ragdolls')
DISALLOW_PLAYERS = CreateConVar('ppm2_sv_edit_no_players', '1', {FCVAR_NOTIFY, FCVAR_REPLICATED}, 'When unrestricted edit allowed, do not allow to edit players.')

genericUsageCheck = (ply, ent) ->
	return false if not IsValid(ply)
	return false if not IsValid(ent)
	return false if ALLOW_ONLY_RAGDOLLS\GetBool() and ent\GetClass() ~= 'prop_ragdoll'
	return false if DISALLOW_PLAYERS\GetBool() and ent\IsPlayer()
	return false if not ent\IsPony()
	return false if not hook.Run('CanTool', ply, {Entity: ent, HitPos: ent\GetPos(), HitNormal: Vector()}, 'ponydata')
	return false if not hook.Run('CanProperty', ply, 'ponydata', ent)
	return true

net.ReceiveAntispam('PPM2.RagdollEdit')
net.Receive 'PPM2.RagdollEdit', (len = 0, ply = NULL) ->
	ent = net.ReadEntity()
	useLocal = net.ReadBool()
	return if not genericUsageCheck(ply, ent)

	if useLocal
		return if not ply\GetPonyData()
		if not ent\GetPonyData()
			data = PPM2.NetworkedPonyData(nil, ent)

		data = ent\GetPonyData()
		plydata = ply\GetPonyData()
		plydata\ApplyDataToObject(data)

		data\Create() if not data\IsNetworked()
	else
		if not ent\GetPonyData()
			data = PPM2.NetworkedPonyData(nil, ent)

		data = ent\GetPonyData()
		data\ReadNetworkData(len, ply, false, false)
		data\ReBroadcast()
		data\Create() if not data\IsNetworked()

	duplicator.StoreEntityModifier(ent, 'ppm2_ragdolledit', ent\GetPonyData()\NetworkedIterable(false))

net.ReceiveAntispam('PPM2.RagdollEditFlex')
net.Receive 'PPM2.RagdollEditFlex', (len = 0, ply = NULL) ->
	ent = net.ReadEntity()
	status = net.ReadBool()
	return if not genericUsageCheck(ply, ent)

	if not ent\GetPonyData()
		data = PPM2.NetworkedPonyData(nil, ent)

	data = ent\GetPonyData()
	data\SetNoFlex(status)
	data\Create() if not data\IsNetworked()

net.ReceiveAntispam('PPM2.RagdollEditEmote')
net.Receive 'PPM2.RagdollEditEmote', (len = 0, ply = NULL) ->
	ent = net.ReadEntity()
	return if not genericUsageCheck(ply, ent)
	self = ply
	emoteID = net.ReadUInt(8)
	return if not PPM2.AVALIABLE_EMOTES[emoteID]
	@__ppm2_last_played_emote = @__ppm2_last_played_emote or 0
	return if @__ppm2_last_played_emote > RealTimeL()
	@__ppm2_last_played_emote = RealTimeL() + 1
	net.Start('PPM2.PlayEmote')
	net.WriteUInt(emoteID, 8)
	net.WriteEntity(ent)
	net.SendOmit(ply)

duplicator.RegisterEntityModifier 'ppm2_ragdolledit', (ply = NULL, ent = NULL, storeddata = {}) ->
	return if not IsValid(ent)
	if not ent\GetPonyData()
		data = PPM2.NetworkedPonyData(nil, ent)

	data = ent\GetPonyData()
	data["Set#{key}"](data, value, false) for _, {key, value} in ipairs storeddata when data["Set#{key}"]
	data\ReBroadcast()
	timer.Simple 0.5, -> data\Create() if not data\IsNetworked()


added tests for 128 bit keys

added tests for 128 bit keys


imageupload: chunked upload, video

imageupload: chunked upload, video

Former-commit-id: 896a3bb6234ee53df83a47771027325c1d8b6a90 [formerly ff3f16394072e5ec93a2a93cbbced0e54b47b542]
Former-commit-id: 356faf981ee508ae32f33dc24e15fa71160f7eaa

{:simplehttp, :json, :urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
fs = require'nixio.fs'
ltn12 = require'ltn12'

urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\match '.png'
      content_type = 'image/png'
    if file\match '.svg'
      content_type = 'image/svg'
    if file\match '.mp4'
      content_type = 'video/mp4'
    if file\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn

    return

  channel = req.url\match('channel=(.+)')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the two yellow buttons to attach image. It will appear instantly on IRC!</p
    <form>

        <input id="sender" type="text" required>

        Your name

        <input id="text" type="text" required>

        Attach text

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">

        What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools -> Add to homescreen to create a convenient shortcut for easy and fast access to sharing

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        --fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        --written = fd\write file
        --fd\sync!
        --fd\close!
        -- Move tempfile to real location
        fs.move req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender}> "
        msg = "[IRCSNAP] #{sender}#{text\sub(1, 100)} http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}file/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}?channel=#{channel}"

{:simplehttp, :json, :urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
ltn12 = require'ltn12'

urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\match '.png'
      content_type = 'image/png'
    if file\match '.svg'
      content_type = 'image/svg'
    if file\match '.mp4'
      content_type = 'video/mp4'
    if file\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn

    return

  channel = req.url\match('channel=(.+)')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the two yellow buttons to attach image. It will appear instantly on IRC!</p
    <form>

        <input id="sender" type="text" required>

        Your name

        <input id="text" type="text" required>

        Attach text

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">

        What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools -> Add to homescreen to create a convenient shortcut for easy and fast access to sharing

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.body
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        written = fd\write file
        fd\sync!
        fd\close!
        if sender ~= ''
          sender = "<#{sender}> "
        msg = "[IRCSNAP] #{sender}#{text\sub(1, 100)} http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}file/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "http://irc.lart.no:#{ivar2.config.webserverport}#{urlbase}?channel=#{channel}"


improve help

improve help

Prettyfi = (patt) ->
  -- Make them prettier
  patt = patt\gsub('%^%%p', '!')
  patt = patt\gsub('%$$', '')
  patt = patt\gsub('^%^', '')
  patt = patt\gsub '%$', ''
  return patt

listModules = (source, destination, arg) =>
  out = {}
  for moduleName, moduleTable in next, @Events!['PRIVMSG']
    unless @IsModuleDisabled moduleName, destination
      table.insert out, moduleName
  if #out > 0
    say "Type !help name-of-module to list patterns for each module. Modules: %s", table.concat(out, ' ')

listPatterns = (source, destination, moduleName) =>
  moduleTable = @Events!['PRIVMSG'][moduleName]
  unless moduleTable
    return

  if @IsModuleDisabled moduleName, destination
    return

  out = {}
  for pattern, callback in next, moduleTable
    patt = @ChannelCommandPattern(pattern, moduleName, destination)
    patt = Prettyfi patt
    table.insert out, patt

  if #out > 0
    say "%s patterns:\n %s", moduleName, table.concat(out, ',\n ')

Apropos = (s, d, what) =>
  moduleTable = @Events!['PRIVMSG']
  unless moduleTable
    return

  out = {}
  for moduleName, moduleTable in next, moduleTable
    if @IsModuleDisabled moduleName, destination
      return

    for pattern, callback in next, moduleTable
      if type(pattern) == 'string'
        patt = @ChannelCommandPattern(pattern, moduleName, destination)
        if patt and type(patt) == 'string' and patt\match(what)
          out[#out+1] = "#{ivar2.util.bold moduleName} module has pattern #{ivar2.util.italic Prettyfi patt}"
  if #out == 0
    say "Could not find a command pattern that matches that."
  else
    say table.concat(out, ',\n')

PRIVMSG:
  '^%plist$': listModules
  '^%plist (%w+)$': listPatterns
  '^%phelp$': listModules
  '^%phelp (%w+)$': listPatterns
  '^%papropos (.*)$': Apropos

Prettyfi = (patt) ->
  -- Make them prettier
  patt = patt\gsub('%^%%p', '!')
  patt = patt\gsub('%$$', '')
  patt = patt\gsub('^%^', '')
  patt = patt\gsub '%$', ''
  return patt

listModules = (source, destination, arg) =>
  out = {}
  for moduleName, moduleTable in next, @Events!['PRIVMSG']
    unless @IsModuleDisabled moduleName, destination
      table.insert out, moduleName
  if #out > 0
    say "Modules: %s", table.concat(out, ' ')

listPatterns = (source, destination, moduleName) =>
  moduleTable = @Events!['PRIVMSG'][moduleName]
  unless moduleTable
    return

  if @IsModuleDisabled moduleName, destination
    return

  out = {}
  for pattern, callback in next, moduleTable
    patt = @ChannelCommandPattern(pattern, moduleName, destination)
    patt = Prettyfi patt
    table.insert out, patt

  if #out > 0
    say "%s patterns:\n %s", moduleName, table.concat(out, ',\n ')

Apropos = (s, d, what) =>
  moduleTable = @Events!['PRIVMSG']
  unless moduleTable
    return

  out = {}
  for moduleName, moduleTable in next, moduleTable
    if @IsModuleDisabled moduleName, destination
      return

    for pattern, callback in next, moduleTable
      if type(pattern) == 'string'
        patt = @ChannelCommandPattern(pattern, moduleName, destination)
        if patt and type(patt) == 'string' and patt\match(what)
          out[#out+1] = "#{ivar2.util.bold moduleName} module has pattern #{ivar2.util.italic Prettyfi patt}"
  if #out == 0
    say "Could not find a command pattern that matches that."
  else
    say table.concat(out, ',\n')

PRIVMSG:
  '^%plist$': listModules
  '^%plist (%w+)$': listPatterns
  '^%phelp$': listModules
  '^%phelp (%w+)$': listPatterns
  '^%papropos (.*)$': Apropos


utf8: fix zalgorithm

utf8: fix zalgorithm

html2unicode = require'html'
math = require'math'

an = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
ci = 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl = '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'

an2ci = {}
-- Circled letters are 3 bytes, just use sub string
for i=1, #an
  f = i*3+1-3
  t = i*3
  an2ci[an\sub(i,i)] = ci\sub(f, t)

an2bl = {}
i=1
-- Since blackletters have varying byte length, use the common lua pattern to find multibyte chars
for uchar in string.gfind(bl, "([%z\1-\127\194-\244][\128-\191]*)")
  an2bl[an\sub(i,i)] = uchar
  i = i +1

codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

unichr = (n) ->
  html2unicode('&#x%x;'\format(n))

wireplace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      -- Replace space width ideographic space for fullwidth offset
      if bc == 32 and offset == 0xFEE0
        t[#t + 1] = '\227\128\128'
      elseif bc == 32
        t[#t + 1] = ' '
      elseif bc < 0x80 then
        t[#t + 1] = html2unicode("&#" .. (offset + bc) .. ";")
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

remap = (map, s) ->
  table.concat [map[s\sub(i,i)] or s\sub(i,i) for i=1, #s], ''

zalgo = (text, intensity=50) ->
  zalgo_chars = {}
  for i=0x0300, 0x036f
    zalgo_chars[i-0x2ff] = unichr(i)

  zalgo_chars[#zalgo_chars + 1] = unichr(0x0488)
  zalgo_chars[#zalgo_chars + 0] = unichr(0x0489)

  zalgoized = {}
  for letter in codepoints(text)
    zalgoized[#zalgoized + 1] = letter
    zalgo_num = math.random(1, intensity)
    for i=1, zalgo_num
      zalgoized[#zalgoized + 1] = zalgo_chars[math.random(1, #zalgo_chars)]
  table.concat(zalgoized)

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, zalgo(arg, 10)

html2unicode = require'html'
math = require'math'

an = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
ci = 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl = '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'

an2ci = {}
-- Circled letters are 3 bytes, just use sub string
for i=1, #an
  f = i*3+1-3
  t = i*3
  an2ci[an\sub(i,i)] = ci\sub(f, t)

an2bl = {}
i=1
-- Since blackletters have varying byte length, use the common lua pattern to find multibyte chars
for uchar in string.gfind(bl, "([%z\1-\127\194-\244][\128-\191]*)")
  an2bl[an\sub(i,i)] = uchar
  i = i +1

codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

unichr = (n) ->
  html2unicode('&#x%x;'\format(n))

wireplace = (offset, arg) ->
    s = arg or ''
    t = {}
    for i = 1, #s
      bc = string.byte(s, i, i)
      -- Replace space width ideographic space for fullwidth offset
      if bc == 32 and offset == 0xFEE0
        t[#t + 1] = '\227\128\128'
      elseif bc == 32
        t[#t + 1] = ' '
      elseif bc < 0x80 then
        t[#t + 1] = html2unicode("&#" .. (offset + bc) .. ";")
      else
        t[#t + 1] = s\sub(i, i)

    table.concat(t, "")

remap = (map, s) ->
  table.concat [map[s\sub(i,i)] or s\sub(i,i) for i=1, #s], ''

zalgo = (text, intensity=50) ->
  zalgo_threshold = intensity
  zalgo_chars = {}
  for i=0x0300, 0x036
    zalgo_chars[i-0x2ff] = unichr(i)

  zalgo_chars[#zalgo_chars + 1] = unichr(0x0488)
  zalgo_chars[#zalgo_chars + 1] = unichr(0x0489)

  source = text\upper()
  --source = _insert_randoms(source)

  zalgoized = {}
  for letter in codepoints(source)
    zalgoized[#zalgoized + 1] = letter
    zalgo_num = math.random(1, zalgo_threshold)
    for i=1, zalgo_num
      zalgoized[#zalgoized + 1] = zalgo_chars[math.random(1, #zalgo_chars)]
  table.concat(zalgoized)

PRIVMSG:
  '^%pwide (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, remap(an2ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    @Msg 'privmsg', destination, source, zalgo(arg, 10)


list: also register for command help. Remove leading ^

listModules = (source, destination, arg) =>
  out = {}
  for moduleName, moduleTable in next, @Events!['PRIVMSG']
    unless @IsModuleDisabled moduleName, destination
      table.insert out, moduleName
  if #out > 0
    say "Modules: %s", table.concat(out, ' ')

listPatterns = (source, destination, moduleName) =>
  moduleTable = @Events!['PRIVMSG'][moduleName]
  unless moduleTable
    return

  if @IsModuleDisabled moduleName, destination
    return

  out = {}
  for pattern, callback in next, moduleTable
    patt = @ChannelCommandPattern(pattern, moduleName, destination)
    -- Make them prettier
    patt = patt\gsub('%^%%p', '!')
    patt = patt\gsub('%$$', '')
    patt = patt\gsub('^%^', '')
    table.insert out, patt

  if #out > 0
    say "%s patterns: %s", moduleName, table.concat(out, ', ')

PRIVMSG:
  '^%plist$': listModules
  '^%plist (%w+)$': listPatterns
  '^%phelp$': listModules
  '^%phelp (%w+)$': listPatterns

listModules = (source, destination, arg) =>
  out = {}
  for moduleName, moduleTable in next, @Events!['PRIVMSG']
    unless @IsModuleDisabled moduleName, destination
      table.insert out, moduleName
  if #out > 0
    say "Modules: %s", table.concat(out, ' ')

listPatterns = (source, destination, moduleName) =>
  moduleTable = @Events!['PRIVMSG'][moduleName]
  unless moduleTable
    return

  if @IsModuleDisabled moduleName, destination
    return

  out = {}
  for pattern, callback in next, moduleTable
    patt = @ChannelCommandPattern(pattern, moduleName, destination)
    -- Make them prettier
    patt = patt\gsub('%^%%p', '!')
    patt = patt\gsub('%$$', '')
    table.insert out, patt

  if #out > 0
    say "%s patterns: %s", moduleName, table.concat(out, ', ')

PRIVMSG:
  '^%plist$': listModules
  '^%plist (%w+)$': listPatterns


omdb: fix error handling

omdb: fix error handling

Former-commit-id: 681f68de89ea051f5db820ec85704d6981c4dafd [formerly cf5a32da89a21f0c1ce3c1b5f6b824a35dd2874a]
Former-commit-id: 8ae0a0300079fdfab6c85dd0f76de4b5b879c640

util = require 'util'

rtcolor = (score) ->
  if not tonumber(score) then return score
  if tonumber(score) > 50
    return util.red(string.format("%s%%", score))
  else
    return util.green(string.format("%s%%", score))

metacolor = (score) ->
  if not tonumber(score) then return score
  if tonumber(score) >= 60
    return util.green(string.format("%s%%", score))
  elseif tonumber(score) => 40
    return util.yellow(string.format("%s%%", score))
  else
    return util.red(string.format("%s%%", score))

omdbfetch = (arg, cb) ->
  util.simplehttp "http://www.omdbapi.com/?t=#{util.urlEncode arg}&y=&plot=short&r=json&tomatoes=true", (data) ->
    js = util.json.decode(data)
    if js and not js.Error
      cb(js)
    else
      say('%s', js.Error)

omdb = (source, destination, arg) =>
  omdbfetch arg, (js) ->
    say "[#{util.bold js.Title}] (#{js.Year}) #{js.Genre} Metacritic: [#{metacolor js.Metascore}] RT: [#{rtcolor js.tomatoMeter} / #{rtcolor js.tomatoRotten}] IMDB: [#{js.imdbRating}] #{js.Plot}"

plot = (source, destination, arg) =>
  omdbfetch arg, (js) ->
    say "[#{util.bold js.Title}] (#{js.Year}) #{js.Plot}"

PRIVMSG:
  '^%pomdb (.+)': omdb
  '^%pimdb (.+)': omdb
  '^%prt (.+)': omdb
  '^%pmovie (.+)': omdb
  '^%pplot (.+)': plot

util = require 'util'

rtcolor = (score) ->
  if not tonumber(score) then return score
  if tonumber(score) > 50
    return util.red(string.format("%s%%", score))
  else
    return util.green(string.format("%s%%", score))

metacolor = (score) ->
  if not tonumber(score) then return score
  if tonumber(score) >= 60
    return util.green(string.format("%s%%", score))
  elseif tonumber(score) => 40
    return util.yellow(string.format("%s%%", score))
  else
    return util.red(string.format("%s%%", score))

omdbfetch = (arg, cb) ->
  util.simplehttp "http://www.omdbapi.com/?t=#{util.urlEncode arg}&y=&plot=short&r=json&tomatoes=true", (data) ->
    js = util.json.decode(data)
    if js and not js.Error
      cb(js)
    else
      say('(%d) %s', js.code, js.Error)

omdb = (source, destination, arg) =>
  omdbfetch arg, (js) ->
    say "[#{util.bold js.Title}] (#{js.Year}) #{js.Genre} Metacritic: [#{metacolor js.Metascore}] RT: [#{rtcolor js.tomatoMeter} / #{rtcolor js.tomatoRotten}] IMDB: [#{js.imdbRating}] #{js.Plot}"

plot = (source, destination, arg) =>
  omdbfetch arg, (js) ->
    say "[#{util.bold js.Title}] (#{js.Year}) #{js.Plot}"

PRIVMSG:
  '^%pomdb (.+)': omdb
  '^%pimdb (.+)': omdb
  '^%prt (.+)': omdb
  '^%pmovie (.+)': omdb
  '^%pplot (.+)': plot


Small typo in documentation [ci skip]

Small typo in documentation [ci skip]

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

Source = require "non.objects.Source"

sources = {}
globalvolume = 1

getNumSources = ->
  #sources

getVolume = ->
  globalvolume

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

pause = (source) ->
  source\pause!

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

resume = (source) ->
  source\resume!

setVolume = (volume) ->
  globalvolume = volume

  for i, source in ipairs sources
    source\setVolume globalvolume

stop = (source) ->
  source\stop!

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

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

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

@module non.audio
]]

Source = require "non.objects.Source"

sources = {}
globalvolume = 1

getNumSources = ->
  #sources

getVolume = ->
  globalvolume

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

pause = (source) ->
  source\pause!

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

resume = (source) ->
  source\resume!

setVolume = (volume) ->
  globalvolume = volume

  for i, source in ipairs sources
    source\setVolume globalvolume

stop = (source) ->
  source\stop!

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

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


Fixed minor typo causing logic error.

Fixed minor typo causing logic error.

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

glurg = require "glurg"
Promise = require "glurg.common.Promise"
MeshBlob = require "glurg.resources.MeshBlob"

class Mesh
	new: (blob) =>
		Promise.keep("blob", Promise.IsClass(blob, MeshBlob))

		@_mesh = glurg.resources.Mesh.new(blob._blob)
		@num_values = @_mesh.num_values
		@blob = blob

	get_value: (index) =>
		Promise.keep("index", Promise.IsNumber(index))
		Promise.keep("index >= 1", index >= 1)
		Promise.keep("index <= @num_values", index <= @num_values)

		return @_mesh\get_value(index - 1)

return Mesh

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

glurg = require "glurg"
Promise = require "glurg.common.Promise"
MeshBlob = require "glurg.resources.MeshBlob"

class Mesh
	new: (blob) =>
		Promise.keep("blob", Promise.IsClass(blob, MeshBlob))

		@_mesh = glurg.resources.Mesh.new(blob._blob)
		@num_values = @_mesh.num_values
		@blob = blob

	get_value: (index) =>
		Promise.keep("index", Promise.IsNumber(blob))
		Promise.keep("index >= 1", index >= 1)
		Promise.keep("index <= @num_values", index <= @num_values)

		return @_mesh\get_value(index - 1)

return Mesh