--Object skins v1.2
--Script made by Chaobrother, special thanks to Ashnal for the initial rewrite. 
--Object spawn state swapping may crash with objects that have MF_RUNSPAWNFUNC.
--This is a script which changes a particular property of an object.
--Hopefully it will cut down on freeslot/thing starvation.  
--There are also server commands for global overrides.

--Map Header Setup: Use Lua.ObjectSkin then a comma separated list.
--Each object to change can have all of its properties modified. The order can vary, but:
--Start off with your object type you want to modify starting with "MT_"
--The following arguments should alternate until the end of the level header or you want to use a new object.
	--The first argument is the type of property to change: S_RAISESTATE, S_SPAWNSTATE, etc.
		--this should be written as the property appears in an object definition (e.g spawnstate)
	--Changing the spawn state will also alter the current state.
	--The second is the property's new value. Use _G[MT_Object] if the new value is an object.

--Lua.spawnskin merely changes the frame and sprite of an object on spawn.
--Like object skin enter your parameters as a comma separated list.
	--The first argument is the object to look for.
	--The second is the new sprite prefix with SPR_ in the front.
	--The third is the new frame.

--If you run out of space on a line add a number starting from one to a new level header parameter.
	--Example: Lua.spawnskin, Lua.spawnskin1,Lua.spawnskin2,... 
	--The script will concatenate each header.
	
--[[Server Commands
		obsk_add: Defines custom state overrides for all maps. By default changes here
				  do not take precedence over custom overrides defined by level header parameters.
				  Argument: A comma seperated string that is formatted the same as 
				  lua.objectskin level header parameter.
				  --Start off with your object type you want to modify starting with "MT_"
				  --The following arguments should alternate until the end of the levelheader or you want to use a new object.
					--The first argument is the type of property to change: S_RAISESTATE, S_SPAWNSTATE, etc.
						--this should be written as the property appears in an object definition (e.g spawnstate)
					--Changing the spawn state will also alter the current state.
					--The second is the new property.
		obsk_remove: Removes a custom overide from all maps
				  Argument: A comma seperated list with the object and all states that need to be cleared to follow.
				  Every custom override for a given object is cleared if no states are given.
				e.g: MT_OBJECT, spawnstate
		obsk_clear: clears all of the server's custom overrides for all maps.
					No Arguments
		obsk_disable: Prevents (but does not remove) the server's custom overrides from loading.
					Arguments: on or off.
		obsk_serverpriority: Determines if the server's overrides or the maps should take precedence.
					Arguments: on or off.
		obsk_fastload: Makes changes to the server's overrides immediate. May cause issues if enabled.
					Arguments: on or off.			
]]

--The complete script is not to be modified as it would change the behavior of other map packs which use it.
--It is free to be packaged with your file.
--Any part of the script is free to reuse for external purposes, however.
if (objectskin_initialized) then return end -- Check if the script has already been run

local function tableparse(x)
	local y = {}
	local i = 0
	if x
		for word in x:gmatch('[^,%s]+') do
			y[i+1] = word 
			i = $+1
		end
	end
	return y
end

local function tableconcat(param)
	local i = 1
	local z = tableparse(mapheaderinfo[gamemap][param])
	local y = {}
	repeat
		y = tableparse(mapheaderinfo[gamemap][param..i])
		if not y break end
		for j = 1 , #y
			table.insert(z,y[j])
		end
		i = $1 + 1
	until not mapheaderinfo[gamemap][param..i]
	return z
end

local function stroperators(val1,token,val2)
	if token ==  "<<" then
		return val1 << val2
	elseif token ==  ">>" then
		return val1 >> val2
	elseif token ==  "|" then
		return val1 | val2
	elseif token ==  "&" then
		return val1 & val2
	--[[elseif token ==  "^^" then
		return val1 ^^ val2
	elseif token ==  "~" then
		return val1 ! val2
	elseif token == "!" then
		return val1 ! val2]]
	end
	if token ==  "-" then
		return val1 - val2
	elseif token ==  "+" then
		return val1 + val2
	elseif token ==  "/" then
		return val1 / val2
	elseif token ==  "*" then
		return val1 * val2
	elseif token ==  "%" then
		return val1 % val2
	elseif token ==  "^" then
		return val1 ^ val2
	else
	    print("Operation error with "..(val1 or "nil")..","..(token or "nil")..", and "..(val2 or "nil"))
	    return nil
	end
end

