increase ttl drastically

increase ttl drastically

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

class PPM2.NetworkChangeState
	new: (key = '', keyValid = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@keyValid = keyValid
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTimeL()
		@rtime = RealTimeL()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
		@cantApply = false
		@networkChange = true
	GetPlayer: => @ply
	ChangedByClient: => not @networkChange or IsValid(@ply)
	ChangedByPlayer: => not @networkChange or IsValid(@ply)
	ChangedByServer: => not @networkChange or not IsValid(@ply)
	GetKey: => @keyValid
	GetVariable: => @keyValid
	GetVar: => @keyValid
	GetKeyInternal: => @key
	GetVariableInternal: => @key
	GetVarInternal: => @key
	GetNewValue: => @newValue
	GetValue: => @newValue
	GetCantApply: => @cantApply
	SetCantApply: (val) => @cantApply = val
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTimeL: => @rtime
	GetRealTimeL: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len
	ChangedByNetwork: => @networkChange

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

do
	nullify = ->
		ProtectedCall ->
			for _, ent in ipairs ents.GetAll()
				ent.__PPM2_PonyData\Remove() if ent.__PPM2_PonyData

		for _, ent in ipairs ents.GetAll()
			ent.__PPM2_PonyData = nil

	nullify()
	timer.Simple 0, -> timer.Simple 0, -> timer.Simple 0, nullify
	hook.Add 'InitPostEntity', 'PPM2.FixSingleplayer', nullify

wUInt = (def = 0, size = 8) ->
	return (arg = def) -> net.WriteUInt(arg, size)

wInt = (def = 0, size = 8) ->
	return (arg = def) -> net.WriteInt(arg, size)

rUInt = (size = 8, min = 0, max = 255) ->
	return -> math.Clamp(net.ReadUInt(size), min, max)

rInt = (size = 8, min = -128, max = 127) ->
	return -> math.Clamp(net.ReadInt(size), min, max)

rFloat = (min = 0, max = 255) ->
	return -> math.Clamp(net.ReadFloat(), min, max)

wFloat = net.WriteFloat
rBool = net.ReadBool
wBool = net.WriteBool
rColor = net.ReadColor
wColor = net.WriteColor
rString = net.ReadString
wString = net.WriteString

class NetworkedPonyData extends PPM2.ModifierBase
	@AddNetworkVar = (getName = 'Var', readFunc = (->), writeFunc = (->), defValue, onSet = ((val) => val), networkByDefault = true) =>
		defFunc = defValue
		defFunc = (-> defValue) if type(defValue) ~= 'function'
		strName = "_NW_#{getName}"
		@NW_NextVarID += 1
		id = @NW_NextVarID
		tab = {:strName, :readFunc, :getName, :writeFunc, :defValue, :defFunc, :id, :onSet}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[id] = tab
		@__base[strName] = defFunc()

		@__base["Get#{getName}"] = => @[strName]
		@__base["Set#{getName}"] = (val = defFunc(), networkNow = networkByDefault) =>
			oldVal = @[strName]
			nevVal = onSet(@, val)
			@[strName] = nevVal
			state = PPM2.NetworkChangeState(strName, getName, nevVal, @)
			state.networkChange = false
			@SetLocalChange(state)
			return unless networkNow and @NETWORKED and (CLIENT and @ent == LocalPlayer() or SERVER)
			net.Start(@@NW_Modify)
			net.WriteUInt(@GetNetworkID(), 16)
			net.WriteUInt(id, 16)
			writeFunc(nevVal)
			net.SendToServer() if CLIENT
			net.Broadcast() if SERVER

	@NetworkVar = (...) => @AddNetworkVar(...)
	@GetSet = (fname, fvalue) =>
		@__base["Get#{fname}"] = => @[fvalue]
		@__base["Set#{fname}"] = (fnewValue = @[fvalue]) =>
			oldVal = @[fvalue]
			@[fvalue] = fnewValue
			state = PPM2.NetworkChangeState(fvalue, fname, fnewValue, @)
			state.networkChange = false
			@SetLocalChange(state)

	@NW_WAIT = {}

	if CLIENT
		hook.Add 'OnEntityCreated', 'PPM2.NW_WAIT', (ent) -> timer.Simple 0, ->
			return if not IsValid(ent)

			dirty = false
			entid = ent\EntIndex()
			ttl = RealTimeL()

			for controller in *@NW_WAIT
				if controller.removed
					controller.isNWWaiting = false
					dirty = true
				elseif controller.waitEntID == entid
					controller.isNWWaiting = false
					controller.ent = ent
					controller.modelCached = ent\GetModel()
					controller\SetupEntity(ent)
					--print('FOUND', ent)
					dirty = true
				elseif controller.waitTTL < ttl
					dirty = true
					controller.isNWWaiting = false

			if dirty
				@NW_WAIT = [controller for controller in *@NW_WAIT when controller.isNWWaiting]

		hook.Add 'NotifyShouldTransmit', 'PPM2.NW_WAIT', (ent, should) -> timer.Simple 0, ->
			return if not IsValid(ent)

			dirty = false
			entid = ent\EntIndex()
			ttl = RealTimeL()

			for controller in *@NW_WAIT
				if controller.removed
					controller.isNWWaiting = false
					dirty = true
				elseif controller.waitEntID == entid
					controller.isNWWaiting = false
					controller.ent = ent
					controller.modelCached = ent\GetModel()
					controller\SetupEntity(ent)
					--print('FOUND', ent)
					dirty = true
				elseif controller.waitTTL < ttl
					dirty = true
					controller.isNWWaiting = false

			if dirty
				@NW_WAIT = [controller for controller in *@NW_WAIT when controller.isNWWaiting]

	@OnNetworkedCreated = (ply = NULL, len = 0, nwobj) =>
		if CLIENT
			netID = net.ReadUInt16()
			entid = net.ReadUInt16()
			obj = @NW_Objects[netID] or @(netID, entid)
			obj.NETWORKED = true
			obj.CREATED_BY_SERVER = true
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
		else
			ply[@NW_CooldownTimer] = ply[@NW_CooldownTimer] or 0
			ply[@NW_CooldownTimerCount] = ply[@NW_CooldownTimerCount] or 0

			if ply[@NW_CooldownTimer] < RealTimeL()
				ply[@NW_CooldownTimerCount] = 1
				ply[@NW_CooldownTimer] = RealTimeL() + 10
			else
				ply[@NW_CooldownTimerCount] += 1

			if ply[@NW_CooldownTimerCount] >= 3
				ply[@NW_CooldownMessage] = ply[@NW_CooldownMessage] or 0
				if ply[@NW_CooldownMessage] < RealTimeL()
					PPM2.Message 'Player ', ply, " is creating #{@__name} too quickly!"
					ply[@NW_CooldownMessage] = RealTimeL() + 1
				return

			waitID = net.ReadUInt16()
			obj = @(nil, ply)
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			obj\Create()
			timer.Simple 0.5, ->
				return if not IsValid(ply)
				net.Start(@NW_ReceiveID)
				net.WriteUInt(waitID, 16)
				net.WriteUInt(obj.netID, 16)
				net.Send(ply)

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]

		unless obj
			return if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		if IsValid(ply) and obj.ent ~= ply
			error('Invalid realm for player being specified. If you are running on your own net.* library, check up your code') if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		varID = net.ReadUInt16()
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :getName, :readFunc, :writeFunc, :onSet} = varData
		newVal = onSet(obj, readFunc())
		return if newVal == obj["Get#{getName}"](obj)
		state = PPM2.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, 16)
			writeFunc(newVal)
			net.SendOmit(ply)

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove(true)

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

	@RenderTasks = {}
	@CheckTasks = {}

	@NW_Vars = {}
	@NW_VarsTable = {}
	@NW_Objects = {}
	@O_Slots = {} if CLIENT
	@NW_Waiting = {}
	@NW_WaitID = -1
	@NW_NextVarID = -1
	@NW_Create = 'PPM2.NW.Created'
	@NW_Modify = 'PPM2.NW.Modified'
	@NW_Broadcast = 'PPM2.NW.ModifiedBroadcast'
	@NW_Remove = 'PPM2.NW.Removed'
	@NW_Rejected = 'PPM2.NW.Rejected'
	@NW_ReceiveID = 'PPM2.NW.ReceiveID'
	@NW_CooldownTimerCount = 'ppm2_NW_CooldownTimerCount'
	@NW_CooldownTimer = 'ppm2_NW_CooldownTimer'
	@NW_CooldownMessage = 'ppm2_NW_CooldownMessage'
	@NW_NextObjectID = 0
	@NW_NextObjectID_CL = 0x60000

	if SERVER
		net.pool(@NW_Create)
		net.pool(@NW_Modify)
		net.pool(@NW_Remove)
		net.pool(@NW_ReceiveID)
		net.pool(@NW_Rejected)
		net.pool(@NW_Broadcast)

	net.Receive @NW_Create, (len = 0, ply = NULL, obj) -> @OnNetworkedCreated(ply, len, obj)
	net.Receive @NW_Modify, (len = 0, ply = NULL, obj) -> @OnNetworkedModify(ply, len, obj)
	net.Receive @NW_Remove, (len = 0, ply = NULL, obj) -> @OnNetworkedDelete(ply, len, obj)

	net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
		return if SERVER
		waitID = net.ReadUInt16()
		netID = net.ReadUInt16()
		obj = @NW_Waiting[waitID]
		@NW_Waiting[waitID] = nil
		return unless obj
		obj.NETWORKED = true
		@NW_Objects[obj.netID] = nil
		obj.netID = netID
		obj.waitID = nil
		@NW_Objects[netID] = obj

	net.Receive @NW_Rejected, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		return if obj.__LastReject and obj.__LastReject > RealTimeL()
		obj.__LastReject = RealTimeL() + 3
		obj.NETWORKED = false
		obj\Create()

	net.Receive @NW_Broadcast, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		obj\ReadNetworkData(len, ply)

	@GetSet('UpperManeModel', 'm_upperManeModel')
	@GetSet('LowerManeModel', 'm_lowerManeModel')
	@GetSet('TailModel', 'm_tailModel')
	@GetSet('SocksModel', 'm_socksModel')
	@GetSet('NewSocksModel', 'm_newSocksModel')

	@NetworkVar('Fly',                  rBool,   wBool,                 false)
	@NetworkVar('DisableTask',          rBool,   wBool,                 false)
	@NetworkVar('UseFlexLerp',          rBool,   wBool,                  true)
	@NetworkVar('FlexLerpMultiplier',   rFloat(0, 10),  wFloat,             1)

	@SetupModifiers: =>
		for key, value in pairs PPM2.PonyDataRegistry
			if value.modifiers
				@RegisterModifier(value.getFunc, 0, 0)
				@SetModifierMinMaxFinal(value.getFunc, value.min, value.max) if value.min or value.max
				@SetupLerpTables(value.getFunc)
				strName = '_NW_' .. value.getFunc
				funcLerp = 'Calculate' .. value.getFunc
				@__base['Get' .. value.getFunc] = => @[funcLerp](@, @[strName])

	for key, value in pairs PPM2.PonyDataRegistry
		@NetworkVar(value.getFunc, value.read, value.write, value.default)

	new: (netID, ent) =>
		super()

		@m_upperManeModel = Entity(-1)
		@m_lowerManeModel = Entity(-1)
		@m_tailModel = Entity(-1)
		@m_socksModel = Entity(-1)
		@m_newSocksModel = Entity(-1)

		@recomputeTextures = true
		@isValid = true
		@removed = false
		@valid = true
		@NETWORKED = false
		@NETWORKED_PREDICT = false

		@[data.strName] = data.defFunc() for _, data in ipairs @@NW_Vars when data.defFunc

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

		@@NW_Objects[@netID] = @

		if CLIENT
			for i = 1, 1024
				if not @@O_Slots[i]
					@slotID = i
					@@O_Slots[i] = @
					break

			if not @slotID
				error('dafuq? No empty slots are available')

		@isNWWaiting = false

		if type(ent) == 'number'
			entid = ent
			@waitEntID = entid
			ent = Entity(entid)
			--print(ent, entid)

			if not IsValid(ent)
				@isNWWaiting = true
				@waitTTL = RealTimeL() + 3600
				table.insert(@@NW_WAIT, @)
				PPM2.LMessage('message.ppm2.debug.race_condition')
				--print('WAITING ON ', entid)
				return

		return if not IsValid(ent)
		@ent = ent
		@modelCached = ent\GetModel() if IsValid(ent)
		@SetupEntity(ent)

	GetEntity: => @ent
	IsValid: => @isValid
	GetModel: => @modelCached
	EntIndex: => @entID
	ObjectSlot: => @slotID
	GetObjectSlot: => @slotID

	Clone: (target = @ent) =>
		copy = @@(nil, target)
		@ApplyDataToObject(copy)
		return copy

	SetupEntity: (ent) =>
		if ent.__PPM2_PonyData
			return if ent.__PPM2_PonyData\GetOwner() and IsValid(ent.__PPM2_PonyData\GetOwner()) and ent.__PPM2_PonyData\GetOwner() ~= @GetOwner()
			ent.__PPM2_PonyData\Remove() if ent.__PPM2_PonyData.Remove and ent.__PPM2_PonyData ~= @

		ent.__PPM2_PonyData = @
		@entTable = @ent\GetTable()
		return unless IsValid(ent)
		@modelCached = ent\GetModel()
		ent\PPMBonesModifier()
		@flightController = PPM2.PonyflyController(@)
		@entID = ent\EntIndex()
		@lastLerpThink = RealTimeL()
		@ModelChanges(@modelCached, @modelCached)
		@Reset()
		timer.Simple(0, -> @GetRenderController()\CompileTextures() if @GetRenderController()) if CLIENT
		PPM2.DebugPrint('Ponydata ', @, ' was updated to use for ', @ent)
		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	ModelChanges: (old = @ent\GetModel(), new = old) =>
		@modelCached = new
		@SetFly(false) if SERVER
		timer.Simple 0.5, ->
			return unless IsValid(@ent)
			@Reset()

	GenericDataChange: (state) =>
		hook.Run 'PPM2_PonyDataChanges', @ent, @, state

		if state\GetKey() == 'Fly' and @flightController
			@flightController\Switch(state\GetValue())

		if state\GetKey() == 'DisableTask'
			@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
			@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

		@GetSizeController()\DataChanges(state) if @ent and @GetBodygroupController()
		@GetBodygroupController()\DataChanges(state) if @ent and @GetBodygroupController()

		@GetWeightController()\DataChanges(state) if @ent and @GetWeightController()

		if CLIENT and @ent
			@GetRenderController()\DataChanges(state) if @GetRenderController()

	ResetScale: =>
		if scale = @GetSizeController()
			scale\ResetScale()

	ModifyScale: =>
		if scale = @GetSizeController()
			scale\ModifyScale()

	Reset: =>
		if scale = @GetSizeController()
			scale\Reset()
		if CLIENT
			@GetWeightController()\Reset() if @GetWeightController() and @GetWeightController().Reset
			@GetRenderController()\Reset() if @GetRenderController() and @GetRenderController().Reset
			@GetBodygroupController()\Reset() if @GetBodygroupController() and @GetBodygroupController().Reset

	GetHoofstepVolume: =>
		return 0.8 if @ShouldMuffleHoosteps()
		return 1

	ShouldMuffleHoosteps: =>
		return @GetSocksAsModel() or @GetSocksAsNewModel()

	PlayerRespawn: =>
		return if not IsValid(@ent)
		@entTable.__cachedIsPony = @ent\IsPony()
		if not @entTable.__cachedIsPony
			return if @alreadyCalledRespawn
			@alreadyCalledRespawn = true
			@alreadyCalledDeath = true
		else
			@alreadyCalledRespawn = false
			@alreadyCalledDeath = false
		@ApplyBodygroups(CLIENT, true)
		@SetFly(false) if SERVER

		@ent\PPMBonesModifier()

		if scale = @GetSizeController()
			scale\PlayerRespawn()

		if weight = @GetWeightController()
			weight\PlayerRespawn()

		if CLIENT
			@deathRagdollMerged = false
			@GetRenderController()\PlayerRespawn() if @GetRenderController()
			@GetBodygroupController()\MergeModels(@ent) if IsValid(@ent) and @GetBodygroupController().MergeModels

	PlayerDeath: =>
		return if not IsValid(@ent)

		if @ent.__ppmBonesModifiers
			@ent.__ppmBonesModifiers\Remove()

		@entTable.__cachedIsPony = @ent\IsPony()

		if not @entTable.__cachedIsPony
			return if @alreadyCalledDeath
			@alreadyCalledDeath = true
		else
			@alreadyCalledDeath = false

		@SetFly(false) if SERVER

		if scale = @GetSizeController()
			scale\PlayerDeath()

		if weight = @GetWeightController()
			weight\PlayerDeath()

		if CLIENT
			@DoRagdollMerge()
			@GetRenderController()\PlayerDeath() if @GetRenderController()

	DoRagdollMerge: =>
		return if @deathRagdollMerged
		bgController = @GetBodygroupController()
		rag = @ent\GetRagdollEntity()

		if not bgController.MergeModels
			@deathRagdollMerged = true
		elseif IsValid(rag)
			@deathRagdollMerged = true
			bgController\MergeModels(rag)

	ApplyBodygroups: (updateModels = CLIENT) => @GetBodygroupController()\ApplyBodygroups(updateModels) if @ent
	SetLocalChange: (state) => @GenericDataChange(state)
	NetworkDataChanges: (state) => @GenericDataChange(state)

	GetPonyRaceFlags: =>
		switch @GetRace()
			when PPM2.RACE_EARTH
				return 0
			when PPM2.RACE_PEGASUS
				return PPM2.RACE_HAS_WINGS
			when PPM2.RACE_UNICORN
				return PPM2.RACE_HAS_HORN
			when PPM2.RACE_ALICORN
				return PPM2.RACE_HAS_HORN + PPM2.RACE_HAS_WINGS

	SlowUpdate: =>
		@GetBodygroupController()\SlowUpdate() if @GetBodygroupController()
		@GetWeightController()\SlowUpdate() if @GetWeightController()
		if scale = @GetSizeController()
			scale\SlowUpdate()

		if SERVER and IsValid(@ent) and @ent\IsPlayer()
			arms = @ent\GetHands()

			if IsValid(arms)
				cond = @GetPonyRaceFlags()\band(PPM2.RACE_HAS_HORN) ~= 0 and @ent\GetInfoBool('ppm2_cl_vm_magic_hands', true) and PPM2.HAND_BODYGROUP_MAGIC or PPM2.HAND_BODYGROUP_HOOVES
				arms\SetBodygroup(PPM2.HAND_BODYGROUP_ID, cond)

	Think: =>
	RenderScreenspaceEffects: =>
		time = RealTimeL()
		delta = time - @lastLerpThink
		@lastLerpThink = time
		if @isValid and IsValid(@ent)
			for _, change in ipairs @TriggerLerpAll(delta * 5)
				state = PPM2.NetworkChangeState('_NW_' .. change[1], change[1], change[2] + @['_NW_' .. change[1]], @)
				state\SetCantApply(true)
				@GenericDataChange(state)

	GetFlightController: => @flightController

	GetRenderController: =>
		return if SERVER
		return @renderController if not @isValid
		if not @renderController or @modelCached ~= @modelRender
			@modelRender = @modelCached
			cls = PPM2.GetRenderController(@modelCached)
			if @renderController and cls == @renderController.__class
				@renderController.ent = @ent
				PPM2.DebugPrint('Skipping render controller recreation for ', @ent, ' as part of ', @)
				return @renderController
			@renderController\Remove() if @renderController
			@renderController = cls(@)
		@renderController.ent = @ent
		return @renderController

	GetWeightController: =>
		return @weightController if not @isValid
		if not @weightController or @modelCached ~= @modelWeight
			@modelCached = @modelCached or @ent\GetModel()
			@modelWeight = @modelCached
			cls = PPM2.GetPonyWeightController(@modelCached)
			if @weightController and cls == @weightController.__class
				@weightController.ent = @ent
				PPM2.DebugPrint('Skipping weight controller recreation for ', @ent, ' as part of ', @)
				return @weightController
			@weightController\Remove() if @weightController
			@weightController = cls(@)
		@weightController.ent = @ent
		return @weightController

	GetSizeController: =>
		return @scaleController if not @isValid
		if not @scaleController or @modelCached ~= @modelScale
			@modelCached = @modelCached or @ent\GetModel()
			@modelScale = @modelCached
			cls = PPM2.GetSizeController(@modelCached)
			if @scaleController and cls == @scaleController.__class
				@scaleController.ent = @ent
				PPM2.DebugPrint('Skipping size controller recreation for ', @ent, ' as part of ', @)
				return @scaleController
			@scaleController\Remove() if @scaleController
			@scaleController = cls(@)
		@scaleController.ent = @ent
		return @scaleController
	GetScaleController: => @GetSizeController()

	GetBodygroupController: =>
		return @bodygroups if not @isValid
		if not @bodygroups or @modelBodygroups ~= @modelCached
			@modelCached = @modelCached or @ent\GetModel()
			@modelBodygroups = @modelCached
			cls = PPM2.GetBodygroupController(@modelCached)
			if @bodygroups and cls == @bodygroups.__class
				@bodygroups.ent = @ent
				PPM2.DebugPrint('Skipping bodygroup controller recreation for ', @ent, ' as part of ', @)
				return @bodygroups
			@bodygroups\Remove() if @bodygroups
			@bodygroups = cls(@)
		@bodygroups.ent = @ent
		return @bodygroups

	Remove: (byClient = false) =>
		@removed = true
		@@NW_Objects[@netID] = nil if SERVER or @NETWORKED
		@@O_Slots[@slotID] = nil if @slotID
		@isValid = false
		@ent = @GetEntity() if not IsValid(@ent)
		@entTable.__PPM2_PonyData = nil if IsValid(@ent) and @ent.__PPM2_PonyData == @
		@GetWeightController()\Remove() if @GetWeightController()

		if CLIENT
			@GetRenderController()\Remove() if @GetRenderController()
			if IsValid(@ent) and @ent.__ppm2_task_hit
				@entTable.__ppm2_task_hit = false
				@ent\SetNoDraw(false)
		@GetBodygroupController()\Remove() if @GetBodygroupController()
		@GetSizeController()\Remove() if @GetSizeController()
		@flightController\Switch(false) if @flightController
		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	__tostring: => "[#{@@__name}:#{@netID}|#{@ent}]"

	GetOwner: => @ent
	IsNetworked: => @NETWORKED
	IsGoingToNetwork: => @NETWORKED_PREDICT
	SetIsGoingToNetwork: (val = @NETWORKED) => @NETWORKED_PREDICT = val
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID

	ComputeMagicColor: =>
		color = @GetHornMagicColor()

		if not @GetSeparateMagicColor()
			if not @GetSeparateEyes()
				color = @GetEyeIrisTop()\Lerp(0.5, @GetEyeIrisBottom())
			else
				lerpLeft = @GetEyeIrisTopLeft()\Lerp(0.5, @GetEyeIrisBottomLeft())
				lerpRight = @GetEyeIrisTopRight()\Lerp(0.5, @GetEyeIrisBottomRight())
				color = lerpLeft\Lerp(0.5, lerpRight)

		return color

	ReadNetworkData: (len = 24, ply = NULL, silent = false, applyEntities = true) =>
		data = @@ReadNetworkData()
		validPly = IsValid(ply)
		states = [PPM2.NetworkChangeState(key, keyValid, newVal, @, len, ply) for key, {keyValid, newVal} in pairs data]
		for _, state in ipairs states
			if not validPly or applyEntities or not isentity(state\GetValue())
				state\Apply()
				@NetworkDataChanges(state) unless silent

	NetworkedIterable: (grabEntities = true) =>
		data = [{getName, @[strName]} for _, {:strName, :getName} in ipairs @@NW_Vars when grabEntities or not isentity(@[strName])]
		return data

	ApplyDataToObject: (target, applyEntities = false) =>
		target["Set#{key}"](target, value) for {key, value} in *@NetworkedIterable(applyEntities) when target["Set#{key}"]
		return target

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

	ReBroadcast: =>
		return false if not @NETWORKED
		return false if CLIENT
		net.Start(@@NW_Broadcast)
		net.WriteUInt16(@netID)
		@WriteNetworkData()
		net.Broadcast()
		return true

	Create: =>
		return if @NETWORKED
		return if CLIENT and @CREATED_BY_SERVER -- wtf
		@NETWORKED = true if SERVER
		@NETWORKED_PREDICT = true

		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt16(@netID)
			net.WriteEntity(@ent)
			@WriteNetworkData()
			filter = RecipientFilter()
			filter\AddAllPlayers()
			filter\RemovePlayer(@ent) if IsValid(@ent) and @ent\IsPlayer()
			net.Send(filter)
		else
			@@NW_WaitID += 1
			@waitID = @@NW_WaitID

			net.Start(@@NW_Create)
			before = net.BytesWritten()

			net.WriteUInt16(@waitID)
			@WriteNetworkData()
			after = net.BytesWritten()

			net.SendToServer()
			@@NW_Waiting[@waitID] = @
			return after - before

	NetworkTo: (targets = {}) =>
		net.Start(@@NW_Create)
		net.WriteUInt16(@netID)
		net.WriteEntity(@ent)
		@WriteNetworkData()
		net.Send(targets)

