From e20749bb6b9c488f79452dc8f94a419e01e60829 Mon Sep 17 00:00:00 2001 From: shadowscion Date: Fri, 16 Apr 2021 10:44:08 -0500 Subject: [PATCH] complete refactor --- lua/autorun/netstream.lua | 364 ++++++ lua/autorun/prop2mesh.lua | 44 + .../core/custom/prop2mesh.lua | 370 ++++++ lua/entities/sent_prop2mesh/cl_init.lua | 537 ++++++++ lua/entities/sent_prop2mesh/init.lua | 407 ++++++ lua/entities/sent_prop2mesh/shared.lua | 98 ++ lua/entities/sent_prop2mesh_legacy.lua | 139 ++ lua/prop2mesh/cl_editor.lua | 690 ++++++++++ lua/prop2mesh/cl_meshlab.lua | 676 ++++++++++ lua/prop2mesh/cl_modelfixer.lua | 446 +++++++ lua/prop2mesh/sv_editor.lua | 321 +++++ lua/prop2mesh/sv_entparts.lua | 248 ++++ lua/weapons/gmod_tool/stools/prop2mesh.lua | 1132 +++++++++++++++++ materials/p2m/cube.vmt | 7 + materials/p2m/cube.vtf | Bin 0 -> 349784 bytes models/p2m/cube.dx80.vtx | Bin 0 -> 429 bytes models/p2m/cube.dx90.vtx | Bin 0 -> 429 bytes models/p2m/cube.mdl | Bin 0 -> 1648 bytes models/p2m/cube.phy | Bin 0 -> 692 bytes models/p2m/cube.sw.vtx | Bin 0 -> 429 bytes models/p2m/cube.vvd | Bin 0 -> 1600 bytes 21 files changed, 5479 insertions(+) create mode 100644 lua/autorun/netstream.lua create mode 100644 lua/autorun/prop2mesh.lua create mode 100644 lua/entities/gmod_wire_expression2/core/custom/prop2mesh.lua create mode 100644 lua/entities/sent_prop2mesh/cl_init.lua create mode 100644 lua/entities/sent_prop2mesh/init.lua create mode 100644 lua/entities/sent_prop2mesh/shared.lua create mode 100644 lua/entities/sent_prop2mesh_legacy.lua create mode 100644 lua/prop2mesh/cl_editor.lua create mode 100644 lua/prop2mesh/cl_meshlab.lua create mode 100644 lua/prop2mesh/cl_modelfixer.lua create mode 100644 lua/prop2mesh/sv_editor.lua create mode 100644 lua/prop2mesh/sv_entparts.lua create mode 100644 lua/weapons/gmod_tool/stools/prop2mesh.lua create mode 100644 materials/p2m/cube.vmt create mode 100644 materials/p2m/cube.vtf create mode 100644 models/p2m/cube.dx80.vtx create mode 100644 models/p2m/cube.dx90.vtx create mode 100644 models/p2m/cube.mdl create mode 100644 models/p2m/cube.phy create mode 100644 models/p2m/cube.sw.vtx create mode 100644 models/p2m/cube.vvd diff --git a/lua/autorun/netstream.lua b/lua/autorun/netstream.lua new file mode 100644 index 0000000..56e4d39 --- /dev/null +++ b/lua/autorun/netstream.lua @@ -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) diff --git a/lua/autorun/prop2mesh.lua b/lua/autorun/prop2mesh.lua new file mode 100644 index 0000000..b4d5445 --- /dev/null +++ b/lua/autorun/prop2mesh.lua @@ -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 diff --git a/lua/entities/gmod_wire_expression2/core/custom/prop2mesh.lua b/lua/entities/gmod_wire_expression2/core/custom/prop2mesh.lua new file mode 100644 index 0000000..fb2ad77 --- /dev/null +++ b/lua/entities/gmod_wire_expression2/core/custom/prop2mesh.lua @@ -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 diff --git a/lua/entities/sent_prop2mesh/cl_init.lua b/lua/entities/sent_prop2mesh/cl_init.lua new file mode 100644 index 0000000..4f9129c --- /dev/null +++ b/lua/entities/sent_prop2mesh/cl_init.lua @@ -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) diff --git a/lua/entities/sent_prop2mesh/init.lua b/lua/entities/sent_prop2mesh/init.lua new file mode 100644 index 0000000..0d7fbb2 --- /dev/null +++ b/lua/entities/sent_prop2mesh/init.lua @@ -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 diff --git a/lua/entities/sent_prop2mesh/shared.lua b/lua/entities/sent_prop2mesh/shared.lua new file mode 100644 index 0000000..9c57e35 --- /dev/null +++ b/lua/entities/sent_prop2mesh/shared.lua @@ -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 +}) diff --git a/lua/entities/sent_prop2mesh_legacy.lua b/lua/entities/sent_prop2mesh_legacy.lua new file mode 100644 index 0000000..76a4c98 --- /dev/null +++ b/lua/entities/sent_prop2mesh_legacy.lua @@ -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") diff --git a/lua/prop2mesh/cl_editor.lua b/lua/prop2mesh/cl_editor.lua new file mode 100644 index 0000000..6052261 --- /dev/null +++ b/lua/prop2mesh/cl_editor.lua @@ -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") + diff --git a/lua/prop2mesh/cl_meshlab.lua b/lua/prop2mesh/cl_meshlab.lua new file mode 100644 index 0000000..e8b1cc5 --- /dev/null +++ b/lua/prop2mesh/cl_meshlab.lua @@ -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") ) diff --git a/lua/prop2mesh/cl_modelfixer.lua b/lua/prop2mesh/cl_modelfixer.lua new file mode 100644 index 0000000..1e1f177 --- /dev/null +++ b/lua/prop2mesh/cl_modelfixer.lua @@ -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/]] diff --git a/lua/prop2mesh/sv_editor.lua b/lua/prop2mesh/sv_editor.lua new file mode 100644 index 0000000..b4c181a --- /dev/null +++ b/lua/prop2mesh/sv_editor.lua @@ -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) + diff --git a/lua/prop2mesh/sv_entparts.lua b/lua/prop2mesh/sv_entparts.lua new file mode 100644 index 0000000..a4b0136 --- /dev/null +++ b/lua/prop2mesh/sv_entparts.lua @@ -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 diff --git a/lua/weapons/gmod_tool/stools/prop2mesh.lua b/lua/weapons/gmod_tool/stools/prop2mesh.lua new file mode 100644 index 0000000..d1b32f7 --- /dev/null +++ b/lua/weapons/gmod_tool/stools/prop2mesh.lua @@ -0,0 +1,1132 @@ +TOOL.Category = "Render" +TOOL.Name = "#tool.prop2mesh.name" + +local prop2mesh = prop2mesh +local math = math +local ents = ents +local table = table +local IsValid = IsValid + +if SERVER then + local select_material = "models/debug/debugwhite" + local select_color_class = { + prop_physics = Color(255, 0, 0, 125), + gmod_wire_hologram = Color(0, 255, 0, 125), + starfall_hologram = Color(255, 0, 255, 125), + sent_prop2mesh_legacy = Color(255, 255, 0, 125), + } + local select_color_p2m = Color(0, 0, 255, 255) + + local function checkOwner(ply, ent) + if CPPI then + local owner = ent:CPPIGetOwner() or (ent.GetPlayer and ent:GetPlayer()) + if owner then + return owner == ply + end + end + return true + end + + TOOL.selection = {} + TOOL.p2m = {} + + function TOOL:IsLegacyMode() + return self:GetClientNumber("tool_legacymode") ~= 0 + end + + function TOOL:MakeEnt(tr) + local legacy = self:IsLegacyMode() + local ent = ents.Create(legacy and "sent_prop2mesh_legacy" or "sent_prop2mesh") + local mdl = legacy and "models/hunter/plates/plate.mdl" or self:GetClientInfo("tool_setmodel") + if not IsUselessModel(mdl) then + ent:SetModel(mdl) + else + ent:SetModel("models/p2m/cube.mdl") + end + local ang + if math.abs(tr.HitNormal.x) < 0.001 and math.abs(tr.HitNormal.y) < 0.001 then + ang = Vector(0, 0, tr.HitNormal.z):Angle() + else + ang = tr.HitNormal:Angle() + end + ang.p = ang.p + 90 + ent:SetAngles(ang) + ent:SetPos(tr.HitPos - ent:LocalToWorld(Vector(0, 0, ent:OBBMins().z))) + ent:Spawn() + ent:Activate() + + if CPPI and ent.CPPISetOwner then + ent:CPPISetOwner(self:GetOwner()) + end + + if legacy then + ent:AddController(self:GetClientNumber("tool_setuvsize")) + end + + undo.Create(ent:GetClass()) + undo.AddEntity(ent) + undo.SetPlayer(self:GetOwner()) + undo.Finish() + + return ent + end + + function TOOL:GetFilteredEntities(tr, group) + if next(group) == nil then + return + end + + local class_whitelist = {} + if not tobool(self:GetClientNumber("tool_filter_iprop")) then + class_whitelist.prop_physics = true + end + if not tobool(self:GetClientNumber("tool_filter_iholo")) then + class_whitelist.gmod_wire_hologram = true + class_whitelist.starfall_hologram = true + end + if not tobool(self:GetClientNumber("tool_filter_ilp2m")) then + class_whitelist.sent_prop2mesh_legacy = true + end + + local ignore_invs = tobool(self:GetClientNumber("tool_filter_iinvs")) + local ignore_prnt = tobool(self:GetClientNumber("tool_filter_iprnt")) + local ignore_cnst = tobool(self:GetClientNumber("tool_filter_icnst")) + + local bycol, bymat, bymass + if tr.Entity and not tr.HitWorld then + if tobool(self:GetClientNumber("tool_filter_mcolor")) then + bycol = self.selection[tr.Entity] and self.selection[tr.Entity].col or tr.Entity:GetColor() + end + if tobool(self:GetClientNumber("tool_filter_mmatrl")) then + bymat = self.selection[tr.Entity] and self.selection[tr.Entity].mat or tr.Entity:GetMaterial() + end + end + if tobool(self:GetClientNumber("tool_filter_mmass")) then + bymass = self:GetClientNumber("tool_filter_mmass") + end + + local filtered = {} + for k, v in ipairs(group) do + local class = v:GetClass() + if not class_whitelist[class] then + goto skip + end + if ignore_invs and v:GetColor().a == 0 then + goto skip + end + if ignore_prnt and IsValid(v:GetParent()) then + goto skip + end + if ignore_cnst and v:IsConstrained() then + goto skip + end + if bymat and v:GetMaterial() ~= bymat then + goto skip + end + if bycol then + local c = v:GetColor() + if c.r ~= bycol.r or c.g ~= bycol.g or c.b ~= bycol.b or c.a ~= bycol.a then + goto skip + end + end + if bymass then + local phys = v:GetPhysicsObject() + if phys:IsValid() then + if v.EntityMods and v.EntityMods.mass and v.EntityMods.mass.Mass then + if v.EntityMods.mass.Mass > bymass then + goto skip + end + else + if phys:GetMass() > bymass then + goto skip + end + end + end + end + table.insert(filtered, v) + ::skip:: + end + + return filtered + end + + function TOOL:SelectGroup(group) + if not group or next(group) == nil then + return + end + for k, v in ipairs(group) do + self:SelectEntity(v) + end + end + + function TOOL:SelectEntity(ent) + if not IsValid(ent) or self.selection[ent] or ent == self.p2m.ent or not checkOwner(self:GetOwner(), ent) then + return false + end + local class = ent:GetClass() + if not select_color_class[class] then + return false + end + self.selection[ent] = { col = ent:GetColor(), mat = ent:GetMaterial(), mode = ent:GetRenderMode() } + ent:SetColor(select_color_class[class]) + ent:SetRenderMode(RENDERMODE_TRANSCOLOR) + ent:SetMaterial(select_material) + ent:CallOnRemove("prop2mesh_deselect", function(e) + self.selection[e] = nil + end) + return true + end + + function TOOL:DeselectEntity(ent) + if not self.selection[ent] then + return false + end + ent:SetColor(self.selection[ent].col) + ent:SetRenderMode(self.selection[ent].mode) + ent:SetMaterial(self.selection[ent].mat) + ent:RemoveCallOnRemove("prop2mesh_deselect") + self.selection[ent] = nil + return true + end + + function TOOL:UnsetP2M() + if IsValid(self.p2m.ent) then + self.p2m.ent:SetColor(self.p2m.col) + self.p2m.ent:SetRenderMode(self.p2m.mode) + self.p2m.ent:SetMaterial(self.p2m.mat) + self.p2m.ent:RemoveCallOnRemove("prop2mesh_deselect") + end + self.p2m = {} + self:SetStage(0) + end + + function TOOL:SetP2M(ent) + local legacy = self:IsLegacyMode() + if not IsValid(ent) or ent:GetClass() ~= (legacy and "sent_prop2mesh_legacy" or "sent_prop2mesh") or not checkOwner(self:GetOwner(), ent) then + return + end + + self.p2m = { ent = ent, col = ent:GetColor(), mat = ent:GetMaterial(), mode = ent:GetRenderMode() } + ent:SetColor(select_color_p2m) + ent:SetRenderMode(RENDERMODE_TRANSCOLOR) + ent:SetMaterial(select_material) + ent:CallOnRemove("prop2mesh_deselect", function(e) + for k, v in pairs(self.selection) do + self:DeselectEntity(k) + end + self.selection = {} + end) + self:SetStage(1) + end + + function TOOL:Deploy() + timer.Simple(0.1, function() + if IsValid(self.p2m.ent) then + self:SetStage(1) + end + end) + end + + function TOOL:RightClick(tr) + if not tr.Hit then + return false + end + if not IsValid(self.p2m.ent) then + self:SetP2M(tr.Entity) + else + if self:GetOwner():KeyDown(IN_SPEED) then + self:SelectGroup(self:GetFilteredEntities(tr, ents.FindInSphere(tr.HitPos, math.Clamp(self:GetClientNumber("tool_filter_radius"), 0, 2048)))) + elseif self:GetOwner():KeyDown(IN_USE) then + self:SelectGroup(self:GetFilteredEntities(tr, tr.Entity:GetChildren())) + else + if tr.Entity == self.p2m.ent then + if next(self.selection) ~= nil then + local legacy = self:IsLegacyMode() + local index = self:GetOwner():GetInfoNum("prop2mesh_multitool_index", 1) + if legacy or index ~= 1 then + self.p2m.ent:ToolDataByINDEX(legacy and 1 or (index - 1), self) + local rmv = self:GetClientNumber("tool_setautoremove") ~= 0 + for k, v in pairs(self.selection) do + self:DeselectEntity(k) + if rmv then + SafeRemoveEntity(k) + end + end + self.selection = {} + self:UnsetP2M() + end + end + else + if self:GetClientNumber("tool_filter_iprop") ~= 0 and self:GetClientNumber("tool_filter_iholo") == 0 then + local find = {} + local cone = ents.FindInCone(tr.StartPos, tr.Normal, tr.HitPos:Distance(tr.StartPos) * 2, math.cos(math.rad(3))) + local whitelist = { gmod_wire_hologram = true, starfall_hologram = true } + for k, ent in ipairs(cone) do + if not whitelist[ent:GetClass()] then + goto skip + end + table.insert(find, { ent = ent, len = (tr.StartPos - ent:GetPos()):LengthSqr() }) + ::skip:: + end + for k, v in SortedPairsByMemberValue(find, "len") do + if self.selection[v.ent] then + self:DeselectEntity(v.ent) + break + elseif self:SelectEntity(v.ent) then + break + end + end + else + if self.selection[tr.Entity] then self:DeselectEntity(tr.Entity) else self:SelectEntity(tr.Entity) end + end + end + end + end + return true + end + + function TOOL:LeftClick(tr) + if not tr.Hit then + return false + end + if IsValid(self.p2m.ent) then + if tr.Entity == self.p2m.ent then + if self:IsLegacyMode() then + if self:GetOwner():KeyDown(IN_SPEED) then + self.p2m.ent:SetControllerUVS(1, self:GetClientNumber("tool_setuvsize")) + end + else + if self:GetOwner():KeyDown(IN_SPEED) then + if next(self.selection) ~= nil then + self.p2m.ent:ToolDataAUTO(self) + local rmv = self:GetClientNumber("tool_setautoremove") ~= 0 + for k, v in pairs(self.selection) do + self:DeselectEntity(k) + if rmv then + SafeRemoveEntity(k) + end + end + self.selection = {} + self:UnsetP2M() + end + else + local index = self:GetOwner():GetInfoNum("prop2mesh_multitool_index", 1) + if index == 1 then + self.p2m.ent:AddController(self:GetClientNumber("tool_setuvsize")) + end + end + end + end + else + if (CPPI and tr.Entity:CPPICanTool(self:GetOwner(), "prop2mesh")) or not CPPI then + self:MakeEnt(tr) + end + end + return true + end + + function TOOL:Reload(tr) + if not tr.Hit then + return false + end + if next(self.selection) ~= nil then + for k, v in pairs(self.selection) do + self:DeselectEntity(k) + end + self.selection = {} + else + self:UnsetP2M() + end + return true + end + + function TOOL:Think() + if not IsValid(self.p2m.ent) then + return + end + if self.p2m.ent:GetClass() ~= (self:IsLegacyMode() and "sent_prop2mesh_legacy" or "sent_prop2mesh") then + for k, v in pairs(self.selection) do + self:DeselectEntity(k) + end + self.selection = {} + self:UnsetP2M() + end + end + + local multitool = { modes = {} } + multitool.modes.material = function(ply, tr, index) + if ply:KeyDown(IN_ATTACK2) then + local mat = tr.Entity:GetControllerMat(index - 1) + if mat and not string.find(mat, ";") then + ply:ConCommand("material_override " .. mat) + end + return + end + local mat + if ply:KeyDown(IN_RELOAD) then + mat = prop2mesh.defaultmat + else + mat = ply:GetInfo("material_override") + end + tr.Entity:SetControllerMat(index - 1, mat) + end + multitool.modes.colour = function(ply, tr, index) + if ply:KeyDown(IN_ATTACK2) then + local col = tr.Entity:GetControllerCol(index - 1) + if col then + ply:ConCommand("colour_r " .. col.r) + ply:ConCommand("colour_g " .. col.g) + ply:ConCommand("colour_b " .. col.b) + ply:ConCommand("colour_a " .. col.a) + end + return + end + local col + if ply:KeyDown(IN_RELOAD) then + col = Color(255, 255, 255, 255) + else + col = Color(ply:GetInfoNum("colour_r", 255), ply:GetInfoNum("colour_g", 255), ply:GetInfoNum("colour_b", 255), ply:GetInfoNum("colour_a", 255)) + end + tr.Entity:SetControllerCol(index - 1, col) + end + multitool.modes.remover = function(ply, tr, index) + if ply:KeyDown(IN_ATTACK) then + tr.Entity:RemoveController(index - 1) + end + end + multitool.modes.colmat = function(ply, tr, index) + if ply:KeyDown(IN_ATTACK2) then + local col = tr.Entity:GetControllerCol(index - 1) + if col then + ply:ConCommand("colmat_r " .. col.r) + ply:ConCommand("colmat_g " .. col.g) + ply:ConCommand("colmat_b " .. col.b) + ply:ConCommand("colmat_a " .. col.a) + end + local mat = tr.Entity:GetControllerMat(index - 1) + if mat and not string.find(mat, ";") then + ply:ConCommand("colmat_material " .. mat) + end + return + end + local col, mat + if ply:KeyDown(IN_RELOAD) then + col = Color(255, 255, 255, 255) + mat = "hunter/myplastic" + else + col = Color(ply:GetInfoNum("colmat_r", 255), ply:GetInfoNum("colmat_g", 255), ply:GetInfoNum("colmat_b", 255), ply:GetInfoNum("colmat_a", 255)) + mat = ply:GetInfo("colmat_material") + end + tr.Entity:SetControllerCol(index - 1, col) + tr.Entity:SetControllerMat(index - 1, mat) + end + + hook.Add("CanTool", "prop2mesh_multitool", function(ply, tr, tool) + if not multitool.modes[tool] or not IsValid(tr.Entity) or tr.Entity:GetClass() ~= "sent_prop2mesh" then + return + end + local index = ply:GetInfoNum("prop2mesh_multitool_index", 1) + if index ~= 1 then + multitool.modes[tool](ply, tr, index) + if game.SinglePlayer() then + ply:GetActiveWeapon():CallOnClient("PrimaryAttack") + end + return false + end + end) + + return +end + +function TOOL:RightClick(tr) + return tr.Hit +end +function TOOL:LeftClick(tr) + return tr.Hit +end +function TOOL:Reload(tr) + return tr.Hit +end + + +--[[ + +]] +language.Add("tool.prop2mesh.name", "Prop2Mesh") +language.Add("tool.prop2mesh.desc", "Convert groups of props into a single mesh") + +TOOL.Information = { + { name = "right0", stage = 0 }, + { name = "right1", stage = 1 }, + { name = "right2", stage = 1, icon2 = "gui/key.png" }, + { name = "right3", stage = 1, icon2 = "gui/e.png" }, +} + +language.Add("tool.prop2mesh.right0", "Select a p2m entity") +language.Add("tool.prop2mesh.right1", "Select the entities you wish to convert") +language.Add("tool.prop2mesh.right2", "Select [SHIFT] all entities within radius of your aim position") +language.Add("tool.prop2mesh.right3", "Select [E] all entities parented to your aim entity") + +local ConVars = { + ["tool_legacymode"] = 0, + ["tool_setmodel"] = "models/p2m/cube.mdl", + ["tool_setautocenter"] = 0, + ["tool_setautoremove"] = 0, + ["tool_setuvsize"] = 0, + ["tool_filter_radius"] = 512, + ["tool_filter_mcolor"] = 0, + ["tool_filter_mmatrl"] = 0, + ["tool_filter_mmass"] = 0, + ["tool_filter_iinvs"] = 1, + ["tool_filter_iprnt"] = 0, + ["tool_filter_icnst"] = 0, + ["tool_filter_iprop"] = 0, + ["tool_filter_iholo"] = 0, + ["tool_filter_ilp2m"] = 1, + ["tool_config_halos"] = 1, +} +TOOL.ClientConVar = ConVars + + +--[[ + +]] +local help_font = "DebugFixedSmall" +local function BuildPanel_ToolSettings(self) + local pnl = vgui.Create("DForm") + pnl:SetName("Tool Settings") + + -- + local help = pnl:Help("Danger zone") + help:DockMargin(0, 0, 0, 0) + help:SetFont(help_font) + help.Paint = function(_, w, h) + surface.SetDrawColor(0, 0, 0, 255) + surface.DrawLine(0, h - 1, w, h - 1) + end + + local cbox = pnl:CheckBox("Remove selected props when done", "prop2mesh_tool_setautoremove") + cbox.OnChange = function(_, value) + cbox.Label:SetTextColor(value and Color(255, 0, 0) or nil) + end + + local cbox = pnl:CheckBox("Enable legacy mode", "prop2mesh_tool_legacymode") + cbox.OnChange = function(_, value) + cbox.Label:SetTextColor(value and Color(255, 0, 0) or nil) + end + + local cbox = pnl:CheckBox("Disable rendering", "prop2mesh_render_disable") + cbox.OnChange = function(_, value) + cbox.Label:SetTextColor(value and Color(255, 0, 0) or nil) + end + + -- + local help = pnl:Help("Selection filters") + help:DockMargin(0, 0, 0, 0) + help:SetFont(help_font) + help.Paint = function(_, w, h) + surface.SetDrawColor(0, 0, 0, 255) + surface.DrawLine(0, h - 1, w, h - 1) + end + + local sld = pnl:NumSlider("Selection radius", "prop2mesh_tool_filter_radius", 0, 2048, 0) + sld.Scratch:SetDisabled(true) + pnl:ControlHelp("Hold LSHIFT while selecting to apply below filters to all entities within this radius.") + + pnl:CheckBox("Only select entities with same color", "prop2mesh_tool_filter_mcolor") + pnl:CheckBox("Only select entities with same material", "prop2mesh_tool_filter_mmatrl") + pnl:CheckBox("Ignore all invisible entities", "prop2mesh_tool_filter_iinvs") + pnl:CheckBox("Ignore all parented entities", "prop2mesh_tool_filter_iprnt") + pnl:CheckBox("Ignore all constrained entities", "prop2mesh_tool_filter_icnst") + + local sld = pnl:NumSlider("Ignore by mass", "prop2mesh_tool_filter_mmass", 0, 50000, 0) + pnl:ControlHelp("Ignore entities with mass above this value.") + + -- + local help = pnl:Help("Class filters") + help:DockMargin(0, 0, 0, 0) + help:SetFont(help_font) + help.Paint = function(_, w, h) + surface.SetDrawColor(0, 0, 0, 255) + surface.DrawLine(0, h - 1, w, h - 1) + end + + pnl:CheckBox("Ignore props", "prop2mesh_tool_filter_iprop") + pnl:CheckBox("Ignore holos", "prop2mesh_tool_filter_iholo") + pnl:CheckBox("Ignore legacy p2m", "prop2mesh_tool_filter_ilp2m") + + -- + local help = pnl:Help("Entity options") + help:DockMargin(0, 0, 0, 0) + help:SetFont(help_font) + help.Paint = function(_, w, h) + surface.SetDrawColor(0, 0, 0, 255) + surface.DrawLine(0, h - 1, w, h - 1) + end + + local txt, lbl = pnl:TextEntry("Entity model:", "prop2mesh_tool_setmodel") + + local sld = pnl:NumSlider("Texture Size", "prop2mesh_tool_setuvsize", 0, 512, 0) + pnl:ControlHelp("Tile uvs (1 / n) or set to 0 to use model uvs.") + + local cbx = pnl:CheckBox("Autocenter data:", "prop2mesh_tool_setautocenter") + pnl:ControlHelp("Created mesh will be centered around average position of selection.") + + local help = pnl:Help("You can further modify p2m entities via the context menu.") + help:SetFont(help_font) + + return pnl +end + +local function BuildPanel_Profiler(self) + local pnl = vgui.Create("DForm") + pnl:SetName("Profiler") + pnl:DockPadding(0, 0, 0, 10) + + local tree = vgui.Create("DTree", pnl) + tree:SetTall(256) + tree:Dock(FILL) + pnl:AddItem(tree) + + pnl.Header.OnCursorEntered = function() + tree:Clear() + + local struct = {} + for k, v in ipairs(ents.FindByClass("sent_prop2mesh*")) do + local root = CPPI and v:CPPIGetOwner():Nick() or k + + if not struct[root] then + local sdata = { + root = tree:AddNode(root, "icon16/user.png"), + num_mdls = 0, + num_tris = 0, + num_ctrl = 0, + num_ents = 0, + } + + sdata.node_mdls = sdata.root:AddNode("", "icon16/bullet_black.png") + sdata.node_tris = sdata.root:AddNode("", "icon16/bullet_black.png") + sdata.node_ctrl = sdata.root:AddNode("", "icon16/bullet_black.png") + sdata.node_ents = sdata.root:AddNode("", "icon16/bullet_black.png") + sdata.root:SetExpanded(true, true) + + struct[root] = sdata + end + + local sdata = struct[root] + + sdata.num_ctrl = sdata.num_ctrl + #v.prop2mesh_controllers + sdata.num_ents = sdata.num_ents + 1 + + sdata.node_ctrl:SetText(string.format("%d total controllers", sdata.num_ctrl)) + sdata.node_ents:SetText(string.format("%d total entities", sdata.num_ents)) + + for i, info in ipairs(v.prop2mesh_controllers) do + local pcount, vcount = prop2mesh.getMeshInfo(info.crc, info.uvs) + if pcount and vcount then + sdata.num_mdls = sdata.num_mdls + pcount + sdata.num_tris = sdata.num_tris + vcount / 3 + end + end + + sdata.node_mdls:SetText(string.format("%d total models", sdata.num_mdls)) + sdata.node_tris:SetText(string.format("%d total triangles", sdata.num_tris)) + end + end + + return pnl +end + +TOOL.BuildCPanel = function(self) + local btn = self:Button("Reset all tool options") + btn.DoClick = function() + for var, _ in pairs(ConVars) do + local convar = GetConVar("prop2mesh_" .. var) + if convar then + convar:Revert() + end + end + end + self:AddPanel(BuildPanel_ToolSettings(self)) + self:AddPanel(BuildPanel_Profiler(self)) +end + + +--[[ + +]] +local multitool = { modes = {}, lines = {} } + +multitool.lines_font = "multitool_16" +multitool.title_font = "multitool_32" + +surface.CreateFont(multitool.lines_font, { font = "Consolas", size = 16, weight = 200, shadow = false }) +surface.CreateFont(multitool.title_font, { font = "Consolas", size = 32, weight = 200, shadow = false }) + +multitool.lines_color_text1 = Color(255, 255, 255, 255) +multitool.lines_color_text2 = Color(0, 0, 0, 255) +multitool.lines_color_bg1 = Color(25, 25, 25, 200) +multitool.lines_color_bg2 = Color(75, 75, 75, 200) +multitool.lines_color_bg3 = Color(0, 255, 0, 255) +multitool.title_color_text = Color(255, 255, 255, 255) +multitool.title_color_bg1 = Color(50, 50, 50, 255) + +multitool.lines_display_lim = 10 + +if p2m_dmodelpanel then + p2m_dmodelpanel:Remove() + p2m_dmodelpanel = nil +end +timer.Simple(1, function() + if not p2m_dmodelpanel then + p2m_dmodelpanel = vgui.Create("DModelPanel") + p2m_dmodelpanel:SetSize(200,200) + p2m_dmodelpanel:SetModel("models/hunter/blocks/cube025x025x025.mdl") + + local pos = p2m_dmodelpanel.Entity:GetPos() + p2m_dmodelpanel:SetLookAt(pos) + p2m_dmodelpanel:SetCamPos(pos - Vector(-25, 0, 0)) + p2m_dmodelpanel:SetVisible(false) + end +end) + +local function GetMaxLineSize(tbl, font) + if font then + surface.SetFont(font) + end + local text_w, text_h = 0, 0 + for i = 1, #tbl do + local tw, th = surface.GetTextSize(tbl[i]) + if text_w < tw then text_w = tw end + if text_h < th then text_h = th end + end + return text_w, text_h +end + +local index = CreateClientConVar("prop2mesh_multitool_index", 1, false, true) +function multitool:GetIndex() + return index:GetInt() +end +function multitool:SetIndex(int) + index:SetInt(int) +end + + +--[[ + +]] +function multitool:PlayerBindPress(ply, bind, pressed) + if self.sleep or not pressed or not IsValid(self.entity) then + return + end + + local add + if bind == "invnext" then add = 1 end + if bind == "invprev" then add = -1 end + if not add then + return + end + + self:onTrigger() + + local value = self:GetIndex() + local vdiff = math.Clamp(value + add, 1, self.lines_c) + + if value ~= vdiff then + LocalPlayer():EmitSound("weapons/pistol/pistol_empty.wav") + self:SetIndex(vdiff) + self:onTrigger() + end + + return true +end + + +--[[ + +]] +function multitool:Think() + local ply = LocalPlayer() + local weapon = ply:GetActiveWeapon() + if IsValid(weapon) then + self.sleep = weapon:GetClass() ~= "gmod_tool" + else + self.sleep = true + end + if self.sleep then + if p2m_dmodelpanel and p2m_dmodelpanel:IsVisible() then + p2m_dmodelpanel:SetVisible(false) + end + return + end + self.trace = ply:GetEyeTrace() + if not IsValid(self.entity) then + if IsValid(self.trace.Entity) and self.trace.Entity:GetClass() == "sent_prop2mesh" then + self.entity = self.trace.Entity + self:onTrigger() + end + else + if self.trace.Entity ~= self.entity then + if p2m_dmodelpanel then + p2m_dmodelpanel:SetVisible(false) + end + self.entity = nil + else + local stage = weapon:GetStage() + if self.stage ~= stage then + self.entity.prop2mesh_triggertool = true + self.stage = stage + end + if not self.shift then + if LocalPlayer():KeyDown(IN_SPEED) then + self.shift = true + self:onTrigger() + end + end + if self.shift then + if not LocalPlayer():KeyDown(IN_SPEED) then + self.shift = nil + self:onTrigger() + end + end + if self.entity.prop2mesh_triggertool then + self.entity.prop2mesh_triggertool = nil + self:onTrigger() + else + end + end + end +end + + +--[[ + +]] +function multitool:HUDPaint() + if self.sleep or not IsValid(self.entity) then + return + end + + local _w = ScrW() + local _h = ScrH() + local _x = _w*0.5 + local _y = _h*0.5 + + local mode = self.modes[self.active] + if mode.hud then + mode:hud(_x, _y, _w, _h) + end + + local px = _x - self.lines_w*1.5 + local py = _y - self.lines_t*0.5 + + if px < 0 then px = self.lines_h end + if py < 0 then py = self.lines_h end + + if self.scroll then + surface.SetDrawColor(0, 0, 0, 225) + surface.DrawRect(px + self.lines_w, py, 8, self.scroll_a) + surface.SetDrawColor(255, 255, 255, 225) + surface.DrawRect(px + self.lines_w + 1, py + self.scroll_y + 1, 6, self.scroll_h - 2) + end + + -- header + surface.SetDrawColor(self.title_color_bg1) + surface.DrawRect(px, py - self.title_h, self.lines_w + (self.scroll and 8 or 0), self.title_h) + surface.SetFont(self.title_font) + surface.SetTextColor(self.title_color_text) + surface.SetTextPos(px + 4, py - self.title_h) + surface.DrawText(self.title) + + -- body + surface.SetFont(self.lines_font) + for i = self.lines_display_min, self.lines_display_max do + local ypos = py + self.lines_h*(i - self.lines_display_min) + if i == self.lines_display_sel then + surface.SetTextColor(mode.lines_color_htext or self.lines_color_text2) + surface.SetDrawColor(mode.lines_color_hbg or self.lines_color_bg3) + surface.DrawRect(px + 2, ypos + 2, self.lines_w - 4, self.lines_h - 4) + + if self.model and p2m_dmodelpanel then + p2m_dmodelpanel:SetPos(px + self.lines_w, ypos - self.model*0.5) + self.model = nil + end + else + surface.SetTextColor(self.lines_color_text1) + surface.SetDrawColor(i % 2 == 0 and self.lines_color_bg1 or self.lines_color_bg2) + surface.DrawRect(px, ypos, self.lines_w, self.lines_h) + end + surface.SetTextPos(px + 4, ypos + 4) + surface.DrawText(self.lines[i]) + end +end + + +--[[ + +]] +function multitool:onTrigger() + if not IsValid(self.entity) then + return + end + self.stage = LocalPlayer():GetActiveWeapon():GetStage() + + local mode = self.modes[self.active] + + self.title = mode.title_text + self.title_w = mode.title_w + self.title_h = mode.title_h + + mode:getLines() + + self.lines_c = #self.lines + + if self:GetIndex() > self.lines_c then + self:SetIndex(self.lines_c) + end + + self.lines_display_sel = self:GetIndex() + self.lines_display_num = math.min(self.lines_display_lim - 1, self.lines_c) + self.lines_display_min = math.max(self.lines_display_sel - self.lines_display_num, 1) + self.lines_display_max = math.min(self.lines_display_min + self.lines_display_num, self.lines_c) + + self.lines_w, self.lines_h = GetMaxLineSize(self.lines, self.lines_font) + self.lines_w = math.max(self.lines_w, self.title_w) + 8 + self.lines_h = self.lines_h + 8 + self.lines_t = self.lines_h*self.lines_display_num + + if self.lines_display_num < self.lines_c - 1 then + local all = self.lines_c - 1 + local vis = self.lines_display_num + local pos = (self.lines_display_sel - 1) / all + + self.scroll_a = self.lines_t + self.lines_h + self.scroll_h = (vis / all)*self.scroll_a + self.scroll_y = pos*(self.scroll_a - self.scroll_h) + self.scroll = true + else + self.scroll = nil + end + + if p2m_dmodelpanel then + --p2m_dmodelpanel:SetVisible(false) + local tbl = self.entity.prop2mesh_controllers[self.lines_display_sel - 1] + if tbl then + self.model = self.lines_h*self.lines_display_lim*0.5 + p2m_dmodelpanel:SetSize(self.model, self.model) + p2m_dmodelpanel:SetColor(tbl.col) + p2m_dmodelpanel.Entity:SetMaterial(tbl.mat) + p2m_dmodelpanel:SetVisible(true) + else + p2m_dmodelpanel:SetVisible(false) + self.model = nil + end + end +end + + +--[[ + +]] +function multitool:PreDrawHalos() + if self.sleep or not IsValid(self.entity) then + return + end + local info = self.entity.prop2mesh_controllers[self:GetIndex() - 1] + if info then + halo.Add({ info.ent }, color_white, 2, 2, 5, true) + end +end + +-- +local function SetToolMode(convar_name, value_old, value_new) + if not multitool.modes[value_new] then + if multitool.active then + for k, v in ipairs({"PreDrawHalos", "PostDrawHUD", "Think", "PlayerBindPress"}) do + hook.Remove(v, "prop2mesh_multitool") + end + multitool:SetIndex(1) + multitool.entity = nil + multitool.active = nil + if p2m_dmodelpanel then + p2m_dmodelpanel:SetVisible(false) + end + end + return + end + if not multitool.active then + hook.Add("PreDrawHalos", "prop2mesh_multitool", function() + multitool:PreDrawHalos() + end) + hook.Add("PostDrawHUD", "prop2mesh_multitool", function() + multitool:HUDPaint() + end) + hook.Add("Think", "prop2mesh_multitool", function() + multitool:Think() + end) + hook.Add("PlayerBindPress", "prop2mesh_multitool", function(ply, bind, pressed) + if multitool:PlayerBindPress(ply, bind, pressed) then + return true + end + end) + multitool:SetIndex(1) + multitool.entity = nil + end + multitool.active = value_new + multitool:onTrigger() +end +cvars.AddChangeCallback("gmod_toolmode", SetToolMode)--, "prop2mesh_multitoolmode") + +for k, v in ipairs({"PreDrawHalos", "PostDrawHUD", "Think", "PlayerBindPress"}) do + hook.Remove(v, "prop2mesh_multitool") +end + +hook.Add("Think", "prop2mesh_fixmultitoolmode", function() + local tool = LocalPlayer():GetTool() + if tool then + if tool.Mode and tool.Mode ~= multitool.active then + SetToolMode(nil, nil, tool.Mode) + end + hook.Remove("Think", "prop2mesh_fixmultitoolmode") + end +end) + + +--[[ + +]] +local mode = {} +multitool.modes.prop2mesh = mode + +mode.title_text = "PROP2MESH TOOL" + +surface.SetFont(multitool.title_font) +mode.title_w, mode.title_h = surface.GetTextSize(mode.title_text) + +local lang_tmp0 = "[Right] click to select this p2m entity" +local lang_tmp1 = "[Left] click to automatically add controllers" +local lang_tmp2 = "[Left] click to add controller" +local lang_tmp3 = "[Right] click to use tool on controller [%d]" + +function mode:getLines() + if multitool.stage == 0 then + multitool.lines = { lang_tmp0 } + return + end + if multitool.stage == 1 then + multitool.lines = { multitool.shift and lang_tmp1 or lang_tmp2 } + for i = 1, #multitool.entity.prop2mesh_controllers do + multitool.lines[#multitool.lines + 1] = string.format(lang_tmp3, i) + end + end +end + +local _gray = Color(125, 125, 125) +local _red = Color(255, 0, 0) +local _green = Color(0, 255, 0) +local _blue = Color(0, 0, 255) + +function mode:hud() + cam.Start3D() + local pos = multitool.entity:GetPos() + local min, max = multitool.entity:GetModelBounds() + render.DrawWireframeBox(pos, multitool.entity:GetAngles(), min, max, _gray) + render.DrawLine(pos, pos + multitool.entity:GetForward()*3, _green) + render.DrawLine(pos, pos + multitool.entity:GetRight()*3, _red) + render.DrawLine(pos, pos + multitool.entity:GetUp()*3, _blue) + cam.End3D() +end + + +--[[ + +]] +local mode = {} +multitool.modes.remover = mode + +mode.title_text = "REMOVER TOOL" + +surface.SetFont(multitool.title_font) +mode.title_w, mode.title_h = surface.GetTextSize(mode.title_text) + +mode.lines_color_hbg = Color(255, 0, 0, 255) + +local lang_tmp1 = "Remove entity" +local lang_tmp2 = "Remove controller [%d]" + +function mode:getLines() + multitool.lines = { lang_tmp1 } + for i = 1, #multitool.entity.prop2mesh_controllers do + multitool.lines[#multitool.lines + 1] = string.format(lang_tmp2, i) + end +end + + +--[[ + +]] +local mode = {} +multitool.modes.material = mode + +mode.title_text = "MATERIAL TOOL" + +surface.SetFont(multitool.title_font) +mode.title_w, mode.title_h = surface.GetTextSize(mode.title_text) + +local lang_tmp1 = "entity [%s]" +local lang_tmp2 = "controller [%d] [%s]" + +function mode:getLines() + multitool.lines = { string.format(lang_tmp1, multitool.entity:GetMaterial()) } + for i = 1, #multitool.entity.prop2mesh_controllers do + multitool.lines[#multitool.lines + 1] = string.format(lang_tmp2, i, multitool.entity.prop2mesh_controllers[i].mat) + end +end + + +--[[ + +]] +local mode = {} +multitool.modes.colour = mode + +mode.title_text = "COLOR TOOL" + +surface.SetFont(multitool.title_font) +mode.title_w, mode.title_h = surface.GetTextSize(mode.title_text) + +local lang_tmp1 = "entity [%d %d %d %d]" +local lang_tmp2 = "controller [%d] [%d %d %d %d]" + +function mode:getLines() + local col = multitool.entity:GetColor() + multitool.lines = { string.format(lang_tmp1, col.r, col.b, col.g, col.a) } + for i = 1, #multitool.entity.prop2mesh_controllers do + col = multitool.entity.prop2mesh_controllers[i].col + multitool.lines[#multitool.lines + 1] = string.format(lang_tmp2, i, col.r, col.b, col.g, col.a) + end +end + + +--[[ + +]] +local mode = {} +multitool.modes.colmat = mode + +mode.title_text = "COLORMATER TOOL" + +surface.SetFont(multitool.title_font) +mode.title_w, mode.title_h = surface.GetTextSize(mode.title_text) + +local lang_tmp1 = "entity [%d %d %d %d] [%s]" +local lang_tmp2 = "controller [%d] [%d %d %d %d] [%s]" + +function mode:getLines() + local col = multitool.entity:GetColor() + multitool.lines = { string.format(lang_tmp1, col.r, col.b, col.g, col.a, multitool.entity:GetMaterial()) } + for i = 1, #multitool.entity.prop2mesh_controllers do + col = multitool.entity.prop2mesh_controllers[i].col + multitool.lines[#multitool.lines + 1] = string.format(lang_tmp2, i, col.r, col.b, col.g, col.a, multitool.entity.prop2mesh_controllers[i].mat) + end +end diff --git a/materials/p2m/cube.vmt b/materials/p2m/cube.vmt new file mode 100644 index 0000000..25b6c4a --- /dev/null +++ b/materials/p2m/cube.vmt @@ -0,0 +1,7 @@ +"UnlitGeneric" +{ + "$baseTexture" "p2m/cube" + "$selfillum" "1" + "$reflectivity" "[0 0 0]" + "$nodecal" "1" +} diff --git a/materials/p2m/cube.vtf b/materials/p2m/cube.vtf new file mode 100644 index 0000000000000000000000000000000000000000..dfa26cbc15dbfe3e6d2cfaf2cf57b05b2f032d4b GIT binary patch literal 349784 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 IcZ~RZCO!*P| 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 literal 0 HcmV?d00001 diff --git a/models/p2m/cube.dx80.vtx b/models/p2m/cube.dx80.vtx new file mode 100644 index 0000000000000000000000000000000000000000..23ce94917d9d22d59a5fd24552cb0379cee25171 GIT binary patch literal 429 zcmZ9I$rZvt3`3QW5VDXx0q&H*jXNz+AwAIzNElt>_bmTe>>0ZNaHjVtv15Eb-tUC* zMNz!c6%U)(a4pkWDNvCcT?x%U`9e;$A=8C0<=`$&QtW}xM?eeUQvrp655x|00%}8L z;D_Rfl>$1$(a;;%Y1|nNtzj@^hQ%-$W6TFcldt5|Hq7jTo8{nXnxr@a+h#y>;8PBTfDiZ%a~vu` zDd30vh?N{VK_ln|?9}g!f>tmHGQlF41hXI)oCWa*9E&xSa-Y5S>J$qA5&gZ(|`EY%B~(1Q7x$1O*#Gl3awq9XZUU2t>GAu(ODrjh6ld zNnz<1BACJ-U}<3~ob$3fBo}g*Xz9T0%)Gq$xZS-=ZfkchNkseGosPX`t(NM=@``1c z4Egm&6cBKMYU7D{xP3Cc+D4-wW@R59!c6^nIs{KZxT{EwL8E%7Xfq08ET zo>WZKfFKsk;XKGVHqG^teb0)V*|)_MdOq~ue=ZmesD!gIM6l*NZBNMWuHdS>7(u6f?RqwDboyhnYX z;~ICrJ@2K9eQp98rv`X7mOqHcIdSf3aL<&U1=}p8UfBtHS20^h6X8SUh&1do5BK>r z)CYzH=nWM?uXhPXT6K13Je15f&pF-iS*J6zKiO&KY*xV zlQ%A?*04-)!!$qrbTTYYu%1h4Gp!hr{%d^EUaF)LQ;l^`Zx~w(GB}9iqkN~ zjtSV5F-k)jq4*&o_bLfYA(gsn1Z;w*DS^D=a6XAEgNkRF*kJx56&i&0E|Ue=)4hx% zn?OD_PrxR)LMiH}?i@QO5jUXKWlxs*qfBC_6#CTxu4n%Tc-Cbv+!FL8Q`m|9FZz$p zA=NHhNZ$y37CQ3~qdNMlbNxx?`s`chI_5upBHs3k>UnN>f{)Kn4){MX-r?H^qXBt# z+0?Ups%Bccv8!dZoTh4)s2B)K)lCDeB0LW)Kb8v*6tC)TiIU;Cwu6LjY>8^whpvl? zV_Mtn(kXUr|6kp-4A(csZ|5DmTn%TBPqZEORYNy@N4TQm@useA`_YD&>D#_2M=N4# To_B;?vaOQX;kU?Z>b^=}>FKGZ literal 0 HcmV?d00001 diff --git a/models/p2m/cube.sw.vtx b/models/p2m/cube.sw.vtx new file mode 100644 index 0000000000000000000000000000000000000000..58b5504a2485d3f91e010dbdf26c487315124013 GIT binary patch literal 429 zcmZ9INfv@Y3`7eAk!GG+-FpOg?!AI1^iJFcE5zDR@1^p4NDnDMr4}VlOrQ6ea|Rp1%n_HEP_ce3v$6#5O-i(_MWn(8FKzHV-##+i9ipiP@_Rh VZ_%O0fRR38!i)ux9>FsY`T_m}H!uJI literal 0 HcmV?d00001 diff --git a/models/p2m/cube.vvd b/models/p2m/cube.vvd new file mode 100644 index 0000000000000000000000000000000000000000..0048aca34007ce80a1c3d50c5d7de63012000aa0 GIT binary patch literal 1600 zcmai!!EM7Z3`J$2Lu85c(qn+yhoV5w1(I>$Z&(_V?{)IsTrm*{0lk