local function strtomath(str)
    local operators = {}
    local operands = {}
	for w in str:gmatch("[%+%-%*%/%|%^%%%%&(%<)(%>)]+") do
	    table.insert(operators,w)
	end
	for w in str:gmatch("[^%+%-%*%/%|%^%%%%&(%<)(%>)]+") do
	    table.insert(operands,w)
	end
	local num = _G[operands[1]] or tonumber(operands[1])
	local num2
	for k=1 , #operands do
	   if operands[k+1] and operators[k]then
	      num2 = _G[operands[k+1]] or tonumber(operands[k+1])
	      num = stroperators(num,operators[k],num2)
	   end
	end
	return num;
end


local function swaptablemake(sswaptable, restable, instr)
	local mobjtype = 0
	local propertykey
	local restableticker = 0
	local stateswapval = 0
	local alreadykey = 0
	for i = 1, #instr
		if string.sub(instr[i],1,3) == "MT_"
			mobjtype = _G[instr[i]]
			restableticker = 0
			if restable[mobjtype] == nil
				restable[mobjtype] = {}
			end
			if sswaptable[mobjtype] == nil
				sswaptable[mobjtype] = {}
			end
			continue
		else
			--If you need to use objects for other purposes. 
			--Use _G[objectype] without quotes in the brackets.
			instr[i] = string.gsub(instr[i],"_G%b[]", function(w)
			return _G[string.sub(w,4,-2)] end)
			if restableticker == 1
				if tonumber(instr[i]) 
					stateswapval = instr[i]
				else
					if not _G[instr[i]]
						stateswapval = strtomath(instr[i])
					else
						stateswapval = _G[instr[i]]
					end
				end
				sswaptable[mobjtype][propertykey] = stateswapval
			else
				propertykey = instr[i]
				alreadykey = 0
				--Setting up restoration.
				for j = 1, #restable[mobjtype]
					if instr[i] == restable[mobjtype][j]
						alreadykey = 1
						break
					end
				end
				if alreadykey == 0
					table.insert(restable[mobjtype], instr[i])
					table.insert(restable[mobjtype], --[[ZBno]]mobjinfo[mobjtype][instr[i]])
				end
			end
			restableticker = $1 ^^ 1
		end
	end
	return sswaptable,restable
end

local function obskinparse(sswaptable)
	for k,v in pairs(sswaptable)
		for l,w in pairs(sswaptable[k])
			if l == "spawnstate" continue end
			--[[ZBno]]mobjinfo[k][l] = w
		end
	end
end


local serverswaptable = {}
local serverrestoretable = {}
local serverrestoreclear = 0
local skinremoval = {}

addHook("NetVars",function(net)
	serverswaptable = net{$}
	serverrestoretable = net{$}
	serverrestoreclear = net{$}
	skinremoval = net{$}
end)
local obsk_disable = CV_RegisterVar({
		name = "obsk_disable",
		defaultvalue = 0,
		flags = CV_NETVAR,
		possiblevalue = CV_OnOFF
			})
local obsk_serverpriority = CV_RegisterVar({
		name = "obsk_serverpriority",
		defaultvalue = 0,
		flags = CV_NETVAR,
		possiblevalue = CV_OnOFF
			})

local stateswaptable = {}
local skinswaptable = {}
local restoretable = {}

local function mapobjskin()
	if mapheaderinfo[gamemap].objectskin or mapheaderinfo[gamemap].objectskin1
		stateswaptable,restoretable = swaptablemake(stateswaptable, restoretable, tableconcat("objectskin"))
		obskinparse(stateswaptable)
	end
	if mapheaderinfo[gamemap].spawnskin or mapheaderinfo[gamemap].spawnskin1
		skinswaptable = tableconcat("spawnskin")
	end
end

local function serverobjskin()
	local mobjtype = 0
	if CV_FindVar("obsk_disable").value == 0
		obskinparse(serverswaptable)
	else
		for k, v in pairs(serverrestoretable)
			for i = 1,#serverrestoretable[k],2
				--[[ZBno]]mobjinfo[k][serverrestoretable[k][i]] = serverrestoretable[k][i+1]
			end
		end
	end
	if skinremoval ~= nil and #skinremoval > 1
		obskremove(skinremoval)
	end
end

addHook("NetVars",function(net)	
	stateswaptable = net($)
	skinswaptable = net($)
	restoretable = net($)
end)