PPM2.NetworkedPonyData = NetworkedPonyData

if CLIENT
	net.Receive 'PPM2.NotifyDisconnect', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()

	net.Receive 'PPM2.PonyDataRemove', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()
else
	hook.Add 'PlayerJoinTeam', 'PPM2.TeamWaypoint', (ply, new) ->
		ply.__ppm2_modified_jump = false
	hook.Add 'OnPlayerChangedTeam', 'PPM2.TeamWaypoint', (ply, old, new) ->
		ply.__ppm2_modified_jump = false

_G.LocalPonyData = () -> LocalPlayer()\GetPonyData()
_G.LocalPonydata = () -> LocalPlayer()\GetPonyData()

entMeta = FindMetaTable('Entity')
entMeta.GetPonyData = =>
	self2 = @
	self = entMeta.GetTable(@)
	return if not @
	if @__PPM2_PonyData and @__PPM2_PonyData.ent ~= self2
		@__PPM2_PonyData.ent = self2
		@__PPM2_PonyData\SetupEntity(self2) if CLIENT
	return @__PPM2_PonyData

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

class PPM2.NetworkChangeState
	new: (key = '', keyValid = '', newValue, obj, len = 24, ply = NULL) =>
		@key = key
		@keyValid = keyValid
		@oldValue = obj[key]
		@newValue = newValue
		@ply = ply
		@time = CurTimeL()
		@rtime = RealTimeL()
		@stime = SysTime()
		@obj = obj
		@objID = obj.netID
		@len = len
		@rlen = len - 24 -- ID - 16 bits, variable id - 8 bits
		@cantApply = false
		@networkChange = true
	GetPlayer: => @ply
	ChangedByClient: => not @networkChange or IsValid(@ply)
	ChangedByPlayer: => not @networkChange or IsValid(@ply)
	ChangedByServer: => not @networkChange or not IsValid(@ply)
	GetKey: => @keyValid
	GetVariable: => @keyValid
	GetVar: => @keyValid
	GetKeyInternal: => @key
	GetVariableInternal: => @key
	GetVarInternal: => @key
	GetNewValue: => @newValue
	GetValue: => @newValue
	GetCantApply: => @cantApply
	SetCantApply: (val) => @cantApply = val
	NewValue: => @newValue
	GetOldValue: => @oldValue
	OldValue: => @oldValue
	CurTime: => @time
	GetCurTime: => @time
	GetReceiveTime: => @time
	GetReceiveStamp: => @time
	RealTimeL: => @rtime
	GetRealTimeL: => @rtime
	SysTime: => @stime
	GetSysTime: => @stime
	GetObject: => @obj
	GetNWObject: => @obj
	GetNetworkedObject: => @obj
	GetLength: => @rlen
	GetRealLength: => @len
	ChangedByNetwork: => @networkChange

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

