complete refactor

This commit is contained in:
shadowscion 2021-04-16 10:44:08 -05:00
parent 0d1065e7eb
commit e20749bb6b
21 changed files with 5479 additions and 0 deletions

364
lua/autorun/netstream.lua Normal file
View File

@ -0,0 +1,364 @@
--A net extension which allows sending large streams of data without overflowing the reliable channel
--Keep it in lua/autorun so it will be shared between addons
AddCSLuaFile()
net.Stream = {}
net.Stream.ReadStreamQueues = {} --This holds a read stream for each player, or one read stream for the server if running on the CLIENT
net.Stream.WriteStreams = {} --This holds the write streams
net.Stream.SendSize = 20000 --This is the maximum size of each stream to send
net.Stream.Timeout = 30 --How long the data should exist in the store without being used before being destroyed
net.Stream.MaxServerReadStreams = 128 --The maximum number of keep-alives to have queued. This should prevent naughty players from flooding the network with keep-alive messages.
net.Stream.MaxServerChunks = 3200 --Maximum number of pieces the stream can send to the server. 64 MB
net.Stream.MaxTries = 3 --Maximum times the client may retry downloading the whole data
net.Stream.MaxKeepalive = 15 --Maximum times the client may request data stay live
net.Stream.ReadStream = {}
--Send the data sender a request for data
function net.Stream.ReadStream:Request()
if self.downloads == net.Stream.MaxTries * self.numchunks then self:Remove() return end
self.downloads = self.downloads + 1
-- print("Requesting",self.identifier,false,false,#self.chunks)
net.Start("NetStreamRequest")
net.WriteUInt(self.identifier, 32)
net.WriteBit(false)
net.WriteBit(false)
net.WriteUInt(#self.chunks, 32)
if CLIENT then net.SendToServer() else net.Send(self.player) end
timer.Create("NetStreamReadTimeout" .. self.identifier, net.Stream.Timeout/2, 1, function() self:Request() end)
end
--Received data so process it
function net.Stream.ReadStream:Read(size)
timer.Remove("NetStreamReadTimeout" .. self.identifier)
local progress = net.ReadUInt(32)
if self.chunks[progress] then return end
local crc = net.ReadString()
local data = net.ReadData(size)
if crc == util.CRC(data) then
self.chunks[progress] = data
end
if #self.chunks == self.numchunks then
self.returndata = table.concat(self.chunks)
if self.compressed then
self.returndata = util.Decompress(self.returndata)
end
self:Remove()
else
self:Request()
end
end
--Gets the download progress
function net.Stream.ReadStream:GetProgress()
return #self.chunks/self.numchunks
end
--Pop the queue and start the next task
function net.Stream.ReadStream:Remove()
local ok, err = xpcall(self.callback, debug.traceback, self.returndata)
if not ok then ErrorNoHalt(err) end
net.Start("NetStreamRequest")
net.WriteUInt(self.identifier, 32)
net.WriteBit(false)
net.WriteBit(true)
if CLIENT then net.SendToServer() else net.Send(self.player) end
timer.Remove("NetStreamReadTimeout" .. self.identifier)
timer.Remove("NetStreamKeepAlive" .. self.identifier)
if self == self.queue[1] then
table.remove(self.queue, 1)
local nextInQueue = self.queue[1]
if nextInQueue then
timer.Remove("NetStreamKeepAlive" .. nextInQueue.identifier)
nextInQueue:Request()
else
net.Stream.ReadStreamQueues[self.player] = nil
end
else
for k, v in ipairs(self.queue) do
if v == self then
table.remove(self.queue, k)
break
end
end
end
end
net.Stream.ReadStream.__index = net.Stream.ReadStream
net.Stream.WriteStream = {}
-- The player wants some data
function net.Stream.WriteStream:Write(ply)
local progress = net.ReadUInt(32)+1
local chunk = self.chunks[progress]
if chunk then
self.clients[ply].progress = progress
net.Start("NetStreamDownload")
net.WriteUInt(#chunk.data, 32)
net.WriteUInt(progress, 32)
net.WriteString(chunk.crc)
net.WriteData(chunk.data, #chunk.data)
if CLIENT then net.SendToServer() else net.Send(ply) end
end
end
-- The player notified us they finished downloading or cancelled
function net.Stream.WriteStream:Finished(ply)
self.clients[ply].finished = true
if self.callback then
local ok, err = xpcall(self.callback, debug.traceback, ply)
if not ok then ErrorNoHalt(err) end
end
end
-- Get player's download progress
function net.Stream.WriteStream:GetProgress(ply)
return self.clients[ply].progress / #self.chunks
end
-- If the stream owner cancels it, notify everyone who is subscribed
function net.Stream.WriteStream:Remove()
local sendTo = {}
for ply, client in pairs(self.clients) do
if not client.finished then
client.finished = true
if ply:IsValid() then sendTo[#sendTo+1] = ply end
end
end
net.Start("NetStreamDownload")
net.WriteUInt(0, 32)
net.WriteUInt(self.identifier, 32)
if SERVER then net.Send(sendTo) else net.SendToServer() end
net.Stream.WriteStreams[self.identifier] = nil
end
net.Stream.WriteStream.__index = net.Stream.WriteStream
--Store the data and write the file info so receivers can request it.
local identifier = 1
function net.WriteStream(data, callback, dontcompress)
if not isstring(data) then
error("bad argument #1 to 'WriteStream' (string expected, got " .. type(data) .. ")", 2)
end
if callback ~= nil and not isfunction(callback) then
error("bad argument #2 to 'WriteStream' (function expected, got " .. type(callback) .. ")", 2)
end
local compressed = not dontcompress
if compressed then
data = util.Compress(data) or ""
end
if #data == 0 then
net.WriteUInt(0, 32)
return
end
local numchunks = math.ceil(#data / net.Stream.SendSize)
if CLIENT and numchunks > net.Stream.MaxServerChunks then
ErrorNoHalt("net.WriteStream request is too large! ", #data/1048576, "MiB")
net.WriteUInt(0, 32)
return
end
local chunks = {}
for i=1, numchunks do
local datachunk = string.sub(data, (i - 1) * net.Stream.SendSize + 1, i * net.Stream.SendSize)
chunks[i] = {
data = datachunk,
crc = util.CRC(datachunk),
}
end
local startid = identifier
while net.Stream.WriteStreams[identifier] do
identifier = identifier % 1024 + 1
if identifier == startid then
ErrorNoHalt("Netstream is full of WriteStreams!")
net.WriteUInt(0, 32)
return
end
end
local stream = {
identifier = identifier,
chunks = chunks,
compressed = compressed,
numchunks = numchunks,
callback = callback,
clients = setmetatable({},{__index = function(t,k)
local r = {
finished = false,
downloads = 0,
keepalives = 0,
progress = 0,
} t[k]=r return r
end})
}
setmetatable(stream, net.Stream.WriteStream)
net.Stream.WriteStreams[identifier] = stream
timer.Create("NetStreamWriteTimeout" .. identifier, net.Stream.Timeout, 1, function() stream:Remove() end)
net.WriteUInt(numchunks, 32)
net.WriteUInt(identifier, 32)
net.WriteBool(compressed)
return stream
end
--If the receiver is a player then add it to a queue.
--If the receiver is the server then add it to a queue for each individual player
function net.ReadStream(ply, callback)
if CLIENT then
ply = NULL
else
if type(ply) ~= "Player" then
error("bad argument #1 to 'ReadStream' (Player expected, got " .. type(ply) .. ")", 2)
elseif not ply:IsValid() then
error("bad argument #1 to 'ReadStream' (Tried to use a NULL entity!)", 2)
end
end
if not isfunction(callback) then
error("bad argument #2 to 'ReadStream' (function expected, got " .. type(callback) .. ")", 2)
end
local queue = net.Stream.ReadStreamQueues[ply]
if queue then
if SERVER and #queue == net.Stream.MaxServerReadStreams then
ErrorNoHalt("Receiving too many ReadStream requests from ", ply)
return
end
else
queue = {} net.Stream.ReadStreamQueues[ply] = queue
end
local numchunks = net.ReadUInt(32)
if numchunks == nil then
return
elseif numchunks == 0 then
local ok, err = xpcall(callback, debug.traceback, "")
if not ok then ErrorNoHalt(err) end
return
end
if SERVER and numchunks > net.Stream.MaxServerChunks then
ErrorNoHalt("ReadStream requests from ", ply, " is too large! ", numchunks * net.Stream.SendSize / 1048576, "MiB")
return
end
local identifier = net.ReadUInt(32)
local compressed = net.ReadBool()
--print("Got info", numchunks, identifier, compressed)
for _, v in ipairs(queue) do
if v.identifier == identifier then
ErrorNoHalt("Tried to start a new ReadStream for an already existing stream!")
return
end
end
local stream = {
identifier = identifier,
chunks = {},
compressed = compressed,
numchunks = numchunks,
callback = callback,
queue = queue,
player = ply,
downloads = 0
}
setmetatable(stream, net.Stream.ReadStream)
queue[#queue + 1] = stream
if #queue > 1 then
timer.Create("NetStreamKeepAlive" .. identifier, net.Stream.Timeout / 2, 0, function()
net.Start("NetStreamRequest")
net.WriteUInt(identifier, 32)
net.WriteBit(true)
if CLIENT then net.SendToServer() else net.Send(ply) end
end)
else
stream:Request()
end
return stream
end
if SERVER then
util.AddNetworkString("NetStreamRequest")
util.AddNetworkString("NetStreamDownload")
end
--Stream data is requested
net.Receive("NetStreamRequest", function(len, ply)
local identifier = net.ReadUInt(32)
local stream = net.Stream.WriteStreams[identifier]
if stream then
ply = ply or NULL
local client = stream.clients[ply]
if not client.finished then
local keepalive = net.ReadBit() == 1
if keepalive then
if client.keepalives < net.Stream.MaxKeepalive then
client.keepalives = client.keepalives + 1
timer.Adjust("NetStreamWriteTimeout" .. identifier, net.Stream.Timeout, 1)
end
else
local completed = net.ReadBit() == 1
if completed then
stream:Finished(ply)
else
if client.downloads < net.Stream.MaxTries * #stream.chunks then
client.downloads = client.downloads + 1
stream:Write(ply)
timer.Adjust("NetStreamWriteTimeout" .. identifier, net.Stream.Timeout, 1)
else
client.finished = true
end
end
end
end
end
end)
--Download the stream data
net.Receive("NetStreamDownload", function(len, ply)
ply = ply or NULL
local queue = net.Stream.ReadStreamQueues[ply]
if queue then
local size = net.ReadUInt(32)
if size > 0 then
queue[1]:Read(size)
else
local id = net.ReadUInt(32)
for k, v in ipairs(queue) do
if v.identifier == id then
v:Remove()
break
end
end
end
end
end)

44
lua/autorun/prop2mesh.lua Normal file
View File

@ -0,0 +1,44 @@
--[[
]]
if not prop2mesh then prop2mesh = {} end
--[[
]]
local validClasses = { ["sent_prop2mesh"] = true, ["sent_prop2mesh_legacy"] = true }
function prop2mesh.isValid(self)
return IsValid(self) and validClasses[self:GetClass()]
end
prop2mesh.defaultmat = "hunter/myplastic"
--[[
]]
if SERVER then
AddCSLuaFile("prop2mesh/cl_meshlab.lua")
AddCSLuaFile("prop2mesh/cl_modelfixer.lua")
AddCSLuaFile("prop2mesh/cl_editor.lua")
include("prop2mesh/sv_entparts.lua")
include("prop2mesh/sv_editor.lua")
function prop2mesh.getEmpty()
return {
crc = "!none",
uvs = 0,
col = Color(255, 255, 255, 255),
mat = prop2mesh.defaultmat,
scale = Vector(1, 1, 1),
}
end
elseif CLIENT then
include("prop2mesh/cl_meshlab.lua")
include("prop2mesh/cl_modelfixer.lua")
include("prop2mesh/cl_editor.lua")
end

View File

@ -0,0 +1,370 @@
E2Lib.RegisterExtension("prop2mesh", true, "Allows E2 chips to create and manipulate prop2mesh entities")
local E2Lib, WireLib, prop2mesh, math =
E2Lib, WireLib, prop2mesh, math
--[[
]]
local _COL = 1
local _MAT = 2
local _POS = 3
local _ANG = 4
local _SCALE = 5
local _UVS = 6
local _PARENT = 7
local _MODEL = 8
local _NODRAW = 9
local _BUILD = 10
local cooldowns = {}
cooldowns[_BUILD] = 10
cooldowns[_UVS] = 10
local errors = {}
errors[_BUILD] = "\nDon't spam e:p2mBuild"
errors[_UVS] = "\nDon't spam e:p2mSetUV"
local function canspam(check, wait, time)
if not check or time - check > wait then
return true
end
return false
end
local function antispam(self, action, index)
if not self.prop2mesh_e2_antispam then
self.prop2mesh_e2_antispam = {}
end
local time = CurTime()
local wait = cooldowns[action]
if not index then
if self.prop2mesh_e2_antispam[action] == time then
return false
end
if not wait or (wait and canspam(self.prop2mesh_e2_antispam[action], wait, time)) then
self.prop2mesh_e2_antispam[action] = time
return true
end
error(errors[action])
return false
else
if not self.prop2mesh_e2_antispam[index] then
self.prop2mesh_e2_antispam[index] = {}
end
if self.prop2mesh_e2_antispam[index][action] == time then
return false
end
if not wait or (wait and canspam(self.prop2mesh_e2_antispam[index][action], wait, time)) then
self.prop2mesh_e2_antispam[index][action] = time
return true
end
error(errors[action])
return false
end
end
local function checkvalid(context, self, action, index, restricted)
if not E2Lib.isOwner(context, self) or not prop2mesh.isValid(self) then
return false
end
if restricted and not self.prop2mesh_e2_resevoir then
return false
end
if index and not self.prop2mesh_controllers[index] then
error(string.format("\ncontroller index %d does not exist on %s!", index, tostring(self)))
end
if action then
return antispam(self, action, index)
end
return true
end
--[[
]]
__e2setcost(10)
e2function void entity:p2mSetColor(number index, vector color)
if checkvalid(self, this, _COL, index) then
this:SetControllerCol(index, Color(color[1], color[2], color[3]))
end
end
e2function void entity:p2mSetColor(number index, vector4 color)
if checkvalid(self, this, _COL, index) then
this:SetControllerCol(index, Color(color[1], color[2], color[3], color[4]))
end
end
e2function void entity:p2mSetMaterial(number index, string material)
if checkvalid(self, this, _MAT, index) then
this:SetControllerMat(index, WireLib.IsValidMaterial(material))
end
end
e2function void entity:p2mSetScale(number index, vector scale)
if checkvalid(self, this, _SCALE, index) then
this:SetControllerScale(index, Vector(scale[1], scale[2], scale[3]))
end
end
e2function void entity:p2mSetUV(number index, number uvs)
if checkvalid(self, this, _UVS, index) then
this:SetControllerUVS(index, math.Clamp(math.floor(math.abs(uvs)), 0, 512))
end
end
--[[
]]
e2function void entity:p2mSetPos(vector pos)
if checkvalid(self, this, _POS) then
WireLib.setPos(this, Vector(pos[1], pos[2], pos[3]))
end
end
e2function void entity:p2mSetAng(angle ang)
if checkvalid(self, this, _ANG) then
WireLib.setAng(this, Angle(ang[1], ang[2], ang[3]))
end
end
e2function void entity:p2mSetNodraw(number bool)
if checkvalid(self, this, _NODRAW) then
this:SetNoDraw(tobool(bool))
end
end
e2function void entity:p2mSetModel(string model)
if checkvalid(self, this, _MODEL, nil, true) then
this:SetModel(model)
end
end
--[[
]]
__e2setcost(25)
local function Check_Parents(child, parent)
while IsValid(parent:GetParent()) do
parent = parent:GetParent()
if parent == child then
return false
end
end
return true
end
e2function void entity:p2mSetParent(entity parent)
if not IsValid(parent) or not E2Lib.isOwner(self, parent) or not checkvalid(self, this, _PARENT) then
return
end
if not Check_Parents(this, parent) then
return
end
if parent:GetParent() and parent:GetParent():IsValid() and parent:GetParent() == this then
return
end
this:SetParent(parent)
end
--[[
]]
registerCallback("construct", function(self)
self.data.prop2mesh = {}
end)
registerCallback("destruct", function(self)
for ent, mode in pairs(self.data.prop2mesh) do
if ent then
ent:Remove()
end
end
end)
local function p2mCreate(context, pos, ang, count)
if not count or count < 1 then
count = 1
end
count = math.ceil(count)
if count > 16 then
error("controller limit is 16 per entity")
end
local self = ents.Create("sent_prop2mesh")
self:SetNoDraw(true)
self:SetModel("models/hunter/plates/plate.mdl")
WireLib.setPos(self, pos)
WireLib.setAng(self, ang)
self:Spawn()
if not IsValid(self) then
return NULL
end
if CPPI then
self:CPPISetOwner(context.player)
end
self:SetSolid(SOLID_NONE)
self:SetMoveType(MOVETYPE_NONE)
self:DrawShadow(false)
self:Activate()
self:CallOnRemove("wire_expression2_p2m", function(e)
context.data.prop2mesh[e] = nil
end)
context.data.prop2mesh[self] = true
self.DoNotDuplicate = true
self.prop2mesh_e2_resevoir = {}
for i = 1, count do
self:AddController()
end
return self
end
__e2setcost(50)
e2function entity p2mCreate(number count, vector pos, angle ang)
return p2mCreate(self, Vector(pos[1], pos[2], pos[3]), Angle(ang[1], ang[2], ang[3]), count)
end
--[[
]]
__e2setcost(100)
local function p2mBuild(context, self)
for k, v in pairs(self.prop2mesh_e2_resevoir) do
if self.prop2mesh_controllers[k] then
self:SetControllerData(k, v)
end
end
self.prop2mesh_e2_resevoir = {}
end
e2function number entity:p2mBuild()
if not checkvalid(self, this, _BUILD, nil, true) then
return
end
p2mBuild(self, this)
end
--[[
]]
__e2setcost(5)
local function toVec(vec)
return vec and Vector(vec[1], vec[2], vec[3]) or Vector()
end
local function toAng(ang)
return ang and Angle(ang[1], ang[2], ang[3]) or Angle()
end
local function isVector(op0)
return istable(op0) and #op0 == 3 or type(op0) == "Vector"
end
local function errorcheck(context, self, index)
if not self.prop2mesh_controllers[index] then
error(string.format("\ncontroller index %d does not exist on %s!", index, tostring(self)))
end
if not self.prop2mesh_e2_resevoir[index] then
self.prop2mesh_e2_resevoir[index] = {}
end
if #self.prop2mesh_e2_resevoir[index] + 1 > 250 then
error("model limit is 250 per controller")
end
end
local function checkClips(clips)
if #clips == 0 or #clips % 2 ~= 0 then
return
end
local swap = {}
for i = 1, #clips, 2 do
local op1 = clips[i]
local op2 = clips[i + 1]
if not isVector(op1) or not isVector(op2) then
goto CONTINUE
end
local normal = toVec(op2)
normal:Normalize()
swap[#swap + 1] = { d = toVec(op1):Dot(normal), n = normal }
::CONTINUE::
end
return swap
end
local function p2mPushModel(context, self, index, model, pos, ang, scale, clips, vinside, vsmooth)
errorcheck(context, self, index)
if scale then
scale = toVec(scale)
if scale.x == 1 and scale.y == 1 and scale.z == 1 then
scale = nil
end
end
if clips then clips = checkClips(clips) end
self.prop2mesh_e2_resevoir[index][#self.prop2mesh_e2_resevoir[index] + 1] = {
prop = model,
pos = toVec(pos),
ang = toAng(ang),
scale = scale,
clips = clips,
vinside = tobool(vinside) and 1 or nil,
vsmooth = tobool(vsmooth) and 1 or nil,
}
return #self.prop2mesh_e2_resevoir[index]
end
e2function void entity:p2mPushModel(index, string model, vector pos, angle ang)
if checkvalid(self, this, nil, index, true) then
p2mPushModel(self, this, index, model, pos, ang)
end
end
e2function void entity:p2mPushModel(index, string model, vector pos, angle ang, number renderinside, number renderflat)
if checkvalid(self, this, nil, index, true) then
p2mPushModel(self, this, index, model, pos, ang, nil, nil, renderinside, renderflat)
end
end
e2function void entity:p2mPushModel(index, string model, vector pos, angle ang, number renderinside, number renderflat, array clips)
if checkvalid(self, this, nil, index, true) then
p2mPushModel(self, this, index, model, pos, ang, nil, clips, renderinside, renderflat)
end
end
e2function void entity:p2mPushModel(index, string model, vector pos, angle ang, vector scale)
if checkvalid(self, this, nil, index, true) then
p2mPushModel(self, this, index, model, pos, ang, scale)
end
end
e2function void entity:p2mPushModel(index, string model, vector pos, angle ang, vector scale, number renderinside, number renderflat)
if checkvalid(self, this, nil, index, true) then
p2mPushModel(self, this, index, model, pos, ang, scale, nil, renderinside, renderflat)
end
end
e2function void entity:p2mPushModel(index, string model, vector pos, angle ang, vector scale, number renderinside, array clips)
if checkvalid(self, this, nil, index, true) then
p2mPushModel(self, this, index, model, pos, ang, scale, clips, renderinside)
end
end
e2function void entity:p2mPushModel(index, string model, vector pos, angle ang, vector scale, number renderinside, number renderflat, array clips)
if checkvalid(self, this, nil, index, true) then
p2mPushModel(self, this, index, model, pos, ang, scale, clips, renderinside, renderflat)
end
end

View File

@ -0,0 +1,537 @@
--[[
]]
include("shared.lua")
local prop2mesh = prop2mesh
local defaultmat = Material(prop2mesh.defaultmat)
local debugwhite = Material("models/debug/debugwhite")
local wireframe = Material("models/wireframe")
local net = net
local cam = cam
local table = table
local render = render
local string = string
local empty = { Mesh = Mesh(), Material = Material("models/debug/debugwhite") }
empty.Mesh:BuildFromTriangles({{pos = Vector()},{pos = Vector()},{pos = Vector()}})
--[[
]]
local recycle = {}
local garbage = {}
function prop2mesh.getMeshInfo(crc, uvs)
local mdata = recycle[crc] and recycle[crc].meshes[uvs]
if mdata then
return mdata.pcount, mdata.vcount
end
return
end
function prop2mesh.getMeshData(crc, unzip)
local dat = recycle[crc] and recycle[crc].zip
if not unzip or not dat then
return dat
end
return util.JSONToTable(util.Decompress(dat))
end
concommand.Add("prop2mesh_dump", function()
PrintTable(recycle)
PrintTable(garbage)
end)
timer.Create("prop2trash", 1, 0, function()
local curtime = SysTime()
for crc, usedtime in pairs(garbage) do
if curtime - usedtime > 3 then
if recycle[crc] and recycle[crc].meshes then
for uvs, meshdata in pairs(recycle[crc].meshes) do
if meshdata.basic then
if IsValid(meshdata.basic.Mesh) then
print("destroying", meshdata.basic.Mesh)
meshdata.basic.Mesh:Destroy()
meshdata.basic.Mesh = nil
end
end
if meshdata.complex then
for m, meshpart in pairs(meshdata.complex) do
if IsValid(meshpart) then
print("destroying", meshpart)
meshpart:Destroy()
meshdata.complex[m] = nil
end
end
end
end
end
recycle[crc] = nil
garbage[crc] = nil
end
end
end)
local function checkdownload(self, crc)
if recycle[crc] then
return true
end
recycle[crc] = { users = {}, meshes = {} }
net.Start("prop2mesh_download")
net.WriteEntity(self)
net.WriteString(crc)
net.SendToServer()
return false
end
local function setuser(self, crc, bool)
if not recycle[crc] then
garbage[crc] = nil
return
end
if bool then
recycle[crc].users[self] = true
garbage[crc] = nil
else
recycle[crc].users[self] = nil
if not next(recycle[crc].users) then
garbage[crc] = SysTime()
end
end
end
local function checkmesh(crc, uvs)
if not recycle[crc] or not recycle[crc].zip or recycle[crc].meshes[uvs] then
return recycle[crc].meshes[uvs]
end
recycle[crc].meshes[uvs] = {}
prop2mesh.getMesh(crc, uvs, recycle[crc].zip)
end
hook.Add("prop2mesh_hook_meshdone", "prop2mesh_meshlab", function(crc, uvs, mdata)
if not mdata or not crc or not uvs then
return
end
recycle[crc].meshes[uvs] = mdata
if #mdata.meshes == 1 then
local imesh = Mesh()
imesh:BuildFromTriangles(mdata.meshes[1])
mdata.basic = { Mesh = imesh, Material = defaultmat }
else
mdata.complex = {}
for i = 1, #mdata.meshes do
local imesh = Mesh()
imesh:BuildFromTriangles(mdata.meshes[i])
mdata.complex[i] = imesh
end
end
mdata.meshes = nil
mdata.ready = true
local mins = mdata.vmins
local maxs = mdata.vmaxs
if mins and maxs then
for user in pairs(recycle[crc].users) do
for k, info in pairs(user.prop2mesh_controllers) do
if IsValid(info.ent) and info.crc == crc and info.uvs == uvs then
info.ent:SetRenderBounds(mins, maxs)
end
end
end
end
end)
--[[
]]
local cvar = CreateClientConVar("prop2mesh_render_disable", 0, true, false)
local draw_disable = cvar:GetBool()
cvars.AddChangeCallback("prop2mesh_render_disable", function(cvar, old, new)
draw_disable = tobool(new)
end, "swapdrawdisable")
local draw_wireframe
concommand.Add("prop2mesh_render_wireframe", function(ply, cmd, args)
draw_wireframe = not draw_wireframe
end)
--[[
]]
local function getComplex(crc, uvs)
local meshes = recycle[crc] and recycle[crc].meshes[uvs]
return meshes and meshes.complex
end
local vec = Vector()
local function drawModel(self)
if draw_disable then
local min, max = self:GetRenderBounds()
local color = self:GetColor()
vec.x = color.r/255
vec.y = color.g/255
vec.z = color.b/255
debugwhite:SetVector("$color", vec)
render.SetMaterial(debugwhite)
render.DrawBox(self:GetPos(), self:GetAngles(), min, max)
render.DrawWireframeBox(self:GetPos(), self:GetAngles(), min, max, color_black, true)
return
end
if draw_wireframe and self.isowner then
render.SetBlend(1)
render.SetColorModulation(1, 1, 1)
render.ModelMaterialOverride(wireframe)
self:DrawModel()
render.ModelMaterialOverride()
else
self:DrawModel()
end
local complex = getComplex(self.crc, self.uvs)
if complex then
local matrix = self:GetWorldTransformMatrix()
if self.scale then
matrix:SetScale(self.scale)
end
cam.PushModelMatrix(matrix)
for i = 1, #complex do
complex[i]:Draw()
end
cam.PopModelMatrix()
end
end
local function drawMesh(self)
local meshes = recycle[self.crc] and recycle[self.crc].meshes[self.uvs]
return meshes and meshes.basic or empty
end
local matrix = Matrix()
local function refresh(self, info)
if not IsValid(info.ent) then
info.ent = ents.CreateClientside("base_anim")
info.ent:SetModel("models/hunter/plates/plate.mdl")
info.ent:DrawShadow(false)
info.ent.Draw = drawModel
info.ent.GetRenderMesh = drawMesh
info.ent:Spawn()
info.ent:Activate()
end
info.ent:SetParent(self)
info.ent:SetPos(self:GetPos())
info.ent:SetAngles(self:GetAngles())
info.ent:SetMaterial(info.mat)
info.ent:SetColor(info.col)
info.ent:SetRenderMode(info.col.a == 255 and RENDERMODE_NORMAL or RENDERMODE_TRANSCOLOR)
info.ent.RenderGroup = info.col.a == 255 and RENDERGROUP_OPAQUE or RENDERGROUP_BOTH
if info.scale.x ~= 1 or info.scale.y ~= 1 or info.scale.z ~= 1 then
matrix:SetScale(info.scale)
info.ent:EnableMatrix("RenderMultiply", matrix)
info.ent.scale = info.scale
else
info.ent:DisableMatrix("RenderMultiply")
info.ent.scale = nil
end
info.ent.crc = info.crc
info.ent.uvs = info.uvs
info.ent.isowner = self.isowner
if checkdownload(self, info.crc) then
local mdata = checkmesh(info.crc, info.uvs)
if mdata and mdata.ready then
info.ent:SetRenderBounds(mdata.vmins, mdata.vmaxs)
end
end
setuser(self, info.crc, true)
end
local function refreshAll(self, prop2mesh_controllers)
for k, info in pairs(prop2mesh_controllers) do
refresh(self, info)
end
end
local function discard(self, prop2mesh_controllers)
for _, info in pairs(prop2mesh_controllers) do
if info.ent and IsValid(info.ent) then
info.ent:Remove()
info.ent = nil
end
setuser(self, info.crc, false)
end
end
--[[
]]
function ENT:Initialize()
self.prop2mesh_controllers = {}
end
function ENT:Draw()
self:DrawModel()
end
function ENT:Think()
if not self.prop2mesh_sync then
if CPPI then
self.isowner = self:CPPIGetOwner() == LocalPlayer()
else
self.isowner = true
end
refreshAll(self, self.prop2mesh_controllers)
net.Start("prop2mesh_sync")
net.WriteEntity(self)
net.WriteString(self.prop2mesh_synctime or "")
net.SendToServer()
self.prop2mesh_refresh = nil
self.prop2mesh_sync = true
end
if self.prop2mesh_refresh then
refreshAll(self, self.prop2mesh_controllers)
self.prop2mesh_refresh = nil
end
end
function ENT:OnRemove()
local snapshot = self.prop2mesh_controllers
if not snapshot or next(snapshot) == nil then
return
end
timer.Simple(0, function()
if IsValid(self) then
return
end
discard(self, snapshot)
end)
end
function ENT:GetAllDataReady()
for k, info in ipairs(self.prop2mesh_controllers) do
local crc = info.crc
if not crc or crc == "!none" then
goto CONTINUE
end
if not recycle[crc] or not recycle[crc].zip then
return false
else
local meshes = recycle[crc].meshes[info.uvs]
if meshes and not meshes.ready then
return false
end
end
::CONTINUE::
end
return true
end
function ENT:GetDownloadProgress()
local max
for i = 1, #self.prop2mesh_controllers do
local stream = recycle[self.prop2mesh_controllers[i].crc].stream
if stream then
if not max then max = 0 end
local progress = stream:GetProgress()
if max < progress then
max = progress
end
end
end
return max
end
--[[
]]
local kvpass = {}
kvpass.crc = function(self, info, val)
local crc = info.crc
info.crc = val
local keepdata
for k, v in pairs(self.prop2mesh_controllers) do
if v.crc == crc then
keepdata = true
break
end
end
if not keepdata then
setuser(self, crc, false)
end
end
local function safeuvs(val)
val = math.abs(math.floor(tonumber(val) or 0))
if val > 512 then val = 512 end
return val
end
kvpass.uvs = function(self, info, val)
info.uvs = safeuvs(val)
end
-- https:--github.com/wiremod/wire/blob/1a0c31105d5a02a243cf042ea413867fb569ab4c/lua/wire/wireshared.lua#L56
local function normalizedFilepath(path)
local null = string.find(path, "\x00", 1, true)
if null then
path = string.sub(path, 1, null - 1)
end
local tbl = string.Explode("[/\\]+", path, true)
local i = 1
while i <= #tbl do
if tbl[i] == "." or tbl[i] == "" then
table.remove(tbl, i)
elseif tbl[i] == ".." then
table.remove(tbl, i)
if i > 1 then
i = i - 1
table.remove(tbl, i)
end
else
i = i + 1
end
end
return table.concat(tbl, "/")
end
local baddies = {
["effects/ar2_altfire1"] = true,
["engine/writez"] = true,
["pp/copy"] = true,
}
local function safemat(val)
val = string.sub(val, 1, 260)
local path = string.StripExtension(normalizedFilepath(string.lower(val)))
if baddies[path] then return "" end
return val
end
kvpass.mat = function(self, info, val)
info.mat = safemat(val)
end
--[[
]]
net.Receive("prop2mesh_update", function(len)
local self = net.ReadEntity()
if not prop2mesh.isValid(self) then
return
end
local synctime = net.ReadString()
for index, update in pairs(net.ReadTable()) do
local info = self.prop2mesh_controllers[index]
if not info then
self.prop2mesh_sync = nil
return
end
for key, val in pairs(update) do
if kvpass[key] then kvpass[key](self, info, val) else info[key] = val end
end
refresh(self, info)
end
self.prop2mesh_synctime = synctime
self.prop2mesh_triggertool = true
self.prop2mesh_triggereditor = prop2mesh.editor and true
end)
net.Receive("prop2mesh_sync", function(len)
local self = net.ReadEntity()
if not prop2mesh.isValid(self) then
return
end
discard(self, self.prop2mesh_controllers)
self.prop2mesh_synctime = net.ReadString()
self.prop2mesh_controllers = {}
for i = 1, net.ReadUInt(8) do
self.prop2mesh_controllers[i] = {
crc = net.ReadString(),
uvs = safeuvs(net.ReadUInt(12)),
mat = safemat(net.ReadString()),
col = Color(net.ReadUInt(8), net.ReadUInt(8), net.ReadUInt(8), net.ReadUInt(8)),
scale = Vector(net.ReadFloat(), net.ReadFloat(), net.ReadFloat()),
index = i,
}
end
self.prop2mesh_refresh = true
self.prop2mesh_triggertool = true
self.prop2mesh_triggereditor = prop2mesh.editor and true
end)
prop2mesh.downloads = 0
net.Receive("prop2mesh_download", function(len)
local crc = net.ReadString()
recycle[crc].stream = net.ReadStream(nil, function(data)
if not recycle[crc] then
recycle[crc] = { users = {}, meshes = {} }
end
if crc == util.CRC(data) then
recycle[crc].zip = data
for user in pairs(recycle[crc].users) do
for k, info in pairs(user.prop2mesh_controllers) do
if info.crc == crc then
checkmesh(crc, info.uvs)
end
end
end
else
garbage[crc] = SysTime() + 500
end
recycle[crc].stream = nil
prop2mesh.downloads = math.max(0, prop2mesh.downloads - 1)
end)
prop2mesh.downloads = prop2mesh.downloads + 1
end)
hook.Add("NotifyShouldTransmit", "prop2mesh_sync", function(self, bool)
if bool then self.prop2mesh_sync = nil end
end)
hook.Add("OnGamemodeLoaded", "prop2mesh_sync", function()
for k, self in ipairs(ents.FindByClass("sent_prop2mesh*")) do
self.prop2mesh_sync = nil
end
end)

View File

@ -0,0 +1,407 @@
--[[
]]
AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")
include("shared.lua")
local net = net
local util = util
local table = table
local pairs = pairs
local ipairs = ipairs
local prop2mesh = prop2mesh
--[[
]]
util.AddNetworkString("prop2mesh_sync")
util.AddNetworkString("prop2mesh_update")
util.AddNetworkString("prop2mesh_download")
net.Receive("prop2mesh_sync", function(len, pl)
local self = net.ReadEntity()
if not prop2mesh.isValid(self) then
return
end
if not self.prop2mesh_syncwith then
self.prop2mesh_syncwith = {}
end
self.prop2mesh_syncwith[pl] = net.ReadString()
end)
net.Receive("prop2mesh_download", function(len, pl)
local self = net.ReadEntity()
if not prop2mesh.isValid(self) then
return
end
local crc = net.ReadString()
if self.prop2mesh_partlists[crc] then
net.Start("prop2mesh_download")
net.WriteString(crc)
net.WriteStream(self.prop2mesh_partlists[crc])
net.Send(pl)
end
end)
--[[
]]
function ENT:Initialize()
self:DrawShadow(false)
self:PhysicsInit(SOLID_VPHYSICS)
self:SetMoveType(MOVETYPE_VPHYSICS)
self:SetSolid(SOLID_VPHYSICS)
self.prop2mesh_controllers = {}
self.prop2mesh_partlists = {}
self.prop2mesh_sync = true
end
function ENT:Think()
if self.prop2mesh_upload_queue then
self:SetNetworkedBool("uploading", true)
return
end
if self.prop2mesh_sync then
self.prop2mesh_updates = nil
self.prop2mesh_synctime = SysTime() .. ""
self.prop2mesh_syncwith = nil
self.prop2mesh_sync = nil
self:SendControllers()
else
if self.prop2mesh_syncwith then
local syncwith = {}
for pl, pltime in pairs(self.prop2mesh_syncwith) do
if IsValid(pl) and pltime ~= self.prop2mesh_synctime then
syncwith[#syncwith + 1] = pl
end
end
if next(syncwith) then
self:SendControllers(syncwith)
end
self.prop2mesh_syncwith = nil
end
if self.prop2mesh_updates then
self.prop2mesh_synctime = SysTime() .. ""
net.Start("prop2mesh_update")
net.WriteEntity(self)
net.WriteString(self.prop2mesh_synctime)
for index, update in pairs(self.prop2mesh_updates) do
for key in pairs(update) do
update[key] = self.prop2mesh_controllers[index][key]
end
end
net.WriteTable(self.prop2mesh_updates)
net.Broadcast()
self.prop2mesh_updates = nil
else
if self:GetNetworkedBool("uploading") then
self:SetNetworkedBool("uploading", false)
end
end
end
end
function ENT:PreEntityCopy()
duplicator.StoreEntityModifier(self, "prop2mesh", {
[1] = self.prop2mesh_controllers,
[2] = self.prop2mesh_partlists,
})
end
function ENT:PostEntityCopy()
duplicator.ClearEntityModifier(self, "prop2mesh")
end
function ENT:PostEntityPaste()
duplicator.ClearEntityModifier(self, "prop2mesh")
end
duplicator.RegisterEntityModifier("prop2mesh", function(ply, self, dupe)
if not prop2mesh.isValid(self) then
return
end
local dupe_controllers = dupe[1]
if istable(dupe_controllers) and next(dupe_controllers) and table.IsSequential(dupe_controllers) then
self.prop2mesh_sync = true
local dupe_data = dupe[2]
local dupe_lookup = {}
self.prop2mesh_controllers = {}
for k, v in ipairs(dupe_controllers) do
local info = self:AddController()
self:SetControllerCol(k, v.col)
self:SetControllerMat(k, v.mat)
self:SetControllerUVS(k, v.uvs)
self:SetControllerScale(k, v.scale)
if dupe_data and dupe_data[v.crc] then
dupe_lookup[v.crc] = true
info.crc = v.crc
end
end
self.prop2mesh_partlists = {}
for crc in pairs(dupe_lookup) do
self.prop2mesh_partlists[crc] = dupe_data[crc]
end
end
end)
function ENT:AddController()
table.insert(self.prop2mesh_controllers, prop2mesh.getEmpty())
self.prop2mesh_sync = true
return self.prop2mesh_controllers[#self.prop2mesh_controllers]
end
function ENT:RemoveController(index)
if not self.prop2mesh_controllers[index] then
return false
end
local crc = self.prop2mesh_controllers[index].crc
table.remove(self.prop2mesh_controllers, index)
local keepdata
for k, info in pairs(self.prop2mesh_controllers) do
if info.crc == crc then
keepdata = true
break
end
end
if not keepdata then
self.prop2mesh_partlists[crc] = nil
end
self.prop2mesh_sync = true
return true
end
function ENT:SendControllers(syncwith)
net.Start("prop2mesh_sync")
net.WriteEntity(self)
net.WriteString(self.prop2mesh_synctime)
net.WriteUInt(#self.prop2mesh_controllers, 8)
for i = 1, #self.prop2mesh_controllers do
local info = self.prop2mesh_controllers[i]
net.WriteString(info.crc)
net.WriteUInt(info.uvs, 12)
net.WriteString(info.mat)
net.WriteUInt(info.col.r, 8)
net.WriteUInt(info.col.g, 8)
net.WriteUInt(info.col.b, 8)
net.WriteUInt(info.col.a, 8)
net.WriteFloat(info.scale.x)
net.WriteFloat(info.scale.y)
net.WriteFloat(info.scale.z)
end
if syncwith then
net.Send(syncwith)
else
net.Broadcast()
end
end
function ENT:AddControllerUpdate(index, key)
if self.prop2mesh_sync or not self.prop2mesh_controllers[index] then
return
end
if not self.prop2mesh_updates then self.prop2mesh_updates = {} end
if not self.prop2mesh_updates[index] then self.prop2mesh_updates[index] = {} end
self.prop2mesh_updates[index][key] = true
end
function ENT:SetControllerCol(index, val)
local info = self.prop2mesh_controllers[index]
if (info and IsColor(val)) and (info.col.r ~= val.r or info.col.g ~= val.g or info.col.b ~= val.b or info.col.a ~= val.a) then
info.col.r = val.r
info.col.g = val.g
info.col.b = val.b
info.col.a = val.a
self:AddControllerUpdate(index, "col")
end
end
function ENT:SetControllerMat(index, val)
local info = self.prop2mesh_controllers[index]
if (info and isstring(val)) and (info.mat ~= val) then
info.mat = val
self:AddControllerUpdate(index, "mat")
end
end
function ENT:SetControllerScale(index, val)
local info = self.prop2mesh_controllers[index]
if (info and isvector(val)) and (info.scale.x ~= val.x or info.scale.y ~= val.y or info.scale.z ~= val.z) then
info.scale.x = val.x
info.scale.y = val.y
info.scale.z = val.z
self:AddControllerUpdate(index, "scale")
end
end
function ENT:SetControllerUVS(index, val)
local info = self.prop2mesh_controllers[index]
if (info and isnumber(val)) and (info.uvs ~= val) then
info.uvs = val
self:AddControllerUpdate(index, "uvs")
end
end
function ENT:ResetControllerData(index)
if self.prop2mesh_controllers[index] then
self.prop2mesh_controllers[index].crc = "!none"
self:AddControllerUpdate(index, "crc")
end
end
function ENT:SetControllerData(index, partlist, uvs)
local info = self.prop2mesh_controllers[index]
if not info or not partlist then
return
end
if not next(partlist) then
self:ResetControllerData(index)
return
end
prop2mesh.sanitizeCustom(partlist)
local json = util.TableToJSON(partlist)
if not json then
return
end
local data = util.Compress(json)
local dcrc = util.CRC(data)
local icrc = info.crc
if icrc == dcrc then
return
end
self.prop2mesh_partlists[dcrc] = data
info.crc = dcrc
self:AddControllerUpdate(index, "crc")
if uvs then
info.uvs = uvs
self:AddControllerUpdate(index, "uvs")
end
local keepdata
for k, v in pairs(self.prop2mesh_controllers) do
if v.crc == icrc then
keepdata = true
break
end
end
if not keepdata then
self.prop2mesh_partlists[icrc] = nil
end
end
function ENT:GetControllerData(index, nodecomp)
if not self.prop2mesh_controllers[index] then
return
end
local ret = self.prop2mesh_partlists[self.prop2mesh_controllers[index].crc]
if not ret or nodecomp then
return ret
end
return util.JSONToTable(util.Decompress(ret))
end
function ENT:ToolDataByINDEX(index, tool)
if not self.prop2mesh_controllers[index] then
return false
end
local pos = self:GetPos()
local ang = self:GetAngles()
if tool:GetClientNumber("tool_setautocenter") ~= 0 then
pos = Vector()
local num = 0
for ent, _ in pairs(tool.selection) do
pos = pos + ent:GetPos()
num = num + 1
end
pos = pos * (1 / num)
end
self:SetControllerData(index, prop2mesh.partsFromEnts(tool.selection, pos, ang), tool:GetClientNumber("tool_setuvsize"))
end
function ENT:ToolDataAUTO(tool)
local autocenter = tool:GetClientNumber("tool_setautocenter") ~= 0
local pos, ang, num
if autocenter then
pos = Vector()
ang = self:GetAngles()
num = 0
else
pos = self:GetPos()
ang = self:GetAngles()
end
local sorted = {}
for k, v in pairs(tool.selection) do
local vmat = v.mat
if vmat == "" then vmat = prop2mesh.defaultmat end
if not sorted[vmat] then
sorted[vmat] = {}
end
local key = string.format("%d %d %d %d", v.col.r, v.col.g, v.col.b, v.col.a)
if not sorted[vmat][key] then
sorted[vmat][key] = {}
end
table.insert(sorted[vmat][key], k)
if autocenter then
pos = pos + k:GetPos()
num = num + 1
end
end
if autocenter then
pos = pos * (1 / num)
end
local uvs = tool:GetClientNumber("tool_setuvsize")
for kmat, vmat in pairs(sorted) do
for kcol, vcol in pairs(vmat) do
local parts = prop2mesh.partsFromEnts(vcol, pos, ang)
if parts then
local info = self:AddController()
local temp = string.Explode(" ", kcol)
info.col = Color(temp[1], temp[2], temp[3], temp[4])
info.mat = kmat
info.uvs = parts.uvs or uvs
if parts.uvs then parts.uvs = nil end
self:SetControllerData(#self.prop2mesh_controllers, parts)
end
end
end
end

View File

@ -0,0 +1,98 @@
--[[
]]
DEFINE_BASECLASS("base_anim")
ENT.PrintName = "sent_prop2mesh"
ENT.Author = "shadowscion"
ENT.AdminOnly = false
ENT.Spawnable = true
ENT.Category = "prop2mesh"
ENT.RenderGroup = RENDERGROUP_BOTH
cleanup.Register("sent_prop2mesh")
function ENT:SpawnFunction(ply, tr, ClassName)
if not tr.Hit then
return
end
local ent = ents.Create(ClassName)
ent:SetModel("models/p2m/cube.mdl")
ent:SetPos(tr.HitPos + tr.HitNormal)
ent:Spawn()
ent:Activate()
return ent
end
function ENT:GetControllerCol(index)
return self.prop2mesh_controllers[index] and self.prop2mesh_controllers[index].col
end
function ENT:GetControllerMat(index)
return self.prop2mesh_controllers[index] and self.prop2mesh_controllers[index].mat
end
function ENT:GetControllerUVS(index)
return self.prop2mesh_controllers[index] and self.prop2mesh_controllers[index].uvs
end
function ENT:GetControllerCRC(index)
return self.prop2mesh_controllers[index] and self.prop2mesh_controllers[index].crc
end
function ENT:GetControllerScale(index)
return self.prop2mesh_controllers[index] and self.prop2mesh_controllers[index].scale
end
--[[
]]
properties.Add("prop2mesh", {
MenuLabel = "Edit prop2mesh",
MenuIcon = "icon16/image_edit.png",
PrependSpacer = true,
Order = 3001,
Filter = function(self, ent, pl)
return prop2mesh.isValid(ent) and gamemode.Call("CanProperty", pl, "prop2mesh", ent)
end,
Action = function(self, ent) -- CLIENT
if not self:Filter(ent, LocalPlayer()) then
if IsValid(prop2mesh.editor) then
prop2mesh.editor:Remove()
end
return
end
if not IsValid(prop2mesh.editor) then
prop2mesh.editor = g_ContextMenu:Add("prop2mesh_editor")
elseif prop2mesh.editor.Entity == ent then
return
end
local h = math.floor(ScrH() - 90)
local w = 420
prop2mesh.editor:SetPos(ScrW() - w - 30, ScrH() - h - 30)
prop2mesh.editor:SetSize(w, h)
prop2mesh.editor:SetDraggable(false)
if IsValid(prop2mesh.editor.Entity) then
prop2mesh.editor.Entity:RemoveCallOnRemove("prop2mesh_editor_close")
end
prop2mesh.editor.Entity = ent
prop2mesh.editor.Entity:CallOnRemove("prop2mesh_editor_close", function()
prop2mesh.editor:Remove()
end)
prop2mesh.editor:SetTitle(tostring(prop2mesh.editor.Entity))
prop2mesh.editor:RemakeTree()
end,
Receive = function(self, len, pl) -- SERVER
end
})

View File

@ -0,0 +1,139 @@
AddCSLuaFile()
DEFINE_BASECLASS("sent_prop2mesh")
ENT.PrintName = "prop2mesh_legacy"
ENT.Author = "shadowscion"
ENT.AdminOnly = false
ENT.Spawnable = true
ENT.Category = "prop2mesh"
ENT.RenderGroup = RENDERGROUP_BOTH
cleanup.Register("sent_prop2mesh_legacy")
if CLIENT then
return
end
function ENT:Think()
local info = self.prop2mesh_controllers[1]
if info then
local val = self:GetColor()
if info.col.r ~= val.r or info.col.g ~= val.g or info.col.b ~= val.b or info.col.a ~= val.a then
info.col.r = val.r
info.col.g = val.g
info.col.b = val.b
info.col.a = val.a
self:AddControllerUpdate(1, "col")
end
local val = self:GetMaterial()
if info.mat ~= val then
info.mat = val
self:AddControllerUpdate(1, "mat")
end
end
BaseClass.Think(self)
end
function ENT:AddController(uvs, scale)
for k, v in pairs(self.prop2mesh_controllers) do
self:RemoveController(k)
end
BaseClass.AddController(self)
self:SetControllerUVS(1, uvs)
self:SetControllerScale(1, scale)
return self.prop2mesh_controllers[1]
end
function ENT:PostEntityPaste()
duplicator.ClearEntityModifier(self, "p2m_mods")
duplicator.ClearEntityModifier(self, "p2m_packets")
duplicator.ClearEntityModifier(self, "prop2mesh")
end
-- COMPATIBILITY
local function getLegacyMods(data)
local uvs, scale
if istable(data) then
uvs = tonumber(data.tscale)
scale = tonumber(data.mscale)
if scale then
scale = Vector(scale, scale, scale)
end
end
return uvs, scale
end
local function getLegacyParts(data)
local parts
if istable(data) then
local zip = {}
for i = 1, #data do
zip[#zip + 1] = data[i][1]
end
zip = table.concat(zip)
if util.CRC(zip) == data.crc then
local json = util.JSONToTable(util.Decompress(zip))
if next(json) then
parts = {}
for k, v in ipairs(json) do
local part = { pos = v.pos, ang = v.ang, clips = v.clips, bodygroup = v.bgrp }
if v.scale and (v.scale.x ~= 1 or v.scale.y ~= 1 or v.scale.z ~= 1) then
part.scale = v.scale
end
if v.obj then
local crc = util.CRC(v.obj)
if not parts.custom then parts.custom = {} end
parts.custom[crc] = v.obj
part.objd = crc
part.objn = v.name or crc
part.vsmooth = tonumber(v.smooth)
part.vinvert = v.flip and 1 or nil
else
if v.holo then part.holo = v.mdl else part.prop = v.mdl end
part.vinside = v.inv and 1 or nil
part.vsmooth = v.flat and 1 or nil
end
parts[#parts + 1] = part
end
end
end
end
return parts
end
local function getLegacyInfo(data)
if not data then return nil end
local uvs, scale = getLegacyMods(data.p2m_mods)
return uvs, scale, getLegacyParts(data.p2m_packets)
end
duplicator.RegisterEntityClass("gmod_ent_p2m", function(ply, data)
local compat = ents.Create("sent_prop2mesh_legacy")
if not IsValid(compat) then
return false
end
duplicator.DoGeneric(compat, data)
compat:Spawn()
compat:Activate()
if CPPI and compat.CPPISetOwner then
compat:CPPISetOwner(ply)
end
local uvs, scale, parts = getLegacyInfo(data.EntityMods)
compat:AddController(uvs, scale)
compat:SetControllerData(1, parts)
return compat
end, "Data")

690
lua/prop2mesh/cl_editor.lua Normal file
View File

@ -0,0 +1,690 @@
local string = string
local table = table
local math = math
local net = net
--[[
uploader
]]
local filecache_data = {}
local filecache_keys = {}
local upstreams = {}
net.Receive("prop2mesh_upload_start", function(len)
local eid = net.ReadUInt(16)
for i = 1, net.ReadUInt(8) do
local crc = net.ReadString()
if filecache_keys[crc] and filecache_keys[crc].data then
net.Start("prop2mesh_upload")
net.WriteUInt(eid, 16)
net.WriteString(crc)
upstreams[crc] = net.WriteStream(filecache_keys[crc].data)
net.SendToServer()
end
end
end)
local function upstreamProgress()
local max = 0
for crc, stream in pairs(upstreams) do
local client = next(stream.clients)
if client and stream.clients[client] then
client = stream.clients[client]
if client.finished then
upstreams[crc] = nil
else
local progress = client.progress / stream.numchunks
if max < progress then
max = progress
end
end
end
end
return max
end
local function formatOBJ(filestr)
local vcount = 0
local condensed = {}
for line in string.gmatch(filestr, "(.-)\n") do
local temp = string.Explode(" ", string.gsub(string.Trim(line), "%s+", " "))
local head = table.remove(temp, 1)
if head == "f" then
local v1 = string.Explode("/", temp[1])
local v2 = string.Explode("/", temp[2])
for i = 3, #temp do
local v3 = string.Explode("/", temp[i])
condensed[#condensed + 1] = string.format("f %d %d %d\n", v1[1], v2[1], v3[1])
v2 = v3
end
else
if head == "v" then
local x = tonumber(temp[1])
local y = tonumber(temp[2])
local z = tonumber(temp[3])
x = math.abs(x) < 1e-4 and 0 or x
y = math.abs(y) < 1e-4 and 0 or y
z = math.abs(z) < 1e-4 and 0 or z
condensed[#condensed + 1] = string.format("v %s %s %s\n", x, y, z)
vcount = vcount + 1
end
end
if vcount > 63999 then return end
end
return table.concat(condensed)
end
--[[
skin and panel overrides
]]
local theme = {}
theme.font = "prop2mesheditor"
surface.CreateFont(theme.font, { size = 15, weight = 400, font = "Roboto Mono" })
theme.colorText_add = Color(100, 200, 100)
theme.colorText_edit = Color(100, 100, 255)
theme.colorText_kill = Color(255, 100, 100)
theme.colorText_default = Color(100, 100, 100)
theme.colorMain = Color(75, 75, 75)
theme.colorTree = Color(245, 245, 245)
local TreeAddNode, NodeAddNode
function TreeAddNode(self, text, icon, font)
local node = DTree.AddNode(self, string.lower(text), icon)
node.Label:SetFont(font or theme.font)
node.Label:SetTextColor(theme.colorText_default)
node.AddNode = NodeAddNode
return node
end
function NodeAddNode(self, text, icon, font)
local node = DTree_Node.AddNode(self, string.lower(text), icon)
node.Label:SetFont(font or theme.font)
node.Label:SetTextColor(theme.colorText_default)
node.AddNode = NodeAddNode
return node
end
--[[
editor components
]]
local function changetable(partnode, key, diff)
if not partnode.mod then
if partnode.set then
partnode.set[key] = diff and partnode.new[key] or nil
local color = next(partnode.set) and theme.colorText_edit
partnode.Label:SetTextColor(color or theme.colorText_default)
partnode.Icon:SetImageColor(color or color_white)
end
return
end
if diff then
if not partnode.mod[partnode.num] then
partnode.mod[partnode.num] = {}
end
partnode.mod[partnode.num][key] = partnode.new[key]
local color = partnode.mod[partnode.num].kill and theme.colorText_kill or theme.colorText_edit
partnode.Label:SetTextColor(color)
partnode.Icon:SetImageColor(color)
elseif partnode.mod[partnode.num] then
partnode.mod[partnode.num][key] = nil
if not next(partnode.mod[partnode.num]) then
partnode.mod[partnode.num] = nil
partnode.Label:SetTextColor(theme.colorText_default)
partnode.Icon:SetImageColor(color_white)
end
end
end
local function callbackVector(partnode, name, text, key, i, val)
if partnode.new[key][i] == val then
return
end
partnode.new[key][i] = val
if partnode.new[key][i] ~= partnode.old[key][i] then
name.Label:SetTextColor((partnode.mod or partnode.set) and theme.colorText_edit or theme.colorText_add)
text:SetTextColor((partnode.mod or partnode.set) and theme.colorText_edit or theme.colorText_add)
changetable(partnode, key, true)
else
name.Label:SetTextColor(theme.colorText_default)
text:SetTextColor(theme.colorText_default)
changetable(partnode, key, partnode.new[key] ~= partnode.old[key])
end
end
local function registerVector(partnode, name, key)
local node = partnode:AddNode(name, "icon16/bullet_black.png"):AddNode("")
node.ShowIcons = function() return false end
node:SetDrawLines(false)
local x = vgui.Create("DTextEntry", node)
local y = vgui.Create("DTextEntry", node)
local z = vgui.Create("DTextEntry", node)
node.PerformLayout = function(self, w, h)
DTree_Node.PerformLayout(self, w, h)
local spacing = 4
local cellWidth = math.ceil((w - 48) / 3) - spacing
x:SetPos(24, 0)
x:SetSize(cellWidth, h)
y:SetPos(24 + cellWidth + spacing, 0)
y:SetSize(cellWidth, h)
z:SetPos(24 + (cellWidth + spacing) * 2, 0)
z:SetSize(cellWidth, h)
end
for i, v in ipairs({x, y, z}) do
v:SetFont(theme.font)
v:SetNumeric(true)
v.OnValueChange = function(self, val)
if not tonumber(val) then
self:SetText(string.format("%.4f", partnode.new[key][i]))
return
end
self:SetText(string.format("%.4f", val))
callbackVector(partnode, node:GetParentNode(), self, key, i, val)
end
v:SetValue(partnode.new[key][i])
end
end
local function callbackBoolean(partnode, name, key, val)
if partnode.new[key] == val then
return
end
partnode.new[key] = val
if partnode.new[key] ~= partnode.old[key] then
name:SetTextColor((partnode.mod or partnode.set) and theme.colorText_edit or theme.colorText_add)
changetable(partnode, key, true)
else
name:SetTextColor(theme.colorText_default)
changetable(partnode, key, false)
end
end
local function registerBoolean(partnode, name, key)
local node = partnode:AddNode("")
node.ShowIcons = function() return false end
local x = vgui.Create("DCheckBoxLabel", node)
x:SetText(name)
x:Dock(LEFT)
x:DockMargin(24, 0, 4, 0)
x.Label:SetDisabled(true)
x.OnChange = function(self, val)
callbackBoolean(partnode, self, key, val and 1 or 0)
end
x:SetValue(partnode.new[key] == 1)
x:SetTextColor(theme.colorText_default)
x:SetFont(theme.font)
end
local function registerFloat(partnode, name, key, min, max)
local node = partnode:AddNode("")
node.ShowIcons = function() return false end
local x = vgui.Create("DCheckBoxLabel", node)
x:Dock(LEFT)
x:DockMargin(24, 0, 4, 0)
x:SetText(name)
x:SetFont(theme.font)
x:SetTextColor(theme.colorText_default)
local s = vgui.Create("DNumSlider", node)
s.Scratch:SetVisible(false)
s.Label:SetVisible(false)
s:SetWide(128)
s:DockMargin(24, 0, 4, 0)
s:Dock(LEFT)
s:SetMin(min)
s:SetMax(max)
s:SetDecimals(0)
s.OnValueChanged = function(self, val)
x:SetChecked(val > 0)
callbackBoolean(partnode, x, key, math.Round(val))
end
x.OnChange = function(self, value)
self:SetChecked(s:GetValue() > 0)
end
s:SetValue(partnode.new[key])
end
--[[
menus
]]
local function installEditors(partnode)
registerVector(partnode, "pos", "pos")
registerVector(partnode, "ang", "ang")
registerVector(partnode, "scale", "scale")
registerBoolean(partnode, "render_inside", "vinside")
if partnode.new.objd then
registerBoolean(partnode, "render_invert", "vinvert")
registerFloat(partnode, "render_smooth", "vsmooth", 0, 180)
else
registerBoolean(partnode, "render_flat", "vsmooth")
end
partnode:ExpandRecurse(true)
partnode.edited = true
end
local function partcopy(from)
local a = { pos = Vector(), ang = Angle(), scale = Vector(1,1,1), vinvert = 0, vinside = 0, vsmooth = 0 }
local b = { pos = Vector(), ang = Angle(), scale = Vector(1,1,1), vinvert = 0, vinside = 0, vsmooth = 0 }
for k, v in pairs(from) do
if isnumber(v) or isstring(v) then
a[k] = v
b[k] = v
elseif isvector(v) then
a[k] = Vector(v)
b[k] = Vector(v)
elseif isangle(v) then
a[k] = Angle(v)
b[k] = Angle(v)
end
end
return a, b
end
local function partmenu(frame, partnode)
local menu = DermaMenu()
if not partnode.edited then
menu:AddOption("edit part", function()
installEditors(partnode)
end):SetIcon("icon16/brick_edit.png")
end
menu:AddOption(partnode.new.kill and "undo remove part" or "remove part", function()
partnode.new.kill = not partnode.new.kill
changetable(partnode, "kill", partnode.new.kill)
partnode:SetExpanded(false, true)
end):SetIcon("icon16/brick_delete.png")
menu:AddOption("cancel"):SetIcon("icon16/cancel.png")
menu:Open()
end
local function objmenu(frame, objnode)
local menu = DermaMenu()
menu:AddOption("remove model", function()
objnode:Remove()
objnode = nil
end):SetIcon("icon16/brick_delete.png")
menu:AddOption("cancel"):SetIcon("icon16/cancel.png")
menu:Open()
end
local function attach(pathnode)
local filepath = pathnode.path
local filestr = file.Read(pathnode.path)
local filecrc = tostring(util.CRC(filestr))
if not filecache_data[filecrc] then
local valid, contents = pcall(formatOBJ, filestr)
if not valid then
chat.AddText(Color(255, 125, 125), "unexpected error!")
return
end
if not contents then
chat.AddText(Color(255, 125, 125), ".obj must have fewer than 64000 vertices!")
return
end
local crc = tostring(util.CRC(contents))
filecache_data[filecrc] = { crc = crc, data = util.Compress(contents) }
filecache_keys[crc] = filecache_data[filecrc]
end
if not filecache_data[filecrc] then
chat.AddText(Color(255, 125, 125), "unexpected error!")
return
end
local filename = string.GetFileFromFilename(pathnode.path)
local rootnode = pathnode:GetParentNode()
local partnode = rootnode.list:AddNode(string.format("[new!] %s", filename), "icon16/brick.png")
partnode.Label:SetTextColor(theme.colorText_add)
partnode.Icon:SetImageColor(theme.colorText_add)
partnode.menu = objmenu
partnode.new, partnode.old = partcopy({ objn = filename, objd = filecache_data[filecrc].crc })
rootnode.add[partnode] = true
installEditors(partnode)
partnode:ExpandTo(true)
end
local function filemenu(frame, pathnode)
local menu = DermaMenu()
menu:AddOption("attach model", function()
attach(pathnode)
end):SetIcon("icon16/brick_add.png")
menu:AddOption("cancel"):SetIcon("icon16/cancel.png")
menu:Open()
end
local function conmenu(frame, conroot)
end
--[[
derma
]]
local PANEL = {}
function PANEL:CreateGhost()
self.Ghost = ents.CreateClientside("base_anim")
self.Ghost:SetMaterial("models/wireframe")
self.Ghost:SetNoDraw(true)
self.Ghost.Draw = function(ent)
cam.IgnoreZ(true)
ent:DrawModel()
cam.IgnoreZ(false)
end
self.Ghost:Spawn()
end
function PANEL:Init()
self:CreateGhost()
self.updates = {}
self.contree = vgui.Create("DTree", self)
self.contree:Dock(FILL)
self.contree.AddNode = TreeAddNode
self.contree:SetClickOnDragHover(true)
self.contree.DoRightClick = function(pnl, node)
if node.menu then node.menu(self, node) end
end
self.contree.Paint = function(pnl, w, h)
surface.SetDrawColor(theme.colorTree)
surface.DrawRect(0, 0, w, h)
surface.SetDrawColor(0, 0, 0)
surface.DrawOutlinedRect(0, 0, w, h)
end
self.contree:DockMargin(1, 1, 1, 1)
self.confirm = vgui.Create("DButton", self)
self.confirm:Dock(BOTTOM)
self.confirm:DockMargin(0, 2, 0, 0)
self.confirm:SetText("Confirm changes")
self.confirm.DoClick = function()
if not IsValid(self.Entity) then
return false
end
local set = {}
local add = {}
local mod = {}
for k, v in pairs(self.updates) do
if next(v.set) then
if not set[k] then
set[k] = {}
end
for i, j in pairs(v.set) do
set[k][i] = j
end
if not next(set[k]) then
set[k] = nil
end
end
if next(v.add) then
if not add[k] then
add[k] = {}
end
for i in pairs(v.add) do
if IsValid(i) then
add[k][#add[k] + 1] = i.new
end
end
if not next(add[k]) then
add[k] = nil
end
end
if next(v.mod) then
if not mod[k] then
mod[k] = {}
end
for i, j in pairs(v.mod) do
mod[k][i] = j.kill and { kill = true } or j
end
end
end
if next(set) or next(add) or next(mod) then
net.Start("prop2mesh_upload_start")
net.WriteUInt(self.Entity:EntIndex(), 16)
for k, v in ipairs({set, add, mod}) do
if next(v) then
net.WriteBool(true)
net.WriteTable(v)
else
net.WriteBool(false)
end
end
net.SendToServer()
end
end
self.confirm:DockMargin(1, 1, 1, 1)
self.progress = vgui.Create("DPanel", self)
self.progress:Dock(BOTTOM)
self.progress:DockMargin(1, 1, 1, 1)
self.progress:SetTall(16)
self.progress.Paint = function(pnl, w, h)
surface.SetDrawColor(theme.colorTree)
surface.DrawRect(0, 0, w, h)
if pnl.frac then
surface.SetDrawColor(0, 255, 0)
surface.DrawRect(0, 0, pnl.frac*w, h)
if pnl.text then
draw.SimpleText(pnl.text, theme.font, w*0.5, h*0.5, theme.colorText_default, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
end
end
surface.SetDrawColor(0, 0, 0)
surface.DrawOutlinedRect(0, 0, w, h)
end
end
function PANEL:Paint(w, h)
surface.SetDrawColor(theme.colorMain)
surface.DrawRect(0, 0, w, h)
surface.SetDrawColor(0, 0, 0)
surface.DrawOutlinedRect(0, 0, w, h)
end
function PANEL:OnRemove()
if IsValid(self.Entity) then
self.Entity:RemoveCallOnRemove("prop2mesh_editor_close")
self.Entity.prop2mesh_triggereditor = nil
end
if IsValid(self.Ghost) then
self.Ghost:Remove()
end
end
function PANEL:Think()
if not IsValid(self.Entity) then
return
end
if self.Entity.prop2mesh_triggereditor then
self.Entity.prop2mesh_triggereditor = nil
self:RemakeTree()
return
end
if self.Entity:GetNetworkedBool("uploading", false) then
if not self.contree:GetDisabled() or not self.disable then
self.contree:SetDisabled(true)
self.confirm:SetDisabled(true)
self.disable = true
end
self.progress.frac = upstreamProgress()
self.progress.text = "uploading..."
else
if self.contree:GetDisabled() or self.disable then
if self.Entity:GetAllDataReady() then
self.contree:SetDisabled(false)
self.confirm:SetDisabled(false)
self.disable = nil
self:RemakeTree()
self.progress.frac = nil
else
local frac = self.Entity:GetDownloadProgress()
self.progress.frac = frac or 1
self.progress.text = frac and "downloading..." or "building mesh..."
end
end
end
end
local matrix = Matrix()
local function onPartHover(label)
local self = prop2mesh.editor
if not self then
return
end
local partnode = label:GetParent()
if self.contree:GetSelectedItem() ~= partnode then
self.contree:SetSelectedItem(partnode)
if partnode.new and (partnode.new.holo or partnode.new.prop) then
self.Ghost:SetNoDraw(false)
self.Ghost:SetModel(partnode.new.holo or partnode.new.prop)
local pos, ang = LocalToWorld(partnode.new.pos, partnode.new.ang, self.Entity:GetPos(), self.Entity:GetAngles())
self.Ghost:SetParent(self.Entity)
self.Ghost:SetPos(pos)
self.Ghost:SetAngles(ang)
if partnode.new.scale then
matrix:SetScale(partnode.new.scale)
self.Ghost:EnableMatrix("RenderMultiply", matrix)
else
self.GHost:DisableMatrix("RenderMultiply")
end
else
self.Ghost:SetNoDraw(true)
end
label:InvalidateLayout(true)
end
end
function PANEL:RemakeTree()
self.contree:Clear()
self.updates = {}
local files, filenodes = {}, {}
for k, v in ipairs(file.Find("p2m/*.txt", "DATA")) do table.insert(files, v) end
for k, v in ipairs(file.Find("p2m/*.obj", "DATA")) do table.insert(files, v) end
for i = 1, #self.Entity.prop2mesh_controllers do
self.updates[i] = { mod = {}, add = {}, set = {} }
local condata = prop2mesh.getMeshData(self.Entity.prop2mesh_controllers[i].crc, true) or {}
local conroot = self.contree:AddNode(string.format("controller %d [%d]", i, #condata), "icon16/image.png")
conroot.num = i
conroot.menu = conmenu
local setroot = conroot:AddNode("settings", "icon16/cog.png")
setroot.set = self.updates[i].set
setroot.old = { uvs = self.Entity.prop2mesh_controllers[i].uvs, scale = Vector(self.Entity.prop2mesh_controllers[i].scale) }
setroot.new = { uvs = self.Entity.prop2mesh_controllers[i].uvs, scale = Vector(self.Entity.prop2mesh_controllers[i].scale) }
registerVector(setroot, "mesh scale", "scale")
registerFloat(setroot, "texture size", "uvs", 0, 512)
setroot:ExpandRecurse(true)
local objroot = conroot:AddNode(".obj", "icon16/pictures.png")
local objfile = objroot:AddNode("files", "icon16/bullet_disk.png")
local objlist = objroot:AddNode("attachments", "icon16/bullet_picture.png")
local mdllist = conroot:AddNode(".mdl", "icon16/images.png")
filenodes[#filenodes + 1] = objfile
objfile.list = objlist
objfile.add = self.updates[i].add
for k, v in ipairs(condata) do
local root = v.objd and objlist or mdllist
local part = root:AddNode(string.format("[%d] %s", k, string.GetFileFromFilename(v.objn or v.objd or v.prop or v.holo)))
part:SetIcon("icon16/brick.png")
part.Label.OnCursorEntered = onPartHover
part.menu = partmenu
part.new, part.old = partcopy(v)
part.mod = self.updates[i].mod
part.num = k
end
end
for k, v in SortedPairs(files) do
local path = string.format("p2m/%s", v)
for _, filenode in pairs(filenodes) do
local pathnode = filenode:AddNode(path, "icon16/page_white_text.png")
pathnode.menu = filemenu
pathnode.path = path
end
end
end
vgui.Register("prop2mesh_editor", PANEL, "DFrame")

View File

@ -0,0 +1,676 @@
--[[
]]
local prop2mesh = prop2mesh
local util = util
local string = string
local coroutine = coroutine
local notification = notification
local next = next
local SysTime = SysTime
local tonumber = tonumber
local Vector = Vector
local vec = Vector()
local div = vec.Div
local mul = vec.Mul
local add = vec.Add
local dot = vec.Dot
local cross = vec.Cross
local normalize = vec.Normalize
local rotate = vec.Rotate
local math_cos = math.cos
local math_rad = math.rad
local math_abs = math.abs
local math_min = math.min
local math_max = math.max
local string_format = string.format
local string_explode = string.Explode
local string_gsub = string.gsub
local string_trim = string.Trim
local table_concat = table.concat
local table_remove = table.remove
local a90 = Angle(0, -90, 0)
--[[
]]
local function calcbounds(min, max, pos)
if pos.x < min.x then min.x = pos.x elseif pos.x > max.x then max.x = pos.x end
if pos.y < min.y then min.y = pos.y elseif pos.y > max.y then max.y = pos.y end
if pos.z < min.z then min.z = pos.z elseif pos.z > max.z then max.z = pos.z end
end
local function copy(v)
return {
pos = Vector(v.pos),
normal = Vector(v.normal),
u = v.u,
v = v.v,
rotate = v.rotate,
}
end
local function sign(n)
if n == 0 then
return 0
else
return n > 0 and 1 or -1
end
end
local function getBoxDir(vec)
local x, y, z = math_abs(vec.x), math_abs(vec.y), math_abs(vec.z)
if x > y and x > z then
return vec.x < -0 and -1 or 1
elseif y > z then
return vec.y < 0 and -2 or 2
end
return vec.z < 0 and -3 or 3
end
local function getBoxUV(vert, dir, scale)
if dir == -1 or dir == 1 then
return vert.z * sign(dir) * scale, vert.y * scale
elseif dir == -2 or dir == 2 then
return vert.x * scale, vert.z * sign(dir) * scale
else
return vert.x * -sign(dir) * scale, vert.y * scale
end
end
local function clip(v1, v2, plane, length, getUV)
local d1 = dot(v1.pos, plane) - length
local d2 = dot(v2.pos, plane) - length
local t = d1 / (d1 - d2)
local vert = {
pos = v1.pos + t * (v2.pos - v1.pos),
normal = v1.normal + t * (v2.normal - v1.normal),
rotate = v1.rotate or v2.rotate,
}
if getUV then
vert.u = v1.u + t * (v2.u - v1.u)
vert.v = v1.v + t * (v2.v - v1.v)
end
return vert
end
-- method https:--github.com/chenchenyuyu/DEMO/blob/b6bf971a302c71403e0e34e091402982dfa3cd2d/app/src/pages/vr/decal/decalGeometry.js#L102
local function applyClippingPlane(verts, plane, length, getUV)
local temp = {}
for i = 1, #verts, 3 do
local d1 = length - dot(verts[i + 0].pos, plane)
local d2 = length - dot(verts[i + 1].pos, plane)
local d3 = length - dot(verts[i + 2].pos, plane)
local ov1 = d1 > 0
local ov2 = d2 > 0
local ov3 = d3 > 0
local total = (ov1 and 1 or 0) + (ov2 and 1 or 0) + (ov3 and 1 or 0)
local nv1, nv2, nv3, nv4
if total == 0 then
temp[#temp + 1] = verts[i + 0]
temp[#temp + 1] = verts[i + 1]
temp[#temp + 1] = verts[i + 2]
elseif total == 1 then
if ov1 then
nv1 = verts[i + 1]
nv2 = verts[i + 2]
nv3 = clip(verts[i + 0], nv1, plane, length, getUV)
nv4 = clip(verts[i + 0], nv2, plane, length, getUV)
temp[#temp + 1] = copy(nv1)
temp[#temp + 1] = copy(nv2)
temp[#temp + 1] = nv3
temp[#temp + 1] = nv4
temp[#temp + 1] = copy(nv3)
temp[#temp + 1] = copy(nv2)
elseif ov2 then
nv1 = verts[i + 0]
nv2 = verts[i + 2]
nv3 = clip(verts[i + 1], nv1, plane, length, getUV)
nv4 = clip(verts[i + 1], nv2, plane, length, getUV)
temp[#temp + 1] = nv3
temp[#temp + 1] = copy(nv2)
temp[#temp + 1] = copy(nv1)
temp[#temp + 1] = copy(nv2)
temp[#temp + 1] = copy(nv3)
temp[#temp + 1] = nv4
elseif ov3 then
nv1 = verts[i + 0]
nv2 = verts[i + 1]
nv3 = clip(verts[i + 2], nv1, plane, length, getUV)
nv4 = clip(verts[i + 2], nv2, plane, length, getUV)
temp[#temp + 1] = copy(nv1)
temp[#temp + 1] = copy(nv2)
temp[#temp + 1] = nv3
temp[#temp + 1] = nv4
temp[#temp + 1] = copy(nv3)
temp[#temp + 1] = copy(nv2)
end
elseif total == 2 then
if not ov1 then
nv1 = copy(verts[i + 0])
nv2 = clip(nv1, verts[i + 1], plane, length, getUV)
nv3 = clip(nv1, verts[i + 2], plane, length, getUV)
temp[#temp + 1] = nv1
temp[#temp + 1] = nv2
temp[#temp + 1] = nv3
elseif not ov2 then
nv1 = copy(verts[i + 1])
nv2 = clip(nv1, verts[i + 2], plane, length, getUV)
nv3 = clip(nv1, verts[i + 0], plane, length, getUV)
temp[#temp + 1] = nv1
temp[#temp + 1] = nv2
temp[#temp + 1] = nv3
elseif not ov3 then
nv1 = copy(verts[i + 2])
nv2 = clip(nv1, verts[i + 0], plane, length, getUV)
nv3 = clip(nv1, verts[i + 1], plane, length, getUV)
temp[#temp + 1] = nv1
temp[#temp + 1] = nv2
temp[#temp + 1] = nv3
end
end
end
return temp
end
--[[
]]
local meshmodelcache
local function getVertsFromMDL(partnext, meshtex, vmins, vmaxs)
local modelpath = partnext.prop or partnext.holo
if prop2mesh.isBlockedModel(modelpath) then
return
end
local submeshes
if meshmodelcache[modelpath] then
submeshes = meshmodelcache[modelpath][partnext.bodygroup or 0]
else
meshmodelcache[modelpath] = {}
end
if not submeshes then
submeshes = util.GetModelMeshes(modelpath, 0, partnext.bodygroup or 0)
if not submeshes then
return
end
submeshes.modelfixer = prop2mesh.getModelFix(modelpath)
submeshes.modelfixergeneric = isbool(submeshes.modelfixer)
meshmodelcache[modelpath][partnext.bodygroup or 0] = submeshes
end
local partpos = partnext.pos
local partang = partnext.ang
local partscale = partnext.scale
local partclips = partnext.clips
local submeshfixlookup
if submeshes.modelfixer then
local rotated = Angle(partang)
rotated:RotateAroundAxis(rotated:Up(), 90)
submeshfixlookup = {}
for submeshid = 1, #submeshes do
if submeshes.modelfixergeneric then
submeshfixlookup[submeshid] = { ang = rotated }
else
local ang = submeshes.modelfixer(submeshid, #submeshes, rotated, partang) or rotated
submeshfixlookup[submeshid] = { ang = ang, diff = ang ~= rotated }
end
end
if partscale then
if partnext.holo then
partscale = Vector(partscale.y, partscale.x, partscale.z)
else
partscale = Vector(partscale.x, partscale.z, partscale.y)
end
end
if partclips then
local clips = {}
for clipid = 1, #partclips do
local normal = Vector(partclips[clipid].n)
rotate(normal, a90)
clips[#clips + 1] = {
d = partclips[clipid].d,
no = partclips[clipid].n,
n = normal,
}
end
partclips = clips
end
end
local partverts = {}
local modeluv = not meshtex
for submeshid = 1, #submeshes do
local submeshdata = submeshes[submeshid].triangles
local submeshfix = submeshfixlookup and submeshfixlookup[submeshid]
local submeshverts = {}
for vertid = 1, #submeshdata do
local vert = submeshdata[vertid]
local pos = Vector(vert.pos)
local normal = Vector(vert.normal)
if partscale then
if submeshfix and submeshfix.diff then
pos.x = pos.x * partnext.scale.x
pos.y = pos.y * partnext.scale.y
pos.z = pos.z * partnext.scale.z
else
pos.x = pos.x * partscale.x
pos.y = pos.y * partscale.y
pos.z = pos.z * partscale.z
end
end
local vcopy = {
pos = pos,
normal = normal,
rotate = submeshfix,
}
if modeluv then
vcopy.u = vert.u
vcopy.v = vert.v
end
submeshverts[#submeshverts + 1] = vcopy
end
if partclips then
if submeshfix then
for clipid = 1, #partclips do
submeshverts = applyClippingPlane(submeshverts, submeshfix.diff and partclips[clipid].no or partclips[clipid].n, partclips[clipid].d, modeluv)
end
else
for clipid = 1, #partclips do
submeshverts = applyClippingPlane(submeshverts, partclips[clipid].n, partclips[clipid].d, modeluv)
end
end
end
for vertid = 1, #submeshverts do
local vert = submeshverts[vertid]
if vert.rotate then
rotate(vert.normal, vert.rotate.ang or partang)
rotate(vert.pos, vert.rotate.ang or partang)
vert.rotate = nil
else
rotate(vert.normal, partang)
rotate(vert.pos, partang)
end
add(vert.pos, partpos)
partverts[#partverts + 1] = vert
calcbounds(vmins, vmaxs, vert.pos)
end
end
if #partverts == 0 then
return
end
local nflat = partnext.vsmooth == 1
if meshtex or nflat then
for pv = 1, #partverts, 3 do
local normal = cross(partverts[pv + 2].pos - partverts[pv].pos, partverts[pv + 1].pos - partverts[pv].pos)
normalize(normal)
if nflat then
partverts[pv ].normal = Vector(normal)
partverts[pv + 1].normal = Vector(normal)
partverts[pv + 2].normal = Vector(normal)
end
if meshtex then
local boxDir = getBoxDir(normal)
partverts[pv ].u, partverts[pv ].v = getBoxUV(partverts[pv ].pos, boxDir, meshtex)
partverts[pv + 1].u, partverts[pv + 1].v = getBoxUV(partverts[pv + 1].pos, boxDir, meshtex)
partverts[pv + 2].u, partverts[pv + 2].v = getBoxUV(partverts[pv + 2].pos, boxDir, meshtex)
end
end
end
return partverts
end
local function getVertsFromOBJ(custom, partnext, meshtex, vmins, vmaxs)
local modeluid = tonumber(partnext.objd)
local modelobj = custom[modeluid]
if not modelobj then
return
end
local pos = partnext.pos
local ang = partnext.ang
local scale = partnext.scale
if pos.x == 0 and pos.y == 0 and pos.z == 0 then pos = nil end
if ang.p == 0 and ang.y == 0 and ang.r == 0 then ang = nil end
if scale then
if scale.x == 1 and scale.y == 1 and scale.z == 1 then scale = nil end
end
local vlook = {}
local vmesh = {}
local meshtex = meshtex or 1 / 48
local invert = partnext.vinvert
local smooth = partnext.vsmooth
for line in string.gmatch(modelobj, "(.-)\n") do
local temp = string_explode(" ", string_gsub(string_trim(line), "%s+", " "))
local head = table_remove(temp, 1)
if head == "f" then
local f1 = string_explode("/", temp[1])
local f2 = string_explode("/", temp[2])
for i = 3, #temp do
local f3 = string_explode("/", temp[i])
local v1, v2, v3
if invert then
v1 = { pos = Vector(vlook[tonumber(f3[1])]) }
v2 = { pos = Vector(vlook[tonumber(f2[1])]) }
v3 = { pos = Vector(vlook[tonumber(f1[1])]) }
else
v1 = { pos = Vector(vlook[tonumber(f1[1])]) }
v2 = { pos = Vector(vlook[tonumber(f2[1])]) }
v3 = { pos = Vector(vlook[tonumber(f3[1])]) }
end
local normal = cross(v3.pos - v1.pos, v2.pos - v1.pos)
normalize(normal)
v1.normal = Vector(normal)
v2.normal = Vector(normal)
v3.normal = Vector(normal)
local boxDir = getBoxDir(normal)
v1.u, v1.v = getBoxUV(v1.pos, boxDir, meshtex)
v2.u, v2.v = getBoxUV(v2.pos, boxDir, meshtex)
v3.u, v3.v = getBoxUV(v3.pos, boxDir, meshtex)
vmesh[#vmesh + 1] = v1
vmesh[#vmesh + 1] = v2
vmesh[#vmesh + 1] = v3
f2 = f3
end
end
if head == "v" then
local vert = Vector(tonumber(temp[1]), tonumber(temp[2]), tonumber(temp[3]))
if scale then
vert.x = vert.x * scale.x
vert.y = vert.y * scale.y
vert.z = vert.z * scale.z
end
if ang then
rotate(vert, ang)
end
if pos then
add(vert, pos)
end
vlook[#vlook + 1] = vert
calcbounds(vmins, vmaxs, vert)
end
coroutine.yield(false)
end
-- https:--github.com/thegrb93/StarfallEx/blob/b6de9fbe84040e9ebebcbe858c30adb9f7d937b5/lua/starfall/libs_sh/mesh.lua#L229
-- credit to Sevii
if smooth and smooth ~= 0 then
local smoothrad = math_cos(math_rad(smooth))
if smoothrad ~= 1 then
local norms = setmetatable({},{__index = function(t,k) local r=setmetatable({},{__index=function(t,k) local r=setmetatable({},{__index=function(t,k) local r={} t[k]=r return r end}) t[k]=r return r end}) t[k]=r return r end})
for _, vertex in ipairs(vmesh) do
local pos = vertex.pos
local norm = norms[pos[1]][pos[2]][pos[3]]
norm[#norm+1] = vertex.normal
end
for _, vertex in ipairs(vmesh) do
local normal = Vector()
local count = 0
local pos = vertex.pos
for _, norm in ipairs(norms[pos[1]][pos[2]][pos[3]]) do
if dot(vertex.normal, norm) >= smoothrad then
add(normal, norm)
count = count + 1
end
end
if count > 1 then
div(normal, count)
vertex.normal = normal
end
end
end
end
return #vmesh > 0 and vmesh
end
--[[
]]
local function getMeshFromData(data, uvs)
if not data or not uvs then
coroutine.yield(true)
end
local partlist = util.JSONToTable(util.Decompress(data))
if not partlist then
coroutine.yield(true)
end
prop2mesh.loadModelFixer()
if not meshmodelcache then
meshmodelcache = {}
end
local meshpcount = 0
local meshvcount = 0
local vmins = Vector()
local vmaxs = Vector()
local meshtex = (uvs and uvs ~= 0) and (1 / uvs) or nil
local meshlist = { {} }
local meshnext = meshlist[1]
local partcount = #partlist
for partid = 1, partcount do
local partnext = partlist[partid]
local partverts
if partnext.prop or partnext.holo then
partverts = getVertsFromMDL(partnext, meshtex, vmins, vmaxs)
elseif partnext.objd and partlist.custom then
local valid, opv = pcall(getVertsFromOBJ, partlist.custom, partnext, meshtex, vmins, vmaxs)
if valid and opv then
partverts = opv
end
end
if partverts then
meshpcount = meshpcount + 1
--[[
local nflat = partnext.vsmooth == 0
if not partnext.objd and (meshtex or nflat) then
for pv = 1, #partverts, 3 do
local normal = cross(partverts[pv + 2].pos - partverts[pv].pos, partverts[pv + 1].pos - partverts[pv].pos)
normalize(normal)
if nflat then
partverts[pv ].normal = Vector(normal)
partverts[pv + 1].normal = Vector(normal)
partverts[pv + 2].normal = Vector(normal)
end
if meshtex then
local boxDir = getBoxDir(normal)
partverts[pv ].u, partverts[pv ].v = getBoxUV(partverts[pv ].pos, boxDir, meshtex)
partverts[pv + 1].u, partverts[pv + 1].v = getBoxUV(partverts[pv + 1].pos, boxDir, meshtex)
partverts[pv + 2].u, partverts[pv + 2].v = getBoxUV(partverts[pv + 2].pos, boxDir, meshtex)
end
end
end
]]
if partnext.vinside then
for pv = #partverts, 1, -1 do
local vdupe = copy(partverts[pv])
vdupe.normal = -vdupe.normal
partverts[#partverts + 1] = vdupe
end
end
local partvcount = #partverts
if #meshnext + partvcount > 63999 then
meshlist[#meshlist + 1] = {}
meshnext = meshlist[#meshlist]
end
for pv = 1, partvcount do
meshnext[#meshnext + 1] = partverts[pv]
end
meshvcount = meshvcount + partvcount
end
coroutine.yield(false)
end
coroutine.yield(true, { meshes = meshlist, vcount = meshvcount, vmins = vmins, vmaxs = vmaxs, pcount = meshpcount })
end
--[[
]]
local meshlabs = {}
function prop2mesh.getMesh(crc, uvs, data)
if not crc or not uvs or not data then
return false
end
local key = string.format("%s_%s", crc, uvs)
if not meshlabs[key] then
meshlabs[key] = { crc = crc, uvs = uvs, data = data, coro = coroutine.create(getMeshFromData) }
return true
end
return false
end
local message
local function setmessage(text)
if not IsValid(message) then
local parent
if GetOverlayPanel then parent = GetOverlayPanel() end
message = vgui.Create("DPanel", parent)
local green = Color(255, 255, 0)
local black = Color(0,0,0)
local font = "prop2mesheditor"
message.Paint = function(self, w, h)
draw.SimpleTextOutlined(self.text, font, 0, 0, green, TEXT_ALIGN_LEFT, TEXT_ALIGN_TOP, 1, black)
end
message.SetText = function(self, txt)
if self.text ~= txt then
self.text = txt
surface.SetFont(font)
local w, h = surface.GetTextSize(self.text)
self:SetPos(ScrW() - w - 1, ScrH() - h - 1)
self:SetSize(w, h)
end
end
end
message:SetText(text)
message.time = SysTime()
end
local function pluralf(pattern, number)
return string.format(pattern, number, number == 1 and "" or "s")
end
hook.Add("Think", "prop2mesh_meshlab", function()
if prop2mesh.downloads > 0 then
setmessage(pluralf("prop2mesh %d server download%s remaining", prop2mesh.downloads))
end
if message and SysTime() - message.time > 0.25 then
if IsValid(message) then
message:Remove()
end
message = nil
end
local key, lab = next(meshlabs)
if not key or not lab then
return
end
local curtime = SysTime()
while SysTime() - curtime < 0.05 do
local ok, err, mdata = coroutine.resume(lab.coro, lab.data, lab.uvs)
if not ok then
print(err)
meshlabs[key] = nil
break
end
if err then
hook.Run("prop2mesh_hook_meshdone", lab.crc, lab.uvs, mdata)
meshlabs[key] = nil
break
end
end
if next(meshlabs) == nil then
prop2mesh.unloadModelFixer()
meshmodelcache = nil
else
if not message then
setmessage(pluralf("prop2mesh %d mesh build%s remaining", table.Count(meshlabs)))
end
end
end)
print( pluralf("prop2mesh %d server download%s remaining", 1, "s") )
print( pluralf("prop2mesh %d server download%s remaining", 0, "s") )

View File

@ -0,0 +1,446 @@
--[[
]]
local prop2mesh = prop2mesh
local string = string
local genericmodel, genericfolder, genericmodellist, genericfolderlist, blockedmodel
local specialmodel, specialfolder = {}, {}
--[[
]]
function prop2mesh.isBlockedModel(modelpath)
return blockedmodel[modelpath]
end
function prop2mesh.loadModelFixer()
if not genericmodel then
genericmodel = {}
for s in string.gmatch(genericmodellist, "[^\r\n]+") do
genericmodel[s] = true
end
end
if not genericfolder then
genericfolder = {}
for s in string.gmatch(genericfolderlist, "[^\r\n]+") do
genericfolder[s] = true
end
end
--print("Loading model fixers")
end
function prop2mesh.unloadModelFixer()
if genericmodel then
genericmodel = nil
end
if genericfolder then
genericfolder = nil
end
--print("Unloading model fixers")
end
function prop2mesh.getModelFix(modelpath)
if specialmodel[modelpath] or genericmodel[modelpath] then
return specialmodel[modelpath] or genericmodel[modelpath]
end
local trunc = string.GetPathFromFilename(modelpath)
return specialfolder[trunc] or genericfolder[trunc]
end
--[[
BADDIES
]]
blockedmodel = {
["models/lubprops/seat/raceseat2.mdl"] = true,
["models/lubprops/seat/raceseat.mdl"] = true,
}
--[[
SPECIAL FOLDERS
]]
specialfolder["models/sprops/trans/wheel_b/"] = function(partnum, numparts, rotated, normal)
if partnum == 1 then return rotated else return normal end
end
specialfolder["models/sprops/trans/wheel_d/"] = function(partnum, numparts, rotated, normal)
if partnum == 1 or partnum == 2 then return rotated else return normal end
end
--[[
SPECIAL MODELS
]]
local fix = function(partnum, numparts, rotated, normal)
if partnum == 1 then return rotated else return normal end
end
specialmodel["models/sprops/trans/miscwheels/thin_moto15.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/thin_moto20.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/thin_moto25.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/thin_moto30.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/thick_moto15.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/thick_moto20.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/thick_moto25.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/thick_moto30.mdl"] = fix
local fix = function(partnum, numparts, rotated, normal)
if partnum == 1 or partnum == 2 then return rotated else return normal end
end
specialmodel["models/sprops/trans/miscwheels/tank15.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/tank20.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/tank25.mdl"] = fix
specialmodel["models/sprops/trans/miscwheels/tank30.mdl"] = fix
local fix = function(partnum, numparts, rotated, normal)
local angle = Angle(rotated)
angle:RotateAroundAxis(angle:Forward(), 90)
return angle
end
specialmodel["models/props_mining/diesel_generator_crank.mdl"] = fix
specialmodel["models/props/de_nuke/hr_nuke/nuke_vent_bombsite/nuke_vent_bombsite_breakable_a.mdl"] = fix
specialmodel["models/props/de_nuke/hr_nuke/nuke_vent_bombsite/nuke_vent_bombsite_breakable_b.mdl"] = fix
specialmodel["models/props/de_nuke/hr_nuke/nuke_vent_bombsite/nuke_vent_bombsite_breakable_c.mdl"] = fix
--[[
GENERIC MODELS
]]
genericmodellist =
[[models/autocannon/semiautocannon_25mm.mdl
models/autocannon/semiautocannon_37mm.mdl
models/autocannon/semiautocannon_45mm.mdl
models/autocannon/semiautocannon_57mm.mdl
models/autocannon/semiautocannon_76mm.mdl
models/balloons/balloon_classicheart.mdl
models/balloons/balloon_dog.mdl
models/balloons/balloon_star.mdl
models/balloons/hot_airballoon.mdl
models/balloons/hot_airballoon_basket.mdl
models/blacknecro/ledboard60.mdl
models/blacknecro/tv_plasma_4_3.mdl
models/chairs/armchair.mdl
models/cheeze/wires/gyroscope.mdl
models/cheeze/wires/ram.mdl
models/cheeze/wires/router.mdl
models/cheeze/wires/wireless_card.mdl
models/combinecannon/cironwall.mdl
models/combinecannon/remnants.mdl
models/dynamite/dynamite.mdl
models/engines/emotorlarge.mdl
models/engines/emotormed.mdl
models/engines/emotorsmall.mdl
models/engines/gasturbine_l.mdl
models/engines/gasturbine_m.mdl
models/engines/gasturbine_s.mdl
models/engines/linear_l.mdl
models/engines/linear_m.mdl
models/engines/linear_s.mdl
models/engines/radial7l.mdl
models/engines/radial7m.mdl
models/engines/radial7s.mdl
models/engines/transaxial_l.mdl
models/engines/transaxial_m.mdl
models/engines/transaxial_s.mdl
models/engines/turbine_l.mdl
models/engines/turbine_m.mdl
models/engines/turbine_s.mdl
models/engines/wankel_2_med.mdl
models/engines/wankel_2_small.mdl
models/engines/wankel_3_med.mdl
models/engines/wankel_4_med.mdl
models/extras/info_speech.mdl
models/food/burger.mdl
models/food/hotdog.mdl
models/gears/planet_16.mdl
models/gears/planet_mount.mdl
models/gibs/helicopter_brokenpiece_01.mdl
models/gibs/helicopter_brokenpiece_02.mdl
models/gibs/helicopter_brokenpiece_03.mdl
models/gibs/helicopter_brokenpiece_04_cockpit.mdl
models/gibs/helicopter_brokenpiece_05_tailfan.mdl
models/gibs/helicopter_brokenpiece_06_body.mdl
models/gibs/shield_scanner_gib1.mdl
models/gibs/shield_scanner_gib2.mdl
models/gibs/shield_scanner_gib3.mdl
models/gibs/shield_scanner_gib4.mdl
models/gibs/shield_scanner_gib5.mdl
models/gibs/shield_scanner_gib6.mdl
models/gibs/strider_gib1.mdl
models/gibs/strider_gib2.mdl
models/gibs/strider_gib3.mdl
models/gibs/strider_gib4.mdl
models/gibs/strider_gib5.mdl
models/gibs/strider_gib6.mdl
models/gibs/strider_gib7.mdl
models/holograms/hexagon.mdl
models/holograms/icosphere.mdl
models/holograms/icosphere2.mdl
models/holograms/icosphere3.mdl
models/holograms/prism.mdl
models/holograms/sphere.mdl
models/holograms/tetra.mdl
models/howitzer/howitzer_75mm.mdl
models/howitzer/howitzer_105mm.mdl
models/howitzer/howitzer_122mm.mdl
models/howitzer/howitzer_155mm.mdl
models/howitzer/howitzer_203mm.mdl
models/howitzer/howitzer_240mm.mdl
models/howitzer/howitzer_290mm.mdl
models/hunter/plates/plate05x05_rounded.mdl
models/hunter/plates/plate1x3x1trap.mdl
models/hunter/plates/plate1x4x2trap.mdl
models/hunter/plates/plate1x4x2trap1.mdl
models/items/357ammo.mdl
models/items/357ammobox.mdl
models/items/ammocrate_ar2.mdl
models/items/ammocrate_grenade.mdl
models/items/ammocrate_rockets.mdl
models/items/ammocrate_smg1.mdl
models/items/ammopack_medium.mdl
models/items/ammopack_small.mdl
models/items/crossbowrounds.mdl
models/items/cs_gift.mdl
models/lamps/torch.mdl
models/machinegun/machinegun_20mm_compact.mdl
models/machinegun/machinegun_30mm_compact.mdl
models/machinegun/machinegun_40mm_compact.mdl
models/maxofs2d/button_01.mdl
models/maxofs2d/button_03.mdl
models/maxofs2d/button_04.mdl
models/maxofs2d/button_06.mdl
models/maxofs2d/button_slider.mdl
models/maxofs2d/camera.mdl
models/maxofs2d/logo_gmod_b.mdl
models/mechanics/articulating/arm_base_b.mdl
models/nova/airboat_seat.mdl
models/nova/chair_office01.mdl
models/nova/chair_office02.mdl
models/nova/chair_plastic01.mdl
models/nova/chair_wood01.mdl
models/nova/jalopy_seat.mdl
models/nova/jeep_seat.mdl
models/props/coop_kashbah/coop_stealth_boat/coop_stealth_boat_animated.mdl
models/props/de_inferno/hr_i/inferno_vintage_radio/inferno_vintage_radio.mdl
models/props_c17/doll01.mdl
models/props_c17/door01_left.mdl
models/props_c17/door02_double.mdl
models/props_c17/suitcase_passenger_physics.mdl
models/props_c17/trappropeller_blade.mdl
models/props_c17/tv_monitor01.mdl
models/props_canal/mattpipe.mdl
models/props_canal/winch01b.mdl
models/props_canal/winch02b.mdl
models/props_canal/winch02c.mdl
models/props_canal/winch02d.mdl
models/props_combine/breenbust.mdl
models/props_combine/breenbust_chunk01.mdl
models/props_combine/breenbust_chunk02.mdl
models/props_combine/breenbust_chunk04.mdl
models/props_combine/breenbust_chunk05.mdl
models/props_combine/breenbust_chunk06.mdl
models/props_combine/breenbust_chunk07.mdl
models/props_combine/breenchair.mdl
models/props_combine/breenclock.mdl
models/props_combine/breenpod.mdl
models/props_combine/breenpod_inner.mdl
models/props_combine/breen_tube.mdl
models/props_combine/bunker_gun01.mdl
models/props_combine/bustedarm.mdl
models/props_combine/cell_01_pod_cheap.mdl
models/props_combine/combinebutton.mdl
models/props_combine/combinethumper001a.mdl
models/props_combine/combinethumper002.mdl
models/props_combine/combine_ballsocket.mdl
models/props_combine/combine_mine01.mdl
models/props_combine/combine_tptimer.mdl
models/props_combine/eli_pod_inner.mdl
models/props_combine/health_charger001.mdl
models/props_combine/introomarea.mdl
models/props_combine/soldier_bed.mdl
models/props_combine/stalkerpod_physanim.mdl
models/props_doors/door01_dynamic.mdl
models/props_doors/door03_slotted_left.mdl
models/props_doors/doorklab01.mdl
models/props_junk/ravenholmsign.mdl
models/props_lab/blastdoor001a.mdl
models/props_lab/blastdoor001b.mdl
models/props_lab/blastdoor001c.mdl
models/props_lab/citizenradio.mdl
models/props_lab/clipboard.mdl
models/props_lab/crematorcase.mdl
models/props_lab/hevplate.mdl
models/props_lab/huladoll.mdl
models/props_lab/kennel_physics.mdl
models/props_lab/keypad.mdl
models/props_lab/ravendoor.mdl
models/props_lab/tpplug.mdl
models/props_lab/tpswitch.mdl
models/props_mining/ceiling_winch01.mdl
models/props_mining/control_lever01.mdl
models/props_mining/diesel_generator.mdl
models/props_mining/elevator_winch_cog.mdl
models/props_mining/switch01.mdl
models/props_mining/switch_updown01.mdl
models/props_phx/amraam.mdl
models/props_phx/box_amraam.mdl
models/props_phx/box_torpedo.mdl
models/props_phx/cannon.mdl
models/props_phx/carseat2.mdl
models/props_phx/carseat3.mdl
models/props_phx/construct/metal_angle90.mdl
models/props_phx/construct/metal_angle180.mdl
models/props_phx/construct/metal_dome90.mdl
models/props_phx/construct/metal_dome180.mdl
models/props_phx/construct/metal_plate1.mdl
models/props_phx/construct/metal_plate1x2.mdl
models/props_phx/construct/metal_plate2x2.mdl
models/props_phx/construct/metal_plate2x4.mdl
models/props_phx/construct/metal_plate4x4.mdl
models/props_phx/construct/metal_plate_curve.mdl
models/props_phx/construct/metal_plate_curve2.mdl
models/props_phx/construct/metal_plate_curve2x2.mdl
models/props_phx/construct/metal_plate_curve180.mdl
models/props_phx/construct/metal_wire1x1x1.mdl
models/props_phx/construct/metal_wire1x1x2.mdl
models/props_phx/construct/metal_wire1x1x2b.mdl
models/props_phx/construct/metal_wire1x2.mdl
models/props_phx/construct/metal_wire1x2b.mdl
models/props_phx/construct/metal_wire_angle90x1.mdl
models/props_phx/construct/metal_wire_angle90x2.mdl
models/props_phx/construct/metal_wire_angle180x1.mdl
models/props_phx/construct/metal_wire_angle180x2.mdl
models/props_phx/facepunch_logo.mdl
models/props_phx/games/chess/black_king.mdl
models/props_phx/games/chess/black_knight.mdl
models/props_phx/games/chess/board.mdl
models/props_phx/games/chess/white_king.mdl
models/props_phx/games/chess/white_knight.mdl
models/props_phx/gears/bevel9.mdl
models/props_phx/gears/rack9.mdl
models/props_phx/gears/rack18.mdl
models/props_phx/gears/rack36.mdl
models/props_phx/gears/rack70.mdl
models/props_phx/gears/spur9.mdl
models/props_phx/huge/road_curve.mdl
models/props_phx/huge/road_long.mdl
models/props_phx/huge/road_medium.mdl
models/props_phx/huge/road_short.mdl
models/props_phx/mechanics/slider1.mdl
models/props_phx/mechanics/slider2.mdl
models/props_phx/mk-82.mdl
models/props_phx/playfield.mdl
models/props_phx/torpedo.mdl
models/props_phx/trains/double_wheels_base.mdl
models/props_phx/trains/fsd-overrun.mdl
models/props_phx/trains/fsd-overrun2.mdl
models/props_phx/trains/monorail1.mdl
models/props_phx/trains/monorail_curve.mdl
models/props_phx/trains/trackslides_both.mdl
models/props_phx/trains/trackslides_inner.mdl
models/props_phx/trains/trackslides_outer.mdl
models/props_phx/trains/wheel_base.mdl
models/props_phx/wheels/breakable_tire.mdl
models/props_phx/wheels/magnetic_large_base.mdl
models/props_phx/wheels/magnetic_med_base.mdl
models/props_phx/wheels/magnetic_small_base.mdl
models/props_phx/ww2bomb.mdl
models/props_placeable/witch_hatch_lid.mdl
models/props_survival/repulsor/repulsor.mdl
models/props_trainstation/passengercar001.mdl
models/props_trainstation/passengercar001_dam01a.mdl
models/props_trainstation/passengercar001_dam01c.mdl
models/props_trainstation/train_outro_car01.mdl
models/props_trainstation/train_outro_porch01.mdl
models/props_trainstation/train_outro_porch02.mdl
models/props_trainstation/train_outro_porch03.mdl
models/props_trainstation/wrecked_train.mdl
models/props_trainstation/wrecked_train_02.mdl
models/props_trainstation/wrecked_train_divider_01.mdl
models/props_trainstation/wrecked_train_door.mdl
models/props_trainstation/wrecked_train_panel_01.mdl
models/props_trainstation/wrecked_train_panel_02.mdl
models/props_trainstation/wrecked_train_panel_03.mdl
models/props_trainstation/wrecked_train_rack_01.mdl
models/props_trainstation/wrecked_train_rack_02.mdl
models/props_trainstation/wrecked_train_seat.mdl
models/props_vehicles/mining_car.mdl
models/props_vehicles/van001a_nodoor_physics.mdl
models/props_wasteland/cranemagnet01a.mdl
models/props_wasteland/wood_fence01a.mdl
models/props_wasteland/wood_fence01b.mdl
models/props_wasteland/wood_fence01c.mdl
models/quarterlife/fsd-overrun-toy.mdl
models/radar/radar_sp_big.mdl
models/radar/radar_sp_mid.mdl
models/radar/radar_sp_sml.mdl
models/rotarycannon/kw/14_5mmrac.mdl
models/rotarycannon/kw/20mmrac.mdl
models/rotarycannon/kw/30mmrac.mdl
models/segment.mdl
models/segment2.mdl
models/segment3.mdl
models/shells/shell_9mm.mdl
models/shells/shell_12gauge.mdl
models/shells/shell_57.mdl
models/shells/shell_338mag.mdl
models/shells/shell_556.mdl
models/shells/shell_762nato.mdl
models/sprops/trans/fender_a/a_fender30.mdl
models/sprops/trans/fender_a/a_fender35.mdl
models/sprops/trans/fender_a/a_fender40.mdl
models/sprops/trans/fender_a/a_fender45.mdl
models/sprops/trans/train/double_24.mdl
models/sprops/trans/train/double_36.mdl
models/sprops/trans/train/double_48.mdl
models/sprops/trans/train/double_72.mdl
models/sprops/trans/train/single_24.mdl
models/sprops/trans/train/single_36.mdl
models/sprops/trans/train/single_48.mdl
models/sprops/trans/train/single_72.mdl
models/thrusters/jetpack.mdl
models/vehicles/pilot_seat.mdl
models/vehicles/prisoner_pod.mdl
models/vehicles/prisoner_pod_inner.mdl
models/vehicles/vehicle_van.mdl
models/vehicles/vehicle_vandoor.mdl
models/wingf0x/altisasocket.mdl
models/wingf0x/ethernetplug.mdl
models/wingf0x/ethernetsocket.mdl
models/wingf0x/hdmiplug.mdl
models/wingf0x/hdmisocket.mdl
models/wingf0x/isaplug.mdl
models/wingf0x/isasocket.mdl]]
--[[
GENERIC FOLDERS
]]
genericfolderlist =
[[models/bull/gates/
models/bull/various/
models/cheeze/pcb/
models/combine_turrets/
models/engine/
models/fueltank/
models/jaanus/wiretool/
models/kobilica/
models/misc/
models/phxtended/
models/props_phx/construct/glass/
models/props_phx/construct/plastic/
models/props_phx/construct/windows/
models/props_phx/construct/wood/
models/props_phx/misc/
models/props_phx/trains/tracks/
models/sprops/trans/wheels_g/
models/sprops/trans/wheel_big_g/
models/sprops/trans/wheel_f/
models/squad/sf_bars/
models/squad/sf_plates/
models/squad/sf_tris/
models/squad/sf_tubes/
models/weapons/
models/wings/]]

321
lua/prop2mesh/sv_editor.lua Normal file
View File

@ -0,0 +1,321 @@
--[[
]]
util.AddNetworkString("prop2mesh_upload")
util.AddNetworkString("prop2mesh_upload_start")
local net = net
local util = util
local table = table
local next = next
local pairs = pairs
local pcall = pcall
local isnumber = isnumber
local isvector = isvector
local isangle = isangle
local function canUpload(pl, self)
if not IsValid(pl) then return false end
if not prop2mesh.isValid(self) then return false end
if CPPI and self:CPPIGetOwner() ~= pl then return false end
return true
end
--[[
]]
local kvpass = {}
kvpass.vsmooth = function(data, index, val)
if isnumber(val) then
data[index].vsmooth = (data[index].objd and val) or (val ~= 0 and 1 or nil)
end
end
kvpass.vinvert = function(data, index, val)
if isnumber(val) then
data[index].vinvert = val ~= 0 and 1 or nil
end
end
kvpass.vinside = function(data, index, val)
if isnumber(val) then
data[index].vinside = val ~= 0 and 1 or nil
end
end
kvpass.pos = function(data, index, val)
if isvector(val) then
data[index].pos = val
end
end
kvpass.ang = function(data, index, val)
if isangle(val) then
data[index].ang = val
end
end
kvpass.scale = function(data, index, val)
if isvector(val) and (val.x ~= 1 or val.y ~= 1 or val.z ~= 1) then
data[index].scale = val
end
end
--[[
]]
local function makeSettingChanges(self, index, updates, forceSet)
end
local function makeUpdateChanges(self, index, updates, forceSet)
local currentData = self:GetControllerData(index)
if not currentData then
return
end
for pi, pdata in pairs(updates) do
if pdata.kill then
currentData[pi] = nil
else
for k, v in pairs(pdata) do
local func = kvpass[k]
if func then
func(currentData, pi, v)
end
end
end
end
local updatedData = { custom = currentData.custom }
for k, v in pairs(currentData) do
if tonumber(k) then
updatedData[#updatedData + 1] = v
end
end
if forceSet then
if next(updatedData) then
self:SetControllerData(index, updatedData)
else
self:ResetControllerData(index)
end
end
return updatedData
end
local function insertUpdateChanges(self, index, partlist, updates)
local currentData
if updates then
currentData = makeUpdateChanges(self, index, updates, false)
else
currentData = self:GetControllerData(index)
end
if not currentData then
return
end
for i = 1, #currentData do
partlist[#partlist + 1] = currentData[i]
end
if currentData.custom then
for crc, data in pairs(currentData.custom) do
partlist.custom[crc] = data
end
end
end
local function applyUpload(self)
local finalData = {}
for id, upload in pairs(self.prop2mesh_upload_ready) do
for index, changes in pairs(upload.controllers) do
if not finalData[index] then
finalData[index] = { custom = {} }
end
pcall(insertUpdateChanges, self, index, finalData[index], changes.modme)
if changes.setme then
if changes.setme.uvs then self:SetControllerUVS(index, changes.setme.uvs) end
if changes.setme.scale then self:SetControllerScale(index, changes.setme.scale) end
end
finalData[index].custom[id] = upload.data
for i = 1, #changes.addme do
finalData[index][#finalData[index] + 1] = changes.addme[i]
end
end
end
for index, partlist in pairs(finalData) do
if next(partlist) then
self:SetControllerData(index, partlist)
else
self:ResetControllerData(index)
end
end
self.prop2mesh_upload_ready = nil
self.prop2mesh_upload_queue = nil
end
--[[
]]
net.Receive("prop2mesh_upload_start", function(len, pl)
if pl.prop2mesh_antispam then
local wait = SysTime() - pl.prop2mesh_antispam
if wait < 1 then
pl:ChatPrint(string.format("Wait %d more seconds before uploading again", 1 - wait))
return
end
end
local self = Entity(net.ReadUInt(16) or 0)
if not canUpload(pl, self) then
return
end
if self.prop2mesh_upload_queue then
return
end
local set, add, mod
if net.ReadBool() then set = net.ReadTable() else set = {} end
if net.ReadBool() then add = net.ReadTable() else add = {} end
if net.ReadBool() then mod = net.ReadTable() else mod = {} end
if not next(add) then
if next(set) or next(mod) then
self:SetNetworkedBool("uploading", true)
for index, updates in pairs(set) do
if updates.uvs then
self:SetControllerUVS(index, updates.uvs)
end
if updates.scale then
self:SetControllerScale(index, updates.scale)
end
end
for index, updates in pairs(mod) do
pcall(makeUpdateChanges, self, index, updates, true)
end
self.prop2mesh_upload_queue = true
timer.Simple(0, function()
self.prop2mesh_upload_queue = nil
end)
end
return
end
pl.prop2mesh_antispam = SysTime()
local uploadQueue = {}
local uploadReady = {}
for index, parts in pairs(add) do
if not self.prop2mesh_controllers[index] then
goto CONTINUE
end
local data = self.prop2mesh_partlists[self.prop2mesh_controllers[index].crc]
if data then
data = util.JSONToTable(util.Decompress(data)) -- dangerrrrr
data = data.custom
end
for k, part in pairs(parts) do
local crc = part.objd
if crc then
local datagrab = data and data[tonumber(crc)] or nil
local utable = datagrab and uploadReady or uploadQueue
if not utable[crc] then
utable[crc] = { controllers = {}, data = datagrab }
end
if not utable[crc].controllers[index] then
utable[crc].controllers[index] = {
addme = {},
setme = set[index],
modme = mod[index],
}
end
for key, val in pairs(part) do
local func = kvpass[key]
if func then
func(parts, k, val)
end
end
table.insert(utable[crc].controllers[index].addme, part)
end
end
::CONTINUE::
end
if next(uploadQueue) then
self.prop2mesh_upload_queue = uploadQueue
self.prop2mesh_upload_ready = uploadReady
local keys = table.GetKeys(uploadQueue)
net.Start("prop2mesh_upload_start")
net.WriteUInt(self:EntIndex(), 16)
net.WriteUInt(#keys, 8)
for i = 1, #keys do
net.WriteString(keys[i])
end
net.Send(pl)
elseif next(uploadReady) then
self:SetNetworkedBool("uploading", true)
self.prop2mesh_upload_queue = uploadQueue
self.prop2mesh_upload_ready = uploadReady
applyUpload(self)
end
end)
net.Receive("prop2mesh_upload", function(len, pl)
local self = Entity(net.ReadUInt(16) or 0)
if not canUpload(pl, self) then
return
end
local crc = net.ReadString()
net.ReadStream(pl, function(data)
if not canUpload(pl, self) then
return
end
local decomp = util.Decompress(data)
if crc == tostring(util.CRC(decomp)) then
self.prop2mesh_upload_ready[crc] = self.prop2mesh_upload_queue[crc]
self.prop2mesh_upload_ready[crc].data = decomp
self.prop2mesh_upload_queue[crc] = nil
else
self.prop2mesh_upload_queue[crc] = nil
end
if next(self.prop2mesh_upload_queue) then
return
end
applyUpload(self)
end)
end)

View File

@ -0,0 +1,248 @@
--[[
-- one of
prop -- modelpath
holo -- modelpath
objd -- crc of uncompressed data
-- required
pos -- local pos
ang -- local ang
-- optional
scale -- x y z scales
bodygroup -- mask
vsmooth -- unset to use model normals, 0 to use flat shading, degrees to calculate
vinvert -- flip normals
vinside -- render inside
objn -- prettyprint
]]
local prop2mesh = prop2mesh
local next = next
local TypeID = TypeID
local IsValid = IsValid
local WorldToLocal = WorldToLocal
local LocalToWorld = LocalToWorld
local entclass = {}
--[[
]]
function prop2mesh.partsFromEnts(entlist, worldpos, worldang)
local partlist = {}
for k, v in pairs(entlist) do
local ent
if TypeID(k) == TYPE_ENTITY then ent = k else ent = v end
local class = IsValid(ent) and entclass[ent:GetClass()]
if class then
class(partlist, ent, worldpos, worldang)
end
end
return next(partlist) and partlist
end
function prop2mesh.sanitizeCustom(partlist) -- remove unused obj data
if not partlist.custom then
return
end
local lookup = {}
for crc, data in pairs(partlist.custom) do
lookup[crc .. ""] = data
end
local custom = {}
for index, part in ipairs(partlist) do
if part.objd then
local crc = part.objd .. ""
if crc and lookup[crc] then
custom[crc] = lookup[crc]
lookup[crc] = nil
end
end
if not next(lookup) then
break
end
end
partlist.custom = custom
end
--[[
]]
local function getBodygroupMask(ent)
local mask = 0
local offset = 1
for index = 0, ent:GetNumBodyGroups() - 1 do
local bg = ent:GetBodygroup(index)
mask = mask + offset * bg
offset = offset * ent:GetBodygroupCount(index)
end
return mask
end
entclass.prop_physics = function(partlist, ent, worldpos, worldang)
local part = { prop = ent:GetModel() }
part.pos, part.ang = WorldToLocal(ent:GetPos(), ent:GetAngles(), worldpos, worldang)
local bodygroup = getBodygroupMask(ent)
if bodygroup ~= 0 then
part.bodygroup = bodygroup
end
local scale = ent:GetManipulateBoneScale(0)
if scale.x ~= 1 or scale.y ~= 1 or scale.z ~= 1 then
part.scale = scale
end
local clips = ent.ClipData or ent.EntityMods and ent.EntityMods.clips
if clips then
local pclips = {}
for _, clip in ipairs(clips) do
if not clip.n or not clip.d then
goto badclip
end
if clip.inside then
part.vinside = 1
end
local normal = clip.n:Forward()
pclips[#pclips + 1] = { n = normal, d = clip.d + normal:Dot(ent:OBBCenter()) }
::badclip::
end
if next(pclips) then
part.clips = pclips
end
end
partlist[#partlist + 1] = part
end
entclass.gmod_wire_hologram = function(partlist, ent, worldpos, worldang)
local holo = ent.E2HoloData
if not holo then
return
end
local part = { holo = ent:GetModel() }
part.pos, part.ang = WorldToLocal(ent:GetPos(), ent:GetAngles(), worldpos, worldang)
local bodygroup = getBodygroupMask(ent)
if bodygroup ~= 0 then
part.bodygroup = bodygroup
end
if holo.scale and (holo.scale.x ~= 1 or holo.scale.y ~= 1 or holo.scale.z ~= 1) then
part.scale = Vector(holo.scale)
end
if holo.clips then
local pclips = {}
for _, clip in pairs(holo.clips) do
if clip.localentid == 0 then -- this is a global clip... what to do here?
goto badclip
end
local clipTo = Entity(clip.localentid)
if not IsValid(clipTo) then
goto badclip
end
local normal = ent:WorldToLocal(clipTo:LocalToWorld(clip.normal:GetNormalized()) - clipTo:GetPos() + ent:GetPos())
local origin = ent:WorldToLocal(clipTo:LocalToWorld(clip.origin))
pclips[#pclips + 1] = { n = normal, d = normal:Dot(origin) }
::badclip::
end
if next(pclips) then
part.clips = pclips
end
end
partlist[#partlist + 1] = part
end
entclass.starfall_hologram = function(partlist, ent, worldpos, worldang)
local part = { holo = ent:GetModel() }
part.pos, part.ang = WorldToLocal(ent:GetPos(), ent:GetAngles(), worldpos, worldang)
local bodygroup = getBodygroupMask(ent)
if bodygroup ~= 0 then
part.bodygroup = bodygroup
end
if ent.scale then
part.scale = ent:GetScale()
end
if ent.clips then
local pclips = {}
for _, clip in pairs(holo.clips) do
if not IsValid(clip.entity) then
goto badclip
end
local normal = ent:WorldToLocal(clip.entity:LocalToWorld(clip.normal:GetNormalized()) - clip.entity:GetPos() + ent:GetPos())
local origin = ent:WorldToLocal(clip.entity:LocalToWorld(clip.origin))
pclips[#pclips + 1] = { n = normal, d = normal:Dot(origin) }
::badclip::
end
if next(pclips) then
part.clips = pclips
end
end
partlist[#partlist + 1] = part
end
local function transformPartlist(ent, index, worldpos, worldang)
local partlist = ent:GetControllerData(index)
if not partlist then
return
end
local localpos = ent:GetPos()
local localang = ent:GetAngles()
for k, v in ipairs(partlist) do
v.pos, v.ang = LocalToWorld(v.pos, v.ang, localpos, localang)
v.pos, v.ang = WorldToLocal(v.pos, v.ang, worldpos, worldang)
end
return partlist
end
entclass.sent_prop2mesh_legacy = function(partlist, ent, worldpos, worldang)
local ok, err = pcall(transformPartlist, ent, 1, worldpos, worldang)
if ok then
if err.custom then
if not partlist.custom then
partlist.custom = {}
end
for k, v in pairs(err.custom) do
partlist.custom[k] = v
end
end
for i = 1, #err do
partlist[#partlist + 1] = err[i]
end
partlist.uvs = ent:GetControllerUVS(1)
else
print(err)
end
end

File diff suppressed because it is too large Load Diff

7
materials/p2m/cube.vmt Normal file
View File

@ -0,0 +1,7 @@
"UnlitGeneric"
{
"$baseTexture" "p2m/cube"
"$selfillum" "1"
"$reflectivity" "[0 0 0]"
"$nodecal" "1"
}

BIN
materials/p2m/cube.vtf Normal file

Binary file not shown.

BIN
models/p2m/cube.dx80.vtx Normal file

Binary file not shown.

BIN
models/p2m/cube.dx90.vtx Normal file

Binary file not shown.

BIN
models/p2m/cube.mdl Normal file

Binary file not shown.

BIN
models/p2m/cube.phy Normal file

Binary file not shown.

BIN
models/p2m/cube.sw.vtx Normal file

Binary file not shown.

BIN
models/p2m/cube.vvd Normal file

Binary file not shown.