addHook("MapChange",function()
		for k, v in pairs(restoretable)
			for i = 1,#restoretable[k],2
				--[[ZBno]]mobjinfo[k][restoretable[k][i]] = restoretable[k][i+1]
			end
		end
		--restoretable = {}
		
		if serverrestoreclear == 1
			for k, v in pairs(serverrestoretable)
				for i = 1,#serverrestoretable[k],2
					--[[ZBno]]mobjinfo[k][serverrestoretable[k][i]] = serverrestoretable[k][i+1]
				end
			end
			serverswaptable = {}
			serverrestoretable = {}
			serverrestoreclear = 0
		end
		if CV_FindVar("obsk_serverpriority").value == 1
			mapobjskin()
			serverobjskin()
		else
			serverobjskin()
			mapobjskin()
		end
end)

addHook("PlayerJoin",function()
	if CV_FindVar("obsk_serverpriority").value == 1
		obskinparse(stateswaptable)			
		obskinparse(serverswaptable)
	else
		obskinparse(serverswaptable)
		obskinparse(stateswaptable)
	end
	if mapheaderinfo[gamemap].spawnskin or mapheaderinfo[gamemap].spawnskin1
		for mt in mapthings.iterate do
			if not mt.mobj continue end
			for i = 1,#skinswaptable,3
				if mt.mobj.type == _G[skinswaptable[i]]
					if (mt.mobj.flags & MF_NOTHINK)
						mt.mobj.flags = $1 & ~(MF_NOTHINK|MF_NOBLOCKMAP|MF_SCENERY)
						P_RemoveMobj(mt.mobj)
						break
					end
				end
			end
		end
	end
end)

addHook("ThinkFrame",function()
	if not mapheaderinfo[gamemap].spawnskin or mapheaderinfo[gamemap].spawnskin1 or leveltime != 1 return end
	for mt in mapthings.iterate do
		if not mt.mobj continue end
		for i = 1,#skinswaptable,3
			if mt.mobj.type == _G[skinswaptable[i]]
				if (mt.mobj.flags & MF_NOTHINK)
					local obskin = P_SpawnMobj(mt.mobj.x,mt.mobj.y,mt.mobj.z,MT_THOK)
					obskin.tics = -1
					mt.mobj.flags = $1 & ~(MF_NOTHINK|MF_NOBLOCKMAP|MF_SCENERY)
					K_MatchGenericExtraFlags(obskin, mt.mobj)
					obskin.sprite = _G[skinswaptable[i+1]]
					obskin.frame = _G[skinswaptable[i+2]]
					P_RemoveMobj(mt.mobj)
					break
				end
			end
		end
		
	end
end)

addHook("MobjSpawn",function(mo)
	if not mapheaderinfo[gamemap].spawnskin or mapheaderinfo[gamemap].spawnskin1 return end
	for i = 1,#skinswaptable,3
		if mo.type == _G[skinswaptable[i]] and mo.valid
			mo.sprite = _G[skinswaptable[i+1]]
			mo.frame = _G[skinswaptable[i+2]]
			break
		end
	end
end)

addHook("MobjSpawn",function(mo)
	if CV_FindVar("obsk_serverpriority").value == 1
		if stateswaptable[mo.type] and (mapheaderinfo[gamemap].objectskin or mapheaderinfo[gamemap].objectskin1)
			mo.flags = $1 & ~(MF_NOTHINK|MF_SCENERY)
			mo.state = stateswaptable[mo.type]["spawnstate"] or mo.info.spawnstate
		end		
		if serverswaptable[mo.type] and CV_FindVar("obsk_disable").value == 1
			mo.flags = $1 & ~(MF_NOTHINK|MF_SCENERY)
			mo.state = serverswaptable[mo.type]["spawnstate"] or mo.info.spawnstate
		end	
	else
		if serverswaptable[mo.type] and CV_FindVar("obsk_disable").value == 1
			mo.flags = $1 & ~(MF_NOTHINK|MF_SCENERY)
			mo.state = serverswaptable[mo.type]["spawnstate"] or mo.info.spawnstate
		end	
		if stateswaptable[mo.type] and (mapheaderinfo[gamemap].objectskin or mapheaderinfo[gamemap].objectskin1)
			mo.flags = $1 & ~(MF_NOTHINK|MF_SCENERY)
			mo.state = stateswaptable[mo.type]["spawnstate"] or mo.info.spawnstate
		end		
	end
end)
rawset(_G, "objectskin_initialized", true) 