do
	nullify = ->
		ProtectedCall ->
			for _, ent in ipairs ents.GetAll()
				ent.__PPM2_PonyData\Remove() if ent.__PPM2_PonyData

		for _, ent in ipairs ents.GetAll()
			ent.__PPM2_PonyData = nil

	nullify()
	timer.Simple 0, -> timer.Simple 0, -> timer.Simple 0, nullify
	hook.Add 'InitPostEntity', 'PPM2.FixSingleplayer', nullify

wUInt = (def = 0, size = 8) ->
	return (arg = def) -> net.WriteUInt(arg, size)

wInt = (def = 0, size = 8) ->
	return (arg = def) -> net.WriteInt(arg, size)

rUInt = (size = 8, min = 0, max = 255) ->
	return -> math.Clamp(net.ReadUInt(size), min, max)

rInt = (size = 8, min = -128, max = 127) ->
	return -> math.Clamp(net.ReadInt(size), min, max)

rFloat = (min = 0, max = 255) ->
	return -> math.Clamp(net.ReadFloat(), min, max)

wFloat = net.WriteFloat
rBool = net.ReadBool
wBool = net.WriteBool
rColor = net.ReadColor
wColor = net.WriteColor
rString = net.ReadString
wString = net.WriteString

class NetworkedPonyData extends PPM2.ModifierBase
	@AddNetworkVar = (getName = 'Var', readFunc = (->), writeFunc = (->), defValue, onSet = ((val) => val), networkByDefault = true) =>
		defFunc = defValue
		defFunc = (-> defValue) if type(defValue) ~= 'function'
		strName = "_NW_#{getName}"
		@NW_NextVarID += 1
		id = @NW_NextVarID
		tab = {:strName, :readFunc, :getName, :writeFunc, :defValue, :defFunc, :id, :onSet}
		table.insert(@NW_Vars, tab)
		@NW_VarsTable[id] = tab
		@__base[strName] = defFunc()

		@__base["Get#{getName}"] = => @[strName]
		@__base["Set#{getName}"] = (val = defFunc(), networkNow = networkByDefault) =>
			oldVal = @[strName]
			nevVal = onSet(@, val)
			@[strName] = nevVal
			state = PPM2.NetworkChangeState(strName, getName, nevVal, @)
			state.networkChange = false
			@SetLocalChange(state)
			return unless networkNow and @NETWORKED and (CLIENT and @ent == LocalPlayer() or SERVER)
			net.Start(@@NW_Modify)
			net.WriteUInt(@GetNetworkID(), 16)
			net.WriteUInt(id, 16)
			writeFunc(nevVal)
			net.SendToServer() if CLIENT
			net.Broadcast() if SERVER

	@NetworkVar = (...) => @AddNetworkVar(...)
	@GetSet = (fname, fvalue) =>
		@__base["Get#{fname}"] = => @[fvalue]
		@__base["Set#{fname}"] = (fnewValue = @[fvalue]) =>
			oldVal = @[fvalue]
			@[fvalue] = fnewValue
			state = PPM2.NetworkChangeState(fvalue, fname, fnewValue, @)
			state.networkChange = false
			@SetLocalChange(state)

	@NW_WAIT = {}

	if CLIENT
		hook.Add 'OnEntityCreated', 'PPM2.NW_WAIT', (ent) -> timer.Simple 0, ->
			return if not IsValid(ent)

			dirty = false
			entid = ent\EntIndex()
			ttl = RealTimeL()

			for controller in *@NW_WAIT
				if controller.removed
					controller.isNWWaiting = false
					dirty = true
				elseif controller.waitEntID == entid
					controller.isNWWaiting = false
					controller.ent = ent
					controller.modelCached = ent\GetModel()
					controller\SetupEntity(ent)
					--print('FOUND', ent)
					dirty = true
				elseif controller.waitTTL < ttl
					dirty = true
					controller.isNWWaiting = false

			if dirty
				@NW_WAIT = [controller for controller in *@NW_WAIT when controller.isNWWaiting]

		hook.Add 'NotifyShouldTransmit', 'PPM2.NW_WAIT', (ent, should) -> timer.Simple 0, ->
			return if not IsValid(ent)

			dirty = false
			entid = ent\EntIndex()
			ttl = RealTimeL()

			for controller in *@NW_WAIT
				if controller.removed
					controller.isNWWaiting = false
					dirty = true
				elseif controller.waitEntID == entid
					controller.isNWWaiting = false
					controller.ent = ent
					controller.modelCached = ent\GetModel()
					controller\SetupEntity(ent)
					--print('FOUND', ent)
					dirty = true
				elseif controller.waitTTL < ttl
					dirty = true
					controller.isNWWaiting = false

			if dirty
				@NW_WAIT = [controller for controller in *@NW_WAIT when controller.isNWWaiting]

	@OnNetworkedCreated = (ply = NULL, len = 0, nwobj) =>
		if CLIENT
			netID = net.ReadUInt16()
			entid = net.ReadUInt16()
			obj = @NW_Objects[netID] or @(netID, entid)
			obj.NETWORKED = true
			obj.CREATED_BY_SERVER = true
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
		else
			ply[@NW_CooldownTimer] = ply[@NW_CooldownTimer] or 0
			ply[@NW_CooldownTimerCount] = ply[@NW_CooldownTimerCount] or 0

			if ply[@NW_CooldownTimer] < RealTimeL()
				ply[@NW_CooldownTimerCount] = 1
				ply[@NW_CooldownTimer] = RealTimeL() + 10
			else
				ply[@NW_CooldownTimerCount] += 1

			if ply[@NW_CooldownTimerCount] >= 3
				ply[@NW_CooldownMessage] = ply[@NW_CooldownMessage] or 0
				if ply[@NW_CooldownMessage] < RealTimeL()
					PPM2.Message 'Player ', ply, " is creating #{@__name} too quickly!"
					ply[@NW_CooldownMessage] = RealTimeL() + 1
				return

			waitID = net.ReadUInt16()
			obj = @(nil, ply)
			obj.NETWORKED_PREDICT = true
			obj\ReadNetworkData()
			obj\Create()
			timer.Simple 0.5, ->
				return if not IsValid(ply)
				net.Start(@NW_ReceiveID)
				net.WriteUInt(waitID, 16)
				net.WriteUInt(obj.netID, 16)
				net.Send(ply)

	@OnNetworkedModify = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]

		unless obj
			return if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		if IsValid(ply) and obj.ent ~= ply
			error('Invalid realm for player being specified. If you are running on your own net.* library, check up your code') if CLIENT
			net.Start(@NW_Rejected)
			net.WriteUInt(id, 16)
			net.Send(ply)
			return

		varID = net.ReadUInt16()
		varData = @NW_VarsTable[varID]
		return unless varData
		{:strName, :getName, :readFunc, :writeFunc, :onSet} = varData
		newVal = onSet(obj, readFunc())
		return if newVal == obj["Get#{getName}"](obj)
		state = PPM2.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, 16)
			writeFunc(newVal)
			net.SendOmit(ply)

	@OnNetworkedDelete = (ply = NULL, len = 0) =>
		id = net.ReadUInt16()
		obj = @NW_Objects[id]
		return unless obj
		obj\Remove(true)

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

	@RenderTasks = {}
	@CheckTasks = {}

	@NW_Vars = {}
	@NW_VarsTable = {}
	@NW_Objects = {}
	@O_Slots = {} if CLIENT
	@NW_Waiting = {}
	@NW_WaitID = -1
	@NW_NextVarID = -1
	@NW_Create = 'PPM2.NW.Created'
	@NW_Modify = 'PPM2.NW.Modified'
	@NW_Broadcast = 'PPM2.NW.ModifiedBroadcast'
	@NW_Remove = 'PPM2.NW.Removed'
	@NW_Rejected = 'PPM2.NW.Rejected'
	@NW_ReceiveID = 'PPM2.NW.ReceiveID'
	@NW_CooldownTimerCount = 'ppm2_NW_CooldownTimerCount'
	@NW_CooldownTimer = 'ppm2_NW_CooldownTimer'
	@NW_CooldownMessage = 'ppm2_NW_CooldownMessage'
	@NW_NextObjectID = 0
	@NW_NextObjectID_CL = 0x60000

	if SERVER
		net.pool(@NW_Create)
		net.pool(@NW_Modify)
		net.pool(@NW_Remove)
		net.pool(@NW_ReceiveID)
		net.pool(@NW_Rejected)
		net.pool(@NW_Broadcast)

	net.Receive @NW_Create, (len = 0, ply = NULL, obj) -> @OnNetworkedCreated(ply, len, obj)
	net.Receive @NW_Modify, (len = 0, ply = NULL, obj) -> @OnNetworkedModify(ply, len, obj)
	net.Receive @NW_Remove, (len = 0, ply = NULL, obj) -> @OnNetworkedDelete(ply, len, obj)

	net.Receive @NW_ReceiveID, (len = 0, ply = NULL) ->
		return if SERVER
		waitID = net.ReadUInt16()
		netID = net.ReadUInt16()
		obj = @NW_Waiting[waitID]
		@NW_Waiting[waitID] = nil
		return unless obj
		obj.NETWORKED = true
		@NW_Objects[obj.netID] = nil
		obj.netID = netID
		obj.waitID = nil
		@NW_Objects[netID] = obj

	net.Receive @NW_Rejected, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		return if obj.__LastReject and obj.__LastReject > RealTimeL()
		obj.__LastReject = RealTimeL() + 3
		obj.NETWORKED = false
		obj\Create()

	net.Receive @NW_Broadcast, (len = 0, ply = NULL) ->
		return if SERVER
		netID = net.ReadUInt16()
		obj = @NW_Objects[netID]
		return unless obj
		obj\ReadNetworkData(len, ply)

	@GetSet('UpperManeModel', 'm_upperManeModel')
	@GetSet('LowerManeModel', 'm_lowerManeModel')
	@GetSet('TailModel', 'm_tailModel')
	@GetSet('SocksModel', 'm_socksModel')
	@GetSet('NewSocksModel', 'm_newSocksModel')

	@NetworkVar('Fly',                  rBool,   wBool,                 false)
	@NetworkVar('DisableTask',          rBool,   wBool,                 false)
	@NetworkVar('UseFlexLerp',          rBool,   wBool,                  true)
	@NetworkVar('FlexLerpMultiplier',   rFloat(0, 10),  wFloat,             1)

	@SetupModifiers: =>
		for key, value in pairs PPM2.PonyDataRegistry
			if value.modifiers
				@RegisterModifier(value.getFunc, 0, 0)
				@SetModifierMinMaxFinal(value.getFunc, value.min, value.max) if value.min or value.max
				@SetupLerpTables(value.getFunc)
				strName = '_NW_' .. value.getFunc
				funcLerp = 'Calculate' .. value.getFunc
				@__base['Get' .. value.getFunc] = => @[funcLerp](@, @[strName])

	for key, value in pairs PPM2.PonyDataRegistry
		@NetworkVar(value.getFunc, value.read, value.write, value.default)

	new: (netID, ent) =>
		super()

		@m_upperManeModel = Entity(-1)
		@m_lowerManeModel = Entity(-1)
		@m_tailModel = Entity(-1)
		@m_socksModel = Entity(-1)
		@m_newSocksModel = Entity(-1)

		@recomputeTextures = true
		@isValid = true
		@removed = false
		@valid = true
		@NETWORKED = false
		@NETWORKED_PREDICT = false

		@[data.strName] = data.defFunc() for _, data in ipairs @@NW_Vars when data.defFunc

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

		@@NW_Objects[@netID] = @

		if CLIENT
			for i = 1, 1024
				if not @@O_Slots[i]
					@slotID = i
					@@O_Slots[i] = @
					break

			if not @slotID
				error('dafuq? No empty slots are available')

		@isNWWaiting = false

		if type(ent) == 'number'
			entid = ent
			@waitEntID = entid
			ent = Entity(entid)
			--print(ent, entid)

			if not IsValid(ent)
				@isNWWaiting = true
				@waitTTL = RealTimeL() + 60
				table.insert(@@NW_WAIT, @)
				PPM2.LMessage('message.ppm2.debug.race_condition')
				--print('WAITING ON ', entid)
				return

		return if not IsValid(ent)
		@ent = ent
		@modelCached = ent\GetModel() if IsValid(ent)
		@SetupEntity(ent)

	GetEntity: => @ent
	IsValid: => @isValid
	GetModel: => @modelCached
	EntIndex: => @entID
	ObjectSlot: => @slotID
	GetObjectSlot: => @slotID

	Clone: (target = @ent) =>
		copy = @@(nil, target)
		@ApplyDataToObject(copy)
		return copy

	SetupEntity: (ent) =>
		if ent.__PPM2_PonyData
			return if ent.__PPM2_PonyData\GetOwner() and IsValid(ent.__PPM2_PonyData\GetOwner()) and ent.__PPM2_PonyData\GetOwner() ~= @GetOwner()
			ent.__PPM2_PonyData\Remove() if ent.__PPM2_PonyData.Remove and ent.__PPM2_PonyData ~= @

		ent.__PPM2_PonyData = @
		@entTable = @ent\GetTable()
		return unless IsValid(ent)
		@modelCached = ent\GetModel()
		ent\PPMBonesModifier()
		@flightController = PPM2.PonyflyController(@)
		@entID = ent\EntIndex()
		@lastLerpThink = RealTimeL()
		@ModelChanges(@modelCached, @modelCached)
		@Reset()
		timer.Simple(0, -> @GetRenderController()\CompileTextures() if @GetRenderController()) if CLIENT
		PPM2.DebugPrint('Ponydata ', @, ' was updated to use for ', @ent)
		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	ModelChanges: (old = @ent\GetModel(), new = old) =>
		@modelCached = new
		@SetFly(false) if SERVER
		timer.Simple 0.5, ->
			return unless IsValid(@ent)
			@Reset()

	GenericDataChange: (state) =>
		hook.Run 'PPM2_PonyDataChanges', @ent, @, state

		if state\GetKey() == 'Fly' and @flightController
			@flightController\Switch(state\GetValue())

		if state\GetKey() == 'DisableTask'
			@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
			@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

		@GetSizeController()\DataChanges(state) if @ent and @GetBodygroupController()
		@GetBodygroupController()\DataChanges(state) if @ent and @GetBodygroupController()

		@GetWeightController()\DataChanges(state) if @ent and @GetWeightController()

		if CLIENT and @ent
			@GetRenderController()\DataChanges(state) if @GetRenderController()

	ResetScale: =>
		if scale = @GetSizeController()
			scale\ResetScale()

	ModifyScale: =>
		if scale = @GetSizeController()
			scale\ModifyScale()

	Reset: =>
		if scale = @GetSizeController()
			scale\Reset()
		if CLIENT
			@GetWeightController()\Reset() if @GetWeightController() and @GetWeightController().Reset
			@GetRenderController()\Reset() if @GetRenderController() and @GetRenderController().Reset
			@GetBodygroupController()\Reset() if @GetBodygroupController() and @GetBodygroupController().Reset

	GetHoofstepVolume: =>
		return 0.8 if @ShouldMuffleHoosteps()
		return 1

	ShouldMuffleHoosteps: =>
		return @GetSocksAsModel() or @GetSocksAsNewModel()

	PlayerRespawn: =>
		return if not IsValid(@ent)
		@entTable.__cachedIsPony = @ent\IsPony()
		if not @entTable.__cachedIsPony
			return if @alreadyCalledRespawn
			@alreadyCalledRespawn = true
			@alreadyCalledDeath = true
		else
			@alreadyCalledRespawn = false
			@alreadyCalledDeath = false
		@ApplyBodygroups(CLIENT, true)
		@SetFly(false) if SERVER

		@ent\PPMBonesModifier()

		if scale = @GetSizeController()
			scale\PlayerRespawn()

		if weight = @GetWeightController()
			weight\PlayerRespawn()

		if CLIENT
			@deathRagdollMerged = false
			@GetRenderController()\PlayerRespawn() if @GetRenderController()
			@GetBodygroupController()\MergeModels(@ent) if IsValid(@ent) and @GetBodygroupController().MergeModels

	PlayerDeath: =>
		return if not IsValid(@ent)

		if @ent.__ppmBonesModifiers
			@ent.__ppmBonesModifiers\Remove()

		@entTable.__cachedIsPony = @ent\IsPony()

		if not @entTable.__cachedIsPony
			return if @alreadyCalledDeath
			@alreadyCalledDeath = true
		else
			@alreadyCalledDeath = false

		@SetFly(false) if SERVER

		if scale = @GetSizeController()
			scale\PlayerDeath()

		if weight = @GetWeightController()
			weight\PlayerDeath()

		if CLIENT
			@DoRagdollMerge()
			@GetRenderController()\PlayerDeath() if @GetRenderController()

	DoRagdollMerge: =>
		return if @deathRagdollMerged
		bgController = @GetBodygroupController()
		rag = @ent\GetRagdollEntity()

		if not bgController.MergeModels
			@deathRagdollMerged = true
		elseif IsValid(rag)
			@deathRagdollMerged = true
			bgController\MergeModels(rag)

	ApplyBodygroups: (updateModels = CLIENT) => @GetBodygroupController()\ApplyBodygroups(updateModels) if @ent
	SetLocalChange: (state) => @GenericDataChange(state)
	NetworkDataChanges: (state) => @GenericDataChange(state)

	GetPonyRaceFlags: =>
		switch @GetRace()
			when PPM2.RACE_EARTH
				return 0
			when PPM2.RACE_PEGASUS
				return PPM2.RACE_HAS_WINGS
			when PPM2.RACE_UNICORN
				return PPM2.RACE_HAS_HORN
			when PPM2.RACE_ALICORN
				return PPM2.RACE_HAS_HORN + PPM2.RACE_HAS_WINGS

	SlowUpdate: =>
		@GetBodygroupController()\SlowUpdate() if @GetBodygroupController()
		@GetWeightController()\SlowUpdate() if @GetWeightController()
		if scale = @GetSizeController()
			scale\SlowUpdate()

		if SERVER and IsValid(@ent) and @ent\IsPlayer()
			arms = @ent\GetHands()

			if IsValid(arms)
				cond = @GetPonyRaceFlags()\band(PPM2.RACE_HAS_HORN) ~= 0 and @ent\GetInfoBool('ppm2_cl_vm_magic_hands', true) and PPM2.HAND_BODYGROUP_MAGIC or PPM2.HAND_BODYGROUP_HOOVES
				arms\SetBodygroup(PPM2.HAND_BODYGROUP_ID, cond)

	Think: =>
	RenderScreenspaceEffects: =>
		time = RealTimeL()
		delta = time - @lastLerpThink
		@lastLerpThink = time
		if @isValid and IsValid(@ent)
			for _, change in ipairs @TriggerLerpAll(delta * 5)
				state = PPM2.NetworkChangeState('_NW_' .. change[1], change[1], change[2] + @['_NW_' .. change[1]], @)
				state\SetCantApply(true)
				@GenericDataChange(state)

	GetFlightController: => @flightController

	GetRenderController: =>
		return if SERVER
		return @renderController if not @isValid
		if not @renderController or @modelCached ~= @modelRender
			@modelRender = @modelCached
			cls = PPM2.GetRenderController(@modelCached)
			if @renderController and cls == @renderController.__class
				@renderController.ent = @ent
				PPM2.DebugPrint('Skipping render controller recreation for ', @ent, ' as part of ', @)
				return @renderController
			@renderController\Remove() if @renderController
			@renderController = cls(@)
		@renderController.ent = @ent
		return @renderController

	GetWeightController: =>
		return @weightController if not @isValid
		if not @weightController or @modelCached ~= @modelWeight
			@modelCached = @modelCached or @ent\GetModel()
			@modelWeight = @modelCached
			cls = PPM2.GetPonyWeightController(@modelCached)
			if @weightController and cls == @weightController.__class
				@weightController.ent = @ent
				PPM2.DebugPrint('Skipping weight controller recreation for ', @ent, ' as part of ', @)
				return @weightController
			@weightController\Remove() if @weightController
			@weightController = cls(@)
		@weightController.ent = @ent
		return @weightController

	GetSizeController: =>
		return @scaleController if not @isValid
		if not @scaleController or @modelCached ~= @modelScale
			@modelCached = @modelCached or @ent\GetModel()
			@modelScale = @modelCached
			cls = PPM2.GetSizeController(@modelCached)
			if @scaleController and cls == @scaleController.__class
				@scaleController.ent = @ent
				PPM2.DebugPrint('Skipping size controller recreation for ', @ent, ' as part of ', @)
				return @scaleController
			@scaleController\Remove() if @scaleController
			@scaleController = cls(@)
		@scaleController.ent = @ent
		return @scaleController
	GetScaleController: => @GetSizeController()

	GetBodygroupController: =>
		return @bodygroups if not @isValid
		if not @bodygroups or @modelBodygroups ~= @modelCached
			@modelCached = @modelCached or @ent\GetModel()
			@modelBodygroups = @modelCached
			cls = PPM2.GetBodygroupController(@modelCached)
			if @bodygroups and cls == @bodygroups.__class
				@bodygroups.ent = @ent
				PPM2.DebugPrint('Skipping bodygroup controller recreation for ', @ent, ' as part of ', @)
				return @bodygroups
			@bodygroups\Remove() if @bodygroups
			@bodygroups = cls(@)
		@bodygroups.ent = @ent
		return @bodygroups

	Remove: (byClient = false) =>
		@removed = true
		@@NW_Objects[@netID] = nil if SERVER or @NETWORKED
		@@O_Slots[@slotID] = nil if @slotID
		@isValid = false
		@ent = @GetEntity() if not IsValid(@ent)
		@entTable.__PPM2_PonyData = nil if IsValid(@ent) and @ent.__PPM2_PonyData == @
		@GetWeightController()\Remove() if @GetWeightController()

		if CLIENT
			@GetRenderController()\Remove() if @GetRenderController()
			if IsValid(@ent) and @ent.__ppm2_task_hit
				@entTable.__ppm2_task_hit = false
				@ent\SetNoDraw(false)
		@GetBodygroupController()\Remove() if @GetBodygroupController()
		@GetSizeController()\Remove() if @GetSizeController()
		@flightController\Switch(false) if @flightController
		@@RenderTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task.ent\IsPlayer() and not task\GetDisableTask()]
		@@CheckTasks = [task for i, task in pairs @@NW_Objects when task\IsValid() and IsValid(task.ent) and not task\GetDisableTask()]

	__tostring: => "[#{@@__name}:#{@netID}|#{@ent}]"

	GetOwner: => @ent
	IsNetworked: => @NETWORKED
	IsGoingToNetwork: => @NETWORKED_PREDICT
	SetIsGoingToNetwork: (val = @NETWORKED) => @NETWORKED_PREDICT = val
	IsLocal: => @isLocal
	IsLocalObject: => @isLocal
	GetNetworkID: => @netID
	NetworkID: => @netID
	NetID: => @netID

	ComputeMagicColor: =>
		color = @GetHornMagicColor()

		if not @GetSeparateMagicColor()
			if not @GetSeparateEyes()
				color = @GetEyeIrisTop()\Lerp(0.5, @GetEyeIrisBottom())
			else
				lerpLeft = @GetEyeIrisTopLeft()\Lerp(0.5, @GetEyeIrisBottomLeft())
				lerpRight = @GetEyeIrisTopRight()\Lerp(0.5, @GetEyeIrisBottomRight())
				color = lerpLeft\Lerp(0.5, lerpRight)

		return color

	ReadNetworkData: (len = 24, ply = NULL, silent = false, applyEntities = true) =>
		data = @@ReadNetworkData()
		validPly = IsValid(ply)
		states = [PPM2.NetworkChangeState(key, keyValid, newVal, @, len, ply) for key, {keyValid, newVal} in pairs data]
		for _, state in ipairs states
			if not validPly or applyEntities or not isentity(state\GetValue())
				state\Apply()
				@NetworkDataChanges(state) unless silent

	NetworkedIterable: (grabEntities = true) =>
		data = [{getName, @[strName]} for _, {:strName, :getName} in ipairs @@NW_Vars when grabEntities or not isentity(@[strName])]
		return data

	ApplyDataToObject: (target, applyEntities = false) =>
		target["Set#{key}"](target, value) for {key, value} in *@NetworkedIterable(applyEntities) when target["Set#{key}"]
		return target

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

	ReBroadcast: =>
		return false if not @NETWORKED
		return false if CLIENT
		net.Start(@@NW_Broadcast)
		net.WriteUInt16(@netID)
		@WriteNetworkData()
		net.Broadcast()
		return true

	Create: =>
		return if @NETWORKED
		return if CLIENT and @CREATED_BY_SERVER -- wtf
		@NETWORKED = true if SERVER
		@NETWORKED_PREDICT = true

		if SERVER
			net.Start(@@NW_Create)
			net.WriteUInt16(@netID)
			net.WriteEntity(@ent)
			@WriteNetworkData()
			filter = RecipientFilter()
			filter\AddAllPlayers()
			filter\RemovePlayer(@ent) if IsValid(@ent) and @ent\IsPlayer()
			net.Send(filter)
		else
			@@NW_WaitID += 1
			@waitID = @@NW_WaitID

			net.Start(@@NW_Create)
			before = net.BytesWritten()

			net.WriteUInt16(@waitID)
			@WriteNetworkData()
			after = net.BytesWritten()

			net.SendToServer()
			@@NW_Waiting[@waitID] = @
			return after - before

	NetworkTo: (targets = {}) =>
		net.Start(@@NW_Create)
		net.WriteUInt16(@netID)
		net.WriteEntity(@ent)
		@WriteNetworkData()
		net.Send(targets)

PPM2.NetworkedPonyData = NetworkedPonyData

if CLIENT
	net.Receive 'PPM2.NotifyDisconnect', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()

	net.Receive 'PPM2.PonyDataRemove', ->
		netID = net.ReadUInt16()
		data = NetworkedPonyData.NW_Objects[netID]
		return if not data
		data\Remove()
else
	hook.Add 'PlayerJoinTeam', 'PPM2.TeamWaypoint', (ply, new) ->
		ply.__ppm2_modified_jump = false
	hook.Add 'OnPlayerChangedTeam', 'PPM2.TeamWaypoint', (ply, old, new) ->
		ply.__ppm2_modified_jump = false

_G.LocalPonyData = () -> LocalPlayer()\GetPonyData()
_G.LocalPonydata = () -> LocalPlayer()\GetPonyData()

entMeta = FindMetaTable('Entity')
entMeta.GetPonyData = =>
	self2 = @
	self = entMeta.GetTable(@)
	return if not @
	if @__PPM2_PonyData and @__PPM2_PonyData.ent ~= self2
		@__PPM2_PonyData.ent = self2
		@__PPM2_PonyData\SetupEntity(self2) if CLIENT
	return @__PPM2_PonyData


improved mixin because of new purecss requirements

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"

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


googleapi: add image search

googleapi: add image search

-- ported from luabot's google module. Thanks!
html2unicode = require'html'

imagesearch = (source, destination, term, num=1) =>
    data, err = ivar2.util.simplehttp 'http://ajax.googleapis.com/ajax/services/search/images?safe=off&v=1.0&q='..ivar2.util.urlEncode(term), (data) ->
      res = ivar2.util.json.decode(data)
      results = res.responseData.results

      total = #results

      if num >= total or num < 0
          say "Couldn't find anything matching \002#{term}\002."
          return

      url = results[num]['unescapedUrl']
      say "(#{num} of #{total}) #{url}"

PRIVMSG:
  '^%pimage (.+)': imagesearch
  '^%pimage (.+) ([0-9]+)': imagesearch
  '^%pg (.+)': (source, destination, query) =>
    data, err = ivar2.util.simplehttp 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q='..ivar2.util.urlEncode(query), (data) ->

      if query == 'google'
        return reply "wow. That's really clever."

      unless data
        return reply "no response"

      if data\sub(1, 1) ~= '{' then
        return false, 'invalid reply?'

      resp = ivar2.util.json.decode(data)
      unless resp.responseData
        return reply 'no response data'

      res = resp.responseData.results
      if res[1]
        say(html2unicode(res[1].titleNoFormatting)..': '..res[1].unescapedUrl)
      else
        return reply 'no results'

-- ported from luabot's google module. Thanks!
html2unicode = require'html'

PRIVMSG:
  '^%pg (.+)': (source, destination, query) =>
    data, err = ivar2.util.simplehttp 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q='..ivar2.util.urlEncode(query), (data) ->

      if query == 'google'
        return reply "wow. That's really clever."

      unless data
        return reply "no response"

      if data\sub(1, 1) ~= '{' then
        return false, 'invalid reply?'

      resp = ivar2.util.json.decode(data)
      unless resp.responseData
        return reply 'no response data'

      res = resp.responseData.results
      if res[1]
        say(html2unicode(res[1].titleNoFormatting)..': '..res[1].unescapedUrl)
      else
        return reply 'no results'


Fixed details are not present on socks

Fixed details are not present on socks

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

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

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

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

PPM2.USE_HIGHRES_BODY = CreateConVar('ppm2_cl_hires_body', '0', {FCVAR_ARCHIVE}, 'Use high resoluation when rendering pony bodies. AFFECTS ONLY TEXTURE COMPILATION TIME (increases lag spike on pony data load)')
PPM2.USE_HIGHRES_TEXTURES = CreateConVar('ppm2_cl_hires_generic', '0', {FCVAR_ARCHIVE}, 'Create 1024x1024 textures instead of 512x512 on texture compiling')

RELOADABLE_MATERIALS = {}
PPM2.RELOADABLE_MATERIALS = RELOADABLE_MATERIALS
concommand.Add 'ppm2_reload_materials', ->
	cTime = SysTime()
	for _, mat in ipairs RELOADABLE_MATERIALS
		if texname = mat\GetString('$basetexture')
			mat\SetTexture('$basetexture', texname)
		if texture = mat\GetTexture('$basetexture')
			texture\Download()
		mat\Recompute()
	PPM2.PonyTextureController.URL_MATERIAL_CACHE = {}
	PPM2.PonyTextureController.SessionID = math.random(1, 1000)
	PPM2.Message('Reloaded textures in ', math.floor((SysTime() - cTime) * 100000) / 100, ' milliseconds.')
	RunConsoleCommand('ppm2_reload')
	RunConsoleCommand('ppm2_require')

_Material = Material
_CreateMaterial = CreateMaterial
Material = (path) ->
	matNew, time = _Material(path)
	table.insert(RELOADABLE_MATERIALS, matNew)
	return matNew, time

CreateMaterial = (name, shader, data) ->
	matNew, time = _CreateMaterial(name, shader, data)
	table.insert(RELOADABLE_MATERIALS, matNew)
	return matNew, time

module = {
	BODY_DETAILS: {
		Material('models/ppm2/partrender/body_leggrad1.png')
		Material('models/ppm2/partrender/body_lines1.png')
		Material('models/ppm2/partrender/body_stripes1.png')
		Material('models/ppm2/partrender/body_headstripes1.png')
		Material('models/ppm2/partrender/body_freckles.png')
		Material('models/ppm2/partrender/body_hooves1.png')
		Material('models/ppm2/partrender/body_hooves2.png')
		Material('models/ppm2/partrender/body_headmask1.png')
		Material('models/ppm2/partrender/body_hooves1_crit.png')
		Material('models/ppm2/partrender/body_hooves2_crit.png')
		Material('models/ppm2/partrender/body_spots1.png')
		Material('models/ppm2/partrender/body_robotic.png')
		Material('models/ppm2/partrender/dash-e.png')
		Material('models/ppm2/partrender/eye_scar.png')
		Material('models/ppm2/partrender/eye_wound.png')
		Material('models/ppm2/partrender/body_scar.png')
		Material('models/ppm2/partrender/gear_socks.png')
		Material('models/ppm2/partrender/sharp_hooves.png')
		Material('models/ppm2/partrender/sharp_hooves2.png')
		Material('models/ppm2/partrender/separated_muzzle.png')
		Material('models/ppm2/partrender/eye_scar_left.png')
		Material('models/ppm2/partrender/eye_scar_right.png')
	}

	UPPER_MANE_DETAILS: {
		[4]: {Material('models/ppm2/partrender/upmane_5_mask0.png')}
		[5]: {Material('models/ppm2/partrender/upmane_6_mask0.png')}
		[7]: {Material('models/ppm2/partrender/upmane_8_mask0.png'), Material('models/ppm2/partrender/upmane_8_mask1.png')}
		[8]: {Material('models/ppm2/partrender/upmane_9_mask0.png'), Material('models/ppm2/partrender/upmane_9_mask1.png'), Material('models/ppm2/partrender/upmane_9_mask2.png')}
		[9]: {Material('models/ppm2/partrender/upmane_10_mask0.png')}
		[10]: {Material('models/ppm2/partrender/upmane_11_mask0.png'), Material('models/ppm2/partrender/upmane_11_mask1.png'), Material('models/ppm2/partrender/upmane_11_mask2.png')}
		[11]: {Material('models/ppm2/partrender/upmane_12_mask0.png')}
		[12]: {Material('models/ppm2/partrender/upmane_13_mask0.png')}
		[13]: {Material('models/ppm2/partrender/upmane_14_mask0.png')}
		[14]: {Material('models/ppm2/partrender/upmane_15_mask0.png')}
	}

	LOWER_MANE_DETAILS: {
		[4]: {Material('models/ppm2/partrender/dnmane_5_mask0.png')}
		[7]: {Material('models/ppm2/partrender/dnmane_8_mask0.png'), Material('models/ppm2/partrender/dnmane_8_mask1.png')}
		[8]: {Material('models/ppm2/partrender/dnmane_9_mask0.png'), Material('models/ppm2/partrender/dnmane_9_mask1.png')}
		[9]: {Material('models/ppm2/partrender/dnmane_10_mask0.png'), Material('models/ppm2/partrender/dnmane_10_mask1.png'), Material('models/ppm2/partrender/dnmane_10_mask2.png')}
		[10]: {Material('models/ppm2/partrender/dnmane_11_mask0.png'), Material('models/ppm2/partrender/dnmane_11_mask1.png')}
		[11]: {Material('models/ppm2/partrender/dnmane_12_mask0.png')}
	}

	TAIL_DETAILS: {
		[4]: {Material('models/ppm2/partrender/tail_5_mask0.png')}
		[7]: {Material('models/ppm2/partrender/tail_8_mask0.png'), Material('models/ppm2/partrender/tail_8_mask1.png'), Material('models/ppm2/partrender/tail_8_mask2.png'), Material('models/ppm2/partrender/tail_8_mask3.png'), Material('models/ppm2/partrender/tail_8_mask4.png')}
		[9]: {Material('models/ppm2/partrender/tail_10_mask0.png')}
		[10]: {Material('models/ppm2/partrender/tail_11_mask0.png'), Material('models/ppm2/partrender/tail_11_mask1.png'), Material('models/ppm2/partrender/tail_11_mask2.png')}
		[11]: {Material('models/ppm2/partrender/tail_12_mask0.png'), Material('models/ppm2/partrender/tail_12_mask1.png')}
		[12]: {Material('models/ppm2/partrender/tail_13_mask0.png')}
		[13]: {Material('models/ppm2/partrender/tail_14_mask0.png')}
	}

	SOCKS_PATCHS: {
		'models/props_pony/ppm/ppm_socks/socks_striped_unlit'
		'models/props_pony/ppm2/ppm_socks/custom/geometric1_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric2_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric3_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric4_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric5_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric6_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric7_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric8_1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/dark1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers10.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers11.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers12.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers13.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers14.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers15.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers16.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers17.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers18.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers19.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers20.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers3.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers4.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers5.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers6.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers7.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers8.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers9.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/grey1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/grey2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/grey3.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/hearts1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/hearts2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/snow1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/wallpaper1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/wallpaper2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/wallpaper3.png'
	}

	SOCKS_DETAILS_PATCHS: {
		[2]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric1_4.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric1_5.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric1_6.png'
		}

		[3]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric2_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric2_4.png'
		}

		[4]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric3_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric3_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric3_5.png'
		}

		[5]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric4_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric4_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric4_4.png'
		}

		[6]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric5_4.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric5_5.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric5_6.png'
		}

		[7]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric6_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric6_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric6_4.png'
		}

		[8]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric7_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric7_4.png'
		}

		[9]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric8_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric8_3.png'
		}
	}
}

additionTable = (...) ->
	tab = {'$ignorez': 1, '$vertexcolor': 1, '$vertexalpha': 1, '$nolod': 1}
	args = {...}
	for i = 1, #args, 2
		key, val = args[i], args[i + 1]
		tab[key] = val
	return tab

module.SOCKS_MATERIALS = [Material(id) for _, id in ipairs module.SOCKS_PATCHS]
module.SOCKS_DETAILS = {i, [Material(path) for path in *data] for i, data in pairs module.SOCKS_DETAILS_PATCHS}
module.CUTIEMARKS = [Material("models/ppm2/cmarks/#{mark}.png") for _, mark in ipairs PPM2.DefaultCutiemarks]
module.SUITS = [Material("models/ppm2/texclothes/#{mat}.png") for _, mat in ipairs {
	'clothes_royalguard', 'clothes_sbs_full'
	'clothes_sbs_light', 'clothes_wbs_full'
	'clothes_wbs_light', 'spidermane_light'
	'spidermane_full'
}]

module.TATTOOS = [Material("models/ppm2/partrender/tattoo/#{fil\lower()}.png") for _, fil in ipairs PPM2.TATTOOS_REGISTRY when fil ~= 'NONE']
debugwhite = {
	'$basetexture': 'models/debug/debugwhite'
	'$ignorez': 1
	'$vertexcolor': 1
	'$vertexalpha': 1
	'$nolod': 1
}

module.EYE_OVALS = {
	Material('models/ppm2/partrender/eye_oval.png')
	Material('models/ppm2/partrender/eye_oval_aperture.png')
}

module.EYE_REFLECTIONS = {
	Material('models/ppm2/partrender/eye_reflection.png')
	Material('models/ppm2/partrender/eye_reflection_crystal_foal.png')
	Material('models/ppm2/partrender/eye_reflection_crystal_unisex.png')
	Material('models/ppm2/partrender/eye_reflection_foal.png')
	Material('models/ppm2/partrender/eye_reflection_male.png')
}

module.DEBUGWHITE = CreateMaterial('PPM2.Debugwhite', 'UnlitGeneric', debugwhite)
module.HAIR_MATERIAL_COLOR = CreateMaterial('PPM2.ManeTextureBase', 'UnlitGeneric', debugwhite)
module.TAIL_MATERIAL_COLOR = CreateMaterial('PPM2.TailTextureBase', 'UnlitGeneric', debugwhite)
module.WINGS_MATERIAL_COLOR = CreateMaterial('PPM2.WingsMaterialBase', 'UnlitGeneric', debugwhite)
module.HORN_MATERIAL_COLOR = CreateMaterial('PPM2.HornMaterialBase', 'UnlitGeneric', additionTable('$basetexture', 'models/ppm2/base/horn'))
module.BODY_MATERIAL = CreateMaterial('PPM2.BodyTexture', 'UnlitGeneric', additionTable('$basetexture', 'models/ppm2/base/body'))
module.HORN_DETAIL_BUMP = CreateMaterial('PPM2.HornBumpMapRenderer', 'UnlitGeneric', additionTable('$basetexture', 'models/ppm2/base/horn_normal'))
module.HORN_DETAIL_COLOR = Material('models/ppm2/partrender/horn_detail.png')
module.EYE_OVAL = Material('models/ppm2/partrender/eye_oval.png')
module.EYE_GRAD = Material('models/ppm2/partrender/eye_grad.png')
module.EYE_EFFECT = Material('models/ppm2/partrender/eye_effect.png')
module.EYE_LINE_L_1 = Material('models/ppm2/partrender/eye_line_l1.png')
module.EYE_LINE_R_1 = Material('models/ppm2/partrender/eye_line_r1.png')
module.EYE_LINE_L_2 = Material('models/ppm2/partrender/eye_line_l2.png')
module.EYE_LINE_R_2 = Material('models/ppm2/partrender/eye_line_r2.png')
module.EYEBROWS = Material('models/ppm2/partrender/eyebrows.png')
module.PONY_SOCKS = Material('models/ppm2/texclothes/pony_socks.png')

module.LIPS = Material('models/ppm2/partrender/lips.png')
module.NOSE = Material('models/ppm2/partrender/nose.png')

module.EYE_CORNERA = Material('models/ppm2/eyes/eye_cornea')
module.EYE_CORNERA_OVAL = Material('models/ppm2/eyes/eye_cornea_oval')
module.EYE_EXTRA = Material('models/ppm2/eyes/eye_extra')
module.EYE_EXTRA2 = Material('models/ppm2/eyes/eye_extra2')
module.EYE_LIGHTWARP = Material('models/ppm2/eyes/eye_lightwarp')
module.EYE_REFLECTION2 = Material('models/ppm2/eyes/eye_reflection')

PPM2.MaterialsRegistry = module
return module

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

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

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

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

PPM2.USE_HIGHRES_BODY = CreateConVar('ppm2_cl_hires_body', '0', {FCVAR_ARCHIVE}, 'Use high resoluation when rendering pony bodies. AFFECTS ONLY TEXTURE COMPILATION TIME (increases lag spike on pony data load)')
PPM2.USE_HIGHRES_TEXTURES = CreateConVar('ppm2_cl_hires_generic', '0', {FCVAR_ARCHIVE}, 'Create 1024x1024 textures instead of 512x512 on texture compiling')

RELOADABLE_MATERIALS = {}
PPM2.RELOADABLE_MATERIALS = RELOADABLE_MATERIALS
concommand.Add 'ppm2_reload_materials', ->
	cTime = SysTime()
	for _, mat in ipairs RELOADABLE_MATERIALS
		if texname = mat\GetString('$basetexture')
			mat\SetTexture('$basetexture', texname)
		if texture = mat\GetTexture('$basetexture')
			texture\Download()
		mat\Recompute()
	PPM2.PonyTextureController.URL_MATERIAL_CACHE = {}
	PPM2.PonyTextureController.SessionID = math.random(1, 1000)
	PPM2.Message('Reloaded textures in ', math.floor((SysTime() - cTime) * 100000) / 100, ' milliseconds.')
	RunConsoleCommand('ppm2_reload')
	RunConsoleCommand('ppm2_require')

_Material = Material
_CreateMaterial = CreateMaterial
Material = (path) ->
	matNew, time = _Material(path)
	table.insert(RELOADABLE_MATERIALS, matNew)
	return matNew, time

CreateMaterial = (name, shader, data) ->
	matNew, time = _CreateMaterial(name, shader, data)
	table.insert(RELOADABLE_MATERIALS, matNew)
	return matNew, time

module = {
	BODY_DETAILS: {
		Material('models/ppm2/partrender/body_leggrad1.png')
		Material('models/ppm2/partrender/body_lines1.png')
		Material('models/ppm2/partrender/body_stripes1.png')
		Material('models/ppm2/partrender/body_headstripes1.png')
		Material('models/ppm2/partrender/body_freckles.png')
		Material('models/ppm2/partrender/body_hooves1.png')
		Material('models/ppm2/partrender/body_hooves2.png')
		Material('models/ppm2/partrender/body_headmask1.png')
		Material('models/ppm2/partrender/body_hooves1_crit.png')
		Material('models/ppm2/partrender/body_hooves2_crit.png')
		Material('models/ppm2/partrender/body_spots1.png')
		Material('models/ppm2/partrender/body_robotic.png')
		Material('models/ppm2/partrender/dash-e.png')
		Material('models/ppm2/partrender/eye_scar.png')
		Material('models/ppm2/partrender/eye_wound.png')
		Material('models/ppm2/partrender/body_scar.png')
		Material('models/ppm2/partrender/gear_socks.png')
		Material('models/ppm2/partrender/sharp_hooves.png')
		Material('models/ppm2/partrender/sharp_hooves2.png')
		Material('models/ppm2/partrender/separated_muzzle.png')
		Material('models/ppm2/partrender/eye_scar_left.png')
		Material('models/ppm2/partrender/eye_scar_right.png')
	}

	UPPER_MANE_DETAILS: {
		[4]: {Material('models/ppm2/partrender/upmane_5_mask0.png')}
		[5]: {Material('models/ppm2/partrender/upmane_6_mask0.png')}
		[7]: {Material('models/ppm2/partrender/upmane_8_mask0.png'), Material('models/ppm2/partrender/upmane_8_mask1.png')}
		[8]: {Material('models/ppm2/partrender/upmane_9_mask0.png'), Material('models/ppm2/partrender/upmane_9_mask1.png'), Material('models/ppm2/partrender/upmane_9_mask2.png')}
		[9]: {Material('models/ppm2/partrender/upmane_10_mask0.png')}
		[10]: {Material('models/ppm2/partrender/upmane_11_mask0.png'), Material('models/ppm2/partrender/upmane_11_mask1.png'), Material('models/ppm2/partrender/upmane_11_mask2.png')}
		[11]: {Material('models/ppm2/partrender/upmane_12_mask0.png')}
		[12]: {Material('models/ppm2/partrender/upmane_13_mask0.png')}
		[13]: {Material('models/ppm2/partrender/upmane_14_mask0.png')}
		[14]: {Material('models/ppm2/partrender/upmane_15_mask0.png')}
	}

	LOWER_MANE_DETAILS: {
		[4]: {Material('models/ppm2/partrender/dnmane_5_mask0.png')}
		[7]: {Material('models/ppm2/partrender/dnmane_8_mask0.png'), Material('models/ppm2/partrender/dnmane_8_mask1.png')}
		[8]: {Material('models/ppm2/partrender/dnmane_9_mask0.png'), Material('models/ppm2/partrender/dnmane_9_mask1.png')}
		[9]: {Material('models/ppm2/partrender/dnmane_10_mask0.png'), Material('models/ppm2/partrender/dnmane_10_mask1.png'), Material('models/ppm2/partrender/dnmane_10_mask2.png')}
		[10]: {Material('models/ppm2/partrender/dnmane_11_mask0.png'), Material('models/ppm2/partrender/dnmane_11_mask1.png')}
		[11]: {Material('models/ppm2/partrender/dnmane_12_mask0.png')}
	}

	TAIL_DETAILS: {
		[4]: {Material('models/ppm2/partrender/tail_5_mask0.png')}
		[7]: {Material('models/ppm2/partrender/tail_8_mask0.png'), Material('models/ppm2/partrender/tail_8_mask1.png'), Material('models/ppm2/partrender/tail_8_mask2.png'), Material('models/ppm2/partrender/tail_8_mask3.png'), Material('models/ppm2/partrender/tail_8_mask4.png')}
		[9]: {Material('models/ppm2/partrender/tail_10_mask0.png')}
		[10]: {Material('models/ppm2/partrender/tail_11_mask0.png'), Material('models/ppm2/partrender/tail_11_mask1.png'), Material('models/ppm2/partrender/tail_11_mask2.png')}
		[11]: {Material('models/ppm2/partrender/tail_12_mask0.png'), Material('models/ppm2/partrender/tail_12_mask1.png')}
		[12]: {Material('models/ppm2/partrender/tail_13_mask0.png')}
		[13]: {Material('models/ppm2/partrender/tail_14_mask0.png')}
	}

	SOCKS_PATCHS: {
		'models/props_pony/ppm/ppm_socks/socks_striped_unlit'
		'models/props_pony/ppm2/ppm_socks/custom/geometric1_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric2_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric3_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric4_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric5_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric6_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric7_1.png'
		'models/props_pony/ppm2/ppm_socks/custom/geometric8_1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/dark1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers10.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers11.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers12.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers13.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers14.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers15.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers16.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers17.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers18.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers19.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers20.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers3.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers4.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers5.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers6.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers7.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers8.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/flowers9.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/grey1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/grey2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/grey3.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/hearts1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/hearts2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/snow1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/wallpaper1.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/wallpaper2.png'
		'models/props_pony/ppm2/ppm_socks/custom_textured/wallpaper3.png'
	}

	SOCKS_DETAILS_PATCHS: {
		[2]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric1_4.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric1_5.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric1_6.png'
		}

		[3]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric2_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric2_4.png'
		}

		[4]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric3_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric3_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric3_5.png'
		}

		[5]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric4_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric4_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric4_4.png'
		}

		[6]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric5_4.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric5_5.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric5_6.png'
		}

		[7]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric6_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric6_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric6_4.png'
		}

		[8]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric7_3.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric7_4.png'
		}

		[9]: {
			'models/props_pony/ppm2/ppm_socks/custom/geometric8_2.png'
			'models/props_pony/ppm2/ppm_socks/custom/geometric8_3.png'
		}
	}
}

additionTable = (...) ->
	tab = {'$ignorez': 1, '$vertexcolor': 1, '$vertexalpha': 1, '$nolod': 1}
	args = {...}
	for i = 1, #args, 2
		key, val = args[i], args[i + 1]
		tab[key] = val
	return tab

module.SOCKS_MATERIALS = [Material(id) for _, id in ipairs module.SOCKS_PATCHS]
module.SOCKS_DETAILS = [Material(id) for _, id in ipairs module.SOCKS_DETAILS_PATCHS]
module.CUTIEMARKS = [Material("models/ppm2/cmarks/#{mark}.png") for _, mark in ipairs PPM2.DefaultCutiemarks]
module.SUITS = [Material("models/ppm2/texclothes/#{mat}.png") for _, mat in ipairs {
	'clothes_royalguard', 'clothes_sbs_full'
	'clothes_sbs_light', 'clothes_wbs_full'
	'clothes_wbs_light', 'spidermane_light'
	'spidermane_full'
}]

module.TATTOOS = [Material("models/ppm2/partrender/tattoo/#{fil\lower()}.png") for _, fil in ipairs PPM2.TATTOOS_REGISTRY when fil ~= 'NONE']
debugwhite = {
	'$basetexture': 'models/debug/debugwhite'
	'$ignorez': 1
	'$vertexcolor': 1
	'$vertexalpha': 1
	'$nolod': 1
}

module.EYE_OVALS = {
	Material('models/ppm2/partrender/eye_oval.png')
	Material('models/ppm2/partrender/eye_oval_aperture.png')
}

module.EYE_REFLECTIONS = {
	Material('models/ppm2/partrender/eye_reflection.png')
	Material('models/ppm2/partrender/eye_reflection_crystal_foal.png')
	Material('models/ppm2/partrender/eye_reflection_crystal_unisex.png')
	Material('models/ppm2/partrender/eye_reflection_foal.png')
	Material('models/ppm2/partrender/eye_reflection_male.png')
}

module.DEBUGWHITE = CreateMaterial('PPM2.Debugwhite', 'UnlitGeneric', debugwhite)
module.HAIR_MATERIAL_COLOR = CreateMaterial('PPM2.ManeTextureBase', 'UnlitGeneric', debugwhite)
module.TAIL_MATERIAL_COLOR = CreateMaterial('PPM2.TailTextureBase', 'UnlitGeneric', debugwhite)
module.WINGS_MATERIAL_COLOR = CreateMaterial('PPM2.WingsMaterialBase', 'UnlitGeneric', debugwhite)
module.HORN_MATERIAL_COLOR = CreateMaterial('PPM2.HornMaterialBase', 'UnlitGeneric', additionTable('$basetexture', 'models/ppm2/base/horn'))
module.BODY_MATERIAL = CreateMaterial('PPM2.BodyTexture', 'UnlitGeneric', additionTable('$basetexture', 'models/ppm2/base/body'))
module.HORN_DETAIL_BUMP = CreateMaterial('PPM2.HornBumpMapRenderer', 'UnlitGeneric', additionTable('$basetexture', 'models/ppm2/base/horn_normal'))
module.HORN_DETAIL_COLOR = Material('models/ppm2/partrender/horn_detail.png')
module.EYE_OVAL = Material('models/ppm2/partrender/eye_oval.png')
module.EYE_GRAD = Material('models/ppm2/partrender/eye_grad.png')
module.EYE_EFFECT = Material('models/ppm2/partrender/eye_effect.png')
module.EYE_LINE_L_1 = Material('models/ppm2/partrender/eye_line_l1.png')
module.EYE_LINE_R_1 = Material('models/ppm2/partrender/eye_line_r1.png')
module.EYE_LINE_L_2 = Material('models/ppm2/partrender/eye_line_l2.png')
module.EYE_LINE_R_2 = Material('models/ppm2/partrender/eye_line_r2.png')
module.EYEBROWS = Material('models/ppm2/partrender/eyebrows.png')
module.PONY_SOCKS = Material('models/ppm2/texclothes/pony_socks.png')

module.LIPS = Material('models/ppm2/partrender/lips.png')
module.NOSE = Material('models/ppm2/partrender/nose.png')

module.EYE_CORNERA = Material('models/ppm2/eyes/eye_cornea')
module.EYE_CORNERA_OVAL = Material('models/ppm2/eyes/eye_cornea_oval')
module.EYE_EXTRA = Material('models/ppm2/eyes/eye_extra')
module.EYE_EXTRA2 = Material('models/ppm2/eyes/eye_extra2')
module.EYE_LIGHTWARP = Material('models/ppm2/eyes/eye_lightwarp')
module.EYE_REFLECTION2 = Material('models/ppm2/eyes/eye_reflection')

PPM2.MaterialsRegistry = module
return module


clue: some more fun(cs)

clue: some more fun(cs)

Former-commit-id: 8ebb5dd79bc12463b0f67a07c239044c51c2f557
Former-commit-id: c6dfc7eabca88ea1a123167777b1671eb8238bc6

-- Clue module requires clue data files and tosql.py from emnh/clue2

dbi = require 'DBI'
require'logging.console'
log = logging.console()
conn = false

attrs = {'grammar','reference', 'country', 'context', 'text'}

connect = ->
  conn, err = DBI.Connect('PostgreSQL', ivar2.config.dbname, ivar2.config.dbuser, ivar2.config.dbpass, ivar2.config.dbhost, ivar2.config.dbport)
  unless conn
    log\error "Unable to connect to DB: #{err}"
    return

  conn\autocommit(true)

dbh = ->
  connect! unless conn

  -- Check if connection is alive
  alive = conn\ping!
  connect! unless alive

  success, err = DBI.Do(conn, 'SELECT NOW()')
  unless success
    log\error "SQL Connection :#{err}"
    connect!

  return conn

lookup = (source, destination, lookup) =>
  stmt = dbh!\prepare [[
    SELECT
      tablename
    from
      pg_catalog.pg_tables
    where
      tablename like 'cl%'
    ]]
  stmt\execute!
  tables = {}
  for row in stmt\rows(true) -- true for column names
    tables[#tables+1] = row.tablename

  out = {}
  for dict in *tables
    out[dict] = {}
    stmt = dbh!\prepare [[
      SELECT
        *
      FROM
        ]]..dict..[[
      WHERE
        word = ?
      ]]
    stmt\execute lookup
    for row in stmt\rows(true) -- true for column names
      table.insert(out[dict], row)

  i = 0
  for dict, rows in pairs out
    res = {}
    dfrom = dict\sub(3, 4)
    to = dict\sub(5, 6)
    first = true
    for row in *rows
      i += 1
      if first
        table.insert(res, "[#{ivar2.util.bold row.word}]")
        table.insert(res, "(#{ivar2.util.italic dfrom}")
        table.insert(res, "-> #{ivar2.util.italic to})")
        first = false
      for attr in *attrs
        if row[attr] != nil and row[attr] != ''
          if attr == 'grammar'
            table.insert(res, "(#{row[attr]}. )")
          else
            table.insert(res, row[attr])
    say table.concat(res, ' ')
  if i == 0
    reply 'Nope.'

upper = (s) ->
  (string.upper(s)\gsub('æ','Æ')\gsub('ø','Ø')\gsub('å','Å'))

getWord = (lang, wordclass, count, maxlen) ->
  stmt = dbh!\prepare [[
    SELECT
     *
    FROM
      ]]..lang..[[
    WHERE
      grammar = ?
    AND
      length(word) < ?
    ORDER BY
      random()
    LIMIT ?
  ]]
  stmt\execute wordclass, maxlen, count
  out = {}
  for row in stmt\rows(true) -- true for column names
    table.insert(out, row)
  return out

bankid = (source, destination) =>

  adj = getWord 'clnono', 'adj', 1, 8
  sub = getWord 'clnono', 's', 1, 10

  say "#{upper adj[1].word} #{upper sub[1].word}"

isLogWord = (word) ->
  myconn = dbh!
  sql = [[
    SELECT
     count(*)
    FROM
      log
    WHERE
      message ~* '\y]]..word..[[\y'
  ]]
  stmt = myconn\prepare sql
  stmt\execute!
  count = 0
  row = stmt\fetch!
  count = row[1]
  return tonumber(count) > 0

bankid2 = (source, destination) =>

  adj = ""
  sub = ""
  while true
    adj = getWord 'clnono', 'adj', 1, 20
    adj = adj[1].word
    continue unless isLogWord(adj)
  while true
    sub = getWord 'clnono', 's', 1, 20
    sub = sub[1].word
    continue unless isLogWord(sub)

  say "#{upper adj} #{upper sub}"

bankidmany = (source, destination, count) =>
  count = math.min(count, 100)

  adj = getWord 'clnono', 'adj', count, 8
  sub = getWord 'clnono', 's', count, 10

  out = {}
  for i=1, count
    table.insert(out, "#{upper adj[i].word} #{upper sub[i].word}")
  say table.concat(out, ', ')

adjsub = (source, destination, count) =>
  count = tonumber count
  if not count
    count = 1
  count = math.min(count, 100)

  adj = getWord 'clukuk', 'adj', count, 8
  sub = getWord 'clukuk', 'n', count, 10

  out = {}
  for i=1, count
    table.insert(out, "#{adj[i].word} #{sub[i].word}")
  say table.concat(out, ', ')

PRIVMSG:
  '^%pclue (.+)$': lookup
  '^%pbankid$': bankid
  '^%pbankid (%d+)$': bankidmany
  '^%padjsub$': adjsub
  '^%padjsub (%d+)$': adjsub
  '^%pfluffle (.+)$': (source, destination, arg) =>
    adj = getWord 'clukuk', 'adj', 1, 8
    sub = getWord 'clukuk', 'n', 1, 10
    say "#{ivar2.util.trim arg} got fluffled by a #{adj[1].word} #{sub[1].word}"
  --'^%pbankid2$': bankid2

-- Clue module requires clue data files and tosql.py from emnh/clue2

dbi = require 'DBI'
require'logging.console'
log = logging.console()
conn = false

attrs = {'grammar','reference', 'country', 'context', 'text'}

connect = ->
  conn, err = DBI.Connect('PostgreSQL', ivar2.config.dbname, ivar2.config.dbuser, ivar2.config.dbpass, ivar2.config.dbhost, ivar2.config.dbport)
  unless conn
    log\error "Unable to connect to DB: #{err}"
    return

  conn\autocommit(true)

dbh = ->
  connect! unless conn

  -- Check if connection is alive
  alive = conn\ping!
  connect! unless alive

  success, err = DBI.Do(conn, 'SELECT NOW()')
  unless success
    log\error "SQL Connection :#{err}"
    connect!

  return conn

lookup = (source, destination, lookup) =>
  stmt = dbh!\prepare [[
    SELECT
      tablename
    from
      pg_catalog.pg_tables
    where
      tablename like 'cl%'
    ]]
  stmt\execute!
  tables = {}
  for row in stmt\rows(true) -- true for column names
    tables[#tables+1] = row.tablename

  out = {}
  for dict in *tables
    out[dict] = {}
    stmt = dbh!\prepare [[
      SELECT
        *
      FROM
        ]]..dict..[[
      WHERE
        word = ?
      ]]
    stmt\execute lookup
    for row in stmt\rows(true) -- true for column names
      table.insert(out[dict], row)

  i = 0
  for dict, rows in pairs out
    res = {}
    dfrom = dict\sub(3, 4)
    to = dict\sub(5, 6)
    first = true
    for row in *rows
      i += 1
      if first
        table.insert(res, "[#{ivar2.util.bold row.word}]")
        table.insert(res, "(#{ivar2.util.italic dfrom}")
        table.insert(res, "-> #{ivar2.util.italic to})")
        first = false
      for attr in *attrs
        if row[attr] != nil and row[attr] != ''
          if attr == 'grammar'
            table.insert(res, "(#{row[attr]}. )")
          else
            table.insert(res, row[attr])
    say table.concat(res, ' ')
  if i == 0
    reply 'Nope.'

upper = (s) ->
  (string.upper(s)\gsub('æ','Æ')\gsub('ø','Ø')\gsub('å','Å'))

getWord = (wordclass) ->
  stmt = dbh!\prepare [[
    SELECT
     *
    FROM
      clnono
    WHERE
      grammar = ?
    AND
      length(word) < 8
    ORDER BY
      random()
    LIMIT 1
  ]]
  stmt\execute wordclass
  out = {}
  for row in stmt\rows(true) -- true for column names
    table.insert(out, row)
  return out

bankid = (source, destination) =>

  adj = getWord 'adj'
  sub = getWord 's'

  say "#{upper adj[1].word} #{upper sub[1].word}"

PRIVMSG:
  '^%pclue (.+)$': lookup
  '^%pbankid$': bankid


list: tune output

list: tune output

Former-commit-id: bf176d20200f9421afb002ea22858dc8ba1aab0f [formerly 8542b733d3081110252d15ac475aa2b795ea57f0]
Former-commit-id: e502dd2e9c72f19459b291b6621df170168b31c5

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

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)
    table.insert out, patt

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

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


utf8: new function throw. （╯°□°）╯︵

utf8: new function throw. （╯°□°）╯︵

Former-commit-id: f0d89ae99f3c06d973dfcd236388aa71e7a4117c [formerly 57372a63fb2e4b67e0b44cb21fc31a81175736b5]
Former-commit-id: 200b1455cdde4a5139b6bda3e526023919e24b9a

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

an = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.?!"'`()[]{}<>&_]]
charmaps = {
ci: 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl: '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'
ud: [[∀BƆDƎℲפHIſK˥WNOԀQRS┴∩ΛMX⅄Zɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz0ƖᄅƐㄣϛ9ㄥ86'˙¿¡,,,)(][}{><⅋‾]]
nc: [[🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿]]
sq: [[🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789,⊡]]
ns: [[🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉]]
ds: [[𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡]]
bo: [[𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗]]
bi: [[𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛0123456789]]
bs: [[𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃]]
pt: [[⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵0⑴⑵⑶⑷⑸⑹⑺⑻⑼]]
}
codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

-- Construct a table which can be used for lookup replacement later
-- Iterate over normal ascii and find counterparts in the weirdo strings as substrings
maps = {}
for charmap, chars in pairs charmaps
  i = 1
  maps[charmap] = {}
  for uchar in string.gmatch(chars, "([%z\1-\127\194-\244][\128-\191]*)")
    maps[charmap][an\sub(i,i)] = uchar
    i = i +1

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) =>
    say wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    say remap(maps.bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    say remap(maps.ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    say zalgo(arg, 7)
  '^%pupsidedown (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pflip (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pthrow (.+)$': (source, destination, arg) =>
    say "（╯°□°）╯︵ #{remap maps.ud, arg}"
  '^%pparanthesized (.+)$': (source, destination, arg) =>
    say remap(maps.pt, arg)
  '^%pnegcircle (.+)$': (source, destination, arg) =>
    say remap(maps['nc'], arg)
  '^%psquare (.+)$': (source, destination, arg) =>
    say remap(maps.sq, arg)
  '^%pnegsquare (.+)$': (source, destination, arg) =>
    say remap(maps.ns, arg)
  '^%pdoublestruck (.+)$': (source, destination, arg) =>
    say remap(maps.ds, arg)
  '^%pubold (.+)$': (source, destination, arg) =>
    say remap(maps.bo, arg)
  '^%pbolditalic (.+)$': (source, destination, arg) =>
    say remap(maps.bi, arg)
  '^%pboldscript (.+)$': (source, destination, arg) =>
    say remap(maps.bs, arg)
  '^%putfuk (.+)$': (source, destination, arg) =>
    keys = [x for x,_ in pairs(maps)]
    say table.concat([remap(maps[keys[math.random(#keys)]], letter) for letter in codepoints(arg)])

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

an = [[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789,.?!"'`()[]{}<>&_]]
charmaps = {
ci: 'ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ⓪①②③④⑤⑥⑦⑧⑨'
bl: '𝔄𝔅ℭ𝔇𝔈𝔉𝔊ℌℑ𝔍𝔎𝔏𝔐𝔑𝔒𝔓𝔔ℜ𝔖𝔗𝔘𝔙𝔚𝔛𝔜ℨ𝔞𝔟𝔠𝔡𝔢𝔣𝔤𝔥𝔦𝔧𝔨𝔩𝔪𝔫𝔬𝔭𝔮𝔯𝔰𝔱𝔲𝔳𝔴𝔵𝔶𝔷'
ud: [[∀BƆDƎℲפHIſK˥WNOԀQRS┴∩ΛMX⅄Zɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz0ƖᄅƐㄣϛ9ㄥ86'˙¿¡,,,)(][}{><⅋‾]]
nc: [[🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩🅐🅑🅒🅓🅔🅕🅖🅗🅘🅙🅚🅛🅜🅝🅞🅟🅠🅡🅢🅣🅤🅥🅦🅧🅨🅩⓿]]
sq: [[🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉🄰🄱🄲🄳🄴🄵🄶🄷🄸🄹🄺🄻🄼🄽🄾🄿🅀🅁🅂🅃🅄🅅🅆🅇🅈🅉0123456789,⊡]]
ns: [[🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉🅰🅱🅲🅳🅴🅵🅶🅷🅸🅹🅺🅻🅼🅽🅾🅿🆀🆁🆂🆃🆄🆅🆆🆇🆈🆉]]
ds: [[𝔸𝔹ℂ𝔻𝔼𝔽𝔾ℍ𝕀𝕁𝕂𝕃𝕄ℕ𝕆ℙℚℝ𝕊𝕋𝕌𝕍𝕎𝕏𝕐ℤ𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡]]
bo: [[𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗]]
bi: [[𝑨𝑩𝑪𝑫𝑬𝑭𝑮𝑯𝑰𝑱𝑲𝑳𝑴𝑵𝑶𝑷𝑸𝑹𝑺𝑻𝑼𝑽𝑾𝑿𝒀𝒁𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛0123456789]]
bs: [[𝓐𝓑𝓒𝓓𝓔𝓕𝓖𝓗𝓘𝓙𝓚𝓛𝓜𝓝𝓞𝓟𝓠𝓡𝓢𝓣𝓤𝓥𝓦𝓧𝓨𝓩𝓪𝓫𝓬𝓭𝓮𝓯𝓰𝓱𝓲𝓳𝓴𝓵𝓶𝓷𝓸𝓹𝓺𝓻𝓼𝓽𝓾𝓿𝔀𝔁𝔂𝔃]]
pt: [[⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵0⑴⑵⑶⑷⑸⑹⑺⑻⑼]]
}
codepoints = (str) ->
  str\gmatch("[%z\1-\127\194-\244][\128-\191]*")

-- Construct a table which can be used for lookup replacement later
-- Iterate over normal ascii and find counterparts in the weirdo strings as substrings
maps = {}
for charmap, chars in pairs charmaps
  i = 1
  maps[charmap] = {}
  for uchar in string.gmatch(chars, "([%z\1-\127\194-\244][\128-\191]*)")
    maps[charmap][an\sub(i,i)] = uchar
    i = i +1

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) =>
    say wireplace(0xFEE0, arg)
  '^%pblackletter (.+)$': (source, destination, arg) =>
    say remap(maps.bl, arg)
  '^%pcircled (.+)$': (source, destination, arg) =>
    say remap(maps.ci, arg)
  '^%pzalgo (.+)$': (source, destination, arg) =>
    say zalgo(arg, 7)
  '^%pupsidedown (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pflip (.+)$': (source, destination, arg) =>
    say remap(maps.ud, arg)
  '^%pparanthesized (.+)$': (source, destination, arg) =>
    say remap(maps.pt, arg)
  '^%pnegcircle (.+)$': (source, destination, arg) =>
    say remap(maps['nc'], arg)
  '^%psquare (.+)$': (source, destination, arg) =>
    say remap(maps.sq, arg)
  '^%pnegsquare (.+)$': (source, destination, arg) =>
    say remap(maps.ns, arg)
  '^%pdoublestruck (.+)$': (source, destination, arg) =>
    say remap(maps.ds, arg)
  '^%pubold (.+)$': (source, destination, arg) =>
    say remap(maps.bo, arg)
  '^%pbolditalic (.+)$': (source, destination, arg) =>
    say remap(maps.bi, arg)
  '^%pboldscript (.+)$': (source, destination, arg) =>
    say remap(maps.bs, arg)
  '^%putfuk (.+)$': (source, destination, arg) =>
    keys = [x for x,_ in pairs(maps)]
    say table.concat([remap(maps[keys[math.random(#keys)]], letter) for letter in codepoints(arg)])